feat(ui): redesign UI with Tailwind CSS, CodeMirror 6, and new layout
- Replace all hand-written CSS with Tailwind utility classes - Upgrade CodeMirror 5 to CodeMirror 6 (scoped @codemirror/* packages) - Redesign layout with responsive sidebar navigation using JSX/Mithril - Flatten admin routes (e.g. /admin/presets -> /presets) - Add "remember me" login option with 2-week persistent cookie - Add delete confirmation dialogs across admin pages - Upgrade esbuild from 0.21 to 0.27 and update browser targets - Add Inter and Roboto Mono variable fonts with subset script
This commit is contained in:
1 parent
a7256620e0
commit
4a191afdec
64 files changed
+4147
-2421
No files matched your search
+34
-4
@@ -110,12 +110,28 @@ const inlineDepsPlugin = {
|
|||||||
const deps = [
|
const deps = [
|
||||||
"parsimmon",
|
"parsimmon",
|
||||||
"espresso-iisojs",
|
"espresso-iisojs",
|
||||||
"codemirror",
|
"@codemirror",
|
||||||
"mithril",
|
"mithril",
|
||||||
"yaml",
|
"yaml",
|
||||||
];
|
];
|
||||||
|
const depFiles = new Set();
|
||||||
build.onResolve({ filter: /^[^.]/ }, async (args) => {
|
build.onResolve({ filter: /^[^.]/ }, async (args) => {
|
||||||
if (deps.some((d) => args.path.startsWith(d))) return undefined;
|
if (args.pluginData === "inlineDeps") return undefined;
|
||||||
|
if (
|
||||||
|
depFiles.has(args.importer) ||
|
||||||
|
deps.some((d) => args.path.startsWith(d))
|
||||||
|
) {
|
||||||
|
const res = await build.resolve(args.path, {
|
||||||
|
importer: args.importer,
|
||||||
|
namespace: args.namespace,
|
||||||
|
resolveDir: args.resolveDir,
|
||||||
|
kind: args.kind,
|
||||||
|
with: args.with,
|
||||||
|
pluginData: "inlineDeps",
|
||||||
|
});
|
||||||
|
depFiles.add(res.path);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
return { sideEffects: false, external: true };
|
return { sideEffects: false, external: true };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -226,6 +242,16 @@ async function copyStatic(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function generateCss(): Promise<void> {
|
async function generateCss(): Promise<void> {
|
||||||
|
const tailwindPlugin = {
|
||||||
|
name: "tailwind",
|
||||||
|
setup(build) {
|
||||||
|
build.onLoad({ filter: /\/ui\/css\/app.css$/ }, async (args) => {
|
||||||
|
const res = await execAsync(`npx @tailwindcss/cli -i ${args.path}`);
|
||||||
|
return { loader: "css", contents: res.stdout };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
} as esbuild.Plugin;
|
||||||
|
|
||||||
const res = await esbuild.build({
|
const res = await esbuild.build({
|
||||||
bundle: true,
|
bundle: true,
|
||||||
absWorkingDir: INPUT_DIR,
|
absWorkingDir: INPUT_DIR,
|
||||||
@@ -235,7 +261,11 @@ async function generateCss(): Promise<void> {
|
|||||||
entryPoints: ["ui/css/app.css"],
|
entryPoints: ["ui/css/app.css"],
|
||||||
entryNames: "[dir]/[name]-[hash]",
|
entryNames: "[dir]/[name]-[hash]",
|
||||||
outfile: path.join(OUTPUT_DIR, "public/app.css"),
|
outfile: path.join(OUTPUT_DIR, "public/app.css"),
|
||||||
target: ["chrome109", "safari15.6", "firefox115", "opera102", "edge118"],
|
plugins: [tailwindPlugin],
|
||||||
|
loader: {
|
||||||
|
".woff2": "dataurl",
|
||||||
|
},
|
||||||
|
target: ["chrome111", "safari16.4", "firefox128"],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -293,7 +323,7 @@ async function generateFrontendJs(): Promise<void> {
|
|||||||
sourcesContent: false,
|
sourcesContent: false,
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
format: "esm",
|
format: "esm",
|
||||||
target: ["chrome109", "safari15.6", "firefox115", "opera102", "edge118"],
|
target: ["chrome111", "safari16.4", "firefox128"],
|
||||||
entryPoints: ["ui/app.ts"],
|
entryPoints: ["ui/app.ts"],
|
||||||
entryNames: "[dir]/[name]-[hash]",
|
entryNames: "[dir]/[name]-[hash]",
|
||||||
outdir: path.join(OUTPUT_DIR, "public"),
|
outdir: path.join(OUTPUT_DIR, "public"),
|
||||||
|
|||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
declare -A FONTS=(
|
||||||
|
[InterVariable]=https://rsms.me/inter/font-files/InterVariable.woff2
|
||||||
|
[InterVariable-Italic]=https://rsms.me/inter/font-files/InterVariable-Italic.woff2
|
||||||
|
[RobotoMono]=https://raw.githubusercontent.com/googlefonts/RobotoMono/main/fonts/variable/RobotoMono%5Bwght%5D.ttf
|
||||||
|
)
|
||||||
|
|
||||||
|
for name in "${!FONTS[@]}"
|
||||||
|
do
|
||||||
|
url="${FONTS[$name]}"
|
||||||
|
echo "Downloading $url"
|
||||||
|
TMP=$(mktemp)
|
||||||
|
curl "$url" -s --output "$TMP"
|
||||||
|
pyftsubset "$TMP" --output-file="../ui/css/$name.woff2" --flavor=woff2 --unicodes="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD" --layout-features+="tnum"
|
||||||
|
done
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { complement } from "espresso-iisojs";
|
import { complement } from "espresso-iisojs";
|
||||||
import { Expression } from "../../types";
|
import { Expression } from "../../types.ts";
|
||||||
import normalize from "./normalize";
|
import normalize from "./normalize.ts";
|
||||||
import { Clause, SynthContext } from "./synth";
|
import { Clause, SynthContext } from "./synth.ts";
|
||||||
import { and, or } from "./util";
|
import { and, or } from "./util.ts";
|
||||||
|
|
||||||
type Bookmark = Record<string, null | boolean | number | string>;
|
type Bookmark = Record<string, null | boolean | number | string>;
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ router.post("/login", async (ctx) => {
|
|||||||
|
|
||||||
const username = ctx.request.body.username;
|
const username = ctx.request.body.username;
|
||||||
const password = ctx.request.body.password;
|
const password = ctx.request.body.password;
|
||||||
|
const remember = ctx.request.body.remember;
|
||||||
|
const TWO_WEEKS_SECS = 1209600;
|
||||||
|
const ONE_DAY_SECS = 86400;
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
message: "Log in",
|
message: "Log in",
|
||||||
@@ -115,8 +118,14 @@ router.post("/login", async (ctx) => {
|
|||||||
|
|
||||||
function success(authMethod): void {
|
function success(authMethod): void {
|
||||||
log.method = authMethod;
|
log.method = authMethod;
|
||||||
const token = jwt.sign({ username, authMethod }, JWT_SECRET);
|
const expiresIn = remember ? TWO_WEEKS_SECS : ONE_DAY_SECS;
|
||||||
ctx.cookies.set(JWT_COOKIE, token, { sameSite: "lax" });
|
const token = jwt.sign({ username, authMethod }, JWT_SECRET, {
|
||||||
|
expiresIn,
|
||||||
|
});
|
||||||
|
ctx.cookies.set(JWT_COOKIE, token, {
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: remember ? expiresIn * 1000 : null,
|
||||||
|
});
|
||||||
ctx.body = JSON.stringify(token);
|
ctx.body = JSON.stringify(token);
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
}
|
}
|
||||||
@@ -219,7 +228,7 @@ router.get("/", async (ctx) => {
|
|||||||
<link rel="shortcut icon" type="image/png" href="${FAVICON_PNG}" />
|
<link rel="shortcut icon" type="image/png" href="${FAVICON_PNG}" />
|
||||||
<link rel="stylesheet" href="${APP_CSS}">
|
<link rel="stylesheet" href="${APP_CSS}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="h-full bg-stone-100">
|
||||||
<noscript>GenieACS UI requires JavaScript to work. Please enable JavaScript in your browser.</noscript>
|
<noscript>GenieACS UI requires JavaScript to work. Please enable JavaScript in your browser.</noscript>
|
||||||
<script>
|
<script>
|
||||||
window.clientConfig = ${JSON.stringify({
|
window.clientConfig = ${JSON.stringify({
|
||||||
|
|||||||
Generated
+1906
-86
File diff suppressed because it is too large.
Load diff
+9
-2
@@ -42,7 +42,14 @@
|
|||||||
"seedrandom": "^3.0.5"
|
"seedrandom": "^3.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/codemirror": "^5.60.17",
|
"@codemirror/commands": "^6.10.2",
|
||||||
|
"@codemirror/lang-javascript": "^6.2.5",
|
||||||
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
|
"@codemirror/language": "^6.12.2",
|
||||||
|
"@codemirror/state": "^6.5.4",
|
||||||
|
"@codemirror/view": "^6.39.16",
|
||||||
|
"@tailwindcss/cli": "^4.2.1",
|
||||||
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/koa": "^2.15.0",
|
"@types/koa": "^2.15.0",
|
||||||
"@types/koa-compress": "^4.0.7",
|
"@types/koa-compress": "^4.0.7",
|
||||||
@@ -52,7 +59,6 @@
|
|||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||||
"@typescript-eslint/parser": "^7.18.0",
|
"@typescript-eslint/parser": "^7.18.0",
|
||||||
"codemirror": "^5.65.21",
|
|
||||||
"esbuild": "^0.27.4",
|
"esbuild": "^0.27.4",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-prettier": "^9.1.2",
|
"eslint-config-prettier": "^9.1.2",
|
||||||
@@ -60,6 +66,7 @@
|
|||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"sql.js": "^1.14.1",
|
"sql.js": "^1.14.1",
|
||||||
"svgo": "^3.3.3",
|
"svgo": "^3.3.3",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"yaml": "^1.10.2"
|
"yaml": "^1.10.2"
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-1
@@ -12,7 +12,9 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"jsxFactory": "m"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./bin/*.ts",
|
"./bin/*.ts",
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
import m, { ClosureComponent, Component } from "mithril";
|
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
|
||||||
return {
|
|
||||||
view: (vnode) => {
|
|
||||||
const active = { [vnode.attrs["page"]]: "active" };
|
|
||||||
const tabs = [];
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("presets", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["presets"] },
|
|
||||||
m("a", { href: "#!/admin/presets" }, "Presets"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("provisions", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["provisions"] },
|
|
||||||
m("a", { href: "#!/admin/provisions" }, "Provisions"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("virtualParameters", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["virtualParameters"] },
|
|
||||||
m(
|
|
||||||
"a",
|
|
||||||
{ href: "#!/admin/virtualParameters" },
|
|
||||||
"Virtual Parameters",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("files", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["files"] },
|
|
||||||
m("a", { href: "#!/admin/files" }, "Files"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("config", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["config"] },
|
|
||||||
m("a", { href: "#!/admin/config" }, "Config"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("permissions", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["permissions"] },
|
|
||||||
m("a", { href: "#!/admin/permissions" }, "Permissions"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("users", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["users"] },
|
|
||||||
m("a", { href: "#!/admin/users" }, "Users"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return m("nav#side-menu", m("ul", tabs));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default component;
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import m, { RouteResolver } from "mithril";
|
import m, { RouteResolver } from "mithril";
|
||||||
import layout from "./layout.ts";
|
import layout from "./layout.tsx";
|
||||||
import * as store from "./store.ts";
|
import * as store from "./store.ts";
|
||||||
import * as wizardPage from "./wizard-page.ts";
|
import * as wizardPage from "./wizard-page.ts";
|
||||||
import * as loginPage from "./login-page.ts";
|
import * as loginPage from "./login-page.tsx";
|
||||||
import * as overviewPage from "./overview-page.ts";
|
import * as overviewPage from "./overview-page.ts";
|
||||||
import * as devicesPage from "./devices-page.ts";
|
import * as devicesPage from "./devices-page.ts";
|
||||||
import * as devicePage from "./device-page.ts";
|
import * as devicePage from "./device-page.ts";
|
||||||
@@ -16,9 +16,10 @@ import * as configPage from "./config-page.ts";
|
|||||||
import * as permissionsPage from "./permissions-page.ts";
|
import * as permissionsPage from "./permissions-page.ts";
|
||||||
import * as usersPage from "./users-page.ts";
|
import * as usersPage from "./users-page.ts";
|
||||||
import Authorizer from "../lib//common/authorizer.ts";
|
import Authorizer from "../lib//common/authorizer.ts";
|
||||||
import * as notifications from "./notifications.ts";
|
|
||||||
import { contextifyComponent } from "./components.ts";
|
import { contextifyComponent } from "./components.ts";
|
||||||
import { PermissionSet, UiConfig } from "../lib/types.ts";
|
import { PermissionSet, UiConfig } from "../lib/types.ts";
|
||||||
|
import drawerComponent from "./drawer-component.ts";
|
||||||
|
import { render as renderOverlay } from "./overlay.ts";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -33,16 +34,6 @@ declare global {
|
|||||||
|
|
||||||
window.authorizer = new Authorizer(window.permissionSets);
|
window.authorizer = new Authorizer(window.permissionSets);
|
||||||
|
|
||||||
const adminPages = [
|
|
||||||
"presets",
|
|
||||||
"provisions",
|
|
||||||
"virtualParameters",
|
|
||||||
"files",
|
|
||||||
"config",
|
|
||||||
"users",
|
|
||||||
"permissions",
|
|
||||||
];
|
|
||||||
|
|
||||||
let state;
|
let state;
|
||||||
|
|
||||||
function pagify(pageName, page): RouteResolver {
|
function pagify(pageName, page): RouteResolver {
|
||||||
@@ -52,10 +43,14 @@ function pagify(pageName, page): RouteResolver {
|
|||||||
let p;
|
let p;
|
||||||
if (state?.error) p = m(errorPage.component, state);
|
if (state?.error) p = m(errorPage.component, state);
|
||||||
else p = m(contextifyComponent(page.component), state);
|
else p = m(contextifyComponent(page.component), state);
|
||||||
const attrs = {};
|
const attrs = {
|
||||||
attrs["page"] = pageName;
|
page: pageName,
|
||||||
attrs["oncreate"] = attrs["onupdate"] = () => {
|
oncreate: () => {
|
||||||
store.fulfill(lastRenderTimestamp);
|
store.fulfill(lastRenderTimestamp);
|
||||||
|
},
|
||||||
|
onupdate: () => {
|
||||||
|
store.fulfill(lastRenderTimestamp);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return m(layout, attrs, p);
|
return m(layout, attrs, p);
|
||||||
},
|
},
|
||||||
@@ -78,10 +73,8 @@ function pagify(pageName, page): RouteResolver {
|
|||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (!window.username && err.message.indexOf("authorized") >= 0) {
|
if (!window.username && err.message.indexOf("authorized") >= 0)
|
||||||
notifications.push("error", err.message);
|
|
||||||
m.route.set("/login", { continue: requestedPath });
|
m.route.set("/login", { continue: requestedPath });
|
||||||
}
|
|
||||||
state = { error: err.message };
|
state = { error: err.message };
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
@@ -91,37 +84,20 @@ function pagify(pageName, page): RouteResolver {
|
|||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
function redirectAdminPage(): RouteResolver {
|
|
||||||
const component: RouteResolver = {
|
|
||||||
onmatch: () => {
|
|
||||||
for (const page of adminPages) {
|
|
||||||
if (window.authorizer.hasAccess(page, 2)) {
|
|
||||||
m.route.set(`/admin/${page}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return component;
|
|
||||||
}
|
|
||||||
|
|
||||||
m.route(document.body, "/overview", {
|
m.route(document.body, "/overview", {
|
||||||
"/wizard": pagify("wizard", wizardPage),
|
"/wizard": pagify("wizard", wizardPage),
|
||||||
"/login": pagify("login", loginPage),
|
|
||||||
"/overview": pagify("overview", overviewPage),
|
"/overview": pagify("overview", overviewPage),
|
||||||
"/devices": pagify("devices", devicesPage),
|
"/devices": pagify("devices", devicesPage),
|
||||||
"/devices/:id": pagify("devices", devicePage),
|
"/devices/:id": pagify("devices", devicePage),
|
||||||
"/faults": pagify("faults", faultsPage),
|
"/faults": pagify("faults", faultsPage),
|
||||||
"/admin": redirectAdminPage(),
|
"/presets": pagify("presets", presetsPage),
|
||||||
"/admin/presets": pagify("presets", presetsPage),
|
"/provisions": pagify("provisions", provisionsPage),
|
||||||
"/admin/provisions": pagify("provisions", provisionsPage),
|
"/virtualParameters": pagify("virtualParameters", virtualParametersPage),
|
||||||
"/admin/virtualParameters": pagify(
|
"/files": pagify("files", filesPage),
|
||||||
"virtualParameters",
|
"/config": pagify("config", configPage),
|
||||||
virtualParametersPage,
|
"/users": pagify("users", usersPage),
|
||||||
),
|
"/permissions": pagify("permissions", permissionsPage),
|
||||||
"/admin/files": pagify("files", filesPage),
|
"/login": {
|
||||||
"/admin/config": pagify("config", configPage),
|
render: () => [m(loginPage.component), renderOverlay(), m(drawerComponent)],
|
||||||
"/admin/users": pagify("users", usersPage),
|
},
|
||||||
"/admin/permissions": pagify("permissions", permissionsPage),
|
|
||||||
});
|
});
|
||||||
@@ -12,7 +12,7 @@ export default class Autocomplete {
|
|||||||
declare private selection: number;
|
declare private selection: number;
|
||||||
declare private container: HTMLElement;
|
declare private container: HTMLElement;
|
||||||
|
|
||||||
public constructor(className: string, callback: AutocompleteCallback) {
|
public constructor(callback: AutocompleteCallback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.element = null;
|
this.element = null;
|
||||||
this.hideTimeout = null;
|
this.hideTimeout = null;
|
||||||
@@ -24,7 +24,8 @@ export default class Autocomplete {
|
|||||||
this.container.style.position = "absolute";
|
this.container.style.position = "absolute";
|
||||||
this.container.style.display = "block";
|
this.container.style.display = "block";
|
||||||
this.container.style.opacity = "0";
|
this.container.style.opacity = "0";
|
||||||
this.container.className = className;
|
this.container.className =
|
||||||
|
"absolute py-1 mt-2 rounded-md shadow-lg bg-white";
|
||||||
}
|
}
|
||||||
|
|
||||||
public attach(el: HTMLInputElement): void {
|
public attach(el: HTMLInputElement): void {
|
||||||
@@ -140,9 +141,10 @@ export default class Autocomplete {
|
|||||||
for (const [idx, suggestion] of suggestions.entries()) {
|
for (const [idx, suggestion] of suggestions.entries()) {
|
||||||
const e = document.createElement("div");
|
const e = document.createElement("div");
|
||||||
if (suggestion.tip) e.title = suggestion.tip;
|
if (suggestion.tip) e.title = suggestion.tip;
|
||||||
e.classList.add("suggestion");
|
e.className =
|
||||||
|
"text-stone-700 block px-4 py-2 text-sm hover:bg-stone-100 hover:text-stone-900";
|
||||||
if (idx === this.selection) {
|
if (idx === this.selection) {
|
||||||
e.classList.add("selected");
|
e.classList.add("bg-stone-100", "text-stone-900");
|
||||||
selectedElement = e;
|
selectedElement = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,19 +21,26 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
const form = [
|
const form = [
|
||||||
m(
|
m(
|
||||||
"p",
|
"p",
|
||||||
m("label", { for: "username" }, "Username"),
|
m(
|
||||||
m("input", {
|
"label.block text-sm font-semibold text-stone-700 mt-2 mb-1",
|
||||||
name: "username",
|
{ for: "username" },
|
||||||
type: "text",
|
"Username",
|
||||||
value: vnode.state["username"],
|
),
|
||||||
disabled: !!username,
|
m(
|
||||||
oninput: (e) => {
|
"input.shadow-sm focus:ring-cyan-500 focus:border-cyan-500 block sm:text-sm border-stone-300 rounded-md",
|
||||||
vnode.state["username"] = e.target.value;
|
{
|
||||||
|
name: "username",
|
||||||
|
type: "text",
|
||||||
|
value: vnode.state["username"],
|
||||||
|
disabled: !!username,
|
||||||
|
oninput: (e) => {
|
||||||
|
vnode.state["username"] = e.target.value;
|
||||||
|
},
|
||||||
|
oncreate: (_vnode) => {
|
||||||
|
(_vnode.dom as HTMLSelectElement).focus();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
oncreate: (_vnode) => {
|
),
|
||||||
(_vnode.dom as HTMLSelectElement).focus();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -48,31 +55,38 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
form.push(
|
form.push(
|
||||||
m(
|
m(
|
||||||
"p",
|
"p",
|
||||||
m("label", { for: f }, l),
|
m(
|
||||||
m("input", {
|
"label.block text-sm font-semibold text-stone-700 mt-2 mb-1",
|
||||||
name: f,
|
{ for: f },
|
||||||
type: "password",
|
l,
|
||||||
value: vnode.state[f],
|
),
|
||||||
oninput: (e) => {
|
m(
|
||||||
vnode.state[f] = e.target.value;
|
"input.shadow-sm focus:ring-cyan-500 focus:border-cyan-500 block sm:text-sm border-stone-300 rounded-md",
|
||||||
|
{
|
||||||
|
name: f,
|
||||||
|
type: "password",
|
||||||
|
value: vnode.state[f],
|
||||||
|
oninput: (e) => {
|
||||||
|
vnode.state[f] = e.target.value;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = m(
|
const submit = m(
|
||||||
"button.primary",
|
"button.ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-xs text-sm font-medium rounded-md text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500",
|
||||||
{
|
{
|
||||||
type: "submit",
|
type: "submit",
|
||||||
},
|
},
|
||||||
"Change password",
|
"Change password",
|
||||||
) as VnodeDOM;
|
) as VnodeDOM;
|
||||||
|
|
||||||
form.push(m(".actions-bar", submit));
|
form.push(m("div.flex justify-end mt-5", submit));
|
||||||
|
|
||||||
const children = [
|
const children = [
|
||||||
m("h1", "Change password"),
|
m("h2.text-lg leading-6 font-medium text-stone-900", "Change password"),
|
||||||
m(
|
m(
|
||||||
"form",
|
"form",
|
||||||
{
|
{
|
||||||
|
|||||||
+50
-25
@@ -1,4 +1,3 @@
|
|||||||
import { Editor } from "codemirror";
|
|
||||||
import { ClosureComponent } from "mithril";
|
import { ClosureComponent } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import { codeMirror } from "./dynamic-loader.ts";
|
import { codeMirror } from "./dynamic-loader.ts";
|
||||||
@@ -11,41 +10,67 @@ interface Attrs {
|
|||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
onSubmit?: (dom: Element) => void;
|
onSubmit?: (dom: Element) => void;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onReady?: (editor: Editor) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const component: ClosureComponent<Attrs> = () => {
|
const component: ClosureComponent<Attrs> = () => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
return m("textarea", {
|
return m("div.font-mono text-sm", {
|
||||||
name: vnode.attrs.id,
|
|
||||||
value: vnode.attrs.value,
|
|
||||||
oncreate: (_vnode) => {
|
oncreate: (_vnode) => {
|
||||||
const editor = codeMirror.fromTextArea(
|
const theme = codeMirror.EditorView.theme({
|
||||||
_vnode.dom as HTMLTextAreaElement,
|
"&.cm-editor": {
|
||||||
{
|
display: "block",
|
||||||
mode: vnode.attrs.mode,
|
width: "50rem",
|
||||||
lineNumbers: true,
|
height: "30rem",
|
||||||
readOnly: vnode.attrs.readOnly,
|
"max-width": "100%",
|
||||||
extraKeys: {
|
"border-radius": "0.375rem",
|
||||||
"Ctrl-Enter": () => {
|
"border-width": "1px",
|
||||||
if (vnode.attrs.onSubmit) vnode.attrs.onSubmit(_vnode.dom);
|
"border-color": "var(--color-stone-300)",
|
||||||
},
|
"box-shadow":
|
||||||
"Cmd-Enter": () => {
|
"var(--tw-ring-shadow, 0 0 #0000), 0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
||||||
if (vnode.attrs.onSubmit) vnode.attrs.onSubmit(_vnode.dom);
|
overflow: "hidden",
|
||||||
},
|
"& > .cm-scroller": {
|
||||||
|
"font-family": "inherit",
|
||||||
|
"line-height": "inherit",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
"&.cm-editor.cm-focused": {
|
||||||
|
outline: "none",
|
||||||
|
"border-color": "var(--color-cyan-500)",
|
||||||
|
"--tw-ring-shadow": "0 0 0 1px var(--color-cyan-500)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (vnode.attrs.onChange) {
|
const extensions = [
|
||||||
editor.on("change", (e) => {
|
theme,
|
||||||
vnode.attrs.onChange(e.getValue());
|
codeMirror.lineNumbers(),
|
||||||
});
|
codeMirror.history(),
|
||||||
}
|
codeMirror.syntaxHighlighting(codeMirror.defaultHighlightStyle),
|
||||||
|
codeMirror.keymap.of([
|
||||||
|
...codeMirror.defaultKeymap,
|
||||||
|
...codeMirror.historyKeymap,
|
||||||
|
]),
|
||||||
|
codeMirror.EditorState.readOnly.of(!!vnode.attrs.readOnly),
|
||||||
|
codeMirror.EditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged && vnode.attrs.onChange)
|
||||||
|
vnode.attrs.onChange(update.state.doc.toString());
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (vnode.attrs.mode === "javascript")
|
||||||
|
extensions.push(codeMirror.javascript());
|
||||||
|
else if (vnode.attrs.mode === "yaml")
|
||||||
|
extensions.push(codeMirror.yaml());
|
||||||
|
|
||||||
|
const editor = new codeMirror.EditorView({
|
||||||
|
state: codeMirror.EditorState.create({
|
||||||
|
doc: vnode.attrs.value,
|
||||||
|
extensions,
|
||||||
|
}),
|
||||||
|
parent: _vnode.dom as HTMLTextAreaElement,
|
||||||
|
});
|
||||||
|
|
||||||
if (vnode.attrs.focus) editor.focus();
|
if (vnode.attrs.focus) editor.focus();
|
||||||
if (vnode.attrs.onReady) vnode.attrs.onReady(editor);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export { EditorView, keymap, lineNumbers } from "@codemirror/view";
|
||||||
|
export { EditorState } from "@codemirror/state";
|
||||||
|
export { history, historyKeymap, defaultKeymap } from "@codemirror/commands";
|
||||||
|
export {
|
||||||
|
syntaxHighlighting,
|
||||||
|
defaultHighlightStyle,
|
||||||
|
} from "@codemirror/language";
|
||||||
|
export { javascript } from "@codemirror/lang-javascript";
|
||||||
|
export { yaml } from "@codemirror/lang-yaml";
|
||||||
+1
-1
@@ -127,7 +127,7 @@ export function contextifyComponent(component: ComponentTypes): ComponentTypes {
|
|||||||
return res;
|
return res;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return m(
|
return m(
|
||||||
"p.error",
|
"p.text-sm font-bold text-red-500 cursor-pointer",
|
||||||
{
|
{
|
||||||
title: "Click to print stack trace to console",
|
title: "Click to print stack trace to console",
|
||||||
onclick: () => console.error(err),
|
onclick: () => console.error(err),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { m } from "../components.ts";
|
|||||||
import * as taskQueue from "../task-queue.ts";
|
import * as taskQueue from "../task-queue.ts";
|
||||||
import { parse } from "../../lib/common/expression/parser.ts";
|
import { parse } from "../../lib/common/expression/parser.ts";
|
||||||
import memoize from "../../lib/common/memoize.ts";
|
import memoize from "../../lib/common/memoize.ts";
|
||||||
import { getIcon } from "../icons.ts";
|
import { icon } from "../tailwind-utility-components.ts";
|
||||||
import { QueryResponse, evaluateExpression } from "../store.ts";
|
import { QueryResponse, evaluateExpression } from "../store.ts";
|
||||||
import debounce from "../../lib/common/debounce.ts";
|
import debounce from "../../lib/common/debounce.ts";
|
||||||
import { Expression } from "../../lib/types.ts";
|
import { Expression } from "../../lib/types.ts";
|
||||||
@@ -55,14 +55,17 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
const limit =
|
const limit =
|
||||||
(evaluateExpression(vnode.attrs.limit, device) as number) || 100;
|
(evaluateExpression(vnode.attrs.limit, device) as number) || 100;
|
||||||
|
|
||||||
const search = m("input", {
|
const search = m(
|
||||||
type: "text",
|
"input.appearance-none border-0 block w-full px-4 py-3 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 text-sm rounded-t-lg font-mono focus:ring-2",
|
||||||
placeholder: "Search parameters",
|
{
|
||||||
oninput: (e) => {
|
type: "text",
|
||||||
formQueryString(e.target.value);
|
placeholder: "Search parameters",
|
||||||
e.redraw = false;
|
oninput: (e) => {
|
||||||
|
formQueryString(e.target.value);
|
||||||
|
e.redraw = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
const instanceRegex = /\.[0-9]+$/;
|
const instanceRegex = /\.[0-9]+$/;
|
||||||
let re;
|
let re;
|
||||||
@@ -92,8 +95,6 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
const rows = filteredKeys.map((k) => {
|
const rows = filteredKeys.map((k) => {
|
||||||
const p = device[k];
|
const p = device[k];
|
||||||
const val = [];
|
const val = [];
|
||||||
const attrs = { key: k };
|
|
||||||
|
|
||||||
if (p.object === false) {
|
if (p.object === false) {
|
||||||
val.push(
|
val.push(
|
||||||
m(
|
m(
|
||||||
@@ -116,7 +117,11 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("delete-instance"),
|
m(icon, {
|
||||||
|
name: "delete-instance",
|
||||||
|
class:
|
||||||
|
"inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -133,7 +138,11 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("add-instance"),
|
m(icon, {
|
||||||
|
name: "add-instance",
|
||||||
|
class:
|
||||||
|
"inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -152,15 +161,17 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("refresh"),
|
m(icon, {
|
||||||
|
name: "refresh",
|
||||||
|
class: "inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"tr",
|
"tr",
|
||||||
attrs,
|
m("td.pl-4 pr-2 py-2 truncate", m("long-text", { text: k })),
|
||||||
m("td.left", m("long-text", { text: k })),
|
m("td.pr-4 py-2 text-right flex justify-end", val),
|
||||||
m("td.right", val),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,25 +179,37 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
"loading",
|
"loading",
|
||||||
{ queries: [vnode.attrs.deviceQuery] },
|
{ queries: [vnode.attrs.deviceQuery] },
|
||||||
m(
|
m(
|
||||||
".all-parameters",
|
".bg-white shadow-sm rounded-lg",
|
||||||
m(
|
|
||||||
"a.download-csv",
|
|
||||||
{
|
|
||||||
href: `api/devices/${encodeURIComponent(
|
|
||||||
device["DeviceID.ID"].value[0],
|
|
||||||
)}.csv`,
|
|
||||||
download: "",
|
|
||||||
style: "float: right;",
|
|
||||||
},
|
|
||||||
"Download",
|
|
||||||
),
|
|
||||||
search,
|
search,
|
||||||
m(
|
m(
|
||||||
".parameter-list",
|
".overflow-hidden",
|
||||||
m("table", m("tbody", rows)),
|
|
||||||
m(
|
m(
|
||||||
"m",
|
".overflow-y-scroll h-96 shadow-inner",
|
||||||
`Displaying ${filteredKeys.length} out of ${count} parameters.`,
|
m(
|
||||||
|
"table.w-full table-fixed font-mono text-xs text-stone-900",
|
||||||
|
m("tbody.divide-y divide-stone-200", rows),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"div.text-stone-700 px-4 py-3 flex justify-between items-end",
|
||||||
|
m(
|
||||||
|
"span.text-xs",
|
||||||
|
"Displaying ",
|
||||||
|
m("span.font-medium", "" + filteredKeys.length),
|
||||||
|
" out of ",
|
||||||
|
m("span.font-medium", "" + count),
|
||||||
|
" parameters",
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"a.text-cyan-700 hover:text-cyan-900 text-sm font-medium",
|
||||||
|
{
|
||||||
|
href: `api/devices/${encodeURIComponent(
|
||||||
|
device["DeviceID.ID"].value[0],
|
||||||
|
)}.csv`,
|
||||||
|
download: "",
|
||||||
|
},
|
||||||
|
"Download",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Reboot device",
|
title: "Reboot device",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
@@ -29,7 +29,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.critical",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Factory reset device",
|
title: "Factory reset device",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
@@ -45,7 +45,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.critical",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Push a firmware or a config file",
|
title: "Push a firmware or a config file",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
@@ -61,7 +61,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Delete device",
|
title: "Delete device",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
@@ -83,7 +83,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return m(".actions-bar", buttons);
|
return m("div.flex gap-3 mt-4", buttons);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { ClosureComponent, Component } from "mithril";
|
|||||||
import { m } from "../components.ts";
|
import { m } from "../components.ts";
|
||||||
import * as store from "../store.ts";
|
import * as store from "../store.ts";
|
||||||
import * as notifications from "../notifications.ts";
|
import * as notifications from "../notifications.ts";
|
||||||
import { getIcon } from "../icons.ts";
|
|
||||||
import { stringify } from "../../lib/common/yaml.ts";
|
import { stringify } from "../../lib/common/yaml.ts";
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (): Component => {
|
||||||
@@ -23,63 +22,111 @@ const component: ClosureComponent = (): Component => {
|
|||||||
"Detail",
|
"Detail",
|
||||||
"Retries",
|
"Retries",
|
||||||
"Timestamp",
|
"Timestamp",
|
||||||
].map((l) => m("th", l));
|
"",
|
||||||
const thead = m("thead", m("tr", headers));
|
].map((l, i) => {
|
||||||
|
let padding: string;
|
||||||
|
if (i === 0) padding = "pl-6 pr-3";
|
||||||
|
else if (i === 6) padding = "pl-3";
|
||||||
|
else padding = "px-3";
|
||||||
|
return m(
|
||||||
|
"th",
|
||||||
|
{
|
||||||
|
scope: "col",
|
||||||
|
class:
|
||||||
|
"py-3.5 text-left text-sm font-semibold text-stone-500 " +
|
||||||
|
padding,
|
||||||
|
},
|
||||||
|
l,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const thead = m("thead.bg-stone-50", m("tr", headers));
|
||||||
|
|
||||||
const rows = [];
|
const rows = [];
|
||||||
for (const f of faults.value) {
|
for (const f of faults.value) {
|
||||||
rows.push([
|
rows.push(
|
||||||
m("td", f["channel"]),
|
|
||||||
m("td", f["code"]),
|
|
||||||
m("td", m("long-text", { text: f["message"] })),
|
|
||||||
m("td", m("long-text", { text: stringify(f["detail"]) })),
|
|
||||||
|
|
||||||
m("td", f["retries"]),
|
|
||||||
m("td", new Date(f["timestamp"]).toLocaleString()),
|
|
||||||
m(
|
m(
|
||||||
"td",
|
"tr",
|
||||||
m(
|
m(
|
||||||
"button",
|
"td.whitespace-nowrap pl-6 pr-3 py-4 text-sm text-stone-900",
|
||||||
{
|
f["channel"],
|
||||||
title: "Delete fault",
|
),
|
||||||
onclick: (e) => {
|
m(
|
||||||
e.redraw = false;
|
"td.whitespace-nowrap px-3 py-4 text-sm text-stone-900",
|
||||||
store
|
f["code"],
|
||||||
.deleteResource("faults", f["_id"])
|
),
|
||||||
.then(() => {
|
m(
|
||||||
notifications.push("success", "Fault deleted");
|
"td.whitespace-nowrap px-3 py-4 text-sm text-stone-900",
|
||||||
store.setTimestamp(Date.now());
|
m("long-text", { text: f["message"], class: "max-w-xs" }),
|
||||||
m.redraw();
|
),
|
||||||
})
|
m(
|
||||||
.catch((err) => {
|
"td.whitespace-nowrap px-3 py-4 text-sm text-stone-900",
|
||||||
notifications.push("error", err.message);
|
m("long-text", {
|
||||||
store.setTimestamp(Date.now());
|
text: stringify(f["detail"]),
|
||||||
});
|
class: "max-w-xs",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
m(
|
||||||
|
"td.whitespace-nowrap px-3 py-4 text-sm text-stone-900",
|
||||||
|
f["retries"],
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"td.whitespace-nowrap px-3 py-4 text-sm text-stone-900",
|
||||||
|
new Date(f["timestamp"]).toLocaleString(),
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"td.whitespace-nowrap pl-3 pr-6 py-4 text-sm text-stone-900",
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class: "text-cyan-700 hover:text-cyan-900 font-medium",
|
||||||
|
title: "Delete fault",
|
||||||
|
onclick: (e) => {
|
||||||
|
e.redraw = false;
|
||||||
|
store
|
||||||
|
.deleteResource("faults", f["_id"])
|
||||||
|
.then(() => {
|
||||||
|
notifications.push("success", "Fault deleted");
|
||||||
|
store.setTimestamp(Date.now());
|
||||||
|
m.redraw();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
store.setTimestamp(Date.now());
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
"Delete",
|
||||||
getIcon("remove"),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tbody;
|
if (!rows.length) {
|
||||||
if (rows.length) {
|
rows.push(
|
||||||
tbody = m(
|
m(
|
||||||
"tbody",
|
"tr",
|
||||||
rows.map((r) => m("tr", r)),
|
m(
|
||||||
);
|
"td.bg-stripes text-sm font-medium text-center text-stone-500 p-4",
|
||||||
} else {
|
{ colspan: headers.length },
|
||||||
tbody = m(
|
"No faults",
|
||||||
"tbody",
|
),
|
||||||
m("tr.empty", m("td", { colspan: headers.length }, "No faults")),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"loading",
|
"loading",
|
||||||
{ queries: [faults] },
|
{ queries: [faults] },
|
||||||
m("table.table", thead, tbody),
|
m(
|
||||||
|
"div.shadow-sm overflow-hidden rounded-lg w-max",
|
||||||
|
m(
|
||||||
|
"table.divide-y divide-stone-200",
|
||||||
|
thead,
|
||||||
|
m("tbody.divide-y divide-stone-200 bg-white", rows),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
});
|
});
|
||||||
if (deviceId) {
|
if (deviceId) {
|
||||||
return m(
|
return m(
|
||||||
"a",
|
"a.text-cyan-700 hover:text-cyan-900 font-medium",
|
||||||
{ href: `#!/devices/${encodeURIComponent(deviceId)}` },
|
{ href: `#!/devices/${encodeURIComponent(deviceId)}` },
|
||||||
children,
|
children,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,25 +16,22 @@ const component: ClosureComponent = (): Component => {
|
|||||||
const filter = slice["filter"];
|
const filter = slice["filter"];
|
||||||
if (evaluateExpression(filter, device || {})) {
|
if (evaluateExpression(filter, device || {})) {
|
||||||
const dot = m(
|
const dot = m(
|
||||||
"svg",
|
"svg.inline",
|
||||||
{
|
{
|
||||||
width: "1em",
|
width: "1em",
|
||||||
height: "1em",
|
height: "1em",
|
||||||
|
style: "margin: 0 0.2em 0.2em",
|
||||||
xmlns: "http://www.w3.org/2000/svg",
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
||||||
},
|
},
|
||||||
m("circle", {
|
m("circle.stroke-stone-200 stroke-1", {
|
||||||
cx: "0.5em",
|
cx: "0.5em",
|
||||||
cy: "0.5em",
|
cy: "0.5em",
|
||||||
r: "0.4em",
|
r: "0.4em",
|
||||||
fill: evaluateExpression(slice["color"], null),
|
fill: evaluateExpression(slice["color"], null),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return m(
|
return m("span", dot, evaluateExpression(slice["label"], null));
|
||||||
"span.overview-dot",
|
|
||||||
dot,
|
|
||||||
evaluateExpression(slice["label"], null),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"tr",
|
"div.py-3 grid grid-cols-3 gap-4 px-6",
|
||||||
{
|
{
|
||||||
oncreate: (vn) => {
|
oncreate: (vn) => {
|
||||||
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
|
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
|
||||||
@@ -43,15 +43,22 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
: "none";
|
: "none";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
m("th", evaluateExpression(parameter.label, device)),
|
m(
|
||||||
m("td", p),
|
"dt.text-sm font-medium text-stone-500",
|
||||||
|
evaluateExpression(parameter["label"], device),
|
||||||
|
),
|
||||||
|
m("dd.text-sm text-stone-900 col-span-2", p),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"loading",
|
"loading",
|
||||||
{ queries: [vnode.attrs.deviceQuery] },
|
{ queries: [vnode.attrs.deviceQuery] },
|
||||||
m("table.parameter-list", rows),
|
m(
|
||||||
|
"dl.bg-white shadow-sm overflow-hidden rounded-lg w-max py-1",
|
||||||
|
{ class: "[&>*+*]:border-t [&>*+*]:border-stone-200" },
|
||||||
|
rows,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { m } from "../components.ts";
|
|||||||
import * as taskQueue from "../task-queue.ts";
|
import * as taskQueue from "../task-queue.ts";
|
||||||
import { QueryResponse, evaluateExpression } from "../store.ts";
|
import { QueryResponse, evaluateExpression } from "../store.ts";
|
||||||
import * as expressionParser from "../../lib/common/expression/parser.ts";
|
import * as expressionParser from "../../lib/common/expression/parser.ts";
|
||||||
import { getIcon } from "../icons.ts";
|
import { icon } from "../tailwind-utility-components.ts";
|
||||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||||
import { Expression } from "../../lib/types.ts";
|
import { Expression } from "../../lib/types.ts";
|
||||||
|
|
||||||
@@ -42,11 +42,24 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = Object.values(parameters).map((p) =>
|
const headers = Object.values(parameters).map((p, i) => {
|
||||||
m("th", evaluateExpression(p["label"], device)),
|
const padding = i ? "px-3" : "pl-6 pr-3";
|
||||||
);
|
|
||||||
|
|
||||||
const thead = m("thead", m("tr", headers));
|
return m(
|
||||||
|
"th",
|
||||||
|
{
|
||||||
|
scope: "col",
|
||||||
|
class:
|
||||||
|
"py-3.5 text-left text-sm font-semibold text-stone-500 " +
|
||||||
|
padding,
|
||||||
|
},
|
||||||
|
evaluateExpression(p["label"], device),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
headers.push(m("th.pl-3", { scope: "col" }));
|
||||||
|
|
||||||
|
const thead = m("thead.bg-stone-50", m("tr", headers));
|
||||||
|
|
||||||
const rows = [];
|
const rows = [];
|
||||||
for (const i of instances) {
|
for (const i of instances) {
|
||||||
@@ -60,7 +73,9 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
|
|
||||||
if (!evaluateExpression(filter, device)) continue;
|
if (!evaluateExpression(filter, device)) continue;
|
||||||
|
|
||||||
const row = parameters.map((p) => {
|
const row = parameters.map((p, j) => {
|
||||||
|
const padding = j ? "px-3" : "pl-6 pr-3";
|
||||||
|
|
||||||
const param = expressionParser.map(p.parameter, (e) => {
|
const param = expressionParser.map(p.parameter, (e) => {
|
||||||
if (Array.isArray(e) && e[0] === "PARAM")
|
if (Array.isArray(e) && e[0] === "PARAM")
|
||||||
return ["PARAM", ["||", i, ".", e[1]]];
|
return ["PARAM", ["||", i, ".", e[1]]];
|
||||||
@@ -68,6 +83,9 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
});
|
});
|
||||||
return m(
|
return m(
|
||||||
"td",
|
"td",
|
||||||
|
{
|
||||||
|
class: "whitespace-nowrap py-4 text-sm text-stone-900 " + padding,
|
||||||
|
},
|
||||||
m.context(
|
m.context(
|
||||||
{
|
{
|
||||||
device: device,
|
device: device,
|
||||||
@@ -87,6 +105,10 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
row.push(
|
row.push(
|
||||||
m(
|
m(
|
||||||
"td",
|
"td",
|
||||||
|
{
|
||||||
|
class:
|
||||||
|
"whitespace-nowrap pl-3 pr-6 py-4 text-sm text-stone-900",
|
||||||
|
},
|
||||||
m(
|
m(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
@@ -99,17 +121,29 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("delete-instance"),
|
m(icon, {
|
||||||
|
name: "delete-instance",
|
||||||
|
class: "inline h-4 w-4 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
row.push(m("td"));
|
||||||
}
|
}
|
||||||
rows.push(m("tr", row));
|
rows.push(m("tr", row));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
rows.push(
|
rows.push(
|
||||||
m("tr.empty", m("td", { colspan: headers.length }, "No instances")),
|
m(
|
||||||
|
"tr",
|
||||||
|
m(
|
||||||
|
"td.bg-stripes text-sm font-medium text-center text-stone-500 p-4",
|
||||||
|
{ colspan: headers.length },
|
||||||
|
"No instances",
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +166,11 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("add-instance"),
|
m(icon, {
|
||||||
|
name: "add-instance",
|
||||||
|
class:
|
||||||
|
"inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -149,7 +187,14 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
m(
|
m(
|
||||||
"loading",
|
"loading",
|
||||||
{ queries: [vnode.attrs.deviceQuery] },
|
{ queries: [vnode.attrs.deviceQuery] },
|
||||||
m("table.table", thead, m("tbody", rows)),
|
m(
|
||||||
|
"div.shadow-sm overflow-hidden rounded-lg w-max",
|
||||||
|
m(
|
||||||
|
"table.divide-y divide-stone-200",
|
||||||
|
thead,
|
||||||
|
m("tbody.divide-y divide-stone-200 bg-white", rows),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as store from "../store.ts";
|
|||||||
import { evaluate } from "../../lib/common/expression/util.ts";
|
import { evaluate } from "../../lib/common/expression/util.ts";
|
||||||
import memoize from "../../lib/common/memoize.ts";
|
import memoize from "../../lib/common/memoize.ts";
|
||||||
import timeAgo from "../timeago.ts";
|
import timeAgo from "../timeago.ts";
|
||||||
import { getIcon } from "../icons.ts";
|
import { icon } from "../tailwind-utility-components.ts";
|
||||||
|
|
||||||
const evaluateParam = memoize((exp, obj, now: number) => {
|
const evaluateParam = memoize((exp, obj, now: number) => {
|
||||||
let timestamp = now;
|
let timestamp = now;
|
||||||
@@ -84,16 +84,18 @@ const component: ClosureComponent = (): Component => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("edit"),
|
m(icon, {
|
||||||
|
name: "edit",
|
||||||
|
class: "inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = m("long-text", { text: `${value}` });
|
const el = m("long-text", { text: `${value}` });
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"span",
|
"span.inline-flex overflow-hidden align-top",
|
||||||
{
|
{
|
||||||
class: "parameter-value",
|
|
||||||
onmouseover: (e) => {
|
onmouseover: (e) => {
|
||||||
e.redraw = false;
|
e.redraw = false;
|
||||||
// Don't update any child element
|
// Don't update any child element
|
||||||
@@ -104,7 +106,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
el,
|
m("span.truncate", el),
|
||||||
edit,
|
edit,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const component: ClosureComponent = (vn): Component => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m("div", host ? `Pinging ${host}:` : "");
|
return m("div.text-sm my-4", host ? `Pinging ${host}:` : "");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ const component: ClosureComponent = (): Component => {
|
|||||||
const device = vnode.attrs["device"];
|
const device = vnode.attrs["device"];
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"button.primary",
|
"button",
|
||||||
{
|
{
|
||||||
|
class:
|
||||||
|
"px-2.5 py-1.5 border border-transparent text-xs font-medium rounded-sm shadow-xs text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500",
|
||||||
title: "Initiate session and refresh basic parameters",
|
title: "Initiate session and refresh basic parameters",
|
||||||
onclick: (e) => {
|
onclick: (e) => {
|
||||||
e.target.disabled = true;
|
e.target.disabled = true;
|
||||||
|
|||||||
+40
-9
@@ -2,7 +2,7 @@ import { ClosureComponent } from "mithril";
|
|||||||
import { m } from "../components.ts";
|
import { m } from "../components.ts";
|
||||||
import * as notifications from "../notifications.ts";
|
import * as notifications from "../notifications.ts";
|
||||||
import * as store from "../store.ts";
|
import * as store from "../store.ts";
|
||||||
import { getIcon } from "../icons.ts";
|
import { icon } from "../tailwind-utility-components.ts";
|
||||||
import { decodeTag } from "../../lib/util.ts";
|
import { decodeTag } from "../../lib/util.ts";
|
||||||
import { Expression } from "../../lib/types.ts";
|
import { Expression } from "../../lib/types.ts";
|
||||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||||
@@ -28,20 +28,36 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
|
|
||||||
if (!writable) {
|
if (!writable) {
|
||||||
return m(
|
return m(
|
||||||
".tags",
|
"div",
|
||||||
tags.map((t) => m("span.tag", t)),
|
tags.map((t) =>
|
||||||
|
m(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
class:
|
||||||
|
"inline-flex items-center px-3 py-0.5 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800 mr-2 -my-0.5 ring-1 ring-yellow-200",
|
||||||
|
},
|
||||||
|
t,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
".tags",
|
"div",
|
||||||
tags.map((tag) =>
|
tags.map((tag) =>
|
||||||
m(
|
m(
|
||||||
"span.tag",
|
"span",
|
||||||
|
{
|
||||||
|
class:
|
||||||
|
"inline-flex items-center pl-3 pr-1 py-0.5 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800 mr-2 ring-1 ring-yellow-200",
|
||||||
|
},
|
||||||
tag,
|
tag,
|
||||||
m(
|
m(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
|
title: "Remove tag",
|
||||||
|
class:
|
||||||
|
"flex-shrink-0 ml-0.5 h-4 w-4 rounded-full inline-flex items-center justify-center text-yellow-400 hover:bg-yellow-200 hover:text-yellow-500 focus:outline-hidden focus:bg-yellow-500 focus:text-white",
|
||||||
onclick: (e) => {
|
onclick: (e) => {
|
||||||
e.target.disabled = true;
|
e.target.disabled = true;
|
||||||
const deviceId = device["DeviceID.ID"].value[0] as string;
|
const deviceId = device["DeviceID.ID"].value[0] as string;
|
||||||
@@ -64,16 +80,27 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("remove"),
|
m("span.sr-only", "Remove tag"),
|
||||||
|
m(icon, {
|
||||||
|
name: "remove",
|
||||||
|
class: "h-4 w-4",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
m(
|
m(
|
||||||
"span.tag.writable",
|
"span",
|
||||||
m.trust(" "),
|
{
|
||||||
|
class:
|
||||||
|
"inline-flex items-center pl-1 pr-1 py-0.5 rounded-full text-sm font-medium bg-yellow-50 ring-1 ring-yellow-200",
|
||||||
|
},
|
||||||
|
m.trust("​"),
|
||||||
m(
|
m(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
|
title: "Add tag",
|
||||||
|
class:
|
||||||
|
"flex-shrink-0 h-4 w-4 rounded-full inline-flex items-center justify-center text-yellow-400 hover:bg-yellow-200 hover:text-yellow-500 focus:outline-hidden focus:bg-yellow-500 focus:text-white",
|
||||||
onclick: (e) => {
|
onclick: (e) => {
|
||||||
e.target.disabled = true;
|
e.target.disabled = true;
|
||||||
const deviceId = device["DeviceID.ID"].value[0] as string;
|
const deviceId = device["DeviceID.ID"].value[0] as string;
|
||||||
@@ -96,7 +123,11 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("add"),
|
m("span.sr-only", "Add tag"),
|
||||||
|
m(icon, {
|
||||||
|
name: "add",
|
||||||
|
class: "h-4 w-4",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
+41
-26
@@ -7,7 +7,7 @@ import uiConfigComponent from "./ui-config-component.ts";
|
|||||||
import * as overlay from "./overlay.ts";
|
import * as overlay from "./overlay.ts";
|
||||||
import { parse, stringify } from "../lib/common/expression/parser.ts";
|
import { parse, stringify } from "../lib/common/expression/parser.ts";
|
||||||
import { loadCodeMirror, loadYaml } from "./dynamic-loader.ts";
|
import { loadCodeMirror, loadYaml } from "./dynamic-loader.ts";
|
||||||
import { getIcon } from "./icons.ts";
|
import { icon } from "./tailwind-utility-components.ts";
|
||||||
|
|
||||||
const attributes = [
|
const attributes = [
|
||||||
{ id: "_id", label: "Key" },
|
{ id: "_id", label: "Key" },
|
||||||
@@ -64,6 +64,7 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
|
|||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
} else if (action === "delete") {
|
} else if (action === "delete") {
|
||||||
|
if (!confirm("Deleting config. Are you sure?")) return void resolve(null);
|
||||||
store
|
store
|
||||||
.deleteResource("config", object["_id"])
|
.deleteResource("config", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -166,7 +167,10 @@ function renderTable(confsResponse, searchString): Children {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("edit"),
|
m(icon, {
|
||||||
|
name: "edit",
|
||||||
|
class: "inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const del = m(
|
const del = m(
|
||||||
@@ -181,26 +185,32 @@ function renderTable(confsResponse, searchString): Children {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("remove"),
|
m(icon, {
|
||||||
|
name: "remove",
|
||||||
|
class: "inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
rows.push(
|
rows.push(
|
||||||
m(
|
m(
|
||||||
"tr",
|
"tr",
|
||||||
attrs,
|
attrs,
|
||||||
m("td.left", m("long-text", { text: conf._id })),
|
m("td.pl-4 pr-2 py-2 truncate", m("long-text", { text: conf._id })),
|
||||||
m(
|
m(
|
||||||
"td.right",
|
"td.px-2 py-2 text-right truncate",
|
||||||
m("span", [m("long-text", { text: `${conf.value}` }), edit, del]),
|
m("long-text", { text: `${conf.value}` }),
|
||||||
),
|
),
|
||||||
|
m("td.pl-2 pr-4 py-2 w-max", edit, del),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rows.length)
|
if (!rows.length) rows.push(m("tr", m("td", { colspan: 3 }, "No config")));
|
||||||
rows.push(m("tr.empty", m("td", { colspan: 2 }, "No config")));
|
|
||||||
|
|
||||||
return m("table", m("tbody", rows));
|
return m(
|
||||||
|
"table.w-full table-fixed font-mono text-sm text-stone-700",
|
||||||
|
m("tbody.bg-white divide-y divide-stone-200", rows),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const component: ClosureComponent = (): Component => {
|
export const component: ClosureComponent = (): Component => {
|
||||||
@@ -208,16 +218,19 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
document.title = "Config - GenieACS";
|
document.title = "Config - GenieACS";
|
||||||
|
|
||||||
const search = m("input", {
|
const search = m(
|
||||||
type: "text",
|
"input.appearance-none block w-full px-3 py-2 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 focus:border-cyan-500 sm:text-sm rounded-md mt-1 max-w-screen-sm shadow-sm mb-5",
|
||||||
placeholder: "Search config",
|
{
|
||||||
oninput: (e) => {
|
type: "text",
|
||||||
vnode.state["searchString"] = e.target.value;
|
placeholder: "Search config",
|
||||||
e.redraw = false;
|
oninput: (e) => {
|
||||||
clearTimeout(vnode.state["timeout"]);
|
vnode.state["searchString"] = e.target.value;
|
||||||
vnode.state["timeout"] = setTimeout(m.redraw, 250);
|
e.redraw = false;
|
||||||
|
clearTimeout(vnode.state["timeout"]);
|
||||||
|
vnode.state["timeout"] = setTimeout(m.redraw, 250);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
const confs = store.fetch("config", true);
|
const confs = store.fetch("config", true);
|
||||||
|
|
||||||
@@ -226,7 +239,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
if (window.authorizer.hasAccess("config", 3)) {
|
if (window.authorizer.hasAccess("config", 3)) {
|
||||||
newConfig = m(
|
newConfig = m(
|
||||||
"button.primary",
|
"button.mr-4 px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500",
|
||||||
{
|
{
|
||||||
title: "Create new config",
|
title: "Create new config",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
@@ -295,7 +308,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
const attrs = { prefix: sub.prefix, name: sub.name, data: sub.data };
|
const attrs = { prefix: sub.prefix, name: sub.name, data: sub.data };
|
||||||
subs.push(
|
subs.push(
|
||||||
m(
|
m(
|
||||||
"button",
|
"button.mr-4 px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500",
|
||||||
{
|
{
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
let cb: () => Children = null;
|
let cb: () => Children = null;
|
||||||
@@ -345,19 +358,21 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing config"),
|
m("h1.text-xl font-medium text-stone-900 mb-5", "Listing config"),
|
||||||
m(
|
m(
|
||||||
"loading",
|
"loading",
|
||||||
{ queries: [confs] },
|
{ queries: [confs] },
|
||||||
m(
|
m(
|
||||||
".all-parameters",
|
"div",
|
||||||
search,
|
search,
|
||||||
m(
|
m(
|
||||||
".parameter-list",
|
".shadow-sm overflow-hidden border-b border-stone-200 rounded-lg bg-white",
|
||||||
{ style: "height: 400px" },
|
m(
|
||||||
renderTable(confs, vnode.state["searchString"]),
|
".overflow-y-scroll h-96",
|
||||||
|
renderTable(confs, vnode.state["searchString"]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
m(".actions-bar", [newConfig].concat(subs)),
|
m(".mt-5", [newConfig].concat(subs)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+140
-779
@@ -1,805 +1,166 @@
|
|||||||
@import "normalize.css";
|
@import "tailwindcss";
|
||||||
@import "codemirror/lib/codemirror.css";
|
@plugin "@tailwindcss/forms";
|
||||||
|
|
||||||
:root {
|
@source inline("h-full bg-stone-100");
|
||||||
--base-font-size: 13px;
|
@source "../../ui/**/*.{ts,tsx}";
|
||||||
--base-line-height: 16px;
|
|
||||||
|
|
||||||
--color1: #ebe0bf;
|
@theme {
|
||||||
--color2: #b9b098;
|
--font-sans:
|
||||||
--color3: #f2ebd5;
|
"Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||||
--color4: #046380;
|
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
--color5: #002f2f;
|
--font-mono:
|
||||||
|
"Roboto Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
"Liberation Mono", "Courier New", monospace;
|
||||||
|
|
||||||
--fade: 0.2s;
|
/* Reset default color palette to match v3 exclusive palette */
|
||||||
|
--color-*: initial;
|
||||||
|
|
||||||
--disabled: #666;
|
--color-black: #000;
|
||||||
|
--color-white: #fff;
|
||||||
|
|
||||||
--status-red: 0 80% 80%;
|
--color-cyan-50: #edfdfe;
|
||||||
--status-green: 125 80% 80%;
|
--color-cyan-100: #d3f7fa;
|
||||||
--status-gray: 58 3% 80%;
|
--color-cyan-200: #aceef5;
|
||||||
--status-yellow: 46 79% 80%;
|
--color-cyan-300: #72e0ee;
|
||||||
|
--color-cyan-400: #31c8df;
|
||||||
|
--color-cyan-500: #15abc5;
|
||||||
|
--color-cyan-600: #1589a5;
|
||||||
|
--color-cyan-700: #186e86;
|
||||||
|
--color-cyan-800: #1c5a6e;
|
||||||
|
--color-cyan-900: #1c4b5d;
|
||||||
|
|
||||||
|
--color-stone-50: #faf8f8;
|
||||||
|
--color-stone-100: #f5f2f0;
|
||||||
|
--color-stone-200: #e7e4e2;
|
||||||
|
--color-stone-300: #d6d3d1;
|
||||||
|
--color-stone-400: #a8a29e;
|
||||||
|
--color-stone-500: #78716c;
|
||||||
|
--color-stone-600: #57534e;
|
||||||
|
--color-stone-700: #44403c;
|
||||||
|
--color-stone-800: #292524;
|
||||||
|
--color-stone-900: #1c1917;
|
||||||
|
|
||||||
|
--color-red-50: #fdf3f3;
|
||||||
|
--color-red-100: #fce4e4;
|
||||||
|
--color-red-200: #facece;
|
||||||
|
--color-red-300: #f5acac;
|
||||||
|
--color-red-400: #ee7b7b;
|
||||||
|
--color-red-500: #e25151;
|
||||||
|
--color-red-600: #ce3434;
|
||||||
|
--color-red-700: #ad2828;
|
||||||
|
--color-red-800: #902424;
|
||||||
|
--color-red-900: #782424;
|
||||||
|
|
||||||
|
--color-emerald-50: #edfcf5;
|
||||||
|
--color-emerald-100: #d4f7e5;
|
||||||
|
--color-emerald-200: #adedd0;
|
||||||
|
--color-emerald-300: #77deb5;
|
||||||
|
--color-emerald-400: #40c796;
|
||||||
|
--color-emerald-500: #1dac7d;
|
||||||
|
--color-emerald-600: #108b65;
|
||||||
|
--color-emerald-700: #0d6f53;
|
||||||
|
--color-emerald-800: #0d5843;
|
||||||
|
--color-emerald-900: #0b4938;
|
||||||
|
|
||||||
|
--color-yellow-50: #fcfbea;
|
||||||
|
--color-yellow-100: #faf5c7;
|
||||||
|
--color-yellow-200: #f5e993;
|
||||||
|
--color-yellow-300: #efd755;
|
||||||
|
--color-yellow-400: #e9c226;
|
||||||
|
--color-yellow-500: #d9aa19;
|
||||||
|
--color-yellow-600: #bb8513;
|
||||||
|
--color-yellow-700: #956013;
|
||||||
|
--color-yellow-800: #7c4c17;
|
||||||
|
--color-yellow-900: #6a3f19;
|
||||||
|
|
||||||
|
--color-blue-50: #f0f6fe;
|
||||||
|
--color-blue-100: #deeafb;
|
||||||
|
--color-blue-200: #c4dcf9;
|
||||||
|
--color-blue-300: #9bc5f5;
|
||||||
|
--color-blue-400: #6ca6ee;
|
||||||
|
--color-blue-500: #4985e8;
|
||||||
|
--color-blue-600: #3469dc;
|
||||||
|
--color-blue-700: #2b55ca;
|
||||||
|
--color-blue-800: #2946a4;
|
||||||
|
--color-blue-900: #263e82;
|
||||||
|
|
||||||
|
--background-image-stripes: repeating-linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
transparent,
|
||||||
|
transparent 10px,
|
||||||
|
rgba(0, 0, 0, 0.02) 10px,
|
||||||
|
rgba(0, 0, 0, 0.02) 20px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
body,
|
@supports (font-variation-settings: normal) {
|
||||||
input,
|
@font-face {
|
||||||
button,
|
font-family: "Roboto Mono";
|
||||||
select,
|
src: url("RobotoMono.woff2") format("woff2-variations");
|
||||||
option,
|
font-weight: 100 700;
|
||||||
textarea {
|
font-style: normal;
|
||||||
outline-color: var(--color4);
|
unicode-range:
|
||||||
font-family: sans-serif;
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||||
color: var(--color5);
|
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
||||||
}
|
U+FEFF, U+FFFD;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
@font-face {
|
||||||
color: var(--color4);
|
font-family: "Inter";
|
||||||
cursor: pointer;
|
font-weight: 100 900;
|
||||||
transition:
|
font-display: swap;
|
||||||
background-color var(--fade),
|
font-style: normal;
|
||||||
color var(--fade);
|
font-named-instance: "Regular";
|
||||||
&:visited {
|
src: url("InterVariable.woff2") format("woff2-variations");
|
||||||
color: var(--color4);
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||||
|
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
||||||
|
U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
&:hover {
|
|
||||||
color: white;
|
@font-face {
|
||||||
background-color: var(--color4);
|
font-family: "Inter";
|
||||||
}
|
font-weight: 100 900;
|
||||||
/* text-decoration does not propagate to inline-block children */
|
font-display: swap;
|
||||||
& span {
|
font-style: italic;
|
||||||
text-decoration: inherit;
|
font-named-instance: "Italic";
|
||||||
|
src: url("InterVariable-Italic.woff2") format("woff2-variations");
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||||
|
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
||||||
|
U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
strong,
|
.device-page {
|
||||||
b {
|
h1 {
|
||||||
font-weight: bold;
|
@apply text-xl font-medium text-stone-900 mb-5;
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pending-blink {
|
|
||||||
from {
|
|
||||||
background-color: hsl(var(--status-green) / 0.9);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-wrapper {
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header {
|
|
||||||
background-color: #f2eddf;
|
|
||||||
border-bottom: 1px solid var(--color1);
|
|
||||||
overflow: visible;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
& > .drawer-wrapper {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 10;
|
|
||||||
width: 720px;
|
|
||||||
margin-left: -360px;
|
|
||||||
top: 0;
|
|
||||||
left: 50%;
|
|
||||||
& > .drawer {
|
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
|
||||||
border-bottom-left-radius: 6px;
|
|
||||||
border-bottom-right-radius: 6px;
|
|
||||||
border-bottom: 1px solid var(--color1);
|
|
||||||
border-left: 1px solid var(--color1);
|
|
||||||
border-right: 1px solid var(--color1);
|
|
||||||
margin-top: -1px;
|
|
||||||
box-shadow: 0 0 10px #ccc;
|
|
||||||
overflow: hidden;
|
|
||||||
transition:
|
|
||||||
height var(--fade),
|
|
||||||
opacity var(--fade);
|
|
||||||
|
|
||||||
& > div {
|
|
||||||
padding: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .status {
|
|
||||||
display: flex;
|
|
||||||
& > span {
|
|
||||||
margin-right: 1em;
|
|
||||||
padding-left: 0.2em;
|
|
||||||
padding-right: 0.2em;
|
|
||||||
border-radius: 0.4em;
|
|
||||||
}
|
|
||||||
& > .pending.active {
|
|
||||||
&.active {
|
|
||||||
animation: 0.5s linear 0s infinite alternate pending-blink;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& > .stale.active {
|
|
||||||
background-color: hsl(var(--status-gray) / 0.9);
|
|
||||||
}
|
|
||||||
& > .fault.active {
|
|
||||||
background-color: hsl(var(--status-red) / 0.9);
|
|
||||||
}
|
|
||||||
& > .queued.active {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .actions {
|
|
||||||
flex-grow: 1;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .staging {
|
|
||||||
& .parameter {
|
|
||||||
font-style: italic;
|
|
||||||
max-width: 500px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: inline-block;
|
|
||||||
overflow: hidden;
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
& input {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0.3em;
|
|
||||||
margin-top: 0.3em;
|
|
||||||
margin-bottom: 0.3em;
|
|
||||||
}
|
|
||||||
& .actions {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .queue {
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
& > .pending {
|
|
||||||
animation: 0.5s linear 0s infinite alternate pending-blink;
|
|
||||||
border-radius: 0.4em;
|
|
||||||
}
|
|
||||||
& > .stale {
|
|
||||||
background-color: hsl(var(--status-gray) / 0.9);
|
|
||||||
border-radius: 0.4em;
|
|
||||||
}
|
|
||||||
& > .fault {
|
|
||||||
background-color: hsl(var(--status-red) / 0.9);
|
|
||||||
border-radius: 0.4em;
|
|
||||||
}
|
|
||||||
& .parameter {
|
|
||||||
font-style: italic;
|
|
||||||
max-width: 400px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: inline-block;
|
|
||||||
overflow: hidden;
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
}
|
|
||||||
& .value {
|
|
||||||
max-width: 200px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: inline-block;
|
|
||||||
overflow: hidden;
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
& > div {
|
|
||||||
display: flex;
|
|
||||||
& > .actions {
|
|
||||||
flex-grow: 1;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& > .notifications-wrapper {
|
|
||||||
& > .notification {
|
|
||||||
padding: 0.3em;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
|
||||||
border: 1px solid var(--color1);
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 0 10px #ccc;
|
|
||||||
transition:
|
|
||||||
opacity var(--fade),
|
|
||||||
top var(--fade);
|
|
||||||
|
|
||||||
&.error {
|
|
||||||
background-color: hsl(var(--status-red) / 0.9);
|
|
||||||
border: 1px solid hsl(var(--status-red));
|
|
||||||
}
|
|
||||||
&.success {
|
|
||||||
background-color: hsl(var(--status-green) / 0.9);
|
|
||||||
border: 1px solid hsl(var(--status-green));
|
|
||||||
}
|
|
||||||
&.warning {
|
|
||||||
background-color: hsl(var(--status-yellow) / 0.9);
|
|
||||||
border: 1px solid hsl(var(--status-yellow));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& > nav {
|
|
||||||
display: inline-block;
|
|
||||||
overflow: visible;
|
|
||||||
position: absolute;
|
|
||||||
bottom: -1px;
|
|
||||||
|
|
||||||
& > ul {
|
|
||||||
font-weight: bold;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
& > li {
|
|
||||||
margin: 0 2px;
|
|
||||||
background-color: var(--color1);
|
|
||||||
display: inline-block;
|
|
||||||
border-top-left-radius: 6px;
|
|
||||||
border-top-right-radius: 6px;
|
|
||||||
border-top: 1px solid var(--color1);
|
|
||||||
border-left: 1px solid var(--color1);
|
|
||||||
border-right: 1px solid var(--color1);
|
|
||||||
transition: background-color var(--fade);
|
|
||||||
&.active,
|
|
||||||
&:hover {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > a,
|
|
||||||
& > a:visited,
|
|
||||||
& > a:hover {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 140px;
|
|
||||||
color: var(--color4);
|
|
||||||
text-decoration: none;
|
|
||||||
background: none;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .logo {
|
h2 {
|
||||||
display: inline-block;
|
@apply text-lg font-semibold text-stone-900 mb-5 mt-8;
|
||||||
position: relative;
|
|
||||||
& > img {
|
|
||||||
margin: 10px;
|
|
||||||
height: 56px;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .version {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 5px;
|
|
||||||
right: 10px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .user-menu {
|
h3 {
|
||||||
float: right;
|
@apply text-lg font-medium text-stone-900 mb-5 mt-8;
|
||||||
margin: 5px;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#side-menu {
|
span.inform {
|
||||||
width: 200px;
|
@apply flex gap-3;
|
||||||
min-width: 200px;
|
& > button {
|
||||||
& > ul {
|
@apply -my-1.5;
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
|
|
||||||
& > li {
|
|
||||||
& > a {
|
|
||||||
display: block;
|
|
||||||
text-decoration: none;
|
|
||||||
margin: 5px 0;
|
|
||||||
padding: 8px;
|
|
||||||
border-bottom-right-radius: 6px;
|
|
||||||
border-top-right-radius: 6px;
|
|
||||||
background-color: var(--color3);
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color4);
|
|
||||||
color: var(--color3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
@layer base {
|
||||||
flex-grow: 1;
|
button:not(:disabled),
|
||||||
margin: 10px;
|
[role="button"]:not(:disabled) {
|
||||||
min-height: 250px;
|
cursor: pointer;
|
||||||
|
|
||||||
/* Workaround for Chrome ignoring bottom margin when flex container overflows */
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
padding-bottom: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table.table {
|
|
||||||
border-spacing: 0;
|
|
||||||
text-align: left;
|
|
||||||
overflow-x: auto;
|
|
||||||
|
|
||||||
& table.table {
|
|
||||||
font-size: 90%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& th {
|
:focus-visible {
|
||||||
white-space: nowrap;
|
@apply outline-2 outline-cyan-500;
|
||||||
color: var(--color4);
|
|
||||||
font-weight: bold;
|
|
||||||
border-bottom: 2px solid var(--color4);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.2em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
& td {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.2em 0.5em;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& tfoot td > a {
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.highlight > tbody > tr {
|
|
||||||
& > td {
|
|
||||||
border-bottom: 1px dotted var(--color1);
|
|
||||||
}
|
|
||||||
transition: background-color var(--fade);
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& tbody > tr.empty {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--disabled);
|
|
||||||
&:hover {
|
|
||||||
background-color: initial;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: var(--color4);
|
|
||||||
font-size: 130%;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: var(--color5);
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 120%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
color: var(--color5);
|
|
||||||
font-size: 110%;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.error {
|
|
||||||
color: #a00;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove button padding in FF */
|
|
||||||
input::-moz-focus-inner {
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="button"],
|
|
||||||
button {
|
|
||||||
background: transparent;
|
|
||||||
border: 0;
|
|
||||||
color: var(--color4);
|
|
||||||
border-radius: 4px;
|
|
||||||
transition:
|
|
||||||
background-color var(--fade),
|
|
||||||
color var(--fade);
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
&.primary {
|
|
||||||
border: 1px solid var(--color1);
|
|
||||||
background-color: var(--color3);
|
|
||||||
}
|
|
||||||
&.critical {
|
|
||||||
border: 1px solid var(--color1);
|
|
||||||
background-color: var(--color3);
|
|
||||||
color: #700;
|
|
||||||
}
|
|
||||||
&:hover:enabled {
|
|
||||||
background-color: var(--color4);
|
|
||||||
color: var(--color3);
|
|
||||||
}
|
|
||||||
&:disabled {
|
|
||||||
color: var(--disabled);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror,
|
|
||||||
textarea,
|
|
||||||
select,
|
|
||||||
input {
|
|
||||||
display: block;
|
|
||||||
border: 1px solid var(--color1);
|
|
||||||
background-color: var(--color3);
|
|
||||||
border-radius: 4px;
|
|
||||||
transition:
|
|
||||||
border-color var(--fade),
|
|
||||||
background-color var(--fade);
|
|
||||||
&.error {
|
|
||||||
background-color: #efcbcf;
|
|
||||||
border-color: #e6b2b7;
|
|
||||||
}
|
|
||||||
&:focus,
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--color4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter {
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
& > input {
|
|
||||||
font-size: 90%;
|
|
||||||
width: 35em;
|
|
||||||
margin: 2px;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-overview {
|
|
||||||
& > h1 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.overview-chart-group {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
& h2 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table.parameter-list {
|
|
||||||
border-spacing: 0;
|
|
||||||
text-align: left;
|
|
||||||
& th {
|
|
||||||
padding: 0;
|
|
||||||
padding-right: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.overview-dot {
|
|
||||||
& > svg {
|
|
||||||
margin-bottom: -0.125em;
|
|
||||||
margin-left: 0.2em;
|
|
||||||
margin-right: 0.2em;
|
|
||||||
& > circle {
|
|
||||||
stroke: var(--color5);
|
|
||||||
stroke-width: 0.08em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span.inform {
|
|
||||||
& > * {
|
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pie-chart {
|
|
||||||
margin: 20px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
& > svg {
|
|
||||||
& > path {
|
|
||||||
stroke: var(--color1);
|
|
||||||
stroke-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > a {
|
|
||||||
transition: opacity var(--fade);
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
& > text {
|
|
||||||
font-weight: bold;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > path {
|
|
||||||
stroke: var(--color4);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .legend {
|
|
||||||
text-align: left;
|
|
||||||
margin: 15px;
|
|
||||||
|
|
||||||
& > .legend-line {
|
|
||||||
& > .color {
|
|
||||||
height: 12px;
|
|
||||||
width: 12px;
|
|
||||||
margin-right: 5px;
|
|
||||||
border: 1px solid var(--color1);
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span.na {
|
|
||||||
color: var(--disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
.all-parameters {
|
|
||||||
& > input {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
padding: 4px;
|
|
||||||
width: 700px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .parameter-list {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
& button {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
overflow-y: scroll;
|
|
||||||
height: 200px;
|
|
||||||
|
|
||||||
& > table {
|
|
||||||
width: 100%;
|
|
||||||
table-layout: fixed;
|
|
||||||
border-spacing: 0;
|
|
||||||
& > tbody {
|
|
||||||
& > tr {
|
|
||||||
& > td {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
border-bottom: 1px dotted var(--color1);
|
|
||||||
}
|
|
||||||
transition: background-color var(--fade);
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color3);
|
|
||||||
}
|
|
||||||
|
|
||||||
& > td.left {
|
|
||||||
& > * {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& > td.right {
|
|
||||||
text-align: right;
|
|
||||||
padding-right: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-right {
|
|
||||||
float: right;
|
|
||||||
clear: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-full-width {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-bar {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
& > button {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span.tag {
|
|
||||||
display: inline-block;
|
|
||||||
background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="190" height="16"><path d="M6.59 15.42l-6-6a2 2 0 0 1 0-2.83l6-6A2 2 0 0 1 8 0h180a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H8a2 2 0 0 1-1.41-.58zM4.35 6.59A2 2 0 1 0 7.17 9.4 2 2 0 0 0 4.35 6.6z" fill="%23e8d5b2"></path></svg>');
|
|
||||||
background-size: cover;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
border-radius: 0.2em;
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags {
|
|
||||||
& > .tag {
|
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-wrapper {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
|
||||||
z-index: 100;
|
|
||||||
transition:
|
|
||||||
opacity var(--fade),
|
|
||||||
top var(--fade);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
& > .overlay {
|
|
||||||
max-width: 90vw;
|
|
||||||
max-height: 90vh;
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid var(--color1);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
box-shadow: 0 0 15px #aaa;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay > .put-form {
|
|
||||||
min-width: 700px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete {
|
|
||||||
box-shadow: 0 0 10px #ccc;
|
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
|
||||||
border: 1px solid var(--color1);
|
|
||||||
border-radius: 4px;
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
transition: opacity var(--fade);
|
|
||||||
& > .suggestion {
|
|
||||||
padding: 5px;
|
|
||||||
|
|
||||||
&.selected,
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span.long-text {
|
|
||||||
display: inline-block;
|
|
||||||
overflow-x: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 400px;
|
|
||||||
/*
|
|
||||||
"overflow: hidden" changes the baseline as explained here:
|
|
||||||
https://stackoverflow.com/questions/9273016/why-is-this-inline-block-element-pushed-downward
|
|
||||||
*/
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.long-text-overflowed {
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
text-decoration-style: dotted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea.long-text {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
min-width: 10vw;
|
|
||||||
min-height: 10vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.parameter-value {
|
|
||||||
& > span:empty::before {
|
|
||||||
content: "blank";
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wizard-dialog {
|
|
||||||
max-width: 720px;
|
|
||||||
margin: 10px auto;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid var(--color1);
|
|
||||||
box-shadow: 0 0 10px #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
display: block;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
fill: none;
|
|
||||||
stroke: currentColor;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
&.icon-unsorted {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-overlay {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
&::after {
|
|
||||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTEwIDUwYTQwIDQwIDAgMDA4MCAwIDQwIDQyIDAgMDEtODAgMCIgZmlsbD0iI2IxMmQ1YyIgLz48L3N2Zz4=);
|
|
||||||
content: "";
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 100% 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
background-color: #eee;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 3px;
|
|
||||||
& > .progress-bar {
|
|
||||||
height: 100%;
|
|
||||||
background-color: #b12d5c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* To keep consistent style after changing from Quirks to Standards mode */
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
Vendored
-450
@@ -1,450 +0,0 @@
|
|||||||
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
|
|
||||||
|
|
||||||
/* Document
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Correct the line height in all browsers.
|
|
||||||
* 2. Prevent adjustments of font size after orientation changes in
|
|
||||||
* IE on Windows Phone and in iOS.
|
|
||||||
*/
|
|
||||||
|
|
||||||
html {
|
|
||||||
line-height: 1.15; /* 1 */
|
|
||||||
-ms-text-size-adjust: 100%; /* 2 */
|
|
||||||
-webkit-text-size-adjust: 100%; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sections
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the margin in all browsers (opinionated).
|
|
||||||
*/
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the correct display in IE 9-.
|
|
||||||
*/
|
|
||||||
|
|
||||||
article,
|
|
||||||
aside,
|
|
||||||
footer,
|
|
||||||
header,
|
|
||||||
nav,
|
|
||||||
section {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Correct the font size and margin on `h1` elements within `section` and
|
|
||||||
* `article` contexts in Chrome, Firefox, and Safari.
|
|
||||||
*/
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2em;
|
|
||||||
margin: 0.67em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Grouping content
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the correct display in IE 9-.
|
|
||||||
* 1. Add the correct display in IE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
figcaption,
|
|
||||||
figure,
|
|
||||||
main {
|
|
||||||
/* 1 */
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the correct margin in IE 8.
|
|
||||||
*/
|
|
||||||
|
|
||||||
figure {
|
|
||||||
margin: 1em 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Add the correct box sizing in Firefox.
|
|
||||||
* 2. Show the overflow in Edge and IE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
hr {
|
|
||||||
box-sizing: content-box; /* 1 */
|
|
||||||
height: 0; /* 1 */
|
|
||||||
overflow: visible; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
|
||||||
* 2. Correct the odd `em` font sizing in all browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
pre {
|
|
||||||
font-family: monospace, monospace; /* 1 */
|
|
||||||
font-size: 1em; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Text-level semantics
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Remove the gray background on active links in IE 10.
|
|
||||||
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
|
|
||||||
*/
|
|
||||||
|
|
||||||
a {
|
|
||||||
background-color: transparent; /* 1 */
|
|
||||||
-webkit-text-decoration-skip: objects; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
|
|
||||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
|
||||||
*/
|
|
||||||
|
|
||||||
abbr[title] {
|
|
||||||
border-bottom: none; /* 1 */
|
|
||||||
text-decoration: underline; /* 2 */
|
|
||||||
text-decoration: underline dotted; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
|
|
||||||
*/
|
|
||||||
|
|
||||||
b,
|
|
||||||
strong {
|
|
||||||
font-weight: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
|
||||||
*/
|
|
||||||
|
|
||||||
b,
|
|
||||||
strong {
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
|
||||||
* 2. Correct the odd `em` font sizing in all browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
code,
|
|
||||||
kbd,
|
|
||||||
samp {
|
|
||||||
font-family: monospace, monospace; /* 1 */
|
|
||||||
font-size: 1em; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the correct font style in Android 4.3-.
|
|
||||||
*/
|
|
||||||
|
|
||||||
dfn {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the correct background and color in IE 9-.
|
|
||||||
*/
|
|
||||||
|
|
||||||
mark {
|
|
||||||
background-color: #ff0;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the correct font size in all browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
small {
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
|
||||||
* all browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
sub,
|
|
||||||
sup {
|
|
||||||
font-size: 75%;
|
|
||||||
line-height: 0;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub {
|
|
||||||
bottom: -0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
sup {
|
|
||||||
top: -0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Embedded content
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the correct display in IE 9-.
|
|
||||||
*/
|
|
||||||
|
|
||||||
audio,
|
|
||||||
video {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the correct display in iOS 4-7.
|
|
||||||
*/
|
|
||||||
|
|
||||||
audio:not([controls]) {
|
|
||||||
display: none;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the border on images inside links in IE 10-.
|
|
||||||
*/
|
|
||||||
|
|
||||||
img {
|
|
||||||
border-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide the overflow in IE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
svg:not(:root) {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forms
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Change the font styles in all browsers (opinionated).
|
|
||||||
* 2. Remove the margin in Firefox and Safari.
|
|
||||||
*/
|
|
||||||
|
|
||||||
button,
|
|
||||||
input,
|
|
||||||
optgroup,
|
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
font-family: sans-serif; /* 1 */
|
|
||||||
font-size: 100%; /* 1 */
|
|
||||||
line-height: 1.15; /* 1 */
|
|
||||||
margin: 0; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the overflow in IE.
|
|
||||||
* 1. Show the overflow in Edge.
|
|
||||||
*/
|
|
||||||
|
|
||||||
button,
|
|
||||||
input {
|
|
||||||
/* 1 */
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
|
||||||
* 1. Remove the inheritance of text transform in Firefox.
|
|
||||||
*/
|
|
||||||
|
|
||||||
button,
|
|
||||||
select {
|
|
||||||
/* 1 */
|
|
||||||
text-transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
|
|
||||||
* controls in Android 4.
|
|
||||||
* 2. Correct the inability to style clickable types in iOS and Safari.
|
|
||||||
*/
|
|
||||||
|
|
||||||
button,
|
|
||||||
html [type="button"],
|
|
||||||
/* 1 */ [type="reset"],
|
|
||||||
[type="submit"] {
|
|
||||||
-webkit-appearance: button; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the inner border and padding in Firefox.
|
|
||||||
*/
|
|
||||||
|
|
||||||
button::-moz-focus-inner,
|
|
||||||
[type="button"]::-moz-focus-inner,
|
|
||||||
[type="reset"]::-moz-focus-inner,
|
|
||||||
[type="submit"]::-moz-focus-inner {
|
|
||||||
border-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore the focus styles unset by the previous rule.
|
|
||||||
*/
|
|
||||||
|
|
||||||
button:-moz-focusring,
|
|
||||||
[type="button"]:-moz-focusring,
|
|
||||||
[type="reset"]:-moz-focusring,
|
|
||||||
[type="submit"]:-moz-focusring {
|
|
||||||
outline: 1px dotted ButtonText;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Correct the padding in Firefox.
|
|
||||||
*/
|
|
||||||
|
|
||||||
fieldset {
|
|
||||||
padding: 0.35em 0.75em 0.625em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Correct the text wrapping in Edge and IE.
|
|
||||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
|
||||||
* 3. Remove the padding so developers are not caught out when they zero out
|
|
||||||
* `fieldset` elements in all browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
legend {
|
|
||||||
box-sizing: border-box; /* 1 */
|
|
||||||
color: inherit; /* 2 */
|
|
||||||
display: table; /* 1 */
|
|
||||||
max-width: 100%; /* 1 */
|
|
||||||
padding: 0; /* 3 */
|
|
||||||
white-space: normal; /* 1 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Add the correct display in IE 9-.
|
|
||||||
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
|
||||||
*/
|
|
||||||
|
|
||||||
progress {
|
|
||||||
display: inline-block; /* 1 */
|
|
||||||
vertical-align: baseline; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the default vertical scrollbar in IE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Add the correct box sizing in IE 10-.
|
|
||||||
* 2. Remove the padding in IE 10-.
|
|
||||||
*/
|
|
||||||
|
|
||||||
[type="checkbox"],
|
|
||||||
[type="radio"] {
|
|
||||||
box-sizing: border-box; /* 1 */
|
|
||||||
padding: 0; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
|
||||||
*/
|
|
||||||
|
|
||||||
[type="number"]::-webkit-inner-spin-button,
|
|
||||||
[type="number"]::-webkit-outer-spin-button {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Correct the odd appearance in Chrome and Safari.
|
|
||||||
* 2. Correct the outline style in Safari.
|
|
||||||
*/
|
|
||||||
|
|
||||||
[type="search"] {
|
|
||||||
-webkit-appearance: textfield; /* 1 */
|
|
||||||
outline-offset: -2px; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
|
|
||||||
*/
|
|
||||||
|
|
||||||
[type="search"]::-webkit-search-cancel-button,
|
|
||||||
[type="search"]::-webkit-search-decoration {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
|
||||||
* 2. Change font properties to `inherit` in Safari.
|
|
||||||
*/
|
|
||||||
|
|
||||||
::-webkit-file-upload-button {
|
|
||||||
-webkit-appearance: button; /* 1 */
|
|
||||||
font: inherit; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Interactive
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Add the correct display in IE 9-.
|
|
||||||
* 1. Add the correct display in Edge, IE, and Firefox.
|
|
||||||
*/
|
|
||||||
|
|
||||||
details,
|
|
||||||
/* 1 */ menu {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Add the correct display in all browsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
summary {
|
|
||||||
display: list-item;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scripting
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the correct display in IE 9-.
|
|
||||||
*/
|
|
||||||
|
|
||||||
canvas {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the correct display in IE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
template {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hidden
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the correct display in IE 10-.
|
|
||||||
*/
|
|
||||||
|
|
||||||
[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
+7
-3
@@ -25,8 +25,12 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
const dev = store.fetch("devices", vnode.attrs["deviceFilter"]);
|
const dev = store.fetch("devices", vnode.attrs["deviceFilter"]);
|
||||||
if (!dev.value.length) {
|
if (!dev.value.length) {
|
||||||
if (!dev.fulfilling)
|
if (!dev.fulfilling) {
|
||||||
return m("p.error", `No such device ${vnode.attrs["deviceId"]}`);
|
return m(
|
||||||
|
"p.text-sm font-bold text-red-500",
|
||||||
|
`No such device ${vnode.attrs["deviceId"]}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
return m(
|
return m(
|
||||||
"loading",
|
"loading",
|
||||||
{ queries: [dev] },
|
{ queries: [dev] },
|
||||||
@@ -47,7 +51,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [m("h1", vnode.attrs["deviceId"]), cmps];
|
return m("div.device-page", m("h1", vnode.attrs["deviceId"]), cmps);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
+8
-8
@@ -69,7 +69,7 @@ function renderActions(selected: Set<string>): Children {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Reboot selected devices",
|
title: "Reboot selected devices",
|
||||||
disabled: !selected.size,
|
disabled: !selected.size,
|
||||||
@@ -87,7 +87,7 @@ function renderActions(selected: Set<string>): Children {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.critical",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Factory reset selected devices",
|
title: "Factory reset selected devices",
|
||||||
disabled: !selected.size,
|
disabled: !selected.size,
|
||||||
@@ -105,7 +105,7 @@ function renderActions(selected: Set<string>): Children {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.critical",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Push a firmware or a config file",
|
title: "Push a firmware or a config file",
|
||||||
disabled: !selected.size,
|
disabled: !selected.size,
|
||||||
@@ -122,7 +122,7 @@ function renderActions(selected: Set<string>): Children {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Delete selected devices",
|
title: "Delete selected devices",
|
||||||
disabled: !selected.size,
|
disabled: !selected.size,
|
||||||
@@ -153,7 +153,7 @@ function renderActions(selected: Set<string>): Children {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Tag selected devices",
|
title: "Tag selected devices",
|
||||||
disabled: !selected.size,
|
disabled: !selected.size,
|
||||||
@@ -185,7 +185,7 @@ function renderActions(selected: Set<string>): Children {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Untag selected devices",
|
title: "Untag selected devices",
|
||||||
disabled: !selected.size,
|
disabled: !selected.size,
|
||||||
@@ -299,7 +299,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
attrs["valueCallback"] = valueCallback;
|
attrs["valueCallback"] = valueCallback;
|
||||||
attrs["recordActionsCallback"] = (device): Children => {
|
attrs["recordActionsCallback"] = (device): Children => {
|
||||||
return m(
|
return m(
|
||||||
"a",
|
"a.text-cyan-700 hover:text-cyan-900",
|
||||||
{
|
{
|
||||||
href: `#!/devices/${encodeURIComponent(
|
href: `#!/devices/${encodeURIComponent(
|
||||||
device["DeviceID.ID"].value[0],
|
device["DeviceID.ID"].value[0],
|
||||||
@@ -319,7 +319,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing devices"),
|
m("h1.text-xl font-medium text-stone-900 mb-5", "Listing devices"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m("loading", { queries: [devs, count] }, m(indexTableComponent, attrs)),
|
m("loading", { queries: [devs, count] }, m(indexTableComponent, attrs)),
|
||||||
];
|
];
|
||||||
|
|||||||
+239
-118
@@ -7,7 +7,7 @@ import m, {
|
|||||||
} from "mithril";
|
} from "mithril";
|
||||||
import * as store from "./store.ts";
|
import * as store from "./store.ts";
|
||||||
import * as notifications from "./notifications.ts";
|
import * as notifications from "./notifications.ts";
|
||||||
import { getIcon } from "./icons.ts";
|
import { icon } from "./tailwind-utility-components.ts";
|
||||||
import {
|
import {
|
||||||
clear,
|
clear,
|
||||||
commit,
|
commit,
|
||||||
@@ -21,14 +21,6 @@ import {
|
|||||||
|
|
||||||
const invalid: WeakSet<StageTask> = new WeakSet();
|
const invalid: WeakSet<StageTask> = new WeakSet();
|
||||||
|
|
||||||
function mparam(param): Children {
|
|
||||||
return m("span.parameter", { title: param }, `${param}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mval(val): Children {
|
|
||||||
return m("span.value", { title: val }, `${val}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderStagingSpv(task: StageTask, queueFunc, cancelFunc): Children {
|
function renderStagingSpv(task: StageTask, queueFunc, cancelFunc): Children {
|
||||||
function keydown(e: KeyboardEvent): void {
|
function keydown(e: KeyboardEvent): void {
|
||||||
if (e.key === "Enter") queueFunc();
|
if (e.key === "Enter") queueFunc();
|
||||||
@@ -39,7 +31,7 @@ function renderStagingSpv(task: StageTask, queueFunc, cancelFunc): Children {
|
|||||||
let input;
|
let input;
|
||||||
if (task.parameterValues[0][2] === "xsd:boolean") {
|
if (task.parameterValues[0][2] === "xsd:boolean") {
|
||||||
input = m(
|
input = m(
|
||||||
"select",
|
"select.mt-1 w-full block pl-3 pr-10 py-2 text-base border-stone-300 focus:outline-hidden focus:ring-cyan-500 focus:border-cyan-500 sm:text-sm rounded-md",
|
||||||
{
|
{
|
||||||
value: task.parameterValues[0][1].toString(),
|
value: task.parameterValues[0][1].toString(),
|
||||||
onchange: (e) => {
|
onchange: (e) => {
|
||||||
@@ -61,25 +53,43 @@ function renderStagingSpv(task: StageTask, queueFunc, cancelFunc): Children {
|
|||||||
let value = task.parameterValues[0][1];
|
let value = task.parameterValues[0][1];
|
||||||
if (type === "xsd:dateTime" && typeof value === "number")
|
if (type === "xsd:dateTime" && typeof value === "number")
|
||||||
value = new Date(value).toJSON() || value;
|
value = new Date(value).toJSON() || value;
|
||||||
input = m("input", {
|
input = m(
|
||||||
type: ["xsd:int", "xsd:unsignedInt"].includes(type) ? "number" : "text",
|
"input.mt-1 w-full shadow-xs focus:ring-cyan-500 focus:border-cyan-500 block sm:text-sm border-stone-300 rounded-md",
|
||||||
value: value,
|
{
|
||||||
oninput: (e) => {
|
type: ["xsd:int", "xsd:unsignedInt"].includes(type) ? "number" : "text",
|
||||||
e.redraw = false;
|
value: value,
|
||||||
task.parameterValues[0][1] = input.dom.value;
|
oninput: (e) => {
|
||||||
|
e.redraw = false;
|
||||||
|
task.parameterValues[0][1] = input.dom.value;
|
||||||
|
},
|
||||||
|
onkeydown: keydown,
|
||||||
|
oncreate: (vnode) => {
|
||||||
|
(vnode.dom as HTMLInputElement).focus();
|
||||||
|
(vnode.dom as HTMLInputElement).select();
|
||||||
|
// Need to prevent scrolling on focus because
|
||||||
|
// we're animating height and using overflow: hidden
|
||||||
|
(vnode.dom.parentNode.parentNode as Element).scrollTop = 0;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
onkeydown: keydown,
|
);
|
||||||
oncreate: (vnode) => {
|
|
||||||
(vnode.dom as HTMLInputElement).focus();
|
|
||||||
(vnode.dom as HTMLInputElement).select();
|
|
||||||
// Need to prevent scrolling on focus because
|
|
||||||
// we're animating height and using overflow: hidden
|
|
||||||
(vnode.dom.parentNode.parentNode as Element).scrollTop = 0;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [m("span", "Editing ", mparam(task.parameterValues[0][0])), input];
|
return [
|
||||||
|
m(
|
||||||
|
"span.text-sm text-stone-700 inline-flex max-w-full gap-2",
|
||||||
|
"Editing",
|
||||||
|
m(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
title: task.parameterValues[0][0],
|
||||||
|
dir: "rtl",
|
||||||
|
class: "italic pr-1 min-w-0 truncate",
|
||||||
|
},
|
||||||
|
task.parameterValues[0][0],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
input,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStagingDownload(task: StageTask): Children {
|
function renderStagingDownload(task: StageTask): Children {
|
||||||
@@ -138,11 +148,13 @@ function renderStagingDownload(task: StageTask): Children {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return m("div.flex items-center gap-2 text-sm text-stone-700 max-w-full", [
|
||||||
"Push ",
|
"Push",
|
||||||
m(
|
m(
|
||||||
"select",
|
"select",
|
||||||
{
|
{
|
||||||
|
class:
|
||||||
|
"min-w-0 pl-3 pr-10 py-2 text-base border-stone-300 focus:outline-hidden focus:ring-cyan-500 focus:border-cyan-500 sm:text-sm rounded-md",
|
||||||
onchange: (e) => {
|
onchange: (e) => {
|
||||||
const f = e.target.value;
|
const f = e.target.value;
|
||||||
task.fileName = f;
|
task.fileName = f;
|
||||||
@@ -151,21 +163,22 @@ function renderStagingDownload(task: StageTask): Children {
|
|||||||
if (file._id === f) task.fileType = file["metadata.fileType"];
|
if (file._id === f) task.fileType = file["metadata.fileType"];
|
||||||
},
|
},
|
||||||
disabled: files.fulfilling,
|
disabled: files.fulfilling,
|
||||||
style: "width: 350px",
|
|
||||||
},
|
},
|
||||||
filesList,
|
filesList,
|
||||||
),
|
),
|
||||||
" as ",
|
"as",
|
||||||
m(
|
m(
|
||||||
"select",
|
"select",
|
||||||
{
|
{
|
||||||
|
class:
|
||||||
|
"min-w-0 pl-3 pr-10 py-2 text-base border-stone-300 focus:outline-hidden focus:ring-cyan-500 focus:border-cyan-500 sm:text-sm rounded-md",
|
||||||
onchange: (e) => {
|
onchange: (e) => {
|
||||||
task.fileType = e.target.value;
|
task.fileType = e.target.value;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
typesList,
|
typesList,
|
||||||
),
|
),
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStaging(staging: Set<StageTask>): Child[] {
|
function renderStaging(staging: Set<StageTask>): Child[] {
|
||||||
@@ -190,17 +203,34 @@ function renderStaging(staging: Set<StageTask>): Child[] {
|
|||||||
else if (s.name === "download") elms = renderStagingDownload(s);
|
else if (s.name === "download") elms = renderStagingDownload(s);
|
||||||
|
|
||||||
const queue = m(
|
const queue = m(
|
||||||
"button.primary",
|
"button",
|
||||||
{ title: "Queue task", onclick: queueFunc, disabled: invalid.has(s) },
|
{
|
||||||
|
class:
|
||||||
|
"px-2.5 py-1.5 border border-transparent text-xs font-medium rounded-sm shadow-xs text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
title: "Queue task",
|
||||||
|
onclick: queueFunc,
|
||||||
|
disabled: invalid.has(s),
|
||||||
|
},
|
||||||
"Queue",
|
"Queue",
|
||||||
);
|
);
|
||||||
const cancel = m(
|
const cancel = m(
|
||||||
"button",
|
"button",
|
||||||
{ title: "Cancel edit", onclick: cancelFunc },
|
{
|
||||||
|
class:
|
||||||
|
"px-2.5 py-1.5 border border-stone-300 shadow-xs text-xs font-medium rounded-sm text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
title: "Cancel edit",
|
||||||
|
onclick: cancelFunc,
|
||||||
|
},
|
||||||
"Cancel",
|
"Cancel",
|
||||||
);
|
);
|
||||||
|
|
||||||
elements.push(m(".staging", elms, m("div.actions", queue, cancel)));
|
elements.push(
|
||||||
|
m(
|
||||||
|
"div.p-4",
|
||||||
|
elms,
|
||||||
|
m("div.flex mt-4 justify-center gap-4", cancel, queue),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
@@ -214,9 +244,10 @@ function renderQueue(queue: Set<QueueTask>): Child[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(devices)) {
|
for (const [k, v] of Object.entries(devices)) {
|
||||||
details.push(m("strong", k));
|
details.push(m("h3.font-semibold text-stone-700", k));
|
||||||
for (const t of v) {
|
for (const t of v) {
|
||||||
const actions = [];
|
const actions: ReturnType<typeof m>[] = [];
|
||||||
|
let task: ReturnType<typeof m>;
|
||||||
|
|
||||||
if (t.status === "fault" || t.status === "stale") {
|
if (t.status === "fault" || t.status === "stale") {
|
||||||
actions.push(
|
actions.push(
|
||||||
@@ -228,7 +259,10 @@ function renderQueue(queue: Set<QueueTask>): Child[] {
|
|||||||
queueTask(t);
|
queueTask(t);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("retry"),
|
m(icon, {
|
||||||
|
name: "retry",
|
||||||
|
class: "inline h-4 w-4 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -242,74 +276,116 @@ function renderQueue(queue: Set<QueueTask>): Child[] {
|
|||||||
deleteTask(t);
|
deleteTask(t);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("remove"),
|
m(icon, {
|
||||||
|
name: "remove",
|
||||||
|
class: "inline h-4 w-4 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (t.name === "setParameterValues") {
|
if (t.name === "setParameterValues") {
|
||||||
details.push(
|
task = m(
|
||||||
|
"span.text-stone-900 inline-flex max-w-full gap-2",
|
||||||
|
"Set",
|
||||||
m(
|
m(
|
||||||
`div.${t.status}`,
|
"span",
|
||||||
m(
|
{
|
||||||
"span",
|
title: t.parameterValues[0][0],
|
||||||
"Set ",
|
dir: "rtl",
|
||||||
mparam(t.parameterValues[0][0]),
|
class: "italic pr-1 min-w-0 truncate",
|
||||||
" to '",
|
},
|
||||||
mval(t.parameterValues[0][1]),
|
t.parameterValues[0][0],
|
||||||
"'",
|
),
|
||||||
),
|
"to",
|
||||||
m(".actions", actions),
|
m(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
title: t.parameterValues[0][1],
|
||||||
|
class: "min-w-0 truncate",
|
||||||
|
},
|
||||||
|
t.parameterValues[0][1],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (t.name === "refreshObject") {
|
} else if (t.name === "refreshObject") {
|
||||||
details.push(
|
task = m(
|
||||||
|
"span.text-stone-900 inline-flex max-w-full gap-2",
|
||||||
|
"Refresh",
|
||||||
m(
|
m(
|
||||||
`div.${t.status}`,
|
"span",
|
||||||
m("span", "Refresh ", mparam(t.parameterName)),
|
{
|
||||||
m(".actions", actions),
|
title: t.parameterName,
|
||||||
|
dir: "rtl",
|
||||||
|
class: "italic pr-1 min-w-0 truncate",
|
||||||
|
},
|
||||||
|
t.parameterName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (t.name === "reboot") {
|
} else if (t.name === "reboot") {
|
||||||
details.push(m(`div.${t.status}`, "Reboot", m(".actions", actions)));
|
task = m("span.text-stone-900", "Reboot");
|
||||||
} else if (t.name === "factoryReset") {
|
} else if (t.name === "factoryReset") {
|
||||||
details.push(
|
task = m("span.text-stone-900", "Factory reset");
|
||||||
m(`div.${t.status}`, "Factory reset", m(".actions", actions)),
|
|
||||||
);
|
|
||||||
} else if (t.name === "addObject") {
|
} else if (t.name === "addObject") {
|
||||||
details.push(
|
task = m(
|
||||||
|
"span.text-stone-900 inline-flex max-w-full gap-2",
|
||||||
|
"Add",
|
||||||
m(
|
m(
|
||||||
`div.${t.status}`,
|
"span",
|
||||||
m("span", "Add ", mparam(t.objectName)),
|
{
|
||||||
m(".actions", actions),
|
title: t.objectName,
|
||||||
|
dir: "rtl",
|
||||||
|
class: "italic pr-1 min-w-0 truncate",
|
||||||
|
},
|
||||||
|
t.objectName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (t.name === "deleteObject") {
|
} else if (t.name === "deleteObject") {
|
||||||
details.push(
|
task = m(
|
||||||
|
"span.text-stone-900 inline-flex max-w-full gap-2",
|
||||||
|
"Delete",
|
||||||
m(
|
m(
|
||||||
`div.${t.status}`,
|
"span",
|
||||||
m("span", "Delete ", mparam(t.objectName)),
|
{
|
||||||
m(".actions", actions),
|
title: t.objectName,
|
||||||
|
dir: "rtl",
|
||||||
|
class: "italic pr-1 min-w-0 truncate",
|
||||||
|
},
|
||||||
|
t.objectName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (t.name === "getParameterValues") {
|
} else if (t.name === "getParameterValues") {
|
||||||
details.push(
|
task = m(
|
||||||
m(
|
"span.text-stone-900",
|
||||||
`div.${t.status}`,
|
`Refresh ${t.parameterNames.length} parameters`,
|
||||||
`Refresh ${t.parameterNames.length} parameters`,
|
|
||||||
m(".actions", actions),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else if (t.name === "download") {
|
} else if (t.name === "download") {
|
||||||
details.push(
|
task = m(
|
||||||
m(
|
"span.text-stone-900",
|
||||||
`div.${t.status}`,
|
`Push file: ${t.fileName} (${t.fileType})`,
|
||||||
`Push file: ${t.fileName} (${t.fileType})`,
|
|
||||||
m(".actions", actions),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
details.push(m(`div.${t.status}`, t.name, m(".actions", actions)));
|
task = m("span.text-stone-900", t.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let bgDiv: ReturnType<typeof m>;
|
||||||
|
if (t.status === "pending") {
|
||||||
|
bgDiv = m(
|
||||||
|
"div.block absolute inset-0 bg-emerald-200 rounded-sm animate-pulse",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
} else if (t.status === "fault") {
|
||||||
|
bgDiv = m("div.block absolute inset-0 bg-red-200 rounded-sm", "");
|
||||||
|
} else if (t.status === "stale") {
|
||||||
|
bgDiv = m("div.block absolute inset-0 bg-stone-200 rounded-sm", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
details.push(
|
||||||
|
m(
|
||||||
|
"div.flex justify-between w-full rounded-sm items-center relative",
|
||||||
|
bgDiv,
|
||||||
|
m("div.overflow-hidden relative", task),
|
||||||
|
m("div.flex whitespace-nowrap gap-2 ml-2 relative", actions),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,20 +396,47 @@ function renderNotifications(notifs): Child[] {
|
|||||||
const notificationElements: Child[] = [];
|
const notificationElements: Child[] = [];
|
||||||
|
|
||||||
for (const n of notifs) {
|
for (const n of notifs) {
|
||||||
|
let notifColors = "",
|
||||||
|
buttonColors = "";
|
||||||
|
if (n.type === "success") {
|
||||||
|
notifColors = "bg-emerald-50 text-emerald-800 border-emerald-100";
|
||||||
|
buttonColors =
|
||||||
|
"hover:bg-emerald-100 text-emerald-800 focus:ring-offset-emerald-50 focus:ring-emerald-600";
|
||||||
|
} else if (n.type === "error") {
|
||||||
|
notifColors = "bg-red-50 text-red-800 border-red-100";
|
||||||
|
buttonColors =
|
||||||
|
"hover:bg-red-100 text-red-800 focus:ring-offset-red-50 focus:ring-red-600";
|
||||||
|
} else if (n.type === "warning") {
|
||||||
|
notifColors = "bg-yellow-50 text-yellow-800 border-yellow-100";
|
||||||
|
buttonColors =
|
||||||
|
"hover:bg-yellow-100 text-yellow-800 focus:ring-offset-yellow-50 focus:ring-yellow-600";
|
||||||
|
}
|
||||||
|
|
||||||
let buttons;
|
let buttons;
|
||||||
if (n.actions) {
|
if (n.actions) {
|
||||||
const btns = Object.entries(n.actions).map(([label, onclick]) =>
|
const btns = Object.entries(n.actions).map(([label, onclick]) =>
|
||||||
m("button.primary", { onclick: onclick }, label),
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class:
|
||||||
|
"ml-2 px-2 py-1.5 -my-1.5 rounded-md text-sm font-medium focus:outline-hidden focus:ring-2 focus:ring-offset-2 " +
|
||||||
|
buttonColors,
|
||||||
|
onclick: onclick,
|
||||||
|
},
|
||||||
|
label,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (btns.length) buttons = m("div", { style: "float: right" }, btns);
|
if (btns.length) buttons = m("div", btns);
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationElements.push(
|
notificationElements.push(
|
||||||
m(
|
m(
|
||||||
"div.notification",
|
"div",
|
||||||
{
|
{
|
||||||
class: n.type,
|
class:
|
||||||
style: "position: absolute;opacity: 0",
|
"absolute flex justify-between rounded-md w-full text-sm shadow-md p-4 border transition-[top,opacity] " +
|
||||||
|
notifColors,
|
||||||
|
style: "opacity: 0",
|
||||||
oncreate: (vnode) => {
|
oncreate: (vnode) => {
|
||||||
(vnode.dom as HTMLDivElement).style.opacity = "1";
|
(vnode.dom as HTMLDivElement).style.opacity = "1";
|
||||||
},
|
},
|
||||||
@@ -347,7 +450,8 @@ function renderNotifications(notifs): Child[] {
|
|||||||
},
|
},
|
||||||
key: n.timestamp,
|
key: n.timestamp,
|
||||||
},
|
},
|
||||||
m("div", buttons, n.message),
|
n.message,
|
||||||
|
buttons,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -367,10 +471,10 @@ const component: ClosureComponent = (): Component => {
|
|||||||
const queueElements = renderQueue(queue);
|
const queueElements = renderQueue(queue);
|
||||||
|
|
||||||
function repositionNotifications(): void {
|
function repositionNotifications(): void {
|
||||||
let top = 10;
|
let top = 16;
|
||||||
for (const c of notificationElements as VnodeDOM[]) {
|
for (const c of notificationElements as VnodeDOM[]) {
|
||||||
(c.dom as HTMLDivElement).style.top = `${top}px`;
|
(c.dom as HTMLDivElement).style.top = `${top}px`;
|
||||||
top += (c.dom as HTMLDivElement).offsetHeight + 10;
|
top += (c.dom as HTMLDivElement).offsetHeight + 16;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,10 +501,23 @@ const component: ClosureComponent = (): Component => {
|
|||||||
for (const t of queue) statusCount[t["status"]] += 1;
|
for (const t of queue) statusCount[t["status"]] += 1;
|
||||||
|
|
||||||
const actions = m(
|
const actions = m(
|
||||||
".actions",
|
"div.flex ml-auto gap-2",
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button",
|
||||||
{
|
{
|
||||||
|
class:
|
||||||
|
"px-2.5 py-1.5 -my-1.5 border border-stone-300 shadow-xs text-xs font-medium rounded-sm text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
|
title: "Clear tasks",
|
||||||
|
onclick: clear,
|
||||||
|
disabled: !queueElements.length,
|
||||||
|
},
|
||||||
|
"Clear",
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class:
|
||||||
|
"px-2.5 py-1.5 -my-1.5 border border-transparent text-xs font-medium rounded-sm shadow-xs text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
title: "Commit queued tasks",
|
title: "Commit queued tasks",
|
||||||
disabled: !statusCount.queued,
|
disabled: !statusCount.queued,
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
@@ -458,45 +575,47 @@ const component: ClosureComponent = (): Component => {
|
|||||||
},
|
},
|
||||||
"Commit",
|
"Commit",
|
||||||
),
|
),
|
||||||
m(
|
|
||||||
"button",
|
|
||||||
{
|
|
||||||
title: "Clear tasks",
|
|
||||||
onclick: clear,
|
|
||||||
disabled: !queueElements.length,
|
|
||||||
},
|
|
||||||
"Clear",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
statusElement = m(
|
statusElement = m(
|
||||||
".status",
|
"div.flex p-4 gap-5 items-center text-sm",
|
||||||
m(
|
m(
|
||||||
"span.queued",
|
"span.text-stone-700 -mx-1 px-1",
|
||||||
{ class: statusCount.queued ? "active" : "" },
|
{ class: statusCount.queued ? "font-semibold" : "" },
|
||||||
`Queued: ${statusCount.queued}`,
|
`Queued: ${statusCount.queued}`,
|
||||||
),
|
),
|
||||||
m(
|
m(
|
||||||
"span.pending",
|
"span.text-stone-700 relative",
|
||||||
{ class: statusCount.pending ? "active" : "" },
|
statusCount.pending
|
||||||
`Pending: ${statusCount.pending}`,
|
? m(
|
||||||
|
"div.block absolute -inset-x-1 inset-y-0 rounded-sm bg-emerald-200 animate-pulse",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
m("span.relative", `Pending: ${statusCount.pending}`),
|
||||||
),
|
),
|
||||||
m(
|
m(
|
||||||
"span.fault",
|
"span.text-stone-700 relative",
|
||||||
{ class: statusCount.fault ? "active" : "" },
|
m("span.relative", `Fault: ${statusCount.fault}`),
|
||||||
`Fault: ${statusCount.fault}`,
|
|
||||||
),
|
),
|
||||||
m(
|
m(
|
||||||
"span.stale",
|
"span.text-stone-700 relative",
|
||||||
{ class: statusCount.stale ? "active" : "" },
|
statusCount.stale
|
||||||
`Stale: ${statusCount.stale}`,
|
? m(
|
||||||
|
"div.block absolute -inset-x-1 inset-y-0 rounded-sm bg-stone-200",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
m("span.relative", `Stale: ${statusCount.stale}`),
|
||||||
),
|
),
|
||||||
actions,
|
actions,
|
||||||
);
|
);
|
||||||
|
|
||||||
drawerElement = m(
|
drawerElement = m(
|
||||||
".drawer",
|
"div",
|
||||||
{
|
{
|
||||||
|
class:
|
||||||
|
"w-[48rem] mx-auto pointer-events-auto bg-white rounded-b-lg border-stone-300 border-x border-b shadow-md overflow-hidden transition-[height] -mt-px",
|
||||||
key: "drawer",
|
key: "drawer",
|
||||||
style: "opacity: 0;height: 0;",
|
style: "opacity: 0;height: 0;",
|
||||||
oncreate: (vnode2) => {
|
oncreate: (vnode2) => {
|
||||||
@@ -504,20 +623,20 @@ const component: ClosureComponent = (): Component => {
|
|||||||
(vnode2.dom as HTMLDivElement).style.opacity = "1";
|
(vnode2.dom as HTMLDivElement).style.opacity = "1";
|
||||||
resizeDrawer();
|
resizeDrawer();
|
||||||
},
|
},
|
||||||
onmouseover: (e) => {
|
onmouseenter: (e) => {
|
||||||
|
if (drawerElement.dom.style.opacity === "0") return;
|
||||||
vnode.state["mouseIn"] = true;
|
vnode.state["mouseIn"] = true;
|
||||||
resizeDrawer();
|
resizeDrawer();
|
||||||
e.redraw = false;
|
e.redraw = false;
|
||||||
},
|
},
|
||||||
onmouseleave: (e) => {
|
onmouseleave: (e) => {
|
||||||
|
if (drawerElement.dom.style.opacity === "0") return;
|
||||||
vnode.state["mouseIn"] = false;
|
vnode.state["mouseIn"] = false;
|
||||||
resizeDrawer();
|
resizeDrawer();
|
||||||
e.redraw = false;
|
e.redraw = false;
|
||||||
},
|
},
|
||||||
onupdate: resizeDrawer,
|
onupdate: resizeDrawer,
|
||||||
onbeforeremove: (vnode2) => {
|
onbeforeremove: (vnode2) => {
|
||||||
(vnode2.dom as HTMLDivElement).onmouseover = null;
|
|
||||||
(vnode2.dom as HTMLDivElement).onmouseleave = null;
|
|
||||||
(vnode2.dom as HTMLDivElement).style.opacity = "0";
|
(vnode2.dom as HTMLDivElement).style.opacity = "0";
|
||||||
(vnode2.dom as HTMLDivElement).style.height = "0";
|
(vnode2.dom as HTMLDivElement).style.height = "0";
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -526,18 +645,20 @@ const component: ClosureComponent = (): Component => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
statusElement,
|
statusElement,
|
||||||
stagingElements.length ? stagingElements : m(".queue", queueElements),
|
stagingElements.length
|
||||||
|
? stagingElements
|
||||||
|
: m("div.px-4 pb-4 text-sm", queueElements),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"div.drawer-wrapper",
|
"div.fixed pointer-events-none inset-0 z-30",
|
||||||
drawerElement,
|
drawerElement,
|
||||||
m(
|
m(
|
||||||
"div.notifications-wrapper",
|
"div",
|
||||||
{
|
{
|
||||||
|
class: "relative w-[48rem] mx-auto pointer-events-auto",
|
||||||
key: "notifications",
|
key: "notifications",
|
||||||
style: "position: relative;",
|
|
||||||
onupdate: repositionNotifications,
|
onupdate: repositionNotifications,
|
||||||
oncreate: repositionNotifications,
|
oncreate: repositionNotifications,
|
||||||
},
|
},
|
||||||
|
|||||||
+7
-12
@@ -1,7 +1,7 @@
|
|||||||
import * as notifications from "./notifications.ts";
|
import * as notifications from "./notifications.ts";
|
||||||
|
|
||||||
export let codeMirror: typeof import("codemirror");
|
export let codeMirror: typeof import("./codemirror-loader");
|
||||||
export let yaml: typeof import("yaml");
|
export let yaml: typeof import("./yaml-loader");
|
||||||
|
|
||||||
let note;
|
let note;
|
||||||
|
|
||||||
@@ -23,14 +23,9 @@ export function loadCodeMirror(): Promise<void> {
|
|||||||
if (codeMirror) return Promise.resolve();
|
if (codeMirror) return Promise.resolve();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const promises = [
|
import("./codemirror-loader")
|
||||||
import("codemirror"),
|
.then((module) => {
|
||||||
import("codemirror/mode/javascript/javascript"),
|
codeMirror = module;
|
||||||
import("codemirror/mode/yaml/yaml"),
|
|
||||||
];
|
|
||||||
Promise.all(promises)
|
|
||||||
.then((modules) => {
|
|
||||||
codeMirror = modules[0].default;
|
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -44,9 +39,9 @@ export function loadYaml(): Promise<void> {
|
|||||||
if (yaml) return Promise.resolve();
|
if (yaml) return Promise.resolve();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
import("yaml")
|
import("./yaml-loader")
|
||||||
.then((module) => {
|
.then((module) => {
|
||||||
yaml = module.default;
|
yaml = module;
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return {
|
return {
|
||||||
view: function (vnode) {
|
view: function (vnode) {
|
||||||
document.title = "Error! - GenieACS";
|
document.title = "Error! - GenieACS";
|
||||||
return m("p.error", vnode.attrs["error"]);
|
return m("p.text-sm font-bold text-red-500", vnode.attrs["error"]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
+14
-6
@@ -127,14 +127,22 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
fault["device"],
|
fault["device"],
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
return m("a", { href: deviceHref }, fault["device"]);
|
return m(
|
||||||
|
"a.text-cyan-700 hover:text-cyan-900 font-medium",
|
||||||
|
{ href: deviceHref },
|
||||||
|
fault["device"],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attr.id === "message")
|
if (attr.id === "message")
|
||||||
return m("long-text", { text: fault["message"] });
|
return m("long-text", { text: fault["message"], class: "max-w-xs" });
|
||||||
|
|
||||||
if (attr.id === "detail")
|
if (attr.id === "detail") {
|
||||||
return m("long-text", { text: yamlStringify(fault["detail"]) });
|
return m("long-text", {
|
||||||
|
text: yamlStringify(fault["detail"]),
|
||||||
|
class: "max-w-xs",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (attr.id === "timestamp")
|
if (attr.id === "timestamp")
|
||||||
return new Date(fault["timestamp"]).toLocaleString();
|
return new Date(fault["timestamp"]).toLocaleString();
|
||||||
@@ -155,7 +163,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
if (window.authorizer.hasAccess("faults", 3)) {
|
if (window.authorizer.hasAccess("faults", 3)) {
|
||||||
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
||||||
return m(
|
return m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
disabled: selected.size === 0,
|
disabled: selected.size === 0,
|
||||||
title: "Delete selected faults",
|
title: "Delete selected faults",
|
||||||
@@ -190,7 +198,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing faults"),
|
m("h1.text-xl font-medium text-stone-900 mb-5", "Listing faults"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(
|
||||||
"loading",
|
"loading",
|
||||||
|
|||||||
+12
-6
@@ -120,7 +120,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter): void {
|
||||||
const ops = { filter };
|
const ops = { filter };
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
m.route.set("/admin/files", ops);
|
m.route.set("/files", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = vnode.attrs["sort"]
|
const sort = vnode.attrs["sort"]
|
||||||
@@ -137,7 +137,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||||
const ops = { sort: JSON.stringify(_sort) };
|
const ops = { sort: JSON.stringify(_sort) };
|
||||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||||
m.route.set("/admin/files", ops);
|
m.route.set("/files", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
let filter = vnode.attrs["filter"]
|
||||||
@@ -163,14 +163,20 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
attrs["onSortChange"] = onSortChange;
|
attrs["onSortChange"] = onSortChange;
|
||||||
attrs["downloadUrl"] = downloadUrl;
|
attrs["downloadUrl"] = downloadUrl;
|
||||||
attrs["recordActionsCallback"] = (file) => {
|
attrs["recordActionsCallback"] = (file) => {
|
||||||
return [m("a", { href: "api/blob/files/" + file["_id"] }, "Download")];
|
return [
|
||||||
|
m(
|
||||||
|
"a.text-cyan-700 hover:text-cyan-900",
|
||||||
|
{ href: "api/blob/files/" + file["_id"] },
|
||||||
|
"Download",
|
||||||
|
),
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("files", 3)) {
|
if (window.authorizer.hasAccess("files", 3)) {
|
||||||
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
||||||
return [
|
return [
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Create new file",
|
title: "Create new file",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
@@ -257,7 +263,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"New",
|
"New",
|
||||||
),
|
),
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Delete selected files",
|
title: "Delete selected files",
|
||||||
disabled: !selected.size,
|
disabled: !selected.size,
|
||||||
@@ -300,7 +306,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing files"),
|
m("h1.text-xl font-medium text-stone-900 mb-5", "Listing files"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(
|
||||||
"loading",
|
"loading",
|
||||||
|
|||||||
+34
-25
@@ -8,7 +8,7 @@ import { Expression } from "../lib/types.ts";
|
|||||||
|
|
||||||
const getAutocomplete = memoize((resource) => {
|
const getAutocomplete = memoize((resource) => {
|
||||||
const labels = smartQuery.getLabels(resource);
|
const labels = smartQuery.getLabels(resource);
|
||||||
const autocomplete = new Autocomplete("autocomplete", (txt, cb) => {
|
const autocomplete = new Autocomplete((txt, cb) => {
|
||||||
txt = txt.toLowerCase();
|
txt = txt.toLowerCase();
|
||||||
cb(
|
cb(
|
||||||
labels
|
labels
|
||||||
@@ -111,32 +111,41 @@ const component: ClosureComponent<Attrs> = (initialVnode) => {
|
|||||||
|
|
||||||
attrs = vnode.attrs;
|
attrs = vnode.attrs;
|
||||||
|
|
||||||
return m("div.filter", [
|
return m("div.mb-5", [
|
||||||
m("b", "Filter"),
|
m("label.text-sm font-semibold text-stone-700", "Filter"),
|
||||||
...filterList.map((fltr, idx) => {
|
m(
|
||||||
return m("input", {
|
"div.shadow-sm rounded-md mt-1 max-w-screen-sm -space-y-px",
|
||||||
type: "text",
|
...filterList.map((fltr, idx) => {
|
||||||
class: `${(filterInvalid >> idx) & 1 ? "error" : ""}`,
|
let classNames =
|
||||||
value: fltr,
|
"appearance-none rounded-none relative block w-full px-3 py-2 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm";
|
||||||
oninput: (e) => {
|
if (idx === 0) classNames += " rounded-t-md";
|
||||||
e.redraw = false;
|
if (idx === filterList.length - 1) classNames += " rounded-b-md";
|
||||||
filterList[idx] = e.target.value;
|
if (filterInvalid & (1 << idx)) classNames += " text-red-700";
|
||||||
filterTouched = true;
|
|
||||||
},
|
|
||||||
oncreate: (vn) => {
|
|
||||||
const el = vn.dom as HTMLInputElement;
|
|
||||||
getAutocomplete(vnode.attrs.resource).attach(el);
|
|
||||||
|
|
||||||
el.addEventListener("blur", () => {
|
return m(`input`, {
|
||||||
if (filterTouched) onChange();
|
type: "text",
|
||||||
});
|
class: classNames,
|
||||||
|
value: fltr,
|
||||||
|
oninput: (e) => {
|
||||||
|
e.redraw = false;
|
||||||
|
filterList[idx] = e.target.value;
|
||||||
|
filterTouched = true;
|
||||||
|
},
|
||||||
|
oncreate: (vn) => {
|
||||||
|
const el = vn.dom as HTMLInputElement;
|
||||||
|
getAutocomplete(vnode.attrs.resource).attach(el);
|
||||||
|
|
||||||
el.addEventListener("keydown", (e) => {
|
el.addEventListener("blur", () => {
|
||||||
if (e.key === "Enter" && filterTouched) onChange();
|
if (filterTouched) onChange();
|
||||||
});
|
});
|
||||||
},
|
|
||||||
});
|
el.addEventListener("keydown", (e) => {
|
||||||
}),
|
if (e.key === "Enter" && filterTouched) onChange();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
import m, { Children } from "mithril";
|
|
||||||
import { ICONS_SVG } from "../build/assets.ts";
|
|
||||||
|
|
||||||
export function getIcon(name: string): Children {
|
|
||||||
return m(
|
|
||||||
`svg.icon.icon-${name}`,
|
|
||||||
{ key: `icon-${name}` },
|
|
||||||
m("use", { href: `${ICONS_SVG}#icon-${name}` }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 18L18 6M6 6l12 12"/></svg>
|
||||||
|
After Width: | Height: | Size: 219 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
|
||||||
|
After Width: | Height: | Size: 257 B |
+179
-56
@@ -1,6 +1,6 @@
|
|||||||
import { ClosureComponent, Component, Children } from "mithril";
|
import { ClosureComponent, Component, Children } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import { getIcon } from "./icons.ts";
|
import { icon } from "./tailwind-utility-components.ts";
|
||||||
import debounce from "../lib/common/debounce.ts";
|
import debounce from "../lib/common/debounce.ts";
|
||||||
|
|
||||||
interface Attribute {
|
interface Attribute {
|
||||||
@@ -11,6 +11,22 @@ interface Attribute {
|
|||||||
|
|
||||||
const MAX_PAGE_SIZE = 200;
|
const MAX_PAGE_SIZE = 200;
|
||||||
|
|
||||||
|
function getExcerpt(text: string, maxLength = 80, maxLines = 10): string[] {
|
||||||
|
let lines: string[] = text?.split("\n", maxLines + 1) ?? [""];
|
||||||
|
|
||||||
|
if (lines.length > maxLines) {
|
||||||
|
lines.pop();
|
||||||
|
lines[maxLines - 1] = "\ufe19";
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = lines.map((l) => {
|
||||||
|
if (l.length <= maxLength) return l;
|
||||||
|
return l.slice(0, maxLength - 1) + "\u2026";
|
||||||
|
});
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
function renderTable(
|
function renderTable(
|
||||||
attributes: Attribute[],
|
attributes: Attribute[],
|
||||||
data: Record<string, any>[],
|
data: Record<string, any>[],
|
||||||
@@ -40,32 +56,66 @@ function renderTable(
|
|||||||
// Table header
|
// Table header
|
||||||
const labels = [];
|
const labels = [];
|
||||||
if (buttons.length) {
|
if (buttons.length) {
|
||||||
const selectAll = m("input", {
|
const selectAll = m(
|
||||||
type: "checkbox",
|
"input.focus:ring-cyan-500 h-4 w-4 text-cyan-700 border-stone-300 rounded-sm",
|
||||||
checked: records.length && selected.size === records.length,
|
{
|
||||||
onchange: (e) => {
|
type: "checkbox",
|
||||||
for (const record of records) {
|
checked: records.length && selected.size === records.length,
|
||||||
const id = record["_id"] || record["DeviceID.ID"].value[0];
|
onchange: (e) => {
|
||||||
if (e.target.checked) selected.add(id);
|
for (const record of records) {
|
||||||
else selected.delete(id);
|
const id = record["_id"] || record["DeviceID.ID"].value[0];
|
||||||
}
|
if (e.target.checked) selected.add(id);
|
||||||
|
else selected.delete(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disabled: !total,
|
||||||
},
|
},
|
||||||
disabled: !total,
|
);
|
||||||
});
|
labels.push(
|
||||||
labels.push(m("th", selectAll));
|
m(
|
||||||
|
"th",
|
||||||
|
{ class: "px-6 py-3.5 w-0", scope: "col" },
|
||||||
|
m("span.sr-only", "Select"),
|
||||||
|
selectAll,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < attributes.length; i++) {
|
for (const [i, attr] of attributes.entries()) {
|
||||||
const attr = attributes[i];
|
let padding: string;
|
||||||
|
if (i === 0) padding = buttons.length ? "pr-3" : "pl-6 pr-3";
|
||||||
|
else if (i === attributes.length - +!recordActionsCallback)
|
||||||
|
padding = "pl-3 pr-6";
|
||||||
|
else padding = "px-3";
|
||||||
|
|
||||||
const label = attr.label;
|
const label = attr.label;
|
||||||
if (!sortAttributes.hasOwnProperty(i)) {
|
if (!sortAttributes.hasOwnProperty(i)) {
|
||||||
labels.push(m("th", label));
|
labels.push(
|
||||||
|
m(
|
||||||
|
"th",
|
||||||
|
{
|
||||||
|
class:
|
||||||
|
"py-3.5 text-left text-sm font-semibold text-stone-500 " +
|
||||||
|
padding,
|
||||||
|
scope: "col",
|
||||||
|
},
|
||||||
|
label,
|
||||||
|
),
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let symbol = getIcon("unsorted");
|
let symbol: Children;
|
||||||
if (sortAttributes[i] > 0) symbol = getIcon("sorted-asc");
|
if (sortAttributes[i] > 0) {
|
||||||
else if (sortAttributes[i] < 0) symbol = getIcon("sorted-dsc");
|
symbol = m(icon, { name: "sorted-asc", class: "inline h-4 w-4 ml-1" });
|
||||||
|
} else if (sortAttributes[i] < 0) {
|
||||||
|
symbol = m(icon, { name: "sorted-dsc", class: "inline h-4 w-4 ml-1" });
|
||||||
|
} else {
|
||||||
|
symbol = m(icon, {
|
||||||
|
name: "unsorted",
|
||||||
|
class: "inline h-4 w-4 ml-1 opacity-50 hover:opacity-100",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const sortable = m(
|
const sortable = m(
|
||||||
"button",
|
"button",
|
||||||
@@ -78,41 +128,71 @@ function renderTable(
|
|||||||
symbol,
|
symbol,
|
||||||
);
|
);
|
||||||
|
|
||||||
labels.push(m("th", [label, sortable]));
|
labels.push(
|
||||||
|
m(
|
||||||
|
"th",
|
||||||
|
{
|
||||||
|
class:
|
||||||
|
"py-3.5 text-left text-sm font-semibold text-stone-500 whitespace-nowrap " +
|
||||||
|
padding,
|
||||||
|
scope: "col",
|
||||||
|
},
|
||||||
|
[label, sortable],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recordActionsCallback)
|
||||||
|
labels.push(m("th", { class: "pl-3 pr-6 py-3.5 w-0", scope: "col" }));
|
||||||
|
|
||||||
// Table rows
|
// Table rows
|
||||||
const rows = [];
|
const rows = [];
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const id = record["_id"] || record["DeviceID.ID"].value[0];
|
const id = record["_id"] || record["DeviceID.ID"].value[0];
|
||||||
const tds = [];
|
const tds = [];
|
||||||
|
const isSelected = selected.has(id);
|
||||||
if (buttons.length) {
|
if (buttons.length) {
|
||||||
const checkbox = m("input", {
|
const checkbox = m(
|
||||||
type: "checkbox",
|
"input.focus:ring-cyan-500 h-4 w-4 text-cyan-700 border-stone-300 rounded-sm",
|
||||||
checked: selected.has(id),
|
{
|
||||||
onchange: (e) => {
|
type: "checkbox",
|
||||||
if (e.target.checked) selected.add(id);
|
checked: isSelected,
|
||||||
else selected.delete(id);
|
onchange: (e) => {
|
||||||
|
if (e.target.checked) selected.add(id);
|
||||||
|
else selected.delete(id);
|
||||||
|
},
|
||||||
|
onclick: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.redraw = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
onclick: (e) => {
|
);
|
||||||
e.stopPropagation();
|
tds.push(
|
||||||
e.redraw = false;
|
m("td.px-6 py-4 whitespace-nowrap text-sm text-stone-500", checkbox),
|
||||||
},
|
);
|
||||||
});
|
|
||||||
tds.push(m("td", checkbox));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const attr of attributes) {
|
for (const [i, attr] of attributes.entries()) {
|
||||||
const attrs = {};
|
let padding: string;
|
||||||
|
if (i === 0) padding = buttons.length ? "pr-3" : "pl-6 pr-3";
|
||||||
|
else if (i === attributes.length - +!recordActionsCallback)
|
||||||
|
padding = "pl-3 pr-6";
|
||||||
|
else padding = "px-3";
|
||||||
|
|
||||||
|
const attrs = {
|
||||||
|
class: "py-4 whitespace-nowrap text-sm text-stone-900 " + padding,
|
||||||
|
};
|
||||||
let valueComponent;
|
let valueComponent;
|
||||||
|
|
||||||
if (typeof valueCallback === "function") {
|
if (typeof valueCallback === "function") {
|
||||||
valueComponent = valueCallback(attr, record);
|
valueComponent = valueCallback(attr, record);
|
||||||
} else if (attr.type === "code") {
|
} else if (attr.type === "code") {
|
||||||
const firstLines = record[attr.id].split("\n", 11);
|
const excerpt = getExcerpt(record[attr.id]);
|
||||||
if (firstLines.length > 10) firstLines[10] = ["\ufe19"];
|
valueComponent = m(
|
||||||
if (attrs["title"] == null) attrs["title"] = firstLines.join("\n");
|
"span.font-mono",
|
||||||
valueComponent = firstLines[0] || "";
|
{ title: excerpt.join("\n") },
|
||||||
|
excerpt[0],
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
valueComponent = record[attr.id];
|
valueComponent = record[attr.id];
|
||||||
}
|
}
|
||||||
@@ -129,13 +209,20 @@ function renderTable(
|
|||||||
recordButtons = recordActionsCallback;
|
recordButtons = recordActionsCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const button of recordButtons)
|
for (const button of recordButtons) {
|
||||||
tds.push(m("td.table-row-links", button));
|
tds.push(
|
||||||
|
m(
|
||||||
|
"td.pl-3 pr-6 py-4 whitespace-nowrap text-right text-sm font-medium",
|
||||||
|
button,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
rows.push(
|
rows.push(
|
||||||
m(
|
m(
|
||||||
"tr",
|
"tr",
|
||||||
{
|
{
|
||||||
|
class: isSelected ? "bg-stone-50" : "",
|
||||||
onclick: (e) => {
|
onclick: (e) => {
|
||||||
if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) {
|
if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) {
|
||||||
e.redraw = false;
|
e.redraw = false;
|
||||||
@@ -150,17 +237,27 @@ function renderTable(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rows.length)
|
if (!rows.length) {
|
||||||
rows.push(m("tr.empty", m("td", { colspan: labels.length }, "No records")));
|
rows.push(
|
||||||
|
m(
|
||||||
|
"tr",
|
||||||
|
m(
|
||||||
|
"td.bg-stripes text-sm font-medium text-center text-stone-500 p-4",
|
||||||
|
{ colspan: labels.length },
|
||||||
|
"No records",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Table footer
|
// Table footer
|
||||||
const footerElements = [];
|
const pagination = [];
|
||||||
if (total != null) footerElements.push(`${records.length}/${total}`);
|
if (total != null) pagination.push(`${records.length} / ${total}`);
|
||||||
else footerElements.push(`${records.length}`);
|
else pagination.push(`${records.length}`);
|
||||||
|
|
||||||
footerElements.push(
|
pagination.push(
|
||||||
m(
|
m(
|
||||||
"button",
|
"button.px-4 py-2 border border-stone-300 rounded-md text-stone-700 bg-white hover:bg-stone-50 ml-4 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Show more records",
|
title: "Show more records",
|
||||||
onclick: showMoreCallback,
|
onclick: showMoreCallback,
|
||||||
@@ -171,27 +268,53 @@ function renderTable(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let download: Children;
|
||||||
if (downloadUrl) {
|
if (downloadUrl) {
|
||||||
footerElements.push(
|
download = m(
|
||||||
m("a.download-csv", { href: downloadUrl, download: "" }, "Download"),
|
"a.text-cyan-700 hover:text-cyan-900",
|
||||||
|
{ href: downloadUrl, download: "" },
|
||||||
|
"Download",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tfoot = m(
|
const tfoot = m(
|
||||||
"tfoot",
|
"tfoot.bg-white",
|
||||||
m("tr", m("td", { colspan: labels.length }, footerElements)),
|
m(
|
||||||
|
"tr",
|
||||||
|
m(
|
||||||
|
"td.px-6 py-3 text-sm font-medium text-stone-700",
|
||||||
|
{ colspan: labels.length },
|
||||||
|
m(
|
||||||
|
"div.flex items-center justify-between",
|
||||||
|
m("div", pagination),
|
||||||
|
download,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const children = [
|
const children = [
|
||||||
m(
|
m(
|
||||||
"table.table.highlight",
|
"div.flex flex-col",
|
||||||
m("thead", m("tr", labels)),
|
m(
|
||||||
m("tbody", rows),
|
"div.-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8",
|
||||||
tfoot,
|
m(
|
||||||
|
"div.py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8",
|
||||||
|
m(
|
||||||
|
"div.shadow-sm overflow-hidden border-b border-stone-200 sm:rounded-lg",
|
||||||
|
m(
|
||||||
|
"table.min-w-full divide-y divide-stone-200",
|
||||||
|
m("thead.bg-stone-50", m("tr", labels)),
|
||||||
|
m("tbody.bg-white divide-y divide-stone-200", rows),
|
||||||
|
tfoot,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (buttons.length) children.push(m("div.actions-bar", buttons));
|
if (buttons.length) children.push(m("div.flex gap-3 mt-4", buttons));
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import m, { ClosureComponent, Component } from "mithril";
|
|
||||||
import menu from "./menu.ts";
|
|
||||||
import drawerComponent from "./drawer-component.ts";
|
|
||||||
import userMenu from "./user-menu.ts";
|
|
||||||
import adminMenu from "./admin-menu.ts";
|
|
||||||
import * as overlay from "./overlay.ts";
|
|
||||||
import { version as VERSION } from "../package.json";
|
|
||||||
import datalist from "./datalist.ts";
|
|
||||||
import { LOGO_SVG } from "../build/assets.ts";
|
|
||||||
|
|
||||||
const adminPages = [
|
|
||||||
"presets",
|
|
||||||
"provisions",
|
|
||||||
"virtualParameters",
|
|
||||||
"files",
|
|
||||||
"config",
|
|
||||||
"users",
|
|
||||||
"permissions",
|
|
||||||
];
|
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
|
||||||
return {
|
|
||||||
view: (vnode) => {
|
|
||||||
let sideMenu, group;
|
|
||||||
|
|
||||||
if (adminPages.includes(vnode.attrs["page"])) {
|
|
||||||
group = "admin";
|
|
||||||
const attrs = {};
|
|
||||||
attrs["page"] = vnode.attrs["page"];
|
|
||||||
sideMenu = m(adminMenu, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
const attrs = {};
|
|
||||||
attrs["page"] = group || vnode.attrs["page"];
|
|
||||||
|
|
||||||
return [
|
|
||||||
m("#header", [
|
|
||||||
m(
|
|
||||||
"div.logo",
|
|
||||||
m("img", { src: LOGO_SVG }),
|
|
||||||
m("span.version", "v" + VERSION),
|
|
||||||
),
|
|
||||||
m(userMenu),
|
|
||||||
m(menu, attrs),
|
|
||||||
m(drawerComponent),
|
|
||||||
]),
|
|
||||||
m(
|
|
||||||
"#content-wrapper",
|
|
||||||
sideMenu,
|
|
||||||
m("#content", { class: `page-${vnode.attrs["page"]}` }, [
|
|
||||||
vnode.children,
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
overlay.render(),
|
|
||||||
m(datalist),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default component;
|
|
||||||
+317
@@ -0,0 +1,317 @@
|
|||||||
|
import m, { ClosureComponent } from "mithril";
|
||||||
|
import drawerComponent from "./drawer-component.ts";
|
||||||
|
import * as overlay from "./overlay.ts";
|
||||||
|
import { version as VERSION } from "../package.json";
|
||||||
|
import datalist from "./datalist.ts";
|
||||||
|
import {
|
||||||
|
transitionRoot,
|
||||||
|
transitionChild,
|
||||||
|
dialog,
|
||||||
|
dialogOverlay,
|
||||||
|
icon,
|
||||||
|
} from "./tailwind-utility-components.ts";
|
||||||
|
import * as store from "./store.ts";
|
||||||
|
import * as notifications from "./notifications.ts";
|
||||||
|
import { LOGO_SVG } from "../build/assets.ts";
|
||||||
|
|
||||||
|
function tsxComponent<T>(
|
||||||
|
c: ClosureComponent<T>,
|
||||||
|
): (attrs: T) => ReturnType<typeof m> {
|
||||||
|
return c as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransitionRoot = tsxComponent(transitionRoot);
|
||||||
|
const TransitionChild = tsxComponent(transitionChild);
|
||||||
|
const Dialog = tsxComponent(dialog);
|
||||||
|
const DialogOverlay = tsxComponent(dialogOverlay);
|
||||||
|
const Icon = tsxComponent(icon);
|
||||||
|
|
||||||
|
function classNames(...classes: string[]): string {
|
||||||
|
return classes.filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Attrs {
|
||||||
|
page: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const component: ClosureComponent<Attrs> = () => {
|
||||||
|
let sidebarOpen = false;
|
||||||
|
|
||||||
|
function setSidebarOpen(open: boolean): void {
|
||||||
|
sidebarOpen = open;
|
||||||
|
setTimeout(m.redraw);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
view: (vnode) => {
|
||||||
|
const navigation = [
|
||||||
|
{
|
||||||
|
name: "Overview",
|
||||||
|
href: "#!/overview",
|
||||||
|
enabled: window.authorizer.hasAccess("devices", 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Devices",
|
||||||
|
href: "#!/devices",
|
||||||
|
enabled: window.authorizer.hasAccess("devices", 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Faults",
|
||||||
|
href: "#!/faults",
|
||||||
|
enabled: window.authorizer.hasAccess("faults", 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Presets",
|
||||||
|
href: "#!/presets",
|
||||||
|
enabled: window.authorizer.hasAccess("presets", 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Provisions",
|
||||||
|
href: "#!/provisions",
|
||||||
|
enabled: window.authorizer.hasAccess("provisions", 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Virtual Parameters",
|
||||||
|
href: "#!/virtualParameters",
|
||||||
|
enabled: window.authorizer.hasAccess("virtualParameters", 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Files",
|
||||||
|
href: "#!/files",
|
||||||
|
enabled: window.authorizer.hasAccess("files", 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Config",
|
||||||
|
href: "#!/config",
|
||||||
|
enabled: window.authorizer.hasAccess("config", 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Permissions",
|
||||||
|
href: "#!/permissions",
|
||||||
|
enabled: window.authorizer.hasAccess("permissions", 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Users",
|
||||||
|
href: "#!/users",
|
||||||
|
enabled: window.authorizer.hasAccess("users", 2),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.filter((item) => item.enabled)
|
||||||
|
.map(({ name, href }) => {
|
||||||
|
const n = href.slice(3);
|
||||||
|
return { name, href, active: vnode.attrs["page"] === n };
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
<div>
|
||||||
|
<TransitionRoot show={!!sidebarOpen} duration={300}>
|
||||||
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
class="fixed inset-0 flex z-40 md:hidden"
|
||||||
|
onClose={() => setSidebarOpen(false)}
|
||||||
|
>
|
||||||
|
<TransitionChild
|
||||||
|
enter="transition-opacity ease-linear duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="transition-opacity ease-linear duration-300"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<DialogOverlay class="fixed inset-0 bg-black/50" />
|
||||||
|
</TransitionChild>
|
||||||
|
<TransitionChild
|
||||||
|
enter="transition ease-in-out duration-300 transform"
|
||||||
|
enterFrom="-translate-x-full"
|
||||||
|
enterTo="translate-x-0"
|
||||||
|
leave="transition ease-in-out duration-300 transform"
|
||||||
|
leaveFrom="translate-x-0"
|
||||||
|
leaveTo="-translate-x-full"
|
||||||
|
>
|
||||||
|
<div class="relative flex-1 flex flex-col max-w-xs w-full bg-white">
|
||||||
|
<TransitionChild
|
||||||
|
enter="ease-in-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in-out duration-300"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div class="absolute top-0 right-0 -mr-12 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-hidden focus:ring-2 focus:ring-inset focus:ring-white"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.redraw = false;
|
||||||
|
setSidebarOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="sr-only">Close sidebar</span>
|
||||||
|
<Icon name="close" class="h-6 w-6 text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</TransitionChild>
|
||||||
|
<div class="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
|
||||||
|
<div class="flex-shrink-0 flex items-center px-4">
|
||||||
|
<img class="h-10 w-auto" src={LOGO_SVG} alt="GenieACS" />
|
||||||
|
</div>
|
||||||
|
<nav class="mt-5 px-2 flex flex-col gap-1">
|
||||||
|
{navigation.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
class={classNames(
|
||||||
|
item.active
|
||||||
|
? "bg-stone-100 text-stone-900"
|
||||||
|
: "text-stone-600 hover:bg-stone-50 hover:text-stone-900",
|
||||||
|
"group flex items-center px-2 py-2 text-base font-medium rounded-md",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
|
{window.username ? (
|
||||||
|
<div class="flex items-center px-2 text-stone-600 text-base">
|
||||||
|
{window.username}
|
||||||
|
<button
|
||||||
|
class="ml-auto text-base font-medium text-cyan-600 hover:text-cyan-500"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.target.disabled = true;
|
||||||
|
store
|
||||||
|
.logOut()
|
||||||
|
.then(() => {
|
||||||
|
location.hash = "";
|
||||||
|
location.reload();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
e.target.disabled = false;
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Log out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="px-2">
|
||||||
|
<a
|
||||||
|
class="text-base font-medium text-cyan-700 hover:text-cyan-900"
|
||||||
|
href=""
|
||||||
|
>
|
||||||
|
Log in
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-mono text-stone-400 text-right p-2">
|
||||||
|
v{VERSION}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TransitionChild>
|
||||||
|
<div class="flex-shrink-0 w-14">
|
||||||
|
{/* Force sidebar to shrink to fit close icon */}
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</TransitionRoot>
|
||||||
|
|
||||||
|
{/* Static sidebar for desktop */}
|
||||||
|
<div class="hidden md:flex md:w-64 md:flex-col md:fixed md:inset-y-0">
|
||||||
|
<div class="flex-1 flex flex-col min-h-0 border-r border-stone-200 bg-white">
|
||||||
|
<div class="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
|
||||||
|
<div class="flex items-center flex-shrink-0 px-4">
|
||||||
|
<img class="h-10 w-auto" src={LOGO_SVG} alt="GenieACS" />
|
||||||
|
</div>
|
||||||
|
<nav class="mt-5 flex-1 px-2 bg-white flex flex-col gap-1">
|
||||||
|
{navigation.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
class={classNames(
|
||||||
|
item.active
|
||||||
|
? "bg-stone-100 text-stone-900"
|
||||||
|
: "text-stone-600 hover:bg-stone-50 hover:text-stone-900",
|
||||||
|
"group flex items-center px-2 py-2 text-sm font-medium rounded-md",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
|
{window.username ? (
|
||||||
|
<div class="flex items-center px-2 text-stone-600 text-sm">
|
||||||
|
{window.username}
|
||||||
|
<button
|
||||||
|
class="ml-auto text-sm font-medium text-cyan-700 hover:text-cyan-900"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.target.disabled = true;
|
||||||
|
store
|
||||||
|
.logOut()
|
||||||
|
.then(() => {
|
||||||
|
location.hash = "";
|
||||||
|
location.reload();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
e.target.disabled = false;
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Log out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="px-2">
|
||||||
|
<a
|
||||||
|
class="text-sm font-medium text-cyan-700 hover:text-cyan-900"
|
||||||
|
href=""
|
||||||
|
>
|
||||||
|
Log in
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs font-mono text-stone-400 text-right p-2">
|
||||||
|
v{VERSION}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="md:pl-64 flex flex-col flex-1">
|
||||||
|
<div class="sticky top-0 z-10 md:hidden pl-1 pt-1 sm:pl-3 sm:pt-3 bg-stone-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="-ml-0.5 -mt-0.5 h-12 w-12 inline-flex items-center justify-center rounded-md text-stone-500 hover:text-stone-900 focus:outline-hidden focus:ring-2 focus:ring-inset focus:ring-cyan-500"
|
||||||
|
onclick={(e) => {
|
||||||
|
setSidebarOpen(true);
|
||||||
|
e.redraw = false;
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open sidebar</span>
|
||||||
|
<Icon name="menu" class="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<main class="flex-1">
|
||||||
|
<div class="py-6">
|
||||||
|
<div class="px-4 sm:px-6 md:px-8">
|
||||||
|
{m(drawerComponent)}
|
||||||
|
{vnode.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
overlay.render(),
|
||||||
|
m(datalist),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { ClosureComponent, Component, Children } from "mithril";
|
|
||||||
import { m } from "./components.ts";
|
|
||||||
import * as store from "./store.ts";
|
|
||||||
import * as notifications from "./notifications.ts";
|
|
||||||
import * as overlay from "./overlay.ts";
|
|
||||||
import changePasswordComponent from "./change-password-component.ts";
|
|
||||||
|
|
||||||
export function init(
|
|
||||||
args: Record<string, unknown>,
|
|
||||||
): Promise<Record<string, unknown>> {
|
|
||||||
return Promise.resolve(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const component: ClosureComponent = (): Component => {
|
|
||||||
return {
|
|
||||||
view: (vnode) => {
|
|
||||||
if (window.username) m.route.set(vnode.attrs["continue"] || "/");
|
|
||||||
|
|
||||||
document.title = "Login - GenieACS";
|
|
||||||
return [
|
|
||||||
m("h1", "Log in"),
|
|
||||||
m(
|
|
||||||
"form",
|
|
||||||
m(
|
|
||||||
"p",
|
|
||||||
m("label", { for: "username" }, "Username"),
|
|
||||||
m("input", {
|
|
||||||
name: "username",
|
|
||||||
type: "text",
|
|
||||||
value: vnode.state["username"],
|
|
||||||
oncreate: (vnode2) => {
|
|
||||||
(vnode2.dom as HTMLInputElement).focus();
|
|
||||||
},
|
|
||||||
oninput: (e) => {
|
|
||||||
vnode.state["username"] = e.target.value;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
m(
|
|
||||||
"p",
|
|
||||||
m("label", { for: "password" }, "Password"),
|
|
||||||
m("input", {
|
|
||||||
name: "password",
|
|
||||||
type: "password",
|
|
||||||
value: vnode.state["password"],
|
|
||||||
oninput: (e) => {
|
|
||||||
vnode.state["password"] = e.target.value;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
m(
|
|
||||||
"p",
|
|
||||||
m(
|
|
||||||
"button.primary",
|
|
||||||
{
|
|
||||||
type: "submit",
|
|
||||||
onclick: (e) => {
|
|
||||||
e.target.disabled = true;
|
|
||||||
store
|
|
||||||
.logIn(vnode.state["username"], vnode.state["password"])
|
|
||||||
.then(() => {
|
|
||||||
location.reload();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
notifications.push("error", err.response || err.message);
|
|
||||||
e.target.disabled = false;
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"Login",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
m(
|
|
||||||
"a",
|
|
||||||
{
|
|
||||||
onclick: () => {
|
|
||||||
const cb = (): Children => {
|
|
||||||
const attrs = {
|
|
||||||
onPasswordChange: () => {
|
|
||||||
overlay.close(cb);
|
|
||||||
m.redraw();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return m(changePasswordComponent, attrs);
|
|
||||||
};
|
|
||||||
overlay.open(cb);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"Change password",
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { ClosureComponent, Component, Children } from "mithril";
|
||||||
|
import { m } from "./components.ts";
|
||||||
|
import * as store from "./store.ts";
|
||||||
|
import * as notifications from "./notifications.ts";
|
||||||
|
import * as overlay from "./overlay.ts";
|
||||||
|
import changePasswordComponent from "./change-password-component.ts";
|
||||||
|
|
||||||
|
export function init(
|
||||||
|
args: Record<string, unknown>,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
return Promise.resolve(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const component: ClosureComponent = (): Component => {
|
||||||
|
let username = "";
|
||||||
|
let password = "";
|
||||||
|
let remember = false;
|
||||||
|
|
||||||
|
function logIn(e: MouseEvent): boolean {
|
||||||
|
e.target["disabled"] = true;
|
||||||
|
store
|
||||||
|
.logIn(username, password, remember)
|
||||||
|
.then(() => {
|
||||||
|
location.reload();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
notifications.push("error", err.response || err.message);
|
||||||
|
e.target["disabled"] = false;
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePassword(): void {
|
||||||
|
const cb = (): Children => {
|
||||||
|
const attrs = {
|
||||||
|
onPasswordChange: () => {
|
||||||
|
overlay.close(cb);
|
||||||
|
m.redraw();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return m(changePasswordComponent, attrs);
|
||||||
|
};
|
||||||
|
overlay.open(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
view: (vnode) => {
|
||||||
|
if (window.username) m.route.set(vnode.attrs["continue"] || "/");
|
||||||
|
|
||||||
|
document.title = "Login - GenieACS";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-md w-full flex flex-col gap-8">
|
||||||
|
<div>
|
||||||
|
<svg
|
||||||
|
class="mx-auto h-14 w-auto"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 64 64"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="a">
|
||||||
|
<stop offset="0" stop-color="#b72f5f" />
|
||||||
|
<stop offset="1" stop-color="#872346" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
xlink:href="#a"
|
||||||
|
id="b"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(.28375 0 0 -.28375 27.92 37.384)"
|
||||||
|
x1="16.045"
|
||||||
|
y1="132.803"
|
||||||
|
x2="16.045"
|
||||||
|
y2="-81.164"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
d="m27.92 37.38 2.6 4.94c-1.15 2.01-2.71 3.74-4.68 5.21-3.22 1.9-5.83 1.59-7.85-.92 3.9-1.76 7.21-4.83 9.93-9.22M18.74 27.2c-.27 2.86-.93 5.67-1.96 8.36-.79 2.08-1.33 4.25-1.6 6.45-.19 1.55.03 3.12.64 4.55.62 1.49 1.73 2.76 3.33 3.82-1.73.52-3.24.66-4.54.43-2.27-.44-3.75-1.81-4.44-4.1a13.48 13.48 0 0 1-.35-6.76c.4-2.11.95-4.18 1.64-6.21a43.64 43.64 0 0 0 1.63-6.46c.14-.83.16-1.7.04-2.6-.14-.96-.49-1.87-1.04-2.67a5.838 5.838 0 0 0-2.27-1.91c.93-.17 1.84-.31 2.75-.43.94-.08 1.89.08 2.75.47 1.12.49 1.95 1.3 2.51 2.41.75 1.42 1.06 3.04.89 4.64zm29.81-10.63-6.98 6.34 2.1 2.1 6.34-6.98.82.84-6.97 6.34 1.97 1.97 6.34-6.97.84.82-6.98 6.34 2.1 2.1 6.67-6.67 1.42 1.39-5.98 5.99-7.8 7.79-4.74 1.94-6.8-5.08 4.49 6.03-1.5.61-6.61-6.61 1.68-4.09 6.18 4.64.01.01.04-.03-.01-.02-5.25-6.99.85-2.09 6.39-6.38 7.38-7.38 1.41 1.39-6.68 6.68 2.09 2.09 6.34-6.97zm4.67 36.63C59.07 47.35 62 40.28 62 31.99c0-8.26-2.93-15.33-8.78-21.21C47.34 4.93 40.27 2 32.01 2c-8.29 0-15.36 2.93-21.21 8.78-3.6 3.61-6.08 7.65-7.47 12.15l.6-.33c2.98-1.57 5.62-.77 7.92 2.41-2.08.43-3.75.87-5.01 1.35-.69.27-1.33.63-1.9 1.09-1.28 1.01-2.08 2.3-2.41 3.86-.48 2.19-.47 4.47.04 6.65 1.08 5.77 3.82 10.85 8.23 15.24 5.85 5.87 12.92 8.8 21.21 8.8 8.28 0 15.35-2.93 21.21-8.8"
|
||||||
|
fill="url(#b)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<h2 class="mt-6 text-center text-3xl font-extrabold text-stone-900">
|
||||||
|
Log in to continue
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<form class="mt-8 flex flex-col gap-6">
|
||||||
|
<div class="rounded-md shadow-xs -space-y-px">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="sr-only">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
autocomplete="username"
|
||||||
|
required
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-stone-300 placeholder-stone-500 text-stone-900 rounded-t-md focus:outline-hidden focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="Username"
|
||||||
|
oninput={(e) => {
|
||||||
|
username = e.target.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="sr-only">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
autocomplete="current-password"
|
||||||
|
required
|
||||||
|
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-stone-300 placeholder-stone-500 text-stone-900 rounded-b-md focus:outline-hidden focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="Password"
|
||||||
|
oninput={(e) => {
|
||||||
|
password = e.target.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
id="remember"
|
||||||
|
name="remember"
|
||||||
|
type="checkbox"
|
||||||
|
value={remember}
|
||||||
|
class="h-4 w-4 text-cyan-700 focus:ring-cyan-500 border-stone-300 rounded-sm"
|
||||||
|
onchange={(e) => {
|
||||||
|
remember = e.target.checked;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="remember"
|
||||||
|
class="ml-2 block text-sm text-stone-900"
|
||||||
|
>
|
||||||
|
Remember me
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="font-medium text-cyan-700 hover:text-cyan-900"
|
||||||
|
onclick={changePassword}
|
||||||
|
>
|
||||||
|
Change password
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500"
|
||||||
|
onclick={logIn}
|
||||||
|
>
|
||||||
|
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-cyan-500 group-hover:text-cyan-400"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
Log in
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
+22
-16
@@ -6,21 +6,25 @@ const component: ClosureComponent = (): Component => {
|
|||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
const text = vnode.attrs["text"];
|
const text = vnode.attrs["text"];
|
||||||
const element = vnode.attrs["element"] || "span";
|
const element = vnode.attrs["element"] || "span";
|
||||||
|
const className = vnode.attrs["class"] || "";
|
||||||
|
|
||||||
function overflowed(_vnode): void {
|
function overflowed(_vnode): void {
|
||||||
_vnode.dom.classList.add("long-text-overflowed");
|
_vnode.dom.classList.add("cursor-pointer", "hover:underline");
|
||||||
|
_vnode.dom.setAttribute("title", text);
|
||||||
_vnode.dom.onclick = (e) => {
|
_vnode.dom.onclick = (e) => {
|
||||||
overlay.open(() => {
|
overlay.open(() => {
|
||||||
return m("textarea.long-text", {
|
return m(
|
||||||
value: text,
|
"textarea.font-mono text-sm focus:ring-cyan-500 focus:border-cyan-500 border border-stone-300 rounded-md",
|
||||||
cols: 80,
|
{
|
||||||
rows: 24,
|
value: text,
|
||||||
readonly: "",
|
cols: 80,
|
||||||
oncreate: (vnode2) => {
|
rows: 24,
|
||||||
(vnode2.dom as HTMLTextAreaElement).focus();
|
readonly: "",
|
||||||
(vnode2.dom as HTMLTextAreaElement).select();
|
oncreate: (vnode2) => {
|
||||||
|
(vnode2.dom as HTMLTextAreaElement).focus();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
// prevent index page selection
|
// prevent index page selection
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -32,21 +36,23 @@ const component: ClosureComponent = (): Component => {
|
|||||||
element,
|
element,
|
||||||
{
|
{
|
||||||
oncreate: (vnode2) => {
|
oncreate: (vnode2) => {
|
||||||
if (vnode2.dom.clientWidth !== vnode2.dom.scrollWidth)
|
const w = Math.round(vnode2.dom.getBoundingClientRect().width);
|
||||||
overflowed(vnode2);
|
if (w !== vnode2.dom.scrollWidth) overflowed(vnode2);
|
||||||
},
|
},
|
||||||
onupdate: (vnode2) => {
|
onupdate: (vnode2) => {
|
||||||
if (vnode2.dom.clientWidth === vnode2.dom.scrollWidth) {
|
const w = Math.round(vnode2.dom.getBoundingClientRect().width);
|
||||||
|
if (w === vnode2.dom.scrollWidth) {
|
||||||
(vnode2.dom as HTMLElement).classList.remove(
|
(vnode2.dom as HTMLElement).classList.remove(
|
||||||
"long-text-overflowed",
|
"cursor-pointer",
|
||||||
|
"hover:underline",
|
||||||
);
|
);
|
||||||
(vnode2.dom as HTMLElement).onclick = null;
|
(vnode2.dom as HTMLElement).onclick = null;
|
||||||
|
(vnode2.dom as HTMLElement).removeAttribute("title");
|
||||||
} else {
|
} else {
|
||||||
overflowed(vnode2);
|
overflowed(vnode2);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
class: "long-text",
|
class: "block truncate decoration-dotted max-w-full " + className,
|
||||||
title: text,
|
|
||||||
},
|
},
|
||||||
text,
|
text,
|
||||||
);
|
);
|
||||||
|
|||||||
-63
@@ -1,63 +0,0 @@
|
|||||||
import m, { ClosureComponent, Component } from "mithril";
|
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
|
||||||
return {
|
|
||||||
view: (vnode) => {
|
|
||||||
const active = { [vnode.attrs["page"]]: "active" };
|
|
||||||
|
|
||||||
const tabs = [];
|
|
||||||
if (window.authorizer.hasAccess("devices", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["overview"] },
|
|
||||||
m("a", { href: "#!/overview" }, "Overview"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("devices", 2)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["devices"] },
|
|
||||||
m("a", { href: "#!/devices" }, "Devices"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("faults", 2)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["faults"] },
|
|
||||||
m("a", { href: "#!/faults" }, "Faults"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const adminPages = [
|
|
||||||
"presets",
|
|
||||||
"provisions",
|
|
||||||
"virtualParameters",
|
|
||||||
"files",
|
|
||||||
];
|
|
||||||
for (const page of adminPages) {
|
|
||||||
if (window.authorizer.hasAccess(page, 2)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["admin"] },
|
|
||||||
m("a", { href: "#!/admin" }, "Admin"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return m("nav", m("ul", tabs));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default component;
|
|
||||||
+26
-27
@@ -1,4 +1,5 @@
|
|||||||
import m, { Children } from "mithril";
|
import m, { Children } from "mithril";
|
||||||
|
import { dialog, dialogOverlay, icon } from "./tailwind-utility-components.ts";
|
||||||
|
|
||||||
type OverlayCallback = () => Children;
|
type OverlayCallback = () => Children;
|
||||||
type CloseCallback = () => boolean;
|
type CloseCallback = () => boolean;
|
||||||
@@ -28,35 +29,33 @@ export function close(callback: OverlayCallback, force = true): boolean {
|
|||||||
export function render(): Children {
|
export function render(): Children {
|
||||||
if (overlayCallback) {
|
if (overlayCallback) {
|
||||||
return m(
|
return m(
|
||||||
".overlay-wrapper",
|
dialog,
|
||||||
{
|
{
|
||||||
tabindex: 0,
|
as: "div",
|
||||||
onclick: () => {
|
class: "fixed z-10 inset-0 overflow-y-auto",
|
||||||
close(overlayCallback, false);
|
onClose: () => close(overlayCallback, false),
|
||||||
},
|
|
||||||
style: "opacity: 0",
|
|
||||||
oncreate: (vnode) => {
|
|
||||||
(vnode.dom as HTMLDivElement).focus();
|
|
||||||
(vnode.dom as HTMLDivElement).style.opacity = "1";
|
|
||||||
},
|
|
||||||
onbeforeremove: (vnode) => {
|
|
||||||
(vnode.dom as HTMLDivElement).style.opacity = "0";
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve();
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
m(
|
m("div.flex items-center justify-center min-h-screen p-4 text-center", [
|
||||||
".overlay",
|
m(dialogOverlay, {
|
||||||
{
|
class: "fixed inset-0 bg-black/50",
|
||||||
onclick: (e) => {
|
}),
|
||||||
e.stopPropagation();
|
m(
|
||||||
},
|
"div.relative z-10 bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform max-w-full",
|
||||||
},
|
m(
|
||||||
overlayCallback(),
|
"div.block absolute top-0 right-0 pt-4 pr-4",
|
||||||
),
|
m(
|
||||||
|
"button.bg-white rounded-md text-stone-400 hover:text-stone-500 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500",
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
onclick: () => close(overlayCallback, false),
|
||||||
|
},
|
||||||
|
m("span.sr-only", "Close"),
|
||||||
|
m(icon, { name: "close", class: "h-6 w-6" }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
overlayCallback(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-6
@@ -41,28 +41,43 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
document.title = "Overview - GenieACS";
|
document.title = "Overview - GenieACS";
|
||||||
const children = [];
|
const children = [];
|
||||||
for (const group of Object.values(GROUPS)) {
|
for (const group of Object.values(GROUPS)) {
|
||||||
if (group["label"])
|
if (group["label"]) {
|
||||||
children.push(
|
children.push(
|
||||||
m("h1", store.evaluateExpression(group["label"], null)),
|
m(
|
||||||
|
"h1.text-xl font-medium text-stone-900 mb-5",
|
||||||
|
store.evaluateExpression(group["label"], null),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const groupChildren = [];
|
const groupChildren = [];
|
||||||
for (const chartName of Object.values(group["charts"]) as string[]) {
|
for (const chartName of Object.values(group["charts"]) as string[]) {
|
||||||
const chart = vnode.attrs["charts"][chartName];
|
const chart = vnode.attrs["charts"][chartName];
|
||||||
const chartChildren = [];
|
const chartChildren = [];
|
||||||
if (chart.label)
|
if (chart.label) {
|
||||||
chartChildren.push(
|
chartChildren.push(
|
||||||
m("h2", store.evaluateExpression(chart.label, null)),
|
m(
|
||||||
|
"h2.text-lg font-semibold text-stone-700 truncate mb-5 text-center",
|
||||||
|
store.evaluateExpression(chart.label, null),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const attrs = {};
|
const attrs = {};
|
||||||
attrs["chart"] = chart;
|
attrs["chart"] = chart;
|
||||||
chartChildren.push(m(pieChartComponent, attrs));
|
chartChildren.push(m(pieChartComponent, attrs));
|
||||||
|
|
||||||
groupChildren.push(m(".overview-chart", chartChildren));
|
groupChildren.push(
|
||||||
|
m(
|
||||||
|
"div.p-4 bg-white shadow-sm rounded-lg sm:p-6 sm:px-8",
|
||||||
|
chartChildren,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
children.push(m(".overview-chart-group", groupChildren));
|
children.push(
|
||||||
|
m("div.flex justify-center mt-5 mb-10 gap-x-10", groupChildren),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
|
|||||||
+27
-6
@@ -44,6 +44,22 @@ const attributes = [
|
|||||||
{ id: "validate", label: "Validate", type: "textarea" },
|
{ id: "validate", label: "Validate", type: "textarea" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function getExcerpt(text: string, maxLength = 80, maxLines = 10): string[] {
|
||||||
|
let lines: string[] = text?.split("\n", maxLines + 1) ?? [""];
|
||||||
|
|
||||||
|
if (lines.length > maxLines) {
|
||||||
|
lines.pop();
|
||||||
|
lines[maxLines - 1] = "\ufe19";
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = lines.map((l) => {
|
||||||
|
if (l.length <= maxLength) return l;
|
||||||
|
return l.slice(0, maxLength - 1) + "\u2026";
|
||||||
|
});
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
const unpackSmartQuery = memoize((query) => {
|
const unpackSmartQuery = memoize((query) => {
|
||||||
return map(query, (e) => {
|
return map(query, (e) => {
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
||||||
@@ -119,6 +135,8 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
} else if (action === "delete") {
|
} else if (action === "delete") {
|
||||||
|
if (!confirm("Deleting permission. Are you sure?"))
|
||||||
|
return void resolve(null);
|
||||||
store
|
store
|
||||||
.deleteResource("permissions", object["_id"])
|
.deleteResource("permissions", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -177,7 +195,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter): void {
|
||||||
const ops = { filter };
|
const ops = { filter };
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
m.route.set("/admin/permissions", ops);
|
m.route.set("/permissions", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = vnode.attrs["sort"]
|
const sort = vnode.attrs["sort"]
|
||||||
@@ -197,7 +215,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||||
const ops = { sort: JSON.stringify(_sort) };
|
const ops = { sort: JSON.stringify(_sort) };
|
||||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||||
m.route.set("/admin/permissions", ops);
|
m.route.set("/permissions", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
let filter = vnode.attrs["filter"]
|
||||||
@@ -221,6 +239,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
else if (val === 2) return "2: read";
|
else if (val === 2) return "2: read";
|
||||||
else if (val === 3) return "3: write";
|
else if (val === 3) return "3: write";
|
||||||
return val;
|
return val;
|
||||||
|
} else if (attr.id === "validate" || attr.id === "filter") {
|
||||||
|
const except = getExcerpt(permission[attr.id], 80, 1);
|
||||||
|
return m("span.font-mono", { title: permission[attr.id] }, except[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return permission[attr.id];
|
return permission[attr.id];
|
||||||
@@ -244,7 +265,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
if (val === 3) permission["access"] = "3: write";
|
if (val === 3) permission["access"] = "3: write";
|
||||||
return [
|
return [
|
||||||
m(
|
m(
|
||||||
"a",
|
"button.text-cyan-700 hover:text-cyan-900 font-medium",
|
||||||
{
|
{
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
let cb: () => Children = null;
|
let cb: () => Children = null;
|
||||||
@@ -306,7 +327,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
||||||
return [
|
return [
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Create new permission",
|
title: "Create new permission",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
@@ -352,7 +373,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"New",
|
"New",
|
||||||
),
|
),
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Delete selected permissions",
|
title: "Delete selected permissions",
|
||||||
disabled: !selected.size,
|
disabled: !selected.size,
|
||||||
@@ -397,7 +418,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing permissions"),
|
m("h1.text-xl font-medium text-stone-900 mb-5", "Listing permissions"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(
|
||||||
"loading",
|
"loading",
|
||||||
|
|||||||
+38
-22
@@ -23,21 +23,30 @@ function drawChart(chartData): Children {
|
|||||||
for (const slice of Object.values(slices)) {
|
for (const slice of Object.values(slices)) {
|
||||||
const percent = total > 0 ? (slice["count"]["value"] || 0) / total : 0;
|
const percent = total > 0 ? (slice["count"]["value"] || 0) / total : 0;
|
||||||
legend.push(
|
legend.push(
|
||||||
m(".legend-line", [
|
m("tr", [
|
||||||
m("span.color", {
|
|
||||||
style: `background-color: ${store.evaluateExpression(slice["color"], null)} !important;`,
|
|
||||||
}),
|
|
||||||
`${store.evaluateExpression(slice["label"], null)}: `,
|
|
||||||
m(
|
m(
|
||||||
"a",
|
"td",
|
||||||
{
|
m("span.inline-block w-3 h-3 border border-stone-200 mr-1", {
|
||||||
href: `#!/devices/?${m.buildQueryString({
|
style: `background-color: ${store.evaluateExpression(slice["color"], null)} !important;`,
|
||||||
filter: memoizedStringify(slice["filter"]),
|
}),
|
||||||
})}`,
|
),
|
||||||
},
|
m("td.w-full", store.evaluateExpression(slice["label"], null)),
|
||||||
slice["count"]["value"] || 0,
|
m(
|
||||||
|
"td.text-stone-500 text-right tabular-nums",
|
||||||
|
`${Math.round(percent * 100)}%`,
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"td.text-right tabular-nums",
|
||||||
|
m(
|
||||||
|
"a.text-cyan-700 hover:text-cyan-900 font-medium ml-2",
|
||||||
|
{
|
||||||
|
href: `#!/devices/?${m.buildQueryString({
|
||||||
|
filter: memoizedStringify(slice["filter"]),
|
||||||
|
})}`,
|
||||||
|
},
|
||||||
|
slice["count"]["value"] || 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
` (${(percent * 100).toFixed(2)}%)`,
|
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -56,7 +65,7 @@ function drawChart(chartData): Children {
|
|||||||
startY = endY;
|
startY = endY;
|
||||||
|
|
||||||
paths.push(
|
paths.push(
|
||||||
m("path", {
|
m("path.stroke-white stroke-1", {
|
||||||
d: sketch,
|
d: sketch,
|
||||||
fill: store.evaluateExpression(slice["color"], null),
|
fill: store.evaluateExpression(slice["color"], null),
|
||||||
}),
|
}),
|
||||||
@@ -69,26 +78,26 @@ function drawChart(chartData): Children {
|
|||||||
|
|
||||||
links.push(
|
links.push(
|
||||||
m(
|
m(
|
||||||
"a",
|
"a.opacity-0 hover:opacity-100 focus-visible:opacity-100 outline-hidden",
|
||||||
{
|
{
|
||||||
"xlink:href": `#!/devices/?${m.buildQueryString({
|
"xlink:href": `#!/devices/?${m.buildQueryString({
|
||||||
filter: memoizedStringify(slice["filter"]),
|
filter: memoizedStringify(slice["filter"]),
|
||||||
})}`,
|
})}`,
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
m("path", {
|
m("path.stroke-cyan-500 stroke-1", {
|
||||||
d: sketch,
|
d: sketch,
|
||||||
"fill-opacity": 0,
|
"fill-opacity": 0,
|
||||||
}),
|
}),
|
||||||
m(
|
m(
|
||||||
"text",
|
"text.opacity-40 font-medium fill-black",
|
||||||
{
|
{
|
||||||
x: percentageX,
|
x: percentageX,
|
||||||
y: percentageY,
|
y: percentageY,
|
||||||
"dominant-baseline": "middle",
|
"dominant-baseline": "middle",
|
||||||
"text-anchor": "middle",
|
"text-anchor": "middle",
|
||||||
},
|
},
|
||||||
`${(percent * 100).toFixed(2)}%`,
|
`${Math.round(percent * 100)}%`,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -96,16 +105,23 @@ function drawChart(chartData): Children {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
legend.push(m("span.legend-total", `Total: ${total}`));
|
legend.push(
|
||||||
|
m(
|
||||||
|
"tr",
|
||||||
|
m("td", ""),
|
||||||
|
m("td", { colspan: 2 }, "Total"),
|
||||||
|
m("td.text-right tabular-nums", total),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"loading",
|
"loading",
|
||||||
{
|
{
|
||||||
queries: Object.values(chartData.slices).map((s) => s["count"]),
|
queries: Object.values(chartData.slices).map((s) => s["count"]),
|
||||||
},
|
},
|
||||||
m("div", { class: "pie-chart" }, [
|
m("div", [
|
||||||
m(
|
m(
|
||||||
"svg",
|
"svg.m-4",
|
||||||
{
|
{
|
||||||
// Adding 2 as padding; strokes must not be more than 2
|
// Adding 2 as padding; strokes must not be more than 2
|
||||||
viewBox: "-102 -102 204 204",
|
viewBox: "-102 -102 204 204",
|
||||||
@@ -116,7 +132,7 @@ function drawChart(chartData): Children {
|
|||||||
},
|
},
|
||||||
paths.concat(links),
|
paths.concat(links),
|
||||||
),
|
),
|
||||||
m(".legend", legend),
|
m("table.mt-8 text-sm", legend),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-10
@@ -39,6 +39,22 @@ interface ValidationErrors {
|
|||||||
[prop: string]: string;
|
[prop: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExcerpt(text: string, maxLength = 80, maxLines = 10): string[] {
|
||||||
|
let lines: string[] = text?.split("\n", maxLines + 1) ?? [""];
|
||||||
|
|
||||||
|
if (lines.length > maxLines) {
|
||||||
|
lines.pop();
|
||||||
|
lines[maxLines - 1] = "\ufe19";
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = lines.map((l) => {
|
||||||
|
if (l.length <= maxLength) return l;
|
||||||
|
return l.slice(0, maxLength - 1) + "\u2026";
|
||||||
|
});
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const object = Object.assign({}, _object);
|
const object = Object.assign({}, _object);
|
||||||
@@ -90,6 +106,8 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
} else if (action === "delete") {
|
} else if (action === "delete") {
|
||||||
|
if (!confirm("Deleting preset. Are you sure?")) return void resolve(null);
|
||||||
|
|
||||||
store
|
store
|
||||||
.deleteResource("presets", object["_id"])
|
.deleteResource("presets", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -149,7 +167,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter): void {
|
||||||
const ops = { filter };
|
const ops = { filter };
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
m.route.set("/admin/presets", ops);
|
m.route.set("/presets", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = vnode.attrs["sort"]
|
const sort = vnode.attrs["sort"]
|
||||||
@@ -176,7 +194,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||||
const ops = { sort: JSON.stringify(_sort) };
|
const ops = { sort: JSON.stringify(_sort) };
|
||||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||||
m.route.set("/admin/presets", ops);
|
m.route.set("/presets", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
let filter = vnode.attrs["filter"]
|
||||||
@@ -227,18 +245,24 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"a",
|
"a.text-cyan-700 hover:text-cyan-900 font-mono",
|
||||||
{ href: devicesUrl, title: preset["precondition"] },
|
{ href: devicesUrl, title: preset["precondition"] },
|
||||||
preset["precondition"],
|
getExcerpt(preset["precondition"], 80, 1)[0],
|
||||||
|
);
|
||||||
|
} else if (attr.id === "provisionArgs") {
|
||||||
|
return m(
|
||||||
|
"span.font-mono",
|
||||||
|
{ title: preset["provisionArgs"] },
|
||||||
|
getExcerpt(preset["provisionArgs"], 80, 1)[0],
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
attr.id === "provision" &&
|
attr.id === "provision" &&
|
||||||
userDefinedProvisions.has(preset[attr.id])
|
userDefinedProvisions.has(preset[attr.id])
|
||||||
) {
|
) {
|
||||||
return m(
|
return m(
|
||||||
"a",
|
"a.text-cyan-700 hover:text-cyan-900",
|
||||||
{
|
{
|
||||||
href: `#!/admin/provisions?${m.buildQueryString({
|
href: `#!/provisions?${m.buildQueryString({
|
||||||
filter: `Q("ID", "${preset["provision"]}")`,
|
filter: `Q("ID", "${preset["provision"]}")`,
|
||||||
})}`,
|
})}`,
|
||||||
},
|
},
|
||||||
@@ -261,7 +285,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
attrs["recordActionsCallback"] = (preset) => {
|
attrs["recordActionsCallback"] = (preset) => {
|
||||||
return [
|
return [
|
||||||
m(
|
m(
|
||||||
"a",
|
"button.text-cyan-700 hover:text-cyan-900 font-medium",
|
||||||
{
|
{
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
let cb: () => Children = null;
|
let cb: () => Children = null;
|
||||||
@@ -322,7 +346,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
||||||
return [
|
return [
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Create new preset",
|
title: "Create new preset",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
@@ -368,7 +392,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"New",
|
"New",
|
||||||
),
|
),
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Delete selected presets",
|
title: "Delete selected presets",
|
||||||
disabled: !selected.size,
|
disabled: !selected.size,
|
||||||
@@ -411,7 +435,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing presets"),
|
m("h1.text-xl font-medium text-stone-900 mb-5", "Listing presets"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(
|
||||||
"loading",
|
"loading",
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
} else if (action === "delete") {
|
} else if (action === "delete") {
|
||||||
|
if (!confirm("Deleting provision. Are you sure?"))
|
||||||
|
return void resolve(null);
|
||||||
store
|
store
|
||||||
.deleteResource("provisions", object["_id"])
|
.deleteResource("provisions", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -142,7 +144,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter): void {
|
||||||
const ops = { filter };
|
const ops = { filter };
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
m.route.set("/admin/provisions", ops);
|
m.route.set("/provisions", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = vnode.attrs["sort"]
|
const sort = vnode.attrs["sort"]
|
||||||
@@ -159,7 +161,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||||
const ops = { sort: JSON.stringify(_sort) };
|
const ops = { sort: JSON.stringify(_sort) };
|
||||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||||
m.route.set("/admin/provisions", ops);
|
m.route.set("/provisions", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
let filter = vnode.attrs["filter"]
|
||||||
@@ -187,7 +189,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
attrs["recordActionsCallback"] = (provision) => {
|
attrs["recordActionsCallback"] = (provision) => {
|
||||||
return [
|
return [
|
||||||
m(
|
m(
|
||||||
"a",
|
"button.text-cyan-700 hover:text-cyan-900 font-medium",
|
||||||
{
|
{
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
let cb: () => Children = null;
|
let cb: () => Children = null;
|
||||||
@@ -239,7 +241,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
||||||
return [
|
return [
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Create new provision",
|
title: "Create new provision",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
@@ -285,7 +287,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"New",
|
"New",
|
||||||
),
|
),
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Delete selected provisions",
|
title: "Delete selected provisions",
|
||||||
disabled: !selected.size,
|
disabled: !selected.size,
|
||||||
@@ -330,7 +332,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing provisions"),
|
m("h1.text-xl font-medium text-stone-900 mb-5", "Listing provisions"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(
|
||||||
"loading",
|
"loading",
|
||||||
|
|||||||
+75
-54
@@ -27,7 +27,7 @@ function createField(current, attr, focus): Children {
|
|||||||
options.push(m("option", { value: op }, op));
|
options.push(m("option", { value: op }, op));
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"select",
|
"select.mt-1 block pl-3 pr-10 py-2 text-base border-stone-300 focus:outline-hidden focus:ring-cyan-500 focus:border-cyan-500 sm:text-sm rounded-md",
|
||||||
{
|
{
|
||||||
name: attr.id,
|
name: attr.id,
|
||||||
value: selected,
|
value: selected,
|
||||||
@@ -68,7 +68,16 @@ function createField(current, attr, focus): Children {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return m("tr", [m("td", m("input", opts)), m("td", op)]);
|
return m("tr", [
|
||||||
|
m(
|
||||||
|
"td",
|
||||||
|
m(
|
||||||
|
"input.focus:ring-cyan-500 h-4 w-4 text-cyan-700 border-stone-300 rounded-sm",
|
||||||
|
opts,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
m("td", op),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
return m("table", options);
|
return m("table", options);
|
||||||
@@ -102,18 +111,55 @@ function createField(current, attr, focus): Children {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (attr.type === "textarea") {
|
} else if (attr.type === "textarea") {
|
||||||
return m("textarea", {
|
return m(
|
||||||
|
"textarea.shadow-xs block focus:ring-cyan-500 focus:border-cyan-500 sm:text-sm border border-stone-300 rounded-md",
|
||||||
|
{
|
||||||
|
name: attr.id,
|
||||||
|
value: current.object[attr.id],
|
||||||
|
readonly: attr.id === "_id" && !current.isNew,
|
||||||
|
cols: attr.cols || 80,
|
||||||
|
rows: attr.rows || 4,
|
||||||
|
style: "resize: none;",
|
||||||
|
oncreate: focus
|
||||||
|
? (_vnode) => {
|
||||||
|
const dom = _vnode.dom as HTMLInputElement;
|
||||||
|
dom.focus();
|
||||||
|
dom.setSelectionRange(dom.value.length, dom.value.length);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
oninput: (e) => {
|
||||||
|
current.object[attr.id] = e.target.value;
|
||||||
|
current.modified = true;
|
||||||
|
e.redraw = false;
|
||||||
|
},
|
||||||
|
onkeypress: (e) => {
|
||||||
|
e.redraw = false;
|
||||||
|
if (e.which === 13 && !e.shiftKey) {
|
||||||
|
const dom = e.target;
|
||||||
|
dom.form.querySelector("button[type=submit]").click();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let datalist: string = null;
|
||||||
|
if (attr.options) datalist = getDatalistId(attr.options);
|
||||||
|
|
||||||
|
return m(
|
||||||
|
"input.shadow-xs focus:ring-cyan-500 focus:border-cyan-500 block sm:text-sm border-stone-300 rounded-md",
|
||||||
|
{
|
||||||
|
type: attr.type === "password" ? "password" : "text",
|
||||||
name: attr.id,
|
name: attr.id,
|
||||||
|
list: datalist,
|
||||||
|
autocomplete: datalist ? "off" : null,
|
||||||
|
disabled: attr.id === "_id" && !current.isNew,
|
||||||
value: current.object[attr.id],
|
value: current.object[attr.id],
|
||||||
readonly: attr.id === "_id" && !current.isNew,
|
|
||||||
cols: attr.cols || 80,
|
|
||||||
rows: attr.rows || 4,
|
|
||||||
style: "resize: none;",
|
|
||||||
oncreate: focus
|
oncreate: focus
|
||||||
? (_vnode) => {
|
? (_vnode) => {
|
||||||
const dom = _vnode.dom as HTMLInputElement;
|
(_vnode.dom as HTMLInputElement).focus();
|
||||||
dom.focus();
|
|
||||||
dom.setSelectionRange(dom.value.length, dom.value.length);
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
oninput: (e) => {
|
oninput: (e) => {
|
||||||
@@ -121,39 +167,8 @@ function createField(current, attr, focus): Children {
|
|||||||
current.modified = true;
|
current.modified = true;
|
||||||
e.redraw = false;
|
e.redraw = false;
|
||||||
},
|
},
|
||||||
onkeypress: (e) => {
|
|
||||||
e.redraw = false;
|
|
||||||
if (e.which === 13 && !e.shiftKey) {
|
|
||||||
const dom = e.target;
|
|
||||||
dom.form.querySelector("button[type=submit]").click();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let datalist: string = null;
|
|
||||||
if (attr.options) datalist = getDatalistId(attr.options);
|
|
||||||
|
|
||||||
return m("input", {
|
|
||||||
type: attr.type === "password" ? "password" : "text",
|
|
||||||
name: attr.id,
|
|
||||||
list: datalist,
|
|
||||||
autocomplete: datalist ? "off" : null,
|
|
||||||
disabled: attr.id === "_id" && !current.isNew,
|
|
||||||
value: current.object[attr.id],
|
|
||||||
oncreate: focus
|
|
||||||
? (_vnode) => {
|
|
||||||
(_vnode.dom as HTMLInputElement).focus();
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
oninput: (e) => {
|
|
||||||
current.object[attr.id] = e.target.value;
|
|
||||||
current.modified = true;
|
|
||||||
e.redraw = false;
|
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Attrs {
|
interface Attrs {
|
||||||
@@ -195,24 +210,22 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
form.push(
|
form.push(
|
||||||
m(
|
m(
|
||||||
"p",
|
"p",
|
||||||
m("label", { for: attr.id }, attr.label || attr.id),
|
m(
|
||||||
m("br"),
|
"label.block text-sm font-semibold text-stone-700 mt-2 mb-1",
|
||||||
|
{ for: attr.id },
|
||||||
|
attr.label || attr.id,
|
||||||
|
),
|
||||||
createField(current, attr, focus),
|
createField(current, attr, focus),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = m(
|
const buttons: VnodeDOM[] = [];
|
||||||
"button.primary",
|
|
||||||
{ type: "submit" },
|
|
||||||
"Save",
|
|
||||||
) as VnodeDOM;
|
|
||||||
const buttons = [submit];
|
|
||||||
|
|
||||||
if (!current.isNew) {
|
if (!current.isNew) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-xs text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-red-500",
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
title: `Delete ${singular[resource] || resource}`,
|
title: `Delete ${singular[resource] || resource}`,
|
||||||
@@ -229,11 +242,19 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
form.push(m(".actions-bar", buttons));
|
const submit = m(
|
||||||
|
"button.ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-xs text-sm font-medium rounded-md text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500",
|
||||||
|
{ type: "submit" },
|
||||||
|
"Save",
|
||||||
|
) as VnodeDOM;
|
||||||
|
|
||||||
|
buttons.push(submit);
|
||||||
|
|
||||||
|
form.push(m("div.flex justify-end mt-5", buttons));
|
||||||
|
|
||||||
const children = [
|
const children = [
|
||||||
m(
|
m(
|
||||||
"h1",
|
"h2.text-lg leading-6 font-medium text-stone-900",
|
||||||
`${current.isNew ? "New" : "Editing"} ${
|
`${current.isNew ? "New" : "Editing"} ${
|
||||||
singular[resource] || resource
|
singular[resource] || resource
|
||||||
}`,
|
}`,
|
||||||
@@ -259,7 +280,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return m("div.put-form", children);
|
return m("div", children);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+6
-2
@@ -657,12 +657,16 @@ export function changePassword(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logIn(username: string, password: string): Promise<void> {
|
export function logIn(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
remember = false,
|
||||||
|
): Promise<void> {
|
||||||
return xhrRequest({
|
return xhrRequest({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "login",
|
url: "login",
|
||||||
background: true,
|
background: true,
|
||||||
body: { username, password },
|
body: { username, password, remember },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import m, {
|
||||||
|
ChildArrayOrPrimitive,
|
||||||
|
ClosureComponent,
|
||||||
|
mount,
|
||||||
|
Vnode,
|
||||||
|
VnodeDOM,
|
||||||
|
} from "mithril";
|
||||||
|
import { ICONS_SVG } from "../build/assets.ts";
|
||||||
|
|
||||||
|
export const portal: ClosureComponent<void> = () => {
|
||||||
|
let rootElement: HTMLElement;
|
||||||
|
let children: ChildArrayOrPrimitive;
|
||||||
|
|
||||||
|
return {
|
||||||
|
oncreate: (vnode) => {
|
||||||
|
children = vnode.children;
|
||||||
|
rootElement = document.createElement("div");
|
||||||
|
document.body.appendChild(rootElement);
|
||||||
|
mount(rootElement, { view: () => children });
|
||||||
|
},
|
||||||
|
|
||||||
|
onupdate: (vnode) => {
|
||||||
|
children = vnode.children;
|
||||||
|
},
|
||||||
|
|
||||||
|
onremove: () => {
|
||||||
|
if (document.body.contains(rootElement)) {
|
||||||
|
mount(rootElement, null);
|
||||||
|
document.body.removeChild(rootElement);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
view: () => {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DialogAttrs {
|
||||||
|
onClose?: () => void;
|
||||||
|
as: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dialog: ClosureComponent<DialogAttrs> = () => {
|
||||||
|
return {
|
||||||
|
view(vnode) {
|
||||||
|
return m(
|
||||||
|
portal,
|
||||||
|
m(
|
||||||
|
vnode.attrs.as,
|
||||||
|
{ class: vnode.attrs.class, role: "dialog", "aria-modal": "true" },
|
||||||
|
vnode.children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DialogOverlayAttrs {
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dialogOverlay: ClosureComponent<DialogOverlayAttrs> = () => {
|
||||||
|
return {
|
||||||
|
view(vnode) {
|
||||||
|
return m(
|
||||||
|
"div",
|
||||||
|
{ class: vnode.attrs.class, "aria-hidden": "true" },
|
||||||
|
vnode.children,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let transitionShow = false;
|
||||||
|
let transitionTimeout = 0;
|
||||||
|
|
||||||
|
interface TransitionRootAttrs {
|
||||||
|
show: boolean;
|
||||||
|
duration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const transitionRoot: ClosureComponent<TransitionRootAttrs> = (
|
||||||
|
initialVnode,
|
||||||
|
) => {
|
||||||
|
let show = initialVnode.attrs.show;
|
||||||
|
let transitionTimestamp = 0;
|
||||||
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
view(vnode: Vnode<TransitionRootAttrs>) {
|
||||||
|
if (show !== vnode.attrs.show) {
|
||||||
|
const now = Date.now();
|
||||||
|
transitionTimestamp = now;
|
||||||
|
show = vnode.attrs.show;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => m.redraw(), vnode.attrs.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
transitionShow = show;
|
||||||
|
transitionTimeout = Math.max(
|
||||||
|
0,
|
||||||
|
vnode.attrs.duration - (Date.now() - transitionTimestamp),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!transitionShow && !transitionTimeout) return null;
|
||||||
|
|
||||||
|
return vnode.children;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TransitionChildAttrs {
|
||||||
|
enter: string;
|
||||||
|
enterFrom: string;
|
||||||
|
enterTo: string;
|
||||||
|
leave: string;
|
||||||
|
leaveFrom: string;
|
||||||
|
leaveTo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const transitionChild: ClosureComponent<TransitionChildAttrs> = () => {
|
||||||
|
let show: boolean;
|
||||||
|
let timeout: number;
|
||||||
|
let firstFrame: boolean;
|
||||||
|
|
||||||
|
function updateCssClasses(vnode: VnodeDOM<TransitionChildAttrs>): void {
|
||||||
|
const dom = vnode.dom as HTMLElement;
|
||||||
|
const enter = vnode.attrs.enter.split(" ");
|
||||||
|
const enterFrom = vnode.attrs.enterFrom.split(" ");
|
||||||
|
const enterTo = vnode.attrs.enterTo.split(" ");
|
||||||
|
const leave = vnode.attrs.leave.split(" ");
|
||||||
|
const leaveFrom = vnode.attrs.leaveFrom.split(" ");
|
||||||
|
const leaveTo = vnode.attrs.leaveTo.split(" ");
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
dom.classList.remove(...leave, ...leaveFrom, ...leaveTo);
|
||||||
|
if (firstFrame) {
|
||||||
|
dom.classList.add(...enterFrom);
|
||||||
|
void dom.getBoundingClientRect();
|
||||||
|
dom.classList.remove(...enterFrom);
|
||||||
|
}
|
||||||
|
if (timeout) dom.classList.add(...enter);
|
||||||
|
else dom.classList.remove(...enter);
|
||||||
|
dom.classList.add(...enterTo);
|
||||||
|
} else {
|
||||||
|
dom.classList.remove(...enter, ...enterFrom, ...enterTo);
|
||||||
|
if (firstFrame) {
|
||||||
|
dom.classList.add(...leaveFrom);
|
||||||
|
void dom.getBoundingClientRect();
|
||||||
|
dom.classList.remove(...leaveFrom);
|
||||||
|
}
|
||||||
|
if (timeout) dom.classList.add(...leave);
|
||||||
|
else dom.classList.remove(...leave);
|
||||||
|
dom.classList.add(...leaveTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
view(vnode: Vnode<TransitionChildAttrs>) {
|
||||||
|
firstFrame = show !== transitionShow;
|
||||||
|
show = transitionShow;
|
||||||
|
timeout = transitionTimeout;
|
||||||
|
return vnode.children;
|
||||||
|
},
|
||||||
|
|
||||||
|
oncreate(vnode: VnodeDOM<TransitionChildAttrs>) {
|
||||||
|
updateCssClasses(vnode);
|
||||||
|
},
|
||||||
|
|
||||||
|
onupdate(vnode: VnodeDOM<TransitionChildAttrs>) {
|
||||||
|
updateCssClasses(vnode);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IconAttrs {
|
||||||
|
name: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const icon: ClosureComponent<IconAttrs> = () => {
|
||||||
|
return {
|
||||||
|
view(vnode) {
|
||||||
|
return m(
|
||||||
|
`svg`,
|
||||||
|
{
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
fill: "none",
|
||||||
|
stroke: "currentColor",
|
||||||
|
"stroke-width": "2",
|
||||||
|
class: vnode.attrs.class,
|
||||||
|
"aria-hidden": "true",
|
||||||
|
},
|
||||||
|
m("use", { href: `${ICONS_SVG}#icon-${vnode.attrs.name}` }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -110,10 +110,17 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const code = m(codeEditorComponent, attrs);
|
const code = m(codeEditorComponent, attrs);
|
||||||
const submit = m("button.primary", { type: "submit" }, "Save");
|
const submit = m(
|
||||||
|
"button.ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-xs text-sm font-medium rounded-md text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500",
|
||||||
|
{ type: "submit" },
|
||||||
|
"Save",
|
||||||
|
);
|
||||||
|
|
||||||
return m("div.put-form", [
|
return m("div", [
|
||||||
m("h1", `Editing ${name}`),
|
m(
|
||||||
|
"h2.mb-5 text-lg leading-6 font-medium text-stone-900",
|
||||||
|
`Editing ${name}`,
|
||||||
|
),
|
||||||
m(
|
m(
|
||||||
"form",
|
"form",
|
||||||
{
|
{
|
||||||
@@ -128,7 +135,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
.catch(vnode.attrs.onError);
|
.catch(vnode.attrs.onError);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[code, m(".actions-bar", [submit])],
|
[code, m(".flex justify-end mt-5", [submit])],
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import m, { ClosureComponent, Component } from "mithril";
|
|
||||||
import * as store from "./store.ts";
|
|
||||||
import * as notifications from "./notifications.ts";
|
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
|
||||||
return {
|
|
||||||
view: () => {
|
|
||||||
if (window.username) {
|
|
||||||
return m(
|
|
||||||
"div.user-menu",
|
|
||||||
window.username,
|
|
||||||
m(
|
|
||||||
"button",
|
|
||||||
{
|
|
||||||
onclick: (e) => {
|
|
||||||
e.target.disabled = true;
|
|
||||||
store
|
|
||||||
.logOut()
|
|
||||||
.then(() => {
|
|
||||||
location.hash = "";
|
|
||||||
location.reload();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
e.target.disabled = false;
|
|
||||||
notifications.push("error", err.message);
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"Log out",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return m(
|
|
||||||
"div.user-menu",
|
|
||||||
m(
|
|
||||||
"a",
|
|
||||||
{
|
|
||||||
href:
|
|
||||||
"#!/login?" + m.buildQueryString({ continue: m.route.get() }),
|
|
||||||
},
|
|
||||||
"Log in",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default component;
|
|
||||||
+7
-6
@@ -97,6 +97,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
} else if (action === "delete") {
|
} else if (action === "delete") {
|
||||||
|
if (!confirm("Deleting user. Are you sure?")) return void resolve(null);
|
||||||
store
|
store
|
||||||
.deleteResource("users", object["_id"])
|
.deleteResource("users", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -152,7 +153,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter): void {
|
||||||
const ops = { filter };
|
const ops = { filter };
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
m.route.set("/admin/users", ops);
|
m.route.set("/users", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = vnode.attrs["sort"]
|
const sort = vnode.attrs["sort"]
|
||||||
@@ -172,7 +173,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||||
const ops = { sort: JSON.stringify(_sort) };
|
const ops = { sort: JSON.stringify(_sort) };
|
||||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||||
m.route.set("/admin/users", ops);
|
m.route.set("/users", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
let filter = vnode.attrs["filter"]
|
||||||
@@ -211,7 +212,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
attrs["recordActionsCallback"] = (user) => {
|
attrs["recordActionsCallback"] = (user) => {
|
||||||
return [
|
return [
|
||||||
m(
|
m(
|
||||||
"a",
|
"button.text-cyan-700 hover:text-cyan-900 font-medium",
|
||||||
{
|
{
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
let cb: () => Children = null;
|
let cb: () => Children = null;
|
||||||
@@ -296,7 +297,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
||||||
return [
|
return [
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Create new user",
|
title: "Create new user",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
@@ -342,7 +343,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"New",
|
"New",
|
||||||
),
|
),
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Delete selected users",
|
title: "Delete selected users",
|
||||||
disabled: !selected.size,
|
disabled: !selected.size,
|
||||||
@@ -385,7 +386,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing users"),
|
m("h1.text-xl font-medium text-stone-900 mb-5", "Listing users"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(
|
||||||
"loading",
|
"loading",
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
} else if (action === "delete") {
|
} else if (action === "delete") {
|
||||||
|
if (!confirm("Deleting virtual parameter. Are you sure?"))
|
||||||
|
return void resolve(null);
|
||||||
store
|
store
|
||||||
.deleteResource("virtualParameters", object["_id"])
|
.deleteResource("virtualParameters", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -142,7 +144,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter): void {
|
||||||
const ops = { filter };
|
const ops = { filter };
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
m.route.set("/admin/virtualParameters", ops);
|
m.route.set("/virtualParameters", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = vnode.attrs["sort"]
|
const sort = vnode.attrs["sort"]
|
||||||
@@ -159,7 +161,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||||
const ops = { sort: JSON.stringify(_sort) };
|
const ops = { sort: JSON.stringify(_sort) };
|
||||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||||
m.route.set("/admin/virtualParameters", ops);
|
m.route.set("/virtualParameters", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
let filter = vnode.attrs["filter"]
|
||||||
@@ -187,7 +189,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
attrs["recordActionsCallback"] = (virtualParameter) => {
|
attrs["recordActionsCallback"] = (virtualParameter) => {
|
||||||
return [
|
return [
|
||||||
m(
|
m(
|
||||||
"a",
|
"button.text-cyan-700 hover:text-cyan-900 font-medium",
|
||||||
{
|
{
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
let cb: () => Children = null;
|
let cb: () => Children = null;
|
||||||
@@ -239,7 +241,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
||||||
return [
|
return [
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Create new virtual parameter",
|
title: "Create new virtual parameter",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
@@ -285,7 +287,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"New",
|
"New",
|
||||||
),
|
),
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Delete selected virtual parameters",
|
title: "Delete selected virtual parameters",
|
||||||
disabled: !selected.size,
|
disabled: !selected.size,
|
||||||
@@ -330,7 +332,10 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing virtual parameters"),
|
m(
|
||||||
|
"h1.text-xl font-medium text-stone-900 mb-5",
|
||||||
|
"Listing virtual parameters",
|
||||||
|
),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(
|
m(
|
||||||
"loading",
|
"loading",
|
||||||
|
|||||||
+88
-81
@@ -1,4 +1,5 @@
|
|||||||
import m, { ClosureComponent, Component } from "mithril";
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "./components.ts";
|
||||||
import * as notifications from "./notifications.ts";
|
import * as notifications from "./notifications.ts";
|
||||||
|
|
||||||
export async function init(): Promise<Record<string, unknown>> {
|
export async function init(): Promise<Record<string, unknown>> {
|
||||||
@@ -14,93 +15,99 @@ export const component: ClosureComponent = (vnode): Component => {
|
|||||||
view: () => {
|
view: () => {
|
||||||
document.title = "Initialization wizard - GenieACS";
|
document.title = "Initialization wizard - GenieACS";
|
||||||
|
|
||||||
const checkboxes = [
|
const items = [
|
||||||
"users",
|
{ key: "users", label: "Users, roles and permissions" },
|
||||||
"presets",
|
{ key: "presets", label: "Presets and provisions" },
|
||||||
"filters",
|
{ key: "filters", label: "Devices predefined search filters" },
|
||||||
"device",
|
{ key: "device", label: "Device details page" },
|
||||||
"index",
|
{ key: "index", label: "Devices listing page" },
|
||||||
"overview",
|
{ key: "overview", label: "Overview page" },
|
||||||
].map((s) => {
|
];
|
||||||
if (!options[s]) selected.delete(s);
|
|
||||||
return m("input", {
|
|
||||||
type: "checkbox",
|
|
||||||
checked: selected.has(s),
|
|
||||||
disabled: !options[s],
|
|
||||||
style: "display: inline; margin-right: 0.5em;",
|
|
||||||
onclick: (e) => {
|
|
||||||
if (e.target.checked) selected.add(s);
|
|
||||||
else selected.delete(s);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return m(".wizard-dialog", [
|
return [
|
||||||
m("h1", "Initialization wizard"),
|
|
||||||
m(
|
m(
|
||||||
"p",
|
"h1.text-xl font-medium text-stone-900 mb-5",
|
||||||
"This wizard will seed the database with a minimal initial configuration to serve as a starting point. Select what you want to initialize and click 'ABRACADABRA!'.",
|
"Initialization wizard",
|
||||||
),
|
),
|
||||||
m("div", m("label", checkboxes[0], "Users, roles and permissions")),
|
m(".bg-white shadow-sm rounded-lg p-6 sm:p-8 max-w-lg", [
|
||||||
m("div", m("label", checkboxes[1], "Presets and provisions")),
|
m(
|
||||||
m(
|
"p.text-sm text-stone-600 mb-6",
|
||||||
"div",
|
"This wizard will seed the database with a minimal initial configuration to serve as a starting point. Select what you want to initialize and click the button below.",
|
||||||
m("label", checkboxes[2], "Devices predefined search filters"),
|
),
|
||||||
),
|
m(
|
||||||
m("div", m("label", checkboxes[3], "Device details page")),
|
".flex flex-col gap-3 mb-6",
|
||||||
m("div", m("label", checkboxes[4], "Devices listing page")),
|
items.map((item) => {
|
||||||
m("div", m("label", checkboxes[5], "Overview page")),
|
if (!options[item.key]) selected.delete(item.key);
|
||||||
m(
|
return m(
|
||||||
"button.primary",
|
"label.flex items-center text-sm text-stone-700",
|
||||||
{
|
{ class: options[item.key] ? "" : "opacity-50" },
|
||||||
style: "margin: 10px;",
|
m(
|
||||||
disabled: selected.size === 0,
|
"input.focus:ring-cyan-500 h-4 w-4 text-cyan-700 border-stone-300 rounded-sm",
|
||||||
onclick: (e) => {
|
{
|
||||||
e.target.disabled = true;
|
type: "checkbox",
|
||||||
|
checked: selected.has(item.key),
|
||||||
|
disabled: !options[item.key],
|
||||||
|
onclick: (e) => {
|
||||||
|
if (e.target.checked) selected.add(item.key);
|
||||||
|
else selected.delete(item.key);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
m("span.ml-2", item.label),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"button.inline-flex justify-center py-2 px-4 border border-transparent shadow-xs text-sm font-medium rounded-md text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
|
{
|
||||||
|
disabled: selected.size === 0,
|
||||||
|
onclick: (e) => {
|
||||||
|
e.target.disabled = true;
|
||||||
|
|
||||||
const opts = {};
|
const opts = {};
|
||||||
for (const s of selected) opts[s] = true;
|
for (const s of selected) opts[s] = true;
|
||||||
|
|
||||||
m.request({
|
m.request({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "init",
|
url: "init",
|
||||||
body: opts,
|
body: opts,
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
m.request({ url: "init" })
|
|
||||||
.then((o) => {
|
|
||||||
e.target.disabled = false;
|
|
||||||
options = o;
|
|
||||||
notifications.push(
|
|
||||||
"success",
|
|
||||||
"Initialization complete",
|
|
||||||
{
|
|
||||||
"Open Sesame!": () => {
|
|
||||||
m.route.set("/login");
|
|
||||||
window.location.reload();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
notifications.push("error", err.message);
|
|
||||||
});
|
|
||||||
}, 3000);
|
|
||||||
if (opts["users"]) {
|
|
||||||
alert(
|
|
||||||
"An administrator user has been created for you. Use admin/admin to log in. Don't forget to change the default password.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.then(() => {
|
||||||
notifications.push("error", err.message);
|
setTimeout(() => {
|
||||||
});
|
m.request({ url: "init" })
|
||||||
|
.then((o) => {
|
||||||
|
e.target.disabled = false;
|
||||||
|
options = o;
|
||||||
|
notifications.push(
|
||||||
|
"success",
|
||||||
|
"Initialization complete",
|
||||||
|
{
|
||||||
|
"Open Sesame!": () => {
|
||||||
|
m.route.set("/login");
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
if (opts["users"]) {
|
||||||
|
alert(
|
||||||
|
"An administrator user has been created for you. Use admin/admin to log in. Don't forget to change the default password.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
"ABRACADABRA!",
|
||||||
"ABRACADABRA!",
|
),
|
||||||
),
|
]),
|
||||||
]);
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { parse, stringify } from "yaml";
|
||||||
Reference in new issue
Block a user