diff --git a/package-lock.json b/package-lock.json index b598b7d..6137b3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,12 @@ "@mdi/font": "6.2.95", "@vueuse/core": "^10.10.0", "axios": "^1.7.2", + "chart.js": "^4.4.3", "core-js": "^3.34.0", "moment": "^2.30.1", "roboto-fontface": "*", "vue": "^3.4.21", + "vue-chartjs": "^5.3.1", "vuetify": "^3.5.8" }, "devDependencies": { @@ -682,6 +684,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@mdi/font": { "version": "6.2.95", "resolved": "https://registry.npmjs.org/@mdi/font/-/font-6.2.95.tgz", @@ -1974,6 +1981,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", + "integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6317,6 +6335,15 @@ } } }, + "node_modules/vue-chartjs": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.1.tgz", + "integrity": "sha512-rZjqcHBxKiHrBl0CIvcOlVEBwRhpWAVf6rDU3vUfa7HuSRmGtCslc0Oc8m16oAVuk0erzc1FCtH1VCriHsrz+A==", + "peerDependencies": { + "chart.js": "^4.1.1", + "vue": "^3.0.0-0 || ^2.7.0" + } + }, "node_modules/vue-eslint-parser": { "version": "9.4.2", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", diff --git a/package.json b/package.json index c94dc92..e72eab5 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,12 @@ "@mdi/font": "6.2.95", "@vueuse/core": "^10.10.0", "axios": "^1.7.2", + "chart.js": "^4.4.3", "core-js": "^3.34.0", "moment": "^2.30.1", "roboto-fontface": "*", "vue": "^3.4.21", + "vue-chartjs": "^5.3.1", "vuetify": "^3.5.8" }, "devDependencies": { diff --git a/src/api/index.ts b/src/api/index.ts index 39a4439..5b1c239 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -2,10 +2,12 @@ import auth from './auth'; import users from './users'; import groups from './groups'; import permissions from './permissions'; +import plugins from './plugins'; export default { auth, users, groups, permissions, + plugins, }; diff --git a/src/api/plugins.ts b/src/api/plugins.ts new file mode 100644 index 0000000..9c9b24e --- /dev/null +++ b/src/api/plugins.ts @@ -0,0 +1,13 @@ +import { $axios } from '@/plugins/axios'; +import { AxiosResponse } from 'axios'; + +export default { + getAll: async (): Promise<[AxiosResponse?, string?]> => { + try { + const data = await $axios.get('v1/plugins'); + return [data, undefined]; + } catch (e: any) { + return [undefined, e?.response?.data?.error ?? e.message]; + } + }, +}; diff --git a/src/enums/time-units.enum.ts b/src/enums/time-units.enum.ts new file mode 100644 index 0000000..891ff39 --- /dev/null +++ b/src/enums/time-units.enum.ts @@ -0,0 +1,5 @@ +export enum timeUnits { + Seconds, + Minutes, + Hours, +} diff --git a/src/locales/en/actions.json b/src/locales/en/actions.json index 4c7934c..6673a03 100644 --- a/src/locales/en/actions.json +++ b/src/locales/en/actions.json @@ -11,6 +11,9 @@ "apply": "Apply", "confirm": "Confirm", "cancel": "Cancel", - "hide": "Hide" + "hide": "Hide", + "start-task": "Start task", + "stop-task": "Stop task", + "save": "Save" } } \ No newline at end of file diff --git a/src/locales/ru/actions.json b/src/locales/ru/actions.json index 3331c94..224cd71 100644 --- a/src/locales/ru/actions.json +++ b/src/locales/ru/actions.json @@ -11,6 +11,9 @@ "apply": "Применить", "confirm": "Подтвердить", "cancel": "Отменить", - "hide": "Скрыть" + "hide": "Скрыть", + "start-task": "Запустить задачу", + "stop-task": "Остановить задачу", + "save": "Сохранить" } } \ No newline at end of file diff --git a/src/locales/ru/tasks.json b/src/locales/ru/tasks.json new file mode 100644 index 0000000..9e68dc3 --- /dev/null +++ b/src/locales/ru/tasks.json @@ -0,0 +1,9 @@ +{ + "tasks": { + "title": "Задачи", + "new-task": { + "title": "Новая задача", + "parameters": "Параметры задачи" + } + } +} \ No newline at end of file diff --git a/src/pages/tasks/components/TaskParameters.vue b/src/pages/tasks/components/TaskParameters.vue new file mode 100644 index 0000000..3d898bf --- /dev/null +++ b/src/pages/tasks/components/TaskParameters.vue @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/src/pages/tasks/components/TweaksConstructor.vue b/src/pages/tasks/components/TweaksConstructor.vue new file mode 100644 index 0000000..d89b9a3 --- /dev/null +++ b/src/pages/tasks/components/TweaksConstructor.vue @@ -0,0 +1,140 @@ + + + + + + Interval Form + + + + Add Interval + + + + + + + + + + + + + + + + mdi-delete + + + + + + + Submit + + + + Result + + {{ result }} + + + + Graph + + + + + + + + diff --git a/src/pages/tasks/constructor.vue b/src/pages/tasks/constructor.vue new file mode 100644 index 0000000..5cfb8c1 --- /dev/null +++ b/src/pages/tasks/constructor.vue @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/src/pages/tasks/index.vue b/src/pages/tasks/index.vue new file mode 100644 index 0000000..a5f5fac --- /dev/null +++ b/src/pages/tasks/index.vue @@ -0,0 +1,46 @@ + + + + + + + + {{ t('$vuetify.users.title') }} + + + + {{ + tab === UsersPageTabs.USERS ? 'person_add' : 'group_add' + }} + {{ + t( + tab === UsersPageTabs.USERS + ? '$vuetify.actions.create-user' + : '$vuetify.actions.create-group', + ) + }} + + + + + + + + + + refresh + {{ t('$vuetify.actions.refresh') }} + + + + diff --git a/src/stores/permissions.ts b/src/stores/permissions.ts index 8abb2f9..4062414 100644 --- a/src/stores/permissions.ts +++ b/src/stores/permissions.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import api from '../api'; +import api from '@/api'; import { GroupPermission } from '@/types/group-permission'; import { Permissions } from '@/constants/permissions'; diff --git a/src/stores/plugins.ts b/src/stores/plugins.ts new file mode 100644 index 0000000..a345e89 --- /dev/null +++ b/src/stores/plugins.ts @@ -0,0 +1,27 @@ +import { defineStore } from 'pinia'; +import api from '@/api'; +import { Plugin } from '@/types/plugin'; + +export const usePluginsStore = defineStore('plugins', () => { + const all: Ref = ref([]); + // TODO notifies store + + const getAll = async () => { + const [res, e] = await api.plugins.getAll(); + if (e && typeof e === 'string') { + // error.value = e; + // TODO notify + } else { + all.value = res?.data ?? {}; + } + }; + + const getByID = (ID: number): Plugin | undefined => + all.value.find((i) => i.ID === ID); + + return { + all, + getAll, + getByID, + }; +}); diff --git a/src/stores/tasks/index.ts b/src/stores/tasks/index.ts new file mode 100644 index 0000000..93abff7 --- /dev/null +++ b/src/stores/tasks/index.ts @@ -0,0 +1,10 @@ +import { defineStore } from 'pinia'; +import * as t from './tweaks'; + +export const useTasksStore = defineStore('tasks', () => { + const tweaks = { ...t }; + + return { + ...tweaks, + }; +}); diff --git a/src/stores/tasks/tweaks.ts b/src/stores/tasks/tweaks.ts new file mode 100644 index 0000000..ff6b583 --- /dev/null +++ b/src/stores/tasks/tweaks.ts @@ -0,0 +1,276 @@ +interface Interval { + start: number; + startUnit: string; + end: number; + endUnit: string; + value: number; +} + +// +// MARK: Common variables +// +const chartOptions = { + responsive: true, + plugins: { + legend: { + display: true, + }, + tooltip: { + callbacks: { + label: (tooltipItem) => { + return `Time: ${tooltipItem.raw.x} seconds, Value: ${tooltipItem.raw.y}`; + }, + }, + }, + title: { + display: true, + text: 'Value', + }, + }, + scales: { + x: { + type: 'linear', + position: 'bottom', + }, + y: { + beginAtZero: false, + }, + }, +}; +const mockData = [ + { + start: 0, + startUnit: 'Seconds', + end: 10, + endUnit: 'Seconds', + value: 5, + }, + { + start: 10, + startUnit: 'Seconds', + end: 1, + endUnit: 'Minutes', + value: 15, + }, + { + start: 1, + startUnit: 'Minutes', + end: 1.5, + endUnit: 'Minutes', + value: 20, + }, + { + start: 1.5, + startUnit: 'Minutes', + end: 2, + endUnit: 'Minutes', + value: 25, + }, + { + start: 2, + startUnit: 'Minutes', + end: 0.5, + endUnit: 'Hours', + value: 30, + }, + { + start: 0.5, + startUnit: 'Hours', + end: 1, + endUnit: 'Hours', + value: 40, + }, +]; +const duration = ref(3600); // Duration in seconds (1 hour) +const limit = ref(50); // Limit for value +const intervals = ref(mockData); +const chartData = computed(() => { + const labels = intervals.value.flatMap((interval) => [ + `${convertToSeconds(interval.start, interval.startUnit)}`, + `${convertToSeconds(interval.end, interval.endUnit)}`, + ]); + const data = intervals.value.flatMap((interval) => [ + { + x: convertToSeconds(interval.start, interval.startUnit), + y: interval.value, + }, + { + x: convertToSeconds(interval.end, interval.endUnit), + y: interval.value, + }, + ]); + + return { + labels: labels, + datasets: [ + { + label: 'Interval Values', + data: data, + borderColor: '#42A5F5', + backgroundColor: 'rgba(66, 165, 245, 0.2)', + borderWidth: 2, + // stepped: true, + }, + ], + }; +}); + +// +// MARK: Convert and validate +// +const convertToSeconds = (value: number, unit: string): number => { + switch (unit) { + case 'Minutes': + return value * 60; + case 'Hours': + return value * 3600; + default: + return value; + } +}; +const validateStartTime = ( + startValue: number, + startUnit: string, + index: number, +): boolean => { + if (index === 0) return true; + + const currentStartInSeconds = convertToSeconds(startValue, startUnit); + const previous = intervals.value[index - 1]; + const previousEndInSeconds = convertToSeconds(previous.end, previous.endUnit); + + return ( + currentStartInSeconds >= previousEndInSeconds && + currentStartInSeconds <= duration.value + ); +}; +const validateEndTime = ( + endValue: number, + endUnit: string, + index: number, +): boolean => { + const current = intervals.value[index]; + const currentStartInSeconds = convertToSeconds( + current.start, + current.startUnit, + ); + const currentEndInSeconds = convertToSeconds(endValue, endUnit); + + return ( + currentEndInSeconds >= currentStartInSeconds && + currentEndInSeconds <= duration.value + ); +}; +const isValidInterval = (index: number): boolean => { + if (index === 0) return true; + + const current = intervals.value[index]; + const previous = intervals.value[index - 1]; + + const currentStartInSeconds = convertToSeconds( + current.start, + current.startUnit, + ); + const currentEndInSeconds = convertToSeconds(current.end, current.endUnit); + const previousEndInSeconds = convertToSeconds(previous.end, previous.endUnit); + + return ( + currentStartInSeconds >= previousEndInSeconds && + currentEndInSeconds >= currentStartInSeconds && + currentEndInSeconds <= duration.value + ); +}; + +// +// MARK: Actions +// +const addInterval = () => { + const lastInterval = intervals.value[intervals.value.length - 1]; + const newIntervalStart = convertToSeconds( + lastInterval.end, + lastInterval.endUnit, + ); + const newIntervalStartUnit = lastInterval.endUnit; + intervals.value.push({ + start: newIntervalStart, + startUnit: newIntervalStartUnit, + end: newIntervalStart, + endUnit: newIntervalStartUnit, + value: 0, + }); +}; +const removeInterval = (index: number) => { + intervals.value.splice(index, 1); +}; +// const submitForm = () => { +// for (let i = 0; i < intervals.value.length; i++) { +// if (!isValidInterval(i)) { +// result.value = `Invalid interval at index ${i}`; +// return; +// } +// } + +// // Convert all intervals to seconds +// const intervalsInSeconds = intervals.value.map((interval) => ({ +// start: convertToSeconds(interval.start, interval.startUnit), +// end: convertToSeconds(interval.end, interval.endUnit), +// value: interval.value, +// })); + +// // Prepare data for the chart +// const data = []; +// let lastEnd = 0; + +// intervalsInSeconds.forEach((interval) => { +// if (interval.start > lastEnd) { +// // Add a line to the last known value before the current interval +// data.push({ +// x: interval.start, +// y: data.length > 0 ? data[data.length - 1].y : 0, +// }); +// } +// // Start the interval value +// data.push({ x: interval.start, y: interval.value }); +// // End the interval value +// data.push({ x: interval.end, y: interval.value }); +// lastEnd = interval.end; +// }); + +// // Fill up to the duration +// if (lastEnd < duration.value) { +// data.push({ +// x: duration.value, +// y: data.length > 0 ? data[data.length - 1].y : 0, +// }); +// } + +// result.value = JSON.stringify(intervalsInSeconds, null, 2); + +// chartData.value = { +// labels: chartData.map((point) => point.x), +// datasets: [ +// { +// label: 'Interval Values', +// data: chartData, +// borderColor: '#42A5F5', +// backgroundColor: 'rgba(66, 165, 245, 0.2)', +// borderWidth: 2, +// fill: false, +// }, +// ], +// }; +// }; + +export { + chartOptions, + duration, + limit, + intervals, + chartData, + convertToSeconds, + validateStartTime, + validateEndTime, + isValidInterval, + addInterval, + removeInterval, +}; diff --git a/src/typed-router.d.ts b/src/typed-router.d.ts index 176a581..82234a4 100644 --- a/src/typed-router.d.ts +++ b/src/typed-router.d.ts @@ -20,6 +20,10 @@ declare module 'vue-router/auto-routes' { export interface RouteNamedMap { '/': RouteRecordInfo<'/', '/', Record, Record>, '/auth': RouteRecordInfo<'/auth', '/auth', Record, Record>, + '/tasks/': RouteRecordInfo<'/tasks/', '/tasks', Record, Record>, + '/tasks/components/TaskParameters': RouteRecordInfo<'/tasks/components/TaskParameters', '/tasks/components/TaskParameters', Record, Record>, + '/tasks/components/TweaksConstructor': RouteRecordInfo<'/tasks/components/TweaksConstructor', '/tasks/components/TweaksConstructor', Record, Record>, + '/tasks/constructor': RouteRecordInfo<'/tasks/constructor', '/tasks/constructor', Record, Record>, '/users/': RouteRecordInfo<'/users/', '/users', Record, Record>, '/users/components/AddGroup': RouteRecordInfo<'/users/components/AddGroup', '/users/components/AddGroup', Record, Record>, '/users/components/AddUser': RouteRecordInfo<'/users/components/AddUser', '/users/components/AddUser', Record, Record>, diff --git a/src/types/plugin.d.ts b/src/types/plugin.d.ts new file mode 100644 index 0000000..251ed04 --- /dev/null +++ b/src/types/plugin.d.ts @@ -0,0 +1,5 @@ +export interface Plugin { + ID: number; + name: string; + onlyUdp: boolean; +}
{{ result }}