isundil 2 months ago
parent
commit
a1c39ce1bb

+ 15 - 1
front/DAL/systemInfo.ts

@@ -1,6 +1,7 @@
 
 import $ from "jquery"
-import { SystemInfoDescription } from "@models/systemInfo";
+import { SystemInfoDescription, SystemInfo as ModelSystemInfo } from "../../src/models/systemInfo";
+import { HostnameServiceDescription } from "../../src/models/service";
 
 export namespace DAL {
     export class SystemInfo {
@@ -21,10 +22,23 @@ export namespace DAL {
             return this.getData();
         }
 
+        public static async getInfo(hostname: string): Promise<ModelSystemInfo|undefined> {
+            await this.getDataWithCache();
+            return this.lastCachedData?.systemInfo[hostname];
+        }
+
         public static async listHosts(): Promise<Array<string>> {
             await this.getDataWithCache();
             return Object.keys(this.lastCachedData?.systemInfo ?? {});
         }
+
+        public static async getServices(): Promise<HostnameServiceDescription> {
+            return new Promise(ok => {
+                $.get("/api/services/list", response => {
+                    ok(response.services);
+                });
+            });
+        }
     }
 }
 

+ 5 - 1
front/index.ts

@@ -3,11 +3,15 @@ import $ from "jquery";
 import "bootstrap";
 import "bootstrap-select";
 
+import DataTable from 'datatables.net-react';
+import DT from 'datatables.net-bs4';
+import 'datatables.net-columncontrol-bs4';
+DataTable.use(DT);
+
 import templateManager from "./templates/templateManager";
 
 templateManager.setLoading(true);
 $(async () => {
     await templateManager.showDefaultPage();
-    templateManager.setLoading(false);
 });
 

+ 0 - 2
front/templates/login.ts

@@ -15,8 +15,6 @@ class LoginPage extends Page {
         menu.hide();
         return this.load();
     }
-
-    public onFilterUpdated(): void {}
 }
 
 export default new LoginPage();

+ 7 - 2
front/templates/menu.ts

@@ -1,12 +1,12 @@
 
 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[] =[];
