Browse Source

Front-end stuff

isundil 2 months ago
parent
commit
6780ee6356

+ 7 - 0
front/DAL/login.ts

@@ -0,0 +1,7 @@
+
+export namespace DAL {
+    export function isLoggedUser(): Promise<boolean> {
+        return Promise.resolve(true);
+    }
+}
+

+ 30 - 0
front/DAL/systemInfo.ts

@@ -0,0 +1,30 @@
+
+import $ from "jquery"
+import { SystemInfoDescription } from "@models/systemInfo";
+
+export namespace DAL {
+    export class SystemInfo {
+        private static lastCachedData: SystemInfoDescription|null = null;
+
+        private static getData(): Promise<void> {
+            return new Promise(ok => {
+                $.get("/api/sysinfo", response => {
+                    this.lastCachedData = response;
+                    ok();
+                });
+            });
+        }
+
+        private static getDataWithCache(): Promise<void> {
+            if (this.lastCachedData)
+                return Promise.resolve();
+            return this.getData();
+        }
+
+        public static async listHosts(): Promise<Array<string>> {
+            await this.getDataWithCache();
+            return Object.keys(this.lastCachedData?.systemInfo ?? {});
+        }
+    }
+}
+

+ 11 - 0
front/index.ts

@@ -1,2 +1,13 @@
 
+import $ from "jquery";
+import "bootstrap";
+import "bootstrap-select";
+
+import templateManager from "./templates/templateManager";
+
+templateManager.setLoading(true);
+$(async () => {
+    await templateManager.showDefaultPage();
+    templateManager.setLoading(false);
+});
 

+ 23 - 0
front/templates/login.ts

@@ -0,0 +1,23 @@
+import menu from "./menu";
+import {Page} from "./page";
+
+class LoginPage extends Page {
+    public constructor() {
+        super("login");
+    }
+
+    protected load(): Promise<void> {
+        return Promise.resolve();
+    }
+
+    public override show(): Promise<void> {
+        super.showWithoutHistory();
+        menu.hide();
+        return this.load();
+    }
+
+    public onFilterUpdated(): void {}
+}
+
+export default new LoginPage();
+

+ 53 - 0
front/templates/menu.ts

@@ -0,0 +1,53 @@
+
+import $ from "jquery";
+import { DAL } from "../DAL/systemInfo";
+import templateManager from "./templateManager";
+
+class Menu {
+    private navBar: HTMLElement = document.getElementById("navbar")!;
+    private initialized: boolean = false;
+    private filteredHostname: string[] =[];
+
+    public init(): Promise<void> {
+        if (this.initialized)
+            return Promise.resolve();
+        (document.getElementById("navbar-hostfilter")!).addEventListener("change", evt => {
+            this.checkFilters(evt.target as HTMLSelectElement);
+        });
+        return this.reload();
+    }
+
+    private checkFilters(selectElement: HTMLSelectElement) {
+        this.filteredHostname = Array.from(selectElement.children).filter(x => (x as HTMLOptionElement).selected).map(x => (x as HTMLOptionElement).value);
+        templateManager.currentSection?.onFilterUpdated();
+    }
+
+    private async reload(): Promise<void> {
+        let hostFilter: HTMLElement = document.getElementById("navbar-hostfilter")!;
+        hostFilter.textContent = "";
+        for (let host of await DAL.SystemInfo.listHosts()) {
+            let htmlChild: HTMLOptionElement = document.createElement("option");
+            htmlChild.value = host;
+            htmlChild.textContent = host;
+            hostFilter.appendChild(htmlChild);
+        }
+        $("#navbar-hostfilter").selectpicker("refresh");
+        $("#navbar-hostfilter").selectpicker("selectAll");
+    }
+
+    public async show(): Promise<void> {
+        this.navBar.classList.remove("hidden");
+        return this.init();
+    }
+
+    public hide() {
+        this.navBar.classList.add("hidden");
+    }
+
+    public getFilteredHostnames(): string[] {
+        return this.filteredHostname;
+    }
+}
+
+export default new Menu();
+

+ 42 - 0
front/templates/page.ts

@@ -0,0 +1,42 @@
+import menu from "./menu";
+import templateManager from "./templateManager";
+
+export abstract class Page {
+    protected rootSection: HTMLElement;
+    protected pageName: string;
+
+    protected abstract load(): Promise<void>;
+
+    public abstract onFilterUpdated(): void;
+
+    protected constructor(pageName: string) {
+        let rootSection = document.getElementById("page-"+pageName);
+        if (!rootSection)
+            throw new Error("Missing section for page " +typeof this);
+        this.rootSection = rootSection;
+        this.pageName = pageName;
+        this.hide();
+    }
+
+    protected hide() {
+        this.rootSection.classList.add("hidden");
+    }
+
+    protected showWithoutHistory() {
+        if (templateManager.currentSection === this)
+            return;
+        templateManager.currentSection?.hide();
+        templateManager.currentSection = this;
+        this.rootSection.classList.remove("hidden");
+    }
+
+    public async show(): Promise<void> {
+        if (templateManager.currentSection === this)
+            return Promise.resolve();
+        this.showWithoutHistory();
+        await menu.show();
+        history.pushState("", "", `#${this.pageName}`)
+        return this.load();
+    }
+}
+

