From dba7d896672f7ac97270ab7152467d504a9a1690 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 12 Jun 2019 20:16:53 +0200 Subject: [PATCH 01/21] Add basic Sync Rules Landing page and Wizard --- i18n/en.pot | 47 ++++- src/components/app/Root.jsx | 12 ++ .../rules-creation-page/SyncRulesWizard.jsx | 111 ++++++++++++ .../rules-list-page/SyncRulesConfigurator.jsx | 165 ++++++++++++++++++ src/models/syncRule.ts | 58 ++++++ src/types/synchronization.d.ts | 9 + 6 files changed, 397 insertions(+), 5 deletions(-) create mode 100644 src/components/rules-creation-page/SyncRulesWizard.jsx create mode 100644 src/components/rules-list-page/SyncRulesConfigurator.jsx create mode 100644 src/models/syncRule.ts diff --git a/i18n/en.pot b/i18n/en.pot index 13a603ff8..6dc7e6ede 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2019-06-13T06:24:14.176Z\n" -"PO-Revision-Date: 2019-06-13T06:24:14.176Z\n" +"POT-Creation-Date: 2019-06-13T06:37:16.186Z\n" +"PO-Revision-Date: 2019-06-13T06:37:16.186Z\n" msgid "" msgstr "" @@ -190,6 +190,46 @@ msgstr "" msgid "Back" msgstr "" +msgid "General info" +msgstr "" + +msgid "New Sync Rule" +msgstr "" + +msgid "Edit Sync Rule" +msgstr "" + +msgid "Cancel Sync Rule Creation" +msgstr "" + +msgid "Cancel Sync Rule Editing" +msgstr "" + +msgid "Deleting Sync Rules" +msgstr "" + +msgid "Failed to delete some rules" +msgstr "" + +msgid "Successfully deleted {{count}} rules" +msgid_plural "Successfully deleted {{count}} rules" +msgstr[0] "" +msgstr[1] "" + +msgid "Name" +msgstr "" + +msgid "Execute" +msgstr "" + +msgid "Delete Rules?" +msgstr "" + +msgid "Are you sure you want to delete {{count}} rules?" +msgid_plural "Are you sure you want to delete {{count}} rules?" +msgstr[0] "" +msgstr[1] "" + msgid "Synchronizing metadata" msgstr "" @@ -289,9 +329,6 @@ msgstr "" msgid "Network error {{error}}, check if server is up and CORS is enabled" msgstr "" -msgid "Name" -msgstr "" - msgid "Last update" msgstr "" diff --git a/src/components/app/Root.jsx b/src/components/app/Root.jsx index 61be702d1..fa97869d6 100644 --- a/src/components/app/Root.jsx +++ b/src/components/app/Root.jsx @@ -10,6 +10,8 @@ import DataElementPage from "../synchronization-page/DataElementPage"; import IndicatorPage from "../synchronization-page/IndicatorPage"; import ValidationRulePage from "../synchronization-page/ValidationRulePage"; import NotificationsPage from "../notification-list-page/NotificationsPage"; +import SyncRulesWizard from "../rules-creation-page/SyncRulesWizard"; +import SyncRulesConfigurator from "../rules-list-page/SyncRulesConfigurator"; class Root extends React.Component { static propTypes = { @@ -56,6 +58,16 @@ class Root extends React.Component { render={props => } /> + } + /> + + } + /> + } /> ); diff --git a/src/components/rules-creation-page/SyncRulesWizard.jsx b/src/components/rules-creation-page/SyncRulesWizard.jsx new file mode 100644 index 000000000..900fec5f4 --- /dev/null +++ b/src/components/rules-creation-page/SyncRulesWizard.jsx @@ -0,0 +1,111 @@ +import React from "react"; +import PropTypes from "prop-types"; +import i18n from "@dhis2/d2-i18n"; +import { withRouter } from "react-router-dom"; +import { ConfirmationDialog, Wizard } from "d2-ui-components"; + +import Instance from "../../models/instance"; + +import PageHeader from "../page-header/PageHeader"; + +class SyncRulesWizard extends React.Component { + static propTypes = { + d2: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + }; + + state = { + dialogOpen: false, + instance: Instance.create(), + }; + + id = this.props.match.params.id; + isEdit = this.props.match.params.action === "edit" && this.id; + + static getStepsBaseInfo() { + return [ + { + key: "general-info", + label: i18n.t("General info"), + component: () =>

