🎉 Init
This commit is contained in:
commit
b8327507c3
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
75
README.md
Normal file
75
README.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Nuxt Minimal Starter
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
28
app.vue
Normal file
28
app.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<NuxtPage></NuxtPage>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocation } from 'vue-router';
|
||||
|
||||
// Guard dashboard and redirect to login
|
||||
useRouter().beforeEach((route: RouteLocation) => {
|
||||
guard(route.fullPath);
|
||||
});
|
||||
|
||||
guard(useRoute().fullPath);
|
||||
|
||||
function guard(route: string)
|
||||
{
|
||||
if(route.startsWith('/dashboard') && getToken() == null)
|
||||
{
|
||||
useRouter().push('/');
|
||||
}
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: 'Kubooboo'
|
||||
})
|
||||
</script>
|
||||
239
assets/base-style.css
Normal file
239
assets/base-style.css
Normal file
@ -0,0 +1,239 @@
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.left-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.right-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stretch-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.left-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.center-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.center-bottom {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.top-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.spaced-top {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.spaced-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expand {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content, *[class^='content-'], *[class*=' content-'] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content, *[class^='content-'], *[class*=' content-'] > *:is(div, form, header, footer) {
|
||||
width: 100%;
|
||||
justify-self: stretch;
|
||||
}
|
||||
.content .full {
|
||||
width: 100%;
|
||||
}
|
||||
.content-s {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.content-m {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.content-l {
|
||||
gap: 1rem;
|
||||
}
|
||||
.content-xl {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.content-2xl {
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.content-3xl {
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.padding-m {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.padding-l {
|
||||
padding: 1rem;
|
||||
}
|
||||
.padding-xl {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.narrow {
|
||||
width: min(100%, 1340px);
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
.narrow.s {
|
||||
width: min(100%, 320px);
|
||||
}
|
||||
.narrow.m {
|
||||
width: min(100%, 540px);
|
||||
}
|
||||
.narrow-b {
|
||||
width: min(100%, 740px);
|
||||
}
|
||||
|
||||
.tile, *[class^='tile-'], *[class*=' tile-'] {
|
||||
background-color: var(--background-color);
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.tile-s {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.tile-m {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.tile-l {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.tile-xl {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.col-2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.col-3 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.col-4 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.col-5 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.grayed-out {
|
||||
color: #777777;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.row_auto-1fr {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.height_100 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gap-0 {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.col-1-m {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
.col-2-m {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
.col-3-m {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
.center-m {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.full_20rem {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 20rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.base-shape {
|
||||
height: 2.5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 915 KiB |
48
assets/style.css
Normal file
48
assets/style.css
Normal file
@ -0,0 +1,48 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap');
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-family: "Source Code Pro", monospace;
|
||||
}
|
||||
|
||||
html {
|
||||
--primary-color: rgb(87, 75, 255);
|
||||
}
|
||||
|
||||
html, body, #__nuxt {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.resource-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto 1fr auto auto;
|
||||
align-content: start;
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
.resource, .header {
|
||||
display: contents;
|
||||
}
|
||||
.header > * {
|
||||
padding: 0.75rem;
|
||||
background-color: rgb(29, 29, 29);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
.resource > .grid-element {
|
||||
padding: 0.25rem 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.resource p {
|
||||
font-weight: 600;
|
||||
font-family: "Source Code Pro", monospace;
|
||||
}
|
||||
.even > .grid-element {
|
||||
background-color: rgb(216, 216, 216);
|
||||
}
|
||||
6
classes/Ingress.ts
Normal file
6
classes/Ingress.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { Metadata } from "./Metadata";
|
||||
|
||||
export class Ingress
|
||||
{
|
||||
metadata?: Metadata;
|
||||
}
|
||||
7
classes/Metadata.ts
Normal file
7
classes/Metadata.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export class Metadata
|
||||
{
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
creationTimestamp?: string;
|
||||
uid?: string;
|
||||
}
|
||||
6
classes/Namespace.ts
Normal file
6
classes/Namespace.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { Metadata } from "./Metadata";
|
||||
|
||||
export class Namespace
|
||||
{
|
||||
metadata?: Metadata;
|
||||
}
|
||||
25
classes/Node.ts
Normal file
25
classes/Node.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { Metadata } from "./Metadata";
|
||||
|
||||
export class NodeStats
|
||||
{
|
||||
node?: Node;
|
||||
relativeCpuUsage?: number;
|
||||
relativeMemory?: number;
|
||||
}
|
||||
|
||||
class Node
|
||||
{
|
||||
metadata?: Metadata;
|
||||
status?: Status;
|
||||
}
|
||||
|
||||
class Status
|
||||
{
|
||||
conditions?: Condition[]
|
||||
}
|
||||
|
||||
class Condition
|
||||
{
|
||||
type?: string;
|
||||
status?: string;
|
||||
}
|
||||
111
classes/Pod.ts
Normal file
111
classes/Pod.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import dayjs from "dayjs";
|
||||
import advancedFormat from "dayjs/plugin/advancedFormat";
|
||||
import type { Metadata } from "./Metadata";
|
||||
|
||||
export class Pod
|
||||
{
|
||||
metadata?: Metadata
|
||||
status?: Status
|
||||
spec?: Spec
|
||||
}
|
||||
|
||||
class Spec {
|
||||
nodeName?: string;
|
||||
}
|
||||
|
||||
class Status
|
||||
{
|
||||
phase?: PodStatus
|
||||
}
|
||||
|
||||
enum PodStatus
|
||||
{
|
||||
RUNNING = "Running",
|
||||
SUCCEEDED = "Succeeded",
|
||||
FAILED = "Failed"
|
||||
}
|
||||
|
||||
enum ChronoUnit
|
||||
{
|
||||
SECOND, MINUTE, HOUR, DAY
|
||||
}
|
||||
|
||||
export function calcAge(datetime: string | undefined)
|
||||
{
|
||||
dayjs.extend(advancedFormat);
|
||||
if(datetime != null)
|
||||
{
|
||||
const today = Number(dayjs().format('X'));
|
||||
const createdAt = Number(dayjs(datetime).format('X'));
|
||||
const dif = today - createdAt;
|
||||
if(dif < 60)
|
||||
{
|
||||
return format(dif, ChronoUnit.SECOND);
|
||||
}
|
||||
if(dif < 60 * 60)
|
||||
{
|
||||
const minutes = Math.floor((dif / 60));
|
||||
return format(minutes, ChronoUnit.MINUTE);
|
||||
}
|
||||
if(dif < 60 * 60 * 24)
|
||||
{
|
||||
const hours = Math.floor(dif / (60 * 60));
|
||||
return format(hours, ChronoUnit.HOUR);
|
||||
}
|
||||
else
|
||||
{
|
||||
const days = Math.floor(dif / (60 * 60 * 24));
|
||||
return format(days, ChronoUnit.DAY);
|
||||
}
|
||||
}
|
||||
return "-";
|
||||
}
|
||||
|
||||
function format(count: number, unit: ChronoUnit): string
|
||||
{
|
||||
if(unit === ChronoUnit.SECOND)
|
||||
{
|
||||
if(count != 1)
|
||||
{
|
||||
return count + " Sekunden";
|
||||
}
|
||||
else
|
||||
{
|
||||
return count + " Sekunde";
|
||||
}
|
||||
}
|
||||
if(unit === ChronoUnit.MINUTE)
|
||||
{
|
||||
if(count != 1)
|
||||
{
|
||||
return count + " Minuten";
|
||||
}
|
||||
else
|
||||
{
|
||||
return count + " Minute";
|
||||
}
|
||||
}
|
||||
if(unit === ChronoUnit.HOUR)
|
||||
{
|
||||
if(count != 1)
|
||||
{
|
||||
return count + " Stunden";
|
||||
}
|
||||
else
|
||||
{
|
||||
return count + " Stunde";
|
||||
}
|
||||
}
|
||||
if(unit === ChronoUnit.DAY)
|
||||
{
|
||||
if(count != 1)
|
||||
{
|
||||
return count + " Tage";
|
||||
}
|
||||
else
|
||||
{
|
||||
return count + " Tag";
|
||||
}
|
||||
}
|
||||
return "-";
|
||||
}
|
||||
17
classes/Service.ts
Normal file
17
classes/Service.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { Metadata } from "./Metadata";
|
||||
|
||||
export class Service
|
||||
{
|
||||
metadata?: Metadata;
|
||||
spec?: ServiceSpec;
|
||||
}
|
||||
|
||||
export class ServiceSpec
|
||||
{
|
||||
type?: ServiceType
|
||||
}
|
||||
|
||||
export enum ServiceType
|
||||
{
|
||||
CLUSTER_IP = "ClusterIP"
|
||||
}
|
||||
13
classes/Session.ts
Normal file
13
classes/Session.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import type { User } from "./User";
|
||||
|
||||
export class Session
|
||||
{
|
||||
user: User;
|
||||
token: string;
|
||||
|
||||
constructor(user: User, token: string)
|
||||
{
|
||||
this.user = user;
|
||||
this.token = token;
|
||||
}
|
||||
}
|
||||
12
classes/Settings.ts
Normal file
12
classes/Settings.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export class Settings
|
||||
{
|
||||
kubeconfigDefaultPath: boolean;
|
||||
kubeconfigPath?: string;
|
||||
refreshInterval: number;
|
||||
|
||||
constructor(kubeconfigDefaultPath: boolean, refreshInterval: number)
|
||||
{
|
||||
this.kubeconfigDefaultPath = kubeconfigDefaultPath;
|
||||
this.refreshInterval = refreshInterval;
|
||||
}
|
||||
}
|
||||
24
classes/User.ts
Normal file
24
classes/User.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export class User
|
||||
{
|
||||
username?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
roles?: string[];
|
||||
initial?: boolean;
|
||||
}
|
||||
|
||||
export function hasAnyRole(user: User | undefined, requiredRoles: string[])
|
||||
{
|
||||
if(user && user.roles != undefined)
|
||||
{
|
||||
const roles = user.roles;
|
||||
for(const role of roles)
|
||||
{
|
||||
if(requiredRoles.includes(role))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
63
components/AccountPopup.vue
Normal file
63
components/AccountPopup.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<PopupTemplate heading="Account" ref="base">
|
||||
<div class="account-page">
|
||||
<div class="nav">
|
||||
<NuxtLink class="nav-link" :class="{ active: tab === 0 }" @click="() => tab = 0">Allgemein</NuxtLink>
|
||||
<NuxtLink class="nav-link" :class="{ active: tab === 1 }" @click="() => tab = 1">Einstellungen</NuxtLink>
|
||||
<NuxtLink class="nav-link" :class="{ active: tab === 2 }" @click="() => tab = 2" v-if="hasAnyRole(getUser(), ['admin'])">Benutzer</NuxtLink>
|
||||
<NuxtLink class="nav-link" :class="{ active: tab === 3 }" @click="() => tab = 3">Logout</NuxtLink>
|
||||
</div>
|
||||
<div>
|
||||
<AccountTab v-if="tab === 0"></AccountTab>
|
||||
<SettingsTab v-if="tab === 1"></SettingsTab>
|
||||
<UserTab v-if="tab === 2" :goto="(n: number) => tab = n"></UserTab>
|
||||
<LogoutTab v-if="tab === 3"></LogoutTab>
|
||||
<AddUserTab v-if="tab === 3.5" :goto="(n: number) => tab = n"></AddUserTab>
|
||||
</div>
|
||||
</div>
|
||||
</PopupTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import UserTab from './account/UserTab.vue';
|
||||
import SettingsTab from './account/SettingsTab.vue';
|
||||
import { hasAnyRole } from '~/classes/User';
|
||||
import LogoutTab from './account/LogoutTab.vue';
|
||||
import AddUserTab from './account/AddUserTab.vue';
|
||||
|
||||
const base = ref();
|
||||
|
||||
const tab = ref(0);
|
||||
|
||||
function open()
|
||||
{
|
||||
tab.value = 0;
|
||||
base.value.open();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.account-page {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.nav-link {
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nav-link.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
30
components/ActionButton.vue
Normal file
30
components/ActionButton.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="action-button">
|
||||
<span class="material-symbols-outlined"><slot></slot></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.action-button {
|
||||
background-color: darkblue;
|
||||
height: 1.75rem;
|
||||
width: 1.75rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: black;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.action-button * {
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings:
|
||||
'FILL' 0,
|
||||
'wght' 400,
|
||||
'GRAD' 0,
|
||||
'opsz' 24
|
||||
}
|
||||
</style>
|
||||
14
components/IngressComponent.vue
Normal file
14
components/IngressComponent.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<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>
|
||||
58
components/LogPopup.vue
Normal file
58
components/LogPopup.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<PopupTemplate :heading="'Logs: ' + pod.metadata?.namespace + '/' + pod.metadata?.name" ref="base">
|
||||
<ScrollComponent ref="scrollComponent" v-show="logs && logs.length > 0">
|
||||
<div class="console">
|
||||
<p class="log" v-for="log, index in logs" :class="{ even: index % 2 }">{{ log }}</p>
|
||||
</div>
|
||||
</ScrollComponent>
|
||||
</PopupTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Pod } from '~/classes/Pod';
|
||||
import { getLogs } from '~/requests/logs';
|
||||
|
||||
const base = ref();
|
||||
|
||||
const logs: Ref<string[] | undefined> = ref(undefined);
|
||||
|
||||
const props = defineProps<{
|
||||
pod: Pod
|
||||
}>();
|
||||
|
||||
const scrollComponent = ref();
|
||||
|
||||
function open()
|
||||
{
|
||||
logs.value = undefined;
|
||||
base.value.open();
|
||||
getLogs(props.pod.metadata?.uid, (_logs: string[]) => {
|
||||
logs.value = _logs;
|
||||
scrollComponent.value.scrollToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.log {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
color: white;
|
||||
padding: 0.25rem 1rem;
|
||||
}
|
||||
.console {
|
||||
background-color: black;
|
||||
border-radius: 0.5rem;
|
||||
display: grid;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
.even {
|
||||
background-color: rgb(24, 24, 24);
|
||||
}
|
||||
</style>
|
||||
102
components/NodeComponent.vue
Normal file
102
components/NodeComponent.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="grid-element" v-if="nodeStats.node && nodeStats.node.metadata">{{ nodeStats.node.metadata.name }}</p>
|
||||
<div class="grid-element">
|
||||
<p>{{ calcAge(nodeStats.node?.metadata?.creationTimestamp) }}</p>
|
||||
</div>
|
||||
<div class="grid-element">
|
||||
<NodeReadyComponent :ready="isReady(nodeStats)"></NodeReadyComponent>
|
||||
</div>
|
||||
<div class="grid-element">
|
||||
<p class="usage" :class="cpuUsageFlag(nodeStats.relativeCpuUsage)">{{ nodeStats.relativeCpuUsage }}%</p>
|
||||
</div>
|
||||
<div class="grid-element">
|
||||
<p class="usage" :class="ramUsageFlag(nodeStats.relativeMemory)">{{ nodeStats.relativeMemory }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { NodeStats } from '~/classes/Node';
|
||||
import NodeReadyComponent from './NodeReadyComponent.vue';
|
||||
import { calcAge } from '~/classes/Pod';
|
||||
|
||||
defineProps<{
|
||||
nodeStats: NodeStats;
|
||||
}>();
|
||||
|
||||
function isReady(nodeStats: NodeStats): boolean | undefined
|
||||
{
|
||||
const conditions = nodeStats.node?.status?.conditions;
|
||||
if(conditions != undefined)
|
||||
{
|
||||
for(const condition of conditions)
|
||||
{
|
||||
if(condition.type === "Ready" && condition.status === "True")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function cpuUsageFlag(usage: number | undefined)
|
||||
{
|
||||
if(!usage)
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
if(usage < 20)
|
||||
{
|
||||
return "green";
|
||||
}
|
||||
if(usage < 60)
|
||||
{
|
||||
return "orange";
|
||||
}
|
||||
return "red";
|
||||
}
|
||||
function ramUsageFlag(usage: number | undefined)
|
||||
{
|
||||
if(!usage)
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
if(usage < 30)
|
||||
{
|
||||
return "green";
|
||||
}
|
||||
if(usage < 75)
|
||||
{
|
||||
return "orange";
|
||||
}
|
||||
return "red";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.usage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 4rem;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgb(179, 179, 179);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.green {
|
||||
background-color: green;
|
||||
color: white;
|
||||
}
|
||||
.orange {
|
||||
color: white;
|
||||
background-color: #d68100;
|
||||
}
|
||||
.red {
|
||||
background-color: #ff2a0e;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
31
components/NodeReadyComponent.vue
Normal file
31
components/NodeReadyComponent.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="phase" :class="{ ready: ready === true, unknown: ready === undefined }">
|
||||
<p v-if="ready === true">Ready</p>
|
||||
<p v-if="ready === false">Not Ready</p>
|
||||
<p v-if="ready === undefined">Unknown</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
ready: boolean | undefined
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.phase {
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
background-color: red;
|
||||
}
|
||||
.phase * {
|
||||
color: white;
|
||||
}
|
||||
.phase.ready {
|
||||
background-color: green;
|
||||
}
|
||||
.phase.unknown {
|
||||
background-color: rgb(0, 119, 119);
|
||||
}
|
||||
</style>
|
||||
30
components/PhaseComponent.vue
Normal file
30
components/PhaseComponent.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div v-if="phase" class="phase" :class="phase.toLowerCase()">
|
||||
<p>{{ phase }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
phase: string | undefined
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.phase {
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.phase * {
|
||||
color: white;
|
||||
}
|
||||
.running, .succeeded {
|
||||
background-color: green;
|
||||
}
|
||||
.failed {
|
||||
background-color: crimson;
|
||||
}
|
||||
.pending {
|
||||
background-color: chocolate;
|
||||
}
|
||||
</style>
|
||||
89
components/PopupTemplate.vue
Normal file
89
components/PopupTemplate.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="overlay" @click="close" v-show="visible">
|
||||
<div class="popup" @click.stop>
|
||||
<div class="popup__header">
|
||||
<h2>{{ heading }}</h2>
|
||||
<UiButton icon="close" @click="() => close()" class="square"></UiButton>
|
||||
</div>
|
||||
<div class="popup__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const visible = ref(false);
|
||||
|
||||
function close() {
|
||||
enableScrolling();
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function open() {
|
||||
disableScrolling();
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
defineProps({
|
||||
heading: String
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
close,
|
||||
open
|
||||
})
|
||||
|
||||
function disableScrolling()
|
||||
{
|
||||
const body = document.getElementsByTagName('body');
|
||||
for(const element of body)
|
||||
{
|
||||
element.style.overflow = "hidden";
|
||||
}
|
||||
}
|
||||
|
||||
function enableScrolling()
|
||||
{
|
||||
const body = document.getElementsByTagName('body');
|
||||
for(const element of body)
|
||||
{
|
||||
element.style.overflow = "visible";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.overlay {
|
||||
background-color: rgba(0, 0, 0, 0.514);
|
||||
backdrop-filter: blur(0.1rem);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1rem;
|
||||
padding: 2rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.popup {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
align-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.popup__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.popup__body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
39
components/ScrollComponent.vue
Normal file
39
components/ScrollComponent.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="scroll-component" :id="id">
|
||||
<div class="inner-scroll-component">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
function scrollToBottom()
|
||||
{
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(id);
|
||||
if(element)
|
||||
{
|
||||
element.scrollTo({top: element.scrollHeight, behavior: 'instant'});
|
||||
}
|
||||
}, 25)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
scrollToBottom
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scroll-component {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: scroll;
|
||||
}
|
||||
.inner-scroll-component {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
85
components/Sidebar.vue
Normal file
85
components/Sidebar.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<nav class="sidebar">
|
||||
<div class="content-l">
|
||||
<h2>Kubooboo</h2>
|
||||
<div class="nav">
|
||||
<NuxtLink class="resources" to="/dashboard/nodes">Nodes</NuxtLink>
|
||||
<NuxtLink class="resources" to="/dashboard/ingresses">Ingresses</NuxtLink>
|
||||
<NuxtLink class="resources" to="/dashboard/services">Services</NuxtLink>
|
||||
<NuxtLink class="resources" to="/dashboard/pods">Pods</NuxtLink>
|
||||
</div>
|
||||
<div class="divider" :class="{ hide: !inNamespaceScopedResource }"></div>
|
||||
<div :class="{ hide: !inNamespaceScopedResource }">
|
||||
<div class="namespace" :class="{ active: currentNamespace === undefined }" @click="() => namespaceStore.selectNamespace(undefined)">
|
||||
<p>Alle</p>
|
||||
</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 class="left-center" v-if="user" @click="() => accountPopup.open()">
|
||||
<UiIcon>account_circle</UiIcon>
|
||||
<p>{{ user.username }}</p>
|
||||
</div>
|
||||
</nav>
|
||||
<AccountPopup ref="accountPopup"></AccountPopup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useNamespaceStore } from '#imports';
|
||||
|
||||
const namespaceStore = useNamespaceStore();
|
||||
|
||||
const namespaces = computed(namespaceStore.getNamespaces);
|
||||
|
||||
const currentNamespace = computed(namespaceStore.getCurrentNamespace)
|
||||
|
||||
const user = getUser();
|
||||
|
||||
const accountPopup = ref();
|
||||
|
||||
const inNamespaceScopedResource: ComputedRef<boolean> = computed(() => {
|
||||
if(useRoute().fullPath.startsWith('/dashboard/nodes'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
padding: 0.75rem;
|
||||
background-color: rgb(29, 29, 29);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
.sidebar * {
|
||||
color: white;
|
||||
}
|
||||
.namespace, .resources {
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.namespace.active, .resources.router-link-active {
|
||||
background-color: var(--primary-color)
|
||||
}
|
||||
.namespace.active *, .resources.router-link-active {
|
||||
color: white;
|
||||
}
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: lightgray;
|
||||
}
|
||||
.nav > * {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
.hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
43
components/account/AccountTab.vue
Normal file
43
components/account/AccountTab.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="content-l">
|
||||
<h2>Account</h2>
|
||||
<div class="content-m">
|
||||
<h3>Passwort ändern</h3>
|
||||
<div class="col-2">
|
||||
<UiInput label="Passwort">
|
||||
<input type="password" v-model="password">
|
||||
</UiInput>
|
||||
<UiInput label="Passwort wiederholen">
|
||||
<input type="password" v-model="passwordRepeat">
|
||||
</UiInput>
|
||||
</div>
|
||||
<div class="center">
|
||||
<UiButton :onclick="() => change()" :disabled="password.length === 0 || password !== passwordRepeat">Passwort Ändern</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { changePassword } from '~/requests/user';
|
||||
|
||||
const password = ref("");
|
||||
const passwordRepeat = ref("");
|
||||
|
||||
function change()
|
||||
{
|
||||
if(password.value !== passwordRepeat.value)
|
||||
{
|
||||
throw new Error("[Method: AccountTab.change] passwords do not match.");
|
||||
}
|
||||
changePassword(requireUser().username, password.value, () => {
|
||||
resetFields();
|
||||
});
|
||||
}
|
||||
|
||||
function resetFields()
|
||||
{
|
||||
password.value = "";
|
||||
passwordRepeat.value = "";
|
||||
}
|
||||
</script>
|
||||
58
components/account/AddUserTab.vue
Normal file
58
components/account/AddUserTab.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="content-l">
|
||||
<h3>Benutzer hinzufügen</h3>
|
||||
<UiInput label="Username" required>
|
||||
<input type="text" v-model="user.username">
|
||||
</UiInput>
|
||||
<UiInput label="Rolle" required>
|
||||
<select name="" id="">
|
||||
<option value="admin">Admin</option>
|
||||
<option value="maintainer">Maintainer</option>
|
||||
<option value="developer" selected>Developer</option>
|
||||
</select>
|
||||
</UiInput>
|
||||
<div class="col-2">
|
||||
<UiInput label="Passwort" required>
|
||||
<input type="password" v-model="user.password">
|
||||
</UiInput>
|
||||
<UiInput label="Passwort wiederholen" required>
|
||||
<input type="password">
|
||||
</UiInput>
|
||||
</div>
|
||||
<div class="center">
|
||||
<UiButton class="hollow">Zurück</UiButton>
|
||||
<UiButton :onclick="() => create()">User Anlegen</UiButton>
|
||||
</div>
|
||||
<p>{{ user }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { User } from '~/classes/User';
|
||||
import { createUser } from '~/requests/user';
|
||||
|
||||
const props = defineProps<{
|
||||
goto: (tab: number) => void
|
||||
}>();
|
||||
|
||||
const user = ref(new User());
|
||||
|
||||
function create()
|
||||
{
|
||||
if(!user.value.username)
|
||||
{
|
||||
throw new Error("username is required");
|
||||
}
|
||||
if(!user.value.roles)
|
||||
{
|
||||
throw new Error("roles are required");
|
||||
}
|
||||
if(!user.value.password)
|
||||
{
|
||||
throw new Error("password is required");
|
||||
}
|
||||
createUser(user.value, () => {
|
||||
props.goto(3);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
15
components/account/LogoutTab.vue
Normal file
15
components/account/LogoutTab.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<h3>Logout</h3>
|
||||
<div>
|
||||
<UiButton @click="() => logout()">Logout</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
function logout()
|
||||
{
|
||||
const session = useCookie('session');
|
||||
session.value = undefined;
|
||||
useRouter().push('/');
|
||||
}
|
||||
</script>
|
||||
23
components/account/SettingsTab.vue
Normal file
23
components/account/SettingsTab.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="content-l">
|
||||
<h3>Einstellungen</h3>
|
||||
<div class="content-m">
|
||||
<UiInput label="Kubeconfig Default Pfad">
|
||||
<UiRadio code="true" :active="String(settings.kubeconfigDefaultPath)" label="Default" @click="() => settings.kubeconfigDefaultPath = true"></UiRadio>
|
||||
<UiRadio code="false" :active="String(settings.kubeconfigDefaultPath)" label="Custom" @click="() => settings.kubeconfigDefaultPath = false"></UiRadio>
|
||||
</UiInput>
|
||||
<UiInput label="Kubeconfig Pfad">
|
||||
<input type="text" :disabled="settings.kubeconfigDefaultPath">
|
||||
</UiInput>
|
||||
<UiInput label="Refresh Interval [Sekunden]">
|
||||
<input type="number" v-model="settings.refreshInterval">
|
||||
</UiInput>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Settings } from '~/classes/Settings';
|
||||
|
||||
const settings = ref(new Settings(true, 10));
|
||||
</script>
|
||||
47
components/account/UserTab.vue
Normal file
47
components/account/UserTab.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="content-l">
|
||||
<div class="left-center">
|
||||
<h3>Users</h3>
|
||||
<UiButton icon="add" class="square small" @click="() => goto(3.5)"></UiButton>
|
||||
</div>
|
||||
<div class="resource-container user-container">
|
||||
<div class="header">
|
||||
<p>Username</p>
|
||||
<p>E-Mail</p>
|
||||
<p>Roles</p>
|
||||
<p>Initial</p>
|
||||
</div>
|
||||
<div class="resource" v-for="user, index in users" :class="{ even: index % 2 }">
|
||||
<p class="grid-element">{{ user.username }}</p>
|
||||
<p class="grid-element"><span v-if="user.email">{{ user.email }}</span><span v-else>-</span></p>
|
||||
<p class="grid-element" v-if="user.roles">{{ user.roles.join(", ") }}</p>
|
||||
<p class="grid-element">{{ user.initial === true }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { User } from '~/classes/User';
|
||||
import { getUsers } from '~/requests/user';
|
||||
|
||||
defineProps<{
|
||||
goto: (tab: number) => void;
|
||||
}>();
|
||||
|
||||
const users: Ref<User[] | undefined> = ref(undefined);
|
||||
|
||||
onMounted(() => {
|
||||
getUsers((_users: User[]) => {
|
||||
users.value = _users;
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-container {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
</style>
|
||||
30
components/pod/PodComponent.vue
Normal file
30
components/pod/PodComponent.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<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">{{ calcAge(pod.metadata.creationTimestamp) }}</p>
|
||||
<p class="grid-element" v-if="pod.spec">{{ pod.spec.nodeName }}</p>
|
||||
<div class="grid-element">
|
||||
<PhaseComponent v-if="pod.status" :phase="pod.status.phase"></PhaseComponent>
|
||||
</div>
|
||||
<div class="grid-element action-buttons">
|
||||
<ActionButton @click="() => logPopup.open()">text_snippet</ActionButton>
|
||||
<ActionButton>open_in_full</ActionButton>
|
||||
<ActionButton @click="() => deletePod(pod.metadata?.uid, () => {})" v-if="hasAnyRole(getUser(), ['admin', 'maintainer'])">delete</ActionButton>
|
||||
</div>
|
||||
<LogPopup :pod="pod" ref="logPopup"></LogPopup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Pod } from '~/classes/Pod';
|
||||
import { calcAge } from '~/classes/Pod';
|
||||
import { hasAnyRole } from '~/classes/User';
|
||||
import { deletePod } from '~/requests/pod';
|
||||
|
||||
defineProps<{
|
||||
pod: Pod
|
||||
}>();
|
||||
|
||||
const logPopup = ref();
|
||||
</script>
|
||||
19
components/service/ServiceComponent.vue
Normal file
19
components/service/ServiceComponent.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="grid-element" v-if="service.metadata">{{ service.metadata.name }}</p>
|
||||
<p class="grid-element" v-if="service.metadata">{{ service.metadata.namespace }}</p>
|
||||
<p class="grid-element" v-if="service.spec">{{ service.spec.type }}</p>
|
||||
<div class="grid-element">
|
||||
<ActionButton v-if="hasAnyRole(getUser(), ['admin', 'maintainer'])">delete</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Service } from '~/classes/Service';
|
||||
import { hasAnyRole } from '~/classes/User';
|
||||
|
||||
defineProps<{
|
||||
service: Service
|
||||
}>();
|
||||
</script>
|
||||
97
components/ui/UiButton.vue
Normal file
97
components/ui/UiButton.vue
Normal file
@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<button @click="() => click()" class="base-shape button pointer center" :class="{ loading: loading, reverse: reverse, disabled: disabled }"><UiIcon v-if="icon && !loading">{{ icon }}</UiIcon><UiLoadingIcon v-if="loading"></UiLoadingIcon><slot></slot></button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import UiIcon from '@/components/ui/UiIcon.vue';
|
||||
|
||||
function click() {
|
||||
if(props.disabled !== true) {
|
||||
if(props.onclick != null) {
|
||||
props.onclick()
|
||||
}
|
||||
if(props.to != null) {
|
||||
useRouter().push(props.to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
icon?: string,
|
||||
reverse?: boolean,
|
||||
loading?: boolean | undefined,
|
||||
disabled?: boolean,
|
||||
onclick?: () => void
|
||||
to?: string
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.button {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
outline: none;
|
||||
gap: 0.5rem;
|
||||
border: 2px solid var(--primary-color);
|
||||
white-space: nowrap;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.button.square {
|
||||
padding: 0;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
.button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.button * {
|
||||
color: white;
|
||||
fill: white;
|
||||
}
|
||||
.button.secondary {
|
||||
background-color: var(--secondary-color);
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
.button:hover.secondary {
|
||||
background-color: var(--secondary-color-alt);
|
||||
}
|
||||
.button.reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.button.loading {
|
||||
background-color: #5c5c5c;
|
||||
border-color: #5c5c5c;
|
||||
cursor: default;
|
||||
}
|
||||
.button.disabled {
|
||||
background-color: #7a7a7a;
|
||||
border-color: #7a7a7a;
|
||||
cursor: default;
|
||||
}
|
||||
.button.hollow {
|
||||
background-color: white;
|
||||
border: 2px solid var(--primary-color);;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.button.hollow * {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.button.hollow:hover {
|
||||
background-color: rgba(0, 0, 0, 0.055);
|
||||
}
|
||||
.border.secondary.hollow {
|
||||
border: 2px solid var(--secondary-color);;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
.reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.button.small {
|
||||
height: 2rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
.button.small * {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
35
components/ui/UiIcon.vue
Normal file
35
components/ui/UiIcon.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<span class="material-symbols-outlined"><slot></slot></span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings:
|
||||
'FILL' 0,
|
||||
'wght' 400,
|
||||
'GRAD' 0,
|
||||
'opsz' 24;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.styled {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: 0.25rem;
|
||||
border-radius: 100rem;
|
||||
}
|
||||
.styled.secondary {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
.black {
|
||||
background-color: var(--font-color);
|
||||
color: var(--background-color);
|
||||
}
|
||||
.success {
|
||||
background-color: #2e9a00;
|
||||
}
|
||||
.small {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
</style>
|
||||
52
components/ui/UiInput.vue
Normal file
52
components/ui/UiInput.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="field">
|
||||
<div class="left-center">
|
||||
<label v-if="label">{{ label }} <span class="required" v-if="required">*</span></label>
|
||||
<slot name="label"></slot>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label?: string,
|
||||
required?: boolean
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.field input, .field .input, .field textarea, .field select {
|
||||
min-height: 2.5rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgb(255, 255, 255);
|
||||
width: 100%;
|
||||
border: 2px solid #303030;
|
||||
outline: none;
|
||||
padding: 0.25rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.field textarea {
|
||||
min-height: 5rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
.field input:focus, .field textarea:focus, .field select:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.required {
|
||||
color: #e63515;
|
||||
font-weight: bold;
|
||||
}
|
||||
.field input:disabled, textarea:disabled, select:disabled {
|
||||
border: 2px solid #7a7a7a;
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
</style>
|
||||
16
components/ui/UiLoadingIcon.vue
Normal file
16
components/ui/UiLoadingIcon.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<UiIcon class="rotate">progress_activity</UiIcon>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import UiIcon from './UiIcon.vue';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rotate{
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
@keyframes rotate{
|
||||
to{ transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
42
components/ui/UiRadio.vue
Normal file
42
components/ui/UiRadio.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="radio left-center" @click="() => emits('select', code)">
|
||||
<div class="radio__outer center">
|
||||
<div class="radio__inner" :class="{ disabled: disabled }" v-if="active != null && active === code"></div>
|
||||
</div>
|
||||
<label>{{ label }}</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
active: string | undefined,
|
||||
disabled?: boolean,
|
||||
label: string,
|
||||
code: string | undefined
|
||||
}>();
|
||||
const emits = defineEmits<{
|
||||
(e: 'select', payload: string | undefined): void
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
.radio__outer {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
background-color: white;
|
||||
border: 2px solid black;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
.radio__inner {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
.radio__inner.disabled {
|
||||
background-color: #7a7a7a;
|
||||
}
|
||||
</style>
|
||||
26
nuxt.config.ts
Normal file
26
nuxt.config.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-05-15',
|
||||
devtools: { enabled: true },
|
||||
|
||||
css: [
|
||||
'@/assets/style.css',
|
||||
'@/assets/base-style.css'
|
||||
],
|
||||
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
apiBase: 'http://localhost:9090'
|
||||
}
|
||||
},
|
||||
|
||||
app: {
|
||||
head: {
|
||||
link: [
|
||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
modules: ['@pinia/nuxt']
|
||||
});
|
||||
10467
package-lock.json
generated
Normal file
10467
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "nuxt-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "^0.11.0",
|
||||
"axios": "^1.9.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"nuxt": "^3.17.4",
|
||||
"pinia": "^3.0.2",
|
||||
"vue": "^3.5.15",
|
||||
"vue-router": "^4.5.1"
|
||||
}
|
||||
}
|
||||
26
pages/dashboard.vue
Normal file
26
pages/dashboard.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<Sidebar></Sidebar>
|
||||
<ScrollComponent>
|
||||
<NuxtPage></NuxtPage>
|
||||
</ScrollComponent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useNamespaceStore } from '#imports';
|
||||
|
||||
const namespaceStore = useNamespaceStore();
|
||||
onMounted(() => {
|
||||
namespaceStore.init();
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
min-height: 100%;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
3
pages/dashboard/deployments.vue
Normal file
3
pages/dashboard/deployments.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
33
pages/dashboard/ingresses.vue
Normal file
33
pages/dashboard/ingresses.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<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>
|
||||
47
pages/dashboard/nodes.vue
Normal file
47
pages/dashboard/nodes.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<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>
|
||||
55
pages/dashboard/pods.vue
Normal file
55
pages/dashboard/pods.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<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>
|
||||
53
pages/dashboard/services.vue
Normal file
53
pages/dashboard/services.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<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>
|
||||
75
pages/index.vue
Normal file
75
pages/index.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="login-wrapper">
|
||||
<h2>Kubooboo</h2>
|
||||
<img class="logo" src="@/assets/logo.png" alt="">
|
||||
<form class="login content-xl" @submit.prevent="doLogin()">
|
||||
<div class="center">
|
||||
<h1>Login</h1>
|
||||
</div>
|
||||
<div class="content-l">
|
||||
<UiInput label="Username">
|
||||
<input type="text" v-model="loginCredentials.username">
|
||||
</UiInput>
|
||||
<UiInput label="Passwort">
|
||||
<input type="password" v-model="loginCredentials.password">
|
||||
</UiInput>
|
||||
</div>
|
||||
<div class="center">
|
||||
<UiButton @onclick="() => doLogin()" :loading="loading" icon="login" reverse>Login</UiButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { User } from '~/classes/User';
|
||||
import UiInput from '~/components/ui/UiInput.vue';
|
||||
import { login } from '~/requests/login';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { getUser } from '~/requests/user';
|
||||
import { Session } from '~/classes/Session';
|
||||
|
||||
const loginCredentials = ref(new User());
|
||||
|
||||
const loading = ref(false);
|
||||
function doLogin()
|
||||
{
|
||||
loading.value = true;
|
||||
login(loginCredentials.value, (token: string) => {
|
||||
const decode = jwtDecode(token) as any;
|
||||
getUser(decode.upn, token, (user: User) => {
|
||||
setSessionCookie(new Session(user, token), decode.exp as number);
|
||||
useRouter().push('/dashboard/nodes');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setSessionCookie(session: Session, exp: number)
|
||||
{
|
||||
const cookie = useCookie('session', {
|
||||
expires: new Date(exp * 1000)
|
||||
});
|
||||
cookie.value = JSON.stringify(session);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.login {
|
||||
width: 640px;
|
||||
background-color: #f1f1f1;
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.logo {
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
}
|
||||
</style>
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
27
requests/ingresses.ts
Normal file
27
requests/ingresses.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import axios from "axios";
|
||||
import type { Ingress } from "~/classes/Ingress";
|
||||
|
||||
export function getIngresses(namespace: string | undefined, onSuccess: (ingresses: Ingress[]) => void)
|
||||
{
|
||||
axios.get<Ingress[]>(useRuntimeConfig().public.apiBase + '/ingresses', {
|
||||
headers: {
|
||||
Authorization: "Bearer " + requireToken()
|
||||
},
|
||||
params: getParams(namespace)
|
||||
})
|
||||
.then(response => {
|
||||
onSuccess(response.data);
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
|
||||
function getParams(namespace: string | undefined)
|
||||
{
|
||||
if(namespace != undefined)
|
||||
{
|
||||
return {
|
||||
namespace: namespace
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
11
requests/login.ts
Normal file
11
requests/login.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import axios from "axios";
|
||||
import type { User } from "~/classes/User";
|
||||
|
||||
export function login(user: User, onSuccess: (token: string) => void)
|
||||
{
|
||||
axios.post<string>(useRuntimeConfig().public.apiBase + '/login', user)
|
||||
.then((response) => {
|
||||
onSuccess(response.data);
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
13
requests/logs.ts
Normal file
13
requests/logs.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import axios from "axios";
|
||||
|
||||
export function getLogs(podId: string | undefined, onSuccess: (logs: string[]) => void)
|
||||
{
|
||||
axios.get<string[]>(useRuntimeConfig().public.apiBase + '/pods/' + podId + "/logs", {
|
||||
headers: {
|
||||
Authorization: "Bearer " + requireToken()
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
onSuccess(response.data);
|
||||
});
|
||||
}
|
||||
16
requests/namespace.ts
Normal file
16
requests/namespace.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import axios from "axios";
|
||||
import type { Namespace } from "~/classes/Namespace";
|
||||
|
||||
|
||||
export function getNamespaces(onSuccess: (namespaces: Namespace[]) => void)
|
||||
{
|
||||
axios.get<Namespace[]>(useRuntimeConfig().public.apiBase + '/namespaces', {
|
||||
headers: {
|
||||
Authorization: "Bearer " + requireToken()
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
onSuccess(response.data);
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
16
requests/nodes.ts
Normal file
16
requests/nodes.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import axios from "axios";
|
||||
import type { NodeStats } from "~/classes/Node";
|
||||
|
||||
|
||||
export function getNodes(onSuccess: (nodes: NodeStats[]) => void)
|
||||
{
|
||||
axios.get<NodeStats[]>(useRuntimeConfig().public.apiBase + '/nodes', {
|
||||
headers: {
|
||||
Authorization: "Bearer " + requireToken()
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
onSuccess(response.data);
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
40
requests/pod.ts
Normal file
40
requests/pod.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import axios from "axios";
|
||||
import type { Pod } from "~/classes/Pod";
|
||||
|
||||
export function getPods(namespace: string | undefined, onSuccess: (pods: Pod[]) => void)
|
||||
{
|
||||
axios.get<Pod[]>(useRuntimeConfig().public.apiBase + '/pods', {
|
||||
headers: {
|
||||
Authorization: "Bearer " + requireToken()
|
||||
},
|
||||
params: getParams(namespace)
|
||||
})
|
||||
.then((response) => {
|
||||
onSuccess(response.data);
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
|
||||
export function deletePod(id: string | undefined, onSuccess: () => void)
|
||||
{
|
||||
axios.delete(useRuntimeConfig().public.apiBase + '/pods/' + id, {
|
||||
headers: {
|
||||
Authorization: "Bearer " + requireToken()
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
onSuccess();
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
|
||||
function getParams(namespace: string | undefined)
|
||||
{
|
||||
if(namespace != undefined)
|
||||
{
|
||||
return {
|
||||
namespace: namespace
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
27
requests/services.ts
Normal file
27
requests/services.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import axios, { type AxiosRequestConfig } from "axios";
|
||||
import type { Service } from "~/classes/Service";
|
||||
|
||||
export function getServices(namespace: string | undefined, onSuccess: (services: Service[]) => void)
|
||||
{
|
||||
axios.get<Service[]>(useRuntimeConfig().public.apiBase + '/services', {
|
||||
headers: {
|
||||
Authorization: "Bearer " + requireToken()
|
||||
},
|
||||
params: getParams(namespace)
|
||||
})
|
||||
.then(response => {
|
||||
onSuccess(response.data);
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
|
||||
function getParams(namespace: string | undefined)
|
||||
{
|
||||
if(namespace != undefined)
|
||||
{
|
||||
return {
|
||||
namespace: namespace
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
59
requests/user.ts
Normal file
59
requests/user.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import axios from "axios";
|
||||
import type { User } from "~/classes/User";
|
||||
|
||||
export function getUser(username: string, token: string, onSuccess: (user: User) => void)
|
||||
{
|
||||
axios.get<User>(useRuntimeConfig().public.apiBase + '/users/' + username, {
|
||||
headers: {
|
||||
Authorization: "Bearer " + token
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
onSuccess(response.data);
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
|
||||
export function getUsers(onSuccess: (users: User[]) => void)
|
||||
{
|
||||
axios.get<User[]>(useRuntimeConfig().public.apiBase + '/users', {
|
||||
headers: {
|
||||
Authorization: "Bearer " + requireToken()
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
onSuccess(response.data);
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
|
||||
export function createUser(user: User, onSuccess: () => void)
|
||||
{
|
||||
axios.post(useRuntimeConfig().public.apiBase + '/users', user, {
|
||||
headers: {
|
||||
Authorization: "Bearer " + requireToken()
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
onSuccess();
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
|
||||
export function changePassword(username: string | undefined, password: string, onSuccess: () => void)
|
||||
{
|
||||
if(username == null)
|
||||
{
|
||||
throw new Error("[Method: changePassword] username is undefined.");
|
||||
}
|
||||
axios.put(useRuntimeConfig().public.apiBase + '/users/' + username + '/password', password, {
|
||||
headers: {
|
||||
Authorization: "Bearer " + requireToken(),
|
||||
"Content-Type": "text/plain"
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
onSuccess();
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
3
server/tsconfig.json
Normal file
3
server/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
||||
38
stores/NamespaceStore.ts
Normal file
38
stores/NamespaceStore.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import type { Namespace } from "~/classes/Namespace"
|
||||
import { getNamespaces } from "~/requests/namespace"
|
||||
|
||||
export const useNamespaceStore = defineStore('namespace', {
|
||||
state: () => ({
|
||||
namespaces: undefined as Namespace[] | undefined,
|
||||
currentNamespace: undefined as Namespace | undefined
|
||||
}),
|
||||
getters: {
|
||||
getNamespaces: (state) => {
|
||||
return (): Namespace[] | undefined => state.namespaces;
|
||||
},
|
||||
getCurrentNamespace: (state) => {
|
||||
return (): Namespace | undefined => state.currentNamespace;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
init() {
|
||||
getNamespaces((namespaces: Namespace[]) => {
|
||||
this.namespaces = namespaces;
|
||||
});
|
||||
},
|
||||
selectNamespace(namespace: Namespace | undefined) {
|
||||
if(namespace == null)
|
||||
{
|
||||
this.currentNamespace = undefined;
|
||||
}
|
||||
else if(this.currentNamespace?.metadata?.name !== namespace?.metadata?.name)
|
||||
{
|
||||
this.currentNamespace = namespace;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.currentNamespace = undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
47
utils/cookie.ts
Normal file
47
utils/cookie.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import type { Session } from "~/classes/Session";
|
||||
import type { User } from "~/classes/User";
|
||||
|
||||
export function requireToken(): string
|
||||
{
|
||||
const token = getToken();
|
||||
if(token != null)
|
||||
{
|
||||
return token;
|
||||
}
|
||||
throw new Error("No session set. Log in first.");
|
||||
}
|
||||
|
||||
export function getToken(): string | undefined
|
||||
{
|
||||
const session = getSession();
|
||||
if(session != null)
|
||||
{
|
||||
return session.token;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getUser(): User | undefined
|
||||
{
|
||||
const session = getSession();
|
||||
if(session != null)
|
||||
{
|
||||
return session.user;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function requireUser(): User
|
||||
{
|
||||
const session = getSession();
|
||||
if(session != null)
|
||||
{
|
||||
return session.user;
|
||||
}
|
||||
throw new Error("User is required but undefined");
|
||||
}
|
||||
|
||||
function getSession()
|
||||
{
|
||||
return useCookie<Session | undefined>('session').value;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user