Add secrets and view popups

This commit is contained in:
andreas.dinauer 2025-11-01 16:58:35 +01:00
parent fe3dcd2165
commit ee5ed78d35
32 changed files with 604 additions and 54 deletions

View File

@ -16,10 +16,14 @@ guard(useRoute().fullPath);
function guard(route: string) function guard(route: string)
{ {
if(route.startsWith('/dashboard') && getToken() == null) if(route.startsWith('/account') && getToken() == null)
{ {
useRouter().push('/'); useRouter().push('/');
} }
if(route === '/' && getToken() != null)
{
useRouter().push('/account/inspect/nodes/_all');
}
} }
useHead({ useHead({

View File

@ -237,3 +237,8 @@
height: 2.5rem; height: 2.5rem;
padding: 0.5rem; padding: 0.5rem;
} }
.width-6rem {
width: 6rem;
}

View File

@ -11,8 +11,10 @@
html { html {
--primary-color: rgb(87, 75, 255); --primary-color: rgb(87, 75, 255);
--tile-color: rgb(255, 255, 255); --tile-color: #ebf1ff;
background-color: rgb(240, 240, 240); background-color: rgb(255, 255, 255);
--shade-light: #f3f3f3;
--shade-dark: rgb(233, 233, 233);
} }
html * { html * {
@ -28,26 +30,36 @@ html, body, #__nuxt {
display: grid; display: grid;
grid-template-columns: auto auto auto 1fr auto auto; grid-template-columns: auto auto auto 1fr auto auto;
align-content: start; align-content: start;
background-color: rgb(247, 247, 247); background-color: rgb(255, 255, 255);
} }
.resource, .header { .resource, .header {
display: contents; display: contents;
} }
.header > * { .header > * {
padding: 0.75rem; padding: 0.75rem;
background-color: rgb(12, 12, 12); background-color: var(--tile-color);
color: white; color: rgb(0, 0, 0);
font-weight: bold; font-weight: bold;
position: sticky; position: sticky;
top: 0; top: 0;
margin-bottom: 1rem;
}
.header *:first-of-type {
border-top-left-radius: 0rem;
border-bottom-left-radius: 0rem;
}
.header *:last-of-type {
border-top-right-radius: 0rem;
border-bottom-right-radius: 0rem;
} }
.resource > .grid-element { .resource > .grid-element {
padding: 0.25rem 0.75rem; padding: 0.25rem 0.75rem;
display: flex; display: flex;
align-items: center; align-items: center;
background-color: #f3f3f3;
} }
.resource:hover > .grid-element { .resource:hover > .grid-element {
background-color: #c2c2c2; background-color: #dbdbdb;
} }
.resource p { .resource p {
font-weight: 600; font-weight: 600;

View File

@ -2,5 +2,63 @@ import type { Metadata } from "./Metadata";
export class Ingress export class Ingress
{ {
metadata?: Metadata; constructor (
public metadata: Metadata,
public spec: IngressSpec
) { }
}
export class IngressSpec
{
constructor (
public ingressClassName: string,
public rules: IngressRule[],
public tls: IngressTLS[]
) { }
}
export class IngressRule
{
constructor (
public host: string,
public http: IngressRuleHttp
) { }
}
export class IngressRuleHttp
{
constructor (
public paths: IngressRulePath[]
) { }
}
export class IngressRulePath
{
constructor (
public path: string,
public pathType: string,
public backend: IngressRuleBackend
) { }
}
export class IngressRuleBackend
{
constructor (
public service: IngressRuleBackendService
) { }
}
export class IngressRuleBackendService
{
constructor (
public name: string
) { }
}
export class IngressTLS
{
constructor (
public hosts: string[],
public secretName: string
) {}
} }

View File

@ -1,7 +1,10 @@
export class Metadata export class Metadata
{ {
name?: string;
namespace?: string;
creationTimestamp?: string; creationTimestamp?: string;
uid?: string; uid?: string;
constructor (
public namespace: string,
public name: string
) { }
} }

8
classes/Secret.ts Normal file
View File

@ -0,0 +1,8 @@
import type { Metadata } from "./Metadata";
export class Secret
{
constructor (
public metadata: Metadata
) { }
}

View File

@ -1,14 +0,0 @@
<template>
<div>
<p class="grid-element" v-if="ingress.metadata">{{ ingress.metadata.name }}</p>
<p class="grid-element" v-if="ingress.metadata">{{ ingress.metadata.namespace }}</p>
</div>
</template>
<script setup lang="ts">
import type { Ingress } from '~/classes/Ingress';
defineProps<{
ingress: Ingress
}>();
</script>

View File

@ -3,12 +3,11 @@
<div class="content-l"> <div class="content-l">
<h2>Kubooboo</h2> <h2>Kubooboo</h2>
<div class="nav"> <div class="nav">
<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" v-for="[key, value] of resources" :to="getRoute(key, namespace)" :class="{ 'router-link-active': useRoute().params.resource === key }">{{ value }}</NuxtLink>
</div> </div>
<div class="divider" :class="{ hide: !inNamespaceScopedResource }"></div> <div class="divider" :class="{ hide: !inNamespaceScopedResource }"></div>
<div class="nav" :class="{ hide: !inNamespaceScopedResource }"> <div class="nav" :class="{ hide: !inNamespaceScopedResource }">
<NuxtLink class="namespace" :to="base + '/_all'">Alle</NuxtLink> <NuxtLink v-for="[key, value] in namespaces" class="namespace" :to="getRoute(resource, key)">{{ value }}</NuxtLink>
<NuxtLink v-for="namespace in namespaces" class="namespace" :to="base + '/' + namespace.metadata.name">{{ namespace.metadata.name }}</NuxtLink>
</div> </div>
</div> </div>
<div class="left-center" v-if="user" @click="() => accountPopup.open()"> <div class="left-center" v-if="user" @click="() => accountPopup.open()">
@ -21,20 +20,30 @@
<script setup lang="ts"> <script setup lang="ts">
import SidebarTemplate from './SidebarTemplate.vue'; import SidebarTemplate from './SidebarTemplate.vue';
import { useNamespaceStore } from '#imports'; import { StringUtils, useNamespaceStore } from '#imports';
const resources = new Map<string, string>([["nodes", "Nodes"], ["ingresses", "Ingresses"], ["services", "Services"], ["deployments", "Deployments"], ["pods", "Pods"], ["custom-resource-definitions", "CDRs"]]); const resources = new Map<string, string>([["nodes", "Nodes"], ["ingresses", "Ingresses"], ["services", "Services"], ["deployments", "Deployments"], ["stateful-sets", "Stateful Sets"], ["pods", "Pods"], ["secrets", "Secrets"], ["custom-resource-definitions", "CDRs"]]);
const namespaceStore = useNamespaceStore(); const namespaceStore = useNamespaceStore();
const namespaces = computed(namespaceStore.getNamespaces); const namespaces = computed(() => namespaceStore.getNamespaceNames(true));
const currentNamespace = computed(namespaceStore.getCurrentNamespace)
const user = getUser(); const user = getUser();
const accountPopup = ref(); const accountPopup = ref();
const resource = computed(() => {
return useRoute().params.resource as string;
});
const namespace = computed(() => {
return useRoute().params.namespace as string;
});
function getRoute(resource: string, namespace: string) {
return StringUtils.format("/account/inspect/%s/%s", resource, namespace)
}
const base = computed(() => { const base = computed(() => {
const resource = useRoute().params.resource as string; const resource = useRoute().params.resource as string;
if (resource) if (resource)

View File

@ -1,20 +1,25 @@
<template> <template>
<PopupTemplate :heading="'Logs: ' + pod.metadata?.namespace + '/' + pod.metadata?.name" ref="base"> <PopupTemplate :heading="'Logs: ' + pod.metadata?.namespace + '/' + pod.metadata?.name" ref="base">
<ScrollComponent ref="scrollComponent" v-show="logs && logs.length > 0"> <ScrollComponent ref="scrollComponent" v-show="logs">
<div class="console"> <div class="console" v-if="logs">
<p class="log" v-for="log, index in logs" :class="{ even: index % 2 }">{{ log }}</p> <p class="log" v-for="log, index in logs" :class="{ even: index % 2 }"><span>[{{ dayjs(log.timestamp).format('YYYY-MM-DD HH:mm') }}]</span><span>{{ log.message }}</span></p>
</div> </div>
</ScrollComponent> </ScrollComponent>
<div v-if="logs == null" class="center" style="height: 100%; width: 100%">
<UiLoadingIcon size="2rem"></UiLoadingIcon>
</div>
</PopupTemplate> </PopupTemplate>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs';
import type { Pod } from '~/classes/Pod'; import type { Pod } from '~/classes/Pod';
import { getLogs } from '~/requests/logs'; import { getLogs } from '~/requests/logs';
import { Log } from '~/requests/logs';
const base = ref(); const base = ref();
const logs: Ref<string[] | undefined> = ref(undefined); const logs: Ref<Log[] | undefined> = ref(undefined);
const props = defineProps<{ const props = defineProps<{
pod: Pod pod: Pod
@ -26,7 +31,7 @@ function open()
{ {
logs.value = undefined; logs.value = undefined;
base.value.open(); base.value.open();
getLogs(props.pod.metadata?.uid, (_logs: string[]) => { getLogs(props.pod.metadata?.uid, (_logs: Log[]) => {
logs.value = _logs; logs.value = _logs;
scrollComponent.value.scrollToBottom(); scrollComponent.value.scrollToBottom();
}); });
@ -39,19 +44,27 @@ defineExpose({
<style scoped> <style scoped>
.log { .log {
display: grid;
grid-template-columns: auto 1fr;
padding: 0.25rem 1rem;
gap: 1rem;
}
.log * {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
word-break: break-all; word-break: break-all;
white-space: pre-wrap; white-space: pre-wrap;
color: white; color: white;
padding: 0.25rem 1rem; line-height: 1.15rem;
line-height: 1.0.5rem; font-weight: 600;
} }
.console { .console {
background-color: black; background-color: black;
border-radius: 0.5rem; border-radius: 0.5rem;
display: grid; display: grid;
padding: 1rem 0; padding: 1rem 0;
align-content: start;
} }
.even { .even {
background-color: rgb(24, 24, 24); background-color: rgb(24, 24, 24);

View File

@ -0,0 +1,17 @@
<template>
<div>
<UiInput>
<select name="" id="">
<option :value="key" v-for="[key, value] in namespaces">{{ value }}</option>
</select>
</UiInput>
</div>
</template>
<script setup lang="ts">
const namespaces = computed(() => useNamespaceStore().getNamespaceNames(false));
</script>
<style scoped>
</style>

View File

@ -2,7 +2,7 @@
<div class="table"> <div class="table">
<slot v-if="!loading"></slot> <slot v-if="!loading"></slot>
<div class="loading center" v-if="loading"> <div class="loading center" v-if="loading">
<UiLoadingIcon></UiLoadingIcon> <UiLoadingIcon size="2.5rem"></UiLoadingIcon>
</div> </div>
</div> </div>
</template> </template>

View File

@ -0,0 +1,23 @@
<template>
<div @click="() => viewIngressPopup.open()">
<p class="grid-element">{{ ingress.metadata.name }}</p>
<p class="grid-element">{{ ingress.metadata.namespace }}</p>
<p class="grid-element">{{ ingress.spec.ingressClassName }}</p>
<p class="grid-element">{{ ingress.spec.rules.length }}</p>
<div class="grid-element action-buttons">
<ActionButton>delete</ActionButton>
</div>
<IngressViewPopup :ingress="ingress" ref="viewIngressPopup"></IngressViewPopup>
</div>
</template>
<script setup lang="ts">
import type { Ingress } from '~/classes/Ingress';
import IngressViewPopup from './view/IngressViewPopup.vue';
defineProps<{
ingress: Ingress
}>();
const viewIngressPopup = ref();
</script>

View File

@ -0,0 +1,47 @@
<template>
<PopupTemplate ref="base" :heading="ingress.metadata?.name + '/' + ingress.metadata?.namespace">
<div class="col-2">
<div class="content-l">
<h2>General</h2>
<div class="content-m">
<h3>Ingress Class Name</h3>
<p class="tile-m">{{ ingress.spec.ingressClassName }}</p>
</div>
<h2>TLS</h2>
<div class="content-m" v-for="tls in ingress.spec.tls">
<h3>Host(s): {{ tls.hosts.join(", ") }}</h3>
<p class="tile-m">Secret Name: {{ tls.secretName }}</p>
</div>
</div>
<div class="content-l">
<h2>Routes</h2>
<div class="content-m" v-for="rule in ingress.spec.rules">
<h3>Host: {{ rule.host }}</h3>
<div class="tile-m" v-for="path in rule.http.paths">
<p>[{{ path.pathType }}] {{ path.path }}</p>
</div>
</div>
</div>
</div>
</PopupTemplate>
</template>
<script setup lang="ts">
import type { Ingress } from '~/classes/Ingress';
import PopupTemplate from '~/components/popup/PopupTemplate.vue';
const base = ref();
defineProps<{
ingress: Ingress
}>();
function open()
{
base.value.open();
}
defineExpose({
open
});
</script>

View File

@ -4,6 +4,9 @@
<div class="header"> <div class="header">
<p>Name</p> <p>Name</p>
<p>Namespace</p> <p>Namespace</p>
<p>Ingress Class Name</p>
<p>Rules</p>
<p>Actions</p>
</div> </div>
<IngressComponent :ingress="ingress" v-for="ingress, index in ingresses" class="resource" :class="{ even: index % 2 }"></IngressComponent> <IngressComponent :ingress="ingress" v-for="ingress, index in ingresses" class="resource" :class="{ even: index % 2 }"></IngressComponent>
</div> </div>
@ -19,6 +22,6 @@ const ingresses = ResourceRepo.init<Ingress>().load('ingresses').get();
<style scoped> <style scoped>
.ingress-container { .ingress-container {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr 1fr 1fr auto;
} }
</style> </style>

View File

@ -0,0 +1,33 @@
<template>
<div class="resource-container secret-container">
<div class="header">
<div class="left-center">
<p>Name</p>
<UiButton icon="add" class="extra-small" reverse @click="() => secretAddComponent.open()">Add</UiButton>
</div>
<p>Namespace</p>
<p>Alter</p>
<p>Aktionen</p>
</div>
<SecretComponent v-for="secret, index in secrets" :secret="secret" class="resource" :class="{ even: index % 2 }"></SecretComponent>
<SecretAddComponent ref="secretAddComponent"></SecretAddComponent>
</div>
</template>
<script setup lang="ts">
import { ResourceRepo } from '~/classes/ResourceRepo';
import type { Secret } from '~/classes/Secret';
import SecretComponent from '~/components/secrets/SecretComponent.vue';
import SecretAddComponent from '~/components/secrets/SecretAddComponent.vue';
const secrets = ResourceRepo.init<Secret>().load("secrets").get();
const secretAddComponent = ref();
</script>
<style scoped>
.secret-container {
display: grid;
grid-template-columns: auto auto 1fr auto;
}
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div @click="() => podViewPopup.open()">
<p class="grid-element" v-if="pod.metadata">{{ pod.metadata.name }}</p> <p class="grid-element" v-if="pod.metadata">{{ pod.metadata.name }}</p>
<p class="grid-element" v-if="pod.metadata">{{ pod.metadata.namespace }}</p> <p class="grid-element" v-if="pod.metadata">{{ pod.metadata.namespace }}</p>
<p class="grid-element" v-if="pod.metadata">{{ calcAge(pod.metadata.creationTimestamp) }}</p> <p class="grid-element" v-if="pod.metadata">{{ calcAge(pod.metadata.creationTimestamp) }}</p>
@ -13,6 +13,7 @@
<ActionButton @click="() => deletePod(pod.metadata?.uid, () => {})" v-if="hasAnyRole(getUser(), ['admin', 'maintainer'])">delete</ActionButton> <ActionButton @click="() => deletePod(pod.metadata?.uid, () => {})" v-if="hasAnyRole(getUser(), ['admin', 'maintainer'])">delete</ActionButton>
</div> </div>
<LogPopup :pod="pod" ref="logPopup"></LogPopup> <LogPopup :pod="pod" ref="logPopup"></LogPopup>
<PodViewPopup :pod="pod" ref="podViewPopup"></PodViewPopup>
</div> </div>
</template> </template>
@ -27,4 +28,6 @@ defineProps<{
}>(); }>();
const logPopup = ref(); const logPopup = ref();
const podViewPopup = ref();
</script> </script>

18
components/pod/env/EnvVar.ts vendored Normal file
View File

@ -0,0 +1,18 @@
import axios from "axios";
export class EnvVar
{
constructor (
public key: string,
public value: string
) {}
static get(namespace: string, name: string, onSuccess: (envVars: EnvVar[]) => void)
{
const url = StringUtils.format('%s/pods/%s/%s/env', useRuntimeConfig().public.apiBase, namespace, name);
axios.get<EnvVar[]>(url)
.then((response) => {
onSuccess(response.data);
})
}
}

View File

@ -0,0 +1,90 @@
<template>
<PopupTemplate ref="base" :heading="StringUtils.format('%s/%s', pod.metadata.namespace, pod.metadata.name)">
<div class="col-2 expand">
<div class="content-l">
<h2>General</h2>
<div class="content-m">
<h3>Pod</h3>
<p class="tile-m">{{ StringUtils.format('%s/%s', pod.metadata.namespace, pod.metadata.name) }}</p>
</div>
<div class="content-m">
<h3>Age</h3>
<p class="tile-m">{{ calcAge(pod.metadata.creationTimestamp) }}</p>
</div>
<div class="content-m">
<h3>Status</h3>
<div class="left-center">
<PhaseComponent :phase="pod.status?.phase"></PhaseComponent>
</div>
</div>
</div>
<div class="content-l">
<h2>Env</h2>
<UiInput label="Filter">
<input type="text" name="" id="" v-model="filter">
</UiInput>
<ScrollComponent>
<div>
<p class="env" v-for="env, index in filteredEnv">{{ env.key }} = {{ env.value }}</p>
</div>
</ScrollComponent>
</div>
</div>
</PopupTemplate>
</template>
<script setup lang="ts">
import { calcAge, type Pod } from '~/classes/Pod';
import PhaseComponent from '~/components/PhaseComponent.vue';
import { EnvVar } from '../env/EnvVar';
import ScrollComponent from '~/components/ScrollComponent.vue';
const base = ref();
const props = defineProps<{
pod: Pod
}>();
const envs: Ref<EnvVar[] | undefined> = ref(undefined);
onMounted(() => {
EnvVar.get(props.pod.metadata.namespace, props.pod.metadata.name, (_envs: EnvVar[]) => {
envs.value = _envs;
});
})
const filter = ref("");
const filteredEnv = computed(() => {
if (envs.value)
{
return envs.value.filter(item => item.key.toLowerCase().includes(filter.value.toLowerCase()));
}
});
function open() {
base.value.open();
}
onMounted(() => {
document.addEventListener('keydown', (event) => {
if(event.key === 'Escape')
{
base.value.close();
}
});
})
defineExpose({ open });
</script>
<style scoped>
.env {
background-color: var(--shade-light);
padding: 0.25rem 0.5rem;
}
.env.env:nth-of-type(2n) {
background-color: var(--shade-dark);
}
.env:hover {
background-color: #cecece;
}
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="overlay" @click="close" v-show="visible"> <div class="overlay center" @click="close" v-show="visible">
<div class="popup" @click.stop> <div class="popup" :class="size" @click.stop>
<div class="popup__header"> <div class="popup__header">
<h2>{{ heading }}</h2> <h2>{{ heading }}</h2>
<UiButton icon="close" @click="() => close()" class="square"></UiButton> <UiButton icon="close" @click="() => close()" class="square"></UiButton>
@ -26,7 +26,8 @@ function open() {
} }
defineProps({ defineProps({
heading: String heading: String,
size: String
}) })
defineExpose({ defineExpose({
@ -55,7 +56,7 @@ function enableScrolling()
<style scoped> <style scoped>
.overlay { .overlay {
background-color: rgba(223, 223, 223, 0.514); background-color: rgba(54, 54, 54, 0.514);
backdrop-filter: blur(0.1rem); backdrop-filter: blur(0.1rem);
position: fixed; position: fixed;
top: 0; top: 0;
@ -87,4 +88,8 @@ function enableScrolling()
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
.MEDIUM {
height: auto;
width: 740px;
}
</style> </style>

View File

@ -0,0 +1,52 @@
<template>
<PopupTemplate heading="Add Secret" size="MEDIUM" ref="base">
<div class="content-l">
<h3>Type</h3>
<UiInput>
<select name="" id="">
<option value="generic">Generic</option>
<option value="tls">TLS</option>
<option value="basic-auth">Basic Auth</option>
<option value="docker-registry">Docker Registry</option>
</select>
</UiInput>
<h3>Metadata</h3>
<div class="tile-m col-2">
<UiInput label="Name">
<input type="text">
</UiInput>
<UiInput label="Namespace">
<NamespacePicker></NamespacePicker>
</UiInput>
</div>
<h3>Data</h3>
<div class="col-2 tile-m">
<UiInput label="Key">
<input type="text">
</UiInput>
<UiInput label="Value">
<input type="text">
</UiInput>
</div>
<div class="center">
<UiButton class="hollow width-6rem">Cancel</UiButton>
<UiButton class="width-6rem">Create</UiButton>
</div>
</div>
</PopupTemplate>
</template>
<script setup lang="ts">
import type PopupTemplate from '../popup/PopupTemplate.vue';
import NamespacePicker from '../NamespacePicker.vue';
const base = ref();
defineExpose({
open: () => base.value.open()
});
</script>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<template>
<div class="resource">
<p class="grid-element">{{ secret.metadata.name }}</p>
<p class="grid-element">{{ secret.metadata.namespace }}</p>
<p class="grid-element">-</p>
<div class="grid-element action-buttons">
<ActionButton>edit</ActionButton>
<ActionButton>delete</ActionButton>
</div>
</div>
</template>
<script setup lang="ts">
import type { Secret } from '~/classes/Secret';
defineProps<{
secret: Secret
}>();
</script>
<style scoped>
</style>

View File

@ -0,0 +1,13 @@
<template>
<div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

View File

@ -94,4 +94,10 @@ const props = defineProps<{
.button.small * { .button.small * {
font-size: 1.25rem; font-size: 1.25rem;
} }
.button.extra-small {
height: 1.25rem;
padding: 0 0.25rem;
border-radius: 0.25rem;
gap: 0.25rem;
}
</style> </style>

View File

@ -4,12 +4,20 @@
<script setup lang="ts"> <script setup lang="ts">
import UiIcon from './UiIcon.vue'; import UiIcon from './UiIcon.vue';
import { Optional } from '#imports';
const props = defineProps<{
size?: string
}>();
const fontSize = Optional.ofNullable(props.size).orElse("1rem");
</script> </script>
<style scoped> <style scoped>
.rotate{ .rotate{
animation: rotate 1s linear infinite; animation: rotate 1s linear infinite;
user-select: none; user-select: none;
font-size: v-bind(fontSize);
} }
@keyframes rotate{ @keyframes rotate{
to{ transform: rotate(360deg); } to{ transform: rotate(360deg); }

View File

@ -2,8 +2,9 @@
<div class="account-page"> <div class="account-page">
<NuxtPage></NuxtPage> <NuxtPage></NuxtPage>
<div class="left-center footer"> <div class="left-center footer">
<NuxtLink to="/account/inspect">Inspect</NuxtLink> <NuxtLink to="/account/inspect/nodes/_all">Inspect</NuxtLink>
<NuxtLink to="/account/monitorings/nodes">Monitorings</NuxtLink> <NuxtLink to="/account/monitorings/nodes">Monitorings</NuxtLink>
<NuxtLink to="/account/settings">Settings</NuxtLink>
</div> </div>
</div> </div>
</template> </template>
@ -15,7 +16,7 @@
grid-template-rows: 1fr auto; grid-template-rows: 1fr auto;
} }
.footer { .footer {
background-color: rgb(12, 12, 12); background-color: rgb(235, 235, 235);
padding: 1rem; padding: 1rem;
} }
</style> </style>

View File

@ -1,9 +1,11 @@
<template> <template>
<div id="app"> <div id="app">
<InspectSidebar></InspectSidebar> <InspectSidebar></InspectSidebar>
<ScrollComponent> <div class="page">
<NuxtPage></NuxtPage> <ScrollComponent>
</ScrollComponent> <NuxtPage></NuxtPage>
</ScrollComponent>
</div>
</div> </div>
</template> </template>
@ -24,4 +26,10 @@ onMounted(() => {
min-height: 100%; min-height: 100%;
align-items: flex-start; align-items: flex-start;
} }
.page {
padding: 1rem;
height: 100%;
width: 100%;
border-radius: 0.5rem;
}
</style> </style>

View File

@ -5,6 +5,7 @@
<ServiceComponent v-else-if="resource === 'services'"></ServiceComponent> <ServiceComponent v-else-if="resource === 'services'"></ServiceComponent>
<DeploymentComponent v-else-if="resource === 'deployments'"></DeploymentComponent> <DeploymentComponent v-else-if="resource === 'deployments'"></DeploymentComponent>
<NodeComponent v-else-if="resource === 'nodes'"></NodeComponent> <NodeComponent v-else-if="resource === 'nodes'"></NodeComponent>
<SecretComponent v-else-if="resource === 'secrets'"></SecretComponent>
<p v-else>Invalid resource</p> <p v-else>Invalid resource</p>
</template> </template>
@ -15,6 +16,7 @@ import IngressComponent from '~/components/inspect/resources/IngressComponent.vu
import ServiceComponent from '~/components/inspect/resources/ServiceComponent.vue'; import ServiceComponent from '~/components/inspect/resources/ServiceComponent.vue';
import DeploymentComponent from '~/components/inspect/resources/DeploymentComponent.vue'; import DeploymentComponent from '~/components/inspect/resources/DeploymentComponent.vue';
import NodeComponent from '~/components/inspect/resources/NodeComponent.vue'; import NodeComponent from '~/components/inspect/resources/NodeComponent.vue';
import SecretComponent from '~/components/inspect/resources/SecretComponent.vue';
const resource = useRoute().params.resource as string; const resource = useRoute().params.resource as string;
</script> </script>

View File

@ -0,0 +1,13 @@
<template>
<div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

View File

@ -1,8 +1,16 @@
import axios from "axios"; import axios from "axios";
export function getLogs(podId: string | undefined, onSuccess: (logs: string[]) => void) export class Log
{ {
axios.get<string[]>(useRuntimeConfig().public.apiBase + '/pods/' + podId + "/logs", { constructor (
public timestamp: string,
public message: string
) {}
}
export function getLogs(podId: string | undefined, onSuccess: (logs: Log[]) => void)
{
axios.get<Log[]>(useRuntimeConfig().public.apiBase + '/pods/' + podId + "/logs", {
headers: { headers: {
Authorization: "Bearer " + requireToken() Authorization: "Bearer " + requireToken()
} }

View File

@ -12,6 +12,21 @@ export const useNamespaceStore = defineStore('namespace', {
return state.namespaces; return state.namespaces;
} }
}, },
getNamespaceNames: (state) => {
return (includeAll?: boolean): Map<string, string> | undefined => {
if (state.namespaces)
{
const result = new Map<string, string>();
if (includeAll === true)
{
result.set('_all', 'Alle')
}
state.namespaces.forEach(namespace => { result.set(namespace.metadata.name, namespace.metadata.name) });
return result;
}
return undefined;
}
},
getCurrentNamespace: (state) => { getCurrentNamespace: (state) => {
return (): Namespace | undefined => state.currentNamespace; return (): Namespace | undefined => state.currentNamespace;
} }

42
utils/Optional.ts Normal file
View File

@ -0,0 +1,42 @@
export class Optional<T>
{
private object?: T;
private constructor(object?: T)
{
this.object = object;
}
static ofNullable<T>(object?: T): Optional<T>
{
return new Optional(object);
}
isEmpty()
{
return this.object == null;
}
isPresent()
{
return this.object != null;
}
orElse(other: T): T
{
if(this.object != null)
{
return this.object;
}
return other;
}
get(): T
{
if(this.object != null)
{
return this.object;
}
throw new Error("No value present in Optional.");
}
}

22
utils/StringUtils.ts Normal file
View File

@ -0,0 +1,22 @@
export class StringUtils
{
static format(template: string, ...varContext: string[]): string
{
const context = Array.from(varContext);
let contextIndex = 0;
while (template.includes("%s"))
{
const currentContext = context.at(contextIndex);
if (currentContext != null)
{
template = template.replace("%s", currentContext);
contextIndex++;
}
else
{
return template;
}
}
return template;
}
}