diff --git a/cypress/commons/actions/brewery/BreweryParameters.js b/cypress/commons/actions/brewery/BreweryParameters.js
index 38e9c4df6..aadd1f4cc 100644
--- a/cypress/commons/actions/brewery/BreweryParameters.js
+++ b/cypress/commons/actions/brewery/BreweryParameters.js
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
import { BREWERY_SELECTORS } from '../../constants/brewery/IdConstants';
import { GENERIC_SELECTORS } from '../../constants/generic/IdConstants';
+import { apiUtils } from '../../utils';
import { FileParameters, TableParameters, ScenarioParameters } from '../generic';
// Get tabs elements
@@ -305,6 +306,15 @@ function clearCustomersTableStringCell(colName, rowIndex, useDelKey = false) {
return TableParameters.clearStringCell(getCustomersTable, colName, rowIndex, useDelKey);
}
+function getCustomersRevertTableButton() {
+ return TableParameters.getRevertDataButton(getCustomersTable());
+}
+
+function revertCustomersTable(response = {}) {
+ apiUtils.interceptPostDatasetTwingraphQuery(response, false);
+ TableParameters.revertTableData(getCustomersTable());
+}
+
function getEventsTable() {
return cy.get(BREWERY_SELECTORS.scenario.parameters.events.table);
}
@@ -360,6 +370,10 @@ function clearEventsTableStringCell(colName, rowIndex, useDelKey = false) {
return TableParameters.clearStringCell(getEventsTable, colName, rowIndex, useDelKey);
}
+function getEventsRevertTableButton() {
+ return TableParameters.getRevertDataButton(getEventsTable());
+}
+
function getExampleDatasetPart1FileName() {
return FileParameters.getFileName(getExampleDatasetPart1());
}
@@ -517,6 +531,8 @@ export const BreweryParameters = {
deleteRowsCustomersTableData,
deleteRowsEventsTableData,
editCustomersTableStringCell,
+ getCustomersRevertTableButton,
+ revertCustomersTable,
getEventsTableLabel,
getEventsTableGrid,
getEventsImportButton,
@@ -531,6 +547,7 @@ export const BreweryParameters = {
exportEventsTableDataToCSV,
exportEventsTableDataToXLSX,
editEventsTableStringCell,
+ getEventsRevertTableButton,
getStock,
getRestock,
getWaiters,
diff --git a/cypress/commons/actions/generic/TableParameters.js b/cypress/commons/actions/generic/TableParameters.js
index a0a77478b..fe31cf956 100644
--- a/cypress/commons/actions/generic/TableParameters.js
+++ b/cypress/commons/actions/generic/TableParameters.js
@@ -99,6 +99,14 @@ function getDeleteRowsDialogConfirmButton() {
return cy.get(GENERIC_SELECTORS.genericComponents.table.toolbar.deleteRowsDialogConfirmButton);
}
+function getRevertDataButton(tableParameterElement) {
+ return tableParameterElement.find(GENERIC_SELECTORS.genericComponents.table.toolbar.revertButton);
+}
+
+function getRevertDialogConfirmButton() {
+ return cy.get(GENERIC_SELECTORS.genericComponents.table.toolbar.revertDialogConfirmButton);
+}
+
function getHeader(tableParameterElement) {
return getGrid(tableParameterElement).find(GENERIC_SELECTORS.genericComponents.table.header);
}
@@ -234,6 +242,11 @@ function clearStringCell(getTableElement, colName, rowIndex, useDelKey = false)
return getCell(getTableElement(), colName, rowIndex);
}
+function revertTableData(tableParameterElement) {
+ getRevertDataButton(tableParameterElement).click();
+ getRevertDialogConfirmButton().click();
+}
+
export const TableParameters = {
getFullscreenTable,
getLabel,
@@ -279,4 +292,6 @@ export const TableParameters = {
setFileExportName,
editStringCell,
clearStringCell,
+ revertTableData,
+ getRevertDataButton,
};
diff --git a/cypress/commons/constants/generic/IdConstants.js b/cypress/commons/constants/generic/IdConstants.js
index 517401dc9..e044b30a0 100644
--- a/cypress/commons/constants/generic/IdConstants.js
+++ b/cypress/commons/constants/generic/IdConstants.js
@@ -275,6 +275,8 @@ export const GENERIC_SELECTORS = {
addRowButton: '[data-cy=add-row-button]',
deleteRowsButton: '[data-cy=delete-rows-button]',
deleteRowsDialogConfirmButton: '[data-cy=delete-rows-dialog-confirm-button]',
+ revertButton: '[data-cy=revert-table-button]',
+ revertDialogConfirmButton: '[data-cy=revert-table-data-dialog-confirm-button]',
},
header: '[class=ag-header-container]',
placeholder: '[data-cy=empty-table-placeholder]',
diff --git a/cypress/e2e/brewery/TableParameters-dynamic_table.cy.js b/cypress/e2e/brewery/TableParameters-dynamic_table.cy.js
new file mode 100644
index 000000000..e5e8bb86d
--- /dev/null
+++ b/cypress/e2e/brewery/TableParameters-dynamic_table.cy.js
@@ -0,0 +1,109 @@
+// Copyright (c) Cosmo Tech.
+// Licensed under the MIT license.
+import { Login, ScenarioParameters, Scenarios, ScenarioSelector } from '../../commons/actions';
+import { BreweryParameters } from '../../commons/actions/brewery';
+import { stub } from '../../commons/services/stubbing';
+import { apiUtils } from '../../commons/utils';
+import { SOLUTION_WITH_DYNAMIC_TABLE } from '../../fixtures/stubbing/TableParameters-dynamic_table/solution';
+import { DEFAULT_SCENARIOS_LIST } from '../../fixtures/stubbing/default';
+
+const EDITED_DATA_CSV = 'customers_from_dataset_edited.csv';
+const twingraphQueryResponse = [
+ {
+ fields: {
+ name: 'Customer3',
+ thirsty: false,
+ satisfaction: 0,
+ surroundingSatisfaction: 0,
+ },
+ },
+ {
+ fields: {
+ name: 'Customer1',
+ thirsty: false,
+ satisfaction: 0,
+ surroundingSatisfaction: 0,
+ },
+ },
+ {
+ fields: {
+ name: 'Customer2',
+ thirsty: false,
+ satisfaction: 0,
+ surroundingSatisfaction: 0,
+ },
+ },
+ {
+ fields: {
+ name: 'Customer4',
+ thirsty: false,
+ satisfaction: 0,
+ surroundingSatisfaction: 0,
+ },
+ },
+];
+
+describe('can use dataset data in editable table', () => {
+ before(() => {
+ stub.start();
+ stub.setSolutions([SOLUTION_WITH_DYNAMIC_TABLE]);
+ });
+ beforeEach(() => {
+ Login.login();
+ });
+ after(() => {
+ stub.stop();
+ });
+ it('can display a table filled with data fetched from dataset', () => {
+ apiUtils.interceptPostDatasetTwingraphQuery(twingraphQueryResponse, false);
+ Scenarios.getScenarioViewTab(60).should('be.visible');
+ ScenarioParameters.expandParametersAccordion();
+ BreweryParameters.switchToCustomersTab();
+ BreweryParameters.getCustomersTable().should('be.visible');
+ BreweryParameters.getCustomersTableLabel().should('be.visible').should('have.text', 'Customers');
+ BreweryParameters.getCustomersTableGrid().should('exist');
+ BreweryParameters.getCustomersTableCell('name', 0).should('have.text', twingraphQueryResponse[0].fields.name);
+ });
+ it('can export data fetched from dataset and upload a new table', () => {
+ apiUtils.interceptPostDatasetTwingraphQuery(twingraphQueryResponse, false);
+ Scenarios.getScenarioViewTab(60).should('be.visible');
+ ScenarioParameters.expandParametersAccordion();
+ BreweryParameters.switchToCustomersTab();
+ BreweryParameters.getCustomersTable().should('be.visible');
+ BreweryParameters.getCustomersTableGrid().should('exist');
+ BreweryParameters.exportCustomersTableDataToCSV();
+ BreweryParameters.importCustomersTableData(EDITED_DATA_CSV);
+ BreweryParameters.getCustomersTableCell('name', 0).should('have.text', 'Client');
+ BreweryParameters.getCustomersTableCell('name', 1).should('have.text', 'Client');
+ });
+ it('can fetch data from dataset, edit it without saving and revert', () => {
+ apiUtils.interceptPostDatasetTwingraphQuery(twingraphQueryResponse, false);
+ Scenarios.getScenarioViewTab(60).should('be.visible');
+ ScenarioParameters.expandParametersAccordion();
+ BreweryParameters.getEventsRevertTableButton().should('not.exist');
+ BreweryParameters.switchToCustomersTab();
+ BreweryParameters.getCustomersTableGrid().should('exist');
+ BreweryParameters.getCustomersRevertTableButton().should('exist');
+ BreweryParameters.editCustomersTableStringCell('name', 0, 'Client').should('have.text', 'Client');
+ BreweryParameters.revertCustomersTable(twingraphQueryResponse);
+ ScenarioParameters.getSaveButton().should('not.exist');
+ });
+ it('can fetch data from dataset and save table as dataset part, then revert data', () => {
+ apiUtils.interceptPostDatasetTwingraphQuery(twingraphQueryResponse, false);
+ Scenarios.getScenarioViewTab(60).should('be.visible');
+ ScenarioParameters.expandParametersAccordion();
+ BreweryParameters.switchToCustomersTab();
+ BreweryParameters.getCustomersTableGrid().should('exist');
+ BreweryParameters.editCustomersTableStringCell('name', 0, 'Client').should('have.text', 'Client');
+ ScenarioParameters.save({ datasetsEvents: [{ id: 'd-stbddtspr1', securityChanges: { default: 'admin' } }] });
+ BreweryParameters.switchToEventsTab();
+ ScenarioSelector.selectScenario(DEFAULT_SCENARIOS_LIST[1].name, DEFAULT_SCENARIOS_LIST[1].id);
+ ScenarioSelector.selectScenario(DEFAULT_SCENARIOS_LIST[0].name, DEFAULT_SCENARIOS_LIST[0].id);
+ apiUtils.interceptDownloadWorkspaceFile();
+ BreweryParameters.switchToCustomersTab();
+ BreweryParameters.getCustomersTableGrid().should('exist');
+ BreweryParameters.getCustomersTableCell('name', 0).should('have.text', 'Client');
+ BreweryParameters.revertCustomersTable(twingraphQueryResponse);
+ ScenarioParameters.getSaveButton().should('exist');
+ });
+});
diff --git a/cypress/fixtures/customers_from_dataset_edited.csv b/cypress/fixtures/customers_from_dataset_edited.csv
new file mode 100644
index 000000000..e92e0f2a5
--- /dev/null
+++ b/cypress/fixtures/customers_from_dataset_edited.csv
@@ -0,0 +1,5 @@
+name,satisfaction,surroundingSatisfaction,thirsty
+Client,0,0,true
+Client,5,0,false
+Customer2,0,0,false
+Customer4,8,0,false
diff --git a/cypress/fixtures/stubbing/TableParameters-dynamic_table/solution.js b/cypress/fixtures/stubbing/TableParameters-dynamic_table/solution.js
new file mode 100644
index 000000000..e7735b559
--- /dev/null
+++ b/cypress/fixtures/stubbing/TableParameters-dynamic_table/solution.js
@@ -0,0 +1,134 @@
+// Copyright (c) Cosmo Tech.
+// Licensed under the MIT license.
+import { DEFAULT_SOLUTION } from '../default';
+
+export const SOLUTION_WITH_DYNAMIC_TABLE = {
+ ...DEFAULT_SOLUTION,
+ runTemplates: [
+ {
+ id: '3',
+ name: 'Run template with mock basic types parameters',
+ description: 'Run template with mock basic types parameters',
+ csmSimulation: 'BreweryDemoSimulationWithConnector',
+ tags: ['3', 'Example'],
+ parameterGroups: ['events', 'customers'],
+ },
+ ],
+ parameterGroups: [
+ {
+ id: 'events',
+ labels: {
+ en: 'Events',
+ fr: 'Événements',
+ },
+ parameters: ['events'],
+ },
+ {
+ id: 'customers',
+ labels: {
+ en: 'Customers',
+ fr: 'Clients',
+ },
+ parameters: ['customers'],
+ },
+ ],
+ parameters: [
+ {
+ id: 'customers',
+ labels: {
+ fr: 'Clients',
+ en: 'Customers',
+ },
+ varType: '%DATASETID%',
+ defaultValue: null,
+ minValue: null,
+ maxValue: null,
+ regexValidation: null,
+ options: {
+ canChangeRowsNumber: true,
+ connectorId: 'c-d7e5p9o0kjn9',
+ subType: 'TABLE',
+ dynamicValues: {
+ query:
+ 'MATCH(customer: Customer) WITH {name: customer.id, satisfaction: customer.Satisfaction, ' +
+ 'surroundingSatisfaction: customer.SurroundingSatisfaction, thirsty: customer.Thirsty} ' +
+ 'as fields RETURN fields',
+ resultKey: 'fields',
+ },
+ columns: [
+ {
+ field: 'name',
+ headerName: 'Name',
+ type: ['string'],
+ },
+ {
+ field: 'satisfaction',
+ headerName: 'Satisfaction',
+ type: ['int'],
+ minValue: 0,
+ maxValue: 10,
+ acceptsEmptyFields: true,
+ },
+ {
+ field: 'surroundingSatisfaction',
+ headerName: 'SurroundingSatisfaction',
+ type: ['int'],
+ minValue: 0,
+ maxValue: 10,
+ acceptsEmptyFields: true,
+ },
+ {
+ field: 'thirsty',
+ headerName: 'Thirsty',
+ type: ['bool'],
+ acceptsEmptyFields: true,
+ },
+ ],
+ },
+ },
+ {
+ id: 'events',
+ labels: {
+ fr: 'Événements',
+ en: 'Events',
+ },
+ varType: '%DATASETID%',
+ options: {
+ subType: 'TABLE',
+ columns: [
+ {
+ field: 'theme',
+ type: ['string'],
+ },
+ {
+ field: 'date',
+ type: ['date'],
+ minValue: '1900-01-01',
+ maxValue: '2999-12-31',
+ },
+ {
+ field: 'timeOfDay',
+ type: ['enum'],
+ enumValues: ['morning', 'midday', 'afternoon', 'evening'],
+ },
+ {
+ field: 'eventType',
+ type: ['string', 'nonResizable', 'nonEditable'],
+ },
+ {
+ field: 'reservationsNumber',
+ type: ['int'],
+ minValue: 0,
+ maxValue: 300,
+ acceptsEmptyFields: true,
+ },
+ {
+ field: 'online',
+ type: ['bool', 'nonSortable'],
+ },
+ ],
+ dateFormat: 'dd/MM/yyyy',
+ },
+ },
+ ],
+};
diff --git a/doc/scenarioParametersConfiguration.md b/doc/scenarioParametersConfiguration.md
index dd8011a76..7ea7ebe38 100644
--- a/doc/scenarioParametersConfiguration.md
+++ b/doc/scenarioParametersConfiguration.md
@@ -267,10 +267,6 @@ mode is enabled by setting `options.subType` to `TABLE`. The `options` dict can
- `dateFormat`: a string describing the expected format of dates in the table based on
[date-fns format patterns](https://date-fns.org/v2.25.0/docs/parse) (default: `yyyy-MM-dd`)
-Also, if you want to have a default table content instead of the default empty table, you can provide the id of an
-existing dataset in the `defaultValue` property of the parameter description (this dataset must be a CSV file, with
-values separated by commas).
-
Example:
```yaml
@@ -303,6 +299,51 @@ parameters:
canChangeRowsNumber: false
dateFormat: 'dd/MM/yyyy'
```
+#### Data source
+By default, the table component is empty but if you want to display some data, you can define either a default or dynamic
+value for the parameter. You can provide the id of an existing dataset in the `defaultValue` property of the parameter
+description (this dataset must be a CSV file, with values separated by commas). If you want to fetch data from scenario's
+dataset, you can use `dynamicValues` option that uses a cypher query to retrieve data from twingraph dataset.
+
+_Note: only a **cypher** query from **twingraph** dataset is supported_
+
+
+`dynamicValues` is an object with the following keys:
+
+- `query`: the cypher query to run on a twingraph dataset; this query must retrieve a list of property values of the
+ graph elements, and return them with an alias (example: `MATCH(n:Customer) WITH {name: n.name, age: n.age} as alias
+RETURN alias`). Names of the properties must correspond to the `field` key in columns definition.
+- `resultKey`: the alias defined in your query; providing this value is required for the webapp to parse the cypher
+ query results, and retrieve the actual values to display in the table
+
+Example:
+```yaml
+parameters:
+ - id: 'customers'
+ labels:
+ fr: 'Clients'
+ en: 'Customers'
+ varType: '%DATASETID%'
+ options:
+ subType: 'TABLE'
+ connectorId: 'c-d7e5p9o0kjn9'
+ description: 'customers data'
+ dynamicValues:
+ query: 'MATCH(customer: Customer) WITH {name: customer.id, satisfaction: customer.Satisfaction} as fields RETURN fields'
+ resultKey: 'fields'
+ columns:
+ - field: 'name'
+ type:
+ - 'nonEditable'
+ - field: 'satisfaction'
+ type:
+ - 'int'
+ minValue: 0
+ maxValue: 120
+ canChangeRowsNumber: false
+```
+
+_Known issue: Dynamic parameters are not saved as scenario parameters when they are not edited_
#### Columns definition
diff --git a/package.json b/package.json
index 2663dcf1a..3546abc0b 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"@cosmotech/api-ts": "^3.1.0-dev",
"@cosmotech/azure": "^1.3.4",
"@cosmotech/core": "^1.14.0",
- "@cosmotech/ui": "^8.1.1",
+ "@cosmotech/ui": "^9.0.0",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@microsoft/applicationinsights-web": "^3.0.3",
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 437c46ad5..f1e909a79 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -738,7 +738,13 @@
"export": "Export",
"addRow": "Add a new row",
"deleteRows": "Remove selected rows",
- "fullscreen": "Fullscreen"
+ "fullscreen": "Fullscreen",
+ "revert": "Revert",
+ "noDatasetsError": "Impossible to fetch data from dataset because the list of datasets is empty",
+ "notTwingraphDatasetError": "Only twingraph datasets can be used to fetch table data dynamically",
+ "noQueryError": "Impossible to fetch data from dataset because there is no twingraph query defined for this parameter. You can load data manually using Import button",
+ "wrongResultKeyError": "Returned result doesn't have {{resultKey}} property. Probably there is an error in resultKey configuration, please, check your solution",
+ "wrongQueryError": "Returned result is empty, there is probably an error in your query configuration. Please, check your solution"
},
"export": {
"labels": {
@@ -756,6 +762,13 @@
"cancel": "Cancel",
"confirm": "Delete",
"checkbox": "Don't show this message again"
+ },
+ "revertDataDialog": {
+ "title": "Revert table?",
+ "body": "This will replace your data with initial dataset data.",
+ "cancel": "Cancel",
+ "revert": "Revert",
+ "checkbox": "Don't show this message again"
}
}
},
diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json
index a299169da..476bff2fb 100644
--- a/public/locales/fr/translation.json
+++ b/public/locales/fr/translation.json
@@ -738,7 +738,13 @@
"export": "Exporter",
"addRow": "Ajouter une nouvelle ligne",
"deleteRows": "Supprimer les lignes sélectionnées",
- "fullscreen": "Plein écran"
+ "fullscreen": "Plein écran",
+ "revert": "Rétablir",
+ "noDatasetsError": "Il est impossible de charger les données depuis le dataset parce que la liste des datasets est vide",
+ "notTwingraphDatasetError": "Le chargement dynamique des données du tableau est disponible uniquement pour les datasets du type twingraph",
+ "noQueryError": "Il est impossible de charger les données depuis le dataset parce qu'aucune requête du twingraph n'est définie pour ce paramètre. Vous pouvez charger les données manuellement avec le bouton Importer",
+ "wrongResultKeyError": "Le résultat de la requête ne contient pas de propriété {{resultKey}}. Cela provient probablement d'une erreur dans la configuration de resultKey, veuillez vérifier la solution.",
+ "wrongQueryError": "Le résultat retourné est vide. Cela provient probablement d'une erreur dans la configuration de la requête. Veuillez vérifier la solution."
},
"export": {
"labels": {
@@ -756,6 +762,13 @@
"cancel": "Annuler",
"confirm": "Supprimer",
"checkbox": "Ne plus afficher ce message"
+ },
+ "revertDataDialog": {
+ "title": "Rétablir les données ?",
+ "body": "Cette action remplacera les données du tableau par les données initiales du dataset.",
+ "cancel": "Annuler",
+ "revert": "Rétablir",
+ "checkbox": "Ne plus afficher ce message"
}
}
},
diff --git a/src/components/ScenarioParameters/components/ScenarioParametersInputs/GenericTable.js b/src/components/ScenarioParameters/components/ScenarioParametersInputs/GenericTable.js
index 9b78e1b83..cefe6283e 100644
--- a/src/components/ScenarioParameters/components/ScenarioParametersInputs/GenericTable.js
+++ b/src/components/ScenarioParameters/components/ScenarioParametersInputs/GenericTable.js
@@ -8,14 +8,16 @@ import equal from 'fast-deep-equal';
import rfdc from 'rfdc';
import { AgGridUtils, FileBlobUtils } from '@cosmotech/core';
import { Table, TABLE_DATA_STATUS, UPLOAD_FILE_STATUS_KEY } from '@cosmotech/ui';
+import { Api } from '../../../../services/config/Api';
+import { useSetApplicationErrorMessage } from '../../../../state/hooks/ApplicationHooks';
import { useOrganizationId } from '../../../../state/hooks/OrganizationHooks.js';
+import { useCurrentScenarioDatasetList } from '../../../../state/hooks/ScenarioHooks';
import { useWorkspaceId } from '../../../../state/hooks/WorkspaceHooks.js';
import { gridLight, gridDark } from '../../../../theme/';
import { ConfigUtils, TranslationUtils } from '../../../../utils';
import { FileManagementUtils } from '../../../../utils/FileManagementUtils';
import { TableUtils } from '../../../../utils/TableUtils';
-import { TableExportDialog } from './components';
-import { TableDeleteRowsDialog } from './components/TableDeleteRowsDialog';
+import { TableExportDialog, TableRevertDataDialog, TableDeleteRowsDialog } from './components';
const clone = rfdc();
@@ -53,6 +55,8 @@ export const GenericTable = ({
const workspaceId = useWorkspaceId();
const datasets = useSelector((state) => state.dataset?.list?.data);
const scenarioId = useSelector((state) => state.scenario?.current?.data?.id);
+ const currentScenarioDatasetList = useCurrentScenarioDatasetList();
+ const setApplicationErrorMessage = useSetApplicationErrorMessage();
const canChangeRowsNumber = ConfigUtils.getParameterAttribute(parameterData, 'canChangeRowsNumber') ?? false;
const parameterId = parameterData.id;
@@ -86,6 +90,7 @@ export const GenericTable = ({
addRow: t('genericcomponent.table.labels.addRow', 'Add a new row'),
deleteRows: t('genericcomponent.table.labels.deleteRows', 'Remove selected rows'),
fullscreen: t('genericcomponent.table.labels.fullscreen', 'Fullscreen'),
+ revert: t('genericcomponent.table.labels.revert', 'Revert'),
};
const tableExportDialogLabels = {
cancel: t('genericcomponent.table.export.labels.cancel', 'Cancel'),
@@ -116,7 +121,7 @@ export const GenericTable = ({
// Store last parameter in a ref
// Update a state is async, so, in case of multiple call of updateParameterValue in same function
- // parameter state value will be update only in last call.
+ // parameter state value will be updated only in last call.
// We need here to use a ref value for be sure to have the good value.
const lastNewParameterValue = useRef(parameter);
const updateParameterValue = useCallback(
@@ -189,6 +194,97 @@ export const GenericTable = ({
return false;
};
+ const isDataFetchedFromDataset = !!parameterData?.options?.dynamicValues;
+
+ const _getDataFromTwingraphDataset = async (setClientFileDescriptor) => {
+ const fileName = `${parameterData.id}.csv`;
+ setClientFileDescriptor({
+ file: null,
+ content: null,
+ agGridRows: null,
+ errors: null,
+ tableDataStatus: TABLE_DATA_STATUS.DOWNLOADING,
+ });
+ if (_checkForLock()) {
+ return;
+ }
+ GenericTable.downloadLocked[lockId] = true;
+ try {
+ if (!currentScenarioDatasetList || currentScenarioDatasetList?.length === 0)
+ throw new Error(
+ t(
+ 'genericcomponent.table.labels.noDatasetsError',
+ 'Impossible to fetch data from dataset because the list of datasets is empty'
+ )
+ );
+ const sourceDatasetId = currentScenarioDatasetList[0];
+ const dynamicValuesConfig = ConfigUtils.getParameterAttribute(parameterData, 'dynamicValues');
+ const query = dynamicValuesConfig?.query;
+ if (!query)
+ throw new Error(
+ t(
+ 'genericcomponent.table.labels.noQueryError',
+ 'Impossible to fetch data from dataset because there is no twingraph query defined for this parameter. ' +
+ 'You can load data manually using Import button'
+ )
+ );
+ const resultKey = dynamicValuesConfig?.resultKey;
+ const { data } = await Api.Datasets.twingraphQuery(organizationId, sourceDatasetId, { query });
+ if (data.length === 0)
+ throw new Error(
+ t(
+ 'genericcomponent.table.labels.wrongQueryError',
+ 'Returned result is empty, there is probably an error in your query configuration. ' +
+ 'Please, check your solution'
+ )
+ );
+ const rowsDict = data.map((row) => row[resultKey]);
+ if (rowsDict.includes(undefined))
+ throw new Error(
+ t(
+ 'genericcomponent.table.labels.wrongResultKeyError',
+ `Returned result doesn't have ${resultKey} property.
+ Probably there is an error in resultKey configuration, please, check your solution`,
+ { resultKey }
+ )
+ );
+ const csvRows = AgGridUtils.toCSV(rowsDict, columns);
+ const agGridData = _generateGridDataFromCSV(csvRows, parameterData);
+ if (agGridData.error) {
+ setClientFileDescriptor({
+ tableDataStatus: TABLE_DATA_STATUS.ERROR,
+ errors: agGridData.error,
+ });
+ } else
+ setClientFileDescriptor({
+ name: fileName,
+ file: null,
+ agGridRows: agGridData.rows,
+ errors: null,
+ status: UPLOAD_FILE_STATUS_KEY.READY_TO_DOWNLOAD,
+ tableDataStatus: TABLE_DATA_STATUS.READY,
+ uploadPreprocess: { content: _uploadPreprocess },
+ });
+ } catch (error) {
+ const errorComment = error?.response?.status
+ ? t(
+ 'genericcomponent.table.labels.notTwingraphDatasetError',
+ 'Only twingraph datasets can be used to fetch table data dynamically'
+ )
+ : '';
+ setApplicationErrorMessage(error, errorComment);
+ setClientFileDescriptor({
+ file: null,
+ content: null,
+ agGridRows: null,
+ errors: null,
+ tableDataStatus: TABLE_DATA_STATUS.ERROR,
+ });
+ }
+
+ GenericTable.downloadLocked[lockId] = false;
+ };
+
const _downloadDatasetFileContentFromStorage = async (
organizationId,
workspaceId,
@@ -400,6 +496,8 @@ export const GenericTable = ({
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const openExportDialog = () => setIsExportDialogOpen(true);
const closeExportDialog = () => setIsExportDialogOpen(false);
+ const [isRevertDialogOpen, setIsRevertDialogOpen] = useState(false);
+ const closeRevertDialog = () => setIsRevertDialogOpen(false);
const exportCSV = useCallback(
(fileName) => {
const fileContent = AgGridUtils.toCSV(parameter.agGridRows, columns, options);
@@ -486,6 +584,13 @@ export const GenericTable = ({
parameter,
updateParameterValueWithReset
);
+ } else if (
+ isDataFetchedFromDataset &&
+ !parameter.content &&
+ parameter.status === UPLOAD_FILE_STATUS_KEY.EMPTY &&
+ !alreadyDownloaded
+ ) {
+ _getDataFromTwingraphDataset(updateParameterValueWithReset);
}
});
@@ -567,6 +672,25 @@ export const GenericTable = ({
} else deleteRow();
}, [deleteRow]);
+ const revertTableWithDatasetData = useCallback(
+ (isChecked) => {
+ localStorage.setItem('dontAskAgainToRevertTableData', isChecked);
+ closeRevertDialog();
+ // To trigger isDirty state when an already saved table was reverted and avoid it in other cases, we need to
+ // updateParameterValue setter and updateOnFirstEdition function that triggers the start of edition; on the other
+ // hand, updateParameterValueWithReset setter rollbacks modified values without triggering isDirty
+ _getDataFromTwingraphDataset(parameter.id ? updateParameterValue : updateParameterValueWithReset);
+ if (parameter.id) updateOnFirstEdition();
+ },
+ // eslint-disable-next-line
+ [parameter.id, updateParameterValue, updateParameterValueWithReset, updateOnFirstEdition]
+ );
+
+ const onRevertTableData = useCallback(() => {
+ if (localStorage.getItem('dontAskAgainToRevertTableData') !== 'true') setIsRevertDialogOpen(true);
+ else revertTableWithDatasetData('true');
+ }, [setIsRevertDialogOpen, revertTableWithDatasetData]);
+
return (
<>
+ revertTableWithDatasetData(isChecked)}
+ open={isRevertDialogOpen}
+ />
>
);
};
diff --git a/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/TableRevertDataDialog/TableRevertDataDialog.js b/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/TableRevertDataDialog/TableRevertDataDialog.js
new file mode 100644
index 000000000..247e300ae
--- /dev/null
+++ b/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/TableRevertDataDialog/TableRevertDataDialog.js
@@ -0,0 +1,34 @@
+// Copyright (c) Cosmo Tech.
+// Licensed under the MIT license.
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+import { DontAskAgainDialog } from '@cosmotech/ui';
+
+export const TableRevertDataDialog = ({ open, onClose, onConfirm }) => {
+ const { t } = useTranslation();
+ const labels = {
+ title: t('genericcomponent.table.revertDataDialog.title', 'Revert table?'),
+ body: t('genericcomponent.table.revertDataDialog.body', 'This will replace your data with initial dataset data.'),
+ cancel: t('genericcomponent.table.revertDataDialog.cancel', 'Cancel'),
+ confirm: t('genericcomponent.table.revertDataDialog.revert', 'Revert'),
+ checkbox: t('genericcomponent.table.revertDataDialog.checkbox', "Don't show this message again"),
+ };
+
+ return (
+
+ );
+};
+
+TableRevertDataDialog.propTypes = {
+ open: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+ onConfirm: PropTypes.func.isRequired,
+};
diff --git a/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/TableRevertDataDialog/index.js b/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/TableRevertDataDialog/index.js
new file mode 100644
index 000000000..f60f46be9
--- /dev/null
+++ b/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/TableRevertDataDialog/index.js
@@ -0,0 +1,4 @@
+// Copyright (c) Cosmo Tech.
+// Licensed under the MIT license.
+
+export { TableRevertDataDialog } from './TableRevertDataDialog';
diff --git a/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/index.js b/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/index.js
index eb2dc9de5..f17a4dcec 100644
--- a/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/index.js
+++ b/src/components/ScenarioParameters/components/ScenarioParametersInputs/components/index.js
@@ -2,3 +2,5 @@
// Licensed under the MIT license.
export { TableExportDialog } from './TableExportDialog';
+export { TableRevertDataDialog } from './TableRevertDataDialog';
+export { TableDeleteRowsDialog } from './TableDeleteRowsDialog';
diff --git a/src/state/hooks/ScenarioHooks.js b/src/state/hooks/ScenarioHooks.js
index 7a078183f..0591f714b 100644
--- a/src/state/hooks/ScenarioHooks.js
+++ b/src/state/hooks/ScenarioHooks.js
@@ -58,6 +58,10 @@ export const useCurrentScenarioReducerStatus = () => {
return useSelector((state) => state.scenario?.current?.status);
};
+export const useCurrentScenarioDatasetList = () => {
+ return useSelector((state) => state.scenario?.current?.data?.datasetList);
+};
+
export const useResetCurrentScenario = () => {
const dispatch = useDispatch();
return useCallback(() => dispatch(dispatchResetCurrentScenario()), [dispatch]);
diff --git a/src/utils/ConfigUtils.js b/src/utils/ConfigUtils.js
index 93f63d99e..cd62a6ba7 100644
--- a/src/utils/ConfigUtils.js
+++ b/src/utils/ConfigUtils.js
@@ -73,6 +73,7 @@ const getParameterAttribute = (parameter, attributeName) => {
'canChangeRowsNumber',
'shouldRenameFileOnUpload',
'runTemplateFilter',
+ 'dynamicValues',
];
if (!knownAttributesNames.includes(attributeName)) {
console.warn(`The attribute "${attributeName}" is not a known attribute in the scenario parameters configuration.`);
diff --git a/yarn.lock b/yarn.lock
index 87f8628cd..e4413efa3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1267,10 +1267,10 @@
validator "^13.7.0"
xlsx "https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz"
-"@cosmotech/ui@^8.1.1":
- version "8.1.1"
- resolved "https://registry.yarnpkg.com/@cosmotech/ui/-/ui-8.1.1.tgz#3112e372fe2a2a06f2acaea40084776d232e7188"
- integrity sha512-ssyMpJeVAb5nRKIxrxFxTzrDBwhNK5X4uemVPAY/yZZAIsde4585O2A8F2z/xBlVGAkXmN4WwCXLbS9023/ziw==
+"@cosmotech/ui@^9.0.0":
+ version "9.0.0"
+ resolved "https://registry.yarnpkg.com/@cosmotech/ui/-/ui-9.0.0.tgz#59cfebfa02a8964b7e9b8072d9a12a3d1f4b67e8"
+ integrity sha512-t0kHjyvt8tudjEa8VCMDOjGnJF439irI8yGlM5vTXJQJrXryL2C60Fu6lgBiKfvR6R/FrNictWEmmfbnN5DuzA==
dependencies:
"@emotion/react" "^11.10.5"
"@emotion/styled" "^11.10.5"