+ 36 - 0
front/templates/systemInfo.ts

@@ -0,0 +1,36 @@
+import { DAL } from "../DAL/systemInfo";
+import {Page} from "./page";
+
+class SystemInfoPage extends Page {
+    private sections: Map<string, HTMLElement> = new Map();
+    private loaded: boolean = false;
+
+    public constructor() {
+        super("systemInfo");
+    }
+
+    private async createChildElement(hostname: string): Promise<HTMLElement> {
+        let result = document.createElement("section");
+        this.sections.set(hostname, result);
+        return result;
+    }
+
+    protected async load(): Promise<void> {
+        if (this.loaded)
+            return;
+        let container = document.getElementById("page-systemInfo")!;
+        this.sections.clear();
+        container.textContent = "";
+        for (let host of await DAL.SystemInfo.listHosts()) {
+            container.appendChild(await this.createChildElement(host))
+        }
+        this.loaded = true;
+    }
+
+    public onFilterUpdated(): void {
+        // FIXME
+    }
+}
+
+export default new SystemInfoPage();
+

+ 32 - 0
front/templates/templateManager.ts

@@ -0,0 +1,32 @@
+import {DAL} from "../DAL/login";
+import login from "./login";
+import {Page} from "./page";
+import systemInfo from "./systemInfo";
+
+class TemplateManager {
+    public currentSection: Page|null = null;
+    protected pages: Array<Page> =[];
+    protected loading: boolean =false;
+
+    public constructor() {
+        this.pages.push(login);
+        this.pages.push(systemInfo);
+    }
+
+    public async showDefaultPage(): Promise<void> {
+        if (await DAL.isLoggedUser()) {
+            return systemInfo.show();
+        }
+        return login.show();
+    }
+
+    public setLoading(loading: boolean) {
+        if (this.loading === loading)
+            return;
+        this.loading = loading;
+        document.body.classList.toggle("loading", loading);
+    }
+}
+
+export default new TemplateManager();
+

+ 5 - 0
front/tsconfig.json

@@ -3,7 +3,12 @@
     "module": "es2020",
     "target": "esnext",
     "types": [
+        "bootstrap-select",
+        "jquery"
     ],
+    "paths": {
+        "@models/*": ["../src/models/*"]
+    },
     "lib": ["es5", "es6", "dom"],
     "sourceMap": true,
     "declaration": true,

+ 16 - 5
front/webpack.config.js

@@ -1,16 +1,27 @@
 
-var webpack = require('webpack');
-
-module.exports = env => {
+export default env => {
     return {
+        module: {
+            rules: [
+                { test: /\.ts$/, use: 'ts-loader' }
+            ]
+        },
         output: {
             filename: env.output
         },
+        resolve: {
+            extensions: [".ts", ".tsx", ".js"]
+        },
         cache: true,
         devtool: "source-map",
+        externals: [
+        ],
         entry: [
-            "./build/front/index.js"
-        ]
+            "./front/index.ts"
+        ],
+
+        plugins: [
+        ],
     };
 };
 

+ 11 - 2
package.json

@@ -7,8 +7,8 @@
     "dev": "npm run build && npm run run-dev",
     "build": "npm run back && npm run front",
     "back": "tsoa spec-and-routes",
-    "front": "cd ./front && tsc && cd ../ && npm run webpack && npm run webpack-devel",
-    "webpack": "npx webpack -o ./build/ --mode=production -c ./front/webpack.config.js --env=output=front.mins.js",
+    "front": "npm run webpack && npm run webpack-devel",
+    "webpack": "npx webpack -o ./build/ --mode=production -c ./front/webpack.config.js --env=output=front.min.js",
     "webpack-devel": "npx webpack -o ./build/ --mode=development -c ./front/webpack.config.js --env=output=front.js",
     "run": "tsoa spec-and-routes && npx ts-node src/index.ts",
     "run-dev": "nodemon src/index.ts",
@@ -21,16 +21,24 @@
   "author": "isundil",
   "license": "GPG",
   "dependencies": {
+    "@types/bootstrap": "^4.1.0",
+    "@types/bootstrap-select": "^1.13.7",
     "@types/express": "^5.0.3",
     "@types/express-handlebars": "^5.3.1",
+    "@types/jquery": "^3.5.33",
     "@types/swagger-ui-express": "^4.1.8",
+    "bootstrap": "^4.1.0",
+    "bootstrap-select": "^1.13.18",
     "express": "^5.1.0",
     "express-handlebars": "^8.0.3",
+    "jquery": "^3.7.1",
     "linux-systemd": "^0.0.1",
     "mariadb": "^3.4.5",
+    "script-loader": "^0.7.2",
     "swagger-jsdoc": "^6.2.8",
     "swagger-ui-express": "^5.0.1",
     "systeminformation": "^5.27.8",
+    "ts-loader": "^9.5.4",
     "ts-node": "^10.9.2",
     "tsconfig-paths": "^4.2.0",
     "tsoa": "^6.6.0",
@@ -38,6 +46,7 @@
     "webpack-cli": "^6.0.1"
   },
   "devDependencies": {
+    "imports-loader": "^5.0.0",
     "nodemon": "^3.1.10"
   }
 }

