From 1f0dd3afb7ad7f4ab2f7b944abf71e44ba0d2ee8 Mon Sep 17 00:00:00 2001 From: che Date: Wed, 15 Jul 2026 09:41:56 +0500 Subject: [PATCH] fix --- control-ui/package-lock.json | 12 +- control-ui/package.json | 3 +- control-ui/src/App.vue | 3 + control-ui/src/api.ts | 28 + .../components/analize/AnalizeGraphCard.vue | 469 +++++++++++ .../analize/SelectedControlCaseCard.vue | 513 ++++++++++++ .../src/components/params/ParamsEditCard.vue | 107 ++- .../params/ParamsQuickBatchCard.vue | 377 ++++++--- .../src/components/params/ParamsTableCard.vue | 8 +- .../src/composables/useModelCollection.ts | 136 ++++ control-ui/src/composables/useNotify.ts | 11 + control-ui/src/main.ts | 8 + control-ui/src/router/index.ts | 5 + control-ui/src/views/AnalizeChartView.vue | 759 +++--------------- control-ui/src/views/AnalizeListView.vue | 69 +- control-ui/src/views/ControlCaseChartView.vue | 4 + .../src/views/ControlCaseDetailView.vue | 35 + control-ui/src/views/ControlCaseListView.vue | 204 +++-- control-ui/src/views/ParamsListView.vue | 456 ++++++----- control-ui/src/views/SQLExecutorView.vue | 123 +++ control_case/controllers.go | 47 ++ control_case/routers.go | 1 + main.go | 2 + plot.py | 21 + sql_executor/controllers.go | 123 +++ sql_executor/routers.go | 11 + 26 files changed, 2402 insertions(+), 1133 deletions(-) create mode 100644 control-ui/src/components/analize/AnalizeGraphCard.vue create mode 100644 control-ui/src/components/analize/SelectedControlCaseCard.vue create mode 100644 control-ui/src/composables/useModelCollection.ts create mode 100644 control-ui/src/composables/useNotify.ts create mode 100644 control-ui/src/views/SQLExecutorView.vue create mode 100644 plot.py create mode 100644 sql_executor/controllers.go create mode 100644 sql_executor/routers.go diff --git a/control-ui/package-lock.json b/control-ui/package-lock.json index 35c1619..539e6d8 100644 --- a/control-ui/package-lock.json +++ b/control-ui/package-lock.json @@ -13,7 +13,8 @@ "echarts": "^6.1.0", "pinia": "^3.0.4", "vue": "beta", - "vue-router": "^5.1.0" + "vue-router": "^5.1.0", + "vue-toastification": "^2.0.0-rc.5" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", @@ -3542,6 +3543,15 @@ "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", "license": "MIT" }, + "node_modules/vue-toastification": { + "version": "2.0.0-rc.5", + "resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz", + "integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.2" + } + }, "node_modules/vue-tsc": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.7.tgz", diff --git a/control-ui/package.json b/control-ui/package.json index 3291a47..7c3beb2 100644 --- a/control-ui/package.json +++ b/control-ui/package.json @@ -16,7 +16,8 @@ "echarts": "^6.1.0", "pinia": "^3.0.4", "vue": "beta", - "vue-router": "^5.1.0" + "vue-router": "^5.1.0", + "vue-toastification": "^2.0.0-rc.5" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", diff --git a/control-ui/src/App.vue b/control-ui/src/App.vue index 7ade7f7..78bad28 100644 --- a/control-ui/src/App.vue +++ b/control-ui/src/App.vue @@ -22,6 +22,9 @@ import { RouterLink, RouterView } from 'vue-router' Analize graph + + SQL + diff --git a/control-ui/src/api.ts b/control-ui/src/api.ts index e95933e..9408feb 100644 --- a/control-ui/src/api.ts +++ b/control-ui/src/api.ts @@ -81,6 +81,13 @@ export const controlCaseApi = { ); return response.data; }, + + async restart(id: number) { + const response = await apiClient.post( + `/control_case/${id}/restart`, + ); + return response.data; + }, }; export interface AnalizeSeriesPoint { @@ -135,3 +142,24 @@ export const analizeApi = { return response.data; }, }; + +export interface SQLExecutePayload { + sql: string; +} + +export interface SQLExecuteResponse { + type: "select" | "exec"; + columns?: string[]; + rows?: unknown[][]; + rows_affected?: number; +} + +export const sqlApi = { + async execute(payload: SQLExecutePayload) { + const response = await apiClient.post( + "/sql/execute", + payload, + ); + return response.data; + }, +}; diff --git a/control-ui/src/components/analize/AnalizeGraphCard.vue b/control-ui/src/components/analize/AnalizeGraphCard.vue new file mode 100644 index 0000000..ae0d4ee --- /dev/null +++ b/control-ui/src/components/analize/AnalizeGraphCard.vue @@ -0,0 +1,469 @@ + + + diff --git a/control-ui/src/components/analize/SelectedControlCaseCard.vue b/control-ui/src/components/analize/SelectedControlCaseCard.vue new file mode 100644 index 0000000..661419d --- /dev/null +++ b/control-ui/src/components/analize/SelectedControlCaseCard.vue @@ -0,0 +1,513 @@ + + + diff --git a/control-ui/src/components/params/ParamsEditCard.vue b/control-ui/src/components/params/ParamsEditCard.vue index bcc7cc2..6031a42 100644 --- a/control-ui/src/components/params/ParamsEditCard.vue +++ b/control-ui/src/components/params/ParamsEditCard.vue @@ -10,64 +10,63 @@ defineProps<{ defineEmits<{ submit: [] + cancel: [] }>() diff --git a/control-ui/src/components/params/ParamsQuickBatchCard.vue b/control-ui/src/components/params/ParamsQuickBatchCard.vue index e69e9b5..12bf935 100644 --- a/control-ui/src/components/params/ParamsQuickBatchCard.vue +++ b/control-ui/src/components/params/ParamsQuickBatchCard.vue @@ -1,5 +1,9 @@ diff --git a/control-ui/src/components/params/ParamsTableCard.vue b/control-ui/src/components/params/ParamsTableCard.vue index 7de5922..b55b648 100644 --- a/control-ui/src/components/params/ParamsTableCard.vue +++ b/control-ui/src/components/params/ParamsTableCard.vue @@ -23,13 +23,11 @@ defineProps<{ page: number totalPages: number totalCount: number - pageStart: number - pageEnd: number }>() defineEmits<{ create: [] - select: [item: Params] + edit: [item: Params] launch: [item: Params] goToPage: [page: number] }>() @@ -117,7 +115,7 @@ defineEmits<{ {{ item.folder_path || '—' }}
- + @@ -128,7 +126,7 @@ defineEmits<{
-
Показано {{ pageStart }}-{{ pageEnd }} из {{ totalCount }}
+
Всего: {{ totalCount }}
diff --git a/control-ui/src/composables/useModelCollection.ts b/control-ui/src/composables/useModelCollection.ts new file mode 100644 index 0000000..9a281a0 --- /dev/null +++ b/control-ui/src/composables/useModelCollection.ts @@ -0,0 +1,136 @@ +import { computed, ref } from "vue"; +import { + formatApiError, + isPaginatedResponse, + type BaseEntity, + type ListParams, + type ModelApi, +} from "@/api_client.ts"; +import { useNotify } from "@/composables/useNotify.ts"; + +interface UseModelCollectionOptions { + pageSize?: number; + loadErrorMessage?: string; + saveErrorMessage?: string; + deleteErrorMessage?: string; + normalize?: (item: T | Record) => T; + notify?: boolean; +} + +export function useModelCollection( + api: ModelApi, + options: UseModelCollectionOptions = {}, +) { + const items = ref([]); + const loading = ref(false); + const saving = ref(false); + const error = ref(""); + const page = ref(1); + const totalCount = ref(0); + const notify = useNotify(); + const shouldNotify = options.notify ?? true; + + const totalPages = computed(() => { + const size = options.pageSize || items.value.length || 1; + return Math.max(1, Math.ceil(totalCount.value / size)); + }); + + function normalizeItems(values: T[]) { + return options.normalize ? values.map((item) => options.normalize?.(item) ?? item) : values; + } + + async function loadItems(params?: ListParams) { + loading.value = true; + error.value = ""; + + try { + const response = await api.list({ ...(params ?? {}), page: page.value }); + if (isPaginatedResponse(response)) { + items.value = normalizeItems(response.results); + totalCount.value = response.count; + } else { + items.value = normalizeItems(response); + totalCount.value = response.length; + } + } catch (err) { + error.value = formatApiError(err, options.loadErrorMessage); + if (shouldNotify) notify.error(error.value); + } finally { + loading.value = false; + } + } + + async function goToPage(nextPage: number, params?: ListParams) { + page.value = Math.min(Math.max(1, nextPage), totalPages.value); + await loadItems(params); + } + + async function createItem(payload: Partial) { + saving.value = true; + error.value = ""; + + try { + const created = await api.create(payload); + await loadItems(); + if (shouldNotify) notify.success("Запись создана."); + return options.normalize ? options.normalize(created) : created; + } catch (err) { + error.value = formatApiError(err, options.saveErrorMessage); + if (shouldNotify) notify.error(error.value); + return null; + } finally { + saving.value = false; + } + } + + async function updateItem(id: number, payload: Partial) { + saving.value = true; + error.value = ""; + + try { + const updated = await api.update(id, payload); + await loadItems(); + if (shouldNotify) notify.success("Запись сохранена."); + return options.normalize ? options.normalize(updated) : updated; + } catch (err) { + error.value = formatApiError(err, options.saveErrorMessage); + if (shouldNotify) notify.error(error.value); + return null; + } finally { + saving.value = false; + } + } + + async function removeItem(id: number) { + saving.value = true; + error.value = ""; + + try { + await api.remove(id); + await loadItems(); + if (shouldNotify) notify.success("Запись удалена."); + return true; + } catch (err) { + error.value = formatApiError(err, options.deleteErrorMessage); + if (shouldNotify) notify.error(error.value); + return false; + } finally { + saving.value = false; + } + } + + return { + items, + loading, + saving, + error, + page, + totalCount, + totalPages, + loadItems, + goToPage, + createItem, + updateItem, + removeItem, + }; +} diff --git a/control-ui/src/composables/useNotify.ts b/control-ui/src/composables/useNotify.ts new file mode 100644 index 0000000..a7ca255 --- /dev/null +++ b/control-ui/src/composables/useNotify.ts @@ -0,0 +1,11 @@ +import { useToast } from 'vue-toastification' + +export function useNotify() { + const toast = useToast() + + return { + error: (message: string) => toast.error(message), + success: (message: string) => toast.success(message), + info: (message: string) => toast.info(message), + } +} diff --git a/control-ui/src/main.ts b/control-ui/src/main.ts index 2097264..4706158 100644 --- a/control-ui/src/main.ts +++ b/control-ui/src/main.ts @@ -1,6 +1,8 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import 'bootstrap/dist/css/bootstrap.min.css' +import Toast from 'vue-toastification' +import 'vue-toastification/dist/index.css' import App from './App.vue' import router from './router' @@ -9,5 +11,11 @@ const app = createApp(App) app.use(createPinia()) app.use(router) +app.use(Toast, { + position: 'top-right', + timeout: 5000, + closeOnClick: true, + pauseOnHover: true, +}) app.mount('#app') diff --git a/control-ui/src/router/index.ts b/control-ui/src/router/index.ts index 25f2866..b7522fa 100644 --- a/control-ui/src/router/index.ts +++ b/control-ui/src/router/index.ts @@ -39,6 +39,11 @@ const router = createRouter({ name: 'analize-chart', component: () => import('@/views/AnalizeChartView.vue'), }, + { + path: '/sql', + name: 'sql-executor', + component: () => import('@/views/SQLExecutorView.vue'), + }, ], }) diff --git a/control-ui/src/views/AnalizeChartView.vue b/control-ui/src/views/AnalizeChartView.vue index 154b7d5..194f5ad 100644 --- a/control-ui/src/views/AnalizeChartView.vue +++ b/control-ui/src/views/AnalizeChartView.vue @@ -1,45 +1,29 @@ diff --git a/control-ui/src/views/AnalizeListView.vue b/control-ui/src/views/AnalizeListView.vue index 1ed6a8b..155993a 100644 --- a/control-ui/src/views/AnalizeListView.vue +++ b/control-ui/src/views/AnalizeListView.vue @@ -2,30 +2,13 @@ import { computed, onMounted, ref } from 'vue' import { RouterLink } from 'vue-router' import { analizeApi } from '@/api.ts' -import { formatApiError, isPaginatedResponse } from '@/api_client.ts' +import { formatApiError } from '@/api_client.ts' +import { useModelCollection } from '@/composables/useModelCollection.ts' +import { useNotify } from '@/composables/useNotify.ts' import type { Analize } from '@/models.ts' -const items = ref([]) -const loading = ref(false) -const error = ref('') const recalculatingId = ref(null) -const page = ref(1) -const totalCount = ref(0) -const serverPageSize = ref(0) - -const total = computed(() => totalCount.value) -const totalPages = computed(() => { - const size = serverPageSize.value || items.value.length || 1 - return Math.max(1, Math.ceil(totalCount.value / size)) -}) -const pageStart = computed(() => { - const size = serverPageSize.value || items.value.length - return totalCount.value === 0 || size === 0 ? 0 : (page.value - 1) * size + 1 -}) -const pageEnd = computed(() => { - const size = serverPageSize.value || items.value.length - return size === 0 ? 0 : Math.min(page.value * size, totalCount.value) -}) +const notify = useNotify() function normalizeAnalize(item: Analize | Record): Analize { const record = item as Record @@ -43,43 +26,20 @@ function normalizeAnalize(item: Analize | Record): Analize { } } +const { items, loading, error, page, totalCount, totalPages, loadItems, goToPage } = + useModelCollection(analizeApi, { + loadErrorMessage: 'Не удалось загрузить analize.', + normalize: normalizeAnalize, + }) + +const total = computed(() => totalCount.value) + function formatDate(value: string | undefined) { if (!value) return '—' const date = new Date(value) return Number.isNaN(date.getTime()) ? value : date.toLocaleString() } -async function goToPage(nextPage: number) { - page.value = Math.min(Math.max(1, nextPage), totalPages.value) - await loadItems() -} - -async function loadItems() { - loading.value = true - error.value = '' - - try { - const response = await analizeApi.list({ page: page.value }) - if (isPaginatedResponse(response)) { - items.value = response.results.map((item) => normalizeAnalize(item)) - totalCount.value = response.count - if (serverPageSize.value === 0 && response.results.length > 0) { - serverPageSize.value = response.results.length - } - } else { - items.value = response.map((item) => normalizeAnalize(item)) - totalCount.value = response.length - if (serverPageSize.value === 0 && response.length > 0) { - serverPageSize.value = response.length - } - } - } catch (err) { - error.value = formatApiError(err, 'Не удалось загрузить analize.') - } finally { - loading.value = false - } -} - async function recalculate(item: Analize) { recalculatingId.value = item.id error.value = '' @@ -90,6 +50,7 @@ async function recalculate(item: Analize) { items.value = items.value.map((current) => (current.id === normalized.id ? normalized : current)) } catch (err) { error.value = formatApiError(err, 'Не удалось пересчитать analize.') + notify.error(error.value) } finally { recalculatingId.value = null } @@ -105,7 +66,7 @@ onMounted(loadItems)

Analize

Результаты обработки расчетных случаев. Всего записей: {{ total }}

- График @@ -168,7 +129,7 @@ onMounted(loadItems)
- Показано {{ pageStart }}-{{ pageEnd }} из {{ totalCount }} + Всего: {{ totalCount }}
diff --git a/control-ui/src/views/ControlCaseChartView.vue b/control-ui/src/views/ControlCaseChartView.vue index 62973e0..0844992 100644 --- a/control-ui/src/views/ControlCaseChartView.vue +++ b/control-ui/src/views/ControlCaseChartView.vue @@ -4,8 +4,10 @@ import { RouterLink, useRoute } from 'vue-router' import * as echarts from 'echarts' import { controlCaseApi, type CsvChartResponse } from '@/api.ts' import { formatApiError } from '@/api_client.ts' +import { useNotify } from '@/composables/useNotify.ts' const route = useRoute() +const notify = useNotify() const containerRef = ref(null) const loading = ref(false) const error = ref('') @@ -53,6 +55,7 @@ function renderChart() { async function loadData() { if (!Number.isFinite(id.value)) { error.value = 'Некорректный ID' + notify.error(error.value) data.value = null return } @@ -65,6 +68,7 @@ async function loadData() { renderChart() } catch (err) { error.value = formatApiError(err, 'Не удалось загрузить данные графика.') + notify.error(error.value) data.value = null } finally { loading.value = false diff --git a/control-ui/src/views/ControlCaseDetailView.vue b/control-ui/src/views/ControlCaseDetailView.vue index 5d1662c..8c37bf6 100644 --- a/control-ui/src/views/ControlCaseDetailView.vue +++ b/control-ui/src/views/ControlCaseDetailView.vue @@ -3,14 +3,17 @@ import { computed, onMounted, ref, watch } from 'vue' import { RouterLink, useRoute } from 'vue-router' import { analizeApi, controlCaseApi } from '@/api.ts' import { formatApiError } from '@/api_client.ts' +import { useNotify } from '@/composables/useNotify.ts' import type { ControlCase } from '@/models.ts' import { controlCaseStatusMeta } from '@/statuses.ts' const route = useRoute() +const notify = useNotify() const item = ref(null) const loading = ref(false) const recalculating = ref(false) const creatingInitialCondition = ref(false) +const restarting = ref(false) const error = ref('') const statusMessage = ref('') @@ -41,6 +44,7 @@ function formatDate(value: string | undefined) { async function loadItem() { if (!Number.isFinite(id.value)) { error.value = 'Некорректный ID' + notify.error(error.value) item.value = null return } @@ -52,6 +56,7 @@ async function loadItem() { item.value = await controlCaseApi.retrieve(id.value) } catch (err) { error.value = formatApiError(err, 'Не удалось загрузить control_case.') + notify.error(error.value) item.value = null } finally { loading.value = false @@ -70,6 +75,7 @@ async function recalculateAnalysis() { statusMessage.value = 'Анализ пересчитан.' } catch (err) { error.value = formatApiError(err, 'Не удалось пересчитать анализ.') + notify.error(error.value) } finally { recalculating.value = false } @@ -87,11 +93,31 @@ async function createInitialCondition() { statusMessage.value = `Начальные условия созданы: ${response.initial_condition.file_path}` } catch (err) { error.value = formatApiError(err, 'Не удалось создать начальные условия.') + notify.error(error.value) } finally { creatingInitialCondition.value = false } } +async function restartCalculation() { + if (!Number.isFinite(id.value)) return + if (!window.confirm('Удалить данные расчета и запустить заново?')) return + + restarting.value = true + error.value = '' + statusMessage.value = '' + + try { + item.value = await controlCaseApi.restart(id.value) + statusMessage.value = 'Расчет поставлен на перезапуск.' + } catch (err) { + error.value = formatApiError(err, 'Не удалось перезапустить расчет.') + notify.error(error.value) + } finally { + restarting.value = false + } +} + onMounted(loadItem) watch(id, loadItem) @@ -130,6 +156,15 @@ watch(id, loadItem) > {{ creatingInitialCondition ? 'Создание...' : 'Взять за начальные условия' }} +
diff --git a/control-ui/src/views/ControlCaseListView.vue b/control-ui/src/views/ControlCaseListView.vue index 9f1b27f..b8f4e98 100644 --- a/control-ui/src/views/ControlCaseListView.vue +++ b/control-ui/src/views/ControlCaseListView.vue @@ -2,34 +2,44 @@ import { computed, onMounted, ref } from "vue"; import { RouterLink } from "vue-router"; import { controlCaseApi } from "@/api.ts"; -import { formatApiError, isPaginatedResponse } from "@/api_client.ts"; +import { formatApiError, type ListParams } from "@/api_client.ts"; +import { useNotify } from "@/composables/useNotify.ts"; import type { ControlCase } from "@/models.ts"; import { controlCaseStatusMeta } from "@/statuses.ts"; +import { useModelCollection } from "@/composables/useModelCollection.ts"; -const items = ref([]); -const loading = ref(false); -const error = ref(""); -const page = ref(1); -const totalCount = ref(0); -const serverPageSize = ref(0); +const { items, loading, error, page, totalCount, totalPages, loadItems, goToPage } = + useModelCollection(controlCaseApi, { + loadErrorMessage: "Не удалось загрузить control_case.", + }); +const selectedStatus = ref(""); +const selectedCaseIds = ref([]); +const restarting = ref(false); +const notify = useNotify(); const total = computed(() => totalCount.value); -const totalPages = computed(() => { - const size = serverPageSize.value || items.value.length || 1; - return Math.max(1, Math.ceil(totalCount.value / size)); -}); -const pageStart = computed(() => { - const size = serverPageSize.value || items.value.length; - return totalCount.value === 0 || size === 0 ? 0 : (page.value - 1) * size + 1; -}); -const pageEnd = computed(() => { - const size = serverPageSize.value || items.value.length; - return size === 0 ? 0 : Math.min(page.value * size, totalCount.value); +const statusOptions = ["N", "R", "D"]; +const restartableItems = computed(() => items.value.filter((item) => canRestart(item))); +const selectedCount = computed(() => selectedCaseIds.value.length); +const allRestartableSelected = computed(() => { + const ids = restartableItems.value.map((item) => item.id); + return ids.length > 0 && ids.every((id) => selectedCaseIds.value.includes(id)); }); -async function goToPage(nextPage: number) { - page.value = Math.min(Math.max(1, nextPage), totalPages.value); - await loadItems(); +function filterParams(): ListParams | undefined { + return selectedStatus.value ? { status: selectedStatus.value } : undefined; +} + +async function loadFilteredItems(resetPage = false) { + if (resetPage) { + page.value = 1; + selectedCaseIds.value = []; + } + await loadItems(filterParams()); +} + +async function goToFilteredPage(nextPage: number) { + await goToPage(nextPage, filterParams()); } function formatDate(value: string | undefined) { @@ -39,33 +49,68 @@ function formatDate(value: string | undefined) { return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); } -async function loadItems() { - loading.value = true; - error.value = ""; - - try { - const response = await controlCaseApi.list({ page: page.value }); - if (isPaginatedResponse(response)) { - items.value = response.results; - totalCount.value = response.count; - if (serverPageSize.value === 0 && response.results.length > 0) { - serverPageSize.value = response.results.length; - } - } else { - items.value = response; - totalCount.value = response.length; - if (serverPageSize.value === 0 && response.length > 0) { - serverPageSize.value = response.length; - } - } - } catch (err) { - error.value = formatApiError(err, "Не удалось загрузить control_case."); - } finally { - loading.value = false; - } +function canRestart(item: ControlCase) { + return item.status !== "N"; } -onMounted(loadItems); +function isSelected(id: number) { + return selectedCaseIds.value.includes(id); +} + +function toggleItem(item: ControlCase) { + if (!canRestart(item)) return; + + if (isSelected(item.id)) { + selectedCaseIds.value = selectedCaseIds.value.filter((id) => id !== item.id); + return; + } + + selectedCaseIds.value = [...selectedCaseIds.value, item.id]; +} + +function toggleVisibleItems() { + const visibleIds = restartableItems.value.map((item) => item.id); + if (visibleIds.length === 0) return; + + if (allRestartableSelected.value) { + selectedCaseIds.value = selectedCaseIds.value.filter((id) => !visibleIds.includes(id)); + return; + } + + selectedCaseIds.value = Array.from(new Set([...selectedCaseIds.value, ...visibleIds])); +} + +async function restartSelectedCases() { + if (selectedCaseIds.value.length === 0) return; + if (!window.confirm(`Перезапустить выбранные кейсы (${selectedCaseIds.value.length})? Данные расчетов будут удалены.`)) return; + + restarting.value = true; + error.value = ""; + + const ids = [...selectedCaseIds.value]; + const failed: string[] = []; + + for (const id of ids) { + try { + await controlCaseApi.restart(id); + } catch (err) { + failed.push(`#${id}: ${formatApiError(err, "не удалось перезапустить")}`); + } + } + + if (failed.length > 0) { + error.value = `Не удалось перезапустить ${failed.length} из ${ids.length}: ${failed.join("; ")}`; + notify.error(error.value); + } else { + notify.success(`Перезапущено кейсов: ${ids.length}.`); + } + + selectedCaseIds.value = []; + restarting.value = false; + await loadFilteredItems(); +} + +onMounted(() => loadFilteredItems()); diff --git a/control-ui/src/views/SQLExecutorView.vue b/control-ui/src/views/SQLExecutorView.vue new file mode 100644 index 0000000..c91e824 --- /dev/null +++ b/control-ui/src/views/SQLExecutorView.vue @@ -0,0 +1,123 @@ + + +