Show reload notification on config update
This commit is contained in:
1 parent
6c60ce6457
commit
83dba52e96
7 files changed
+105
-20
No files matched your search
@@ -41,6 +41,7 @@ koa.on("error", async err => {
|
|||||||
koa.use(async (ctx, next) => {
|
koa.use(async (ctx, next) => {
|
||||||
const configSnapshot = await localCache.getCurrentSnapshot();
|
const configSnapshot = await localCache.getCurrentSnapshot();
|
||||||
ctx.state.configSnapshot = configSnapshot;
|
ctx.state.configSnapshot = configSnapshot;
|
||||||
|
ctx.set("X-Config-Snapshot", configSnapshot);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,6 +138,7 @@ router.get("/", async ctx => {
|
|||||||
window.clientConfig = ${JSON.stringify({
|
window.clientConfig = ${JSON.stringify({
|
||||||
ui: localCache.getUiConfig(ctx.state.configSnapshot)
|
ui: localCache.getUiConfig(ctx.state.configSnapshot)
|
||||||
})};
|
})};
|
||||||
|
window.configSnapshot = ${JSON.stringify(ctx.state.configSnapshot)};
|
||||||
window.username = ${JSON.stringify(
|
window.username = ${JSON.stringify(
|
||||||
ctx.state.user ? ctx.state.user.username : ""
|
ctx.state.user ? ctx.state.user.username : ""
|
||||||
)};
|
)};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ declare global {
|
|||||||
permissionSets: any;
|
permissionSets: any;
|
||||||
username: string;
|
username: string;
|
||||||
clientConfig: any;
|
clientConfig: any;
|
||||||
|
configSnapshot: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export default window.clientConfig;
|
export default window.clientConfig;
|
||||||
|
export const configSnapshot = window.configSnapshot;
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
--status-red: hsl(0, 80%, 80%);
|
--status-red: hsl(0, 80%, 80%);
|
||||||
--status-green: hsl(125, 80%, 80%);
|
--status-green: hsl(125, 80%, 80%);
|
||||||
--status-gray: hsl(58, 3%, 80%);
|
--status-gray: hsl(58, 3%, 80%);
|
||||||
|
--status-yellow: hsl(46, 79%, 80%);
|
||||||
}
|
}
|
||||||
|
|
||||||
body,
|
body,
|
||||||
@@ -201,6 +202,10 @@ i {
|
|||||||
background-color: color(var(--status-green) a(90%));
|
background-color: color(var(--status-green) a(90%));
|
||||||
border: 1px solid var(--status-green);
|
border: 1px solid var(--status-green);
|
||||||
}
|
}
|
||||||
|
&.warning {
|
||||||
|
background-color: color(var(--status-yellow) a(90%));
|
||||||
|
border: 1px solid var(--status-yellow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -290,6 +290,14 @@ function renderNotifications(notifs): Child[] {
|
|||||||
const notificationElements: Child[] = [];
|
const notificationElements: Child[] = [];
|
||||||
|
|
||||||
for (const n of notifs) {
|
for (const n of notifs) {
|
||||||
|
let buttons;
|
||||||
|
if (n.actions) {
|
||||||
|
const btns = Object.entries(n.actions).map(([label, onclick]) =>
|
||||||
|
m("button.primary", { onclick: onclick }, label)
|
||||||
|
);
|
||||||
|
if (btns.length) buttons = m("div", { style: "float: right" }, btns);
|
||||||
|
}
|
||||||
|
|
||||||
notificationElements.push(
|
notificationElements.push(
|
||||||
m(
|
m(
|
||||||
"div.notification",
|
"div.notification",
|
||||||
@@ -309,7 +317,7 @@ function renderNotifications(notifs): Child[] {
|
|||||||
},
|
},
|
||||||
key: n.timestamp
|
key: n.timestamp
|
||||||
},
|
},
|
||||||
n.message
|
m("div", buttons, n.message)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-3
@@ -4,18 +4,32 @@ interface Notification {
|
|||||||
type: string;
|
type: string;
|
||||||
message: string;
|
message: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
actions?: { [label: string]: () => void };
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifications = new Set<Notification>();
|
const notifications = new Set<Notification>();
|
||||||
|
|
||||||
export function push(type, message): void {
|
export function push(type, message, actions?): Notification {
|
||||||
const n = { type: type, message: message, timestamp: Date.now() };
|
const n: Notification = {
|
||||||
|
type: type,
|
||||||
|
message: message,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
actions: actions
|
||||||
|
};
|
||||||
notifications.add(n);
|
notifications.add(n);
|
||||||
m.redraw();
|
m.redraw();
|
||||||
|
if (!actions) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
dismiss(n);
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dismiss(n: Notification): void {
|
||||||
notifications.delete(n);
|
notifications.delete(n);
|
||||||
m.redraw();
|
m.redraw();
|
||||||
}, 4000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNotifications(): Set<Notification> {
|
export function getNotifications(): Set<Notification> {
|
||||||
|
|||||||
+70
-16
@@ -3,11 +3,14 @@ import { map, stringify } from "../lib/common/expression-parser";
|
|||||||
import { or, and, not, evaluate, subset } from "../lib/common/expression";
|
import { or, and, not, evaluate, subset } from "../lib/common/expression";
|
||||||
import memoize from "../lib/common/memoize";
|
import memoize from "../lib/common/memoize";
|
||||||
import { QueryOptions, Expression } from "../lib/types";
|
import { QueryOptions, Expression } from "../lib/types";
|
||||||
|
import * as notifications from "./notifications";
|
||||||
|
import { configSnapshot } from "./config";
|
||||||
|
|
||||||
const memoizedStringify = memoize(stringify);
|
const memoizedStringify = memoize(stringify);
|
||||||
const memoizedEvaluate = memoize(evaluate);
|
const memoizedEvaluate = memoize(evaluate);
|
||||||
|
|
||||||
let fulfillTimestamp = 0;
|
let fulfillTimestamp = 0;
|
||||||
|
let refreshNotification;
|
||||||
|
|
||||||
const queries = {
|
const queries = {
|
||||||
filter: new WeakMap(),
|
filter: new WeakMap(),
|
||||||
@@ -67,6 +70,59 @@ class QueryResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function xhrRequest(
|
||||||
|
options: { url: string } & m.RequestOptions<{}>
|
||||||
|
): Promise<any> {
|
||||||
|
const extract = options.extract;
|
||||||
|
const deserialize = options.deserialize;
|
||||||
|
|
||||||
|
options.extract = (
|
||||||
|
xhr: XMLHttpRequest,
|
||||||
|
_options?: { url: string } & m.RequestOptions<{}>
|
||||||
|
): any => {
|
||||||
|
if (
|
||||||
|
!refreshNotification &&
|
||||||
|
configSnapshot !== xhr.getResponseHeader("x-config-snapshot")
|
||||||
|
) {
|
||||||
|
refreshNotification = notifications.push(
|
||||||
|
"warning",
|
||||||
|
"Configuration has been modified, please reload the page",
|
||||||
|
{
|
||||||
|
Reload: () => {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof extract === "function") return extract(xhr, _options);
|
||||||
|
|
||||||
|
let response: any;
|
||||||
|
if (typeof deserialize === "function") {
|
||||||
|
response = deserialize(xhr.responseText);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
response = xhr.responseText ? JSON.parse(xhr.responseText) : null;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error("Invalid JSON: " + response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://mithril.js.org/request.html#error-handling
|
||||||
|
if (xhr.status !== 304 && Math.floor(xhr.status / 100) !== 2) {
|
||||||
|
const err = new Error();
|
||||||
|
err["message"] = xhr.responseText;
|
||||||
|
err["code"] = xhr.status;
|
||||||
|
err["response"] = response;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
return m.request(options);
|
||||||
|
}
|
||||||
|
|
||||||
export function unpackExpression(exp): Expression {
|
export function unpackExpression(exp): Expression {
|
||||||
if (!Array.isArray(exp)) return exp;
|
if (!Array.isArray(exp)) return exp;
|
||||||
const e = memoizedEvaluate(exp, null, fulfillTimestamp);
|
const e = memoizedEvaluate(exp, null, fulfillTimestamp);
|
||||||
@@ -271,7 +327,7 @@ export function fulfill(accessTimestamp, _fulfillTimestamp): Promise<boolean> {
|
|||||||
updated = true;
|
updated = true;
|
||||||
let filter = queries.filter.get(queryResponse);
|
let filter = queries.filter.get(queryResponse);
|
||||||
filter = unpackExpression(filter);
|
filter = unpackExpression(filter);
|
||||||
m.request({
|
xhrRequest({
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
url:
|
url:
|
||||||
`/api/${resourceType}/?` +
|
`/api/${resourceType}/?` +
|
||||||
@@ -317,7 +373,7 @@ export function fulfill(accessTimestamp, _fulfillTimestamp): Promise<boolean> {
|
|||||||
updated = true;
|
updated = true;
|
||||||
let filter = queries.filter.get(queryResponse);
|
let filter = queries.filter.get(queryResponse);
|
||||||
filter = unpackExpression(filter);
|
filter = unpackExpression(filter);
|
||||||
m.request({
|
xhrRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url:
|
url:
|
||||||
`/api/${resourceType}/?` +
|
`/api/${resourceType}/?` +
|
||||||
@@ -410,7 +466,7 @@ export function fulfill(accessTimestamp, _fulfillTimestamp): Promise<boolean> {
|
|||||||
|
|
||||||
allPromises2.push(
|
allPromises2.push(
|
||||||
new Promise((resolve2, reject2) => {
|
new Promise((resolve2, reject2) => {
|
||||||
m.request({
|
xhrRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url:
|
url:
|
||||||
`/api/${resourceType}/?` +
|
`/api/${resourceType}/?` +
|
||||||
@@ -473,8 +529,7 @@ export function postTasks(deviceId, tasks): Promise<string> {
|
|||||||
t.device = deviceId;
|
t.device = deviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return xhrRequest({
|
||||||
m.request({
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `/api/devices/${encodeURIComponent(deviceId)}/tasks`,
|
url: `/api/devices/${encodeURIComponent(deviceId)}/tasks`,
|
||||||
data: tasks,
|
data: tasks,
|
||||||
@@ -489,14 +544,13 @@ export function postTasks(deviceId, tasks): Promise<string> {
|
|||||||
tasks[i].status = t.status;
|
tasks[i].status = t.status;
|
||||||
tasks[i].fault = t.fault;
|
tasks[i].fault = t.fault;
|
||||||
}
|
}
|
||||||
resolve(connectionRequestStatus);
|
return connectionRequestStatus;
|
||||||
}
|
}
|
||||||
}).catch(reject);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateTags(deviceId, tags): Promise<void> {
|
export function updateTags(deviceId, tags): Promise<void> {
|
||||||
return m.request({
|
return xhrRequest({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `/api/devices/${encodeURIComponent(deviceId)}/tags`,
|
url: `/api/devices/${encodeURIComponent(deviceId)}/tags`,
|
||||||
data: tags
|
data: tags
|
||||||
@@ -504,7 +558,7 @@ export function updateTags(deviceId, tags): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function deleteResource(resourceType, id): Promise<void> {
|
export function deleteResource(resourceType, id): Promise<void> {
|
||||||
return m.request({
|
return xhrRequest({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: `/api/${resourceType}/${encodeURIComponent(id)}`
|
url: `/api/${resourceType}/${encodeURIComponent(id)}`
|
||||||
});
|
});
|
||||||
@@ -513,7 +567,7 @@ export function deleteResource(resourceType, id): Promise<void> {
|
|||||||
export function putResource(resourceType, id, object): Promise<void> {
|
export function putResource(resourceType, id, object): Promise<void> {
|
||||||
for (const k in object) if (object[k] === undefined) object[k] = null;
|
for (const k in object) if (object[k] === undefined) object[k] = null;
|
||||||
|
|
||||||
return m.request({
|
return xhrRequest({
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
url: `/api/${resourceType}/${encodeURIComponent(id)}`,
|
url: `/api/${resourceType}/${encodeURIComponent(id)}`,
|
||||||
data: object
|
data: object
|
||||||
@@ -522,7 +576,7 @@ export function putResource(resourceType, id, object): Promise<void> {
|
|||||||
|
|
||||||
export function queryConfig(pattern = "%"): Promise<any[]> {
|
export function queryConfig(pattern = "%"): Promise<any[]> {
|
||||||
const filter = stringify(["LIKE", ["PARAM", "_id"], pattern]);
|
const filter = stringify(["LIKE", ["PARAM", "_id"], pattern]);
|
||||||
return m.request({
|
return xhrRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `api/config/?${m.buildQueryString({ filter: filter })}`,
|
url: `api/config/?${m.buildQueryString({ filter: filter })}`,
|
||||||
background: true
|
background: true
|
||||||
@@ -532,7 +586,7 @@ export function queryConfig(pattern = "%"): Promise<any[]> {
|
|||||||
export function resourceExists(resource, id): Promise<number> {
|
export function resourceExists(resource, id): Promise<number> {
|
||||||
const param = resource === "devices" ? "DeviceID.ID" : "_id";
|
const param = resource === "devices" ? "DeviceID.ID" : "_id";
|
||||||
const filter = ["=", ["PARAM", param], id];
|
const filter = ["=", ["PARAM", param], id];
|
||||||
return m.request({
|
return xhrRequest({
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
url:
|
url:
|
||||||
`/api/${resource}/?` +
|
`/api/${resource}/?` +
|
||||||
@@ -556,7 +610,7 @@ export function changePassword(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const data = { newPassword };
|
const data = { newPassword };
|
||||||
if (authPassword) data["authPassword"] = authPassword;
|
if (authPassword) data["authPassword"] = authPassword;
|
||||||
return m.request({
|
return xhrRequest({
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
url: `/api/users/${username}/password`,
|
url: `/api/users/${username}/password`,
|
||||||
background: true,
|
background: true,
|
||||||
@@ -565,7 +619,7 @@ export function changePassword(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function logIn(username, password): Promise<void> {
|
export function logIn(username, password): Promise<void> {
|
||||||
return m.request({
|
return xhrRequest({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/login",
|
url: "/login",
|
||||||
background: true,
|
background: true,
|
||||||
@@ -574,14 +628,14 @@ export function logIn(username, password): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function logOut(): Promise<void> {
|
export function logOut(): Promise<void> {
|
||||||
return m.request({
|
return xhrRequest({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/logout"
|
url: "/logout"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ping(host): Promise<{}> {
|
export function ping(host): Promise<{}> {
|
||||||
return m.request({
|
return xhrRequest({
|
||||||
url: `/api/ping/${encodeURIComponent(host)}`
|
url: `/api/ping/${encodeURIComponent(host)}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user