🎉 Init
This commit is contained in:
commit
c8cfb6eeab
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
|
||||
39
Jenkinsfile
vendored
Normal file
39
Jenkinsfile
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
stages {
|
||||
|
||||
stage('Set Image Name') {
|
||||
steps {
|
||||
script {
|
||||
env.IMAGE = "harbor.dinauer.dev/kubooboo/frontend:${env.BUILD_NUMBER}";
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Build Image') {
|
||||
steps {
|
||||
script {
|
||||
sh "docker build --no-cache -t ${env.IMAGE} ."
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Push Image to Docker Hub') {
|
||||
steps {
|
||||
script {
|
||||
withCredentials([usernamePassword(credentialsId: 'harbor', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
|
||||
sh 'echo ${PASSWORD} | docker login harbor.dinauer.dev -u ${USERNAME} --password-stdin'
|
||||
sh "docker push ${env.IMAGE}"
|
||||
sh "docker logout harbor.dinauer.dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Remove image from host') {
|
||||
steps {
|
||||
script {
|
||||
sh "docker image rm --force ${env.IMAGE}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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.
|
||||
22
app/app.vue
Normal file
22
app/app.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
<NuxtPage></NuxtPage>
|
||||
<PopupTemplate v-if="popup" :heading="popup.config.heading" :size="popup.config.size">
|
||||
<component :is="popup.component"></component>
|
||||
</PopupTemplate>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {usePopup} from "~/components/ui/popup/Popup";
|
||||
import PopupTemplate from "~/components/ui/popup/PopupTemplate.vue";
|
||||
|
||||
const popup = computed(() => usePopup().get());
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.app {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
</style>
|
||||
256
app/assets/base-style.css
Normal file
256
app/assets/base-style.css
Normal file
@ -0,0 +1,256 @@
|
||||
.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(--tile-color);
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
border: 1px solid #cddaff;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.gap-m {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.gap-l {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.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.25rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.width-6rem {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap
|
||||
}
|
||||
BIN
app/assets/logo.png
Normal file
BIN
app/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
27
app/assets/style.css
Normal file
27
app/assets/style.css
Normal file
@ -0,0 +1,27 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
|
||||
html, body, #__nuxt {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
|
||||
html {
|
||||
--tile-color: #eef6ff;
|
||||
--primary-color: #3c74ff;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 550;
|
||||
}
|
||||
43
app/auth/Account.ts
Normal file
43
app/auth/Account.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import axios from "axios";
|
||||
import {AuthApi} from "~/utils/AuthApi";
|
||||
|
||||
export class Account
|
||||
{
|
||||
firstname?: string;
|
||||
lastname?: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
roles?: string[];
|
||||
initial?: boolean;
|
||||
|
||||
static get(id: string, token: string, onSuccess: (account: Account) => void)
|
||||
{
|
||||
AuthApi.get(StringUtils.format('/accounts/%s', id), {
|
||||
headers: {
|
||||
Authorization: StringUtils.format("Bearer %s", token)
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
onSuccess(response.data);
|
||||
});
|
||||
}
|
||||
|
||||
static create(user: AccountCreation, onSuccess: () => void)
|
||||
{
|
||||
AuthApi.post('/accounts', user)
|
||||
.then(() => {
|
||||
onSuccess();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountCreation
|
||||
{
|
||||
firstname?: string;
|
||||
lastname?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
username?: string;
|
||||
role: string = "USER";
|
||||
}
|
||||
32
app/auth/Session.ts
Normal file
32
app/auth/Session.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import {type AxiosResponse} from "axios";
|
||||
import {AuthApi} from "~/utils/AuthApi";
|
||||
import type {Account} from "~/auth/Account";
|
||||
|
||||
export class Session
|
||||
{
|
||||
static COOKIE = "session";
|
||||
|
||||
constructor (
|
||||
public user: Account,
|
||||
public token: string
|
||||
) {}
|
||||
|
||||
static create(sessionCreation: SessionCreation, onSuccess: (token: string) => void, onError: () => void)
|
||||
{
|
||||
AuthApi.post<string>("/sessions", sessionCreation)
|
||||
.then((response: AxiosResponse) => {
|
||||
onSuccess(response.data);
|
||||
})
|
||||
.catch(() => {
|
||||
onError();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionCreation
|
||||
{
|
||||
constructor (
|
||||
public email?: string,
|
||||
public password?: string
|
||||
) {}
|
||||
}
|
||||
44
app/auth/Token.ts
Normal file
44
app/auth/Token.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {MavenApi} from "~/utils/MavenApi";
|
||||
|
||||
export class Token
|
||||
{
|
||||
constructor(
|
||||
public name: string,
|
||||
public createdAt: Date,
|
||||
public expiresAt: Date
|
||||
) {}
|
||||
|
||||
static get(onSuccess: (tokens: Token[]) => void)
|
||||
{
|
||||
MavenApi.get<Token[]>("/tokens")
|
||||
.then((response) => {
|
||||
onSuccess(response.data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenCreation
|
||||
{
|
||||
constructor(
|
||||
public name: string,
|
||||
public expiresAt: Date
|
||||
) {}
|
||||
|
||||
static create(tokenCreation: TokenCreation, onSuccess: (token: TokenSecret) => void)
|
||||
{
|
||||
MavenApi.post<TokenSecret>("/tokens", tokenCreation)
|
||||
.then((response) => {
|
||||
onSuccess(response.data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenSecret
|
||||
{
|
||||
constructor(
|
||||
public name: string,
|
||||
public expiresAt: Date,
|
||||
public createdAt: Date,
|
||||
public token: string
|
||||
) {}
|
||||
}
|
||||
58
app/components/artifact/Artifact.ts
Normal file
58
app/components/artifact/Artifact.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import axios from "axios";
|
||||
|
||||
export class Artifact
|
||||
{
|
||||
|
||||
constructor (
|
||||
public id: string,
|
||||
public groupId: string,
|
||||
public artifactId: string,
|
||||
public updatedAt: string,
|
||||
public versions: Version[],
|
||||
public totalPullCount: number
|
||||
) {}
|
||||
|
||||
static get(onSuccess: (artifacts: Artifact[]) => void)
|
||||
{
|
||||
axios.get<Artifact[]>("http://localhost:8080/artifacts")
|
||||
.then((response) => {
|
||||
onSuccess(response.data)
|
||||
});
|
||||
}
|
||||
|
||||
static getById(id: string, onSuccess: (artifact: Artifact) => void)
|
||||
{
|
||||
axios.get<Artifact>("http://localhost:8080/artifacts/" + id)
|
||||
.then((response) => {
|
||||
onSuccess(response.data)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class Version
|
||||
{
|
||||
constructor (
|
||||
public id: string,
|
||||
public groupId: string,
|
||||
public artifactId: string,
|
||||
public version: string,
|
||||
public jars?: Jar[],
|
||||
public pom?: Pom
|
||||
) {}
|
||||
}
|
||||
|
||||
export class Jar
|
||||
{
|
||||
constructor (
|
||||
public filename: string,
|
||||
public url: string
|
||||
) {}
|
||||
}
|
||||
|
||||
export class Pom
|
||||
{
|
||||
constructor (
|
||||
public filename: string,
|
||||
public url: string
|
||||
) {}
|
||||
}
|
||||
26
app/components/artifact/ArtifactComponent.vue
Normal file
26
app/components/artifact/ArtifactComponent.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<ContentRow class="artifact pointer" @click="() => useRouter().push('artifacts/' + artifact.id)">
|
||||
<ContentCell>{{ artifact.groupId }}</ContentCell>
|
||||
<ContentCell>{{ artifact.artifactId }}</ContentCell>
|
||||
<ContentCell v-for="latest in [artifact.versions.at(0)]"><span v-if="latest">{{ latest.version }}</span></ContentCell>
|
||||
<ContentCell>{{ artifact.totalPullCount }}</ContentCell>
|
||||
<ContentCell>{{ dayjs(artifact.updatedAt).format("DD.MM.YYYY HH:mm") + " Uhr" }}</ContentCell>
|
||||
</ContentRow>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {Artifact} from "~/components/artifact/Artifact";
|
||||
import dayjs from "dayjs";
|
||||
import ContentRow from "~/components/ui/table/ContentRow.vue";
|
||||
import ContentCell from "~/components/ui/table/ContentCell.vue";
|
||||
|
||||
defineProps<{
|
||||
artifact: Artifact
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.artifact {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
32
app/components/artifact/ArtifactList.vue
Normal file
32
app/components/artifact/ArtifactList.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<Table v-if="artifacts" columns="auto auto auto 1fr auto">
|
||||
<HeaderRow>
|
||||
<HeaderCell>Group ID</HeaderCell>
|
||||
<HeaderCell>Artifact ID</HeaderCell>
|
||||
<HeaderCell>Latest Version</HeaderCell>
|
||||
<HeaderCell>Pull Count</HeaderCell>
|
||||
<HeaderCell>Updated At</HeaderCell>
|
||||
</HeaderRow>
|
||||
<ArtifactComponent :artifact="artifact" v-for="artifact in artifacts"></ArtifactComponent>
|
||||
</Table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {Artifact} from "~/components/artifact/Artifact";
|
||||
import ArtifactComponent from "~/components/artifact/ArtifactComponent.vue";
|
||||
import Table from "~/components/ui/table/Table.vue";
|
||||
import HeaderCell from "~/components/ui/table/HeaderCell.vue";
|
||||
import HeaderRow from "~/components/ui/table/HeaderRow.vue";
|
||||
|
||||
const artifacts: Ref<Artifact[] | undefined> = ref(undefined);
|
||||
|
||||
onMounted(() => {
|
||||
Artifact.get((_artifacts: Artifact[]) => {
|
||||
artifacts.value = _artifacts;
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
20
app/components/artifact/Codebox.vue
Normal file
20
app/components/artifact/Codebox.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="codebox">
|
||||
<p v-html="content"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
content: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.codebox * {
|
||||
font-family: "Roboto Mono", monospace;
|
||||
font-optical-sizing: auto;
|
||||
white-space: preserve;
|
||||
tab-size: 2;
|
||||
}
|
||||
</style>
|
||||
18
app/components/auth/DisplayNameComponent.vue
Normal file
18
app/components/auth/DisplayNameComponent.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div>
|
||||
<p v-if="account && account.firstname && account.firstname">{{ account.firstname }}, {{ account.lastname }}</p>
|
||||
<p v-else>-</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {Account} from "~/auth/Account";
|
||||
|
||||
defineProps<{
|
||||
account?: Account
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
43
app/components/auth/LoginComponent.vue
Normal file
43
app/components/auth/LoginComponent.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<h1>Login</h1>
|
||||
<UiInput label="E-Mail">
|
||||
<input type="text" v-model="sessionCreation.email">
|
||||
</UiInput>
|
||||
<UiInput label="Passwort">
|
||||
<input type="password" v-model="sessionCreation.password">
|
||||
</UiInput>
|
||||
<UiButton @click="login">Login</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {Session, SessionCreation} from "~/auth/Session";
|
||||
import {jwtDecode} from "jwt-decode";
|
||||
import {Account} from "~/auth/Account";
|
||||
|
||||
const sessionCreation = ref(function() {
|
||||
if (StringUtils.equals("development", process.env.NODE_ENV))
|
||||
{
|
||||
return new SessionCreation("andreas.j.dinauer@gmail.com", "pw")
|
||||
}
|
||||
return new SessionCreation();
|
||||
}());
|
||||
|
||||
function login()
|
||||
{
|
||||
Session.create(sessionCreation.value, (token: string) => {
|
||||
const decode = jwtDecode(token) as any;
|
||||
Account.get(decode.upn, token, (user: Account) => {
|
||||
useCookie<Session>(Session.COOKIE).value = new Session(user, token);
|
||||
useRouter().push('/app/artifacts');
|
||||
});
|
||||
}, () => {})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
max-width: 540px;
|
||||
}
|
||||
</style>
|
||||
35
app/components/events/Event.ts
Normal file
35
app/components/events/Event.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import axios from "axios";
|
||||
import type {Account} from "~/auth/Account";
|
||||
|
||||
export class Event
|
||||
{
|
||||
constructor (
|
||||
public type: EventType,
|
||||
public timestamp: Date,
|
||||
public resource: Resource,
|
||||
public account: Account
|
||||
) {}
|
||||
|
||||
static get(onSuccess: (events: Event[]) => void)
|
||||
{
|
||||
axios.get<Event[]>("http://localhost:8080/events")
|
||||
.then((response) => {
|
||||
onSuccess(response.data)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class Resource
|
||||
{
|
||||
constructor (
|
||||
public groupId: string,
|
||||
public artifactId: string,
|
||||
public version: string
|
||||
) {}
|
||||
}
|
||||
|
||||
export enum EventType
|
||||
{
|
||||
UPLOAD = "UPLOAD",
|
||||
DELETE = "DELETE"
|
||||
}
|
||||
30
app/components/events/EventComponent.vue
Normal file
30
app/components/events/EventComponent.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<ContentRow>
|
||||
<ContentCell>
|
||||
<UiIcon v-if="event.type === EventType.UPLOAD">upload</UiIcon>
|
||||
</ContentCell>
|
||||
<ContentCell> {{ event.resource.groupId }}</ContentCell>
|
||||
<ContentCell>{{ event.resource.artifactId }}</ContentCell>
|
||||
<ContentCell>{{ event.resource.version }}</ContentCell>
|
||||
<ContentCell>
|
||||
<DisplayNameComponent :account="event.account"></DisplayNameComponent>
|
||||
</ContentCell>
|
||||
<ContentCell>{{ Age.calc(event.timestamp) }}</ContentCell>
|
||||
</ContentRow>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ContentRow from "~/components/ui/table/ContentRow.vue";
|
||||
import ContentCell from "~/components/ui/table/ContentCell.vue";
|
||||
import {Event, EventType} from "~/components/events/Event";
|
||||
import {Age} from "~/utils/Age";
|
||||
import DisplayNameComponent from "~/components/auth/DisplayNameComponent.vue";
|
||||
|
||||
defineProps<{
|
||||
event: Event
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
33
app/components/events/EventList.vue
Normal file
33
app/components/events/EventList.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<Table columns="auto auto auto auto 1fr auto" v-if="events">
|
||||
<HeaderRow>
|
||||
<HeaderCell></HeaderCell>
|
||||
<HeaderCell>Group ID</HeaderCell>
|
||||
<HeaderCell>Artifact ID</HeaderCell>
|
||||
<HeaderCell>Version</HeaderCell>
|
||||
<HeaderCell>User</HeaderCell>
|
||||
<HeaderCell></HeaderCell>
|
||||
</HeaderRow>
|
||||
<EventComponent :event="event" v-for="event in events"></EventComponent>
|
||||
</Table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Table from "~/components/ui/table/Table.vue";
|
||||
import HeaderRow from "~/components/ui/table/HeaderRow.vue";
|
||||
import EventComponent from "~/components/events/EventComponent.vue";
|
||||
import HeaderCell from "~/components/ui/table/HeaderCell.vue";
|
||||
import {Event} from "~/components/events/Event";
|
||||
|
||||
const events: Ref<Event[] | undefined> = ref(undefined);
|
||||
|
||||
onMounted(() => {
|
||||
Event.get((_events: Event[]) => {
|
||||
events.value = _events;
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
16
app/components/group/Group.ts
Normal file
16
app/components/group/Group.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import axios from "axios";
|
||||
|
||||
export class Group
|
||||
{
|
||||
constructor (
|
||||
public groupId: string
|
||||
) {}
|
||||
|
||||
static get(onSuccess: (groups: Group[]) => void)
|
||||
{
|
||||
axios.get<Group[]>("http://localhost:8080/groups")
|
||||
.then((response) => {
|
||||
onSuccess(response.data)
|
||||
});
|
||||
}
|
||||
}
|
||||
11
app/components/group/GroupComponent.vue
Normal file
11
app/components/group/GroupComponent.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
49
app/components/settings/token/TokenCreationComponent.vue
Normal file
49
app/components/settings/token/TokenCreationComponent.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!token" class="content-l">
|
||||
<div class="col-2">
|
||||
<UiInput label="Name" required>
|
||||
<input type="text" v-model="name">
|
||||
</UiInput>
|
||||
<UiInput label="Expires At">
|
||||
<input type="date" v-model="expiresAt">
|
||||
</UiInput>
|
||||
</div>
|
||||
<div class="center">
|
||||
<UiButton @click="create" :disabled="name.length === 0">Create</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="content-l">
|
||||
<p class="tile-m">{{ token.token }}</p>
|
||||
<div class="center">
|
||||
<UiButton class="width-6rem">Copy</UiButton>
|
||||
<UiButton class="width-6rem" @click="usePopup().close()">Close</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {TokenCreation, TokenSecret} from "~/auth/Token";
|
||||
import {usePopup} from "~/components/ui/popup/Popup";
|
||||
|
||||
const name: Ref<string> = ref('');
|
||||
const expiresAt = ref(new Date().toISOString().substring(0, 10));
|
||||
|
||||
const token: Ref<TokenSecret | undefined> = ref(undefined);
|
||||
function create()
|
||||
{
|
||||
if (!name.value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
TokenCreation.create(new TokenCreation(name.value, new Date(expiresAt.value)), (_token: TokenSecret) => {
|
||||
token.value = _token;
|
||||
usePopup().get()!.config.callback?.(_token);
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
61
app/components/ui/UiButton.vue
Normal file
61
app/components/ui/UiButton.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<button @click="() => click()" class="base-shape button pointer center" :class="{ loading: loading, reverse: reverse, disabled: disabled }"><UiIcon v-if="icon && !loading">{{ icon }}</UiIcon><slot></slot></button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import UiIcon from '@/components/ui/UiIcon.vue';
|
||||
|
||||
function click() {
|
||||
if(!props.disabled) {
|
||||
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;
|
||||
outline: none;
|
||||
gap: 0.5rem;
|
||||
white-space: nowrap;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
}
|
||||
.button * {
|
||||
color: white;
|
||||
fill: white;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
</style>
|
||||
14
app/components/ui/UiIcon.vue
Normal file
14
app/components/ui/UiIcon.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<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;
|
||||
}
|
||||
</style>
|
||||
52
app/components/ui/UiInput.vue
Normal file
52
app/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 #444444;
|
||||
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>
|
||||
71
app/components/ui/popup/Popup.ts
Normal file
71
app/components/ui/popup/Popup.ts
Normal file
@ -0,0 +1,71 @@
|
||||
export class Popup
|
||||
{
|
||||
constructor (
|
||||
public component: Component,
|
||||
public config: PopupConfig,
|
||||
public payload?: any
|
||||
) {}
|
||||
}
|
||||
|
||||
export interface PopupConfig
|
||||
{
|
||||
heading: string;
|
||||
size: PopupSize;
|
||||
callback?: (payload?: any) => void;
|
||||
}
|
||||
|
||||
export enum PopupSize
|
||||
{
|
||||
SMALL = "small", MEDIUM = "medium", LARGE = "large", FULL = "full"
|
||||
}
|
||||
|
||||
export const usePopup = defineStore('popup', {
|
||||
state: () => ({
|
||||
popup: shallowRef<Popup | undefined>(undefined)
|
||||
}),
|
||||
getters: {
|
||||
get: (state) => {
|
||||
return () => {
|
||||
return state.popup;
|
||||
}
|
||||
},
|
||||
require: (state) => {
|
||||
return () => {
|
||||
if (state.popup)
|
||||
{
|
||||
return state.popup;
|
||||
}
|
||||
throw new Error('Expected to be in open popup state.');
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
open(popup: Popup) {
|
||||
this.popup = popup;
|
||||
disableScrolling();
|
||||
},
|
||||
close() {
|
||||
this.popup = undefined;
|
||||
enableScrolling();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
80
app/components/ui/popup/PopupTemplate.vue
Normal file
80
app/components/ui/popup/PopupTemplate.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="overlay center" @click="usePopup().close()">
|
||||
<div class="popup" :class="size" @click.stop>
|
||||
<div class="popup__header">
|
||||
<h2>{{ heading }}</h2>
|
||||
<UiButton icon="close" @click="usePopup().close()" class="square"></UiButton>
|
||||
</div>
|
||||
<div class="popup__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {PopupSize, usePopup} from "~/components/ui/popup/Popup";
|
||||
|
||||
defineProps<{
|
||||
heading: string,
|
||||
size: PopupSize
|
||||
}>()
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if(event.key === 'Escape')
|
||||
{
|
||||
usePopup().close();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.overlay {
|
||||
background-color: rgba(54, 54, 54, 0.514);
|
||||
backdrop-filter: blur(0.1rem);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0.5rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.popup {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
align-content: flex-start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.popup__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.popup__body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.medium {
|
||||
height: auto;
|
||||
width: 740px;
|
||||
}
|
||||
.small {
|
||||
height: auto;
|
||||
width: 540px;
|
||||
}
|
||||
.large {
|
||||
height: auto;
|
||||
width: 740px;
|
||||
}
|
||||
.full {
|
||||
height: auto;
|
||||
width: 540px;
|
||||
}
|
||||
</style>
|
||||
15
app/components/ui/table/ContentCell.vue
Normal file
15
app/components/ui/table/ContentCell.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="cell left-center">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cell {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
</style>
|
||||
24
app/components/ui/table/ContentRow.vue
Normal file
24
app/components/ui/table/ContentRow.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="content-row display-contents">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.display-contents {
|
||||
display: contents;
|
||||
}
|
||||
.content-row > * {
|
||||
border-bottom: 1px solid #c1c1c1;
|
||||
}
|
||||
.content-row:hover > * {
|
||||
background-color: var(--tile-color);
|
||||
}
|
||||
.content-row:last-of-type * {
|
||||
border-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
20
app/components/ui/table/HeaderCell.vue
Normal file
20
app/components/ui/table/HeaderCell.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="header">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
border-bottom: 1px solid #c1c1c1;
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: #efefef;
|
||||
}
|
||||
.header, .header * {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
15
app/components/ui/table/HeaderRow.vue
Normal file
15
app/components/ui/table/HeaderRow.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="display-contents">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.display-contents {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
30
app/components/ui/table/Table.vue
Normal file
30
app/components/ui/table/Table.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="table">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
columns: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.table {
|
||||
display: grid;
|
||||
grid-template-columns: v-bind(columns);
|
||||
border: 1px solid #c1c1c1;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.header * {
|
||||
font-weight: 600;
|
||||
}
|
||||
.artifact-list .artifact:hover * {
|
||||
background-color: var(--tile-color);
|
||||
}
|
||||
.artifact-list .artifact:last-of-type * {
|
||||
border-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
14
app/middleware/auth.global.ts
Normal file
14
app/middleware/auth.global.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {Session} from "~/auth/Session";
|
||||
|
||||
export default defineNuxtRouteMiddleware((to) => {
|
||||
const authenticated = useCookie<Session>(Session.COOKIE).value != null;
|
||||
const path = to.path;
|
||||
if (StringUtils.startsWith(path, '/app') && !authenticated)
|
||||
{
|
||||
return navigateTo('/')
|
||||
}
|
||||
if (StringUtils.equals(path, '/') && authenticated)
|
||||
{
|
||||
return navigateTo('/app')
|
||||
}
|
||||
})
|
||||
42
app/pages/app.vue
Normal file
42
app/pages/app.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<div>
|
||||
<NuxtLink class="link left-center" :class="{ active: useRoute().fullPath === '/app' }" to="/app"><UiIcon>home</UiIcon>Home</NuxtLink>
|
||||
<NuxtLink class="link left-center" :class="{ active: useRoute().fullPath.startsWith('/app/artifacts') }" to="/app/artifacts"><UiIcon>box</UiIcon>Artifacts</NuxtLink>
|
||||
</div>
|
||||
<NuxtLink class="link left-center" :class="{ active: useRoute().fullPath.startsWith('/app/settings') }" to="/app/settings"><UiIcon>settings</UiIcon>Settings</NuxtLink>
|
||||
</div>
|
||||
<NuxtPage></NuxtPage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
.page > * {
|
||||
padding: 1rem;
|
||||
}
|
||||
.sidebar {
|
||||
background-color: var(--tile-color);
|
||||
border-right: 1px solid #cddaff;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
}
|
||||
.link {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.link.active, .link.active * {
|
||||
background-color: #3c74ff;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
75
app/pages/app/artifacts/[id]/index.vue
Normal file
75
app/pages/app/artifacts/[id]/index.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div v-if="artifact" class="content-l">
|
||||
<h1>Artifact</h1>
|
||||
<div class="col-2">
|
||||
<p class="tile-m">{{ artifact.groupId }}</p>
|
||||
<p class="tile-m">{{ artifact.artifactId }}</p>
|
||||
</div>
|
||||
<div class="artifact-page">
|
||||
<div class="content-m" v-if="selectedVersion">
|
||||
<h2>Dependency</h2>
|
||||
<div class="tile-m" v-if="dependency">
|
||||
<Codebox :content="dependency"></Codebox>
|
||||
</div>
|
||||
<div class="content-m">
|
||||
<h2>Files</h2>
|
||||
<p class="tile-m" v-if="selectedVersion.jars" v-for="jar in selectedVersion.jars" @click="Download.download(jar.url)">{{ jar.filename }}</p>
|
||||
<a class="tile-m" v-if="selectedVersion.pom" :href="MavenApi.defaults.baseURL + '/maven2/' + selectedVersion.pom.url" target="_blank">{{ selectedVersion.pom.filename }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-m">
|
||||
<h2>Versions</h2>
|
||||
<p class="tile-m pointer" v-for="version in artifact.versions" :class="{ active: (selectedVersion != null && version.id === selectedVersion.id) }" @click="() => { selectedVersion = version }">{{ version.version }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {Artifact, Version} from "~/components/artifact/Artifact";
|
||||
import Codebox from "~/components/artifact/Codebox.vue";
|
||||
import {Download} from "~/utils/Download";
|
||||
import {MavenApi} from "~/utils/MavenApi";
|
||||
const artifact: Ref<Artifact | undefined> = ref(undefined);
|
||||
const selectedVersion: Ref<Version | undefined> = ref(undefined);
|
||||
|
||||
onMounted(() => {
|
||||
const id = useRoute().params.id as string;
|
||||
Artifact.getById(id, (_artifact: Artifact) => {
|
||||
artifact.value = _artifact;
|
||||
const _version = _artifact.versions.at(0);
|
||||
if (_version != null)
|
||||
{
|
||||
selectedVersion.value = _version;
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const dependency = computed(() => {
|
||||
if (selectedVersion.value)
|
||||
{
|
||||
const template = [
|
||||
"<dependency>",
|
||||
"\t<groupId>" + selectedVersion.value.groupId + "</groupId>",
|
||||
"\t<artifactId>" + selectedVersion.value.artifactId + "</artifactId>",
|
||||
"\t<version>" + selectedVersion.value.version + "</version>",
|
||||
"</dependency>"
|
||||
]
|
||||
return template.join("\n").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.artifact-page {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 10rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
.active {
|
||||
background-color: #3c74ff;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
14
app/pages/app/artifacts/index.vue
Normal file
14
app/pages/app/artifacts/index.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div class="content-l">
|
||||
<h1>Artifacts</h1>
|
||||
<ArtifactList></ArtifactList>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ArtifactList from "~/components/artifact/ArtifactList.vue";
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
15
app/pages/app/index.vue
Normal file
15
app/pages/app/index.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="content-l">
|
||||
<h1>Home</h1>
|
||||
<h2>Recent Events</h2>
|
||||
<EventList></EventList>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import EventList from "~/components/events/EventList.vue";
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
66
app/pages/app/settings/index.vue
Normal file
66
app/pages/app/settings/index.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="content-l">
|
||||
<h1>Account Settings</h1>
|
||||
<div class="col-3">
|
||||
<div class="tile-m">
|
||||
<h3>Username</h3>
|
||||
<DisplayNameComponent :account="session.user"></DisplayNameComponent>
|
||||
</div>
|
||||
<div class="tile-m">
|
||||
<h3>E-Mail</h3>
|
||||
<p>andreas.j.dinauer@gmail.com</p>
|
||||
</div>
|
||||
<div class="tile-m">
|
||||
<h3>Password</h3>
|
||||
<p>************</p>
|
||||
</div>
|
||||
</div>
|
||||
<h1>Access Tokens</h1>
|
||||
<div v-if="tokens">
|
||||
<p v-if="tokens.length === 0" class="tile-m">No tokens created yet.</p>
|
||||
<UiTable v-else columns="auto 1fr auto auto">
|
||||
<HeaderRow>
|
||||
<HeaderCell>Name</HeaderCell>
|
||||
<HeaderCell>Created At</HeaderCell>
|
||||
<HeaderCell>Expires At</HeaderCell>
|
||||
<HeaderCell></HeaderCell>
|
||||
</HeaderRow>
|
||||
<ContentRow v-for="token in tokens">
|
||||
<ContentCell>{{ token.name }}</ContentCell>
|
||||
<ContentCell>{{ dayjs(token.createdAt).format("DD.MM.YYYY") }}</ContentCell>
|
||||
<ContentCell>{{ dayjs(token.expiresAt).format("DD.MM.YYYY") }}</ContentCell>
|
||||
<ContentCell style="padding: 0.25rem"><UiButton icon="delete"></UiButton></ContentCell>
|
||||
</ContentRow>
|
||||
</UiTable>
|
||||
</div>
|
||||
<div>
|
||||
<UiButton @click="usePopup().open(new Popup(TokenCreationComponent, { size: PopupSize.MEDIUM, heading: 'Create Token', callback: (token: TokenSecret) => { tokens?.push(new Token(token.name, token.createdAt, token.expiresAt)) } }))">Create</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DisplayNameComponent from "~/components/auth/DisplayNameComponent.vue";
|
||||
import {Session} from "~/auth/Session";
|
||||
import TokenCreationComponent from "~/components/settings/token/TokenCreationComponent.vue";
|
||||
import {Popup, PopupSize, usePopup} from "~/components/ui/popup/Popup";
|
||||
import {Token, type TokenSecret} from "~/auth/Token";
|
||||
import HeaderCell from "~/components/ui/table/HeaderCell.vue";
|
||||
import HeaderRow from "~/components/ui/table/HeaderRow.vue";
|
||||
import ContentRow from "~/components/ui/table/ContentRow.vue";
|
||||
import ContentCell from "~/components/ui/table/ContentCell.vue";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const session = useCookie<Session>(Session.COOKIE);
|
||||
|
||||
const tokens: Ref<Token[] | undefined> = ref(undefined);
|
||||
onMounted(() => {
|
||||
Token.get((_tokens: Token[]) => {
|
||||
tokens.value = _tokens;
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
12
app/pages/index.vue
Normal file
12
app/pages/index.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<LoginComponent></LoginComponent>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import LoginComponent from "~/components/auth/LoginComponent.vue";
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
17
app/server/download.ts
Normal file
17
app/server/download.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { defineEventHandler } from 'h3'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (event.context.params)
|
||||
{
|
||||
const filename = event.context.params.path;
|
||||
const url = `http://localhost:8080/${filename}`
|
||||
|
||||
const res = await fetch(url)
|
||||
const data = await res.arrayBuffer()
|
||||
|
||||
event.res.setHeader('Content-Type', res.headers.get('content-type') || 'application/octet-stream')
|
||||
event.res.setHeader('Content-Disposition', `attachment; filename="${filename}"`)
|
||||
|
||||
return Buffer.from(data)
|
||||
}
|
||||
})
|
||||
83
app/utils/Age.ts
Normal file
83
app/utils/Age.ts
Normal file
@ -0,0 +1,83 @@
|
||||
export class Age
|
||||
{
|
||||
static calc(date: Date)
|
||||
{
|
||||
const today = new Date().getTime() / 1000;
|
||||
const createdAt = new Date(date).getTime() / 1000;
|
||||
const dif = today - createdAt;
|
||||
if (dif < 60)
|
||||
{
|
||||
return Age.format(dif, ChronoUnit.SECOND) + " ago";
|
||||
}
|
||||
if (dif < 60 * 60)
|
||||
{
|
||||
const minutes = Math.floor((dif / 60));
|
||||
return Age.format(minutes, ChronoUnit.MINUTE) + " ago";
|
||||
}
|
||||
if (dif < 60 * 60 * 24)
|
||||
{
|
||||
const hours = Math.floor(dif / (60 * 60));
|
||||
return Age.format(hours, ChronoUnit.HOUR) + " ago";
|
||||
}
|
||||
else
|
||||
{
|
||||
const days = Math.floor(dif / (60 * 60 * 24));
|
||||
return Age.format(days, ChronoUnit.DAY) + " ago";
|
||||
}
|
||||
}
|
||||
|
||||
static format(count: number, unit: ChronoUnit): string
|
||||
{
|
||||
if (unit === ChronoUnit.SECOND)
|
||||
{
|
||||
if (count != 1)
|
||||
{
|
||||
return count.toFixed(0) + " seconds";
|
||||
}
|
||||
else
|
||||
{
|
||||
return count.toFixed(0) + " second";
|
||||
}
|
||||
}
|
||||
if (unit === ChronoUnit.MINUTE)
|
||||
{
|
||||
if (count != 1)
|
||||
{
|
||||
return count.toFixed(0) + " minutes";
|
||||
}
|
||||
else
|
||||
{
|
||||
return count.toFixed(0) + " minute";
|
||||
}
|
||||
}
|
||||
if (unit === ChronoUnit.HOUR)
|
||||
{
|
||||
if (count != 1)
|
||||
{
|
||||
return count.toFixed(0) + " hours";
|
||||
}
|
||||
else
|
||||
{
|
||||
return count.toFixed(0) + " hour";
|
||||
}
|
||||
}
|
||||
if (unit === ChronoUnit.DAY)
|
||||
{
|
||||
if (count != 1)
|
||||
{
|
||||
return count.toFixed(0) + " days";
|
||||
}
|
||||
else
|
||||
{
|
||||
return count.toFixed(0) + " day";
|
||||
}
|
||||
}
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum ChronoUnit
|
||||
{
|
||||
SECOND, MINUTE, HOUR, DAY
|
||||
}
|
||||
19
app/utils/AuthApi.ts
Normal file
19
app/utils/AuthApi.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import axios, {type AxiosInstance} from "axios";
|
||||
import {Session} from "~/auth/Session";
|
||||
|
||||
export const AuthApi: AxiosInstance = function()
|
||||
{
|
||||
const instance: AxiosInstance = axios.create({
|
||||
baseURL: 'http://localhost:8089/api/iam-backend'
|
||||
});
|
||||
instance.interceptors.request.use((config) =>
|
||||
{
|
||||
const session: Session = useCookie<Session>(Session.COOKIE).value
|
||||
if (session)
|
||||
{
|
||||
config.headers["Authorization"] = StringUtils.format("Bearer %s", session.token)
|
||||
}
|
||||
return config;
|
||||
})
|
||||
return instance;
|
||||
}();
|
||||
12
app/utils/Download.ts
Normal file
12
app/utils/Download.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export class Download
|
||||
{
|
||||
static download(url: string)
|
||||
{
|
||||
const a = document.createElement("a");
|
||||
a.style.display = "none";
|
||||
a.href = "http://localhost:8080/maven2/" + url;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
}
|
||||
19
app/utils/MavenApi.ts
Normal file
19
app/utils/MavenApi.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import axios, {type AxiosInstance} from "axios";
|
||||
import {Session} from "~/auth/Session";
|
||||
|
||||
export const MavenApi: AxiosInstance = function()
|
||||
{
|
||||
const instance: AxiosInstance = axios.create({
|
||||
baseURL: 'http://localhost:8080'
|
||||
});
|
||||
instance.interceptors.request.use((config) =>
|
||||
{
|
||||
const session: Session = useCookie<Session>(Session.COOKIE).value
|
||||
if (session)
|
||||
{
|
||||
config.headers["Authorization"] = StringUtils.format("Bearer %s", session.token)
|
||||
}
|
||||
return config;
|
||||
})
|
||||
return instance;
|
||||
}();
|
||||
36
app/utils/StringUtils.ts
Normal file
36
app/utils/StringUtils.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export class StringUtils
|
||||
{
|
||||
static format(template: string, ...varContext: string[]): string
|
||||
{
|
||||
const context = Array.from(varContext);
|
||||
let contextIndex = 0;
|
||||
while (template.includes("%s"))
|
||||
{
|
||||
const currentContext = context.at(contextIndex);
|
||||
if (currentContext != null)
|
||||
{
|
||||
template = template.replace("%s", currentContext);
|
||||
contextIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
return template;
|
||||
}
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
static equals(first: string | null | undefined, second: string | null | undefined): boolean
|
||||
{
|
||||
return first === second;
|
||||
}
|
||||
|
||||
static startsWith(input: string | null | undefined, start: string | null | undefined)
|
||||
{
|
||||
if (input != null && start != null)
|
||||
{
|
||||
return input.startsWith(start);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
20
nuxt.config.ts
Normal file
20
nuxt.config.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true },
|
||||
|
||||
css: [
|
||||
'@/assets/style.css',
|
||||
'@/assets/base-style.css'
|
||||
],
|
||||
|
||||
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']
|
||||
})
|
||||
10162
package-lock.json
generated
Normal file
10162
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"axios": "^1.13.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"nuxt": "^4.3.0",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.27",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.1.0"
|
||||
}
|
||||
}
|
||||
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:
|
||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user