+    private filterUpdatedHandlers: (()=>void)[] = [];
 
     public init(): Promise<void> {
         if (this.initialized)
@@ -17,9 +17,14 @@ class Menu {
         return this.reload();
     }
 
+    public addFilterEventListener(handler: ()=>void) {
+        this.filterUpdatedHandlers.push(handler);
+    }
+
     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();
+        for (var i of this.filterUpdatedHandlers)
+            i();
     }
 
     private async reload(): Promise<void> {

+ 4 - 2
front/templates/page.ts

@@ -7,8 +7,6 @@ export abstract class Page {
 
     protected abstract load(): Promise<void>;
 
-    public abstract onFilterUpdated(): void;
-
     protected constructor(pageName: string) {
         let rootSection = document.getElementById("page-"+pageName);
         if (!rootSection)
@@ -30,6 +28,10 @@ export abstract class Page {
         this.rootSection.classList.remove("hidden");
     }
 
+    public getPageName(): string {
+        return this.pageName;
+    }
+
     public async show(): Promise<void> {
         if (templateManager.currentSection === this)
             return Promise.resolve();

+ 22 - 0
front/templates/services.ts

@@ -0,0 +1,22 @@
+import {HostnameServiceDescription} from "../../src/models/service";
+import {Page} from "./page";
+import {DAL} from "../DAL/systemInfo";
+import {SystemServiceComponent} from "./servicesComponent";
+
+class SystemServicePage extends Page {
+    private data: HostnameServiceDescription|null = null;
+
+    public constructor() {
+        super("services");
+    }
+
+    protected async load(): Promise<void> {
+        if (this.data)
+            return;
+        this.data = await DAL.SystemInfo.getServices();
+        SystemServiceComponent.renderMultiple(document.getElementById("page-services")!, this.data);
+    }
+}
+
+export default new SystemServicePage();
+

+ 79 - 0
front/templates/servicesComponent.tsx

@@ -0,0 +1,79 @@
+import React, { Component } from "react";
+import * as ReactDom from "react-dom/client";
+import DataTable from 'datatables.net-react';
+import {HostnameServiceDescription, Service} from "../../src/models/service";
+import menu from "./menu";
+
+interface SingleServiceProp {
+    hostname: string;
+    serviceName: string;
+    status: string;
+}
+
+interface SystemServiceState {
+    loading: boolean;
+}
+
+interface SystemServiceProps {
+    data: SingleServiceProp[];
+}
+
+export class SystemServiceComponent extends Component<SystemServiceProps, SystemServiceState> {
+    public constructor(props: SystemServiceProps) {
+        super(props);
+
+        this.state = {
+            loading: false,
+        };
+        menu.addFilterEventListener(() => {
+            this.refreshFilter();
+        });
+    }
+
+    private refreshFilter() {
+        // FIXME
+        /*
+        const hidden = this.isFiltered();
+        if (hidden === this.state.hidden)
+            return;
+        this.setState({...this.state, hidden: hidden});
+        */
+    }
+
+    public render(): React.JSX.Element {
+        if (this.state.loading)
+            return (<>Loading..</>);
+        else if (this.props.data === null)
+            return (<>Error</>);
+        return (<>
+            <DataTable options={{
+                    layout: { topEnd: null },
+                    columnDefs: [
+                        { target: 0, columnControl: { target: 0, content: [ "order", [ "searchList" ] ] } },
+                        { target: 1, columnControl: { target: 0, content: [ "order", [ "search" ] ] } },
+                        { target: 2, columnControl: { target: 0, content: [ "order", [ "searchList" ] ] } }
+                    ],
+                    pageLength: 20,
+                    lengthMenu: [10, 20, 50, 100, -1],
+                    ordering: {
+                        indicators: false,
+                        handler: false
+                    }
+                }} data={this.props.data.map(x => [ x.hostname, x.serviceName, x.status ])}>
+                <thead><tr>
+                    <th>Host</th>
+                    <th>Service Name</th>
+                    <th>Status</th>
+                </tr></thead>
+            </DataTable>
+            </>);
+    }
+
+    public static renderMultiple(container: HTMLElement, data: HostnameServiceDescription): void {
+        const dataArray: SingleServiceProp[] = Object.keys(data).reduce((acc: SingleServiceProp[], hostname: string): SingleServiceProp[] => {
+            return acc.concat(data[hostname]!.map((x: Service) : SingleServiceProp => { return { hostname: hostname, serviceName: x.name, status: x.statusText }}));
+        }, []);
+        ReactDom.createRoot(container).render(<SystemServiceComponent data={dataArray}/>);
+    }
+}
+

+ 2 - 18
front/templates/systemInfo.ts

@@ -1,35 +1,19 @@
-import { DAL } from "../DAL/systemInfo";
 import {Page} from "./page";
+import { SystemInfoComponent } from "./systemInfoComponent";
 
 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))
-        }
+        await SystemInfoComponent.renderMultiple(document.getElementById("page-systemInfo")!);
         this.loaded = true;
     }
-
-    public onFilterUpdated(): void {
-        // FIXME
-    }
 }
 
 export default new SystemInfoPage();

+ 192 - 0
front/templates/systemInfoComponent.tsx

