up
This commit is contained in:
@@ -66,11 +66,6 @@ const routes = [
|
||||
component: AdminSettings,
|
||||
meta: { requiresAuth: true, requiresAdmin: true, title: 'Settings' }
|
||||
},
|
||||
// {
|
||||
// path: '/olap-constructor',
|
||||
// component: OLAPConstructor,
|
||||
// meta: { requiresAuth: true, title: 'OLAP Constructor' }
|
||||
// },
|
||||
{
|
||||
path: '/olap/queries',
|
||||
component: OlapQueriesPage,
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
<!-- Таблица SQL -->
|
||||
<div class="flex flex-col">
|
||||
<label class="text-xs text-gray-500">Таблица SQL</label>
|
||||
<label class="text-xs text-gray-500">Таблица SQL *</label>
|
||||
<input type="text" v-model="tableName" @input="validateTableName" class="input-field py-1 text-sm"
|
||||
:class="{ 'border-green-500 bg-green-50': tableNameValid && tableName, 'border-red-500 bg-red-50': tableNameTouched && !tableNameValid && tableName }" />
|
||||
</div>
|
||||
@@ -581,7 +581,7 @@ const daysBack = ref(7)
|
||||
const tableName = ref('')
|
||||
const tableNameValid = ref(true)
|
||||
const tableNameTouched = ref(false)
|
||||
const active = ref(true)
|
||||
const active = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const activeTab = ref<'table' | 'sql'>('table')
|
||||
const collapsed = ref({ number: false, category: false, filter: false })
|
||||
@@ -874,6 +874,8 @@ async function loadQuery(id: number) {
|
||||
// 1. Базовые поля запроса
|
||||
queryName.value = data.name
|
||||
|
||||
active.value = data.active ?? true;
|
||||
|
||||
// 2. Подключение к БД
|
||||
if (data.dbConnectionId) {
|
||||
await loadDbConnections()
|
||||
@@ -974,13 +976,18 @@ async function saveQuery() {
|
||||
showNotification('Выберите хотя бы один ресторан', 'error')
|
||||
return
|
||||
}
|
||||
if (!tableName.value.trim()) {
|
||||
showNotification('Укажите название таблицы SQL', 'error');
|
||||
return;
|
||||
}
|
||||
const config = buildConfigObject()
|
||||
const payload = {
|
||||
name: queryName.value,
|
||||
dbConnectionId: selectedDbConnection.value.id,
|
||||
config: config,
|
||||
restaurantIds: selectedRestaurants.value.map(r => r.id)
|
||||
}
|
||||
restaurantIds: selectedRestaurants.value.map(r => r.id),
|
||||
active: active.value
|
||||
};
|
||||
const url = queryId.value ? `/api/olap/queries/${queryId.value}` : '/api/olap/queries'
|
||||
const method = queryId.value ? 'PUT' : 'POST'
|
||||
try {
|
||||
@@ -1011,9 +1018,17 @@ const filteredCategoryFields = computed(() => availableFields.value.filter((f):
|
||||
const filteredFilterFields = computed(() => availableFields.value.filter((f): f is FilterField => f.role === 'filter' && matchesSearch(f)))
|
||||
|
||||
const validateTableName = () => {
|
||||
tableNameTouched.value = true
|
||||
if (!tableName.value) { tableNameValid.value = true; return }
|
||||
tableNameValid.value = /^[A-Za-zА-Яа-я]/.test(tableName.value)
|
||||
tableNameTouched.value = true;
|
||||
const value = tableName.value.trim();
|
||||
if (!value) {
|
||||
tableNameValid.value = false;
|
||||
return;
|
||||
}
|
||||
// Регулярное выражение:
|
||||
// ^[A-Za-z] - первая буква (только английская)
|
||||
// [A-Za-z0-9]* - далее любые английские буквы или цифры
|
||||
const regex = /^[A-Za-z][A-Za-z0-9]*$/;
|
||||
tableNameValid.value = regex.test(value);
|
||||
}
|
||||
|
||||
const parseValues = (f: FilterField) => {
|
||||
@@ -1184,7 +1199,7 @@ const confirmReset = () => {
|
||||
refreshFieldsAndReset()
|
||||
dateTo.value = ''
|
||||
daysBack.value = 7
|
||||
active.value = true
|
||||
active.value = false
|
||||
tableName.value = ''
|
||||
tableNameValid.value = true
|
||||
tableNameTouched.value = false
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500">ID</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500">Название</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500">Активен</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500">Последнее выполнение</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500">Результат</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500">Подключение</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500">Рестораны</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500">Создан</th>
|
||||
@@ -22,6 +25,17 @@
|
||||
<tr v-for="q in queries" :key="q.id" class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 text-sm">{{ q.id }}</td>
|
||||
<td class="px-6 py-4 text-sm font-medium">{{ q.name }}</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<span :class="q.active ? 'text-green-600' : 'text-red-600'">
|
||||
{{ q.active ? 'Да' : 'Нет' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">{{ q.lastRun ? formatDate(q.lastRun) : '—' }}</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<span v-if="q.lastRunSuccess === null">—</span>
|
||||
<span v-else-if="q.lastRunSuccess" class="text-green-600">Успешно</span>
|
||||
<span v-else class="text-red-600">Ошибка</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">{{ q.dbConnectionName }}</td>
|
||||
<td class="px-6 py-4 text-sm">{{ q.restaurants }}</td>
|
||||
<td class="px-6 py-4 text-sm">{{ formatDate(q.created) }}</td>
|
||||
|
||||
@@ -552,14 +552,31 @@ public class MainVerticle extends AbstractVerticle {
|
||||
JsonArray restaurantIdsArray = body.getJsonArray("restaurantIds", new JsonArray());
|
||||
List<Integer> restaurantIds = restaurantIdsArray.stream().map(id -> (Integer) id).collect(Collectors.toList());
|
||||
|
||||
// Получаем active: сначала из тела, иначе из конфига, иначе true
|
||||
Boolean active = body.getBoolean("active");
|
||||
if (active == null && config != null) active = config.getBoolean("active", true);
|
||||
if (active == null) active = true;
|
||||
|
||||
if (name == null || dbConnectionId == null || config == null) {
|
||||
rc.response().setStatusCode(400).end("Missing required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
// Сначала генерируем SQL
|
||||
String tableName = config.getString("tableName");
|
||||
|
||||
if (tableName.isEmpty()) {
|
||||
rc.response().setStatusCode(400).end("Missing required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!olapQueryService.isValidTableName(tableName)) {
|
||||
rc.response().setStatusCode(400).end("Invalid tableName: must start with a letter and contain only letters and digits");
|
||||
return;
|
||||
}
|
||||
|
||||
Boolean finalActive = active;
|
||||
olapQueryService.generateSql(config, dbConnectionId)
|
||||
.compose(sql -> olapQueryService.createQuery(name, dbConnectionId, config, restaurantIds, sql))
|
||||
.compose(sql -> olapQueryService.createQuery(name, dbConnectionId, config, restaurantIds, sql, finalActive))
|
||||
.onSuccess(id -> rc.response().setStatusCode(201).putHeader("Content-Type", "application/json").end(new JsonObject().put("id", id).encode()))
|
||||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||||
});
|
||||
@@ -573,13 +590,30 @@ public class MainVerticle extends AbstractVerticle {
|
||||
JsonArray restaurantIdsArray = body.getJsonArray("restaurantIds", new JsonArray());
|
||||
List<Integer> restaurantIds = restaurantIdsArray.stream().map(v -> (Integer) v).collect(Collectors.toList());
|
||||
|
||||
Boolean active = body.getBoolean("active");
|
||||
if (active == null && config != null) active = config.getBoolean("active", true);
|
||||
if (active == null) active = true;
|
||||
|
||||
if (name == null || dbConnectionId == null || config == null) {
|
||||
rc.response().setStatusCode(400).end("Missing required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
String tableName = config.getString("tableName");
|
||||
|
||||
if (tableName.isEmpty()) {
|
||||
rc.response().setStatusCode(400).end("Missing required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!olapQueryService.isValidTableName(tableName)) {
|
||||
rc.response().setStatusCode(400).end("Invalid tableName: must start with a letter and contain only letters and digits");
|
||||
return;
|
||||
}
|
||||
|
||||
Boolean finalActive = active;
|
||||
olapQueryService.generateSql(config, dbConnectionId)
|
||||
.compose(sql -> olapQueryService.updateQuery(id, name, dbConnectionId, config, restaurantIds, sql))
|
||||
.compose(sql -> olapQueryService.updateQuery(id, name, dbConnectionId, config, restaurantIds, sql, finalActive))
|
||||
.onSuccess(v -> rc.response().end())
|
||||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import io.vertx.sqlclient.Row;
|
||||
import io.vertx.sqlclient.Tuple;
|
||||
import io.vertx.sqlclient.templates.SqlTemplate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -33,6 +34,9 @@ public class OlapQueryService {
|
||||
config_json JSON NOT NULL,
|
||||
full_config_json JSON NOT NULL,
|
||||
sql_text TEXT,
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
last_run TIMESTAMP NULL,
|
||||
last_run_success BOOLEAN NULL,
|
||||
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (db_connection_id) REFERENCES external_database(id) ON DELETE RESTRICT
|
||||
@@ -56,16 +60,17 @@ public class OlapQueryService {
|
||||
|
||||
// Создание запроса
|
||||
public Future<Integer> createQuery(String name, int dbConnectionId, JsonObject config,
|
||||
List<Integer> restaurantIds, String generatedSql) {
|
||||
List<Integer> restaurantIds, String generatedSql, boolean active) {
|
||||
JsonObject fullConfig = generateFullIikoJson(config);
|
||||
Map<String, Object> params = Map.of(
|
||||
"name", name,
|
||||
"db_connection_id", dbConnectionId,
|
||||
"config_json", config.encode(),
|
||||
"full_config_json", fullConfig.encode(),
|
||||
"sql_text", generatedSql != null ? generatedSql : ""
|
||||
"sql_text", generatedSql != null ? generatedSql : "",
|
||||
"active", active
|
||||
);
|
||||
String sql = "INSERT INTO olap_queries (name, db_connection_id, config_json, full_config_json, sql_text) VALUES (#{name}, #{db_connection_id}, #{config_json}, #{full_config_json}, #{sql_text})";
|
||||
String sql = "INSERT INTO olap_queries (name, db_connection_id, config_json, full_config_json, sql_text, active) VALUES (#{name}, #{db_connection_id}, #{config_json}, #{full_config_json}, #{sql_text}, #{active})";
|
||||
return SqlTemplate.forUpdate(pool, sql)
|
||||
.execute(params)
|
||||
.compose(rows -> getLastInsertId())
|
||||
@@ -73,7 +78,7 @@ public class OlapQueryService {
|
||||
}
|
||||
|
||||
public Future<Void> updateQuery(int id, String name, int dbConnectionId, JsonObject config,
|
||||
List<Integer> restaurantIds, String generatedSql) {
|
||||
List<Integer> restaurantIds, String generatedSql, boolean active) {
|
||||
JsonObject fullConfig = generateFullIikoJson(config);
|
||||
Map<String, Object> params = Map.of(
|
||||
"id", id,
|
||||
@@ -81,15 +86,14 @@ public class OlapQueryService {
|
||||
"db_connection_id", dbConnectionId,
|
||||
"config_json", config.encode(),
|
||||
"full_config_json", fullConfig.encode(),
|
||||
"sql_text", generatedSql != null ? generatedSql : ""
|
||||
"sql_text", generatedSql != null ? generatedSql : "",
|
||||
"active", active
|
||||
);
|
||||
String sql = "UPDATE olap_queries SET name = #{name}, db_connection_id = #{db_connection_id}, config_json = #{config_json}, full_config_json = #{full_config_json}, sql_text = #{sql_text} WHERE id = #{id}";
|
||||
String sql = "UPDATE olap_queries SET name = #{name}, db_connection_id = #{db_connection_id}, config_json = #{config_json}, full_config_json = #{full_config_json}, sql_text = #{sql_text}, active = #{active} WHERE id = #{id}";
|
||||
return SqlTemplate.forUpdate(pool, sql)
|
||||
.execute(params)
|
||||
.compose(v -> {
|
||||
return pool.query("DELETE FROM olap_query_restaurants WHERE query_id = " + id).execute()
|
||||
.compose(del -> linkRestaurants(id, restaurantIds));
|
||||
}).mapEmpty();
|
||||
.compose(v -> pool.query("DELETE FROM olap_query_restaurants WHERE query_id = " + id).execute()
|
||||
.compose(del -> linkRestaurants(id, restaurantIds))).mapEmpty();
|
||||
}
|
||||
|
||||
public JsonObject generateFullIikoJson(JsonObject clientConfig) {
|
||||
@@ -139,14 +143,13 @@ public class OlapQueryService {
|
||||
// Добавляем системные
|
||||
allFilters.mergeIn(systemFilters);
|
||||
|
||||
JsonObject result = new JsonObject()
|
||||
return new JsonObject()
|
||||
.put("reportType", reportType)
|
||||
.put("buildSummary", buildSummary)
|
||||
.put("groupByRowFields", rowFields)
|
||||
.put("groupByColFields", columnFields)
|
||||
.put("aggregateFields", valueFields)
|
||||
.put("filters", allFilters);
|
||||
return result;
|
||||
}
|
||||
|
||||
private JsonObject buildDateFilter(String reportType, String dateToStr, int daysBack) {
|
||||
@@ -195,36 +198,38 @@ public class OlapQueryService {
|
||||
// Получить все запросы (без config_json, для списка)
|
||||
public Future<JsonArray> getAllQueries() {
|
||||
String sql = """
|
||||
SELECT q.id, q.name, q.db_connection_id, q.created, q.updated,
|
||||
GROUP_CONCAT(r.name SEPARATOR ', ') AS restaurants,
|
||||
dc.name AS db_connection_name
|
||||
FROM olap_queries q
|
||||
LEFT JOIN olap_query_restaurants qr ON q.id = qr.query_id
|
||||
LEFT JOIN restaurants r ON qr.restaurant_id = r.id
|
||||
LEFT JOIN external_database dc ON q.db_connection_id = dc.id
|
||||
GROUP BY q.id
|
||||
ORDER BY q.id DESC
|
||||
""";
|
||||
SELECT q.id, q.name, q.db_connection_id, q.active, q.last_run, q.last_run_success, q.created, q.updated,
|
||||
GROUP_CONCAT(r.name SEPARATOR ', ') AS restaurants,
|
||||
dc.name AS db_connection_name
|
||||
FROM olap_queries q
|
||||
LEFT JOIN olap_query_restaurants qr ON q.id = qr.query_id
|
||||
LEFT JOIN restaurants r ON qr.restaurant_id = r.id
|
||||
LEFT JOIN external_database dc ON q.db_connection_id = dc.id
|
||||
GROUP BY q.id
|
||||
ORDER BY q.id DESC
|
||||
""";
|
||||
return pool.query(sql).execute()
|
||||
.map(rows -> {
|
||||
JsonArray arr = new JsonArray();
|
||||
rows.forEach(row -> {
|
||||
arr.add(new JsonObject()
|
||||
.put("id", row.getInteger("id"))
|
||||
.put("name", row.getString("name"))
|
||||
.put("dbConnectionId", row.getInteger("db_connection_id"))
|
||||
.put("dbConnectionName", row.getString("db_connection_name"))
|
||||
.put("restaurants", row.getString("restaurants") != null ? row.getString("restaurants") : "")
|
||||
.put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
|
||||
.put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null));
|
||||
});
|
||||
rows.forEach(row -> arr.add(new JsonObject()
|
||||
.put("id", row.getInteger("id"))
|
||||
.put("name", row.getString("name"))
|
||||
.put("dbConnectionId", row.getInteger("db_connection_id"))
|
||||
.put("dbConnectionName", row.getString("db_connection_name"))
|
||||
.put("restaurants", row.getString("restaurants") != null ? row.getString("restaurants") : "")
|
||||
.put("active", row.getBoolean("active"))
|
||||
.put("lastRun", row.getLocalDateTime("last_run") != null ? row.getLocalDateTime("last_run").toString() : null)
|
||||
.put("lastRunSuccess", row.getBoolean("last_run_success"))
|
||||
.put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
|
||||
.put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null)));
|
||||
return arr;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Получить один запрос с полной конфигурацией
|
||||
public Future<JsonObject> getQueryById(int id) {
|
||||
String querySql = "SELECT id, name, db_connection_id, config_json, sql_text, created, updated FROM olap_queries WHERE id = ?";
|
||||
String querySql = "SELECT id, name, db_connection_id, config_json, sql_text, active, created, updated FROM olap_queries WHERE id = ?";
|
||||
String restaurantsSql = "SELECT restaurant_id FROM olap_query_restaurants WHERE query_id = ?";
|
||||
|
||||
return pool.preparedQuery(querySql).execute(io.vertx.sqlclient.Tuple.of(id))
|
||||
@@ -237,6 +242,7 @@ public class OlapQueryService {
|
||||
.put("dbConnectionId", row.getInteger("db_connection_id"))
|
||||
.put("config", new JsonObject(row.getString("config_json")))
|
||||
.put("sql", row.getString("sql_text"))
|
||||
.put("active", row.getBoolean("active"))
|
||||
.put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
|
||||
.put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null);
|
||||
|
||||
@@ -250,6 +256,12 @@ public class OlapQueryService {
|
||||
});
|
||||
}
|
||||
|
||||
// Метод для обновления статуса выполнения
|
||||
public Future<Void> updateRunStatus(int queryId, boolean success) {
|
||||
String sql = "UPDATE olap_queries SET last_run = NOW(), last_run_success = ? WHERE id = ?";
|
||||
return pool.preparedQuery(sql).execute(Tuple.of(success, queryId)).mapEmpty();
|
||||
}
|
||||
|
||||
// Генерация SQL на основе конфигурации и ID подключения
|
||||
public Future<String> generateSql(JsonObject config, int dbConnectionId) {
|
||||
return externalDataBaseService.findById(dbConnectionId)
|
||||
@@ -264,4 +276,11 @@ public class OlapQueryService {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isValidTableName(String tableName) {
|
||||
if (tableName == null) return false;
|
||||
String trimmed = tableName.trim();
|
||||
// Первый символ — английская буква, далее буквы или цифры
|
||||
return trimmed.matches("^[A-Za-z][A-Za-z0-9]*$");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user