Dashboard work. Move configs to DB. Some script reorganization. Add nodemon configs.

This commit is contained in:
Dragory 2019-06-22 18:52:24 +03:00
parent 168d82a966
commit 2adc5af8d7
39 changed files with 8441 additions and 2915 deletions

8
dashboard/.babelrc Normal file
View file

@ -0,0 +1,8 @@
{
"plugins": [
["transform-runtime", {
"regenerator": true
}],
"transform-object-rest-spread"
]
}

3
dashboard/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/.cache
/dist
/node_modules

7668
dashboard/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
dashboard/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "zeppelin-dashboard",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"build": "rimraf dist && parcel build src/index.html --out-dir dist",
"watch": "parcel src/index.html"
},
"devDependencies": {
"@vue/component-compiler-utils": "^3.0.0",
"babel-core": "^6.26.3",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"parcel-bundler": "^1.12.3",
"vue-template-compiler": "^2.6.10"
},
"dependencies": {
"js-cookie": "^2.2.0",
"vue": "^2.6.10",
"vue-hot-reload-api": "^2.3.3",
"vue-router": "^3.0.6"
},
"browserslist": [
"last 2 Chrome versions"
]
}

53
dashboard/src/api.ts Normal file
View file

@ -0,0 +1,53 @@
const apiUrl = process.env.API_URL;
type QueryParamObject = { [key: string]: string | null };
function buildQueryString(params: QueryParamObject) {
if (Object.keys(params).length === 0) return "";
return (
"?" +
Array.from(Object.entries(params))
.map(pair => `${encodeURIComponent(pair[0])}=${encodeURIComponent(pair[1] || "")}`)
.join("&")
);
}
let apiKey = null;
export function setApiKey(newKey) {
apiKey = newKey;
}
export function hasApiKey() {
return apiKey != null;
}
export function resetApiKey() {
apiKey = null;
}
export function request(resource, fetchOpts: RequestInit = {}) {
return fetch(`${apiUrl}/${resource}`, fetchOpts)
.then(res => res.json())
.catch(err => {
console.log(err);
});
}
export function get(resource: string, params: QueryParamObject = {}) {
const headers: Record<string, string> = apiKey ? { "X-Api-Key": (apiKey as unknown) as string } : {};
return request(resource + buildQueryString(params), {
method: "GET",
headers,
});
}
export function post(resource: string, params: QueryParamObject = {}) {
const headers: Record<string, string> = apiKey ? { "X-Api-Key": (apiKey as unknown) as string } : {};
return request(resource + buildQueryString(params), {
method: "POST",
body: JSON.stringify(params),
headers: {
...headers,
"Content-Type": "application/json",
},
});
}

15
dashboard/src/auth.ts Normal file
View file

@ -0,0 +1,15 @@
import { NavigationGuard } from "vue-router";
import { RootStore } from "./store";
export const authGuard: NavigationGuard = async (to, from, next) => {
if (RootStore.state.auth.apiKey) return next(); // We have an API key -> authenticated
if (RootStore.state.auth.loadedInitialAuth) return next("/login"); // No API key and initial auth data was already loaded -> not authenticated
await RootStore.dispatch("auth/loadInitialAuth"); // Initial auth data wasn't loaded yet (per above check) -> load it now
if (RootStore.state.auth.apiKey) return next();
next("/login"); // Still no API key -> not authenticated
};
export const loginCallbackGuard: NavigationGuard = async (to, from, next) => {
await RootStore.dispatch("auth/setApiKey", to.query.apiKey);
next("/dashboard");
};

View file

@ -0,0 +1,3 @@
<template>
<router-view></router-view>
</template>

View file

@ -0,0 +1,36 @@
<template>
<div v-if="loading">
Loading...
</div>
<div v-else>
<h1>Guilds</h1>
<table v-for="guild in guilds">
<tr>
<td>{{ guild.id }}</td>
<td>{{ guild.name }}</td>
<td>
<a v-bind:href="'/dashboard/guilds/' + guild.id + '/config'">Config</a>
</td>
</tr>
</table>
</div>
</template>
<script>
import {mapGetters, mapState} from "vuex";
import {LoadStatus} from "../store/types";
export default {
mounted() {
this.$store.dispatch("guilds/loadAvailableGuilds");
},
computed: {
loading() {
return this.$state.guilds.availableGuildsLoadStatus !== LoadStatus.Done;
},
...mapState({
guilds: 'guilds/available',
}),
},
};
</script>

View file

@ -0,0 +1,11 @@
<template>
<p>Redirecting...</p>
</template>
<script>
export default {
created() {
this.$router.push('/login');
}
};
</script>

View file

@ -0,0 +1,14 @@
<template>
<a v-bind:href="env.API_URL + '/auth/login'">Login</a>
</template>
<script>
export default {
computed: {
blah() {
return this.$state.apiKey;
}
}
};
</script>

15
dashboard/src/index.html Normal file
View file

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Zeppelin Dashboard</title>
</head>
<body>
<div id="app"></div>
<script src="./main.ts"></script>
</body>
</html>

31
dashboard/src/main.ts Normal file
View file

