From 991ca16e03e560bafba3f0bc2cd90a61c9ecdab5 Mon Sep 17 00:00:00 2001 From: Shweelan Samson Date: Wed, 12 Sep 2018 02:28:39 +0300 Subject: [PATCH] Add sort to index pages --- client/css/app.css | 1 + client/devices-page.js | 64 +++++++++++++++++++++--- client/faults-page.js | 101 +++++++++++++++++++++++++++----------- client/presets-page.js | 72 ++++++++++++++++++++++----- client/provisions-page.js | 58 +++++++++++++++++++--- client/store.js | 4 +- common/expression.js | 10 ++++ config.json | 3 +- 8 files changed, 257 insertions(+), 56 deletions(-) diff --git a/client/css/app.css b/client/css/app.css index d0cbff3..0486562 100644 --- a/client/css/app.css +++ b/client/css/app.css @@ -269,6 +269,7 @@ table.table { } & th { + white-space: nowrap; color: var(--color4); font-weight: bold; border-bottom: 2px solid var(--color4); diff --git a/client/devices-page.js b/client/devices-page.js index fa1ae09..c8cc7fd 100644 --- a/client/devices-page.js +++ b/client/devices-page.js @@ -14,6 +14,17 @@ import memoize from "../common/memoize"; const PAGE_SIZE = config.ui.pageSize || 10; const memoizedParse = memoize(expression.parse); +const memoizedJsonParse = memoize(JSON.parse); +const memoizedGetSortable = memoize(str => { + let expressionParams = expression.extractParams( + expression.parse(str) + ); + if (expressionParams.length === 1) { + let param = expression.evaluate(expressionParams[0]); + if (typeof param === "string") return param; + } + return null; +}); const getDownloadUrl = memoize((filter, indexParameters) => { const columns = {}; @@ -34,8 +45,9 @@ function init(args) { return reject(new Error("You are not authorized to view this page")); const filter = args.filter; + const sort = args.sort; const indexParameters = Object.values(config.ui.index); - resolve({ filter, indexParameters }); + resolve({ filter, indexParameters, sort }); }); } @@ -45,7 +57,9 @@ function renderTable( total, selected, showMoreCallback, - downloadUrl + downloadUrl, + sort, + onSortChange ) { const devices = devicesResponse.value; const selectAll = m("input", { @@ -60,7 +74,32 @@ function renderTable( }); let labels = [m("th", selectAll)]; - for (let param of parameters) labels.push(m("th", param.label)); + for (let param of parameters) { + let label = param.label; + let _param; + if (!param.unsortable && (_param = memoizedGetSortable(param.parameter))) { + let direction = 1; + + let symbol = "\u21f3"; + if (sort[_param] > 0) symbol = "\u2b07"; + else if (sort[_param] < 0) symbol = "\u2b06"; + + let sortable = m( + "button", + { + onclick: () => { + if (sort[_param] > 0) direction *= -1; + return onSortChange(JSON.stringify({ [_param]: direction })); + } + }, + symbol + ); + + labels.push(m("th", [label, sortable])); + } else { + labels.push(m("th", label)); + } + } let rows = []; for (let device of devices) { @@ -296,14 +335,25 @@ const component = { } function onFilterChanged(filter) { - m.route.set("/devices", { filter }); + let ops = { filter }; + if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; + m.route.set("/devices", ops); } + function onSortChange(sort) { + let ops = { sort }; + if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; + m.route.set("/devices", ops); + } + + const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; const filter = vnode.attrs.filter ? memoizedParse(vnode.attrs.filter) : true; + let devs = store.fetch("devices", filter, { - limit: vnode.state.showCount || PAGE_SIZE + limit: vnode.state.showCount || PAGE_SIZE, + sort: sort }); let count = store.count("devices", filter); @@ -332,7 +382,9 @@ const component = { count.value, selected, showMore, - downloadUrl + downloadUrl, + sort, + onSortChange ), renderActions(selected) ]; diff --git a/client/faults-page.js b/client/faults-page.js index a70298a..9d090a6 100644 --- a/client/faults-page.js +++ b/client/faults-page.js @@ -11,17 +11,25 @@ import memoize from "../common/memoize"; const PAGE_SIZE = config.ui.pageSize || 10; const memoizedParse = memoize(expression.parse); +const memoizedJsonParse = memoize(JSON.parse); + +const attributes = [ + { id: "device", label: "Device" }, + { id: "channel", label: "Channel" }, + { id: "code", label: "Code" }, + { id: "retries", label: "Retries" }, + { id: "timestamp", label: "Timestamp" } +]; const getDownloadUrl = memoize(filter => { + let cols = {}; + for (let attr of attributes) + cols[attr.label] = + attr.id === "timestamp" ? `DATE_STRING(${attr.id})` : attr.id; + return `/api/faults.csv?${m.buildQueryString({ filter: filter, - columns: JSON.stringify({ - Device: "device", - Channel: "channel", - Code: "code", - Retries: "retries", - Timestamp: "DATE_STRING(timestamp)" - }) + columns: JSON.stringify(cols) })}`; }); @@ -31,8 +39,9 @@ function init(args) { new Error("You are not authorized to view this page") ); + const sort = args.sort; const filter = args.filter; - return Promise.resolve({ filter }); + return Promise.resolve({ filter, sort }); } function renderTable( @@ -40,7 +49,9 @@ function renderTable( total, selected, showMoreCallback, - downloadUrl + downloadUrl, + sort, + onSortChange ) { const faults = faultsResponse.value; const selectAll = m("input", { @@ -54,15 +65,29 @@ function renderTable( disabled: !total }); - const labels = [ - selectAll, - "Device", - "Channel", - "Code", - "Message", - "Retries", - "Timestamp" - ].map(l => m("th", l)); + const labels = [m("th", selectAll)]; + for (let attr of attributes) { + let label = attr.label; + + let direction = 1; + + let symbol = "\u21f3"; + if (sort[attr.id] > 0) symbol = "\u2b07"; + else if (sort[attr.id] < 0) symbol = "\u2b06"; + + let sortable = m( + "button", + { + onclick: () => { + if (sort[attr.id] > 0) direction *= -1; + return onSortChange(JSON.stringify({ [attr.id]: direction })); + } + }, + symbol + ); + + labels.push(m("th", [label, sortable])); + } let rows = []; for (let f of faults) { @@ -81,6 +106,14 @@ function renderTable( const deviceHref = `#!/devices/${encodeURIComponent(f["device"])}`; + let tds = [m("td", checkbox)]; + for (let attr of attributes) + if (attr.id === "device") + tds.push(m("a", { href: deviceHref }, f[attr.id])); + else if (attr.id === "timestamp") + tds.push(m("td", new Date(f[attr.id]).toLocaleString())); + else tds.push(m("td", f[attr.id])); + rows.push( m( "tr", @@ -94,13 +127,7 @@ function renderTable( if (!selected.delete(f["_id"])) selected.add(f["_id"]); } }, - m("td", checkbox), - m("td", m("a", { href: deviceHref }, f["device"])), - m("td", f["channel"]), - m("td", f["code"]), - m("td", f["message"]), - m("td", f["retries"]), - m("td", new Date(f["timestamp"]).toLocaleString()) + tds ) ); } @@ -180,15 +207,25 @@ const component = { } function onFilterChanged(filter) { - m.route.set("/faults", { filter }); + let ops = { filter }; + if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; + m.route.set("/faults", ops); } + function onSortChange(sort) { + let ops = { sort }; + if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; + m.route.set("/faults", ops); + } + + const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; const filter = vnode.attrs.filter ? memoizedParse(vnode.attrs.filter) : true; let faults = store.fetch("faults", filter, { - limit: vnode.state.showCount || PAGE_SIZE + limit: vnode.state.showCount || PAGE_SIZE, + sort: sort }); let count = store.count("faults", filter); @@ -213,7 +250,15 @@ const component = { filter: vnode.attrs.filter, onChange: onFilterChanged }), - renderTable(faults, count.value, selected, showMore, downloadUrl) + renderTable( + faults, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) ]; } }; diff --git a/client/presets-page.js b/client/presets-page.js index 1cb2644..badffb1 100644 --- a/client/presets-page.js +++ b/client/presets-page.js @@ -14,16 +14,17 @@ import memoize from "../common/memoize"; const PAGE_SIZE = config.ui.pageSize || 10; const memoizedParse = memoize(expression.parse); +const memoizedJsonParse = memoize(JSON.parse); const attributes = [ { id: "_id", label: "Name" }, { id: "channel", label: "Channel" }, { id: "weight", label: "Weight" }, { id: "schedule", label: "Schedule" }, - { id: "events", label: "Events" }, - { id: "precondition", label: "Precondition" }, - { id: "provision", label: "Provision", type: "combo" }, - { id: "provisionArgs", label: "Arguments" } + { id: "events", label: "Events", unsortable: true }, + { id: "precondition", label: "Precondition", unsortable: true }, + { id: "provision", label: "Provision", type: "combo", unsortable: true }, + { id: "provisionArgs", label: "Arguments", unsortable: true } ]; function putActoinHandler(action, object, isNew) { @@ -99,8 +100,9 @@ function init(args) { new Error("You are not authorized to view this page") ); + const sort = args.sort; const filter = args.filter; - return Promise.resolve({ filter }); + return Promise.resolve({ filter, sort }); } function renderTable( @@ -108,7 +110,9 @@ function renderTable( total, selected, showMoreCallback, - downloadUrl + downloadUrl, + sort, + onSortChange ) { const presets = presetsResponse.value; const selectAll = m("input", { @@ -122,9 +126,35 @@ function renderTable( disabled: !total }); - const labels = [selectAll] - .concat(attributes.map(elem => elem.label)) - .map(l => m("th", l)); + const labels = [m("th", selectAll)]; + + for (let attr of attributes) { + let label = attr.label; + + if (attr.unsortable) { + labels.push(m("th", label)); + continue; + } + + let direction = 1; + + let symbol = "\u21f3"; + if (sort[attr.id] > 0) symbol = "\u2b07"; + else if (sort[attr.id] < 0) symbol = "\u2b06"; + + let sortable = m( + "button", + { + onclick: () => { + if (sort[attr.id] > 0) direction *= -1; + return onSortChange(JSON.stringify({ [attr.id]: direction })); + } + }, + symbol + ); + + labels.push(m("th", [label, sortable])); + } let rows = []; for (let preset of presets) { @@ -310,15 +340,25 @@ const component = { } function onFilterChanged(filter) { - m.route.set("/presets", { filter }); + let ops = { filter }; + if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; + m.route.set("/presets", ops); } + function onSortChange(sort) { + let ops = { sort }; + if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; + m.route.set("/presets", ops); + } + + const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; const filter = vnode.attrs.filter ? memoizedParse(vnode.attrs.filter) : true; let presets = store.fetch("presets", filter, { - limit: vnode.state.showCount || PAGE_SIZE + limit: vnode.state.showCount || PAGE_SIZE, + sort: sort }); let count = store.count("presets", filter); @@ -352,7 +392,15 @@ const component = { filter: vnode.attrs.filter, onChange: onFilterChanged }), - renderTable(presets, count.value, selected, showMore, downloadUrl) + renderTable( + presets, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) ]; } }; diff --git a/client/provisions-page.js b/client/provisions-page.js index ee2586b..b11e89f 100644 --- a/client/provisions-page.js +++ b/client/provisions-page.js @@ -14,6 +14,7 @@ import * as overlay from "./overlay"; const PAGE_SIZE = config.ui.pageSize || 10; const memoizedParse = memoize(expression.parse); +const memoizedJsonParse = memoize(JSON.parse); const attributes = [ { id: "_id", label: "Name" }, @@ -93,8 +94,9 @@ function init(args) { new Error("You are not authorized to view this page") ); + const sort = args.sort; const filter = args.filter; - return Promise.resolve({ filter }); + return Promise.resolve({ filter, sort }); } function renderTable( @@ -102,7 +104,9 @@ function renderTable( total, selected, showMoreCallback, - downloadUrl + downloadUrl, + sort, + onSortChange ) { const provisions = provisionsResponse.value; const selectAll = m("input", { @@ -116,9 +120,29 @@ function renderTable( disabled: !total }); - const labels = [selectAll] - .concat(attributes.map(elem => elem.label)) - .map(l => m("th", l)); + const labels = [m("th", selectAll)]; + for (let attr of attributes) { + let label = attr.label; + + let direction = 1; + + let symbol = "\u21f3"; + if (sort[attr.id] > 0) symbol = "\u2b07"; + else if (sort[attr.id] < 0) symbol = "\u2b06"; + + let sortable = m( + "button", + { + onclick: () => { + if (sort[attr.id] > 0) direction *= -1; + return onSortChange(JSON.stringify({ [attr.id]: direction })); + } + }, + symbol + ); + + labels.push(m("th", [label, sortable])); + } let rows = []; for (let provision of provisions) { @@ -297,15 +321,25 @@ const component = { } function onFilterChanged(filter) { - m.route.set("/provisions", { filter }); + let ops = { filter }; + if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; + m.route.set("/provisions", ops); } + function onSortChange(sort) { + let ops = { sort }; + if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; + m.route.set("/provisions", ops); + } + + const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; const filter = vnode.attrs.filter ? memoizedParse(vnode.attrs.filter) : true; let provisions = store.fetch("provisions", filter, { - limit: vnode.state.showCount || PAGE_SIZE + limit: vnode.state.showCount || PAGE_SIZE, + sort: sort }); let count = store.count("provisions", filter); @@ -326,7 +360,15 @@ const component = { filter: vnode.attrs.filter, onChange: onFilterChanged }), - renderTable(provisions, count.value, selected, showMore, downloadUrl) + renderTable( + provisions, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) ]; } }; diff --git a/client/store.js b/client/store.js index 0a51b94..f6c7ba5 100644 --- a/client/store.js +++ b/client/store.js @@ -198,7 +198,9 @@ function inferQuery(resourceType, queryResponse) { function fetch(resourceType, filter, options = {}) { const filterStr = memoizedStringify(filter); - const sort = options.sort || {}; + const sort = Object.assign({}, options.sort); + for (const [k, v] of Object.entries(sort)) sort[k] += Math.sign(v); + const limit = options.limit || 0; if (resourceType === "devices") sort["DeviceID.ID"] = sort["DeviceID.ID"] || 1; diff --git a/common/expression.js b/common/expression.js index cf61052..376fce3 100644 --- a/common/expression.js +++ b/common/expression.js @@ -237,6 +237,15 @@ function likePatternToRegExp(pat, esc = "", flags = "") { return new RegExp(chars.join(""), flags); } +function extractParams(exp) { + let params = []; + expressionParser.map(exp, e => { + if (isArray(e) && e[0] === "PARAM") params.push(e[1]); + return e; + }); + return params; +} + exports.evaluate = evaluate; exports.and = and; exports.or = or; @@ -245,3 +254,4 @@ exports.subset = subset; exports.parse = expressionParser.parse; exports.stringify = expressionParser.stringify; exports.likePatternToRegExp = likePatternToRegExp; +exports.extractParams = extractParams; diff --git a/config.json b/config.json index 169ea95..1130e67 100644 --- a/config.json +++ b/config.json @@ -132,7 +132,8 @@ "label": "Tags", "parameter": "Tags", "type": "tags", - "writable": false + "writable": false, + "unsortable": true } ], "overview": {