🎉 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