stage WIP

This commit is contained in:
Vitaliy Pavlov 2024-08-23 20:57:18 +07:00
parent 88820ce430
commit c48612b9fc
18 changed files with 606 additions and 3 deletions

27
package-lock.json generated
View File

@ -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",

View File

@ -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": {

View File

@ -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,
};

13
src/api/plugins.ts Normal file
View File

@ -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];
}
},
};

View File

@ -0,0 +1,5 @@
export enum timeUnits {
Seconds,
Minutes,
Hours,
}

View File

@ -11,6 +11,9 @@
"apply": "Apply",
"confirm": "Confirm",
"cancel": "Cancel",
"hide": "Hide"
"hide": "Hide",
"start-task": "Start task",
"stop-task": "Stop task",
"save": "Save"
}
}

View File

@ -11,6 +11,9 @@
"apply": "Применить",
"confirm": "Подтвердить",
"cancel": "Отменить",
"hide": "Скрыть"
"hide": "Скрыть",
"start-task": "Запустить задачу",
"stop-task": "Остановить задачу",
"save": "Сохранить"
}
}

View File

@ -0,0 +1,9 @@
{
"tasks": {
"title": "Задачи",
"new-task": {
"title": "Новая задача",
"parameters": "Параметры задачи"
}
}
}

View File

@ -0,0 +1,16 @@
<script lang="ts" setup>
import { useLocale } from 'vuetify';
// Common variables
const { t } = useLocale();
</script>
<template>
<div>
<v-expansion-panel
:title="t('$vuetify.')"
text="Lorem ipsum dolor sit amet consectetur adipisicing elit. Commodi, ratione debitis quis est labore voluptatibus! Eaque cupiditate minima"
>
</v-expansion-panel>
</div>
</template>

View File

@ -0,0 +1,140 @@
<script lang="ts" setup>
import { Line as LineChart } from 'vue-chartjs';
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
LineElement,
PointElement,
CategoryScale,
LinearScale,
} from 'chart.js';
import { ref } from 'vue';
import { useTasksStore } from '@/stores/tasks';
import { timeUnits } from '@/enums/time-units.enum';
const unitsArray = Object.values(timeUnits);
ChartJS.register(
Title,
Tooltip,
Legend,
LineElement,
PointElement,
CategoryScale,
LinearScale,
);
const $tasks = useTasksStore();
const result = ref<string | null>(null);
</script>
<template>
<v-container>
<v-card>
<v-card-title> Interval Form </v-card-title>
<v-card-text>
<v-text-field
v-model.number="$tasks.duration"
label="Duration (seconds)"
type="number"
:rules="[(v) => v > 0 || 'Must be greater than zero']"
required
></v-text-field>
<v-text-field
v-model.number="$tasks.limit"
label="Limit"
type="number"
:rules="[(v) => v > 0 || 'Must be greater than zero']"
required
></v-text-field>
<v-btn @click="$tasks.addInterval">Add Interval</v-btn>
<div v-for="(interval, index) in $tasks.intervals" :key="index">
<v-row>
<v-col cols="3">
<v-text-field
v-model.number="interval.start"
label="Start Time"
type="number"
:rules="[
(v) => v >= 0 || 'Must be positive',
(v) =>
$tasks.validateStartTime(v, interval.startUnit, index) ||
'Start time must be valid and within duration',
]"
required
></v-text-field>
<v-select
v-model="interval.startUnit"
:items="unitsArray"
label="Unit"
required
></v-select>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="interval.end"
label="End Time"
type="number"
:rules="[
(v) =>
$tasks.validateEndTime(v, interval.endUnit, index) ||
'End time must be valid and within duration',
]"
required
></v-text-field>
<v-select
v-model="interval.endUnit"
:items="unitsArray"
label="Unit"
required
></v-select>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="interval.value"
label="Value"
type="number"
:rules="[
(v) =>
v <= $tasks.limit ||
`Value must be less than or equal to ${$tasks.limit}`,
(v) => v >= 0 || 'Must be greater than or equal to 0',
]"
required
></v-text-field>
</v-col>
<v-col cols="1">
<v-btn icon @click="$tasks.removeInterval(index)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-col>
</v-row>
</div>
</v-card-text>
<v-card-actions>
<v-btn type="submit">Submit</v-btn>
</v-card-actions>
</v-card>
<v-card v-if="result">
<v-card-title> Result </v-card-title>
<v-card-text>
<pre>{{ result }}</pre>
</v-card-text>
</v-card>
<v-card v-if="$tasks.chartData.labels.length">
<v-card-title> Graph </v-card-title>
<v-card-text>
<LineChart :data="$tasks.chartData" :options="$tasks.chartOptions" />
</v-card-text>
</v-card>
</v-container>
</template>
<style scoped>
.v-card {
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import { useLocale } from 'vuetify';
import TweaksConstructor from './components/TweaksConstructor.vue';
// Common variables
const { t } = useLocale();
</script>
<template>
<div>
<TweaksConstructor />
<!-- -->
</div>
</template>

46
src/pages/tasks/index.vue Normal file
View File

@ -0,0 +1,46 @@
<script setup lang="ts">
import { useLocale } from 'vuetify';
// Common variables
const { t } = useLocale();
</script>
<template>
<div>
<!-- Header -->
<v-app-bar prominent class="elevation-0 border-b">
<!-- Title -->
<span class="text-h6 ml-5 mr-10">{{ t('$vuetify.users.title') }}</span>
<!-- Create group/user -->
<v-btn
color="primary"
variant="text"
class="mr-3 px-3"
@click="openCreationModal()"
>
<span class="mso mr-1 text-xl">{{
tab === UsersPageTabs.USERS ? 'person_add' : 'group_add'
}}</span>
<span>{{
t(
tab === UsersPageTabs.USERS
? '$vuetify.actions.create-user'
: '$vuetify.actions.create-group',
)
}}</span>
</v-btn>
<!-- User creation modal -->
<AddUser v-model="$users.createModal" />
<!-- Group creation modal -->
<AddGroup v-model="$groups.createModal" />
<!-- Refresh users -->
<v-btn class="px-3" color="primary" variant="text" @click="getAll()">
<span class="mso mr-1 text-xl">refresh</span>
<span>{{ t('$vuetify.actions.refresh') }}</span>
</v-btn>
</v-app-bar>
</div>
</template>

View File

@ -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';

27
src/stores/plugins.ts Normal file
View File

@ -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<Plugin[]> = 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,
};
});

