From 0a9a5ed787dacedb16f9563ad24f45406384dbc8 Mon Sep 17 00:00:00 2001 From: "andreas.dinauer" Date: Thu, 6 Nov 2025 14:36:29 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8Add=20dynamic=20logs=20and=20pods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.vue | 4 +- assets/base-style.css | 4 + classes/ConfigMap.ts | 9 ++ classes/LogRepo.ts | 35 ++++++ classes/Metadata.ts | 4 +- classes/Node.ts | 17 +-- classes/Pod.ts | 18 ++- classes/ResourceRepo.ts | 113 +++++++++++++++--- classes/Secret.ts | 5 +- classes/Service.ts | 8 +- components/ConfigMapComponent.vue | 19 +++ components/InspectSidebar.vue | 31 +++-- components/LogPopup.vue | 10 +- components/NodeComponent.vue | 12 +- components/ScrollComponent.vue | 14 ++- components/SettingsSidebar.vue | 17 +++ components/SidebarTemplate.vue | 3 +- .../inspect/resources/ConfigMapList.vue | 28 +++++ ...t.vue => CustomResourceDefinitionList.vue} | 0 ...oymentComponent.vue => DeploymentList.vue} | 0 .../{IngressComponent.vue => IngressList.vue} | 0 .../{NodeComponent.vue => NodeList.vue} | 4 +- .../{PodComponent.vue => PodList.vue} | 20 +++- .../{SecretComponent.vue => SecretList.vue} | 0 .../{ServiceComponent.vue => ServiceList.vue} | 0 components/pod/PodComponent.vue | 5 +- components/pod/view/PodDeletePopup.vue | 18 ++- components/pod/view/PodViewPopup.vue | 56 ++++++--- nuxt.config.ts | 5 +- pages/account/inspect.vue | 2 +- .../inspect/[resource]/[namespace].vue | 16 +-- pages/account/settings.vue | 19 +++ pages/account/settings/password.vue | 13 ++ pages/account/settings/users.vue | 13 ++ requests/pod.ts | 4 +- utils/useSeconds.ts | 7 ++ 36 files changed, 433 insertions(+), 100 deletions(-) create mode 100644 classes/ConfigMap.ts create mode 100644 classes/LogRepo.ts create mode 100644 components/ConfigMapComponent.vue create mode 100644 components/SettingsSidebar.vue create mode 100644 components/inspect/resources/ConfigMapList.vue rename components/inspect/resources/{CustomResourceDefinitionComponent.vue => CustomResourceDefinitionList.vue} (100%) rename components/inspect/resources/{DeploymentComponent.vue => DeploymentList.vue} (100%) rename components/inspect/resources/{IngressComponent.vue => IngressList.vue} (100%) rename components/inspect/resources/{NodeComponent.vue => NodeList.vue} (86%) rename components/inspect/resources/{PodComponent.vue => PodList.vue} (63%) rename components/inspect/resources/{SecretComponent.vue => SecretList.vue} (100%) rename components/inspect/resources/{ServiceComponent.vue => ServiceList.vue} (100%) create mode 100644 pages/account/settings.vue create mode 100644 pages/account/settings/password.vue create mode 100644 pages/account/settings/users.vue create mode 100644 utils/useSeconds.ts diff --git a/app.vue b/app.vue index 3a09116..05a1382 100644 --- a/app.vue +++ b/app.vue @@ -12,7 +12,9 @@ useRouter().beforeEach((route: RouteLocation) => { guard(route.fullPath); }); -guard(useRoute().fullPath); +onMounted(() => { + guard(useRoute().fullPath); +}) function guard(route: string) { diff --git a/assets/base-style.css b/assets/base-style.css index 944ee9b..e882a64 100644 --- a/assets/base-style.css +++ b/assets/base-style.css @@ -241,4 +241,8 @@ .width-6rem { width: 6rem; +} + +.nowrap { + white-space: nowrap } \ No newline at end of file diff --git a/classes/ConfigMap.ts b/classes/ConfigMap.ts new file mode 100644 index 0000000..7ca7294 --- /dev/null +++ b/classes/ConfigMap.ts @@ -0,0 +1,9 @@ +import type { Metadata } from "./Metadata"; + +export class ConfigMap +{ + constructor ( + public metadata: Metadata, + public data: Record + ) {} +} \ No newline at end of file diff --git a/classes/LogRepo.ts b/classes/LogRepo.ts new file mode 100644 index 0000000..03771f4 --- /dev/null +++ b/classes/LogRepo.ts @@ -0,0 +1,35 @@ +import type { Log } from "~/requests/logs"; + +export class LogRepo +{ + websocket?: WebSocket = undefined; + + listen(namespace: string, name: string, onReceive: (logs: Log[]) => void) + { + const websocket = new WebSocket(StringUtils.format("ws://%s/api/logs/%s/%s", useRuntimeConfig().public.apiWsBase, namespace, name)); + websocket.addEventListener('open', () => { + console.info("Opened Websocket."); + }) + websocket.addEventListener("message", (event) => { + console.log(event.data); + onReceive(JSON.parse(event.data) as Log[]); + }); + const interval = setInterval(() => { + console.info("[PING]"); + websocket.send('[PING]'); + }, 5000); + websocket.addEventListener("close", () => { + console.info("Closing websocket."); + clearTimeout(interval); + }); + this.websocket = websocket; + } + + clear() + { + if (this.websocket) + { + this.websocket.close(); + } + } +} \ No newline at end of file diff --git a/classes/Metadata.ts b/classes/Metadata.ts index a9cc69d..7000414 100644 --- a/classes/Metadata.ts +++ b/classes/Metadata.ts @@ -5,6 +5,8 @@ export class Metadata constructor ( public namespace: string, - public name: string + public name: string, + public labels?: Record, + public annotations?: Record ) { } } \ No newline at end of file diff --git a/classes/Node.ts b/classes/Node.ts index 87bdc30..37898ad 100644 --- a/classes/Node.ts +++ b/classes/Node.ts @@ -1,25 +1,18 @@ import type { Metadata } from "./Metadata"; +import type { HasMetadata } from "./ResourceRepo"; -export class NodeStats +export class Node implements HasMetadata { constructor ( - public node: Node, - public runningPods: number + public metadata: Metadata, + public runningPods: number, + public status: Status ) { } relativeCpuUsage?: number; relativeMemory?: number; } -export class Node -{ - constructor ( - public metadata: Metadata - ) { } - - status?: Status; -} - class Status { conditions?: Condition[] diff --git a/classes/Pod.ts b/classes/Pod.ts index a36c140..fcbabca 100644 --- a/classes/Pod.ts +++ b/classes/Pod.ts @@ -1,19 +1,24 @@ import dayjs from "dayjs"; import advancedFormat from "dayjs/plugin/advancedFormat"; import type { Metadata } from "./Metadata"; +import type { HasMetadata } from "./ResourceRepo"; -export class Pod +export class Pod implements HasMetadata { status?: Status - spec?: Spec constructor ( - public metadata: Metadata + public metadata: Metadata, + public spec: Spec ) { } } class Spec { nodeName?: string; + + constructor ( + public containers: Container[] + ) { } } class Status @@ -21,6 +26,11 @@ class Status phase?: PodStatus } +class Container +{ + +} + enum PodStatus { RUNNING = "Running", @@ -38,7 +48,7 @@ export function calcAge(datetime: string | undefined) dayjs.extend(advancedFormat); if(datetime != null) { - const today = Number(dayjs().format('X')); + const today = Number(dayjs(useSeconds().value).format('X')); const createdAt = Number(dayjs(datetime).format('X')); const dif = today - createdAt; if(dif < 60) diff --git a/classes/ResourceRepo.ts b/classes/ResourceRepo.ts index b21cfd6..52a1b91 100644 --- a/classes/ResourceRepo.ts +++ b/classes/ResourceRepo.ts @@ -1,15 +1,102 @@ import axios from "axios"; +import type { Metadata } from "./Metadata"; -export class ResourceRepo +export interface HasMetadata { + metadata: Metadata +} + +export class ResourceEvent { - private resources: Ref = ref(undefined); - private interval: NodeJS.Timeout | undefined = undefined; + constructor ( + public type: string, + public resources: T[] + ) {} +} - static init() +export class ResourceRepo +{ + private resources: Ref = ref([]); + private interval: NodeJS.Timeout | undefined = undefined; + private websocket: WebSocket | undefined = undefined; + + static init() { return new ResourceRepo(); } + listen(resource: string) + { + const websocket = new WebSocket(StringUtils.format("%s/api/watch/%s/%s", useRuntimeConfig().public.apiWsBase, resource, this.getNamespace())); + websocket.addEventListener('open', () => { + console.info("Opened Websocket."); + }) + websocket.addEventListener("message", (event) => { + const data = JSON.parse(event.data) as ResourceEvent; + console.info(StringUtils.format("[%s] Resource", data.type)); + switch (data.type) + { + case "INIT": + { + this.add(data.resources); + break; + } + case "ADDED": + { + this.add(data.resources); + break; + } + case "MODIFIED": + { + this.update(data.resources); + break; + } + case "DELETED": + { + this.delete(data.resources); + break; + } + } + }); + const interval = setInterval(() => { + console.info("[PING]"); + websocket.send('[PING]'); + }, 5000); + websocket.addEventListener("close", () => { + console.info("Closing websocket."); + clearTimeout(interval); + }); + this.websocket = websocket; + } + + private add(resources: T[]) + { + this.resources.value.push(...resources); + } + + private delete(resources: T[]) + { + for (const resource of resources) + { + const index = this.resources.value.findIndex(item => item.metadata.uid === resource.metadata.uid); + if (index != null) + { + this.resources.value.splice(index, 1); + } + } + } + + private update(resources: T[]) + { + for (const resource of resources) + { + const index = this.resources.value.findIndex(item => item.metadata.uid === resource.metadata.uid); + if (index != null) + { + this.resources.value[index] = resource; + } + } + } + load(resourceType: string) { this.refresh(resourceType); @@ -19,37 +106,35 @@ export class ResourceRepo clear() { clearTimeout(this.interval); + if (this.websocket != null) + { + this.websocket.close(); + } } get() { return computed(() => { - return this.resources.value; + return this.resources.value.toSorted((a, b) => a.metadata.name.localeCompare(b.metadata.name)); }) } private refresh(resourceType: string) { const namespace = this.getNamespace(); - let url = useRuntimeConfig().public.apiBase + '/resources/' + resourceType + let url = StringUtils.format("%s/resources/%s", useRuntimeConfig().public.apiBase, resourceType); if (namespace) { - url = url + "/" + namespace; + url = StringUtils.format("%s/%s", url, namespace); } axios.get(url) .then((response) => { - this.resources.value = undefined; this.resources.value = response.data; }); } private getNamespace() { - const namespace = useRoute().params.namespace as string; - if (namespace !== "_all") - { - return namespace as string; - } - return undefined; + return useRoute().params.namespace as string; } } \ No newline at end of file diff --git a/classes/Secret.ts b/classes/Secret.ts index 8b7b371..0e631c5 100644 --- a/classes/Secret.ts +++ b/classes/Secret.ts @@ -1,6 +1,7 @@ -import type { Metadata } from "./Metadata"; +import { Metadata } from "./Metadata"; +import type { HasMetadata } from "./ResourceRepo"; -export class Secret +export class Secret implements HasMetadata { constructor ( public metadata: Metadata diff --git a/classes/Service.ts b/classes/Service.ts index 990c57c..1bf6ea6 100644 --- a/classes/Service.ts +++ b/classes/Service.ts @@ -1,9 +1,13 @@ import type { Metadata } from "./Metadata"; +import type { HasMetadata } from "./ResourceRepo"; -export class Service +export class Service implements HasMetadata { - metadata?: Metadata; spec?: ServiceSpec; + + constructor ( + public metadata: Metadata + ) {} } export class ServiceSpec diff --git a/components/ConfigMapComponent.vue b/components/ConfigMapComponent.vue new file mode 100644 index 0000000..9bafdd3 --- /dev/null +++ b/components/ConfigMapComponent.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/components/InspectSidebar.vue b/components/InspectSidebar.vue index 37287b1..f4ed0ed 100644 --- a/components/InspectSidebar.vue +++ b/components/InspectSidebar.vue @@ -1,19 +1,24 @@ @@ -22,7 +27,7 @@ import SidebarTemplate from './SidebarTemplate.vue'; import { StringUtils, useNamespaceStore } from '#imports'; -const resources = new Map([["nodes", "Nodes"], ["ingresses", "Ingresses"], ["services", "Services"], ["deployments", "Deployments"], ["stateful-sets", "Stateful Sets"], ["pods", "Pods"], ["secrets", "Secrets"], ["custom-resource-definitions", "CDRs"]]); +const resources = new Map([["nodes", "Nodes"], ["ingresses", "Ingresses"], ["services", "Services"], ["deployments", "Deployments"], ["stateful-sets", "Stateful Sets"], ["pods", "Pods"], ["secrets", "Secrets"], ["config-maps", "Config Maps"], ["custom-resource-definitions", "CDRs"]]); const namespaceStore = useNamespaceStore(); diff --git a/components/LogPopup.vue b/components/LogPopup.vue index 0938911..d81ba8d 100644 --- a/components/LogPopup.vue +++ b/components/LogPopup.vue @@ -8,18 +8,20 @@
+

{{ logs }}

+ + \ No newline at end of file diff --git a/components/SidebarTemplate.vue b/components/SidebarTemplate.vue index 42db482..d4d055b 100644 --- a/components/SidebarTemplate.vue +++ b/components/SidebarTemplate.vue @@ -22,6 +22,7 @@ padding: 0.35rem 0.5rem; border-radius: 0.25rem; cursor: pointer; + white-space: nowrap; } .router-link-active { background-color: var(--primary-color); @@ -31,7 +32,7 @@ height: 1px; background-color: rgb(36, 36, 36); } -.nav > * { +.nav * { display: block; text-decoration: none; } diff --git a/components/inspect/resources/ConfigMapList.vue b/components/inspect/resources/ConfigMapList.vue new file mode 100644 index 0000000..6c74f1a --- /dev/null +++ b/components/inspect/resources/ConfigMapList.vue @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/components/inspect/resources/CustomResourceDefinitionComponent.vue b/components/inspect/resources/CustomResourceDefinitionList.vue similarity index 100% rename from components/inspect/resources/CustomResourceDefinitionComponent.vue rename to components/inspect/resources/CustomResourceDefinitionList.vue diff --git a/components/inspect/resources/DeploymentComponent.vue b/components/inspect/resources/DeploymentList.vue similarity index 100% rename from components/inspect/resources/DeploymentComponent.vue rename to components/inspect/resources/DeploymentList.vue diff --git a/components/inspect/resources/IngressComponent.vue b/components/inspect/resources/IngressList.vue similarity index 100% rename from components/inspect/resources/IngressComponent.vue rename to components/inspect/resources/IngressList.vue diff --git a/components/inspect/resources/NodeComponent.vue b/components/inspect/resources/NodeList.vue similarity index 86% rename from components/inspect/resources/NodeComponent.vue rename to components/inspect/resources/NodeList.vue index 67788eb..6efcce4 100644 --- a/components/inspect/resources/NodeComponent.vue +++ b/components/inspect/resources/NodeList.vue @@ -15,11 +15,11 @@ \ No newline at end of file diff --git a/components/inspect/resources/SecretComponent.vue b/components/inspect/resources/SecretList.vue similarity index 100% rename from components/inspect/resources/SecretComponent.vue rename to components/inspect/resources/SecretList.vue diff --git a/components/inspect/resources/ServiceComponent.vue b/components/inspect/resources/ServiceList.vue similarity index 100% rename from components/inspect/resources/ServiceComponent.vue rename to components/inspect/resources/ServiceList.vue diff --git a/components/pod/PodComponent.vue b/components/pod/PodComponent.vue index bd4cef2..7a19592 100644 --- a/components/pod/PodComponent.vue +++ b/components/pod/PodComponent.vue @@ -2,8 +2,9 @@

{{ pod.metadata.name }}

{{ pod.metadata.namespace }}

-

{{ calcAge(pod.metadata.creationTimestamp) }}

+

{{ calcAge(pod.metadata.creationTimestamp) }}

{{ pod.spec.nodeName }}

+

{{ pod.spec.containers.length }}

@@ -24,7 +25,7 @@ import { calcAge } from '~/classes/Pod'; import { hasAnyRole } from '~/classes/User'; import PodDeletePopup from './view/PodDeletePopup.vue'; -defineProps<{ +const props = defineProps<{ pod: Pod }>(); diff --git a/components/pod/view/PodDeletePopup.vue b/components/pod/view/PodDeletePopup.vue index d1a4b2d..73cdde4 100644 --- a/components/pod/view/PodDeletePopup.vue +++ b/components/pod/view/PodDeletePopup.vue @@ -12,20 +12,32 @@
- Cancel - Delete + Cancel + Delete
\ No newline at end of file diff --git a/pages/account/settings.vue b/pages/account/settings.vue new file mode 100644 index 0000000..df5a717 --- /dev/null +++ b/pages/account/settings.vue @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/pages/account/settings/password.vue b/pages/account/settings/password.vue new file mode 100644 index 0000000..542020b --- /dev/null +++ b/pages/account/settings/password.vue @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/pages/account/settings/users.vue b/pages/account/settings/users.vue new file mode 100644 index 0000000..542020b --- /dev/null +++ b/pages/account/settings/users.vue @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/requests/pod.ts b/requests/pod.ts index 95f9b60..c473f90 100644 --- a/requests/pod.ts +++ b/requests/pod.ts @@ -15,9 +15,9 @@ export function getPods(namespace: string | undefined, onSuccess: (pods: Pod[]) .catch(); } -export function deletePod(id: string | undefined, onSuccess: () => void) +export function deletePod(namespace: string, name: string, onSuccess: () => void) { - axios.delete(useRuntimeConfig().public.apiBase + '/pods/' + id, { + axios.delete(StringUtils.format('%s/pods/%s/%s', useRuntimeConfig().public.apiBase, namespace, name), { headers: { Authorization: "Bearer " + requireToken() } diff --git a/utils/useSeconds.ts b/utils/useSeconds.ts new file mode 100644 index 0000000..72ec7bb --- /dev/null +++ b/utils/useSeconds.ts @@ -0,0 +1,7 @@ +export function useSeconds() { + const seconds = ref(new Date()); + setInterval(() => { + seconds.value = new Date(); + }, 1000) + return seconds; +} \ No newline at end of file