@@ -0,0 +1,192 @@
+import React, { Component } from "react";
+import * as ReactDom from "react-dom/client";
+import { DAL } from "../DAL/systemInfo";
+import {SystemInfo, CpuInfo, NetworkInfo, DriveInfo} from "../../src/models/systemInfo";
+import menu from "./menu";
+
+interface SystemInfoComponentState {
+    loading: boolean;
+    hidden: boolean;
+}
+
+interface SystemInfoProps {
+    hostname: string;
+
+    setHidden: any;//((val:boolean)=>void|null);
+}
+
+export class SystemInfoComponent extends Component<SystemInfoProps, SystemInfoComponentState> {
+    private data: SystemInfo|undefined = undefined;
+
+    public constructor(props: SystemInfoProps) {
+        super(props);
+
+        this.state = {
+            loading: true,
+            hidden: this.isFiltered()
+        };
+        DAL.SystemInfo.getInfo(this.props.hostname).then(data => {
+            this.data = data; // FIXME error handling
+            this.setState({...this.state, loading: false});
+        });
+        menu.addFilterEventListener(() => {
+            this.refreshFilter();
+        });
+    }
+
+    private isFiltered(): boolean {
+        const filterShown = menu.getFilteredHostnames();
+        return (filterShown.indexOf(this.props.hostname) < 0);
+    }
+
+    private refreshFilter() {
+        const hidden = this.isFiltered();
+        if (hidden === this.state.hidden)
+            return;
+        this.setState({...this.state, hidden: hidden});
+    }
+
+    private layout(content: React.JSX.Element) {
+        return <section key={this.props.hostname} className={(this.state.hidden ? "hidden" : "")}><div><h1>{this.props.hostname}</h1>{content}</div></section>;
+    }
+    private statItem(key: string, value: string|React.JSX.Element, listKey?: number): React.JSX.Element {
+        if (!value)
+            return <></>;
+        key = key.charAt(0).toUpperCase()+String(key).slice(1);
+        if (listKey === undefined)
+            return <li>{key}: {value}</li>;
+        return <li key={listKey}>{key}: {value}</li>;
+    }
+    private statItemNumber(key: string, value: number): React.JSX.Element {
+        if (!value || isNaN(value))
+            return <></>;
+        return this.statItem(key, `${value}`);
+    }
+    private statItemMhz(key: string, value: number): React.JSX.Element {
+        if (!value || isNaN(value))
+            return <></>;
+        return this.statItem(key, `${value}`);
+    }
+    private stringifyByte(value: number): string {
+        if (!value || isNaN(value))
+            return "";
+        let units = ["Byte", "kB", "MB", "GB", "TB", "PB"];
+        let unitIndex = 0;
+        while (value > 1024 && unitIndex < units.length) {
+            value /= 1024;
+            unitIndex++;
+        }
+        value = Math.round(value*100)/100;
+        return `${value}${units[unitIndex]}`;
+    }
+    private statItemBytes(key: string, value: number): React.JSX.Element {
+        if (!value || isNaN(value))
+            return <></>;
+        return this.statItem(key, this.stringifyByte(value));
+    }
+    private statItemReadOnly(key: string, value: boolean|undefined): React.JSX.Element {
+        if (value === undefined)
+            return <></>;
+        return this.statItem(key, value ? "Read-Write" : "Read Only");
+    }
+    private statItemBoolean(key: string, value: boolean|undefined): React.JSX.Element {
+        if (value === undefined)
+            return <></>;
+        return this.statItem(key, value ? "true" : "false");
+    }
+    private statItemArray(key: string, values: string[]): React.JSX.Element {
+        return this.statItem(key, <ul>{values.map((x, idx) => <li key={idx}>{x}</li>)}</ul>);
+    }
+    private statItemFragArray(key: string, values: React.JSX.Element[]): React.JSX.Element {
+        return this.statItem(key, <ul>{values}</ul>);
+    }
+    private uptime(data: SystemInfo): string {
+        let totalTime = data.uptime;
+        let timeInSec = Math.floor(totalTime % 60);
+        totalTime /= 60;
+        let timeInMin = Math.floor(totalTime % 60);
+        totalTime /= 60;
+        let timeInHours = Math.floor(totalTime % 24);
+        totalTime /= 24;
+        let timeInDays = Math.floor(totalTime);
+        let result = "";
+        if (timeInDays)
+            result += `${timeInDays}d, `;
+        if (result.length || timeInHours)
+            result += (`${timeInHours}:`).padStart(3, "0");
+        result += (`${timeInMin}`).padStart(2, "0")+':'+(`${timeInSec}`).padStart(2, "0");
+        return result;
+    }
+    private cpuInfos(cpu: CpuInfo, cpuIndex: number): React.JSX.Element {
+        return this.statItem("model", cpu.model, cpuIndex);
+    }
+    private networkInfo(network: NetworkInfo, netIndex: number): React.JSX.Element {
+        return <li key={netIndex}><ul>
+            {this.statItem("Interface", network.iface)}
+            {this.statItem("Address", network.address)}
+            {this.statItem("Netmask", network.netmask)}
+            {this.statItem("Family", network.family)}
+            {this.statItem("Mac Address", network.mac)}
+            </ul></li>;
+    }
+    private driveInfo(drive: DriveInfo, driveIdx: number): React.JSX.Element {
+        return <li key={driveIdx}><ul>
+            {this.statItem("name", drive.name)}
+            {this.statItem("type", drive.type)}
+            {this.statItem("Mount point", drive.mount)}
+            {this.statItem("label", drive.label)}
+            {this.statItemBytes("Size", drive.size)}
+            {this.statItemBytes("Used Size", drive.usedSize)}
+            {this.statItemBytes("Remaining Size", drive.size - drive.usedSize)}
+            {this.statItemNumber("Used (%)", Math.round(10000 * drive.usedSize / drive.size) / 100)}
+            {this.statItem("Physical", drive.physical)}
+            {this.statItem("UUID", drive.uuid)}
+            {this.statItem("model", drive.model)}
+            {this.statItem("serial", drive.serial)}
+            {this.statItemBoolean("removable", drive.removable)}
+            {this.statItem("protocol", drive.protocol)}
+            {this.statItem("device", drive.device)}
+            {this.statItemReadOnly("Access", drive.rw)}
+            </ul></li>;
+    }
+    public render(): React.JSX.Element {
+        if (this.state.loading)
+            return this.layout(<>Loading..</>);
+        else if (this.data === undefined)
+            return this.layout(<>Error</>);
+        const data = this.data!;
+        return this.layout(<>
+            <ul>
+            {this.statItem("Platform", data.platform)}
+            {this.statItem("Distribution", data.distribution)}
+            {this.statItem("Architecture", data.arch)}
+            {this.statItem("OS Version", data.osVersion)}
+            {this.statItem("uptime", this.uptime(data))}
+            {this.statItem("Architecture", data.arch)}
+            {this.statItem("Node Version", data.nodeVersion)}
+            </ul>
+            <ul>
+            {this.statItem("Manufacturer", data.manufacturer)}
+            {this.statItem("Model", data.model)}
+            {this.statItemMhz("CPU Max Speed", data.cpuMaxSpeed)}
+            {this.statItemFragArray("Cpus", data.cpus.map((x, idx) => this.cpuInfos(x, idx)))}
+            {this.statItemBytes("Memory", data.memory)}
+            {this.statItem("Memory Layout", <ul>{data.memoryLayout.map((x, idx) => <li key={idx}>{this.stringifyByte(x)}</li>)}</ul>)}
+            </ul>
+            <ul>
+            {this.statItemArray("DNS Servers", data.dnsServers)}
+            {this.statItemFragArray("Network Interfaces", data.network.map((x, idx) => this.networkInfo(x, idx)))}
+            </ul>
+            <ul>
+            {this.statItemFragArray("Drives", data.drives.map((x, idx) => this.driveInfo(x, idx)))}
+            </ul>
+            </>);
+    }
+
+    public static async renderMultiple(container: HTMLElement): Promise<React.JSX.Element[]> {
+        let domNodes = (await DAL.SystemInfo.listHosts()).map(hostname => <SystemInfoComponent key={hostname} setHidden={null} hostname={hostname}/>);
+        ReactDom.createRoot(container).render(<>{domNodes}</>);
+        return domNodes;
+    }
+}
+

