From 7d9f31012fecc38ca80ec0bd32b9d310ffbb0560 Mon Sep 17 00:00:00 2001 From: Jordi Date: Mon, 23 Apr 2018 13:22:46 +0200 Subject: [PATCH 01/12] Add option to show logs from previous pages in the logs dialog It introduces two new variables to the status: logsFilter: function that selects the relevant logs (we use it to show only the logs that include the selected datasets). logsPageLast: index of the last page that we have retrieved so far. The Log dialog now contains a new action, a flat button that allows it to load logs from previous pages. There are new semantics for the getLogs() function: if it returns null, then there are no more log pages (so we do not need to request more). Also, the pages are a positive number, that is, page 1 is the page previous to the last one (instead of it being page -1). --- src/DataSets/DataSets.component.js | 79 +++++++++++++++++------------- src/DataSets/log.js | 15 ++++-- src/i18n/i18n_module_ar.properties | 1 + src/i18n/i18n_module_en.properties | 1 + src/i18n/i18n_module_es.properties | 1 + src/i18n/i18n_module_fr.properties | 1 + 6 files changed, 59 insertions(+), 39 deletions(-) diff --git a/src/DataSets/DataSets.component.js b/src/DataSets/DataSets.component.js index 2007b9e0d..1025704ac 100644 --- a/src/DataSets/DataSets.component.js +++ b/src/DataSets/DataSets.component.js @@ -93,6 +93,8 @@ const DataSets = React.createClass({ orgUnits: null, helpOpen: false, logs: null, + logsFilter: log => true, + logsPageLast: 0, } }, @@ -105,7 +107,7 @@ const DataSets = React.createClass({ this.registerDisposable(deleteStore.subscribe(deleteObjects => this.getDataSets())); this.registerDisposable(this.subscribeToModelStore(sharingStore, "sharing")); this.registerDisposable(this.subscribeToModelStore(orgUnitsStore, "orgUnits")); - this.registerDisposable(logsStore.subscribe(datasets => this.showDatasetsLogs(datasets))); + this.registerDisposable(logsStore.subscribe(datasets => this.openLogs(datasets))); }, subscribeToModelStore(store, modelName) { @@ -136,26 +138,6 @@ const DataSets = React.createClass({ }); }, - showDatasetsLogs(datasets) { - // Set this.state.logs to the logs that include any of the given - // datasets, and this.state.logsObject to a description of their contents. - if (!datasets) { - this.setState({logsObject: null}); - } else { - const title = this.tr("logs") + " (" + datasets.map(ds => ds.id).join(", ") + ")"; - this.setState({ - logsObject: title, - logs: null, - }); - getLogs().then(logs => { - const idsSelected = new Set(datasets.map(ds => ds.id)); - const hasIds = (log) => log.datasets.some(ds => idsSelected.has(ds.id)); - const logsSelected = _(logs).filter(hasIds).orderBy('date', 'desc').value(); - this.setState({logs: logsSelected}); - }); - } - }, - searchListByName(searchObserver) { //bind key search listener @@ -178,26 +160,48 @@ const DataSets = React.createClass({ this.setState({settingsOpen: false}); }, - openLogs() { - // Retrieve the logs and save them in this.state.logs, and set - // this.state.logsObject to a description of their contents. - const title = this.tr("logs") + " (" + this.tr("all") + ")"; + openLogs(datasets) { + // Set this.state.logs to the logs that include any of the given + // datasets, this.state.logsObject to a description of their contents + // and this.state.logsFilter so it selects only the relevant logs. + if (datasets === null) { + this.setState({logsObject: null}); + return; + } + + let title = this.tr("logs"); + if (Array.isArray(datasets)) { + const ids = datasets.map(ds => ds.id); + const idsSet = new Set(ids); + this.state.logsFilter = log => log.datasets.some(ds => idsSet.has(ds.id)); + title += " (" + ids.join(", ") + ")"; + } else { + this.state.logsFilter = log => true; + title += " (" + this.tr("all") + ")"; + } + this.setState({ logsObject: title, logs: null, }); - getLogs().then(logs => { - this.setState({logs: _(logs).orderBy('date', 'desc').value()}); - }); + this.addLogs([0, 1]); }, - addLogPage() { - // Placeholder for a function that will add another page to - // the already stored logs. - getLogs([-2]).then(logs => { - // TODO: instead of "-2", get the page previous to the last one retrieved. - logs = [].concat(this.state.logs, logs); - this.setState({logs: _(logs).orderBy('date', 'desc').value()}); + addLogs(pages) { + // Add logs from the given log pages (to the already loaded logs if any). + if (!Array.isArray(pages)) + pages = [this.state.logsPageLast + 1]; + + getLogs(pages).then(logs => { + if (logs === null) { + this.setState({logsPageLast: -1}); + } else { + logs = _(logs).filter(this.state.logsFilter).orderBy('date', 'desc').value(); + this.setState({ + logs: _.flatten(_.filter([this.state.logs, logs])), + logsPageLast: pages[pages.length - 1], + }); + } }); }, @@ -324,6 +328,11 @@ const DataSets = React.createClass({ const userCanCreateDataSets = currentUserHasPermission(d2, d2.models.dataSet, "CREATE_PRIVATE"); const logActions = [ + , n >= maxLogPages)) + return null; + store = store ? store : await getStore(); const logsPageCurrent = await store.get('logs-page-current').catch(() => 0); - const pagesNames = pages.map(n => 'logs-page-' + mod(logsPageCurrent + n, maxLogPages)); - const logs = await Promise.all(pagesNames.map(name => store.get(name).catch(() => []))); - return _.flatten(logs); + const pagesNames = pages.map(n => 'logs-page-' + mod(logsPageCurrent - n, maxLogPages)); + const logs = await Promise.all(pagesNames.map(name => store.get(name).catch(() => null))); + if (logs.every(log => log === null)) + return null; + else + return _.flatten(_.filter(logs)); } async function setLogs(logs, store) { diff --git a/src/i18n/i18n_module_ar.properties b/src/i18n/i18n_module_ar.properties index 330598da2..e4a6293fa 100644 --- a/src/i18n/i18n_module_ar.properties +++ b/src/i18n/i18n_module_ar.properties @@ -180,3 +180,4 @@ help_landing_page= logs=Logs logs_none=No logs logs_loading=Loading logs... +logs_more=Add older logs diff --git a/src/i18n/i18n_module_en.properties b/src/i18n/i18n_module_en.properties index 330598da2..e4a6293fa 100644 --- a/src/i18n/i18n_module_en.properties +++ b/src/i18n/i18n_module_en.properties @@ -180,3 +180,4 @@ help_landing_page= logs=Logs logs_none=No logs logs_loading=Loading logs... +logs_more=Add older logs diff --git a/src/i18n/i18n_module_es.properties b/src/i18n/i18n_module_es.properties index c6041ec99..aa91fdd81 100644 --- a/src/i18n/i18n_module_es.properties +++ b/src/i18n/i18n_module_es.properties @@ -180,3 +180,4 @@ help_landing_page= logs=Logs logs_none=Ningún registro logs_loading=Cargando logs... +logs_more=Añadir logs anteriores diff --git a/src/i18n/i18n_module_fr.properties b/src/i18n/i18n_module_fr.properties index 330598da2..e4a6293fa 100644 --- a/src/i18n/i18n_module_fr.properties +++ b/src/i18n/i18n_module_fr.properties @@ -180,3 +180,4 @@ help_landing_page= logs=Logs logs_none=No logs logs_loading=Loading logs... +logs_more=Add older logs From c4b87ad6db28937e3bcb10c02ac502e44fe92744 Mon Sep 17 00:00:00 2001 From: Jordi Date: Mon, 23 Apr 2018 13:46:22 +0200 Subject: [PATCH 02/12] Cosmetics: nicer indentation --- src/DataSets/log.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/DataSets/log.js b/src/DataSets/log.js index 29604654d..b1a4e89f0 100644 --- a/src/DataSets/log.js +++ b/src/DataSets/log.js @@ -74,13 +74,15 @@ function mod(n, m) { // Simple component to show a log entry. function LogEntry(props) { - return (
- Date: {props.date}
- Action: {props.action}
- Status: {props.status}
- User: {props.user.displayName} ({props.user.username})
- Datasets: {props.datasets.map(ds => `${ds.displayName} (${ds.id}) `).join(', ')}
-
); + return ( +
+ Date: {props.date}
+ Action: {props.action}
+ Status: {props.status}
+ User: {props.user.displayName} ({props.user.username})
+ Datasets: {props.datasets.map(ds => `${ds.displayName} (${ds.id}) `).join(', ')}
+
+ ); } From 5664a8349405efa504bbe2b2630a43c8a6528a3b Mon Sep 17 00:00:00 2001 From: Jordi Date: Mon, 23 Apr 2018 13:58:03 +0200 Subject: [PATCH 03/12] Cosmetics: simplify the api slightly Remove the optimization for not retrieving the dataStore multiple times unnecessarily. It turns out that it is negligible, and thus it is better to have a simpler code. --- src/DataSets/log.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/DataSets/log.js b/src/DataSets/log.js index b1a4e89f0..192e6f7d2 100644 --- a/src/DataSets/log.js +++ b/src/DataSets/log.js @@ -8,15 +8,9 @@ const maxLogPages = 100; async function log(actionName, status, datasets) { // Log the name of the action that has been executed, its status // ("success", "failed"), by whom and on which datasets. - const store = await getStore(); - const logs = await getLogs([0], store); + const logs = await getLogs([0]); logs.push(await makeEntry(actionName, status, datasets)); - return setLogs(logs, store); -} - -async function getStore() { - const d2 = await getD2(); - return d2.dataStore.get('dataset-configuration'); + return setLogs(logs); } async function makeEntry(actionName, status, datasets) { @@ -39,7 +33,7 @@ async function makeEntry(actionName, status, datasets) { }; } -async function getLogs(pages, store) { +async function getLogs(pages) { // Return the concatenated logs for the given pages (relative to // logsPageCurrent) if they exist, or an empty list otherwise. // It returns null when there are no more log pages. @@ -47,7 +41,7 @@ async function getLogs(pages, store) { if (pages.every(n => n >= maxLogPages)) return null; - store = store ? store : await getStore(); + const store = await getStore(); const logsPageCurrent = await store.get('logs-page-current').catch(() => 0); const pagesNames = pages.map(n => 'logs-page-' + mod(logsPageCurrent - n, maxLogPages)); const logs = await Promise.all(pagesNames.map(name => store.get(name).catch(() => null))); @@ -57,9 +51,10 @@ async function getLogs(pages, store) { return _.flatten(_.filter(logs)); } -async function setLogs(logs, store) { +async function setLogs(logs) { // Save the given list of logs in the current page index and // update the page index. + const store = await getStore(); const logsPageCurrent = await store.get('logs-page-current').catch(() => 0); return Promise.all([ store.set('logs-page-' + logsPageCurrent, logs), @@ -68,6 +63,11 @@ async function setLogs(logs, store) { ]); } +async function getStore() { + const d2 = await getD2(); + return d2.dataStore.get('dataset-configuration'); +} + function mod(n, m) { return ((n % m) + m) % m; // the modulo operation can be negative otherwise... } From 5192e1c9f18d7cf08ffde1350799f9a3bd5a7eb8 Mon Sep 17 00:00:00 2001 From: Jordi Date: Mon, 23 Apr 2018 14:11:42 +0200 Subject: [PATCH 04/12] Cosmetics --- src/DataSets/log.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DataSets/log.js b/src/DataSets/log.js index 192e6f7d2..3823d49ea 100644 --- a/src/DataSets/log.js +++ b/src/DataSets/log.js @@ -80,7 +80,7 @@ function LogEntry(props) { Action: {props.action}
Status: {props.status}
User: {props.user.displayName} ({props.user.username})
- Datasets: {props.datasets.map(ds => `${ds.displayName} (${ds.id}) `).join(', ')}
+ Datasets: {props.datasets.map(ds => `${ds.displayName} (${ds.id})`).join(', ')}
); } From 59da995ec0dac2accea46eefd7e711be5edd4c08 Mon Sep 17 00:00:00 2001 From: Jordi Date: Tue, 24 Apr 2018 17:16:10 +0200 Subject: [PATCH 05/12] Load log pages with the last page first So, when concatenated, the position of the last log is predictable (it is in position 0), and also of the most recent log (it is in the last position of the array). --- src/DataSets/DataSets.component.js | 2 +- src/DataSets/log.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DataSets/DataSets.component.js b/src/DataSets/DataSets.component.js index 1025704ac..593cf6cca 100644 --- a/src/DataSets/DataSets.component.js +++ b/src/DataSets/DataSets.component.js @@ -184,7 +184,7 @@ const DataSets = React.createClass({ logsObject: title, logs: null, }); - this.addLogs([0, 1]); + this.addLogs([1, 0]); }, addLogs(pages) { diff --git a/src/DataSets/log.js b/src/DataSets/log.js index 3823d49ea..ff2d6742e 100644 --- a/src/DataSets/log.js +++ b/src/DataSets/log.js @@ -37,7 +37,7 @@ async function getLogs(pages) { // Return the concatenated logs for the given pages (relative to // logsPageCurrent) if they exist, or an empty list otherwise. // It returns null when there are no more log pages. - pages = pages || [0, 1]; // by default, take the last two pages + pages = pages || [1, 0]; // by default, take the last two pages if (pages.every(n => n >= maxLogPages)) return null; From 022bb001fbd72ddbf24e22fc5f5280054e459a9c Mon Sep 17 00:00:00 2001 From: Jordi Date: Tue, 24 Apr 2018 17:17:04 +0200 Subject: [PATCH 06/12] Show a more clear message about loading older logs When requesting a new log page in the log dialog, it will now show a button that includes the date of the last log retrieved so far. --- src/DataSets/DataSets.component.js | 6 +++++- src/i18n/i18n_module_ar.properties | 3 ++- src/i18n/i18n_module_en.properties | 3 ++- src/i18n/i18n_module_es.properties | 3 ++- src/i18n/i18n_module_fr.properties | 3 ++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/DataSets/DataSets.component.js b/src/DataSets/DataSets.component.js index 593cf6cca..e9aea827c 100644 --- a/src/DataSets/DataSets.component.js +++ b/src/DataSets/DataSets.component.js @@ -95,6 +95,7 @@ const DataSets = React.createClass({ logs: null, logsFilter: log => true, logsPageLast: 0, + logsOldestDate: null, } }, @@ -196,10 +197,12 @@ const DataSets = React.createClass({ if (logs === null) { this.setState({logsPageLast: -1}); } else { + const logsOldestDate = logs.length > 0 ? logs[0].date : null; logs = _(logs).filter(this.state.logsFilter).orderBy('date', 'desc').value(); this.setState({ logs: _.flatten(_.filter([this.state.logs, logs])), logsPageLast: pages[pages.length - 1], + logsOldestDate: logsOldestDate, }); } }); @@ -329,7 +332,8 @@ const DataSets = React.createClass({ const logActions = [ , diff --git a/src/i18n/i18n_module_ar.properties b/src/i18n/i18n_module_ar.properties index e4a6293fa..fee824fe0 100644 --- a/src/i18n/i18n_module_ar.properties +++ b/src/i18n/i18n_module_ar.properties @@ -180,4 +180,5 @@ help_landing_page= logs=Logs logs_none=No logs logs_loading=Loading logs... -logs_more=Add older logs +logs_older=Load logs older than +logs_no_older=No logs older than diff --git a/src/i18n/i18n_module_en.properties b/src/i18n/i18n_module_en.properties index e4a6293fa..fee824fe0 100644 --- a/src/i18n/i18n_module_en.properties +++ b/src/i18n/i18n_module_en.properties @@ -180,4 +180,5 @@ help_landing_page= logs=Logs logs_none=No logs logs_loading=Loading logs... -logs_more=Add older logs +logs_older=Load logs older than +logs_no_older=No logs older than diff --git a/src/i18n/i18n_module_es.properties b/src/i18n/i18n_module_es.properties index aa91fdd81..c8ce1987a 100644 --- a/src/i18n/i18n_module_es.properties +++ b/src/i18n/i18n_module_es.properties @@ -180,4 +180,5 @@ help_landing_page= logs=Logs logs_none=Ningún registro logs_loading=Cargando logs... -logs_more=Añadir logs anteriores +logs_older=Cargar logs anteriores a +logs_no_older=No hay logs anteriores a diff --git a/src/i18n/i18n_module_fr.properties b/src/i18n/i18n_module_fr.properties index e4a6293fa..fee824fe0 100644 --- a/src/i18n/i18n_module_fr.properties +++ b/src/i18n/i18n_module_fr.properties @@ -180,4 +180,5 @@ help_landing_page= logs=Logs logs_none=No logs logs_loading=Loading logs... -logs_more=Add older logs +logs_older=Load logs older than +logs_no_older=No logs older than From 666db78fda18ddd2440eb430fc9d32041d17634d Mon Sep 17 00:00:00 2001 From: Jordi Date: Tue, 24 Apr 2018 17:58:00 +0200 Subject: [PATCH 07/12] Fix loading of the oldest log first So we show the correct date in the button to get older log pages. --- src/DataSets/DataSets.component.js | 2 +- src/DataSets/log.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/DataSets/DataSets.component.js b/src/DataSets/DataSets.component.js index e9aea827c..3e09ddc47 100644 --- a/src/DataSets/DataSets.component.js +++ b/src/DataSets/DataSets.component.js @@ -201,7 +201,7 @@ const DataSets = React.createClass({ logs = _(logs).filter(this.state.logsFilter).orderBy('date', 'desc').value(); this.setState({ logs: _.flatten(_.filter([this.state.logs, logs])), - logsPageLast: pages[pages.length - 1], + logsPageLast: _.max(pages), logsOldestDate: logsOldestDate, }); } diff --git a/src/DataSets/log.js b/src/DataSets/log.js index ff2d6742e..106be148b 100644 --- a/src/DataSets/log.js +++ b/src/DataSets/log.js @@ -33,11 +33,11 @@ async function makeEntry(actionName, status, datasets) { }; } -async function getLogs(pages) { +async function getLogs(pages = [1, 0]) { // Return the concatenated logs for the given pages (relative to // logsPageCurrent) if they exist, or an empty list otherwise. // It returns null when there are no more log pages. - pages = pages || [1, 0]; // by default, take the last two pages + pages = _(pages).sortBy().reverse().value(); // ensure "oldest first" if (pages.every(n => n >= maxLogPages)) return null; From 8796f5e00a97a05a8f4cb68714896a0a2c1d1498 Mon Sep 17 00:00:00 2001 From: Jordi Date: Wed, 25 Apr 2018 01:43:48 +0200 Subject: [PATCH 08/12] Make it the responsability of getLogs() to return them in recent-first order --- src/DataSets/DataSets.component.js | 8 ++++---- src/DataSets/log.js | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/DataSets/DataSets.component.js b/src/DataSets/DataSets.component.js index 3e09ddc47..87e914978 100644 --- a/src/DataSets/DataSets.component.js +++ b/src/DataSets/DataSets.component.js @@ -185,7 +185,7 @@ const DataSets = React.createClass({ logsObject: title, logs: null, }); - this.addLogs([1, 0]); + this.addLogs([0, 1]); // load the last two log pages }, addLogs(pages) { @@ -197,10 +197,10 @@ const DataSets = React.createClass({ if (logs === null) { this.setState({logsPageLast: -1}); } else { - const logsOldestDate = logs.length > 0 ? logs[0].date : null; - logs = _(logs).filter(this.state.logsFilter).orderBy('date', 'desc').value(); + const logsOldestDate = logs.length > 0 ? logs[logs.length - 1].date : null; + logs = _(logs).filter(this.state.logsFilter).value(); this.setState({ - logs: _.flatten(_.filter([this.state.logs, logs])), + logs: _([this.state.logs, logs]).filter().flatten().value(), logsPageLast: _.max(pages), logsOldestDate: logsOldestDate, }); diff --git a/src/DataSets/log.js b/src/DataSets/log.js index 106be148b..ae3e6b6b5 100644 --- a/src/DataSets/log.js +++ b/src/DataSets/log.js @@ -33,11 +33,10 @@ async function makeEntry(actionName, status, datasets) { }; } -async function getLogs(pages = [1, 0]) { +async function getLogs(pages = [0, 1]) { // Return the concatenated logs for the given pages (relative to // logsPageCurrent) if they exist, or an empty list otherwise. // It returns null when there are no more log pages. - pages = _(pages).sortBy().reverse().value(); // ensure "oldest first" if (pages.every(n => n >= maxLogPages)) return null; @@ -48,7 +47,7 @@ async function getLogs(pages = [1, 0]) { if (logs.every(log => log === null)) return null; else - return _.flatten(_.filter(logs)); + return _(logs).filter().flatten().orderBy('date', 'desc').value(); } async function setLogs(logs) { From eb203e951191e4079e9b28aedb8d70245ab04761 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 18 May 2018 13:36:14 +0200 Subject: [PATCH 09/12] Some logs refactor - Apply styling (spaces/indent) - Create wrapper methods openAllLogs/addNextLog - Render loadMoreLogs button only if necessary - Move loadMoreLogs button to logs list - Abstract logs.getCurrentPage - Refactor getLogs - Fix: Clear always next page in the circular buffer - Format date in logs rendering. --- src/DataSets/DataSets.component.js | 94 ++++++++++++++++++------------ src/DataSets/log.js | 67 +++++++++++++++------ 2 files changed, 107 insertions(+), 54 deletions(-) diff --git a/src/DataSets/DataSets.component.js b/src/DataSets/DataSets.component.js index 87e914978..5a7f32f64 100644 --- a/src/DataSets/DataSets.component.js +++ b/src/DataSets/DataSets.component.js @@ -74,8 +74,8 @@ const DataSets = React.createClass({ }; }, - tr(text, variables={}) { - return this.getTranslation(text, variables); // so it's less verbose + tr(text, namespace = {}) { + return this.getTranslation(text, namespace); }, getInitialState() { @@ -93,6 +93,7 @@ const DataSets = React.createClass({ orgUnits: null, helpOpen: false, logs: null, + logsHasMore: null, logsFilter: log => true, logsPageLast: 0, logsOldestDate: null, @@ -161,46 +162,56 @@ const DataSets = React.createClass({ this.setState({settingsOpen: false}); }, + openAllLogs() { + const title = `${this.tr("logs")} (${this.tr("all")})`; + this.setState({ + logsFilter: log => true, + logsObject: title, + logs: null, + }); + this.addLogs([0, 1]); + }, + openLogs(datasets) { // Set this.state.logs to the logs that include any of the given // datasets, this.state.logsObject to a description of their contents // and this.state.logsFilter so it selects only the relevant logs. if (datasets === null) { this.setState({logsObject: null}); - return; - } - - let title = this.tr("logs"); - if (Array.isArray(datasets)) { + } else { const ids = datasets.map(ds => ds.id); const idsSet = new Set(ids); - this.state.logsFilter = log => log.datasets.some(ds => idsSet.has(ds.id)); - title += " (" + ids.join(", ") + ")"; - } else { - this.state.logsFilter = log => true; - title += " (" + this.tr("all") + ")"; - } + const title = `${this.tr("logs")} (${ids.join(", ")})`; + const logsFilter = log => log.datasets.some(ds => idsSet.has(ds.id)); - this.setState({ - logsObject: title, - logs: null, - }); - this.addLogs([0, 1]); // load the last two log pages + this.setState({ + logsObject: title, + logs: null, + logsFilter, + }); + this.addLogs([0, 1]); // load the last two log pages + } }, + addNextLog() { + return this.addLogs([this.state.logsPageLast + 1]); + }, + addLogs(pages) { - // Add logs from the given log pages (to the already loaded logs if any). - if (!Array.isArray(pages)) - pages = [this.state.logsPageLast + 1]; - - getLogs(pages).then(logs => { - if (logs === null) { - this.setState({logsPageLast: -1}); + return getLogs(pages).then(res => { + if (res === null) { + this.setState({ + logsPageLast: -1, + logs: this.state.logs || [], + }); } else { + const {logs, hasMore: logsHasMore} = res; const logsOldestDate = logs.length > 0 ? logs[logs.length - 1].date : null; - logs = _(logs).filter(this.state.logsFilter).value(); + const filteredLogs = _(logs).filter(this.state.logsFilter).value(); + this.setState({ - logs: _([this.state.logs, logs]).filter().flatten().value(), + logsHasMore: logsHasMore, + logs: _([this.state.logs, filteredLogs]).compact().flatten().value(), logsPageLast: _.max(pages), logsOldestDate: logsOldestDate, }); @@ -321,22 +332,27 @@ const DataSets = React.createClass({ const renderLogsButton = () => (
- +
); const {d2} = this.context; + const { logsPageLast, logsOldestDate, logsHasMore } = this.state; const userCanCreateDataSets = currentUserHasPermission(d2, d2.models.dataSet, "CREATE_PRIVATE"); + const olderLogLiteral = logsPageLast < 0 ? this.tr("logs_no_older") : this.tr("logs_older"); + const dateString = new Date(logsOldestDate || Date()).toLocaleString(); + const label = olderLogLiteral + " " + dateString; - const logActions = [ + const logLoadMoreButton = logsHasMore ? ( , + label={label} + onClick={this.addNextLog} + /> + ) : null; + + const logActions = [ { - const logs = this.state.logs; // shortcut - if (logs === null) + const { logs } = this.state; + + if (!logs) return this.tr("logs_loading"); else if (_(logs).isEmpty()) return this.tr("logs_none"); @@ -449,7 +466,10 @@ const DataSets = React.createClass({ onRequestClose={listActions.hideLogs} autoScrollBodyContent={true} > - { renderLogs() } + {renderLogs()} +
+ {logLoadMoreButton} +
) : null } diff --git a/src/DataSets/log.js b/src/DataSets/log.js index ae3e6b6b5..3d010d40d 100644 --- a/src/DataSets/log.js +++ b/src/DataSets/log.js @@ -4,11 +4,10 @@ import { getInstance as getD2 } from 'd2/lib/d2'; const maxLogsPerPage = 200; // TODO: maybe make it readable from the dataStore const maxLogPages = 100; - async function log(actionName, status, datasets) { // Log the name of the action that has been executed, its status // ("success", "failed"), by whom and on which datasets. - const logs = await getLogs([0]); + const logs = ((await getLogs([0])) || {logs: []}).logs; logs.push(await makeEntry(actionName, status, datasets)); return setLogs(logs); } @@ -35,31 +34,61 @@ async function makeEntry(actionName, status, datasets) { async function getLogs(pages = [0, 1]) { // Return the concatenated logs for the given pages (relative to - // logsPageCurrent) if they exist, or an empty list otherwise. + // currentPage) if they exist, or an empty list otherwise. // It returns null when there are no more log pages. if (pages.every(n => n >= maxLogPages)) return null; const store = await getStore(); - const logsPageCurrent = await store.get('logs-page-current').catch(() => 0); - const pagesNames = pages.map(n => 'logs-page-' + mod(logsPageCurrent - n, maxLogPages)); - const logs = await Promise.all(pagesNames.map(name => store.get(name).catch(() => null))); - if (logs.every(log => log === null)) + const currentPage = await getCurrentPage(store); + const pagesNames = pages.map(n => 'logs-page-' + mod(currentPage - n, maxLogPages)); + const storeKeys = new Set(await store.getKeys()); + const existingPageNames = pagesNames.filter(name => storeKeys.has(name)); + + if (_(existingPageNames).isEmpty()) { return null; - else - return _(logs).filter().flatten().orderBy('date', 'desc').value(); + } else { + const existingPages = _(Array.from(storeKeys)) + .map(key => key.match(/^logs-page-(\d+)$/)) + .map(match => match ? parseInt(match[1]) : null) + .reject(_.isNull) + .value(); + const nextPage = _.max(pages) + 1; + const nextPageIndex = mod(currentPage - nextPage, maxLogPages); + const hasMore = ( + nextPage < maxLogPages && + existingPages.includes(nextPageIndex) + ); + const logs = await Promise.all(existingPageNames.map(name => store.get(name))); + const orderedLogs = _(logs).filter().flatten().orderBy('date', 'desc').value(); + + return {logs: orderedLogs, hasMore}; + } +} + +function getCurrentPage(store) { + return store.get('logs-page-current').catch(err => { + if (err.httpStatusCode === 404) { + return store.set('logs-page-current', 0).then(() => 0); + } else { + throw err; + } + }); } async function setLogs(logs) { - // Save the given list of logs in the current page index and - // update the page index. + // Save the given list of logs in the current page index and update the page index. const store = await getStore(); - const logsPageCurrent = await store.get('logs-page-current').catch(() => 0); + const currentPage = await getCurrentPage(store); + const nextCurrentPage = logs.length < maxLogsPerPage ? currentPage : mod(currentPage + 1, maxLogPages); + return Promise.all([ - store.set('logs-page-' + logsPageCurrent, logs), - store.set('logs-page-current', logs.length < maxLogsPerPage ? - logsPageCurrent : mod(logsPageCurrent + 1, maxLogPages)), - ]); + store.set('logs-page-' + currentPage, logs), + ...(nextCurrentPage === currentPage ? [] : [ + store.set('logs-page-current', nextCurrentPage), + store.set(`logs-page-${nextCurrentPage}`, []), + ]), + ]); } async function getStore() { @@ -71,11 +100,15 @@ function mod(n, m) { return ((n % m) + m) % m; // the modulo operation can be negative otherwise... } +function formatDate(isoDate) { + return new Date(isoDate).toLocaleString(); +} + // Simple component to show a log entry. function LogEntry(props) { return (
- Date: {props.date}
+ Date: {formatDate(props.date)}
Action: {props.action}
Status: {props.status}
User: {props.user.displayName} ({props.user.username})
From ecb4fc754ebd0d49db7864d3c99daa37dfe501bc Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 18 May 2018 14:34:33 +0200 Subject: [PATCH 10/12] Use new SharingDialog prop to log on changes --- src/DataSets/DataSets.component.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/DataSets/DataSets.component.js b/src/DataSets/DataSets.component.js index 5a7f32f64..ffe62c48f 100644 --- a/src/DataSets/DataSets.component.js +++ b/src/DataSets/DataSets.component.js @@ -208,7 +208,7 @@ const DataSets = React.createClass({ const {logs, hasMore: logsHasMore} = res; const logsOldestDate = logs.length > 0 ? logs[logs.length - 1].date : null; const filteredLogs = _(logs).filter(this.state.logsFilter).value(); - + this.setState({ logsHasMore: logsHasMore, logs: _([this.state.logs, filteredLogs]).compact().flatten().value(), @@ -254,6 +254,10 @@ const DataSets = React.createClass({ this.setState({helpOpen: false}); }, + _onSharingSave(sharings) { + log('change sharing settings', 'success', sharings.map(sharing => sharing.model)); + }, + render() { const currentlyShown = calculatePageValue(this.state.pager); @@ -408,9 +412,8 @@ const DataSets = React.createClass({ {this.state.sharing ? { - log('change sharing settings', 'success', this.state.sharing.models); - listActions.hideSharingBox();}} + onRequestClose={listActions.hideSharingBox} + onSave={this._onSharingSave} onError={err => { log('change sharing settings', 'failed', this.state.sharing.models); snackActions.show({message: err && err.message || 'Error'});}} From a518416d8f00a0779f4d3a2ec26dd9f2df891e19 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Wed, 23 May 2018 12:42:21 +0200 Subject: [PATCH 11/12] Save unique log when sharing dialog is closed with changes --- src/DataSets/DataSets.component.js | 19 +++++++++++++------ src/models/Sharing.js | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 src/models/Sharing.js diff --git a/src/DataSets/DataSets.component.js b/src/DataSets/DataSets.component.js index 631e593d6..72466d0f6 100644 --- a/src/DataSets/DataSets.component.js +++ b/src/DataSets/DataSets.component.js @@ -40,6 +40,7 @@ import HelpOutlineIcon from 'material-ui/svg-icons/action/help-outline'; import ListIcon from 'material-ui/svg-icons/action/list'; import FormHelpers from '../forms/FormHelpers'; import {currentUserHasAdminRole, canCreate} from '../utils/Dhis2Helpers'; +import * as sharing from '../models/Sharing'; const {SimpleCheckBox} = FormHelpers; @@ -94,7 +95,8 @@ const DataSets = React.createClass({ logsFilter: log => true, logsPageLast: 0, logsOldestDate: null, - } + sharing: null, + }; }, @@ -126,7 +128,7 @@ const DataSets = React.createClass({ const filteredDataSets = searchValue ? allDataSets.filter().on('displayName').ilike(searchValue) : allDataSets; const order = sorting ? sorting.join(":") : undefined; - const fields = "id,name,displayName,shortName,created,lastUpdated,publicAccess,user,access" + const fields = "id,name,displayName,shortName,created,lastUpdated,externalAccess,publicAccess,userGroupAccesses,user,access" filteredDataSets.list({order, fields}).then(da => { this.setState({ @@ -251,8 +253,14 @@ const DataSets = React.createClass({ this.setState({helpOpen: false}); }, - _onSharingSave(sharings) { - log('change sharing settings', 'success', sharings.map(sharing => sharing.model)); + _onSharingClose(sharings) { + const {updated, all} = sharing.getChanges(this.state.dataRows, sharings); + + if (!_(updated).isEmpty()) { + log('change sharing settings', 'success', updated); + this.setState({dataRows: all}); + } + listActions.hideSharingBox(); }, render() { @@ -408,8 +416,7 @@ const DataSets = React.createClass({ {this.state.sharing ? { log('change sharing settings', 'failed', this.state.sharing.models); snackActions.show({message: err && err.message || 'Error'});}} diff --git a/src/models/Sharing.js b/src/models/Sharing.js new file mode 100644 index 000000000..f387be6f8 --- /dev/null +++ b/src/models/Sharing.js @@ -0,0 +1,26 @@ +const sharingFields = ["externalAccess", "publicAccess", "userGroupAccesses"]; + +function getNormalizedSharing(data) { + const sharing = _.pick(data, sharingFields); + const normalizedUserGroupAccesses = _(sharing.userGroupAccesses) + .map(userGroup => _.pick(userGroup, ["id", "access"])) + .sortBy("id") + .value(); + return {...sharing, userGroupAccesses: normalizedUserGroupAccesses}; +} + +export function getChanges(models, newSharings) { + const sharingById = _(newSharings).keyBy(sharing => sharing.model.id).value(); + const newModels = models.map(model => { + const sharing = sharingById[model.id]; + const sharingChanged = sharing && + !_(getNormalizedSharing(model)).isEqual(getNormalizedSharing(sharing)); + return sharingChanged ? {...model, ...sharing} : model; + }); + const updatedModels = _(models).zip(newModels) + .map(([model, newModel]) => model !== newModel ? newModel : null) + .compact() + .value(); + + return {updated: updatedModels, all: newModels}; +} \ No newline at end of file From 2ae17429a3afddf6969bbb7a03cea87224a4e654 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Wed, 23 May 2018 12:51:15 +0200 Subject: [PATCH 12/12] Use also userAccesses field in Sharing --- src/DataSets/DataSets.component.js | 2 +- src/models/Sharing.js | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/DataSets/DataSets.component.js b/src/DataSets/DataSets.component.js index 72466d0f6..87297252f 100644 --- a/src/DataSets/DataSets.component.js +++ b/src/DataSets/DataSets.component.js @@ -128,7 +128,7 @@ const DataSets = React.createClass({ const filteredDataSets = searchValue ? allDataSets.filter().on('displayName').ilike(searchValue) : allDataSets; const order = sorting ? sorting.join(":") : undefined; - const fields = "id,name,displayName,shortName,created,lastUpdated,externalAccess,publicAccess,userGroupAccesses,user,access" + const fields = "id,name,displayName,shortName,created,lastUpdated,externalAccess,publicAccess,userAccesses,userGroupAccesses,user,access" filteredDataSets.list({order, fields}).then(da => { this.setState({ diff --git a/src/models/Sharing.js b/src/models/Sharing.js index f387be6f8..2b04cb0bd 100644 --- a/src/models/Sharing.js +++ b/src/models/Sharing.js @@ -1,12 +1,19 @@ -const sharingFields = ["externalAccess", "publicAccess", "userGroupAccesses"]; +const sharingFields = ["externalAccess", "publicAccess", "userAccesses", "userGroupAccesses"]; -function getNormalizedSharing(data) { - const sharing = _.pick(data, sharingFields); - const normalizedUserGroupAccesses = _(sharing.userGroupAccesses) - .map(userGroup => _.pick(userGroup, ["id", "access"])) +function normalizeAccesses(accesses) { + return _(accesses || []) + .map(info => _.pick(info, ["id", "access"])) .sortBy("id") .value(); - return {...sharing, userGroupAccesses: normalizedUserGroupAccesses}; +} + +function getNormalizedSharing(data) { + const sharing = _.pick(data, sharingFields); + return { + ...sharing, + userGroupAccesses: normalizeAccesses(sharing.userGroupAccesses), + userAccesses: normalizeAccesses(sharing.userAccesses), + }; } export function getChanges(models, newSharings) {