Initial setup for new frontend.
This commit is contained in:
parent
7671bfd4d3
commit
59f7897d8e
33 changed files with 2594 additions and 12 deletions
17
frontend/src/App.vue
Normal file
17
frontend/src/App.vue
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import {RouterLink, RouterView} from "vue-router";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import PageFooter from "@/components/PageFooter.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader />
|
||||
|
||||
<main>
|
||||
<RouterView />
|
||||
</main>
|
||||
|
||||
<PageFooter />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
19
frontend/src/components/PageFooter.vue
Normal file
19
frontend/src/components/PageFooter.vue
Normal file
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import { useVersionStore } from "@/stores/version";
|
||||
|
||||
const version = useVersionStore();
|
||||
|
||||
function refresh(): void {
|
||||
version.refresh();
|
||||
}
|
||||
|
||||
refresh();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer>
|
||||
{{ version.getVersion }}
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
15
frontend/src/components/PageHeader.vue
Normal file
15
frontend/src/components/PageHeader.vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<div class="wrapper">
|
||||
<nav>
|
||||
<RouterLink to="/">Home</RouterLink> |
|
||||
<RouterLink to="/admin">Admin</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
12
frontend/src/main.ts
Normal file
12
frontend/src/main.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
|
||||
app.mount("#app");
|
21
frontend/src/router/index.ts
Normal file
21
frontend/src/router/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import AdminDashboardView from "@/views/AdminDashboardView.vue";
|
||||
import HomeView from "@/views/HomeView.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
name: "admin",
|
||||
component: AdminDashboardView,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default router;
|
29
frontend/src/stores/statistics.ts
Normal file
29
frontend/src/stores/statistics.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { isStatistics, type Statistics } from "@/types";
|
||||
import { internalApi } from "@/utils/Api";
|
||||
|
||||
interface StatisticsStoreState {
|
||||
statistics: Statistics | null;
|
||||
}
|
||||
|
||||
export const useStatisticsStore = defineStore({
|
||||
id: "statistics",
|
||||
state(): StatisticsStoreState {
|
||||
return {
|
||||
statistics: null,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
getStatistics(state: StatisticsStoreState): Statistics | null {
|
||||
return state.statistics;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async refresh(): Promise<void> {
|
||||
this.statistics = await internalApi.get<Statistics>(
|
||||
"statistics",
|
||||
isStatistics
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
38
frontend/src/stores/version.ts
Normal file
38
frontend/src/stores/version.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { isObject, isVersion, type Version } from "@/types";
|
||||
import { api } from "@/utils/Api";
|
||||
|
||||
interface VersionResponse {
|
||||
version: Version;
|
||||
}
|
||||
|
||||
function isVersionResponse(arg: unknown): arg is VersionResponse {
|
||||
return isObject(arg) && isVersion((arg as VersionResponse).version);
|
||||
}
|
||||
|
||||
interface VersionStoreState {
|
||||
version: Version | null;
|
||||
}
|
||||
|
||||
export const useVersionStore = defineStore({
|
||||
id: "version",
|
||||
state(): VersionStoreState {
|
||||
return {
|
||||
version: null,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
getVersion(state: VersionStoreState): Version | null {
|
||||
return state.version;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async refresh(): Promise<void> {
|
||||
const response = await api.get<VersionResponse>(
|
||||
"version",
|
||||
isVersionResponse
|
||||
);
|
||||
this.version = response.version;
|
||||
},
|
||||
},
|
||||
});
|
1
frontend/src/types/index.ts
Normal file
1
frontend/src/types/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./shared";
|
1
frontend/src/types/shared.ts
Symbolic link
1
frontend/src/types/shared.ts
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../server/types/shared.ts
|
37
frontend/src/utils/Api.ts
Normal file
37
frontend/src/utils/Api.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
class Api {
|
||||
private baseURL: string = import.meta.env.BASE_URL;
|
||||
private apiPrefix = "api/";
|
||||
|
||||
constructor(apiPrefix?: string) {
|
||||
if (apiPrefix) {
|
||||
this.apiPrefix = apiPrefix;
|
||||
}
|
||||
}
|
||||
|
||||
private toURL(path: string): string {
|
||||
return this.baseURL + this.apiPrefix + path;
|
||||
}
|
||||
|
||||
async get<T>(path: string, isT: (arg: unknown) => arg is T): Promise<T> {
|
||||
const url = this.toURL(path);
|
||||
const result = await fetch(url);
|
||||
const json = await result.json();
|
||||
|
||||
if (!isT(json)) {
|
||||
console.log(json);
|
||||
throw new Error(`API get result has wrong type. ${url} => ${json}`);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new Api();
|
||||
|
||||
class InternalApi extends Api {
|
||||
constructor() {
|
||||
super("internal/api/");
|
||||
}
|
||||
}
|
||||
|
||||
export const internalApi = new InternalApi();
|
30
frontend/src/views/AdminDashboardView.vue
Normal file
30
frontend/src/views/AdminDashboardView.vue
Normal file
|
@ -0,0 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
import { useStatisticsStore } from "@/stores/statistics";
|
||||
|
||||
const statistics = useStatisticsStore();
|
||||
|
||||
function refresh(): void {
|
||||
statistics.refresh();
|
||||
}
|
||||
|
||||
refresh();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div v-if="statistics.getStatistics">
|
||||
<h1>Nodes</h1>
|
||||
|
||||
<div>
|
||||
Registered: {{ statistics.getStatistics.nodes.registered }}<br />
|
||||
With VPN-key: {{ statistics.getStatistics.nodes.withVPN }}<br />
|
||||
With coordinates: {{ statistics.getStatistics.nodes.withCoords }}<br />
|
||||
Monitoring active: {{ statistics.getStatistics.nodes.monitoring.active }}<br />
|
||||
Monitoring pending: {{ statistics.getStatistics.nodes.monitoring.pending }}
|
||||
</div>
|
||||
<button @click="refresh()">Refresh</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
8
frontend/src/views/HomeView.vue
Normal file
8
frontend/src/views/HomeView.vue
Normal file
|
@ -0,0 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Home
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
Loading…
Add table
Add a link
Reference in a new issue