+ 18 - 0
front/templates/systemMonitor.ts

@@ -0,0 +1,18 @@
+import {Page} from "./page";
+
+class SystemMonitorPage extends Page {
+    private loaded: boolean = false;
+
+    public constructor() {
+        super("systemMonitor");
+    }
+
+    protected async load(): Promise<void> {
+        if (this.loaded)
+            return;
+        this.loaded = true;
+    }
+}
+
+export default new SystemMonitorPage();
+

+ 15 - 4
front/templates/templateManager.ts

@@ -2,6 +2,8 @@ import {DAL} from "../DAL/login";
 import login from "./login";
 import {Page} from "./page";
 import systemInfo from "./systemInfo";
+import systemMonitor from "./systemMonitor";
+import systemServices from './services';
 
 class TemplateManager {
     public currentSection: Page|null = null;
@@ -11,13 +13,22 @@ class TemplateManager {
     public constructor() {
         this.pages.push(login);
         this.pages.push(systemInfo);
+        this.pages.push(systemMonitor);
+        this.pages.push(systemServices);
+
+        addEventListener("hashchange", () => this.showDefaultPage());
     }
 
     public async showDefaultPage(): Promise<void> {
-        if (await DAL.isLoggedUser()) {
-            return systemInfo.show();
-        }
-        return login.show();
+        this.setLoading(true);
+        if (!await DAL.isLoggedUser())
+            return login.show();
+        const hash = String(window.location.hash).slice(1);
+        let page = this.pages.find(x => x.getPageName() === hash);
+        if (!page)
+            page = systemInfo;
+        await page.show();
+        this.setLoading(false);
     }
 
     public setLoading(loading: boolean) {

+ 34 - 37
front/tsconfig.json

@@ -1,40 +1,37 @@
 {
-  "compilerOptions": {
-    "module": "es2020",
-    "target": "esnext",
-    "types": [
-        "bootstrap-select",
-        "jquery"
-    ],
-    "paths": {
-        "@models/*": ["../src/models/*"]
-    },
-    "lib": ["es5", "es6", "dom"],
-    "sourceMap": true,
-    "declaration": true,
-    "declarationMap": true,
-    "noUncheckedIndexedAccess": true,
-    "strict": true,
-    "jsx": "react-jsx",
-    "noUncheckedSideEffectImports": true,
-    "moduleDetection": "force",
-    "skipLibCheck": true,
+    "compilerOptions": {
+        "paths": {
+            "@models/*": ["../src/models/*"]
+        },
+        "lib": ["es5", "es6", "es7", "dom"],
+        "target": "es2017",
+        "module": "commonjs",
+        "moduleResolution": "node",
+        "rootDir": "../",
+        "outDir": "../build/front",
+        "emitDecoratorMetadata": true,
+        "experimentalDecorators": true,
+        "esModuleInterop": true,
+        "noImplicitAny": true,
+        "noImplicitReturns": true,
+        "noImplicitThis": true,
+        "noUncheckedIndexedAccess": true,
+        "removeComments": true,
+        "strictNullChecks": true,
+        "strictFunctionTypes": true,
+        "strictBindCallApply": true,
+        "strictPropertyInitialization": true,
+        "strict": true,
+        "alwaysStrict": true,
+        "jsx": "react",
+        "resolveJsonModule": true,
+        "allowJs": true,
+        "sourceMap": true,
 
-    "outDir": "../build/front",
-
-    "experimentalDecorators": true,
-    "noImplicitAny": true,
-    "strictNullChecks": true,
-    "strictFunctionTypes": true,
-    "strictBindCallApply": true,
-    "strictPropertyInitialization": true,
-    "noImplicitThis": true,
-    "alwaysStrict": true,
-    "esModuleInterop": true,
-    "removeComments": true,
-    "noUnusedLocals": true,
-    "noUnusedParameters": true,
-    "noImplicitReturns": true,
-    "forceConsistentCasingInFileNames": true
-  }
+        "baseUrl": ".",
+        "paths": {
+            "@modules/*": ["src/modules/*"],
+            "*": ["node_modules/*"]
+        }
+    }
 }

+ 15 - 7
package.json

@@ -8,8 +8,8 @@
     "build": "npm run back && npm run front",
     "back": "tsoa spec-and-routes",
     "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",
+    "webpack": "npx webpack -o ./build/ --mode=production -c ./webpack.config.js --env=output=front.min.js",
+    "webpack-devel": "npx webpack -o ./build/ --mode=development -c ./webpack.config.js --env=output=front.js",
     "run": "tsoa spec-and-routes && npx ts-node src/index.ts",
     "run-dev": "nodemon src/index.ts",
     "generate-key": "npx ts-node src/index.ts --generate-key"
@@ -21,32 +21,40 @@
   "author": "isundil",
   "license": "GPG",
   "dependencies": {
+    "@open-wc/webpack-import-meta-loader": "^0.4.7",
     "@types/bootstrap": "^4.1.0",
     "@types/bootstrap-select": "^1.13.7",
+    "@types/datatables.net": "^1.10.28",
     "@types/express": "^5.0.3",
     "@types/express-handlebars": "^5.3.1",
-    "@types/jquery": "^3.5.33",
+    "@types/node": "^24.5.2",
+    "@types/react": "^19.1.1",
+    "@types/react-dom": "^19.1.9",
     "@types/swagger-ui-express": "^4.1.8",
+    "@types/jquery": "^3.5.33",
     "bootstrap": "^4.1.0",
     "bootstrap-select": "^1.13.18",
+    "datatables.net": "^2.3.4",
+    "datatables.net-bs4": "^2.3.4",
+    "datatables.net-columncontrol-bs4": "^1.1.0",
+    "datatables.net-react": "^1.0.1",
     "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",
+    "module-alias": "^2.2.3",
+    "react": "^19.1.1",
+    "react-dom": "^19.1.1",
     "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",
     "webpack": "^5.101.3",
     "webpack-cli": "^6.0.1"
   },
   "devDependencies": {
-    "imports-loader": "^5.0.0",
     "nodemon": "^3.1.10"
   }
 }

+ 2 - 0
src/index.ts

@@ -64,6 +64,8 @@ import {UnauthorizedMasterApiKey} from "./models/unauthorizedApi";
         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("/css/bootstrap.min.css", express.static(path.join(__dirname, "../node_modules/bootstrap/dist/css"), {index: 'bootstrap.min.css'}));
+        app.use("/css/datatables.net-bs.min.css", express.static(path.join(__dirname, "../node_modules/datatables.net-bs/css"), {index: 'dataTables.bootstrap.min.css'}));
+        app.use("/css/columnControl.datatables.net-bs.min.css", express.static(path.join(__dirname, "../node_modules/datatables.net-columncontrol-bs4/css"), {index: 'columnControl.bootstrap4.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')));
     }

+ 1 - 0
src/models/systemInfo.ts

@@ -48,6 +48,7 @@ export interface SystemInfo {
     cpus: Array<CpuInfo>;
     memory: number;
     memoryLayout: Array<number>;
+    // FIXME swap
 
     // Network
     network: Array<NetworkInfo>;

+ 2 - 0
templates/index.handlebars

@@ -1,2 +1,4 @@
 {{> login}}
 {{> systemInfo}}
+{{> systemMonitor}}
+{{> systemServices}}

+ 8 - 6
templates/layouts/main.handlebars

@@ -3,9 +3,11 @@
 <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">
+    <link rel="stylesheet" href="/css/bootstrap.min.css"/>
+    <link rel="stylesheet" href="/css/bootstrap-select.min.css"/>
+    <link rel="stylesheet" href="/css/datatables.net-bs.min.css"/>
+    <link rel="stylesheet" href="/css/columnControl.datatables.net-bs.min.css"/>
+    <link rel="stylesheet" href="/css/style.css"/>
     <title>{{title}}</title>
 </head>
 <body>
@@ -15,9 +17,9 @@
                 <select id="navbar-hostfilter" class="selectpicker" multiple data-live-search="true">
                 </select>
             </li>
-            <li>System Info</li>
-            <li>System Monitoring</li>
-            <li>Services</li>
+            <li><a href="#systemInfo">System Info</a></li>
+            <li><a href="#systemMonitor">System Monitoring</a></li>
+            <li><a href="#services">Services</a></li>
         </ul>
     </nav>
     {{{body}}}

+ 2 - 0
templates/partials/systemMonitor.handlebars

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

+ 2 - 0
templates/partials/systemServices.handlebars

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

+ 5 - 3
front/webpack.config.js → webpack.config.js

@@ -3,13 +3,15 @@ export default env => {
     return {
         module: {
             rules: [
-                { test: /\.ts$/, use: 'ts-loader' }
+                { test: /\.tsx?$/, use: 'ts-loader' },
+                { parser: { amd: false } }
             ]
         },
         output: {
             filename: env.output
         },
         resolve: {
+            //modules: [ '...', '../node_modules', "./node_modules" ],
             extensions: [".ts", ".tsx", ".js"]
         },
         cache: true,
@@ -19,9 +21,9 @@ export default env => {
         entry: [
             "./front/index.ts"
         ],
-
         plugins: [
-        ],
+        ]//,
+        //stats: "verbose"
     };
 };