From 1a1dd928086b547972dba22eab06688051fabcd1 Mon Sep 17 00:00:00 2001 From: "andreas.dinauer" Date: Sun, 26 Oct 2025 18:56:20 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20Improve=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/base-style.css | 2 +- assets/style.css | 12 ++- classes/Node.ts | 12 ++- classes/Pod.ts | 5 +- classes/Threshold.ts | 12 +++ classes/Ticker.ts | 31 ++++++ .../{Sidebar.vue => InspectSidebar.vue} | 57 ++-------- components/LogPopup.vue | 1 + components/PeriodPicker.vue | 74 +++++++++++++ components/PopupTemplate.vue | 9 +- components/Pulse.vue | 65 +++++++++++ components/SidebarTemplate.vue | 44 ++++++++ components/chart/Chart.vue | 69 ++++++++++++ components/chart/Dataset.ts | 24 +++++ components/monitorings/MonitoredResource.ts | 66 ++++++++++++ components/monitorings/MonitoringConfig.ts | 26 +++++ components/monitorings/NodeMonitoringPage.vue | 92 ++++++++++++++++ .../monitorings/VolumeMonitoringPage.vue | 102 ++++++++++++++++++ components/monitorings/memory/ChartConfig.ts | 52 +++++++++ .../memory/MemoryMonitoredPodComponent.vue | 54 ++++++++++ .../MemoryMonitoringConfigComponent.vue | 32 ++++++ components/monitorings/volumes/ChartConfig.ts | 59 ++++++++++ .../volumes/VolumeMonitoredPodComponent.vue | 26 +++++ .../VolumeMonitoringConfigComponent.vue | 34 ++++++ components/ui/UiInput.vue | 4 +- package-lock.json | 53 ++++++++- package.json | 5 + pages/account.vue | 21 ++++ pages/{dashboard.vue => account/inspect.vue} | 3 +- .../inspect}/deployments.vue | 0 .../inspect}/ingresses.vue | 0 .../{dashboard => account/inspect}/nodes.vue | 0 pages/{dashboard => account/inspect}/pods.vue | 0 .../inspect}/services.vue | 0 pages/account/monitorings.vue | 25 +++++ .../monitorings/applications/1/index.vue | 14 +++ pages/account/monitorings/nodes/index.vue | 7 ++ pages/index.vue | 2 +- 38 files changed, 1029 insertions(+), 65 deletions(-) create mode 100644 classes/Threshold.ts create mode 100644 classes/Ticker.ts rename components/{Sidebar.vue => InspectSidebar.vue} (53%) create mode 100644 components/PeriodPicker.vue create mode 100644 components/Pulse.vue create mode 100644 components/SidebarTemplate.vue create mode 100644 components/chart/Chart.vue create mode 100644 components/chart/Dataset.ts create mode 100644 components/monitorings/MonitoredResource.ts create mode 100644 components/monitorings/MonitoringConfig.ts create mode 100644 components/monitorings/NodeMonitoringPage.vue create mode 100644 components/monitorings/VolumeMonitoringPage.vue create mode 100644 components/monitorings/memory/ChartConfig.ts create mode 100644 components/monitorings/memory/MemoryMonitoredPodComponent.vue create mode 100644 components/monitorings/memory/MemoryMonitoringConfigComponent.vue create mode 100644 components/monitorings/volumes/ChartConfig.ts create mode 100644 components/monitorings/volumes/VolumeMonitoredPodComponent.vue create mode 100644 components/monitorings/volumes/VolumeMonitoringConfigComponent.vue create mode 100644 pages/account.vue rename pages/{dashboard.vue => account/inspect.vue} (81%) rename pages/{dashboard => account/inspect}/deployments.vue (100%) rename pages/{dashboard => account/inspect}/ingresses.vue (100%) rename pages/{dashboard => account/inspect}/nodes.vue (100%) rename pages/{dashboard => account/inspect}/pods.vue (100%) rename pages/{dashboard => account/inspect}/services.vue (100%) create mode 100644 pages/account/monitorings.vue create mode 100644 pages/account/monitorings/applications/1/index.vue create mode 100644 pages/account/monitorings/nodes/index.vue diff --git a/assets/base-style.css b/assets/base-style.css index 991e01a..91676b9 100644 --- a/assets/base-style.css +++ b/assets/base-style.css @@ -133,7 +133,7 @@ } .tile, *[class^='tile-'], *[class*=' tile-'] { - background-color: #ebebeb; + background-color: var(--tile-color); border-radius: 0.25rem; overflow: hidden; width: 100%; diff --git a/assets/style.css b/assets/style.css index cff11ea..f280579 100644 --- a/assets/style.css +++ b/assets/style.css @@ -6,10 +6,16 @@ margin: 0; font-weight: 600; font-family: "Source Code Pro", monospace; + text-decoration: none; } html { --primary-color: rgb(87, 75, 255); + background-color: #f1f1f1; + --tile-color: rgb(226, 226, 226); +} +html * { + color: rgb(31, 31, 31); } html, body, #__nuxt { @@ -21,14 +27,14 @@ html, body, #__nuxt { display: grid; grid-template-columns: auto auto auto 1fr auto auto; align-content: start; - background-color: #ebebeb; + background-color: rgb(228, 228, 228); } .resource, .header { display: contents; } .header > * { padding: 0.75rem; - background-color: rgb(29, 29, 29); + background-color: rgb(12, 12, 12); color: white; font-weight: bold; position: sticky; @@ -44,5 +50,5 @@ html, body, #__nuxt { font-family: "Source Code Pro", monospace; } .even > .grid-element { - background-color: rgb(216, 216, 216); + background-color: rgb(202, 202, 202); } \ No newline at end of file diff --git a/classes/Node.ts b/classes/Node.ts index c765f21..acc03ec 100644 --- a/classes/Node.ts +++ b/classes/Node.ts @@ -2,14 +2,20 @@ import type { Metadata } from "./Metadata"; export class NodeStats { - node?: Node; + constructor ( + public node: Node, + ) { } + relativeCpuUsage?: number; relativeMemory?: number; } -class Node +export class Node { - metadata?: Metadata; + constructor ( + public metadata: Metadata + ) { } + status?: Status; } diff --git a/classes/Pod.ts b/classes/Pod.ts index e57ee1f..a36c140 100644 --- a/classes/Pod.ts +++ b/classes/Pod.ts @@ -4,9 +4,12 @@ import type { Metadata } from "./Metadata"; export class Pod { - metadata?: Metadata status?: Status spec?: Spec + + constructor ( + public metadata: Metadata + ) { } } class Spec { diff --git a/classes/Threshold.ts b/classes/Threshold.ts new file mode 100644 index 0000000..2dc7796 --- /dev/null +++ b/classes/Threshold.ts @@ -0,0 +1,12 @@ +export class Threshold +{ + constructor ( + public value: number, + public calc: (value: number) => State + ) { } +} + +export enum State +{ + GREEN = "GREEN", ORANGE = "ORANGE", RED = "RED" +} \ No newline at end of file diff --git a/classes/Ticker.ts b/classes/Ticker.ts new file mode 100644 index 0000000..70994a1 --- /dev/null +++ b/classes/Ticker.ts @@ -0,0 +1,31 @@ +export function useTicker(interval: string) +{ + const seconds = Duration.parse(interval); + const ticker = ref(new Date()); + setInterval(() => { + ticker.value = new Date(); + }, seconds * 1000); + return ticker; +} + +export class Duration +{ + static parse(input: string): number { + if (input.length > 1) { + const unit = input.charAt(input.length - 1); + const value = parseInt(input.substring(0, input.length - 1), 10); + switch (unit) + { + case 's': + return value; + case 'm': + return value * 60; + case 'h': + return value * 60 * 60; + default: + throw new Error(`Invalid unit ${unit}`); + } + } + throw new Error("Invalid input"); + } +} \ No newline at end of file diff --git a/components/Sidebar.vue b/components/InspectSidebar.vue similarity index 53% rename from components/Sidebar.vue rename to components/InspectSidebar.vue index 0f51f97..94f1151 100644 --- a/components/Sidebar.vue +++ b/components/InspectSidebar.vue @@ -1,13 +1,13 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/components/LogPopup.vue b/components/LogPopup.vue index 7aef20a..01d6fd8 100644 --- a/components/LogPopup.vue +++ b/components/LogPopup.vue @@ -45,6 +45,7 @@ defineExpose({ white-space: pre-wrap; color: white; padding: 0.25rem 1rem; + line-height: 1.0.5rem; } .console { background-color: black; diff --git a/components/PeriodPicker.vue b/components/PeriodPicker.vue new file mode 100644 index 0000000..e9c198c --- /dev/null +++ b/components/PeriodPicker.vue @@ -0,0 +1,74 @@ + + + \ No newline at end of file diff --git a/components/PopupTemplate.vue b/components/PopupTemplate.vue index 4876d24..1f82852 100644 --- a/components/PopupTemplate.vue +++ b/components/PopupTemplate.vue @@ -55,15 +55,15 @@ function enableScrolling() \ No newline at end of file diff --git a/components/SidebarTemplate.vue b/components/SidebarTemplate.vue new file mode 100644 index 0000000..ec4b39b --- /dev/null +++ b/components/SidebarTemplate.vue @@ -0,0 +1,44 @@ + + + + + \ No newline at end of file diff --git a/components/chart/Chart.vue b/components/chart/Chart.vue new file mode 100644 index 0000000..e2f06e0 --- /dev/null +++ b/components/chart/Chart.vue @@ -0,0 +1,69 @@ + + + \ No newline at end of file diff --git a/components/chart/Dataset.ts b/components/chart/Dataset.ts new file mode 100644 index 0000000..e46c603 --- /dev/null +++ b/components/chart/Dataset.ts @@ -0,0 +1,24 @@ +export class Dataset +{ + constructor ( + public unit: string, + public data: Data[] + ) { } + + getLatest() + { + if (this.data.length > 0) + { + return this.data.toSorted((lower, higher) => new Date(higher.label).getTime() - new Date(lower.label).getTime()).at(this.data.length - 1); + } + return undefined; + } +} + +export class Data +{ + constructor ( + public value: number, + public label: Date + ) { } +} \ No newline at end of file diff --git a/components/monitorings/MonitoredResource.ts b/components/monitorings/MonitoredResource.ts new file mode 100644 index 0000000..cb2e005 --- /dev/null +++ b/components/monitorings/MonitoredResource.ts @@ -0,0 +1,66 @@ +import axios from "axios"; +import type { Node } from "~/classes/Node"; +import type { Pod } from "~/classes/Pod"; +import { Data } from "../chart/Dataset"; +import dayjs from "dayjs"; + +export class MonitoredResource +{ + constructor ( + public resource: T, + public jobs: IndexCollection[] + ) { } + + static getMonitorings(monitoringId: string, onSuccess: (monitoredPod: MonitoredResource[]) => void) + { + axios.get[]>(useRuntimeConfig().public.apiBase + '/monitorings/' + monitoringId + '/jobs') + .then(response => { + onSuccess(response.data) + }); + } + + static getNodeMonitorings(from: Date, to: Date, onSuccess: (monitoredPod: MonitoredResource[]) => void) + { + axios.get[]>(useRuntimeConfig().public.apiBase + '/monitorings/nodes/jobs', { + params: { + from: from, + to: to + } + }) + .then(response => { + onSuccess(response.data) + }); + } + + static extractDataset(key: string, jobs: IndexCollection[]): Data[] + { + const data = [] as Data[]; + for (const job of jobs) + { + const metrics = job.metrics[key]; + if (metrics != null) + { + data.push(new Data(metrics.average, dayjs.utc(job.timestamp).local().toDate())); + } + } + console.log(data); + return data; + } +} + +export class IndexCollection +{ + constructor ( + public metrics: Record, + public timestamp: Date + ) { } +} + +export class Metric +{ + constructor ( + public count: number, + public sum: number, + public average: number + ) { } +} \ No newline at end of file diff --git a/components/monitorings/MonitoringConfig.ts b/components/monitorings/MonitoringConfig.ts new file mode 100644 index 0000000..4c48a38 --- /dev/null +++ b/components/monitorings/MonitoringConfig.ts @@ -0,0 +1,26 @@ +import axios from "axios"; + +export class VolumeMonitoringConfig +{ + constructor ( + public id: string, + public configName: string, + public type: string, + public volumeConfig: VolumeConfig + ) {} + + static get(onSuccess: (monitorings: VolumeMonitoringConfig[]) => void) + { + axios.get(useRuntimeConfig().public.apiBase + '/monitorings') + .then(response => { + onSuccess(response.data) + }); + } +} + +class VolumeConfig +{ + constructor ( + public mountPath: string + ) {} +} \ No newline at end of file diff --git a/components/monitorings/NodeMonitoringPage.vue b/components/monitorings/NodeMonitoringPage.vue new file mode 100644 index 0000000..e87233a --- /dev/null +++ b/components/monitorings/NodeMonitoringPage.vue @@ -0,0 +1,92 @@ + + + \ No newline at end of file diff --git a/components/monitorings/VolumeMonitoringPage.vue b/components/monitorings/VolumeMonitoringPage.vue new file mode 100644 index 0000000..7af79d2 --- /dev/null +++ b/components/monitorings/VolumeMonitoringPage.vue @@ -0,0 +1,102 @@ + + + + + \ No newline at end of file diff --git a/components/monitorings/memory/ChartConfig.ts b/components/monitorings/memory/ChartConfig.ts new file mode 100644 index 0000000..1552b2a --- /dev/null +++ b/components/monitorings/memory/ChartConfig.ts @@ -0,0 +1,52 @@ +import { elements } from "chart.js"; +import dayjs from "dayjs" + +export function MEMORY_CHART_CONFIG() { + return { + type: 'line', + options: { + plugins: { + title: { + text: 'Chart.js Time Scale', + display: true + } + }, + aspectRatio: 2.75, + scales: { + x: { + type: 'time', + time: { + // Luxon format string + tooltipFormat: 'DD.MM' + }, + ticks: { + autoSkip: true, + maxTicksLimit: 6, + align: 'center', + color: '#cacacaff', + callback: function (value: Date) { + return dayjs(value).format("HH:mm"); + } + }, + grid: { + color: '#cacacaff', + borderColor: '#cacacaff', + lineWidth: 1 + }, + }, + y: { + beginAtZero: true, + ticks: { + maxTicksLimit: 6, + color: '#cacacaff' + }, + grid: { + color: '#cacacaff', + borderColor: '#cacacaff', + lineWidth: 1 + } + } + } + } + } +}; \ No newline at end of file diff --git a/components/monitorings/memory/MemoryMonitoredPodComponent.vue b/components/monitorings/memory/MemoryMonitoredPodComponent.vue new file mode 100644 index 0000000..bc920a2 --- /dev/null +++ b/components/monitorings/memory/MemoryMonitoredPodComponent.vue @@ -0,0 +1,54 @@ + + + \ No newline at end of file diff --git a/components/monitorings/memory/MemoryMonitoringConfigComponent.vue b/components/monitorings/memory/MemoryMonitoringConfigComponent.vue new file mode 100644 index 0000000..bd7ba33 --- /dev/null +++ b/components/monitorings/memory/MemoryMonitoringConfigComponent.vue @@ -0,0 +1,32 @@ + + + \ No newline at end of file diff --git a/components/monitorings/volumes/ChartConfig.ts b/components/monitorings/volumes/ChartConfig.ts new file mode 100644 index 0000000..72d7177 --- /dev/null +++ b/components/monitorings/volumes/ChartConfig.ts @@ -0,0 +1,59 @@ +import dayjs, { Dayjs } from "dayjs"; +import utc from 'dayjs/plugin/utc'; + +export function VOLUME_CHART_CONFIG(from: Date, to: Date) { + dayjs.extend(utc); + return { + type: 'line', + options: { + animation: false, + plugins: { + title: { + text: 'Chart.js Time Scale', + display: true + } + }, + aspectRatio: 2.75, + scales: { + x: { + type: 'time', + time: { + // Luxon format string + tooltipFormat: 'DD.MM' + }, + ticks: { + autoSkip: true, + maxTicksLimit: 6, + align: 'center', + color: '#cacacaff', + callback: function (value: Date) { + const offset = new Date().getTimezoneOffset(); + const date: Dayjs = dayjs(value).utcOffset(-offset); + return date.format("HH:mm"); + } + }, + min: from, + max: to, + grid: { + color: '#cacacaff', + borderColor: '#cacacaff', + lineWidth: 1 + } + }, + y: { + beginAtZero: true, + max: 100, + ticks: { + stepSize: 20, + color: '#cacacaff', + }, + grid: { + color: '#cacacaff', + borderColor: '#cacacaff', + lineWidth: 1 + } + } + } + } + } +} \ No newline at end of file diff --git a/components/monitorings/volumes/VolumeMonitoredPodComponent.vue b/components/monitorings/volumes/VolumeMonitoredPodComponent.vue new file mode 100644 index 0000000..62b1560 --- /dev/null +++ b/components/monitorings/volumes/VolumeMonitoredPodComponent.vue @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/components/monitorings/volumes/VolumeMonitoringConfigComponent.vue b/components/monitorings/volumes/VolumeMonitoringConfigComponent.vue new file mode 100644 index 0000000..ca825f4 --- /dev/null +++ b/components/monitorings/volumes/VolumeMonitoringConfigComponent.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/components/ui/UiInput.vue b/components/ui/UiInput.vue index 6f57b71..2cd9d4d 100644 --- a/components/ui/UiInput.vue +++ b/components/ui/UiInput.vue @@ -27,9 +27,9 @@ defineProps<{ .field input, .field .input, .field textarea, .field select { min-height: 2.5rem; border-radius: 0.25rem; - background-color: rgb(255, 255, 255); + background-color: rgb(51, 51, 51); width: 100%; - border: 2px solid #303030; + border: 2px solid #444444; outline: none; padding: 0.25rem; font-size: 1rem; diff --git a/package-lock.json b/package-lock.json index 097a780..3d8ef5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,13 @@ "dependencies": { "@pinia/nuxt": "^0.11.0", "axios": "^1.9.0", + "chart.js": "^4.5.0", + "chartjs-adapter-luxon": "^1.3.1", + "chartjs-adapter-moment": "^1.0.1", "dayjs": "^1.11.13", "jwt-decode": "^4.0.0", + "luxon": "^3.7.2", + "moment": "^2.30.1", "nuxt": "^3.17.4", "pinia": "^3.0.2", "vue": "^3.5.15", @@ -981,6 +986,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -3999,6 +4009,35 @@ } ] }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-adapter-luxon": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-luxon/-/chartjs-adapter-luxon-1.3.1.tgz", + "integrity": "sha512-yxHov3X8y+reIibl1o+j18xzrcdddCLqsXhriV2+aQ4hCR66IYFchlRXUvrJVoxglJ380pgytU7YWtoqdIgqhg==", + "peerDependencies": { + "chart.js": ">=3.0.0", + "luxon": ">=1.0.0" + } + }, + "node_modules/chartjs-adapter-moment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz", + "integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==", + "peerDependencies": { + "chart.js": ">=3.0.0", + "moment": "^2.10.2" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -6563,9 +6602,9 @@ } }, "node_modules/luxon": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", - "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "engines": { "node": ">=12" } @@ -6817,6 +6856,14 @@ "node": ">=18" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", diff --git a/package.json b/package.json index 8a3bfea..a300fac 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,13 @@ "dependencies": { "@pinia/nuxt": "^0.11.0", "axios": "^1.9.0", + "chart.js": "^4.5.0", + "chartjs-adapter-luxon": "^1.3.1", + "chartjs-adapter-moment": "^1.0.1", "dayjs": "^1.11.13", "jwt-decode": "^4.0.0", + "luxon": "^3.7.2", + "moment": "^2.30.1", "nuxt": "^3.17.4", "pinia": "^3.0.2", "vue": "^3.5.15", diff --git a/pages/account.vue b/pages/account.vue new file mode 100644 index 0000000..029a70d --- /dev/null +++ b/pages/account.vue @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/pages/dashboard.vue b/pages/account/inspect.vue similarity index 81% rename from pages/dashboard.vue rename to pages/account/inspect.vue index 7bcd0cc..a40b5c2 100644 --- a/pages/dashboard.vue +++ b/pages/account/inspect.vue @@ -1,6 +1,6 @@