Display spinner animation during loading

This commit is contained in:
Zaid Abdulla committed 2021-02-16 00:09:24 -08:00
1 parent 4871613c67
commit 688811317c
23 files changed
+447 -307

No files matched your search

+5 -18
View File
@@ -64,39 +64,27 @@ const adminPages = [
let state;
let lastRenderTimestamp = 0;
let pageVisitTimestamp = 0;
let fulfillTimeout = null;
function fulfill(): void {
clearTimeout(fulfillTimeout);
fulfillTimeout = setTimeout(() => {
store.fulfill(lastRenderTimestamp, pageVisitTimestamp, (updated) => {
if (updated) m.redraw();
});
}, 100);
}
function pagify(pageName, page): RouteResolver {
const component: RouteResolver = {
render: () => {
lastRenderTimestamp = Date.now();
const lastRenderTimestamp = Date.now();
let p;
if (state && state.error) p = m(errorPage.component, state);
else p = m(contextifyComponent(page.component), state);
fulfill();
const attrs = {};
attrs["page"] = pageName;
attrs["oncreate"] = attrs["onupdate"] = () => {
store.fulfill(lastRenderTimestamp);
};
return m(layout, attrs, p);
},
onmatch: null,
};
component.onmatch = (args, requestedPath) => {
pageVisitTimestamp = Date.now();
store.setTimestamp(Date.now());
if (!page.init) {
state = null;
fulfill();
return null;
}
@@ -107,7 +95,6 @@ function pagify(pageName, page): RouteResolver {
if (!st) return void m.route.set("/");
state = st;
resolve();
fulfill();
})
.catch((err) => {
if (!window.username && err.message.indexOf("authorized") >= 0) {
+2
View File
@@ -39,6 +39,7 @@ import tags from "./components/tags";
import ping from "./components/ping";
import deviceLink from "./components/device-link";
import longTextComponent from "./long-text-component";
import loading from "./components/loading";
const comps = {
parameter,
@@ -54,6 +55,7 @@ const comps = {
ping,
"device-link": deviceLink,
"long-text": longTextComponent,
loading: loading,
};
const contextifiedComponents = new WeakMap<ComponentTypes, ComponentTypes>();
+21 -17
View File
@@ -171,25 +171,29 @@ const component: ClosureComponent = (): Component => {
});
return m(
".all-parameters",
"loading",
{ queries: [vnode.attrs["deviceQuery"]] },
m(
"a.download-csv",
{
href: `api/devices/${encodeURIComponent(
device["DeviceID.ID"].value[0]
)}.csv`,
download: "",
style: "float: right;",
},
"Download"
),
search,
m(
".parameter-list",
m("table", m("tbody", rows)),
".all-parameters",
m(
"m",
`Displaying ${filteredKeys.length} out of ${count} parameters.`
"a.download-csv",
{
href: `api/devices/${encodeURIComponent(
device["DeviceID.ID"].value[0]
)}.csv`,
download: "",
style: "float: right;",
},
"Download"
),
search,
m(
".parameter-list",
m("table", m("tbody", rows)),
m(
"m",
`Displaying ${filteredKeys.length} out of ${count} parameters.`
)
)
)
);
+7 -3
View File
@@ -67,12 +67,12 @@ const component: ClosureComponent = (): Component => {
.deleteResource("faults", f["_id"])
.then(() => {
notifications.push("success", "Fault deleted");
store.fulfill(Date.now(), Date.now());
store.setTimestamp(Date.now());
m.redraw();
})
.catch((err) => {
notifications.push("error", err.message);
store.fulfill(Date.now(), Date.now());
store.setTimestamp(Date.now());
});
},
},
@@ -95,7 +95,11 @@ const component: ClosureComponent = (): Component => {
);
}
return m("table.table", thead, tbody);
return m(
"loading",
{ queries: [faults] },
m("table.table", thead, tbody)
);
},
};
};
+80
View File
@@ -0,0 +1,80 @@
/**
* Copyright 2013-2021 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import { VnodeDOM, ClosureComponent, Component } from "mithril";
const component: ClosureComponent = (): Component => {
let overlay: HTMLElement;
let dom: Element;
let loading = false;
function apply(vnode: VnodeDOM): void {
if (!loading) {
if (overlay) overlay.parentElement.remove();
if (dom) dom.classList.remove("loading");
overlay = null;
dom = null;
return;
}
if (dom && dom !== vnode.dom) dom.classList.remove("loading");
dom = vnode.dom;
dom.classList.add("loading");
if (!overlay) {
const wrapper = document.createElement("div");
wrapper.style.position = "relative";
wrapper.style.pointerEvents = "none";
overlay = document.createElement("div");
overlay.classList.add("loading-overlay");
overlay.style.position = "absolute";
wrapper.appendChild(overlay);
}
const wrapper = overlay.parentElement;
if (wrapper.parentElement !== dom.parentElement)
dom.parentNode.appendChild(wrapper);
const wrapperRect = wrapper.getBoundingClientRect();
const domRect = dom.getBoundingClientRect();
overlay.style.width = `${dom.scrollWidth}px`;
overlay.style.height = `${dom.scrollHeight}px`;
overlay.style.left = `${domRect.left - wrapperRect.left}px`;
overlay.style.top = `${domRect.top - wrapperRect.top}px`;
}
return {
view: (vnode) => {
const queries = vnode.attrs["queries"];
loading = queries.some((q) => q.fulfilling);
return vnode.children;
},
oncreate: apply,
onupdate: apply,
onremove: () => {
if (overlay) overlay.parentElement.remove();
if (dom) dom.classList.remove("loading");
overlay = null;
dom = null;
},
};
};
export default component;
+10 -1
View File
@@ -38,6 +38,11 @@ const component: ClosureComponent = (): Component => {
return m(
"tr",
{
oncreate: (vn) => {
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
? ""
: "none";
},
onupdate: (vn) => {
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
? ""
@@ -49,7 +54,11 @@ const component: ClosureComponent = (): Component => {
);
});
return m("table.parameter-list", rows);
return m(
"loading",
{ queries: [vnode.attrs["deviceQuery"]] },
m("table.parameter-list", rows)
);
},
};
};
+8 -1
View File
@@ -149,7 +149,14 @@ const component: ClosureComponent = (): Component => {
let label;
if (vnode.attrs["label"]) label = m("h2", vnode.attrs["label"]);
return [label, m("table.table", thead, m("tbody", rows))];
return [
label,
m(
"loading",
{ queries: [vnode.attrs["deviceQuery"]] },
m("table.table", thead, m("tbody", rows))
),
];
},
};
};
+1 -1
View File
@@ -78,7 +78,7 @@ const component: ClosureComponent = (): Component => {
)
.then(() => {
e.target.disabled = false;
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
})
.catch((err) => {
e.target.disabled = false;
+2 -2
View File
@@ -64,7 +64,7 @@ const component: ClosureComponent = (): Component => {
"success",
`${deviceId}: Tags updated`
);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
})
.catch((err) => {
e.target.disabled = false;
@@ -99,7 +99,7 @@ const component: ClosureComponent = (): Component => {
.then(() => {
e.target.disabled = false;
notifications.push("success", `${deviceId}: Tags updated`);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
})
.catch((err) => {
e.target.disabled = false;
+18 -14
View File
@@ -60,12 +60,12 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
.resourceExists("config", id)
.then((exists) => {
if (exists && isNew) {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
return void resolve({ _id: "Config already exists" });
}
if (!exists && !isNew) {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
return void resolve({ _id: "Config does not exist" });
}
@@ -76,7 +76,7 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
"success",
`Config ${exists ? "updated" : "created"}`
);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
})
.catch(reject);
@@ -87,11 +87,11 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
.deleteResource("config", object["_id"])
.then(() => {
notifications.push("success", "Config deleted");
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
})
.catch((err) => {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
reject(err);
});
} else {
@@ -337,11 +337,11 @@ export const component: ClosureComponent = (): Component => {
);
overlay.close(cb);
}
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
},
onError: (err) => {
notifications.push("error", err.message);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
overlay.close(cb);
},
},
@@ -366,14 +366,18 @@ export const component: ClosureComponent = (): Component => {
return [
m("h1", "Listing config"),
m(
".all-parameters",
search,
"loading",
{ queries: [confs] },
m(
".parameter-list",
{ style: "height: 400px" },
renderTable(confs, vnode.state["searchString"])
),
m(".actions-bar", [newConfig].concat(subs))
".all-parameters",
search,
m(
".parameter-list",
{ style: "height: 400px" },
renderTable(confs, vnode.state["searchString"])
),
m(".actions-bar", [newConfig].concat(subs))
)
),
];
},
+28 -1
View File
@@ -347,7 +347,6 @@ body {
table.table {
border-spacing: 0;
text-align: left;
display: block;
overflow-x: auto;
& table.table {
@@ -753,3 +752,31 @@ span.parameter-value {
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%;
}
}
+16 -4
View File
@@ -42,13 +42,25 @@ export const component: ClosureComponent = (): Component => {
view: (vnode) => {
document.title = `${vnode.attrs["deviceId"]} - Devices - GenieACS`;
const dev = store.fetch("devices", vnode.attrs["deviceFilter"]).value;
if (!dev.length) return "Loading";
const dev = store.fetch("devices", vnode.attrs["deviceFilter"]);
if (!dev.value.length) {
if (!dev.fulfilling)
return m("p.error", `No such device ${vnode.attrs["deviceId"]}`);
return m(
"loading",
{ queries: [dev] },
m("div", { style: "height: 100px;" })
);
}
const conf = config.ui.device;
const cmps = [];
for (const c of Object.values(conf))
cmps.push(m.context({ device: dev[0] }, c["type"], c));
for (const c of Object.values(conf)) {
cmps.push(
m.context({ device: dev.value[0], deviceQuery: dev }, c["type"], c)
);
}
return [m("h1", vnode.attrs["deviceId"]), cmps];
},
+10 -10
View File
@@ -154,14 +154,14 @@ function renderActions(selected: Set<string>): Children {
.deleteResource("devices", id)
.then(() => {
notifications.push("success", `${id}: Deleted`);
if (--counter === 0) store.fulfill(0, Date.now());
if (--counter === 0) store.setTimestamp(Date.now());
})
.catch((err) => {
notifications.push("error", `${id}: ${err.message}`);
if (--counter === 0) store.fulfill(0, Date.now());
if (--counter === 0) store.setTimestamp(Date.now());
});
}
if (--counter === 0) store.fulfill(0, Date.now());
if (--counter === 0) store.setTimestamp(Date.now());
},
},
"Delete"
@@ -186,14 +186,14 @@ function renderActions(selected: Set<string>): Children {
.updateTags(id, { [tag]: true })
.then(() => {
notifications.push("success", `${id}: Tags updated`);
if (--counter === 0) store.fulfill(0, Date.now());
if (--counter === 0) store.setTimestamp(Date.now());
})
.catch((err) => {
notifications.push("error", `${id}: ${err.message}`);
if (--counter === 0) store.fulfill(0, Date.now());
if (--counter === 0) store.setTimestamp(Date.now());
});
}
if (--counter === 0) store.fulfill(0, Date.now());
if (--counter === 0) store.setTimestamp(Date.now());
},
},
"Tag"
@@ -220,14 +220,14 @@ function renderActions(selected: Set<string>): Children {
.updateTags(id, { [tag]: false })
.then(() => {
notifications.push("success", `${id}: Tags updated`);
if (--counter === 0) store.fulfill(0, Date.now());
if (--counter === 0) store.setTimestamp(Date.now());
})
.catch((err) => {
notifications.push("error", `${id}: ${err.message}`);
if (--counter === 0) store.fulfill(0, Date.now());
if (--counter === 0) store.setTimestamp(Date.now());
});
}
if (--counter === 0) store.fulfill(0, Date.now());
if (--counter === 0) store.setTimestamp(Date.now());
},
},
"Untag"
@@ -336,7 +336,7 @@ export const component: ClosureComponent = (): Component => {
return [
m("h1", "Listing devices"),
m(filterComponent, filterAttrs),
m(indexTableComponent, attrs),
m("loading", { queries: [devs, count] }, m(indexTableComponent, attrs)),
];
},
};
+1 -1
View File
@@ -465,7 +465,7 @@ const component: ClosureComponent = (): Component => {
}
)
.then(() => {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
})
.catch((err) => {
notifications.push("error", err.message);
+7 -3
View File
@@ -187,11 +187,11 @@ export const component: ClosureComponent = (): Component => {
"success",
`${res.length} faults deleted`
);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
})
.catch((err) => {
notifications.push("error", err.message);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
});
},
},
@@ -208,7 +208,11 @@ export const component: ClosureComponent = (): Component => {
return [
m("h1", "Listing faults"),
m(filterComponent, filterAttrs),
m(indexTableComponent, attrs),
m(
"loading",
{ queries: [faults, count] },
m(indexTableComponent, attrs)
),
];
},
};
+9 -5
View File
@@ -93,7 +93,7 @@ function putActionHandler(action, _object): Promise<ValidationErrors> {
.resourceExists("files", id)
.then((exists) => {
if (exists) {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
return void resolve({ file: "File already exists" });
}
const headers = Object.assign(
@@ -117,7 +117,7 @@ function putActionHandler(action, _object): Promise<ValidationErrors> {
"success",
`File ${exists ? "updated" : "created"}`
);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
})
.catch(reject);
@@ -288,11 +288,11 @@ export const component: ClosureComponent = (): Component => {
"success",
`${res.length} files deleted`
);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
})
.catch((err) => {
notifications.push("error", err.message);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
});
},
},
@@ -310,7 +310,11 @@ export const component: ClosureComponent = (): Component => {
return [
m("h1", "Listing files"),
m(filterComponent, filterAttrs),
m(indexTableComponent, attrs),
m(
"loading",
{ queries: [files, count] },
m(indexTableComponent, attrs)
),
];
},
};
+11 -7
View File
@@ -117,7 +117,7 @@ function putActionHandler(action, _object): Promise<ValidationErrors> {
.resourceExists("permissions", id)
.then((exists) => {
if (exists) {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
return void resolve({ _id: "Permission already exists" });
}
@@ -125,7 +125,7 @@ function putActionHandler(action, _object): Promise<ValidationErrors> {
.putResource("permissions", id, object)
.then(() => {
notifications.push("success", "Permission created");
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
})
.catch(reject);
@@ -136,11 +136,11 @@ function putActionHandler(action, _object): Promise<ValidationErrors> {
.deleteResource("permissions", object["_id"])
.then(() => {
notifications.push("success", "Permission deleted");
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
})
.catch((err) => {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
reject(err);
});
} else {
@@ -351,11 +351,11 @@ export const component: ClosureComponent = (): Component => {
"success",
`${res.length} permissions deleted`
);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
})
.catch((err) => {
notifications.push("error", err.message);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
});
},
},
@@ -373,7 +373,11 @@ export const component: ClosureComponent = (): Component => {
return [
m("h1", "Listing permissions"),
m(filterComponent, filterAttrs),
m(indexTableComponent, attrs),
m(
"loading",
{ queries: [permissions, count] },
m(indexTableComponent, attrs)
),
];
},
};
+21 -15
View File
@@ -116,21 +116,27 @@ function drawChart(chartData): Children {
legend.push(m("span.legend-total", `Total: ${total}`));
return m("div", { class: "pie-chart" }, [
m(
"svg",
{
// Adding 2 as padding; strokes must not be more than 2
viewBox: "-102 -102 204 204",
width: "204px",
height: "204px",
xmlns: "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink",
},
paths.concat(links)
),
m(".legend", legend),
]);
return m(
"loading",
{
queries: Object.values(chartData.slices).map((s) => s["count"]),
},
m("div", { class: "pie-chart" }, [
m(
"svg",
{
// Adding 2 as padding; strokes must not be more than 2
viewBox: "-102 -102 204 204",
width: "204px",
height: "204px",
xmlns: "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink",
},
paths.concat(links)
),
m(".legend", legend),
])
);
}
const component: ClosureComponent = (): Component => {
+12 -8
View File
@@ -86,12 +86,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.resourceExists("presets", id)
.then((exists) => {
if (exists && isNew) {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
return void resolve({ _id: "Preset already exists" });
}
if (!exists && !isNew) {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
return void resolve({ _id: "Preset does not exist" });
}
@@ -102,7 +102,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
"success",
`Preset ${exists ? "updated" : "created"}`
);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
})
.catch(reject);
@@ -113,12 +113,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.deleteResource("presets", object["_id"])
.then(() => {
notifications.push("success", "Preset deleted");
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
})
.catch((err) => {
reject(err);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
});
} else {
reject(new Error("Undefined action"));
@@ -413,11 +413,11 @@ export const component: ClosureComponent = (): Component => {
"success",
`${res.length} presets deleted`
);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
})
.catch((err) => {
notifications.push("error", err.message);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
});
},
},
@@ -435,7 +435,11 @@ export const component: ClosureComponent = (): Component => {
return [
m("h1", "Listing presets"),
m(filterComponent, filterAttrs),
m(indexTableComponent, attrs),
m(
"loading",
{ queries: [presets, count] },
m(indexTableComponent, attrs)
),
];
},
};
+12 -8
View File
@@ -66,12 +66,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.resourceExists("provisions", id)
.then((exists) => {
if (exists && isNew) {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
return void resolve({ _id: "Provision already exists" });
}
if (!exists && !isNew) {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
return void resolve({ _id: "Provision does not exist" });
}
@@ -82,7 +82,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
"success",
`Provision ${exists ? "updated" : "created"}`
);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
})
.catch((err) => {
@@ -99,11 +99,11 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.deleteResource("provisions", object["_id"])
.then(() => {
notifications.push("success", "Provision deleted");
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
})
.catch((err) => {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
reject(err);
});
} else {
@@ -332,11 +332,11 @@ export const component: ClosureComponent = (): Component => {
"success",
`${res.length} provisions deleted`
);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
})
.catch((err) => {
notifications.push("error", err.message);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
});
},
},
@@ -354,7 +354,11 @@ export const component: ClosureComponent = (): Component => {
return [
m("h1", "Listing provisions"),
m(filterComponent, filterAttrs),
m(indexTableComponent, attrs),
m(
"loading",
{ queries: [provisions, count] },
m(indexTableComponent, attrs)
),
];
},
};
+141 -171
View File
@@ -76,14 +76,14 @@ for (const r of [
}
class QueryResponse {
public get fulfilled(): boolean {
public get fulfilled(): number {
queries.accessed.set(this, Date.now());
return !!queries.fulfilled.get(this);
return queries.fulfilled.get(this) || 0;
}
public get fulfilling(): boolean {
queries.accessed.set(this, Date.now());
return queries.fulfilling.has(this);
return !(queries.fulfilled.get(this) >= fulfillTimestamp);
}
public get value(): any {
@@ -348,18 +348,7 @@ export function fetch(
return queryResponse;
}
function _fulfill(
accessTimestamp: number,
_fulfillTimestamp: number
): Promise<boolean> {
let updated = false;
if (_fulfillTimestamp > fulfillTimestamp) {
for (const resource of Object.values(resources))
resource.combinedFilter = null;
fulfillTimestamp = _fulfillTimestamp;
}
export function fulfill(accessTimestamp: number): void {
const allPromises = [];
for (const [resourceType, resource] of Object.entries(resources)) {
@@ -373,38 +362,32 @@ function _fulfill(
if (!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))) {
queries.fulfilling.add(queryResponse);
let filter = queries.filter.get(queryResponse);
filter = unpackExpression(filter);
allPromises.push(
new Promise((resolve, reject) => {
updated = true;
let filter = queries.filter.get(queryResponse);
filter = unpackExpression(filter);
xhrRequest({
method: "HEAD",
url:
`api/${resourceType}/?` +
m.buildQueryString({
filter: memoizedStringify(filter),
}),
extract: (xhr) => {
if (xhr.status === 403) throw new Error("Not authorized");
if (!xhr.status) {
throw new Error("Server is unreachable");
} else if (xhr.status !== 200) {
throw new Error(
`Unexpected response status code ${xhr.status}`
);
}
return +xhr.getResponseHeader("x-total-count");
},
background: true,
})
.then((c) => {
queries.value.set(queryResponse, c);
queries.fulfilled.set(queryResponse, fulfillTimestamp);
queries.fulfilling.delete(queryResponse);
resolve();
})
.catch((err) => reject(err));
xhrRequest({
method: "HEAD",
url:
`api/${resourceType}/?` +
m.buildQueryString({
filter: memoizedStringify(filter),
}),
extract: (xhr) => {
if (xhr.status === 403) throw new Error("Not authorized");
if (!xhr.status) {
throw new Error("Server is unreachable");
} else if (xhr.status !== 200) {
throw new Error(
`Unexpected response status code ${xhr.status}`
);
}
return +xhr.getResponseHeader("x-total-count");
},
background: false,
}).then((c) => {
queries.value.set(queryResponse, c);
queries.fulfilled.set(queryResponse, fulfillTimestamp);
queries.fulfilling.delete(queryResponse);
})
);
}
@@ -429,45 +412,39 @@ function _fulfill(
const limit = queries.limit.get(queryResponse);
const sort = queries.sort.get(queryResponse);
if (limit) {
let filter = queries.filter.get(queryResponse);
filter = unpackExpression(filter);
allPromises.push(
new Promise((resolve, reject) => {
updated = true;
let filter = queries.filter.get(queryResponse);
filter = unpackExpression(filter);
xhrRequest({
method: "GET",
url:
`api/${resourceType}/?` +
m.buildQueryString({
filter: memoizedStringify(filter),
limit: 1,
skip: limit - 1,
sort: JSON.stringify(sort),
projection: Object.keys(sort).join(","),
}),
})
.then((res) => {
if ((res as any[]).length) {
// Generate bookmark object
const bm = Object.keys(sort).reduce((b, k) => {
if (res[0][k] != null) {
if (typeof res[0][k] === "object") {
if (res[0][k].value != null)
b[k] = res[0][k].value[0];
} else {
b[k] = res[0][k];
}
}
return b;
}, {});
queries.bookmark.set(queryResponse, bm);
} else {
queries.bookmark.delete(queryResponse);
xhrRequest({
method: "GET",
url:
`api/${resourceType}/?` +
m.buildQueryString({
filter: memoizedStringify(filter),
limit: 1,
skip: limit - 1,
sort: JSON.stringify(sort),
projection: Object.keys(sort).join(","),
}),
background: true,
}).then((res) => {
if ((res as any[]).length) {
// Generate bookmark object
const bm = Object.keys(sort).reduce((b, k) => {
if (res[0][k] != null) {
if (typeof res[0][k] === "object") {
if (res[0][k].value != null) b[k] = res[0][k].value[0];
} else {
b[k] = res[0][k];
}
}
resolve();
})
.catch(reject);
return b;
}, {});
queries.bookmark.set(queryResponse, bm);
} else {
queries.bookmark.delete(queryResponse);
}
})
);
}
@@ -475,31 +452,82 @@ function _fulfill(
}
}
return new Promise((resolve, reject) => {
Promise.all(allPromises)
.then(() => {
const allPromises2 = [];
for (const [resourceType, toFetch] of Object.entries(toFetchAll)) {
let combinedFilter = null;
Promise.all(allPromises)
.then(() => {
let updated = false;
const allPromises2 = [];
for (const [resourceType, toFetch] of Object.entries(toFetchAll)) {
let combinedFilter = null;
for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse);
filter = memoizedEvaluate(filter, null, fulfillTimestamp);
const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse);
if (bookmark) filter = limitFilter(filter, sort, bookmark);
combinedFilter = or(combinedFilter, filter);
}
const [union, diff] = unionDiff(
resources[resourceType].combinedFilter,
combinedFilter
);
if (!diff) {
for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse);
filter = memoizedEvaluate(filter, null, fulfillTimestamp);
const limit = queries.limit.get(queryResponse);
const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse);
if (bookmark) filter = limitFilter(filter, sort, bookmark);
combinedFilter = or(combinedFilter, filter);
queries.value.set(
queryResponse,
findMatches(resourceType, filter, sort, limit)
);
queries.fulfilled.set(queryResponse, fulfillTimestamp);
queries.fulfilling.delete(queryResponse);
updated = true;
}
continue;
}
const [union, diff] = unionDiff(
resources[resourceType].combinedFilter,
combinedFilter
);
let deleted = new Set<string>();
if (!resources[resourceType].combinedFilter)
deleted = new Set(resources[resourceType].objects.keys());
const combinedFilterDiff = diff;
resources[resourceType].combinedFilter = union;
allPromises2.push(
xhrRequest({
method: "GET",
url:
`api/${resourceType}/?` +
m.buildQueryString({
filter: memoizedStringify(combinedFilterDiff),
}),
background: false,
}).then((res) => {
for (const r of res as any[]) {
const id =
resourceType === "devices"
? r["DeviceID.ID"].value[0]
: r["_id"];
resources[resourceType].objects.set(id, r);
deleted.delete(id);
}
for (const d of deleted) {
const obj = resources[resourceType].objects.get(d);
if (evaluate(combinedFilterDiff, obj, fulfillTimestamp))
resources[resourceType].objects.delete(d);
}
if (!diff) {
for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse);
filter = memoizedEvaluate(filter, null, fulfillTimestamp);
filter = unpackExpression(filter);
const limit = queries.limit.get(queryResponse);
const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse);
@@ -512,87 +540,29 @@ function _fulfill(
queries.fulfilled.set(queryResponse, fulfillTimestamp);
queries.fulfilling.delete(queryResponse);
}
continue;
}
updated = true;
let deleted = new Set<string>();
if (!resources[resourceType].combinedFilter)
deleted = new Set(resources[resourceType].objects.keys());
const combinedFilterDiff = diff;
resources[resourceType].combinedFilter = union;
allPromises2.push(
new Promise((resolve2, reject2) => {
xhrRequest({
method: "GET",
url:
`api/${resourceType}/?` +
m.buildQueryString({
filter: memoizedStringify(combinedFilterDiff),
}),
})
.then((res) => {
for (const r of res as any[]) {
const id =
resourceType === "devices"
? r["DeviceID.ID"].value[0]
: r["_id"];
resources[resourceType].objects.set(id, r);
deleted.delete(id);
}
for (const d of deleted) {
const obj = resources[resourceType].objects.get(d);
if (evaluate(combinedFilterDiff, obj, fulfillTimestamp))
resources[resourceType].objects.delete(d);
}
for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse);
filter = unpackExpression(filter);
const limit = queries.limit.get(queryResponse);
const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse);
if (bookmark) filter = limitFilter(filter, sort, bookmark);
queries.value.set(
queryResponse,
findMatches(resourceType, filter, sort, limit)
);
queries.fulfilled.set(queryResponse, fulfillTimestamp);
queries.fulfilling.delete(queryResponse);
}
resolve2();
})
.catch(reject2);
})
);
}
Promise.all(allPromises2)
.then(() => resolve(updated))
.catch(reject);
})
.catch(reject);
});
}
export function fulfill(
accessTimestamp: number,
_fulfillTimestamp: number,
callback?: (updated: boolean) => void
): void {
_fulfill(accessTimestamp, _fulfillTimestamp).then(callback, (err) => {
notifications.push("error", err.message);
if (callback) callback(false);
});
})
);
}
if (updated) m.redraw();
return Promise.all(allPromises2);
})
.catch((err) => {
notifications.push("error", err.message);
});
}
export function getTimestamp(): number {
return fulfillTimestamp;
}
export function setTimestamp(t: number): void {
if (t > fulfillTimestamp) {
fulfillTimestamp = t;
for (const resource of Object.values(resources))
resource.combinedFilter = null;
}
}
export function postTasks(
deviceId: string,
tasks: QueueTask[]
+13 -9
View File
@@ -85,12 +85,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.resourceExists("users", id)
.then((exists) => {
if (exists && isNew) {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
return void resolve({ _id: "User already exists" });
}
if (!exists && !isNew) {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
return void resolve({ _id: "User does not exist" });
}
@@ -102,13 +102,13 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.changePassword(id, password)
.then(() => {
notifications.push("success", "User created");
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
})
.catch(reject);
} else {
notifications.push("success", "User updated");
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
}
})
@@ -120,11 +120,11 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.deleteResource("users", object["_id"])
.then(() => {
notifications.push("success", "User deleted");
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
})
.catch((err) => {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
reject(err);
});
} else {
@@ -387,11 +387,11 @@ export const component: ClosureComponent = (): Component => {
"success",
`${res.length} users deleted`
);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
})
.catch((err) => {
notifications.push("error", err.message);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
});
},
},
@@ -409,7 +409,11 @@ export const component: ClosureComponent = (): Component => {
return [
m("h1", "Listing users"),
m(filterComponent, filterAttrs),
m(indexTableComponent, attrs),
m(
"loading",
{ queries: [users, count] },
m(indexTableComponent, attrs)
),
];
},
};
+12 -8
View File
@@ -66,12 +66,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.resourceExists("virtualParameters", id)
.then((exists) => {
if (exists && isNew) {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
return void resolve({ _id: "Virtual parameter already exists" });
}
if (!exists && !isNew) {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
return void resolve({ _id: "Virtual parameter does not exist" });
}
@@ -82,7 +82,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
"success",
`Virtual parameter ${exists ? "updated" : "created"}`
);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
})
.catch((err) => {
@@ -99,11 +99,11 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.deleteResource("virtualParameters", object["_id"])
.then(() => {
notifications.push("success", "Virtual parameter deleted");
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
resolve();
})
.catch((err) => {
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
reject(err);
});
} else {
@@ -332,11 +332,11 @@ export const component: ClosureComponent = (): Component => {
"success",
`${res.length} virtual parameters deleted`
);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
})
.catch((err) => {
notifications.push("error", err.message);
store.fulfill(0, Date.now());
store.setTimestamp(Date.now());
});
},
},
@@ -354,7 +354,11 @@ export const component: ClosureComponent = (): Component => {
return [
m("h1", "Listing virtual parameters"),
m(filterComponent, filterAttrs),
m(indexTableComponent, attrs),
m(
"loading",
{ queries: [virtualParameters, count] },
m(indexTableComponent, attrs)
),
];
},
};