@ -0,0 +1,31 @@
import Vue from "vue";
import { RootStore } from "./store";
import { router } from "./routes";
get("/foo", { bar: "baz" });
// Set up a read-only global variable to access specific env vars
Vue.mixin({
data() {
return {
get env() {
return Object.freeze({
API_URL: process.env.API_URL,
});
},
};
},
});
import App from "./components/App.vue";
import Login from "./components/Login.vue";
import { get } from "./api";
const app = new Vue({
router,
store: RootStore,
el: "#app",
render(h) {
return h(App);
},
});

27
dashboard/src/routes.ts Normal file
View file

@ -0,0 +1,27 @@
import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";
import Index from "./components/Index.vue";
import Login from "./components/Login.vue";
import LoginCallback from "./components/LoginCallback.vue";
import Dashboard from "./components/Dashboard.vue";
import store from "./store";
import { authGuard, loginCallbackGuard } from "./auth";
Vue.use(VueRouter);
const publicRoutes: RouteConfig[] = [
{ path: "/", component: Index },
{ path: "/login", component: Login },
{ path: "/login-callback", beforeEnter: loginCallbackGuard },
];
const authenticatedRoutes: RouteConfig[] = [{ path: "/dashboard", component: Dashboard }];
authenticatedRoutes.forEach(route => {
route.beforeEnter = authGuard;
});
export const router = new VueRouter({
mode: "history",
routes: [...publicRoutes, ...authenticatedRoutes],
});

View file

@ -0,0 +1,46 @@
import { get, hasApiKey, post, setApiKey } from "../api";
import { ActionTree, Module } from "vuex";
import { AuthState, RootState } from "./types";
export const AuthStore: Module<AuthState, RootState> = {
namespaced: true,
state: {
apiKey: null,
loadedInitialAuth: false,
},
actions: {
async loadInitialAuth({ dispatch, commit, state }) {
if (state.loadedInitialAuth) return;
const storedKey = localStorage.getItem("apiKey");
if (storedKey) {
console.log("key?", storedKey);
const result = await post("auth/validate-key", { key: storedKey });
if (result.isValid) {
await dispatch("setApiKey", storedKey);
} else {
localStorage.removeItem("apiKey");
}
}
commit("markInitialAuthLoaded");
},
setApiKey({ commit, state }, newKey: string) {
localStorage.setItem("apiKey", newKey);
commit("setApiKey", newKey);
},
},
mutations: {
setApiKey(state: AuthState, key) {
state.apiKey = key;
},
markInitialAuthLoaded(state: AuthState) {
state.loadedInitialAuth = true;
},
},
};

View file

@ -0,0 +1,34 @@
import { get } from "../api";
import { Module } from "vuex";
import { GuildState, LoadStatus, RootState } from "./types";
export const GuildStore: Module<GuildState, RootState> = {
namespaced: true,
state: {
availableGuildsLoadStatus: LoadStatus.None,
available: [],
configs: {},
},
actions: {
async loadAvailableGuilds({ dispatch, commit, state }) {
if (state.availableGuildsLoadStatus !== LoadStatus.None) return;
commit("setAvailableGuildsLoadStatus", LoadStatus.Loading);
const availableGuilds = await get("guilds/available");
commit("setAvailableGuilds", availableGuilds);
},
},
mutations: {
setAvailableGuildsLoadStatus(state: GuildState, status: LoadStatus) {
state.availableGuildsLoadStatus = status;
},
setAvailableGuilds(state: GuildState, guilds) {
state.available = guilds;
state.availableGuildsLoadStatus = LoadStatus.Done;
},
},
};

View file

@ -0,0 +1,28 @@
import Vue from "vue";
import Vuex, { Store } from "vuex";
Vue.use(Vuex);
import { RootState } from "./types";
import { AuthStore } from "./auth";
import { GuildStore } from "./guilds";
export const RootStore = new Vuex.Store<RootState>({
modules: {
auth: AuthStore,
guilds: GuildStore,
},
});
// Set up typings so Vue/our components know about the state's types
declare module "vue/types/options" {
interface ComponentOptions<V extends Vue> {
store?: Store<RootState>;
}
}
declare module "vue/types/vue" {
interface Vue {
$store: Store<RootState>;
}
}

View file

@ -0,0 +1,27 @@
export enum LoadStatus {
None = 1,
Loading,
Done,
}
export interface AuthState {
apiKey: string | null;
loadedInitialAuth: boolean;
}
export interface GuildState {
availableGuildsLoadStatus: LoadStatus;
available: Array<{
guild_id: string;
name: string;
icon: string | null;
}>;
configs: {
[key: string]: string;
};
}
export type RootState = {
auth: AuthState;
guilds: GuildState;
};

4
dashboard/ts-vue-shim.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}

28
dashboard/tsconfig.json Normal file
View file

@ -0,0 +1,28 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "es2015",
"noImplicitAny": false,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "esnext",
"strict": true,
"lib": [
"es2017",
"esnext",
"dom"
],
"baseUrl": "./",
"resolveJsonModule": true,
"esModuleInterop": true,
"outDir": "./dist"
},
"include": [
"src/**/*.ts",
"src/**/*.vue"
],
"files": [
"ts-vue-shim.d.ts"
]
}