+ 6 - 0
public/css/style.css

@@ -1,4 +1,10 @@
+
 html,body {
     margin: 0;
     padding: 0;
 }
+
+.hidden {
+    display: none !important;
+}
+

+ 7 - 4
src/index.ts

@@ -2,7 +2,7 @@ import express, { json, NextFunction, Request, Response, urlencoded } from "expr
 import { RegisterRoutes } from "../build/routes";
 import { HtmlController } from "./routes/htmlControllers";
 import swaggerUi from "swagger-ui-express";
-import { engine } from 'express-handlebars';
+import { create } from 'express-handlebars';
 import path from "path";
 import ConfigurationManager from "./config";
 import slavery from "./slavery";
@@ -17,8 +17,9 @@ import {UnauthorizedMasterApiKey} from "./models/unauthorizedApi";
     const app = express();
 
     if (!ConfigurationManager.slave) {
-        // Whiskers html template
-        app.engine('handlebars', engine());
+        app.engine('handlebars', create({
+            partialsDir: path.join(__dirname, "../", "templates/partials")
+        }).engine);
         app.set('view engine', 'handlebars');
         app.set('views', path.join(__dirname, '../templates'));
     }
@@ -62,7 +63,9 @@ import {UnauthorizedMasterApiKey} from "./models/unauthorizedApi";
         app.use("/js/front.min.js", express.static(path.join(__dirname, "../build/"), {index: 'front.min.js'}));
         app.use("/js/front.js.map", express.static(path.join(__dirname, "../build/"), {index: 'front.js.map'}));
         app.use("/js/front.min.js.map", express.static(path.join(__dirname, "../build/"), {index: 'front.min.js.map'}));
-        app.use(express.static(path.join(__dirname, 'public')));
+        app.use("/css/bootstrap.min.css", express.static(path.join(__dirname, "../node_modules/bootstrap/dist/css"), {index: 'bootstrap.min.css'}));
+        app.use("/css/bootstrap-select.min.css", express.static(path.join(__dirname, "../node_modules/bootstrap-select/dist/css"), {index: 'bootstrap-select.min.css'}));
+        app.use(express.static(path.join(__dirname, '../public')));
     }
 
     app.listen(ConfigurationManager.port,

+ 0 - 1
src/models/systemInfo.ts

@@ -1,4 +1,3 @@
-import {HostnameData} from "./service";
 
 export interface CpuInfo {
     model: string;

+ 5 - 0
src/slavery.ts

@@ -16,6 +16,7 @@ class SlaveManager {
     }
 
     private async getSlaveDataForNode(node: string, jsonResponsePath: string, urlPath: string): Promise<any> {
+        console.debug(`Getting node informations from ${node}${urlPath}`);
         return new Promise((ok, ko) => {
             http.get(`${node}${urlPath}`, {
                 headers: {
@@ -23,6 +24,10 @@ class SlaveManager {
                 }
             }, response => {
                 let responseBody: string[] = [];
+                if (response.statusCode !== 200) {
+                    ko(`Error reaching ${node}${urlPath}: status code=${response.statusCode}: ${response.statusMessage}`);
+                    return;
+                }
                 response.on('error', ko);
                 response.on('data', data => {
                     responseBody.push(data);

+ 2 - 1
templates/index.handlebars

@@ -1 +1,2 @@
-Test
+{{> login}}
+{{> systemInfo}}

+ 13 - 0
templates/layouts/main.handlebars

@@ -3,10 +3,23 @@
 <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link rel="stylesheet" href="/css/bootstrap.min.css">
+    <link rel="stylesheet" href="/css/bootstrap-select.min.css">
     <link rel="stylesheet" href="/css/style.css">
     <title>{{title}}</title>
 </head>
 <body>
+    <nav id="navbar">
+        <ul>
+            <li>
+                <select id="navbar-hostfilter" class="selectpicker" multiple data-live-search="true">
+                </select>
+            </li>
+            <li>System Info</li>
+            <li>System Monitoring</li>
+            <li>Services</li>
+        </ul>
+    </nav>
     {{{body}}}
     {{#if javascript}}<script src="{{javascript}}"></script>{{/if}}
 </body>

+ 7 - 0
templates/partials/login.handlebars

@@ -0,0 +1,7 @@
+<section id="page-login" class="hidden">
+    <form id="login-form">
+        <input type="text" id="login-username" />
+        <input type="password" id="login-password" />
+        <input type="submit" />
+    </form>
+</section>

+ 2 - 0
templates/partials/systemInfo.handlebars

@@ -0,0 +1,2 @@
+<section id="page-systemInfo" class="hidden">
+</section>