🚚 New Routes for resources

This commit is contained in:
andreas.dinauer 2025-10-30 21:16:28 +01:00
parent 74ebe0b925
commit fe3dcd2165
26 changed files with 317 additions and 276 deletions

View File

@ -0,0 +1,16 @@
import type { Metadata } from "./Metadata";
export class CustomResourceDefinition
{
constructor (
public metadata: Metadata,
public spec: Spec
) { }
}
export class Spec
{
constructor (
public group: string
) { }
}

View File

@ -2,5 +2,7 @@ import type { Metadata } from "./Metadata";
export class Namespace export class Namespace
{ {
metadata?: Metadata; constructor (
public metadata: Metadata
) { }
} }

55
classes/ResourceRepo.ts Normal file
View File

@ -0,0 +1,55 @@
import axios from "axios";
export class ResourceRepo<T>
{
private resources: Ref<T[] | undefined> = ref(undefined);
private interval: NodeJS.Timeout | undefined = undefined;
static init<T>()
{
return new ResourceRepo<T>();
}
load(resourceType: string)
{
this.refresh(resourceType);
return this;
}
clear()
{
clearTimeout(this.interval);
}
get()
{
return computed(() => {
return this.resources.value;
})
}
private refresh(resourceType: string)
{
const namespace = this.getNamespace();
let url = useRuntimeConfig().public.apiBase + '/resources/' + resourceType
if (namespace)
{
url = url + "/" + namespace;
}
axios.get<T[]>(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;
}
}

View File

@ -3,20 +3,12 @@
<div class="content-l"> <div class="content-l">
<h2>Kubooboo</h2> <h2>Kubooboo</h2>
<div class="nav"> <div class="nav">
<NuxtLink class="resources" to="/account/inspect/nodes">Nodes</NuxtLink> <NuxtLink class="resources" v-for="[key, value] of resources" :to="'/account/inspect/' + key + '/_all'" :class="{ 'router-link-active': useRoute().params.resource === key }">{{ value }}</NuxtLink>
<NuxtLink class="resources" to="/account/inspect/ingresses">Ingresses</NuxtLink>
<NuxtLink class="resources" to="/account/inspect/services">Services</NuxtLink>
<NuxtLink class="resources" to="/account/inspect/deployments">Deployments</NuxtLink>
<NuxtLink class="resources" to="/account/inspect/pods">Pods</NuxtLink>
</div> </div>
<div class="divider" :class="{ hide: !inNamespaceScopedResource }"></div> <div class="divider" :class="{ hide: !inNamespaceScopedResource }"></div>
<div :class="{ hide: !inNamespaceScopedResource }"> <div class="nav" :class="{ hide: !inNamespaceScopedResource }">
<div class="namespace" :class="{ active: currentNamespace === undefined }" @click="() => namespaceStore.selectNamespace(undefined)"> <NuxtLink class="namespace" :to="base + '/_all'">Alle</NuxtLink>
<p>Alle</p> <NuxtLink v-for="namespace in namespaces" class="namespace" :to="base + '/' + namespace.metadata.name">{{ namespace.metadata.name }}</NuxtLink>
</div>
<div class="namespace" v-for="namespace in namespaces" @click="() => namespaceStore.selectNamespace(namespace)" :class="{ active: namespace.metadata?.name === currentNamespace?.metadata?.name }">
<p v-if="namespace.metadata">{{ namespace.metadata.name }}</p>
</div>
</div> </div>
</div> </div>
<div class="left-center" v-if="user" @click="() => accountPopup.open()"> <div class="left-center" v-if="user" @click="() => accountPopup.open()">
@ -31,6 +23,8 @@ import SidebarTemplate from './SidebarTemplate.vue';
import { useNamespaceStore } from '#imports'; import { useNamespaceStore } from '#imports';
const resources = new Map<string, string>([["nodes", "Nodes"], ["ingresses", "Ingresses"], ["services", "Services"], ["deployments", "Deployments"], ["pods", "Pods"], ["custom-resource-definitions", "CDRs"]]);
const namespaceStore = useNamespaceStore(); const namespaceStore = useNamespaceStore();
const namespaces = computed(namespaceStore.getNamespaces); const namespaces = computed(namespaceStore.getNamespaces);
@ -41,8 +35,18 @@ const user = getUser();
const accountPopup = ref(); const accountPopup = ref();
const base = computed(() => {
const resource = useRoute().params.resource as string;
if (resource)
{
return '/account/inspect/' + resource;
}
throw new Error();
})
const inNamespaceScopedResource: ComputedRef<boolean> = computed(() => { const inNamespaceScopedResource: ComputedRef<boolean> = computed(() => {
if(useRoute().fullPath.startsWith('/account/inspect/nodes')) const resource = useRoute().params.resource as string;
if(['custom-resource-definitions', 'nodes'].includes(resource))
{ {
return false; return false;
} }

View File

@ -35,5 +35,7 @@ defineExpose({
width: 100%; width: 100%;
position: absolute; position: absolute;
min-height: 100%; min-height: 100%;
display: grid;
grid-template-rows: 1fr;
} }
</style> </style>

View File

@ -24,10 +24,8 @@
border-radius: 0.25rem; border-radius: 0.25rem;
cursor: pointer; cursor: pointer;
} }
.namespace.active, .resources.router-link-active { .router-link-active {
background-color: var(--primary-color) background-color: var(--primary-color);
}
.namespace.active *, .resources.router-link-active {
color: white; color: white;
} }
.divider { .divider {

View File

@ -0,0 +1,32 @@
<template>
<div class="table">
<slot v-if="!loading"></slot>
<div class="loading center" v-if="loading">
<UiLoadingIcon></UiLoadingIcon>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
loading: boolean
}>();
</script>
<style scoped>
.table {
height: 100%;
width: 100%;
display: grid;
}
.loading {
height: 100%;
width: 100%;
}
</style>
<style>
.grid-element {
height: 2.25rem;
}
</style>

View File

@ -14,7 +14,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Deployment } from '~/classes/Deployment'; import { Deployment } from '~/classes/Deployment';
import { rescaleDeployment } from '~/requests/deployments';
import RescaleDeploymentPopup from '../RescaleDeploymentPopup.vue'; import RescaleDeploymentPopup from '../RescaleDeploymentPopup.vue';
defineProps<{ defineProps<{

View File

@ -0,0 +1,27 @@
<template>
<TableComponent :loading="customResourceDefinitions == null">
<div class="resource-container crd-container">
<div class="header">
<p>Name</p>
<p>Gruppe</p>
</div>
<div v-for="customResourceDefinition, index in customResourceDefinitions" class="resource" :class="{ even: index % 2 }">
<p class="grid-element">{{ customResourceDefinition.metadata.name }}</p>
<p class="grid-element">{{ customResourceDefinition.spec.group }}</p>
</div>
</div>
</TableComponent>
</template>
<script setup lang="ts">
import type { CustomResourceDefinition } from '~/classes/CustomResourceDefinition';
import { ResourceRepo } from '~/classes/ResourceRepo';
const customResourceDefinitions = ResourceRepo.init<CustomResourceDefinition>().load('custom-resource-definitions').get();
</script>
<style scoped>
.crd-container {
grid-template-columns: 1fr 1fr;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<TableComponent :loading="deployments == null">
<div class="resource-container deployment-container">
<div class="header">
<p>Name</p>
<p>Namespace</p>
<p>Replicas</p>
<p>Aktionen</p>
</div>
<DeploymentComponent :deployment="deployment" v-for="deployment, index in deployments" class="resource" :class="{ even: index % 2 }"></DeploymentComponent>
</div>
</TableComponent>
</template>
<script setup lang="ts">
import type { Deployment } from '~/classes/Deployment';
import { ResourceRepo } from '~/classes/ResourceRepo';
import DeploymentComponent from '~/components/deployments/DeploymentComponent.vue';
const deployments = ResourceRepo.init<Deployment>().load('deployments').get();
</script>
<style scoped>
.deployment-container {
grid-template-columns: auto 1fr 1fr auto;
}
</style>

View File

@ -0,0 +1,24 @@
<template>
<TableComponent :loading="ingresses == null">
<div class="resource-container ingress-container">
<div class="header">
<p>Name</p>
<p>Namespace</p>
</div>
<IngressComponent :ingress="ingress" v-for="ingress, index in ingresses" class="resource" :class="{ even: index % 2 }"></IngressComponent>
</div>
</TableComponent>
</template>
<script setup lang="ts">
import type { Ingress } from '~/classes/Ingress';
import { ResourceRepo } from '~/classes/ResourceRepo';
const ingresses = ResourceRepo.init<Ingress>().load('ingresses').get();
</script>
<style scoped>
.ingress-container {
grid-template-columns: 1fr 1fr;
}
</style>

View File

@ -0,0 +1,28 @@
<template>
<TableComponent :loading="node == null">
<div class="resource-container node-container">
<div class="header">
<p>Name</p>
<p>Alter</p>
<p>Status</p>
<p>CPU</p>
<p>RAM</p>
</div>
<NodeComponent :node-stats="ns" v-for="ns, index in node" class="resource" :class="{ even: index % 2 }"></NodeComponent>
</div>
</TableComponent>
</template>
<script setup lang="ts">
import type { NodeStats } from '~/classes/Node';
import { ResourceRepo } from '~/classes/ResourceRepo';
import NodeComponent from '~/components/NodeComponent.vue';
const node = ResourceRepo.init<NodeStats>().load('nodes').get();
</script>
<style>
.node-container {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<TableComponent :loading="pods == null">
<div class="resource-container pod-container">
<div class="header">
<p>Pod</p>
<p>Namespace</p>
<p>Alter</p>
<p>Node</p>
<p>Status</p>
<p>Aktionen</p>
</div>
<PodComponent v-for="pod, index in pods" :pod="pod" class="resource" :class="{ even: index % 2 }"></PodComponent>
</div>
</TableComponent>
</template>
<script setup lang="ts">
import type { Pod } from '~/classes/Pod';
import { ResourceRepo } from '~/classes/ResourceRepo';
import PodComponent from '~/components/pod/PodComponent.vue';
const pods = ResourceRepo.init<Pod>().load('pods').get();
</script>

View File

@ -0,0 +1,26 @@
<template>
<TableComponent :loading="services == null">
<div class="resource-container service-container">
<div class="header">
<p>Service</p>
<p>Namespace</p>
<p>Type</p>
<p>Aktionen</p>
</div>
<ServiceComponent :service="service" v-for="service, index in services" class="resource" :class="{ even: index % 2 }"></ServiceComponent>
</div>
</TableComponent>
</template>
<script setup lang="ts">
import { ResourceRepo } from '~/classes/ResourceRepo';
import type { Service } from '~/classes/Service';
const services = ResourceRepo.init<Service>().load('services').get();
</script>
<style>
.service-container {
grid-template-columns: auto auto 1fr auto;
}
</style>

View File

@ -9,6 +9,7 @@ import UiIcon from './UiIcon.vue';
<style scoped> <style scoped>
.rotate{ .rotate{
animation: rotate 1s linear infinite; animation: rotate 1s linear infinite;
user-select: none;
} }
@keyframes rotate{ @keyframes rotate{
to{ transform: rotate(360deg); } to{ transform: rotate(360deg); }

View File

@ -0,0 +1,10 @@
<template>
<NuxtPage></NuxtPage>
</template>
<style>
.action-buttons {
display: flex;
gap: 0.25rem;
}
</style>

View File

@ -0,0 +1,20 @@
<template>
<PodComponent v-if="resource === 'pods'"></PodComponent>
<CustomResourceDefinitionComponent v-else-if="resource === 'custom-resource-definitions'"></CustomResourceDefinitionComponent>
<IngressComponent v-else-if="resource === 'ingresses'"></IngressComponent>
<ServiceComponent v-else-if="resource === 'services'"></ServiceComponent>
<DeploymentComponent v-else-if="resource === 'deployments'"></DeploymentComponent>
<NodeComponent v-else-if="resource === 'nodes'"></NodeComponent>
<p v-else>Invalid resource</p>
</template>
<script setup lang="ts">
import PodComponent from '~/components/inspect/resources/PodComponent.vue';
import CustomResourceDefinitionComponent from '~/components/inspect/resources/CustomResourceDefinitionComponent.vue';
import IngressComponent from '~/components/inspect/resources/IngressComponent.vue';
import ServiceComponent from '~/components/inspect/resources/ServiceComponent.vue';
import DeploymentComponent from '~/components/inspect/resources/DeploymentComponent.vue';
import NodeComponent from '~/components/inspect/resources/NodeComponent.vue';
const resource = useRoute().params.resource as string;
</script>

View File

@ -1,50 +0,0 @@
<template>
<div class="resource-container deployment-container">
<div class="header">
<p>Name</p>
<p>Namespace</p>
<p>Replicas</p>
<p>Aktionen</p>
</div>
<DeploymentComponent :deployment="deployment" v-for="deployment, index in deployments" class="resource" :class="{ even: index % 2 }"></DeploymentComponent>
</div>
</template>
<script setup lang="ts">
import { Deployment } from '~/classes/Deployment';
import { useNamespaceStore } from '#imports';
import type { Namespace } from '~/classes/Namespace';
import { getDeployments } from '~/requests/deployments';
import DeploymentComponent from '~/components/deployments/DeploymentComponent.vue';
const deployments: Ref<Deployment[] | undefined> = ref(undefined);
const namespace = computed(useNamespaceStore().getCurrentNamespace);
let interval: NodeJS.Timeout | undefined = undefined;
onMounted(() => {
watch(namespace, (newNamespace) => {
loadDeployments(newNamespace);
clearInterval(interval);
interval = setInterval(() => {
loadDeployments(newNamespace);
}, 10000);
}, { immediate: true })
})
onUnmounted(() => {
clearInterval(interval);
});
function loadDeployments(namespace?: Namespace)
{
getDeployments(namespace?.metadata?.name, (_deployments: Deployment[]) => {
deployments.value = _deployments;
});
}
</script>
<style scoped>
.deployment-container {
grid-template-columns: 1fr 1fr 1fr auto;
}
</style>

View File

@ -1,33 +0,0 @@
<template>
<div class="resource-container ingress-container">
<div class="header">
<p>Name</p>
<p>Namespace</p>
</div>
<IngressComponent :ingress="ingress" v-for="ingress, index in ingresses" class="resource" :class="{ even: index % 2 }"></IngressComponent>
</div>
</template>
<script setup lang="ts">
import type { Ingress } from '~/classes/Ingress';
import IngressComponent from '~/components/IngressComponent.vue';
import { getIngresses } from '~/requests/ingresses';
const ingresses: Ref<Ingress[] | undefined> = ref(undefined);
const namespace = computed(useNamespaceStore().getCurrentNamespace);
onMounted(() => {
watch(namespace, (newNamespace) => {
getIngresses(newNamespace?.metadata?.name, (_ingresses: Ingress[]) => {
ingresses.value = _ingresses;
});
}, { immediate: true })
})
</script>
<style scoped>
.ingress-container {
grid-template-columns: 1fr 1fr;
}
</style>

View File

@ -1,47 +0,0 @@
<template>
<div class="resource-container node-container">
<div class="header">
<p>Name</p>
<p>Alter</p>
<p>Status</p>
<p>CPU</p>
<p>RAM</p>
</div>
<NodeComponent :node-stats="ns" v-for="ns, index in nodeStats" class="resource" :class="{ even: index % 2 }"></NodeComponent>
</div>
</template>
<script setup lang="ts">
import { NodeStats } from '~/classes/Node';
import { getNodes } from '~/requests/nodes';
const nodeStats: Ref<NodeStats[] | undefined> = ref(undefined);
let interval: NodeJS.Timeout | undefined = undefined;
onMounted(() => {
loadNodes();
interval = setInterval(() => {
loadNodes();
}, 10000)
})
onUnmounted(() => {
clearInterval(interval);
});
function loadNodes()
{
getNodes((_nodes: NodeStats[]) => {
nodeStats.value = _nodes;
});
}
</script>
<style>
.node-container {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
}
.action-buttons {
display: flex;
gap: 0.5rem;
}
</style>

View File

@ -1,55 +0,0 @@
<template>
<div class="resource-container pod-container">
<div class="header">
<p>Pod</p>
<p>Namespace</p>
<p>Alter</p>
<p>Node</p>
<p>Status</p>
<p>Aktionen</p>
</div>
<PodComponent v-for="pod, index in pods" :pod="pod" class="resource" :class="{ even: index % 2 }"></PodComponent>
</div>
</template>
<script setup lang="ts">
import { type Pod } from '~/classes/Pod';
import { getPods } from '~/requests/pod';
import { useNamespaceStore } from '#imports';
import type { Namespace } from '~/classes/Namespace';
const pods: Ref<Pod[] | undefined> = ref(undefined);
const namespace = computed(useNamespaceStore().getCurrentNamespace);
let interval: NodeJS.Timeout | undefined = undefined;
onMounted(() => {
watch(namespace, (newNamespace) => {
loadPods(newNamespace);
clearInterval(interval);
interval = setInterval(() => {
loadPods(newNamespace);
}, 10000);
}, { immediate: true })
})
onUnmounted(() => {
clearInterval(interval);
});
function loadPods(namespace?: Namespace)
{
getPods(namespace?.metadata?.name, (_pods: Pod[]) => {
pods.value = _pods;
});
}
</script>
<style>
.pod-container {
grid-template-columns: auto auto auto 1fr auto auto;
}
.action-buttons {
display: flex;
gap: 0.5rem;
}
</style>

View File

@ -1,53 +0,0 @@
<template>
<div class="resource-container service-container">
<div class="header">
<p>Service</p>
<p>Namespace</p>
<p>Type</p>
<p>Aktionen</p>
</div>
<ServiceComponent :service="service" v-for="service, index in services" class="resource" :class="{ even: index % 2 }"></ServiceComponent>
</div>
</template>
<script setup lang="ts">
import { useNamespaceStore } from '#imports';
import type { Namespace } from '~/classes/Namespace';
import type { Service } from '~/classes/Service';
import { getServices } from '~/requests/services';
const services: Ref<Service[] | undefined> = ref(undefined);
const namespace = computed(useNamespaceStore().getCurrentNamespace);
let interval: NodeJS.Timeout | undefined = undefined;
onMounted(() => {
watch(namespace, (newNamespace) => {
loadServices(newNamespace);
clearInterval(interval);
interval = setInterval(() => {
loadServices(newNamespace);
}, 10000);
}, { immediate: true })
})
onUnmounted(() => {
clearInterval(interval);
})
function loadServices(namespace?: Namespace)
{
getServices(namespace?.metadata?.name, (_services: Service[]) => {
services.value = _services;
});
}
</script>
<style>
.service-container {
grid-template-columns: auto auto 1fr auto;
}
.action-buttons {
display: flex;
gap: 0.5rem;
}
</style>

View File

@ -1,14 +0,0 @@
<template>
<p>{{ namespace }}</p>
</template>
<script setup lang="ts">
const namespace = computed(() => {
const namespace = useRoute().params.namespace;
if (namespace)
{
return namespace;
}
return "Alle";
});
</script>

View File

@ -1,3 +0,0 @@
<template>
</template>

View File

@ -42,7 +42,7 @@ function doLogin()
const decode = jwtDecode(token) as any; const decode = jwtDecode(token) as any;
getUser(decode.upn, token, (user: User) => { getUser(decode.upn, token, (user: User) => {
setSessionCookie(new Session(user, token), decode.exp as number); setSessionCookie(new Session(user, token), decode.exp as number);
useRouter().push('/account/inspect'); useRouter().push('/account/inspect/nodes/_all');
}); });
}); });
} }

View File

@ -8,7 +8,9 @@ export const useNamespaceStore = defineStore('namespace', {
}), }),
getters: { getters: {
getNamespaces: (state) => { getNamespaces: (state) => {
return (): Namespace[] | undefined => state.namespaces; return (): Namespace[] | undefined => {
return state.namespaces;
}
}, },
getCurrentNamespace: (state) => { getCurrentNamespace: (state) => {
return (): Namespace | undefined => state.currentNamespace; return (): Namespace | undefined => state.currentNamespace;