Show reload notification on config update

This commit is contained in:
Shweelan Samson authored and Zaid Abdulla committed 2019-06-05 00:17:36 -07:00
1 parent 6c60ce6457
commit 83dba52e96
7 files changed
+105 -20

No files matched your search

+2
View File
@@ -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 : ""
)}; )};
+1
View File
@@ -24,6 +24,7 @@ declare global {
permissionSets: any; permissionSets: any;
username: string; username: string;
clientConfig: any; clientConfig: any;
configSnapshot: string;
} }
} }
+1
View File
@@ -1 +1,2 @@
export default window.clientConfig; export default window.clientConfig;
export const configSnapshot = window.configSnapshot;
+5
View File
@@ -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);
}
} }
} }
} }
+9 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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)}`
}); });
} }