diff --git a/d2.config.js b/d2.config.js index 1b29a636..487f8d87 100644 --- a/d2.config.js +++ b/d2.config.js @@ -4,6 +4,8 @@ const config = { title: 'Data Administration', coreApp: true, + minDHIS2Version: '2.38', + entryPoints: { app: './src/App.js', }, diff --git a/i18n/en.pot b/i18n/en.pot index 77a4bbf9..7fbcc5bb 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: 2022-02-09T15:18:51.747Z\n" -"PO-Revision-Date: 2022-02-09T15:18:51.747Z\n" +"POT-Creation-Date: 2022-02-14T12:36:03.855Z\n" +"PO-Revision-Date: 2022-02-14T12:36:03.855Z\n" msgid "Open user guide" msgstr "Open user guide" @@ -408,114 +408,15 @@ msgstr "Batch Deletion" msgid "No lock exceptions to show." msgstr "No lock exceptions to show." +msgid "Failed to load available data integrity checks" +msgstr "Failed to load available data integrity checks" + msgid "Checking data integrity..." msgstr "Checking data integrity..." msgid "Run integrity checks" msgstr "Run integrity checks" -msgid "{{issueTitle}} ({{issueElementsCount}})" -msgstr "{{issueTitle}} ({{issueElementsCount}})" - -msgid "Data elements without data set" -msgstr "Data elements without data set" - -msgid "Data elements without groups" -msgstr "Data elements without groups" - -msgid "Data elements violating exclusive group sets" -msgstr "Data elements violating exclusive group sets" - -msgid "Data elements assigned to data sets with different period types" -msgstr "Data elements assigned to data sets with different period types" - -msgid "Data sets not assigned to organisation units" -msgstr "Data sets not assigned to organisation units" - -msgid "Indicators with identical formulas" -msgstr "Indicators with identical formulas" - -msgid "Indicators without groups" -msgstr "Indicators without groups" - -msgid "Invalid indicator numerators" -msgstr "Invalid indicator numerators" - -msgid "Invalid indicator denominators" -msgstr "Invalid indicator denominators" - -msgid "Indicators violating exclusive group sets" -msgstr "Indicators violating exclusive group sets" - -msgid "Organisation units with cyclic references" -msgstr "Organisation units with cyclic references" - -msgid "Orphaned organisation units" -msgstr "Orphaned organisation units" - -msgid "Organisation units without groups" -msgstr "Organisation units without groups" - -msgid "Organisation units violating exclusive group sets" -msgstr "Organisation units violating exclusive group sets" - -msgid "Organisation unit groups without group sets" -msgstr "Organisation unit groups without group sets" - -msgid "Validation rules without groups" -msgstr "Validation rules without groups" - -msgid "Invalid validation rule left side expressions" -msgstr "Invalid validation rule left side expressions" - -msgid "Invalid validation rule right side expressions" -msgstr "Invalid validation rule right side expressions" - -msgid "Invalid program indicator expressions" -msgstr "Invalid program indicator expressions" - -msgid "Invalid program indicator filters" -msgstr "Invalid program indicator filters" - -msgid "There are data elements in the form, but not in the form or sections" -msgstr "There are data elements in the form, but not in the form or sections" - -msgid "Invalid category combinations" -msgstr "Invalid category combinations" - -msgid "Duplicate periods" -msgstr "Duplicate periods" - -msgid "Program rules with no condition" -msgstr "Program rules with no condition" - -msgid "Program rules with no action" -msgstr "Program rules with no action" - -msgid "Program rules with no priority" -msgstr "Program rules with no priority" - -msgid "Program rule variables with no data element" -msgstr "Program rule variables with no data element" - -msgid "Program rule variables with no attribute" -msgstr "Program rule variables with no attribute" - -msgid "Program rule actions with no data object" -msgstr "Program rule actions with no data object" - -msgid "Program rule actions with no notification" -msgstr "Program rule actions with no notification" - -msgid "Program rule actions with no section id" -msgstr "Program rule actions with no section id" - -msgid "Program rule actions with no stage id" -msgstr "Program rule actions with no stage id" - -msgid "Program indicators with no expression" -msgstr "Program indicators with no expression" - msgid "Indicators" msgstr "Indicators" diff --git a/src/pages/data-integrity/DataIntegrity.js b/src/pages/data-integrity/DataIntegrity.js index 91c04f1c..944182ee 100644 --- a/src/pages/data-integrity/DataIntegrity.js +++ b/src/pages/data-integrity/DataIntegrity.js @@ -1,30 +1,78 @@ +import { useDataQuery } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' -import { Button, NoticeBox } from '@dhis2/ui' +import { CenteredContent, CircularLoader, Button, NoticeBox } from '@dhis2/ui' import PropTypes from 'prop-types' -import React from 'react' +import React, { useState } from 'react' import PageHeader from '../../components/PageHeader/PageHeader' import { i18nKeys } from '../../i18n-keys' -import Issues from './Issues/Issues' -import { useDataIntegrity } from './use-data-integrity' +import styles from './DataIntegrity.module.css' +import Section from './Section' -const DataIntegrity = ({ sectionKey }) => { - const { - startDataIntegrityCheck, - loading, - error, - issues, - } = useDataIntegrity() +const query = { + checks: { + resource: 'dataIntegrity', + }, +} + +const groupChecks = checks => + checks.reduce((groupedChecks, check) => { + if (!(check.section in groupedChecks)) { + groupedChecks[check.section] = [] + } + groupedChecks[check.section].push(check) + return groupedChecks + }, {}) + +const DataIntegrity = () => { + const { loading, error, data } = useDataQuery(query) + const [selectedChecks, setSelectedChecks] = useState(new Set()) + // XXX + const [submitting, setSubmitting] = useState(false) + + const startDataIntegrityCheck = () => { + setSubmitting(true) + } + + if (loading) { + return ( + + + + ) + } + + if (error) { + return ( + + {error.message} + + ) + } + + const groupedChecks = groupChecks(data.checks) return ( <> - - {error && {error.message}} - {issues && } - @@ -32,8 +80,18 @@ const DataIntegrity = ({ sectionKey }) => { ) } -DataIntegrity.propTypes = { +const DataIntegrityPage = ({ sectionKey }) => ( + <> + + + +) + +DataIntegrityPage.propTypes = { sectionKey: PropTypes.string.isRequired, } -export default DataIntegrity +export default DataIntegrityPage diff --git a/src/pages/data-integrity/DataIntegrity.module.css b/src/pages/data-integrity/DataIntegrity.module.css new file mode 100644 index 00000000..05af34ab --- /dev/null +++ b/src/pages/data-integrity/DataIntegrity.module.css @@ -0,0 +1,3 @@ +.noticeBox { + max-width: 400px; +} diff --git a/src/pages/data-integrity/Issues/IssueCard.js b/src/pages/data-integrity/Issues/IssueCard.js deleted file mode 100644 index e2cc8970..00000000 --- a/src/pages/data-integrity/Issues/IssueCard.js +++ /dev/null @@ -1,97 +0,0 @@ -import i18n from '@dhis2/d2-i18n' -import { Card } from '@dhis2/ui' -import PropTypes from 'prop-types' -import React from 'react' -import styles from './IssueCard.module.css' - -const Content = ({ content }) => { - const renderValue = value => - Array.isArray(value) ? value.join(', ') : value - - if (Array.isArray(content)) { - return content.map(element => ( -

{renderValue(element)}

- )) - } else { - return Object.entries(content).map(([element, value]) => ( -
-

{element}

-

{renderValue(value)}

-
- )) - } -} - -Content.propTypes = { - content: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), -} - -const Title = ({ title, type }) => ( -

- {title} -

-) - -Title.propTypes = { - title: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, -} - -const countElements = content => { - const sum = arr => - arr.reduce((acc, element) => { - return acc + countElements(element) - }, 0) - - if (Array.isArray(content)) { - return sum(content) - } else if (typeof content === 'object') { - return sum(Object.values(content)) - } else { - return 1 - } -} - -const IssueCard = ({ title, content }) => { - const elementsCount = countElements(content) - - return ( - - {elementsCount > 0 ? ( -
- - - </summary> - <div className={styles.cardText}> - <Content content={content} /> - </div> - </details> - ) : ( - <Title title={title} type="valid" /> - )} - </Card> - ) -} - -IssueCard.propTypes = { - title: PropTypes.string.isRequired, - content: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), -} - -IssueCard.defaultProps = { - content: [], -} - -export default IssueCard diff --git a/src/pages/data-integrity/Issues/IssueCard.module.css b/src/pages/data-integrity/Issues/IssueCard.module.css deleted file mode 100644 index a1f027b7..00000000 --- a/src/pages/data-integrity/Issues/IssueCard.module.css +++ /dev/null @@ -1,40 +0,0 @@ -.card { - padding: var(--spacers-dp12); - margin-bottom: var(--spacers-dp12); -} - -.cardText { - font-size: 14px; -} - -.card h3 { - font-size: 14px; - font-weight: 500; - margin: 0; -} - -.card p { - margin-top: 0; - margin-bottom: 5px; -} - -.title { - display: inline; - font-weight: 500; - font-size: 16px; - margin: 0; -} - -.validTitle { - composes: title; - color: var(--colors-green600); -} - -.invalidTitle { - composes: title; - color: var(--colors-red600); -} - -details[open] .cardHeader { - margin-bottom: var(--spacers-dp12); -} diff --git a/src/pages/data-integrity/Issues/Issues.js b/src/pages/data-integrity/Issues/Issues.js deleted file mode 100644 index a7aadb2d..00000000 --- a/src/pages/data-integrity/Issues/Issues.js +++ /dev/null @@ -1,112 +0,0 @@ -import i18n from '@dhis2/d2-i18n' -import PropTypes from 'prop-types' -import React from 'react' -import IssueCard from './IssueCard' -import styles from './Issues.module.css' - -const controls = { - dataElementsWithoutDataSet: i18n.t('Data elements without data set'), - dataElementsWithoutGroups: i18n.t('Data elements without groups'), - dataElementsViolatingExclusiveGroupSets: i18n.t( - 'Data elements violating exclusive group sets' - ), - dataElementsAssignedToDataSetsWithDifferentPeriodTypes: i18n.t( - 'Data elements assigned to data sets with different period types' - ), - dataSetsNotAssignedToOrganisationUnits: i18n.t( - 'Data sets not assigned to organisation units' - ), - indicatorsWithIdenticalFormulas: i18n.t( - 'Indicators with identical formulas' - ), - indicatorsWithoutGroups: i18n.t('Indicators without groups'), - invalidIndicatorNumerators: i18n.t('Invalid indicator numerators'), - invalidIndicatorDenominators: i18n.t('Invalid indicator denominators'), - indicatorsViolatingExclusiveGroupSets: i18n.t( - 'Indicators violating exclusive group sets' - ), - organisationUnitsWithCyclicReferences: i18n.t( - 'Organisation units with cyclic references' - ), - orphanedOrganisationUnits: i18n.t('Orphaned organisation units'), - organisationUnitsWithoutGroups: i18n.t('Organisation units without groups'), - organisationUnitsViolatingExclusiveGroupSets: i18n.t( - 'Organisation units violating exclusive group sets' - ), - organisationUnitGroupsWithoutGroupSets: i18n.t( - 'Organisation unit groups without group sets' - ), - validationRulesWithoutGroups: i18n.t('Validation rules without groups'), - invalidValidationRuleLeftSideExpressions: i18n.t( - 'Invalid validation rule left side expressions' - ), - invalidValidationRuleRightSideExpressions: i18n.t( - 'Invalid validation rule right side expressions' - ), - invalidProgramIndicatorExpressions: i18n.t( - 'Invalid program indicator expressions' - ), - invalidProgramIndicatorFilters: i18n.t('Invalid program indicator filters'), - dataElementsInDataSetNotInForm: i18n.t( - 'There are data elements in the form, but not in the form or sections' - ), - invalidCategoryCombos: i18n.t('Invalid category combinations'), - duplicatePeriods: i18n.t('Duplicate periods'), - programRulesWithNoCondition: i18n.t('Program rules with no condition'), - programRulesWithNoAction: i18n.t('Program rules with no action'), - programRulesWithNoPriority: i18n.t('Program rules with no priority'), - programRuleVariablesWithNoDataElement: i18n.t( - 'Program rule variables with no data element' - ), - programRuleVariablesWithNoAttribute: i18n.t( - 'Program rule variables with no attribute' - ), - programRuleActionsWithNoDataObject: i18n.t( - 'Program rule actions with no data object' - ), - programRuleActionsWithNoNotification: i18n.t( - 'Program rule actions with no notification' - ), - programRuleActionsWithNoSectionId: i18n.t( - 'Program rule actions with no section id' - ), - programRuleActionsWithNoStageId: i18n.t( - 'Program rule actions with no stage id' - ), - programIndicatorsWithNoExpression: i18n.t( - 'Program indicators with no expression' - ), -} - -const Issues = ({ issues }) => { - const errorElementskeys = Object.keys(issues) - - return ( - <div className={styles.issues}> - {errorElementskeys.map(element => { - const label = controls[element] - if (!label) { - return null - } - return ( - <IssueCard - key={element} - title={label} - content={issues[element]} - /> - ) - })} - {Object.keys(controls) - .filter(element => !errorElementskeys.includes(element)) - .map(element => ( - <IssueCard key={element} title={controls[element]} /> - ))} - </div> - ) -} - -Issues.propTypes = { - issues: PropTypes.object.isRequired, -} - -export default Issues diff --git a/src/pages/data-integrity/Issues/Issues.module.css b/src/pages/data-integrity/Issues/Issues.module.css deleted file mode 100644 index 96dbb605..00000000 --- a/src/pages/data-integrity/Issues/Issues.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.issues { - margin-bottom: var(--spacers-dp16); -} diff --git a/src/pages/data-integrity/Section.js b/src/pages/data-integrity/Section.js new file mode 100644 index 00000000..f89e4f94 --- /dev/null +++ b/src/pages/data-integrity/Section.js @@ -0,0 +1,103 @@ +import i18n from '@dhis2/d2-i18n' +import { + Checkbox, + DataTable, + DataTableHead, + DataTableBody, + DataTableRow, + DataTableColumnHeader, + DataTableCell, +} from '@dhis2/ui' +import PropTypes from 'prop-types' +import React from 'react' +import styles from './Section.module.css' +import Severity from './Severity' +import SeverityPropType from './SeverityPropType' + +const Section = ({ name, checks, selectedChecks, setSelectedChecks }) => { + const allSelected = checks.every(check => selectedChecks.has(check.name)) + const handleToggle = ({ value: checkName }) => { + const newSelectedChecks = new Set(selectedChecks) + if (selectedChecks.has(checkName)) { + newSelectedChecks.delete(checkName) + } else { + newSelectedChecks.add(checkName) + } + setSelectedChecks(newSelectedChecks) + } + const handleToggleAll = () => { + const newSelectedChecks = new Set(selectedChecks) + if (allSelected) { + for (const check of checks) { + newSelectedChecks.delete(check.name) + } + } else { + for (const check of checks) { + newSelectedChecks.add(check.name) + } + } + setSelectedChecks(newSelectedChecks) + } + + return ( + <section key={name} className={styles.section}> + <h2 className={styles.sectionName}>{name}</h2> + <DataTable> + <DataTableHead> + <DataTableRow> + <DataTableColumnHeader width="48px"> + <Checkbox + onChange={handleToggleAll} + checked={allSelected} + /> + </DataTableColumnHeader> + <DataTableColumnHeader> + {i18n.t('Check')} + </DataTableColumnHeader> + <DataTableColumnHeader> + {i18n.t('Description')} + </DataTableColumnHeader> + <DataTableColumnHeader> + {i18n.t('Severity')} + </DataTableColumnHeader> + </DataTableRow> + </DataTableHead> + <DataTableBody> + {checks.map(({ name, description, severity }) => ( + <DataTableRow key={name}> + <DataTableCell width="48px"> + <Checkbox + onChange={handleToggle} + value={name} + /> + </DataTableCell> + <DataTableCell> + {/* TODO: use name as i18n key */} + {name} + </DataTableCell> + <DataTableCell>{description}</DataTableCell> + <DataTableCell> + <Severity severity={severity} /> + </DataTableCell> + </DataTableRow> + ))} + </DataTableBody> + </DataTable> + </section> + ) +} + +Section.propTypes = { + checks: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + severity: SeverityPropType.isRequired, + description: PropTypes.string, + }).isRequired + ).isRequired, + name: PropTypes.string.isRequired, + selectedChecks: PropTypes.instanceOf(Set).isRequired, + setSelectedChecks: PropTypes.func.isRequired, +} + +export default Section diff --git a/src/pages/data-integrity/Section.module.css b/src/pages/data-integrity/Section.module.css new file mode 100644 index 00000000..5de75c93 --- /dev/null +++ b/src/pages/data-integrity/Section.module.css @@ -0,0 +1,10 @@ +.section { + margin-bottom: var(--spacers-dp16); +} + +.sectionName { + font-weight: 500; + font-size: 18px; + margin-top: 0; + margin-bottom: var(--spacers-dp8); +} diff --git a/src/pages/data-integrity/Severity.js b/src/pages/data-integrity/Severity.js new file mode 100644 index 00000000..e50456dd --- /dev/null +++ b/src/pages/data-integrity/Severity.js @@ -0,0 +1,47 @@ +import i18n from '@dhis2/d2-i18n' +import { Tag, Tooltip } from '@dhis2/ui' +import React from 'react' +import SeverityPropType from './SeverityPropType' + +const severities = { + INFO: { + displayName: i18n.t('Information'), + description: i18n.t('For information only'), + }, + WARNING: { + displayName: i18n.t('Warning'), + description: i18n.t('May be a problem, but not necessarily an error'), + }, + SEVERE: { + displayName: i18n.t('Severe'), + description: i18n.t( + 'An error which should be fixed, but which may not necessarily lead to the system not functioning' + ), + }, + CRITICAL: { + displayName: i18n.t('Critical'), + description: i18n.t( + 'An error which must be fixed, and which may lead to end-user error or system crashes' + ), + }, +} + +const Severity = ({ severity }) => { + const { displayName, description } = severities[severity] + + return ( + <Tooltip + content={description} + neutral={severity === 'INFO'} + negative={severity === 'CRITICAL' || severity === 'SEVERE'} + > + <Tag>{displayName}</Tag> + </Tooltip> + ) +} + +Severity.propTypes = { + severity: SeverityPropType.isRequired, +} + +export default Severity diff --git a/src/pages/data-integrity/SeverityPropType.js b/src/pages/data-integrity/SeverityPropType.js new file mode 100644 index 00000000..ffa144d8 --- /dev/null +++ b/src/pages/data-integrity/SeverityPropType.js @@ -0,0 +1,10 @@ +import PropTypes from 'prop-types' + +const SeverityPropType = PropTypes.oneOf([ + 'INFO', + 'WARNING', + 'SEVERE', + 'CRITICAL', +]) + +export default SeverityPropType diff --git a/src/pages/data-integrity/use-data-integrity.js b/src/pages/data-integrity/use-data-integrity.js deleted file mode 100644 index e4f4b171..00000000 --- a/src/pages/data-integrity/use-data-integrity.js +++ /dev/null @@ -1,36 +0,0 @@ -import { useDataMutation } from '@dhis2/app-runtime' -import { usePoll } from '../../hooks/use-poll' - -const pollQuery = { - resource: 'system/taskSummaries/DATA_INTEGRITY', - id: ({ jobId }) => jobId, -} - -const startDataIntegrityCheckMutation = { - resource: 'dataIntegrity', - type: 'create', -} - -export const useDataIntegrity = () => { - const poll = usePoll({ - query: pollQuery, - interval: 3000, - checkDone: data => data, - }) - const [startDataIntegrityCheck, { loading, error }] = useDataMutation( - startDataIntegrityCheckMutation, - { - onComplete: data => { - const { id: jobId } = data.response - poll.start({ jobId }) - }, - } - ) - - return { - startDataIntegrityCheck, - loading: loading || poll.polling, - error: error || poll.error, - issues: poll.data, - } -}