10
src/stores/tasks/index.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineStore } from 'pinia';
import * as t from './tweaks';
export const useTasksStore = defineStore('tasks', () => {
const tweaks = { ...t };
return {
...tweaks,
};
});

276
src/stores/tasks/tweaks.ts Normal file
View File

@ -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<number>(3600); // Duration in seconds (1 hour)
const limit = ref<number>(50); // Limit for value
const intervals = ref<Interval[]>(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,
};

View File

@ -20,6 +20,10 @@ declare module 'vue-router/auto-routes' {
export interface RouteNamedMap {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/auth': RouteRecordInfo<'/auth', '/auth', Record<never, never>, Record<never, never>>,
'/tasks/': RouteRecordInfo<'/tasks/', '/tasks', Record<never, never>, Record<never, never>>,
'/tasks/components/TaskParameters': RouteRecordInfo<'/tasks/components/TaskParameters', '/tasks/components/TaskParameters', Record<never, never>, Record<never, never>>,
'/tasks/components/TweaksConstructor': RouteRecordInfo<'/tasks/components/TweaksConstructor', '/tasks/components/TweaksConstructor', Record<never, never>, Record<never, never>>,
'/tasks/constructor': RouteRecordInfo<'/tasks/constructor', '/tasks/constructor', Record<never, never>, Record<never, never>>,
'/users/': RouteRecordInfo<'/users/', '/users', Record<never, never>, Record<never, never>>,
'/users/components/AddGroup': RouteRecordInfo<'/users/components/AddGroup', '/users/components/AddGroup', Record<never, never>, Record<never, never>>,
'/users/components/AddUser': RouteRecordInfo<'/users/components/AddUser', '/users/components/AddUser', Record<never, never>, Record<never, never>>,

5
src/types/plugin.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
export interface Plugin {
ID: number;
name: string;
onlyUdp: boolean;
}