Test

, + validationKeys: [], + description: i18n.t(`Description`), + help: i18n.t(`Help`), + }, + ]; + } + + componentDidMount = async () => { + if (this.isEdit) { + const instance = await Instance.get(this.props.d2, this.id); + this.setState({ instance }); + } + }; + + cancelSave = () => { + this.setState({ dialogOpen: true }); + }; + + handleConfirm = () => { + this.setState({ dialogOpen: false }); + this.props.history.push("/synchronization-rules"); + }; + + handleDialogCancel = () => { + this.setState({ dialogOpen: false }); + }; + + onChange = instance => { + this.setState({ instance }); + }; + + switchStep = () => {}; + + render() { + const { dialogOpen, instance } = this.state; + const { d2 } = this.props; + + const title = !this.isEdit ? i18n.t("New Sync Rule") : i18n.t("Edit Sync Rule"); + + const cancel = !this.isEdit + ? i18n.t("Cancel Sync Rule Creation") + : i18n.t("Cancel Sync Rule Editing"); + + const steps = SyncRulesWizard.getStepsBaseInfo().map(step => ({ + ...step, + props: { + d2, + instance, + //onChange: this.onChange(step), + onCancel: this.handleConfirm, + }, + })); + + return ( + + + + + + + + ); + } +} + +export default withRouter(SyncRulesWizard); diff --git a/src/components/rules-list-page/SyncRulesConfigurator.jsx b/src/components/rules-list-page/SyncRulesConfigurator.jsx new file mode 100644 index 000000000..5d1f7c03f --- /dev/null +++ b/src/components/rules-list-page/SyncRulesConfigurator.jsx @@ -0,0 +1,165 @@ +import React from "react"; +import PropTypes from "prop-types"; +import _ from "lodash"; +import i18n from "@dhis2/d2-i18n"; +import { ConfirmationDialog, ObjectsTable, withLoading, withSnackbar } from "d2-ui-components"; +import { withRouter } from "react-router-dom"; +import { withStyles } from "@material-ui/core/styles"; + +import PageHeader from "../page-header/PageHeader"; +import SyncRule from "../../models/syncRule"; + +const styles = () => ({ + tableContainer: { marginTop: -10 }, +}); + +class SyncRulesConfigurator extends React.Component { + static propTypes = { + d2: PropTypes.object.isRequired, + snackbar: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + loading: PropTypes.object.isRequired, + }; + static model = { + modelValidations: { + // TODO: Update model validations + }, + }; + + state = { + tableKey: Math.random(), + toDelete: null, + }; + + columns = [ + { name: "name", text: i18n.t("Name"), sortable: true }, + // TODO: Add description, origin and destination + ]; + + initialSorting = ["name", "asc"]; + + detailsFields = [ + { name: "name", text: i18n.t("Name") }, + { name: "description", text: i18n.t("Description") }, + // TODO: Add origin, destination + ]; + + backHome = () => { + this.props.history.push("/"); + }; + + deleteSyncRules = rules => { + this.setState({ toDelete: rules }); + }; + + cancelDelete = () => { + this.setState({ toDelete: null }); + }; + + confirmDelete = async () => { + const { loading, d2 } = this.props; + const { toDelete } = this.state; + + loading.show(true, i18n.t("Deleting Sync Rules")); + const rules = toDelete.map(data => new SyncRule(data)); + + const results = []; + for (const rule of rules) { + results.push(await rule.remove(d2)); + } + + loading.reset(); + this.setState({ tableKey: Math.random(), toDelete: null }); + + if (_.some(results, ["status", false])) { + this.props.snackbar.error(i18n.t("Failed to delete some rules")); + } else { + this.props.snackbar.success( + i18n.t("Successfully deleted {{count}} rules", { count: toDelete.length }) + ); + } + }; + + createRule = () => { + this.props.history.push("/synchronization-rules/new"); + }; + + editRule = rule => { + this.props.history.push(`/synchronization-rules/edit/${rule.id}`); + }; + + executeRule = _rule => { + // TODO + }; + + actions = [ + { + name: "edit", + text: i18n.t("Edit"), + multiple: false, + onClick: this.editInstance, + }, + { + name: "details", + text: i18n.t("Details"), + multiple: false, + type: "details", + }, + { + name: "delete", + text: i18n.t("Delete"), + multiple: true, + onClick: this.deleteSyncRules, + }, + { + name: "execute", + text: i18n.t("Execute"), + multiple: false, + onClick: this.executeRule, + icon: "settings_input_antenna", + }, + ]; + + render() { + const { tableKey, toDelete } = this.state; + const { d2, classes } = this.props; + + return ( + + +
+ +
+ + +
+ ); + } +} + +export default withLoading(withSnackbar(withRouter(withStyles(styles)(SyncRulesConfigurator)))); diff --git a/src/models/syncRule.ts b/src/models/syncRule.ts new file mode 100644 index 000000000..22f51c39f --- /dev/null +++ b/src/models/syncRule.ts @@ -0,0 +1,58 @@ +import _ from "lodash"; +import { generateUid } from "d2/uid"; + +import { deleteData, getDataById, getPaginatedData } from "./dataStore"; +import { D2 } from "../types/d2"; +import { TableFilters, TableList, TablePagination } from "../types/d2-ui-components"; +import { SynchronizationRule } from "../types/synchronization"; +import Instance from "./instance"; + +const dataStoreKey = "rules"; + +export default class SyncRule { + private readonly syncRule: SynchronizationRule; + + constructor(syncRule: SynchronizationRule) { + this.syncRule = { + id: generateUid(), + ..._.pick(syncRule, ["id", "name", "description", "originInstance", "builder"]), + }; + } + + public static create(): SyncRule { + return new SyncRule({ + id: "", + name: "", + originInstance: Instance.create(), + builder: { + targetInstances: [], + metadata: {}, + }, + }); + } + + public static build(syncRule: SynchronizationRule | undefined): SyncRule { + return syncRule ? new SyncRule(syncRule) : this.create(); + } + + public static async get(d2: D2, id: string): Promise { + const data = await getDataById(d2, dataStoreKey, id); + return this.build(data); + } + + public static async list( + d2: D2, + filters: TableFilters, + pagination: TablePagination + ): Promise { + return getPaginatedData(d2, dataStoreKey, filters, pagination); + } + + public async save(_d2: D2): Promise { + // TODO + } + + public async remove(d2: D2): Promise { + await deleteData(d2, dataStoreKey, this.syncRule); + } +} diff --git a/src/types/synchronization.d.ts b/src/types/synchronization.d.ts index fa53cf6ac..197473a3a 100644 --- a/src/types/synchronization.d.ts +++ b/src/types/synchronization.d.ts @@ -1,5 +1,6 @@ import { MetadataImportResponse, MetadataImportStats } from "./d2"; import SyncReport from "../models/syncReport"; +import Instance from "../models/instance"; export interface SynchronizationBuilder { targetInstances: string[]; @@ -51,3 +52,11 @@ export interface SynchronizationState { syncReport?: SyncReport; done?: boolean; } + +export interface SynchronizationRule { + id?: string; + name: string; + description?: string; + originInstance: Instance; + builder: SynchronizationBuilder; +} From b61dc87502212ae2fcc355c0c26fdeffd25cde9c Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Tue, 18 Jun 2019 11:03:45 +0200 Subject: [PATCH 02/21] Add sync rule wizard steps --- i18n/en.pot | 60 +++-- src/components/dropdown/Dropdown.jsx | 9 +- .../rules-creation-page/SyncRulesWizard.jsx | 59 +++-- .../steps/GeneralInfoStep.jsx | 58 +++++ .../steps/InstanceSelectionStep.jsx | 50 ++++ .../steps/MetadataStep.jsx | 245 ++++++++++++++++++ .../rules-creation-page/steps/SaveStep.jsx | 62 +++++ src/logic/metadata.ts | 49 ++++ src/models/d2Model.ts | 11 + src/models/syncRule.ts | 43 ++- src/types/synchronization.d.ts | 1 + src/utils/synchronization.ts | 8 +- 12 files changed, 615 insertions(+), 40 deletions(-) create mode 100644 src/components/rules-creation-page/steps/GeneralInfoStep.jsx create mode 100644 src/components/rules-creation-page/steps/InstanceSelectionStep.jsx create mode 100644 src/components/rules-creation-page/steps/MetadataStep.jsx create mode 100644 src/components/rules-creation-page/steps/SaveStep.jsx create mode 100644 src/logic/metadata.ts diff --git a/i18n/en.pot b/i18n/en.pot index 6dc7e6ede..ced985a8f 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2019-06-13T06:37:16.186Z\n" -"PO-Revision-Date: 2019-06-13T06:37:16.186Z\n" +"POT-Creation-Date: 2019-06-18T09:02:36.522Z\n" +"PO-Revision-Date: 2019-06-18T09:02:36.522Z\n" msgid "" msgstr "" @@ -193,16 +193,52 @@ msgstr "" msgid "General info" msgstr "" -msgid "New Sync Rule" +msgid "Metadata" msgstr "" -msgid "Edit Sync Rule" +msgid "Instance Selection" msgstr "" -msgid "Cancel Sync Rule Creation" +msgid "Summary" +msgstr "" + +msgid "New synchronization rule" +msgstr "" + +msgid "Edit synchronization rule" +msgstr "" + +msgid "Cancel synchronization rule creation" +msgstr "" + +msgid "Cancel synchronization rule editing" +msgstr "" + +msgid "Name (*)" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Metadata Type" +msgstr "" + +msgid "Last update" +msgstr "" + +msgid "Selected elements" +msgstr "" + +msgid "Metadata type" +msgstr "" + +msgid "Last updated date" +msgstr "" + +msgid "{{displayName}} Group" msgstr "" -msgid "Cancel Sync Rule Editing" +msgid "{{displayName}} Level" msgstr "" msgid "Deleting Sync Rules" @@ -216,9 +252,6 @@ msgid_plural "Successfully deleted {{count}} rules" msgstr[0] "" msgstr[1] "" -msgid "Name" -msgstr "" - msgid "Execute" msgstr "" @@ -269,9 +302,6 @@ msgstr "" msgid "Synchronization Results" msgstr "" -msgid "Summary" -msgstr "" - msgid "Messages" msgstr "" @@ -284,9 +314,6 @@ msgstr "" msgid "Please select at least one element from the table to synchronize" msgstr "" -msgid "Last updated date" -msgstr "" - msgid "{{type}} Group" msgstr "" @@ -329,9 +356,6 @@ msgstr "" msgid "Network error {{error}}, check if server is up and CORS is enabled" msgstr "" -msgid "Last update" -msgstr "" - msgid "Level" msgstr "" diff --git a/src/components/dropdown/Dropdown.jsx b/src/components/dropdown/Dropdown.jsx index 47ae13121..21af78951 100644 --- a/src/components/dropdown/Dropdown.jsx +++ b/src/components/dropdown/Dropdown.jsx @@ -56,14 +56,14 @@ const getMaterialTheme = () => }, }); -export default function Dropdown({ items, value, onChange, label }) { +export default function Dropdown({ items, value, onChange, label, hideEmpty }) { const materialTheme = getMaterialTheme(); return ( {label} +