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
@@ -3,7 +3,7 @@ import { m } from "../components.ts";
|
||||
import * as taskQueue from "../task-queue.ts";
|
||||
import { parse } from "../../lib/common/expression/parser.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 debounce from "../../lib/common/debounce.ts";
|
||||
import { Expression } from "../../lib/types.ts";
|
||||
@@ -55,14 +55,17 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
const limit =
|
||||
(evaluateExpression(vnode.attrs.limit, device) as number) || 100;
|
||||
|
||||
const search = m("input", {
|
||||
type: "text",
|
||||
placeholder: "Search parameters",
|
||||
oninput: (e) => {
|
||||
formQueryString(e.target.value);
|
||||
e.redraw = false;
|
||||
const search = m(
|
||||
"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",
|
||||
{
|
||||
type: "text",
|
||||
placeholder: "Search parameters",
|
||||
oninput: (e) => {
|
||||
formQueryString(e.target.value);
|
||||
e.redraw = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const instanceRegex = /\.[0-9]+$/;
|
||||
let re;
|
||||
@@ -92,8 +95,6 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
const rows = filteredKeys.map((k) => {
|
||||
const p = device[k];
|
||||
const val = [];
|
||||
const attrs = { key: k };
|
||||
|
||||
if (p.object === false) {
|
||||
val.push(
|
||||
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 {
|
||||
@@ -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(
|
||||
"tr",
|
||||
attrs,
|
||||
m("td.left", m("long-text", { text: k })),
|
||||
m("td.right", val),
|
||||
m("td.pl-4 pr-2 py-2 truncate", m("long-text", { text: k })),
|
||||
m("td.pr-4 py-2 text-right flex justify-end", val),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -168,25 +179,37 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
"loading",
|
||||
{ queries: [vnode.attrs.deviceQuery] },
|
||||
m(
|
||||
".all-parameters",
|
||||
m(
|
||||
"a.download-csv",
|
||||
{
|
||||
href: `api/devices/${encodeURIComponent(
|
||||
device["DeviceID.ID"].value[0],
|
||||
)}.csv`,
|
||||
download: "",
|
||||
style: "float: right;",
|
||||
},
|
||||
"Download",
|
||||
),
|
||||
".bg-white shadow-sm rounded-lg",
|
||||
search,
|
||||
m(
|
||||
".parameter-list",
|
||||
m("table", m("tbody", rows)),
|
||||
".overflow-hidden",
|
||||
m(
|
||||
"m",
|
||||
`Displaying ${filteredKeys.length} out of ${count} parameters.`,
|
||||
".overflow-y-scroll h-96 shadow-inner",
|
||||
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(
|
||||
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",
|
||||
onclick: () => {
|
||||
@@ -29,7 +29,7 @@ const component: ClosureComponent = (): Component => {
|
||||
|
||||
buttons.push(
|
||||
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",
|
||||
onclick: () => {
|
||||
@@ -45,7 +45,7 @@ const component: ClosureComponent = (): Component => {
|
||||
|
||||
buttons.push(
|
||||
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",
|
||||
onclick: () => {
|
||||
@@ -61,7 +61,7 @@ const component: ClosureComponent = (): Component => {
|
||||
|
||||
buttons.push(
|
||||
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",
|
||||
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 * as store from "../store.ts";
|
||||
import * as notifications from "../notifications.ts";
|
||||
import { getIcon } from "../icons.ts";
|
||||
import { stringify } from "../../lib/common/yaml.ts";
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
@@ -23,63 +22,111 @@ const component: ClosureComponent = (): Component => {
|
||||
"Detail",
|
||||
"Retries",
|
||||
"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 = [];
|
||||
for (const f of faults.value) {
|
||||
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()),
|
||||
rows.push(
|
||||
m(
|
||||
"td",
|
||||
"tr",
|
||||
m(
|
||||
"button",
|
||||
{
|
||||
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());
|
||||
});
|
||||
"td.whitespace-nowrap pl-6 pr-3 py-4 text-sm text-stone-900",
|
||||
f["channel"],
|
||||
),
|
||||
m(
|
||||
"td.whitespace-nowrap px-3 py-4 text-sm text-stone-900",
|
||||
f["code"],
|
||||
),
|
||||
m(
|
||||
"td.whitespace-nowrap px-3 py-4 text-sm text-stone-900",
|
||||
m("long-text", { text: f["message"], class: "max-w-xs" }),
|
||||
),
|
||||
m(
|
||||
"td.whitespace-nowrap px-3 py-4 text-sm text-stone-900",
|
||||
m("long-text", {
|
||||
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());
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
getIcon("remove"),
|
||||
"Delete",
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
);
|
||||
}
|
||||
|
||||
let tbody;
|
||||
if (rows.length) {
|
||||
tbody = m(
|
||||
"tbody",
|
||||
rows.map((r) => m("tr", r)),
|
||||
);
|
||||
} else {
|
||||
tbody = m(
|
||||
"tbody",
|
||||
m("tr.empty", m("td", { colspan: headers.length }, "No faults")),
|
||||
if (!rows.length) {
|
||||
rows.push(
|
||||
m(
|
||||
"tr",
|
||||
m(
|
||||
"td.bg-stripes text-sm font-medium text-center text-stone-500 p-4",
|
||||
{ colspan: headers.length },
|
||||
"No faults",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return m(
|
||||
"loading",
|
||||
{ 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) {
|
||||
return m(
|
||||
"a",
|
||||
"a.text-cyan-700 hover:text-cyan-900 font-medium",
|
||||
{ href: `#!/devices/${encodeURIComponent(deviceId)}` },
|
||||
children,
|
||||
);
|
||||
|
||||
@@ -16,25 +16,22 @@ const component: ClosureComponent = (): Component => {
|
||||
const filter = slice["filter"];
|
||||
if (evaluateExpression(filter, device || {})) {
|
||||
const dot = m(
|
||||
"svg",
|
||||
"svg.inline",
|
||||
{
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
style: "margin: 0 0.2em 0.2em",
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
||||
},
|
||||
m("circle", {
|
||||
m("circle.stroke-stone-200 stroke-1", {
|
||||
cx: "0.5em",
|
||||
cy: "0.5em",
|
||||
r: "0.4em",
|
||||
fill: evaluateExpression(slice["color"], null),
|
||||
}),
|
||||
);
|
||||
return m(
|
||||
"span.overview-dot",
|
||||
dot,
|
||||
evaluateExpression(slice["label"], null),
|
||||
);
|
||||
return m("span", dot, evaluateExpression(slice["label"], null));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -30,7 +30,7 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
);
|
||||
|
||||
return m(
|
||||
"tr",
|
||||
"div.py-3 grid grid-cols-3 gap-4 px-6",
|
||||
{
|
||||
oncreate: (vn) => {
|
||||
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
|
||||
@@ -43,15 +43,22 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
: "none";
|
||||
},
|
||||
},
|
||||
m("th", evaluateExpression(parameter.label, device)),
|
||||
m("td", p),
|
||||
m(
|
||||
"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(
|
||||
"loading",
|
||||
{ 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 { QueryResponse, evaluateExpression } from "../store.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 { Expression } from "../../lib/types.ts";
|
||||
|
||||
@@ -42,11 +42,24 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const headers = Object.values(parameters).map((p) =>
|
||||
m("th", evaluateExpression(p["label"], device)),
|
||||
);
|
||||
const headers = Object.values(parameters).map((p, i) => {
|
||||
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 = [];
|
||||
for (const i of instances) {
|
||||
@@ -60,7 +73,9 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
|
||||
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) => {
|
||||
if (Array.isArray(e) && e[0] === "PARAM")
|
||||
return ["PARAM", ["||", i, ".", e[1]]];
|
||||
@@ -68,6 +83,9 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
});
|
||||
return m(
|
||||
"td",
|
||||
{
|
||||
class: "whitespace-nowrap py-4 text-sm text-stone-900 " + padding,
|
||||
},
|
||||
m.context(
|
||||
{
|
||||
device: device,
|
||||
@@ -87,6 +105,10 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
row.push(
|
||||
m(
|
||||
"td",
|
||||
{
|
||||
class:
|
||||
"whitespace-nowrap pl-3 pr-6 py-4 text-sm text-stone-900",
|
||||
},
|
||||
m(
|
||||
"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));
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
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(
|
||||
"loading",
|
||||
{ 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 memoize from "../../lib/common/memoize.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) => {
|
||||
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}` });
|
||||
|
||||
return m(
|
||||
"span",
|
||||
"span.inline-flex overflow-hidden align-top",
|
||||
{
|
||||
class: "parameter-value",
|
||||
onmouseover: (e) => {
|
||||
e.redraw = false;
|
||||
// Don't update any child element
|
||||
@@ -104,7 +106,7 @@ const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
},
|
||||
},
|
||||
el,
|
||||
m("span.truncate", el),
|
||||
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"];
|
||||
|
||||
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",
|
||||
onclick: (e) => {
|
||||
e.target.disabled = true;
|
||||
|
||||
+40
-9
@@ -2,7 +2,7 @@ import { ClosureComponent } from "mithril";
|
||||
import { m } from "../components.ts";
|
||||
import * as notifications from "../notifications.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 { Expression } from "../../lib/types.ts";
|
||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||
@@ -28,20 +28,36 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
|
||||
if (!writable) {
|
||||
return m(
|
||||
".tags",
|
||||
tags.map((t) => m("span.tag", t)),
|
||||
"div",
|
||||
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(
|
||||
".tags",
|
||||
"div",
|
||||
tags.map((tag) =>
|
||||
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,
|
||||
m(
|
||||
"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) => {
|
||||
e.target.disabled = true;
|
||||
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(
|
||||
"span.tag.writable",
|
||||
m.trust(" "),
|
||||
"span",
|
||||
{
|
||||
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(
|
||||
"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) => {
|
||||
e.target.disabled = true;
|
||||
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",
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user