From 1f9a1339a6f772f7900f7130b85017f220a166b7 Mon Sep 17 00:00:00 2001 From: Hendrik de Graaf Date: Tue, 14 Jan 2025 10:11:15 +0100 Subject: [PATCH 01/37] feat: fetch superset base url with system settings (#3181) * feat: fetch superset base url with system settings * fix: add hook dependencies and ensure fetchSuperSetBaseUrl is a stable reference * feat: add hooks to read global superset data * chore: ensure set in superset is in lower case * chore: adjust request error message --- src/api/supersetGateway.js | 30 ++++++++++++++++++++++++ src/api/systemSettings.js | 2 ++ src/components/SystemSettingsProvider.js | 28 +++++++++++++++------- 3 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 src/api/supersetGateway.js diff --git a/src/api/supersetGateway.js b/src/api/supersetGateway.js new file mode 100644 index 000000000..5e5e4231f --- /dev/null +++ b/src/api/supersetGateway.js @@ -0,0 +1,30 @@ +import { useConfig } from '@dhis2/app-service-config' +import { useCallback, useMemo } from 'react' + +export const useFetchSupersetBaseUrl = () => { + const { baseUrl } = useConfig() + const url = useMemo( + () => + new URL( + 'superset-gateway/api/info', + baseUrl === '..' + ? window.location.href.split('dhis-web-dashboard/')[0] + : `${baseUrl}/` + )?.href, + [baseUrl] + ) + + const fetchSupersetBaseUrl = useCallback(async () => { + const response = await fetch(url) + if (!response.ok) { + throw new Error( + `Could not fetch info from the superset gateway: STATUS ${response.status}` + ) + } + + const data = await response.json() + return data.supersetBaseUrl + }, [url]) + + return fetchSupersetBaseUrl +} diff --git a/src/api/systemSettings.js b/src/api/systemSettings.js index c960bc925..25e8ed61b 100644 --- a/src/api/systemSettings.js +++ b/src/api/systemSettings.js @@ -19,6 +19,7 @@ const SYSTEM_SETTINGS = [ 'keyHideWeeklyPeriods', 'keyHideBiWeeklyPeriods', 'startModuleEnableLightweight', + 'keyEmbeddedDashboardsEnabled', ] const SYSTEM_SETTINGS_REMAPPINGS = { @@ -32,6 +33,7 @@ const SYSTEM_SETTINGS_REMAPPINGS = { keyHideMonthlyPeriods: 'hideMonthlyPeriods', keyHideWeeklyPeriods: 'hideWeeklyPeriods', keyHideBiWeeklyPeriods: 'hideBiWeeklyPeriods', + keyEmbeddedDashboardsEnabled: 'embeddedDashboardsEnabled', } export const renameSystemSettings = (settings) => { diff --git a/src/components/SystemSettingsProvider.js b/src/components/SystemSettingsProvider.js index ddadf9007..135348854 100644 --- a/src/components/SystemSettingsProvider.js +++ b/src/components/SystemSettingsProvider.js @@ -1,6 +1,7 @@ import { useDataEngine } from '@dhis2/app-runtime' import PropTypes from 'prop-types' import React, { useContext, useState, useEffect, createContext } from 'react' +import { useFetchSupersetBaseUrl } from '../api/supersetGateway.js' import { systemSettingsQuery, renameSystemSettings, @@ -12,6 +13,7 @@ export const SystemSettingsCtx = createContext({}) const SystemSettingsProvider = ({ children }) => { const [settings, setSettings] = useState(null) const engine = useDataEngine() + const fetchSupersetBaseUrl = useFetchSupersetBaseUrl() useEffect(() => { async function fetchData() { @@ -26,17 +28,19 @@ const SystemSettingsProvider = ({ children }) => { }, } ) + const resolvedSystemSettings = { + ...renameSystemSettings(DEFAULT_SETTINGS), + ...renameSystemSettings(systemSettings), + } + if (resolvedSystemSettings.embeddedDashboardsEnabled) { + resolvedSystemSettings.supersetBaseUrl = + await fetchSupersetBaseUrl() + } - setSettings( - Object.assign( - {}, - renameSystemSettings(DEFAULT_SETTINGS), - renameSystemSettings(systemSettings) - ) - ) + setSettings(resolvedSystemSettings) } fetchData() - }, []) + }, [engine, fetchSupersetBaseUrl]) return ( useContext(SystemSettingsCtx) +export const useHasSupersetSupport = () => { + const { embeddedDashboardsEnabled, supersetBaseUrl } = useSystemSettings() + return embeddedDashboardsEnabled && !!supersetBaseUrl +} +export const useSupersetBaseUrl = () => { + const { supersetBaseUrl } = useSystemSettings() + return supersetBaseUrl ?? null +} From b159ad9860a1a1244841c3c7ecbf2c2425bf9734 Mon Sep 17 00:00:00 2001 From: Hendrik de Graaf Date: Mon, 20 Jan 2025 12:42:40 +0100 Subject: [PATCH 02/37] feat: create embedded superset dashboard (#3185) * chore: ignore aider ai files * refactor: extract CreateDashboardButton and NavigationMenuDropdownButton components * fix: read system settings from nested object in hooks * feat: creation flow with conditional support for superset embedded dashboards * chore: remove unused import * feat: add form that can be used for creating and editing superset embedded dashboards * chore: add file extensions to imports * chore: addjust formatting * feat: create superset embedded dashboards * fix: complete hook dependencies * fix: consolidate keyboard navigation implementation across modal forms * fix: adjust modal form, radio card styles * chore: add reminder to add tests for UUID validation * fix: set focus on first fiel in modal form instead of setting focus to fieldset * fix: align radio card outline visibility with other inputs * chore: reorganise files --------- Co-authored-by: Joseph John Aas Cooper <33054985+cooper-joe@users.noreply.github.com> # Conflicts: # i18n/en.pot --- .gitignore | 1 + i18n/en.pot | 81 +++++++++-- .../CreateSupersetEmbeddedDashboard.js | 130 ++++++++++++++++++ .../SupersetEmbeddedDashboardFields.js | 102 ++++++++++++++ .../index.js | 1 + ...CreateSupersetEmbeddedDashboard.module.css | 5 + ...SupersetEmbeddedDashboardFields.module.css | 11 ++ .../ChooseDashboardTypeModal.js | 99 +++++++++++++ .../CreateDashboardButton.js | 65 +++++++++ .../DashboardTypeRadio.js | 61 ++++++++ .../IconDashboardExternal.js | 18 +++ .../IconDashboardInternal.js | 32 +++++ .../CreateDashboardButton/index.js | 1 + .../ChooseDashboardTypeModal.module.css | 11 ++ .../styles/CreateDashboardButton.module.css | 5 + .../styles/DashboardTypeRadio.module.css | 71 ++++++++++ src/components/DashboardsBar/DashboardsBar.js | 52 ++----- .../NavigationMenuDropdownButton.js | 28 ++++ .../DashboardsBar/NavigationMenu/index.js | 3 +- .../NavigationMenuDropdownButton.module.css | 5 + .../styles/DashboardsBar.module.css | 3 +- src/components/SystemSettingsProvider.js | 8 +- ...useSupersetEmbeddedDashboardFieldsState.js | 81 +++++++++++ yarn.lock | 2 +- 24 files changed, 813 insertions(+), 63 deletions(-) create mode 100644 src/components/ConfigureSupersetEmbeddedDashboardModal/CreateSupersetEmbeddedDashboard.js create mode 100644 src/components/ConfigureSupersetEmbeddedDashboardModal/SupersetEmbeddedDashboardFields.js create mode 100644 src/components/ConfigureSupersetEmbeddedDashboardModal/index.js create mode 100644 src/components/ConfigureSupersetEmbeddedDashboardModal/styles/CreateSupersetEmbeddedDashboard.module.css create mode 100644 src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardFields.module.css create mode 100644 src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js create mode 100644 src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js create mode 100644 src/components/DashboardsBar/CreateDashboardButton/DashboardTypeRadio.js create mode 100644 src/components/DashboardsBar/CreateDashboardButton/IconDashboardExternal.js create mode 100644 src/components/DashboardsBar/CreateDashboardButton/IconDashboardInternal.js create mode 100644 src/components/DashboardsBar/CreateDashboardButton/index.js create mode 100644 src/components/DashboardsBar/CreateDashboardButton/styles/ChooseDashboardTypeModal.module.css create mode 100644 src/components/DashboardsBar/CreateDashboardButton/styles/CreateDashboardButton.module.css create mode 100644 src/components/DashboardsBar/CreateDashboardButton/styles/DashboardTypeRadio.module.css create mode 100644 src/components/DashboardsBar/NavigationMenu/NavigationMenuDropdownButton.js create mode 100644 src/components/DashboardsBar/NavigationMenu/styles/NavigationMenuDropdownButton.module.css create mode 100644 src/modules/useSupersetEmbeddedDashboardFieldsState.js diff --git a/.gitignore b/.gitignore index 5bcf19efc..58a80937e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ cypress.env.json cypress/screenshots cypress/videos cypress/downloads +.aider* diff --git a/i18n/en.pot b/i18n/en.pot index e707c3631..ea3db942b 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -11,8 +11,68 @@ msgstr "" msgid "Untitled dashboard" msgstr "Untitled dashboard" -msgid "Dashboards" -msgstr "Dashboards" +msgid "New dashboard: configure external source (superset)" +msgstr "New dashboard: configure external source (superset)" + +msgid "Could not create dashboard" +msgstr "Could not create dashboard" + +msgid "An unknown error occurred" +msgstr "An unknown error occurred" + +msgid "Save dashboard" +msgstr "Save dashboard" + +msgid "Back" +msgstr "Back" + +msgid "Title" +msgstr "Title" + +msgid "Code" +msgstr "Code" + +msgid "Description" +msgstr "Description" + +msgid "Superset Embed ID" +msgstr "Superset Embed ID" + +msgid "Invalid UUID" +msgstr "Invalid UUID" + +msgid "Options" +msgstr "Options" + +msgid "Show chart controls on dashboard items" +msgstr "Show chart controls on dashboard items" + +msgid "Show filters" +msgstr "Show filters" + +msgid "New dashboard: choose type" +msgstr "New dashboard: choose type" + +msgid "Internal: Data from {{systemName}}" +msgstr "Internal: Data from {{systemName}}" + +msgid "Show data and visualizations from this DHIS2 instance." +msgstr "Show data and visualizations from this DHIS2 instance." + +msgid "External: Data from another source" +msgstr "External: Data from another source" + +msgid "Embed a dashboard from a third-party source, like Superset." +msgstr "Embed a dashboard from a third-party source, like Superset." + +msgid "Create dashboard" +msgstr "Create dashboard" + +msgid "Configure source" +msgstr "Configure source" + +msgid "Cancel" +msgstr "Cancel" msgid "The dashboard couldn't be made available offline. Try again." msgstr "The dashboard couldn't be made available offline. Try again." @@ -81,9 +141,6 @@ msgstr "No, cancel" msgid "Yes, clear filters and sync" msgstr "Yes, clear filters and sync" -msgid "Cancel" -msgstr "Cancel" - msgid "Confirm" msgstr "Confirm" @@ -120,17 +177,11 @@ msgstr "Search for a dashboard" msgid "No dashboards found for \"{{- filterText}}\"" msgstr "No dashboards found for \"{{- filterText}}\"" -msgid "There was a problem loading this dashboard item" -msgstr "There was a problem loading this dashboard item" - -msgid "Open menu" -msgstr "Open menu" - -msgid "Open in {{appName}}" -msgstr "Open in {{appName}}" +msgid "Dashboards" +msgstr "Dashboards" -msgid "View fullscreen" -msgstr "View fullscreen" +msgid "{{appKey}} app not found" +msgstr "{{appKey}} app not found" msgid "Remove this item" msgstr "Remove this item" diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/CreateSupersetEmbeddedDashboard.js b/src/components/ConfigureSupersetEmbeddedDashboardModal/CreateSupersetEmbeddedDashboard.js new file mode 100644 index 000000000..59709d921 --- /dev/null +++ b/src/components/ConfigureSupersetEmbeddedDashboardModal/CreateSupersetEmbeddedDashboard.js @@ -0,0 +1,130 @@ +import { useDataMutation } from '@dhis2/app-runtime' +import i18n from '@dhis2/d2-i18n' +import { + Button, + Modal, + ModalActions, + ModalContent, + ModalTitle, + NoticeBox, +} from '@dhis2/ui' +import PropTypes from 'prop-types' +import React, { useCallback, useState } from 'react' +import { useDispatch } from 'react-redux' +import { useHistory } from 'react-router-dom' +import { tFetchDashboards } from '../../actions/dashboards.js' +import { useSupersetEmbeddedDashboardFieldsState } from '../../modules/useSupersetEmbeddedDashboardFieldsState.js' +import styles from './styles/CreateSupersetEmbeddedDashboard.module.css' +import { SupersetEmbeddedDashboardFields } from './SupersetEmbeddedDashboardFields.js' + +const postDashboardQuery = { + resource: 'dashboards', + type: 'create', + data: ({ values }) => ({ + name: values.title || 'Untitled dashboard', + description: values.description, + code: values.code, + embedded: { + provider: 'SUPERSET', + id: values.supersetEmbedId, + options: { + hideTab: false, + hideChartControls: !values.showChartControls, + filters: { + visible: values.showFilters, + expanded: false, + }, + }, + }, + }), +} + +export const CreateSupersetEmbeddedDashboard = ({ + backToChooseDashboardModal, + closeModal, +}) => { + const dispatch = useDispatch() + const history = useHistory() + const [loading, setLoading] = useState(false) + const [postDashboard, { error }] = useDataMutation(postDashboardQuery) + const { + hasFieldChanges, + isSupersetEmbedIdValid, + isSupersetEmbedIdFieldTouched, + values, + onChange, + onSupersetEmbedIdFieldBlur, + } = useSupersetEmbeddedDashboardFieldsState() + const handleSubmit = useCallback( + async (event) => { + event.preventDefault() + setLoading(true) + const { response } = await postDashboard({ values }) + await dispatch(tFetchDashboards()) + closeModal() + history.push(`/${response.uid}`) + }, + [values, postDashboard, closeModal, dispatch, history] + ) + + return ( + +
+ + {i18n.t( + 'New dashboard: configure external source (superset)', + { nsSeparator: '###' } + )} + + + + {error && ( + + {error?.details?.response?.errorReports[0] + ?.message ?? + i18n.t('An unknown error occurred')} + + )} + + +
+ + +
+
+
+
+ ) +} + +CreateSupersetEmbeddedDashboard.propTypes = { + backToChooseDashboardModal: PropTypes.func, + closeModal: PropTypes.func, +} diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/SupersetEmbeddedDashboardFields.js b/src/components/ConfigureSupersetEmbeddedDashboardModal/SupersetEmbeddedDashboardFields.js new file mode 100644 index 000000000..d32a8a8e6 --- /dev/null +++ b/src/components/ConfigureSupersetEmbeddedDashboardModal/SupersetEmbeddedDashboardFields.js @@ -0,0 +1,102 @@ +import i18n from '@dhis2/d2-i18n' +import { CheckboxField, InputField, TextAreaField } from '@dhis2/ui' +import PropTypes from 'prop-types' +import React from 'react' +import { fieldNames } from '../../modules/useSupersetEmbeddedDashboardFieldsState.js' +import styles from './styles/SupersetEmbeddedDashboardFields.module.css' + +export const SupersetEmbeddedDashboardFields = ({ + values, + submitting, + onChange, + onSupersetEmbedIdFieldBlur, + isSupersetEmbedIdValid, + isSupersetEmbedIdFieldTouched, +}) => { + const supersetEmbedIdFieldHasError = + isSupersetEmbedIdFieldTouched && !isSupersetEmbedIdValid + return ( + <> + + + + +
+ {i18n.t('Options')} + + +
+ + ) +} + +SupersetEmbeddedDashboardFields.propTypes = { + isSupersetEmbedIdFieldTouched: PropTypes.bool, + isSupersetEmbedIdValid: PropTypes.bool, + submitting: PropTypes.bool, + values: PropTypes.shape({ + code: PropTypes.string, + description: PropTypes.string, + showChartControls: PropTypes.bool, + showFilters: PropTypes.bool, + supersetEmbedId: PropTypes.string, + title: PropTypes.string, + }), + onChange: PropTypes.func, + onSupersetEmbedIdFieldBlur: PropTypes.func, +} diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/index.js b/src/components/ConfigureSupersetEmbeddedDashboardModal/index.js new file mode 100644 index 000000000..d5d02a560 --- /dev/null +++ b/src/components/ConfigureSupersetEmbeddedDashboardModal/index.js @@ -0,0 +1 @@ +export { CreateSupersetEmbeddedDashboard } from './CreateSupersetEmbeddedDashboard.js' diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/CreateSupersetEmbeddedDashboard.module.css b/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/CreateSupersetEmbeddedDashboard.module.css new file mode 100644 index 000000000..daf5ea517 --- /dev/null +++ b/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/CreateSupersetEmbeddedDashboard.module.css @@ -0,0 +1,5 @@ +.buttonStrip { + display: flex; + flex-direction: row-reverse; + gap: var(--spacers-dp8); +} diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardFields.module.css b/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardFields.module.css new file mode 100644 index 000000000..bde8a99dd --- /dev/null +++ b/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardFields.module.css @@ -0,0 +1,11 @@ +.textField { + margin-block-end: var(--spacers-dp12); +} +.options { + all: unset; +} +.options legend { + font-size: 14px; + font-weight: 500; + margin-block-end: var(--spacers-dp4); +} diff --git a/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js b/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js new file mode 100644 index 000000000..fa91dd50d --- /dev/null +++ b/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js @@ -0,0 +1,99 @@ +import { useConfig } from '@dhis2/app-runtime' +import i18n from '@dhis2/d2-i18n' +import { + Button, + Modal, + ModalActions, + ModalContent, + ModalTitle, +} from '@dhis2/ui' +import PropTypes from 'prop-types' +import React, { useCallback, useState } from 'react' +import { DashboardTypeRadio } from './DashboardTypeRadio.js' +import { IconDashboardExternal } from './IconDashboardExternal.js' +import { IconDashboardInternal } from './IconDashboardInternal.js' +import styles from './styles/ChooseDashboardTypeModal.module.css' + +const TYPE_INTERNAL = 'INTERNAL' +const TYPE_SUPERSET = 'SUPERSET' + +export const ChooseDashboardTypeModal = ({ + onCancel, + onSelectSuperset, + onSelectInternal, +}) => { + const { + systemInfo: { systemName }, + } = useConfig() + const [selectedType, setSelectedType] = useState(TYPE_INTERNAL) + const handleDashboardTypeChange = useCallback((event) => { + setSelectedType(event.target.value) + }, []) + const isInternal = selectedType === TYPE_INTERNAL + + return ( + +
+ + {i18n.t('New dashboard: choose type', { + nsSeparator: '###', + })} + + +
+ } + title={i18n.t( + 'Internal: Data from {{systemName}}', + { + systemName, + nsSeparator: '###', + } + )} + subtitle={i18n.t( + 'Show data and visualizations from this DHIS2 instance.' + )} + /> + } + title={i18n.t( + 'External: Data from another source', + { + nsSeparator: '###', + } + )} + subtitle={i18n.t( + 'Embed a dashboard from a third-party source, like Superset.' + )} + /> +
+
+ +
+ + +
+
+
+
+ ) +} + +ChooseDashboardTypeModal.propTypes = { + onCancel: PropTypes.func, + onSelectInternal: PropTypes.func, + onSelectSuperset: PropTypes.func, +} diff --git a/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js b/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js new file mode 100644 index 000000000..763064e3b --- /dev/null +++ b/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js @@ -0,0 +1,65 @@ +import { Button, IconAdd16 } from '@dhis2/ui' +import React, { useCallback, useState } from 'react' +import { useHistory } from 'react-router-dom' +import { CreateSupersetEmbeddedDashboard } from '../../ConfigureSupersetEmbeddedDashboardModal/index.js' +import { useHasSupersetSupport } from '../../SystemSettingsProvider.js' +import { ChooseDashboardTypeModal } from './ChooseDashboardTypeModal.js' +import styles from './styles/CreateDashboardButton.module.css' + +export const CreateDashboardButton = () => { + const history = useHistory() + const hasSupersetSupport = useHasSupersetSupport() + const [isChooseDashboardTypeModalOpen, setIsChooseDashboardTypeModalOpen] = + useState(false) + const [isCreateSupersetDashboardOpen, setIsCreateSupersetDashboardOpen] = + useState(false) + const navigateToNewInternalDashboardView = useCallback(() => { + history.push('/new') + }, [history]) + const handleCreateButtonClick = useCallback(() => { + if (hasSupersetSupport) { + setIsChooseDashboardTypeModalOpen(true) + } else { + navigateToNewInternalDashboardView() + } + }, [hasSupersetSupport, navigateToNewInternalDashboardView]) + + return ( + <> + + + )} + + )} + + ) +} diff --git a/src/pages/view/ViewDashboardContent.js b/src/pages/view/ViewDashboardContent.js index b70d522ac..de55c4df8 100644 --- a/src/pages/view/ViewDashboardContent.js +++ b/src/pages/view/ViewDashboardContent.js @@ -1,11 +1,14 @@ import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' import React from 'react' +import { useSelector } from 'react-redux' import { Link } from 'react-router-dom' import LoadingMask from '../../components/LoadingMask.js' import Notice from '../../components/Notice.js' +import { sGetSelectedIsEmbedded } from '../../reducers/selected.js' import { ROUTE_START_PATH } from '../start/index.js' import { Description } from './Description.js' +import { EmbeddedSupersetDashboard } from './EmbeddedSupersetDashboard.js' import FilterBar from './FilterBar/FilterBar.js' import ItemGrid from './ItemGrid.js' import classes from './styles/ViewDashboard.module.css' @@ -16,6 +19,8 @@ export const ViewDashboardContent = ({ loadFailed, isCached, }) => { + const isEmbeddedDashboard = useSelector(sGetSelectedIsEmbedded) + if (loading) { return } @@ -35,8 +40,12 @@ export const ViewDashboardContent = ({ return ( <> - - + {!isEmbeddedDashboard && } + {isEmbeddedDashboard ? ( + + ) : ( + + )} ) } diff --git a/src/pages/view/styles/Description.module.css b/src/pages/view/styles/Description.module.css index c37420b61..de77bb22e 100644 --- a/src/pages/view/styles/Description.module.css +++ b/src/pages/view/styles/Description.module.css @@ -9,3 +9,7 @@ color: var(--colors-grey600); font-style: italic; } + +.padded { + padding-inline: var(--spacers-dp8); +} diff --git a/src/pages/view/styles/EmbeddedSupersetDashboard.module.css b/src/pages/view/styles/EmbeddedSupersetDashboard.module.css new file mode 100644 index 000000000..4cb276dee --- /dev/null +++ b/src/pages/view/styles/EmbeddedSupersetDashboard.module.css @@ -0,0 +1,66 @@ +.container, +.iframeHost { + block-size: 100%; + flex-grow: 1; + display: flex; + flex-direction: column; +} +.container { + position: relative; +} + +.iframeHost { + opacity: 1; + transition: opacity 120ms ease-in; +} +.iframeHost :global(iframe) { + all: unset; + inline-size: 100%; + flex-grow: 1; +} +.iframeHost.opaque { + opacity: 0; +} + +.contentOverlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.contentOverlay > :global(.error) { + align-self: baseline; + min-inline-size: 400px; + margin-block-start: var(--spacers-dp24); +} +.errorText { + margin-block-start: 0; + margin-block-end: var(--spacers-dp8); +} + +.loaderContainer { + display: flex; + gap: var(--spacers-dp8); + animation: showLoader; + animation-duration: 120ms; + animation-delay: 400ms; + animation-fill-mode: forwards; + opacity: 0; +} +.loaderContainer > p { + all: unset; + color: var(--colors-grey600); +} +@keyframes showLoader { + 0% { + opacity: 0; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } +} diff --git a/src/reducers/selected.js b/src/reducers/selected.js index df772d96f..2a3fad080 100644 --- a/src/reducers/selected.js +++ b/src/reducers/selected.js @@ -12,6 +12,7 @@ const SELECTED_PROPERTIES = { dashboardItems: [], layout: [], itemConfig: {}, + embedded: undefined, } export default (state = DEFAULT_SELECTED_STATE, action) => { @@ -37,6 +38,24 @@ export const sGetSelected = (state) => state.selected export const sGetSelectedId = (state) => sGetSelected(state).id +export const sGetSelectedIsEmbedded = (state) => !!sGetSelected(state).embedded + +export const sGetSelectedSupersetEmbedData = (state) => { + const embedData = sGetSelected(state).embedded + return { + id: embedData.id, + dashboardUiConfig: { + hideTitle: true, + hideTab: true, + hideChartControls: embedData.options.hideChartControls, + filters: { + visible: embedData.options.filters.visible, + expanded: false, + }, + }, + } +} + export const sGetSelectedDisplayName = (state) => sGetSelected(state).displayName diff --git a/yarn.lock b/yarn.lock index 81fd8ae5d..ca13c9b6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3799,6 +3799,19 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@superset-ui/embedded-sdk@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@superset-ui/embedded-sdk/-/embedded-sdk-0.1.3.tgz#32279aff9ae5479a73f70ac1ec41b0b15c24381c" + integrity sha512-7NJMTx+rWzOzY0CHTFkh+iZjxsvz9ga8HJxpAN/LaY+uM6jBObbzo8KXah6qcX6no2wEfOQYdSazDPfWOrnHQw== + dependencies: + "@superset-ui/switchboard" "^0.20.3" + jwt-decode "^4.0.0" + +"@superset-ui/switchboard@^0.20.3": + version "0.20.3" + resolved "https://registry.yarnpkg.com/@superset-ui/switchboard/-/switchboard-0.20.3.tgz#9037c64408dfb6b0291671fa0a136fb32b9511db" + integrity sha512-qEMXFwdRLfXug4gXXdBEGpFtBWZoxdZkCJLBVxj1IR8cQvSqjkWAQOzSSYYdcIeREWqi8iP+iK6apNV1ZQCKcA== + "@surma/rollup-plugin-off-main-thread@^2.2.3": version "2.2.3" resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" @@ -11979,6 +11992,11 @@ just-diff@^5.0.1: resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-5.2.0.tgz#60dca55891cf24cd4a094e33504660692348a241" integrity sha512-6ufhP9SHjb7jibNFrNxyFZ6od3g+An6Ai9mhGRvcYe8UJlH0prseN64M+6ZBBUoKYHZsitDP42gAJ8+eVWr3lw== +jwt-decode@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" + integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== + keyv@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" From a06712793139b11862be621c0a08c7c26fcde488 Mon Sep 17 00:00:00 2001 From: Hendrik de Graaf Date: Thu, 23 Jan 2025 13:56:14 +0100 Subject: [PATCH 04/37] feat: adjust dashboards bar for external dashboards (#3193) * feat: show external data tag in dashboards-bar for embedded dashboards * fix: disable unavailable buttons when showing embedded dashboard * chore: remove unused import * chore: fix broken unit test * feat: edit and delete embedded dashboards using a modal * refactor: encapsulate superset dashboard mutation logic in a hook * chore: remove redundant use of nsSeparator in i18n.t call # Conflicts: # i18n/en.pot --- i18n/en.pot | 54 ++++- .../CreateSupersetEmbeddedDashboard.js | 26 +-- .../UpdateSupersetEmbeddedDashboard.js | 188 ++++++++++++++++++ .../index.js | 1 + ...CreateSupersetEmbeddedDashboard.module.css | 5 - .../SupersetEmbeddedDashboardModal.module.css | 36 ++++ .../InformationBlock/ActionsBar.js | 51 ++++- .../InformationBlock/FilterSelector.js | 15 +- .../InformationBlock/InformationBlock.js | 12 +- .../InformationBlock/LastUpdatedTag.js | 2 +- .../__tests__/FilterSelector.spec.js | 2 +- .../DropdownButton/DropdownButton.js | 8 +- .../DropdownButton/DropdownButton.module.css | 4 + ...rseSupersetEmbeddedDashboardFieldValues.js | 17 ++ ...useSupersetEmbeddedDashboardFieldsState.js | 79 +++++--- .../useSupersetEmbeddedDashboardMutation.js | 107 ++++++++++ 16 files changed, 528 insertions(+), 79 deletions(-) create mode 100644 src/components/ConfigureSupersetEmbeddedDashboardModal/UpdateSupersetEmbeddedDashboard.js delete mode 100644 src/components/ConfigureSupersetEmbeddedDashboardModal/styles/CreateSupersetEmbeddedDashboard.module.css create mode 100644 src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardModal.module.css create mode 100644 src/modules/parseSupersetEmbeddedDashboardFieldValues.js create mode 100644 src/modules/useSupersetEmbeddedDashboardMutation.js diff --git a/i18n/en.pot b/i18n/en.pot index c080ada14..0915b3180 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -50,6 +50,37 @@ msgstr "Show chart controls on dashboard items" msgid "Show filters" msgstr "Show filters" +msgid "Delete dashboard" +msgstr "Delete dashboard" + +msgid "" +"Deleting dashboard \"{{ dashboardName }}\" will remove it for all users. " +"This action cannot be undone. Are you sure you want to permanently delete " +"this dashboard?" +msgstr "" +"Deleting dashboard \"{{ dashboardName }}\" will remove it for all users. " +"This action cannot be undone. Are you sure you want to permanently delete " +"this dashboard?" + +msgid "" +"Note: the source dashboard embedded by this external dashboard will not be " +"deleted." +msgstr "" +"Note: the source dashboard embedded by this external dashboard will not be " +"deleted." + +msgid "Delete" +msgstr "Delete" + +msgid "Cancel" +msgstr "Cancel" + +msgid "Edit external dashboard" +msgstr "Edit external dashboard" + +msgid "Update dashboard" +msgstr "Update dashboard" + msgid "New dashboard: choose type" msgstr "New dashboard: choose type" @@ -71,8 +102,8 @@ msgstr "Create dashboard" msgid "Configure source" msgstr "Configure source" -msgid "Cancel" -msgstr "Cancel" +msgid "Not available for embedded dashboards" +msgstr "Not available for embedded dashboards" msgid "The dashboard couldn't be made available offline. Try again." msgstr "The dashboard couldn't be made available offline. Try again." @@ -153,6 +184,9 @@ msgstr "Failed to unstar the dashboard" msgid "Failed to star the dashboard" msgstr "Failed to star the dashboard" +msgid "External data" +msgstr "External data" + msgid "Offline data last updated {{timeAgo}}" msgstr "Offline data last updated {{timeAgo}}" @@ -351,6 +385,15 @@ msgstr "Apps" msgid "Users" msgstr "Users" +msgid "Could not update dashboard {{name}}" +msgstr "Could not update dashboard {{name}}" + +msgid "Could not delete dashboard {{name}}" +msgstr "Could not delete dashboard {{name}}" + +msgid "Could not load dashboard details" +msgstr "Could not load dashboard details" + msgid "" "Failed to save dashboard. You might be offline or not have access to edit " "this dashboard." @@ -393,9 +436,6 @@ msgstr "Translate" msgid "Cannot delete this dashboard while offline" msgstr "Cannot delete this dashboard while offline" -msgid "Delete" -msgstr "Delete" - msgid "Exit without saving" msgstr "Exit without saving" @@ -409,10 +449,6 @@ msgid "" "Deleting dashboard \"{{- dashboardName }}\" will remove it for all users. " "This action cannot be undone. Are you sure you want to permanently delete " "this dashboard?" -msgstr "" -"Deleting dashboard \"{{- dashboardName }}\" will remove it for all users. " -"This action cannot be undone. Are you sure you want to permanently delete " -"this dashboard?" msgid "Discard changes" msgstr "Discard changes" diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/CreateSupersetEmbeddedDashboard.js b/src/components/ConfigureSupersetEmbeddedDashboardModal/CreateSupersetEmbeddedDashboard.js index 59709d921..f34f99f92 100644 --- a/src/components/ConfigureSupersetEmbeddedDashboardModal/CreateSupersetEmbeddedDashboard.js +++ b/src/components/ConfigureSupersetEmbeddedDashboardModal/CreateSupersetEmbeddedDashboard.js @@ -13,30 +13,15 @@ import React, { useCallback, useState } from 'react' import { useDispatch } from 'react-redux' import { useHistory } from 'react-router-dom' import { tFetchDashboards } from '../../actions/dashboards.js' +import { parseSupersetEmbeddedDashboardFieldValues } from '../../modules/parseSupersetEmbeddedDashboardFieldValues.js' import { useSupersetEmbeddedDashboardFieldsState } from '../../modules/useSupersetEmbeddedDashboardFieldsState.js' -import styles from './styles/CreateSupersetEmbeddedDashboard.module.css' +import styles from './styles/SupersetEmbeddedDashboardModal.module.css' import { SupersetEmbeddedDashboardFields } from './SupersetEmbeddedDashboardFields.js' const postDashboardQuery = { resource: 'dashboards', type: 'create', - data: ({ values }) => ({ - name: values.title || 'Untitled dashboard', - description: values.description, - code: values.code, - embedded: { - provider: 'SUPERSET', - id: values.supersetEmbedId, - options: { - hideTab: false, - hideChartControls: !values.showChartControls, - filters: { - visible: values.showFilters, - expanded: false, - }, - }, - }, - }), + data: ({ values }) => parseSupersetEmbeddedDashboardFieldValues(values), } export const CreateSupersetEmbeddedDashboard = ({ @@ -46,7 +31,9 @@ export const CreateSupersetEmbeddedDashboard = ({ const dispatch = useDispatch() const history = useHistory() const [loading, setLoading] = useState(false) - const [postDashboard, { error }] = useDataMutation(postDashboardQuery) + const [postDashboard, { error }] = useDataMutation(postDashboardQuery, { + onError: () => setLoading(false), + }) const { hasFieldChanges, isSupersetEmbedIdValid, @@ -61,6 +48,7 @@ export const CreateSupersetEmbeddedDashboard = ({ setLoading(true) const { response } = await postDashboard({ values }) await dispatch(tFetchDashboards()) + setLoading(false) closeModal() history.push(`/${response.uid}`) }, diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/UpdateSupersetEmbeddedDashboard.js b/src/components/ConfigureSupersetEmbeddedDashboardModal/UpdateSupersetEmbeddedDashboard.js new file mode 100644 index 000000000..1044e6f74 --- /dev/null +++ b/src/components/ConfigureSupersetEmbeddedDashboardModal/UpdateSupersetEmbeddedDashboard.js @@ -0,0 +1,188 @@ +import i18n from '@dhis2/d2-i18n' +import { + Button, + CircularLoader, + Modal, + ModalActions, + ModalContent, + ModalTitle, + NoticeBox, +} from '@dhis2/ui' +import cx from 'classnames' +import PropTypes from 'prop-types' +import React, { useEffect } from 'react' +import { useSupersetEmbeddedDashboardFieldsState } from '../../modules/useSupersetEmbeddedDashboardFieldsState.js' +import { useSupersetEmbeddedDashboardMutation } from '../../modules/useSupersetEmbeddedDashboardMutation.js' +import styles from './styles/SupersetEmbeddedDashboardModal.module.css' +import { SupersetEmbeddedDashboardFields } from './SupersetEmbeddedDashboardFields.js' + +export const UpdateSupersetEmbeddedDashboard = ({ closeModal }) => { + const { + queryLoading, + queryHasError, + queryErrorTitle, + queryErrorMessage, + mutationLoading, + mutationHasError, + mutationErrorTitle, + mutationErrorText, + dashboard, + showDeleteConfirmDialog, + setShowDeleteConfirmDialog, + handleUpdate, + handleDelete, + } = useSupersetEmbeddedDashboardMutation({ closeModal }) + const { + hasFieldChanges, + isSupersetEmbedIdValid, + isSupersetEmbedIdFieldTouched, + values, + onChange, + onSupersetEmbedIdFieldBlur, + resetFieldsStateWithNewValues, + } = useSupersetEmbeddedDashboardFieldsState() + + useEffect(() => { + if (dashboard) { + resetFieldsStateWithNewValues({ + title: dashboard.name, + code: dashboard.code, + description: dashboard.description, + supersetEmbedId: dashboard.embedded.id, + showChartControls: + !dashboard.embedded.options.hideChartControls, + showFilters: dashboard.embedded.options.filters.visible, + }) + } + }, [dashboard, resetFieldsStateWithNewValues]) + + if (showDeleteConfirmDialog) { + return ( + + {i18n.t('Delete dashboard')} + +

+ {i18n.t( + 'Deleting dashboard "{{ dashboardName }}" will remove it for all users. This action cannot be undone. Are you sure you want to permanently delete this dashboard?', + { dashboardName: dashboard.name } + )} +

+

+ {i18n.t( + 'Note: the source dashboard embedded by this external dashboard will not be deleted.', + { nsSeparator: '###' } + )} +

+
+ +
+ + +
+
+
+ ) + } + + return ( + +
{ + event.preventDefault() + handleUpdate(values) + }} + > + {i18n.t('Edit external dashboard')} + + {(queryLoading || queryHasError) && ( +
+ {queryLoading && } + {queryHasError && ( + + {queryErrorMessage} + + )} +
+ )} + + {mutationHasError && ( + + {mutationErrorText} + + )} +
+ +
+ {!queryHasError && ( + + )} + + {!queryHasError && dashboard?.access?.delete && ( + + )} +
+
+
+
+ ) +} + +UpdateSupersetEmbeddedDashboard.propTypes = { + closeModal: PropTypes.func, +} diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/index.js b/src/components/ConfigureSupersetEmbeddedDashboardModal/index.js index d5d02a560..f17157e6b 100644 --- a/src/components/ConfigureSupersetEmbeddedDashboardModal/index.js +++ b/src/components/ConfigureSupersetEmbeddedDashboardModal/index.js @@ -1 +1,2 @@ export { CreateSupersetEmbeddedDashboard } from './CreateSupersetEmbeddedDashboard.js' +export { UpdateSupersetEmbeddedDashboard } from './UpdateSupersetEmbeddedDashboard.js' diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/CreateSupersetEmbeddedDashboard.module.css b/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/CreateSupersetEmbeddedDashboard.module.css deleted file mode 100644 index daf5ea517..000000000 --- a/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/CreateSupersetEmbeddedDashboard.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.buttonStrip { - display: flex; - flex-direction: row-reverse; - gap: var(--spacers-dp8); -} diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardModal.module.css b/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardModal.module.css new file mode 100644 index 000000000..f8410bf0e --- /dev/null +++ b/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardModal.module.css @@ -0,0 +1,36 @@ +.modalContent { + position: relative; +} +.contentOverlay { + position: absolute; + inset: 0; + z-index: 10; + background-color: var(--colors-white); + display: flex; +} +.contentOverlay.loading { + opacity: 0.75; + align-items: center; + justify-content: center; +} +.contentOverlay.error { + flex-direction: column; +} + +.buttonStrip { + display: flex; + flex-direction: row-reverse; + gap: var(--spacers-dp8); + inline-size: 100%; +} +.deleteButton { + margin-inline-end: auto; +} +.deleteConfirmPrimaryMessage, +.deleteConfirmSecondaryMessage { + margin-block-start: 0; +} +.deleteConfirmSecondaryMessage { + margin-block-end: 0; + color: var(--colors-grey600); +} diff --git a/src/components/DashboardsBar/InformationBlock/ActionsBar.js b/src/components/DashboardsBar/InformationBlock/ActionsBar.js index 14e08180e..5baf15639 100644 --- a/src/components/DashboardsBar/InformationBlock/ActionsBar.js +++ b/src/components/DashboardsBar/InformationBlock/ActionsBar.js @@ -25,8 +25,12 @@ import { useCacheableSection } from '../../../modules/useCacheableSection.js' import { orObject } from '../../../modules/util.js' import { ROUTE_START_PATH } from '../../../pages/start/index.js' import { sGetNamedItemFilters } from '../../../reducers/itemFilters.js' -import { sGetSelected } from '../../../reducers/selected.js' +import { + sGetSelected, + sGetSelectedIsEmbedded, +} from '../../../reducers/selected.js' import { sGetShowDescription } from '../../../reducers/showDescription.js' +import { UpdateSupersetEmbeddedDashboard } from '../../ConfigureSupersetEmbeddedDashboardModal/UpdateSupersetEmbeddedDashboard.js' import FilterSelector from './FilterSelector.js' import classes from './styles/ActionsBar.module.css' @@ -36,6 +40,7 @@ const ActionsBar = ({ showDescription, starred, setSlideshow, + embedded, toggleDashboardStarred, showAlert, updateShowDescription, @@ -51,11 +56,25 @@ const ActionsBar = ({ const [sharingDialogIsOpen, setSharingDialogIsOpen] = useState(false) const [confirmCacheDialogIsOpen, setConfirmCacheDialogIsOpen] = useState(false) + const [ + updateEmbeddedDashboardModalIsOpen, + setUpdateEmbeddedDashboardModalIsOpen, + ] = useState(false) const [redirectUrl, setRedirectUrl] = useState(null) const { isDisconnected: offline } = useDhis2ConnectionStatus() const { lastUpdated, isCached, startRecording, remove } = useCacheableSection(id) const { allowVisFullscreen } = useSystemSettings().systemSettings + const notAvailableForEmbeddedDashboardsMsg = i18n.t( + 'Not available for embedded dashboards' + ) + const handleEditClick = useCallback(() => { + if (embedded) { + setUpdateEmbeddedDashboardModalIsOpen(true) + } else { + setRedirectUrl(`${id}/edit`) + } + }, [embedded, id, setRedirectUrl]) const onRecordError = useCallback(() => { showAlert({ @@ -117,9 +136,14 @@ const ActionsBar = ({ ) : ( )} {lastUpdated && ( @@ -152,10 +176,13 @@ const ActionsBar = ({ /> { - if (!hasSlideshowItems) { + if (embedded) { + return notAvailableForEmbeddedDashboardsMsg + } else if (!hasSlideshowItems) { return i18n.t('No dashboard items to show in slideshow') } else if (offline && !isCached) { return i18n.t('Not available offline') + } else { + return null } - return null } const slideshowTooltipContent = getSlideshowTooltipContent() @@ -208,7 +238,7 @@ const ActionsBar = ({ secondary small disabled={offline} - onClick={() => setRedirectUrl(`${id}/edit`)} + onClick={handleEditClick} > {i18n.t('Edit')} @@ -281,6 +311,13 @@ const ActionsBar = ({ onCancel={() => setConfirmCacheDialogIsOpen(false)} open={confirmCacheDialogIsOpen} /> + {updateEmbeddedDashboardModalIsOpen && ( + + setUpdateEmbeddedDashboardModalIsOpen(false) + } + /> + )} ) } @@ -289,6 +326,7 @@ ActionsBar.propTypes = { access: PropTypes.object, allowedFilters: PropTypes.array, dashboardItems: PropTypes.array, + embedded: PropTypes.bool, filtersLength: PropTypes.number, id: PropTypes.string, removeAllFilters: PropTypes.func, @@ -306,6 +344,7 @@ const mapStateToProps = (state) => { return { ...dashboard, + embedded: sGetSelectedIsEmbedded(state), filtersLength: sGetNamedItemFilters(state).length, showDescription: sGetShowDescription(state), } diff --git a/src/components/DashboardsBar/InformationBlock/FilterSelector.js b/src/components/DashboardsBar/InformationBlock/FilterSelector.js index 69221edf9..4a89ee197 100644 --- a/src/components/DashboardsBar/InformationBlock/FilterSelector.js +++ b/src/components/DashboardsBar/InformationBlock/FilterSelector.js @@ -1,7 +1,7 @@ import { DimensionsPanel } from '@dhis2/analytics' import { useDhis2ConnectionStatus } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' -import { Card, colors, IconFilter24 } from '@dhis2/ui' +import { Card, IconFilter24 } from '@dhis2/ui' import isEmpty from 'lodash/isEmpty.js' import PropTypes from 'prop-types' import React, { useState } from 'react' @@ -13,6 +13,7 @@ import { import useDimensions from '../../../modules/useDimensions.js' import { sGetActiveModalDimension } from '../../../reducers/activeModalDimension.js' import { sGetItemFiltersRoot } from '../../../reducers/itemFilters.js' +import { sGetSelectedIsEmbedded } from '../../../reducers/selected.js' import DropdownButton from '../../DropdownButton/DropdownButton.js' import FilterDialog from './FilterDialog.js' @@ -63,10 +64,16 @@ const FilterSelector = (props) => { secondary small open={filterDialogIsOpen} - disabled={offline} + disabled={offline || props.embedded} + disabledWhenOffline={offline} onClick={toggleFilterDialogIsOpen} - icon={} + icon={} component={getFilterSelector()} + content={ + props.embedded + ? i18n.t('Not available for embedded dashboards') + : undefined + } > {i18n.t('Filter')} @@ -81,6 +88,7 @@ const FilterSelector = (props) => { } const mapStateToProps = (state) => ({ + embedded: sGetSelectedIsEmbedded(state), dimension: sGetActiveModalDimension(state), initiallySelectedItems: sGetItemFiltersRoot(state), }) @@ -89,6 +97,7 @@ FilterSelector.propTypes = { allowedFilters: PropTypes.array, clearActiveModalDimension: PropTypes.func, dimension: PropTypes.object, + embedded: PropTypes.bool, initiallySelectedItems: PropTypes.object, restrictFilters: PropTypes.bool, setActiveModalDimension: PropTypes.func, diff --git a/src/components/DashboardsBar/InformationBlock/InformationBlock.js b/src/components/DashboardsBar/InformationBlock/InformationBlock.js index 9fa1ac01d..3167d9b54 100644 --- a/src/components/DashboardsBar/InformationBlock/InformationBlock.js +++ b/src/components/DashboardsBar/InformationBlock/InformationBlock.js @@ -1,3 +1,4 @@ +import { Tag } from '@dhis2-ui/tag' import { useAlert, useDataEngine } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' @@ -5,7 +6,10 @@ import React, { useCallback } from 'react' import { connect } from 'react-redux' import { acSetDashboardStarred } from '../../../actions/dashboards.js' import { sGetDashboardStarred } from '../../../reducers/dashboards.js' -import { sGetSelected } from '../../../reducers/selected.js' +import { + sGetSelected, + sGetSelectedIsEmbedded, +} from '../../../reducers/selected.js' import ActionsBar from './ActionsBar.js' import { apiStarDashboard } from './apiStarDashboard.js' import LastUpdatedTag from './LastUpdatedTag.js' @@ -14,6 +18,7 @@ import classes from './styles/InformationBlock.module.css' const InformationBlock = ({ id, + isEmbeddedDashboard, displayName, starred, setDashboardStarred, @@ -54,6 +59,9 @@ const InformationBlock = ({ onClick={toggleDashboardStarred} /> + {isEmbeddedDashboard && ( + {i18n.t('External data')} + )} { starred: dashboard.id ? sGetDashboardStarred(state, dashboard.id) : false, + isEmbeddedDashboard: sGetSelectedIsEmbedded(state), } } diff --git a/src/components/DashboardsBar/InformationBlock/LastUpdatedTag.js b/src/components/DashboardsBar/InformationBlock/LastUpdatedTag.js index 081ddcbd9..f347c65e0 100644 --- a/src/components/DashboardsBar/InformationBlock/LastUpdatedTag.js +++ b/src/components/DashboardsBar/InformationBlock/LastUpdatedTag.js @@ -30,7 +30,7 @@ const LastUpdatedTag = ({ id }) => { > {(props) => (
- {message} + {message}
)} diff --git a/src/components/DashboardsBar/InformationBlock/__tests__/FilterSelector.spec.js b/src/components/DashboardsBar/InformationBlock/__tests__/FilterSelector.spec.js index 28d6c81a1..375addac4 100644 --- a/src/components/DashboardsBar/InformationBlock/__tests__/FilterSelector.spec.js +++ b/src/components/DashboardsBar/InformationBlock/__tests__/FilterSelector.spec.js @@ -13,7 +13,7 @@ jest.mock('@dhis2/app-runtime', () => ({ jest.mock('../../../../modules/useDimensions', () => jest.fn()) useDimensions.mockImplementation(() => ['Moomin', 'Snorkmaiden']) -const baseState = { activeModalDimension: {}, itemFilters: {} } +const baseState = { selected: {}, activeModalDimension: {}, itemFilters: {} } const createMockStore = (state) => createStore(() => ({ ...baseState, ...state })) diff --git a/src/components/DropdownButton/DropdownButton.js b/src/components/DropdownButton/DropdownButton.js index 2f1e8c4a2..a64d69079 100644 --- a/src/components/DropdownButton/DropdownButton.js +++ b/src/components/DropdownButton/DropdownButton.js @@ -11,6 +11,7 @@ const DropdownButton = ({ onClick, disabledWhenOffline, component, + content, ...rest }) => { const anchorRef = useRef() @@ -18,7 +19,11 @@ const DropdownButton = ({ const ArrowIconComponent = open ? ArrowUp : ArrowDown return (
- + )} )}
diff --git a/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js b/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js index fa91dd50d..f6008457d 100644 --- a/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js +++ b/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js @@ -26,13 +26,13 @@ export const ChooseDashboardTypeModal = ({ systemInfo: { systemName }, } = useConfig() const [selectedType, setSelectedType] = useState(TYPE_INTERNAL) - const handleDashboardTypeChange = useCallback((event) => { - setSelectedType(event.target.value) + const handleDashboardTypeChange = useCallback(({ value }) => { + setSelectedType(value) }, []) const isInternal = selectedType === TYPE_INTERNAL return ( - +
{i18n.t('New dashboard: choose type', { @@ -43,8 +43,8 @@ export const ChooseDashboardTypeModal = ({
} title={i18n.t( @@ -59,8 +59,8 @@ export const ChooseDashboardTypeModal = ({ )} /> } title={i18n.t( diff --git a/src/components/DashboardsBar/CreateDashboardButton/DashboardTypeRadio.js b/src/components/DashboardsBar/CreateDashboardButton/DashboardTypeRadio.js index 2567df811..b166acedd 100644 --- a/src/components/DashboardsBar/CreateDashboardButton/DashboardTypeRadio.js +++ b/src/components/DashboardsBar/CreateDashboardButton/DashboardTypeRadio.js @@ -1,60 +1,45 @@ +import { Radio } from '@dhis2/ui' import cx from 'classnames' import PropTypes from 'prop-types' -import React, { useEffect, useRef } from 'react' +import React from 'react' import styles from './styles/DashboardTypeRadio.module.css' export const DashboardTypeRadio = ({ - type, - selectedType, + value, + checked, initialFocus, onChange, icon, title, subtitle, -}) => { - const ref = useRef(null) - const checked = type === selectedType - - useEffect(() => { - if (initialFocus) { - ref.current?.focus({ focusVisible: true }) - } - }, [initialFocus]) - - return ( - - ) -} + } + name="dashboard-type" + value={value} + checked={checked} + onChange={onChange} + /> +) DashboardTypeRadio.propTypes = { + checked: PropTypes.bool, icon: PropTypes.node, initialFocus: PropTypes.bool, - selectedType: PropTypes.string, subtitle: PropTypes.string, title: PropTypes.string, - type: PropTypes.string, + value: PropTypes.string, onChange: PropTypes.func, } diff --git a/src/components/DashboardsBar/CreateDashboardButton/styles/DashboardTypeRadio.module.css b/src/components/DashboardsBar/CreateDashboardButton/styles/DashboardTypeRadio.module.css index a6ab81c27..5f62270c4 100644 --- a/src/components/DashboardsBar/CreateDashboardButton/styles/DashboardTypeRadio.module.css +++ b/src/components/DashboardsBar/CreateDashboardButton/styles/DashboardTypeRadio.module.css @@ -1,29 +1,5 @@ .container { - display: flex; - align-items: center; - background-color: var(--colors-white); - color: var(--colors-grey900); - border: 1px solid var(--colors-grey400); - border-radius: 6px; - padding: var(--spacers-dp16); cursor: pointer; - transition: 0.2s ease; - transition-property: background-color, border-color; -} - -.container.checked { - background-color: var(--colors-teal100); - border-color: var(--colors-teal500); - box-shadow: var(--elevations-e100); -} - -.container.unchecked:hover { - border-color: var(--colors-grey500); -} - -.container:has(:focus-visible) { - outline: 3px solid var(--colors-blue600); - outline-offset: -3px; } .container .content { @@ -32,11 +8,11 @@ gap: var(--spacers-dp12); } -.container .icon svg { +.container svg { fill: var(--colors-grey700); } -.container.checked .icon svg { +.container.checked svg { fill: var(--colors-teal700); } diff --git a/src/components/DashboardsBar/InformationBlock/ActionsBar.js b/src/components/DashboardsBar/InformationBlock/ActionsBar.js index ef218aa18..af5fe3d35 100644 --- a/src/components/DashboardsBar/InformationBlock/ActionsBar.js +++ b/src/components/DashboardsBar/InformationBlock/ActionsBar.js @@ -24,7 +24,7 @@ import { itemTypeSupportsFullscreen } from '../../../modules/itemTypes.js' import { useCacheableSection } from '../../../modules/useCacheableSection.js' import { orObject } from '../../../modules/util.js' import { ROUTE_START_PATH } from '../../../pages/start/index.js' -import { sGetNamedItemFilters } from '../../../reducers/itemFilters.js' +import { msGetNamedItemFilters } from '../../../reducers/itemFilters.js' import { sGetSelected, sGetSelectedIsEmbedded, @@ -348,7 +348,7 @@ const mapStateToProps = (state) => { return { ...dashboard, embedded: sGetSelectedIsEmbedded(state), - filtersLength: sGetNamedItemFilters(state).length, + filtersLength: msGetNamedItemFilters(state).length, showDescription: sGetShowDescription(state), } } diff --git a/src/components/Item/PrintTitlePageItem/Item.js b/src/components/Item/PrintTitlePageItem/Item.js index ce2023548..89913b218 100644 --- a/src/components/Item/PrintTitlePageItem/Item.js +++ b/src/components/Item/PrintTitlePageItem/Item.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import React from 'react' import { connect } from 'react-redux' import { sGetIsEditing } from '../../../reducers/editDashboard.js' -import { sGetNamedItemFilters } from '../../../reducers/itemFilters.js' +import { msGetNamedItemFilters } from '../../../reducers/itemFilters.js' import { sGetPrintDashboardName, sGetPrintDashboardDescription, @@ -76,7 +76,7 @@ const mapStateToProps = (state) => { return { name, description, - itemFilters: sGetNamedItemFilters(state), + itemFilters: msGetNamedItemFilters(state), showDescription: sGetShowDescription(state), } } diff --git a/src/modules/__tests__/useSupersetEmbeddedDashboardFieldsState.spec.js b/src/modules/__tests__/useSupersetEmbeddedDashboardFieldsState.spec.js new file mode 100644 index 000000000..fbd47552f --- /dev/null +++ b/src/modules/__tests__/useSupersetEmbeddedDashboardFieldsState.spec.js @@ -0,0 +1,191 @@ +import { + createInitialState, + defaultInitialValues, + FIELD_CHANGE, + fieldNames, + isValidUuid, + reducer, + RESET_FIELD_STATE, + SUPERSET_FIELD_BLUR, +} from '../useSupersetEmbeddedDashboardFieldsState.js' + +const INITIAL_TITLE = 'Initial title' +const UPDATED_TITLE = 'Updated title' +const UUID_V1 = 'dbb4cd86-e206-11ef-9cd2-0242ac120002' +const UUID_V4 = '0394041b-8367-4fe2-9777-fe54b6d2da2f' +const UUID_V7 = '0194cae2-92f9-7363-ab58-9a19b13084eb' +const NILL_UUID = '00000000-0000-0000-0000-000000000000' + +describe('superset embedded fields state reducer', () => { + describe('UUID validator', () => { + it('accepts valid UUID strings for common versions', () => { + expect(isValidUuid(UUID_V1)).toBe(true) + expect(isValidUuid(UUID_V4)).toBe(true) + expect(isValidUuid(UUID_V7)).toBe(true) + }) + it('rejects empty string, invalid string and nill UUID', () => { + expect(isValidUuid(NILL_UUID)).toBe(false) + expect(isValidUuid('')).toBe(false) + expect(isValidUuid('a random string')).toBe(false) + }) + }) + describe('initial state creation', () => { + it('creates the expected initial state when not providing initial values', () => { + expect(createInitialState()).toEqual({ + initialValues: defaultInitialValues, + values: defaultInitialValues, + isSupersetEmbedIdValid: false, + isSupersetEmbedIdFieldTouched: false, + hasFieldChanges: false, + }) + }) + it('creates the expected initial state from provided initial values', () => { + const initialValues = { + title: 'My title', + code: 'MY_CODE', + description: 'My description', + supersetEmbedId: '', + showChartControls: false, + expandFilters: true, + } + expect(createInitialState(initialValues)).toEqual({ + initialValues: initialValues, + values: initialValues, + isSupersetEmbedIdValid: false, + isSupersetEmbedIdFieldTouched: false, + hasFieldChanges: false, + }) + }) + it('the initial state will have report a valid superset embed ID if a valid UUID was provided', () => { + const initialValues = { + ...defaultInitialValues, + supersetEmbedId: UUID_V4, + } + expect(createInitialState(initialValues)).toEqual({ + initialValues: initialValues, + values: initialValues, + isSupersetEmbedIdValid: true, + isSupersetEmbedIdFieldTouched: false, + hasFieldChanges: false, + }) + }) + }) + describe('state updates due to field change action', () => { + it('updates state for text fields', () => { + const initialState = createInitialState() + const expectedState = { + ...initialState, + values: { + ...initialState.values, + title: UPDATED_TITLE, + }, + hasFieldChanges: true, + } + expect( + reducer(initialState, { + type: FIELD_CHANGE, + payload: { name: fieldNames.title, value: UPDATED_TITLE }, + }) + ).toEqual(expectedState) + }) + it('updates state for boolean fields', () => { + const initialState = createInitialState() + const expectedState = { + ...initialState, + values: { + ...initialState.values, + showChartControls: false, + }, + hasFieldChanges: true, + } + expect( + reducer(initialState, { + type: FIELD_CHANGE, + payload: { + name: fieldNames.showChartControls, + checked: false, + }, + }) + ).toEqual(expectedState) + }) + it('reports no field changes if values are changed back to initial values', () => { + const initialState = createInitialState({ + ...defaultInitialValues, + title: INITIAL_TITLE, + }) + const stateAfterTitleChange = reducer(initialState, { + type: FIELD_CHANGE, + payload: { name: fieldNames.title, value: UPDATED_TITLE }, + }) + + expect(stateAfterTitleChange.values.title).toBe(UPDATED_TITLE) + expect(stateAfterTitleChange.hasFieldChanges).toBe(true) + + const stateAfterTitleReset = reducer(stateAfterTitleChange, { + type: FIELD_CHANGE, + payload: { name: fieldNames.title, value: INITIAL_TITLE }, + }) + + expect(stateAfterTitleReset.values.title).toBe(INITIAL_TITLE) + expect(stateAfterTitleReset.hasFieldChanges).toBe(false) + }) + it('reports superset embed ID is valid if the field is changed to a valid value', () => { + const stateAfterEmbedIdChange = reducer(createInitialState(), { + type: FIELD_CHANGE, + payload: { name: fieldNames.supersetEmbedId, value: UUID_V1 }, + }) + + expect(stateAfterEmbedIdChange.values.supersetEmbedId).toBe(UUID_V1) + expect(stateAfterEmbedIdChange.isSupersetEmbedIdValid).toBe(true) + }) + }) + describe('state updates due to superset field blur action', () => { + it('sets superset embed ID field touched to true', () => { + const initialState = createInitialState() + expect( + reducer(initialState, { type: SUPERSET_FIELD_BLUR }) + ).toEqual({ ...initialState, isSupersetEmbedIdFieldTouched: true }) + }) + }) + describe('state updates due to reset field state action', () => { + it('resets the state using the initial values in the payload', () => { + let state = createInitialState() + // Make various state changes + state = reducer(state, { type: SUPERSET_FIELD_BLUR }) + state = reducer(state, { + type: FIELD_CHANGE, + payload: { name: fieldNames.title, value: INITIAL_TITLE }, + }) + state = reducer(state, { + type: FIELD_CHANGE, + payload: { name: fieldNames.supersetEmbedId, value: UUID_V1 }, + }) + expect(state.values.title).toBe(INITIAL_TITLE) + expect(state.values.supersetEmbedId).toBe(UUID_V1) + expect(state.isSupersetEmbedIdFieldTouched).toBe(true) + expect(state.isSupersetEmbedIdValid).toBe(true) + expect(state.hasFieldChanges).toBe(true) + + // Dispatch reset action + const newValues = { + [fieldNames.title]: UPDATED_TITLE, + [fieldNames.code]: 'SOME_CODE', + [fieldNames.description]: 'A description text', + [fieldNames.supersetEmbedId]: NILL_UUID, + [fieldNames.showChartControls]: false, + [fieldNames.expandFilters]: true, + } + state = reducer(state, { + type: RESET_FIELD_STATE, + payload: newValues, + }) + + expect(state.initialValues).toEqual(newValues) + expect(state.values).toEqual(newValues) + expect(state.isSupersetEmbedIdValid).toBe(false) + expect(state.isSupersetEmbedIdFieldTouched).toBe(false) + // Note that this is by design, the initalValues are also reset + expect(state.hasFieldChanges).toBe(false) + }) + }) +}) diff --git a/src/modules/parseSupersetEmbeddedDashboardFieldValues.js b/src/modules/parseSupersetEmbeddedDashboardFieldValues.js index a642cb611..b66ac01aa 100644 --- a/src/modules/parseSupersetEmbeddedDashboardFieldValues.js +++ b/src/modules/parseSupersetEmbeddedDashboardFieldValues.js @@ -9,8 +9,8 @@ export const parseSupersetEmbeddedDashboardFieldValues = (values) => ({ hideTab: false, hideChartControls: !values.showChartControls, filters: { - visible: values.showFilters, - expanded: false, + visible: true, + expanded: values.expandFilters, }, }, }, diff --git a/src/modules/useSupersetEmbeddedDashboardFieldsState.js b/src/modules/useSupersetEmbeddedDashboardFieldsState.js index f6c1f1774..10a81dcda 100644 --- a/src/modules/useSupersetEmbeddedDashboardFieldsState.js +++ b/src/modules/useSupersetEmbeddedDashboardFieldsState.js @@ -6,24 +6,24 @@ export const fieldNames = { description: 'description', supersetEmbedId: 'supersetEmbedId', showChartControls: 'showChartControls', - showFilters: 'showFilters', + expandFilters: 'expandFilters', } -const defaultInitialValues = { +export const defaultInitialValues = { [fieldNames.title]: '', [fieldNames.code]: '', [fieldNames.description]: '', [fieldNames.supersetEmbedId]: '', [fieldNames.showChartControls]: true, - [fieldNames.showFilters]: true, + [fieldNames.expandFilters]: false, } -const FIELD_CHANGE = 'FIELD_CHANGE' -const SUPERSET_FIELD_BLUR = 'SUPERSET_FIELD_BLUR' -const RESET_FIELD_STATE = 'RESET_FIELD_STATE' -// TODO: ensure the reducer unit tests assert the correctness of the UUID validation +export const FIELD_CHANGE = 'FIELD_CHANGE' +export const SUPERSET_FIELD_BLUR = 'SUPERSET_FIELD_BLUR' +export const RESET_FIELD_STATE = 'RESET_FIELD_STATE' +// Adapted from :https://github.com/uuidjs/uuid/blob/main/src/regex.ts const UUID_PATTERN = - /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i -const isValidUuid = (string) => UUID_PATTERN.test(string) -const createInitialState = (initialValues) => ({ + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i +export const isValidUuid = (string) => UUID_PATTERN.test(string) +export const createInitialState = (initialValues = defaultInitialValues) => ({ initialValues, values: initialValues, isSupersetEmbedIdValid: isValidUuid(initialValues.supersetEmbedId), @@ -31,7 +31,7 @@ const createInitialState = (initialValues) => ({ hasFieldChanges: false, }) -const reducer = (state, { type, payload }) => { +export const reducer = (state, { type, payload }) => { switch (type) { case FIELD_CHANGE: { const values = { @@ -47,7 +47,7 @@ const reducer = (state, { type, payload }) => { values, isSupersetEmbedIdValid: payload.name === fieldNames.supersetEmbedId - ? UUID_PATTERN.test(payload.value) + ? isValidUuid(payload.value) : state.isSupersetEmbedIdValid, hasFieldChanges: Object.entries(values).some( ([key, value]) => value !== state.initialValues[key] diff --git a/src/pages/edit/__tests__/EditDashboard.spec.js b/src/pages/edit/__tests__/EditDashboard.spec.js index 15826a439..a3b6e60bc 100644 --- a/src/pages/edit/__tests__/EditDashboard.spec.js +++ b/src/pages/edit/__tests__/EditDashboard.spec.js @@ -62,22 +62,21 @@ jest.mock( ) const mockStore = configureMockStore() +const baseStoreState = { selected: {} } const renderWithRouterMatch = ( ui, { route = '/', history = createMemoryHistory({ initialEntries: [route] }), - store = { - selected: {}, - }, + store, } = {} ) => { return { ...render( <>
- + diff --git a/src/pages/edit/__tests__/__snapshots__/EditDashboard.spec.js.snap b/src/pages/edit/__tests__/__snapshots__/EditDashboard.spec.js.snap index aec8631ab..a22900add 100644 --- a/src/pages/edit/__tests__/__snapshots__/EditDashboard.spec.js.snap +++ b/src/pages/edit/__tests__/__snapshots__/EditDashboard.spec.js.snap @@ -6,7 +6,41 @@ exports[`EditDashboard does not render titlebar and grid if small screen 1`] = ` `; -exports[`EditDashboard renders dashboard 1`] = `
`; +exports[`EditDashboard renders dashboard 1`] = ` +
+
+
+
+ ActionsBar +
+
+
+
+ TitleBar +
+
+ ItemGrid +
+
+
+
+
+
+
+
+`; exports[`EditDashboard renders message when not enough access 1`] = `
diff --git a/src/pages/view/EmbeddedSupersetDashboard.js b/src/pages/view/EmbeddedSupersetDashboard.js index 1e1c5ee06..830edae15 100644 --- a/src/pages/view/EmbeddedSupersetDashboard.js +++ b/src/pages/view/EmbeddedSupersetDashboard.js @@ -7,8 +7,8 @@ import { useSelector } from 'react-redux' import { usePostSupersetGuestToken } from '../../api/supersetGateway.js' import { useSupersetBaseUrl } from '../../components/SystemSettingsProvider.js' import { + msGetSelectedSupersetEmbedData, sGetSelectedId, - sGetSelectedSupersetEmbedData, } from '../../reducers/selected.js' import styles from './styles/EmbeddedSupersetDashboard.module.css' @@ -43,7 +43,7 @@ export const EmbeddedSupersetDashboard = () => { ) const ref = useRef(null) const selectedId = useSelector(sGetSelectedId) - const embedData = useSelector(sGetSelectedSupersetEmbedData) + const embedData = useSelector(msGetSelectedSupersetEmbedData) const supersetDomain = useSupersetBaseUrl() const postSupersetGuestToken = usePostSupersetGuestToken(selectedId) const loadEmbeddedSupersetDashboard = useCallback(async () => { diff --git a/src/pages/view/FilterBar/FilterBar.js b/src/pages/view/FilterBar/FilterBar.js index 526918240..56c0d04a2 100644 --- a/src/pages/view/FilterBar/FilterBar.js +++ b/src/pages/view/FilterBar/FilterBar.js @@ -8,7 +8,7 @@ import { acClearItemFilters, } from '../../../actions/itemFilters.js' import ConfirmActionDialog from '../../../components/ConfirmActionDialog.js' -import { sGetNamedItemFilters } from '../../../reducers/itemFilters.js' +import { msGetNamedItemFilters } from '../../../reducers/itemFilters.js' import FilterBadge from './FilterBadge.js' import classes from './styles/FilterBar.module.css' @@ -63,7 +63,7 @@ FilterBar.defaultProps = { } const mapStateToProps = (state) => ({ - filters: sGetNamedItemFilters(state), + filters: msGetNamedItemFilters(state), }) export default connect(mapStateToProps, { diff --git a/src/pages/view/SlideshowFiltersInfo.js b/src/pages/view/SlideshowFiltersInfo.js index 85f1514d0..abadf7786 100644 --- a/src/pages/view/SlideshowFiltersInfo.js +++ b/src/pages/view/SlideshowFiltersInfo.js @@ -3,7 +3,7 @@ import { Layer, Popper, IconFilter16 } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { useMemo, useState, useRef } from 'react' import { useSelector } from 'react-redux' -import { sGetNamedItemFilters } from '../../reducers/itemFilters.js' +import { msGetNamedItemFilters } from '../../reducers/itemFilters.js' import styles from './styles/SlideshowFiltersInfo.module.css' const popperModifiers = [ @@ -35,7 +35,7 @@ FilterSection.propTypes = { export const SlideshowFiltersInfo = () => { const [isOpen, setIsOpen] = useState(false) const ref = useRef(null) - const filters = useSelector(sGetNamedItemFilters) + const filters = useSelector(msGetNamedItemFilters) const totalFilterCount = useMemo( () => filters.reduce((total, filter) => total + filter.values.length, 0), diff --git a/src/pages/view/styles/Description.module.css b/src/pages/view/styles/Description.module.css index de77bb22e..b1011373b 100644 --- a/src/pages/view/styles/Description.module.css +++ b/src/pages/view/styles/Description.module.css @@ -12,4 +12,5 @@ .padded { padding-inline: var(--spacers-dp8); + padding-block-end: var(--spacers-dp8); } diff --git a/src/reducers/itemFilters.js b/src/reducers/itemFilters.js index b3c0cf7bd..abaa33cdc 100644 --- a/src/reducers/itemFilters.js +++ b/src/reducers/itemFilters.js @@ -36,7 +36,7 @@ export const sGetItemFiltersRoot = (state) => state.itemFilters // simplify the filters structure to: // [{ id: 'pe', name: 'Period', values: [{ id: 2019: name: '2019' }, {id: 'LAST_MONTH', name: 'Last month' }]}, ...] -export const sGetNamedItemFilters = createSelector( +export const msGetNamedItemFilters = createSelector( [sGetItemFiltersRoot, sGetDimensions], (filters, dimensions) => Object.keys(filters).reduce((arr, id) => { diff --git a/src/reducers/selected.js b/src/reducers/selected.js index 2a3fad080..7984fc3b7 100644 --- a/src/reducers/selected.js +++ b/src/reducers/selected.js @@ -1,3 +1,5 @@ +import { createSelector } from 'reselect' + export const SET_SELECTED = 'SET_SELECTED' export const CLEAR_SELECTED = 'CLEAR_SELECTED' @@ -49,12 +51,31 @@ export const sGetSelectedSupersetEmbedData = (state) => { hideTab: true, hideChartControls: embedData.options.hideChartControls, filters: { - visible: embedData.options.filters.visible, - expanded: false, + visible: true, + expanded: embedData.options.filters.expanded, }, }, } } +export const msGetSelectedSupersetEmbedData = createSelector( + [ + (state) => state.selected?.embedded.id, + (state) => state.selected?.embedded.options.hideChartControls, + (state) => state.selected?.embedded.options.filters.expanded, + ], + (id, hideChartControls, expanded) => ({ + id, + dashboardUiConfig: { + hideTitle: true, + hideTab: true, + hideChartControls, + filters: { + visible: true, + expanded, + }, + }, + }) +) export const sGetSelectedDisplayName = (state) => sGetSelected(state).displayName diff --git a/yarn.lock b/yarn.lock index f25dd091a..adb029642 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16038,10 +16038,10 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= -reselect@^4.0.0: - version "4.1.8" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524" - integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== +reselect@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" + integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== resize-observer-polyfill@^1.5.1: version "1.5.1" From f9ddd9e1df64b06f2696a8f1029088a42a45d812 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Mon, 10 Feb 2025 12:02:38 +0100 Subject: [PATCH 07/37] chore: fix problem in en.pot file --- i18n/en.pot | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 9d2d415b0..254d297da 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: 2025-02-07T10:20:57.831Z\n" -"PO-Revision-Date: 2025-02-07T10:20:57.833Z\n" +"POT-Creation-Date: 2025-02-10T11:01:21.083Z\n" +"PO-Revision-Date: 2025-02-10T11:01:21.084Z\n" msgid "Untitled dashboard" msgstr "Untitled dashboard" @@ -439,13 +439,14 @@ msgstr "Exit without saving" msgid "Go to dashboards" msgstr "Go to dashboards" -msgid "Delete dashboard" -msgstr "Delete dashboard" - msgid "" "Deleting dashboard \"{{- dashboardName }}\" will remove it for all users. " "This action cannot be undone. Are you sure you want to permanently delete " "this dashboard?" +msgstr "" +"Deleting dashboard \"{{- dashboardName }}\" will remove it for all users. " +"This action cannot be undone. Are you sure you want to permanently delete " +"this dashboard?" msgid "Discard changes" msgstr "Discard changes" From 5ae8d17d2a225aee84511d0b41a4205cb6a57fc5 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Mon, 10 Feb 2025 12:09:54 +0100 Subject: [PATCH 08/37] chore: always use "continue" text in choose-dashboard-type-modal button --- i18n/en.pot | 11 ++++------- .../ChooseDashboardTypeModal.js | 13 ++++++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 254d297da..c205d3b21 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: 2025-02-10T11:01:21.083Z\n" -"PO-Revision-Date: 2025-02-10T11:01:21.084Z\n" +"POT-Creation-Date: 2025-02-10T11:08:21.621Z\n" +"PO-Revision-Date: 2025-02-10T11:08:21.622Z\n" msgid "Untitled dashboard" msgstr "Untitled dashboard" @@ -93,11 +93,8 @@ msgstr "External: Data from another source" msgid "Embed a dashboard from a third-party source, like Superset." msgstr "Embed a dashboard from a third-party source, like Superset." -msgid "Create dashboard" -msgstr "Create dashboard" - -msgid "Configure source" -msgstr "Configure source" +msgid "Continue" +msgstr "Continue" msgid "Not available for embedded dashboards" msgstr "Not available for embedded dashboards" diff --git a/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js b/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js index f6008457d..571ecebe7 100644 --- a/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js +++ b/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js @@ -29,11 +29,16 @@ export const ChooseDashboardTypeModal = ({ const handleDashboardTypeChange = useCallback(({ value }) => { setSelectedType(value) }, []) - const isInternal = selectedType === TYPE_INTERNAL return ( - + {i18n.t('New dashboard: choose type', { nsSeparator: '###', @@ -78,9 +83,7 @@ export const ChooseDashboardTypeModal = ({
- } - name="dashboard-type" - value={value} - checked={checked} - onChange={onChange} - /> +
+ {title}

} + name="dashboard-type" + value={value} + checked={checked} + onChange={onChange} + /> +
+ {subtitle} +
+
) DashboardTypeRadio.propTypes = { checked: PropTypes.bool, - icon: PropTypes.node, initialFocus: PropTypes.bool, subtitle: PropTypes.string, title: PropTypes.string, diff --git a/src/components/DashboardsBar/CreateDashboardButton/IconDashboardExternal.js b/src/components/DashboardsBar/CreateDashboardButton/IconDashboardExternal.js deleted file mode 100644 index 5e2fe4c07..000000000 --- a/src/components/DashboardsBar/CreateDashboardButton/IconDashboardExternal.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' - -export const IconDashboardExternal = () => ( - - - - -) diff --git a/src/components/DashboardsBar/CreateDashboardButton/IconDashboardInternal.js b/src/components/DashboardsBar/CreateDashboardButton/IconDashboardInternal.js deleted file mode 100644 index 85d2092bb..000000000 --- a/src/components/DashboardsBar/CreateDashboardButton/IconDashboardInternal.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react' - -export const IconDashboardInternal = () => ( - - - - - - -) diff --git a/src/components/DashboardsBar/CreateDashboardButton/styles/DashboardTypeRadio.module.css b/src/components/DashboardsBar/CreateDashboardButton/styles/DashboardTypeRadio.module.css index 5f62270c4..e227cffe7 100644 --- a/src/components/DashboardsBar/CreateDashboardButton/styles/DashboardTypeRadio.module.css +++ b/src/components/DashboardsBar/CreateDashboardButton/styles/DashboardTypeRadio.module.css @@ -2,12 +2,6 @@ cursor: pointer; } -.container .content { - display: flex; - align-items: center; - gap: var(--spacers-dp12); -} - .container svg { fill: var(--colors-grey700); } @@ -16,27 +10,15 @@ fill: var(--colors-teal700); } -.container .text { - display: flex; - flex-direction: column; - gap: 4px; -} - -.container .text p { - margin: 0; -} - .container .title { - font-size: 15px; - line-height: 19px; color: var(--colors-grey900); font-weight: 500; + margin: 0; } -.container .subtitle { - font-size: 13px; - line-height: 17px; - color: var(--colors-grey700); +.subtitle { + margin: 0; + margin-inline-start: var(--spacers-dp24); } .container input { From 87cd24081ec62542f5df251bdd9c6c036e366ff8 Mon Sep 17 00:00:00 2001 From: Joseph John Aas Cooper <33054985+cooper-joe@users.noreply.github.com> Date: Wed, 12 Feb 2025 12:16:54 +0100 Subject: [PATCH 11/37] fix: external creation code help text --- .../SupersetEmbeddedDashboardFields.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/SupersetEmbeddedDashboardFields.js b/src/components/ConfigureSupersetEmbeddedDashboardModal/SupersetEmbeddedDashboardFields.js index 791ec7411..91e17be08 100644 --- a/src/components/ConfigureSupersetEmbeddedDashboardModal/SupersetEmbeddedDashboardFields.js +++ b/src/components/ConfigureSupersetEmbeddedDashboardModal/SupersetEmbeddedDashboardFields.js @@ -30,6 +30,9 @@ export const SupersetEmbeddedDashboardFields = ({ Date: Wed, 12 Feb 2025 12:28:30 +0100 Subject: [PATCH 12/37] fix: labels in tests --- cypress/e2e/embedded_superset_dashboard.cy.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/embedded_superset_dashboard.cy.js b/cypress/e2e/embedded_superset_dashboard.cy.js index d8e423015..2e417e66a 100644 --- a/cypress/e2e/embedded_superset_dashboard.cy.js +++ b/cypress/e2e/embedded_superset_dashboard.cy.js @@ -78,17 +78,13 @@ describe('Creating, viewing, editing and deleting an embedded superset dashboard cy.get(newButtonSel, EXTENDED_TIMEOUT).click() // Choose the embedded dashboard option - cy.contains('External: Data from another source') - .should('be.visible') - .click() + cy.contains('External').should('be.visible').click() // Click the configure source button - cy.contains('Configure source').should('be.visible').click() + cy.contains('Continue').should('be.visible').click() // A modal form to create a new embedded dashboard is showing - cy.contains( - 'New dashboard: configure external source (superset)' - ).should('be.visible') + cy.contains('New dashboard: external').should('be.visible') // Check all initial values and change them getInputByLabelText('Title').should('have.value', '').type(NAME) From 5ffc1505e81ad2e246869a9dfdaad9f693ddd03e Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 11 Feb 2025 15:42:51 +0100 Subject: [PATCH 13/37] chore: update yarn.lock --- yarn.lock | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index adb029642..5ec82e3d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -392,16 +392,6 @@ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== -"@babel/plugin-proposal-private-property-in-object@^7.21.11": - version "7.21.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz#69d597086b6760c4126525cfa154f34631ff272c" - integrity sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-create-class-features-plugin" "^7.21.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -17175,7 +17165,16 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -19130,6 +19129,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From ed85aac4cd1a3beeba40489d4aa7ed4099e730ab Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 11 Feb 2025 15:43:41 +0100 Subject: [PATCH 14/37] chore: clarify supersetGateway api with comments --- src/api/supersetGateway.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/api/supersetGateway.js b/src/api/supersetGateway.js index 474ab3a63..56a359db4 100644 --- a/src/api/supersetGateway.js +++ b/src/api/supersetGateway.js @@ -1,7 +1,10 @@ import { useConfig } from '@dhis2/app-service-config' import { useCallback, useMemo } from 'react' -const useSupersetApiUrl = (resource) => { +/* Since the superset gateway is not part of the DHIS2 Core Web API + * we need to compute the API URL and use regular fetch requests + * instead of using the dataEngine. */ +const useSupersetGatewayApiUrl = (resource) => { const { baseUrl } = useConfig() const url = useMemo(() => { // Trim slashes from start and end @@ -9,7 +12,10 @@ const useSupersetApiUrl = (resource) => { const urlInstance = new URL( `superset-gateway/api/${cleanedResource}`, baseUrl === '..' - ? window.location.href.split('dhis-web-dashboard/')[0] + ? /* On production instances the baseUrl is a relative URL (..) + * To obtain an absolute URL we can read the part of the window + * location that preceedes the app-path */ + window.location.href.split('dhis-web-dashboard/')[0] : `${baseUrl}/` ) return urlInstance.href @@ -18,7 +24,8 @@ const useSupersetApiUrl = (resource) => { } export const useFetchSupersetBaseUrl = () => { - const url = useSupersetApiUrl('info') + const url = useSupersetGatewayApiUrl('info') + // Note that this is an unauthenticated request const fetchSupersetBaseUrl = useCallback(async () => { const response = await fetch(url) if (!response.ok) { @@ -36,7 +43,10 @@ export const useFetchSupersetBaseUrl = () => { } export const usePostSupersetGuestToken = (dashboardId) => { - const url = useSupersetApiUrl(`guestTokens/dhis2/dashboards/${dashboardId}`) + const url = useSupersetGatewayApiUrl( + `guestTokens/dhis2/dashboards/${dashboardId}` + ) + // This is an authenticated request which relies on the cookie set by DHIS2 Core const postSupersetGuestToken = useCallback(async () => { const response = await fetch(url, { method: 'POST', From c3d959fc638811e5ceec85682349b57757059ead Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 11 Feb 2025 16:10:20 +0100 Subject: [PATCH 15/37] chore: rename hasSupersetSupport to isSupersetSupported --- .../CreateDashboardButton/CreateDashboardButton.js | 8 ++++---- src/components/SystemSettingsProvider.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js b/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js index 763064e3b..bf9439c64 100644 --- a/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js +++ b/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js @@ -2,13 +2,13 @@ import { Button, IconAdd16 } from '@dhis2/ui' import React, { useCallback, useState } from 'react' import { useHistory } from 'react-router-dom' import { CreateSupersetEmbeddedDashboard } from '../../ConfigureSupersetEmbeddedDashboardModal/index.js' -import { useHasSupersetSupport } from '../../SystemSettingsProvider.js' +import { useIsSupersetSupported } from '../../SystemSettingsProvider.js' import { ChooseDashboardTypeModal } from './ChooseDashboardTypeModal.js' import styles from './styles/CreateDashboardButton.module.css' export const CreateDashboardButton = () => { const history = useHistory() - const hasSupersetSupport = useHasSupersetSupport() + const isSupersetSupported = useIsSupersetSupported() const [isChooseDashboardTypeModalOpen, setIsChooseDashboardTypeModalOpen] = useState(false) const [isCreateSupersetDashboardOpen, setIsCreateSupersetDashboardOpen] = @@ -17,12 +17,12 @@ export const CreateDashboardButton = () => { history.push('/new') }, [history]) const handleCreateButtonClick = useCallback(() => { - if (hasSupersetSupport) { + if (isSupersetSupported) { setIsChooseDashboardTypeModalOpen(true) } else { navigateToNewInternalDashboardView() } - }, [hasSupersetSupport, navigateToNewInternalDashboardView]) + }, [isSupersetSupported, navigateToNewInternalDashboardView]) return ( <> diff --git a/src/components/SystemSettingsProvider.js b/src/components/SystemSettingsProvider.js index bbbedd2aa..541f87c82 100644 --- a/src/components/SystemSettingsProvider.js +++ b/src/components/SystemSettingsProvider.js @@ -60,7 +60,7 @@ SystemSettingsProvider.propTypes = { export default SystemSettingsProvider export const useSystemSettings = () => useContext(SystemSettingsCtx) -export const useHasSupersetSupport = () => { +export const useIsSupersetSupported = () => { const { systemSettings: { embeddedDashboardsEnabled, supersetBaseUrl }, } = useSystemSettings() From 8857f208ca5b41160891b15343f103e779b77af8 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 11 Feb 2025 16:13:06 +0100 Subject: [PATCH 16/37] fix: correct position of conditional chain --- src/reducers/selected.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/reducers/selected.js b/src/reducers/selected.js index 7984fc3b7..f4c9b4d37 100644 --- a/src/reducers/selected.js +++ b/src/reducers/selected.js @@ -59,9 +59,9 @@ export const sGetSelectedSupersetEmbedData = (state) => { } export const msGetSelectedSupersetEmbedData = createSelector( [ - (state) => state.selected?.embedded.id, - (state) => state.selected?.embedded.options.hideChartControls, - (state) => state.selected?.embedded.options.filters.expanded, + (state) => state.selected.embedded?.id, + (state) => state.selected.embedded?.options.hideChartControls, + (state) => state.selected.embedded?.options.filters.expanded, ], (id, hideChartControls, expanded) => ({ id, From d20a1a72a12149ba43ace6b3a2bf5f1635bb7136 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 11 Feb 2025 16:56:57 +0100 Subject: [PATCH 17/37] chore: use individual constants for field names --- .../SupersetEmbeddedDashboardFields.js | 21 ++++++++---- ...persetEmbeddedDashboardFieldsState.spec.js | 33 +++++++++++-------- ...useSupersetEmbeddedDashboardFieldsState.js | 29 ++++++++-------- 3 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/SupersetEmbeddedDashboardFields.js b/src/components/ConfigureSupersetEmbeddedDashboardModal/SupersetEmbeddedDashboardFields.js index 91e17be08..ac516b49f 100644 --- a/src/components/ConfigureSupersetEmbeddedDashboardModal/SupersetEmbeddedDashboardFields.js +++ b/src/components/ConfigureSupersetEmbeddedDashboardModal/SupersetEmbeddedDashboardFields.js @@ -2,7 +2,14 @@ import i18n from '@dhis2/d2-i18n' import { CheckboxField, InputField, TextAreaField } from '@dhis2/ui' import PropTypes from 'prop-types' import React from 'react' -import { fieldNames } from '../../modules/useSupersetEmbeddedDashboardFieldsState.js' +import { + FIELD_NAME_TITLE, + FIELD_NAME_CODE, + FIELD_NAME_DESCRIPTION, + FIELD_NAME_SUPERSET_EMBED_ID, + FIELD_NAME_EXPAND_FILTERS, + FIELD_NAME_SHOW_CHART_CONTROLS, +} from '../../modules/useSupersetEmbeddedDashboardFieldsState.js' import styles from './styles/SupersetEmbeddedDashboardFields.module.css' export const SupersetEmbeddedDashboardFields = ({ @@ -24,7 +31,7 @@ export const SupersetEmbeddedDashboardFields = ({ onChange={onChange} value={values.title} disabled={submitting} - name={fieldNames.title} + name={FIELD_NAME_TITLE} className={styles.textField} />
diff --git a/src/modules/__tests__/useSupersetEmbeddedDashboardFieldsState.spec.js b/src/modules/__tests__/useSupersetEmbeddedDashboardFieldsState.spec.js index fbd47552f..3dc902c06 100644 --- a/src/modules/__tests__/useSupersetEmbeddedDashboardFieldsState.spec.js +++ b/src/modules/__tests__/useSupersetEmbeddedDashboardFieldsState.spec.js @@ -2,7 +2,12 @@ import { createInitialState, defaultInitialValues, FIELD_CHANGE, - fieldNames, + FIELD_NAME_CODE, + FIELD_NAME_DESCRIPTION, + FIELD_NAME_EXPAND_FILTERS, + FIELD_NAME_SHOW_CHART_CONTROLS, + FIELD_NAME_SUPERSET_EMBED_ID, + FIELD_NAME_TITLE, isValidUuid, reducer, RESET_FIELD_STATE, @@ -84,7 +89,7 @@ describe('superset embedded fields state reducer', () => { expect( reducer(initialState, { type: FIELD_CHANGE, - payload: { name: fieldNames.title, value: UPDATED_TITLE }, + payload: { name: FIELD_NAME_TITLE, value: UPDATED_TITLE }, }) ).toEqual(expectedState) }) @@ -102,7 +107,7 @@ describe('superset embedded fields state reducer', () => { reducer(initialState, { type: FIELD_CHANGE, payload: { - name: fieldNames.showChartControls, + name: FIELD_NAME_SHOW_CHART_CONTROLS, checked: false, }, }) @@ -115,7 +120,7 @@ describe('superset embedded fields state reducer', () => { }) const stateAfterTitleChange = reducer(initialState, { type: FIELD_CHANGE, - payload: { name: fieldNames.title, value: UPDATED_TITLE }, + payload: { name: FIELD_NAME_TITLE, value: UPDATED_TITLE }, }) expect(stateAfterTitleChange.values.title).toBe(UPDATED_TITLE) @@ -123,7 +128,7 @@ describe('superset embedded fields state reducer', () => { const stateAfterTitleReset = reducer(stateAfterTitleChange, { type: FIELD_CHANGE, - payload: { name: fieldNames.title, value: INITIAL_TITLE }, + payload: { name: FIELD_NAME_TITLE, value: INITIAL_TITLE }, }) expect(stateAfterTitleReset.values.title).toBe(INITIAL_TITLE) @@ -132,7 +137,7 @@ describe('superset embedded fields state reducer', () => { it('reports superset embed ID is valid if the field is changed to a valid value', () => { const stateAfterEmbedIdChange = reducer(createInitialState(), { type: FIELD_CHANGE, - payload: { name: fieldNames.supersetEmbedId, value: UUID_V1 }, + payload: { name: FIELD_NAME_SUPERSET_EMBED_ID, value: UUID_V1 }, }) expect(stateAfterEmbedIdChange.values.supersetEmbedId).toBe(UUID_V1) @@ -154,11 +159,11 @@ describe('superset embedded fields state reducer', () => { state = reducer(state, { type: SUPERSET_FIELD_BLUR }) state = reducer(state, { type: FIELD_CHANGE, - payload: { name: fieldNames.title, value: INITIAL_TITLE }, + payload: { name: FIELD_NAME_TITLE, value: INITIAL_TITLE }, }) state = reducer(state, { type: FIELD_CHANGE, - payload: { name: fieldNames.supersetEmbedId, value: UUID_V1 }, + payload: { name: FIELD_NAME_SUPERSET_EMBED_ID, value: UUID_V1 }, }) expect(state.values.title).toBe(INITIAL_TITLE) expect(state.values.supersetEmbedId).toBe(UUID_V1) @@ -168,12 +173,12 @@ describe('superset embedded fields state reducer', () => { // Dispatch reset action const newValues = { - [fieldNames.title]: UPDATED_TITLE, - [fieldNames.code]: 'SOME_CODE', - [fieldNames.description]: 'A description text', - [fieldNames.supersetEmbedId]: NILL_UUID, - [fieldNames.showChartControls]: false, - [fieldNames.expandFilters]: true, + [FIELD_NAME_TITLE]: UPDATED_TITLE, + [FIELD_NAME_CODE]: 'SOME_CODE', + [FIELD_NAME_DESCRIPTION]: 'A description text', + [FIELD_NAME_SUPERSET_EMBED_ID]: NILL_UUID, + [FIELD_NAME_SHOW_CHART_CONTROLS]: false, + [FIELD_NAME_EXPAND_FILTERS]: true, } state = reducer(state, { type: RESET_FIELD_STATE, diff --git a/src/modules/useSupersetEmbeddedDashboardFieldsState.js b/src/modules/useSupersetEmbeddedDashboardFieldsState.js index 10a81dcda..9c6aea409 100644 --- a/src/modules/useSupersetEmbeddedDashboardFieldsState.js +++ b/src/modules/useSupersetEmbeddedDashboardFieldsState.js @@ -1,20 +1,19 @@ import { useCallback, useReducer } from 'react' -export const fieldNames = { - title: 'title', - code: 'code', - description: 'description', - supersetEmbedId: 'supersetEmbedId', - showChartControls: 'showChartControls', - expandFilters: 'expandFilters', -} +export const FIELD_NAME_TITLE = 'title' +export const FIELD_NAME_CODE = 'code' +export const FIELD_NAME_DESCRIPTION = 'description' +export const FIELD_NAME_SUPERSET_EMBED_ID = 'supersetEmbedId' +export const FIELD_NAME_SHOW_CHART_CONTROLS = 'showChartControls' +export const FIELD_NAME_EXPAND_FILTERS = 'expandFilters' + export const defaultInitialValues = { - [fieldNames.title]: '', - [fieldNames.code]: '', - [fieldNames.description]: '', - [fieldNames.supersetEmbedId]: '', - [fieldNames.showChartControls]: true, - [fieldNames.expandFilters]: false, + [FIELD_NAME_TITLE]: '', + [FIELD_NAME_CODE]: '', + [FIELD_NAME_DESCRIPTION]: '', + [FIELD_NAME_SUPERSET_EMBED_ID]: '', + [FIELD_NAME_SHOW_CHART_CONTROLS]: true, + [FIELD_NAME_EXPAND_FILTERS]: false, } export const FIELD_CHANGE = 'FIELD_CHANGE' export const SUPERSET_FIELD_BLUR = 'SUPERSET_FIELD_BLUR' @@ -46,7 +45,7 @@ export const reducer = (state, { type, payload }) => { ...state, values, isSupersetEmbedIdValid: - payload.name === fieldNames.supersetEmbedId + payload.name === FIELD_NAME_SUPERSET_EMBED_ID ? isValidUuid(payload.value) : state.isSupersetEmbedIdValid, hasFieldChanges: Object.entries(values).some( From 7f170a209a5b0b8ec5e6184dd49dd94b75e4e7a3 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 12 Feb 2025 11:56:43 +0100 Subject: [PATCH 18/37] chore: improve naming consistency for blocks --- src/components/DashboardsBar/DashboardsBar.js | 2 +- src/components/DashboardsBar/styles/DashboardsBar.module.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/DashboardsBar/DashboardsBar.js b/src/components/DashboardsBar/DashboardsBar.js index db9085e21..d70f3e3ff 100644 --- a/src/components/DashboardsBar/DashboardsBar.js +++ b/src/components/DashboardsBar/DashboardsBar.js @@ -6,7 +6,7 @@ import styles from './styles/DashboardsBar.module.css' export const DashboardsBar = () => (
-
+
diff --git a/src/components/DashboardsBar/styles/DashboardsBar.module.css b/src/components/DashboardsBar/styles/DashboardsBar.module.css index 7321f001b..8f7474f57 100644 --- a/src/components/DashboardsBar/styles/DashboardsBar.module.css +++ b/src/components/DashboardsBar/styles/DashboardsBar.module.css @@ -6,7 +6,7 @@ align-items: center; } -.blockCreationNavigation { +.creationNavigationBlock { display: flex; block-size: 100%; align-items: center; @@ -17,7 +17,7 @@ } @media only screen and (max-width: 480px) { - .blockCreationNavigation { + .creationNavigationBlock { display: none; } } From 52707066f2586dd3ac3bba314169657ec222c8ba Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 12 Feb 2025 11:58:35 +0100 Subject: [PATCH 19/37] chore: solve sonarcube issue by removing commented code block --- cypress.config.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/cypress.config.js b/cypress.config.js index 91c528dcc..d1b20c73b 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -5,9 +5,6 @@ const createEsbuildPlugin = require('@badeball/cypress-cucumber-preprocessor/esb const createBundler = require('@bahmutov/cypress-esbuild-preprocessor') const { chromeAllowXSiteCookies } = require('@dhis2/cypress-plugins') const { defineConfig } = require('cypress') -// const { -// excludeByVersionTags, -// } = require('./cypress/plugins/excludeByVersionTags.js') async function setupNodeEvents(on, config) { await addCucumberPreprocessorPlugin(on, config) From 29b8fe537a2b7557dd4e7515927a441d4a7821ee Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 12 Feb 2025 12:01:56 +0100 Subject: [PATCH 20/37] chore: reword e2e test description to better reflect its limitations --- cypress/e2e/embedded_superset_dashboard.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/embedded_superset_dashboard.cy.js b/cypress/e2e/embedded_superset_dashboard.cy.js index 2e417e66a..95972566e 100644 --- a/cypress/e2e/embedded_superset_dashboard.cy.js +++ b/cypress/e2e/embedded_superset_dashboard.cy.js @@ -147,7 +147,7 @@ describe('Creating, viewing, editing and deleting an embedded superset dashboard cy.getByDataTest('dashboard-unstarred').should('be.visible') }) - it('can share the superset embedded dashboard', () => { + it('can open the sharing dialog', () => { cy.contains('button', 'Share').should('be.enabled').click() cy.contains('h1', `Sharing and access: ${NAME}`).should('be.visible') // We don't test the actual sharing, just if the sharing modal pops up From 11642202cff070f2371c39f722b725f4dd24b30f Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 12 Feb 2025 12:05:18 +0100 Subject: [PATCH 21/37] chore(e2e): merge disabled buttons step into creation step --- cypress/e2e/embedded_superset_dashboard.cy.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cypress/e2e/embedded_superset_dashboard.cy.js b/cypress/e2e/embedded_superset_dashboard.cy.js index 95972566e..9fb84e950 100644 --- a/cypress/e2e/embedded_superset_dashboard.cy.js +++ b/cypress/e2e/embedded_superset_dashboard.cy.js @@ -110,9 +110,8 @@ describe('Creating, viewing, editing and deleting an embedded superset dashboard .should('be.visible') .and('have.attr', 'src') .and('contain', UUID) - }) - it('has some options disabled in the action-bar', () => { + // some options are disabled // Primary actions cy.contains('button', 'Slideshow').should('be.disabled') cy.contains('button', 'Filter').should('be.disabled') From a96ca85ffb7f229d4c5f43215077a07f865b436a Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 12 Feb 2025 12:17:59 +0100 Subject: [PATCH 22/37] chore: test showing and hiding the description in a single test step --- cypress/e2e/embedded_superset_dashboard.cy.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/embedded_superset_dashboard.cy.js b/cypress/e2e/embedded_superset_dashboard.cy.js index 9fb84e950..0437ec60b 100644 --- a/cypress/e2e/embedded_superset_dashboard.cy.js +++ b/cypress/e2e/embedded_superset_dashboard.cy.js @@ -129,11 +129,13 @@ describe('Creating, viewing, editing and deleting an embedded superset dashboard cy.get('.backdrop').should('be.visible').click() }) - it('shows the description', () => { + it('shows and hides the description', () => { cy.getByDataTest('more-actions-button').should('be.enabled').click() cy.contains('a', 'Show description').should('be.visible').click() cy.contains(DESCRIPTION).should('be.visible') - // Keep visible so we can check it updates correctly later on + cy.getByDataTest('more-actions-button').should('be.enabled').click() + cy.contains('a', 'Hide description').should('be.visible').click() + cy.contains(DESCRIPTION_UPATED).should('not.exist') }) it('stars and unstars the superset embedded dashboard', () => { From 1a437bc7865792d456925edc89c5b8f9d6bca1a0 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 12 Feb 2025 12:18:52 +0100 Subject: [PATCH 23/37] chore: show description before asserting the updated description text --- cypress/e2e/embedded_superset_dashboard.cy.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cypress/e2e/embedded_superset_dashboard.cy.js b/cypress/e2e/embedded_superset_dashboard.cy.js index 0437ec60b..190ed7943 100644 --- a/cypress/e2e/embedded_superset_dashboard.cy.js +++ b/cypress/e2e/embedded_superset_dashboard.cy.js @@ -182,7 +182,13 @@ describe('Creating, viewing, editing and deleting an embedded superset dashboard cy.contains('h3', NAME_UPDATED).should('be.visible') cy.contains('External data').should('be.visible') + + // First show the description + cy.getByDataTest('more-actions-button').should('be.enabled').click() + cy.contains('a', 'Show description').should('be.visible').click() + // Ensure it is showing the updated description cy.contains(DESCRIPTION_UPATED).should('be.visible') + // An iframe should be visible with the UUID in the src cy.get('iframe') .should('be.visible') From 379682d3b730eb453d311326655a46320b28eb04 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 12 Feb 2025 12:20:35 +0100 Subject: [PATCH 24/37] fix: restore last-updated-tag to original width --- src/components/DashboardsBar/InformationBlock/LastUpdatedTag.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DashboardsBar/InformationBlock/LastUpdatedTag.js b/src/components/DashboardsBar/InformationBlock/LastUpdatedTag.js index f347c65e0..081ddcbd9 100644 --- a/src/components/DashboardsBar/InformationBlock/LastUpdatedTag.js +++ b/src/components/DashboardsBar/InformationBlock/LastUpdatedTag.js @@ -30,7 +30,7 @@ const LastUpdatedTag = ({ id }) => { > {(props) => (
- {message} + {message}
)} From 35d0163d9fb95dca1a8de0f8c271507b032e7c8a Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 12 Feb 2025 14:27:23 +0100 Subject: [PATCH 25/37] chore: regenerate pot file after textual changes --- i18n/en.pot | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index c205d3b21..9aa470633 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,14 +5,14 @@ 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: 2025-02-10T11:08:21.621Z\n" -"PO-Revision-Date: 2025-02-10T11:08:21.622Z\n" +"POT-Creation-Date: 2025-02-12T13:19:03.864Z\n" +"PO-Revision-Date: 2025-02-12T13:19:03.865Z\n" msgid "Untitled dashboard" msgstr "Untitled dashboard" -msgid "New dashboard: configure external source (superset)" -msgstr "New dashboard: configure external source (superset)" +msgid "New dashboard: external" +msgstr "New dashboard: external" msgid "Could not create dashboard" msgstr "Could not create dashboard" @@ -32,6 +32,13 @@ msgstr "Title" msgid "Code" msgstr "Code" +msgid "" +"A unique code to reference this dashboard in other apps and services. Max " +"50 characters." +msgstr "" +"A unique code to reference this dashboard in other apps and services. Max " +"50 characters." + msgid "Description" msgstr "Description" @@ -81,14 +88,14 @@ msgstr "Edit external dashboard" msgid "New dashboard: choose type" msgstr "New dashboard: choose type" -msgid "Internal: Data from {{systemName}}" -msgstr "Internal: Data from {{systemName}}" +msgid "Internal" +msgstr "Internal" msgid "Show data and visualizations from this DHIS2 instance." msgstr "Show data and visualizations from this DHIS2 instance." -msgid "External: Data from another source" -msgstr "External: Data from another source" +msgid "External" +msgstr "External" msgid "Embed a dashboard from a third-party source, like Superset." msgstr "Embed a dashboard from a third-party source, like Superset." From f61a0fb34da2915a4d1897e83051cdd2e0f05bcb Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 12 Feb 2025 14:27:59 +0100 Subject: [PATCH 26/37] fix: add max height of 180px to description field --- .../styles/SupersetEmbeddedDashboardFields.module.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardFields.module.css b/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardFields.module.css index bde8a99dd..0df42b45d 100644 --- a/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardFields.module.css +++ b/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardFields.module.css @@ -1,6 +1,9 @@ .textField { margin-block-end: var(--spacers-dp12); } +.textField textarea { + max-height: 180px; +} .options { all: unset; } From 3c5fc26871fc190419793c255674aad61de255ba Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 12 Feb 2025 14:30:01 +0100 Subject: [PATCH 27/37] fix: ensure Superset is capitalized in error message --- i18n/en.pot | 8 ++++---- src/pages/view/EmbeddedSupersetDashboard.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 9aa470633..2bd528481 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: 2025-02-12T13:19:03.864Z\n" -"PO-Revision-Date: 2025-02-12T13:19:03.865Z\n" +"POT-Creation-Date: 2025-02-12T13:29:10.634Z\n" +"PO-Revision-Date: 2025-02-12T13:29:10.634Z\n" msgid "Untitled dashboard" msgstr "Untitled dashboard" @@ -664,8 +664,8 @@ msgstr "Fetching external dashboard" msgid "Error" msgstr "Error" -msgid "Could not load superset dashboard" -msgstr "Could not load superset dashboard" +msgid "Could not load Superset dashboard" +msgstr "Could not load Superset dashboard" msgid "Retry" msgstr "Retry" diff --git a/src/pages/view/EmbeddedSupersetDashboard.js b/src/pages/view/EmbeddedSupersetDashboard.js index 830edae15..13a8ab1fb 100644 --- a/src/pages/view/EmbeddedSupersetDashboard.js +++ b/src/pages/view/EmbeddedSupersetDashboard.js @@ -93,7 +93,7 @@ export const EmbeddedSupersetDashboard = () => { {error && (

- {i18n.t('Could not load superset dashboard')} + {i18n.t('Could not load Superset dashboard')}

Date: Wed, 12 Feb 2025 14:55:23 +0100 Subject: [PATCH 29/37] chore: remove redundant nsSeparators in i18n.t calls --- .../CreateDashboardButton/ChooseDashboardTypeModal.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js b/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js index 02ea92691..c4510174f 100644 --- a/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js +++ b/src/components/DashboardsBar/CreateDashboardButton/ChooseDashboardTypeModal.js @@ -45,9 +45,7 @@ export const ChooseDashboardTypeModal = ({ value={TYPE_INTERNAL} checked={selectedType === TYPE_INTERNAL} onChange={handleDashboardTypeChange} - title={i18n.t('Internal', { - nsSeparator: '###', - })} + title={i18n.t('Internal')} subtitle={i18n.t( 'Show data and visualizations from this DHIS2 instance.' )} @@ -56,9 +54,7 @@ export const ChooseDashboardTypeModal = ({ value={TYPE_SUPERSET} checked={selectedType === TYPE_SUPERSET} onChange={handleDashboardTypeChange} - title={i18n.t('External', { - nsSeparator: '###', - })} + title={i18n.t('External')} subtitle={i18n.t( 'Embed a dashboard from a third-party source, like Superset.' )} From db04f8ce88f4764ccb70fd79cbfe5dff2c843168 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 12 Feb 2025 17:17:04 +0100 Subject: [PATCH 30/37] chore: rename files and and components according to PR feedback Never use Superset and Embedded together, so that names are shorter and all superset dashboards are embedded so the embedded part is redundant anyway Move and rename the superset dashboard create/edit components --- ...shboard.cy.js => superset_dashboard.cy.js} | 0 i18n/en.pot | 31 ++++++++++--------- .../index.js | 2 -- .../CreateSupersetDashboardModal.js} | 20 ++++++------ .../SupersetDashboardFields.js} | 8 ++--- .../UpdateSupersetDashboardModal.js} | 18 +++++------ .../ConfigureSupersetDashboard/index.js | 2 ++ .../SupersetDashboardFields.module.css} | 0 .../styles/SupersetDashboardModal.module.css} | 0 .../CreateDashboardButton.js | 14 ++++----- .../InformationBlock/ActionsBar.js | 4 +-- ...> useSupersetDashboardFieldsState.spec.js} | 2 +- ...s => parseSupersetDashboardFieldValues.js} | 2 +- ....js => useSupersetDashboardFieldsState.js} | 2 +- ...ion.js => useSupersetDashboardMutation.js} | 6 ++-- ...ersetDashboard.js => SupersetDashboard.js} | 12 +++---- src/pages/view/ViewDashboardContent.js | 4 +-- ...odule.css => SupersetDashboard.module.css} | 0 18 files changed, 65 insertions(+), 62 deletions(-) rename cypress/e2e/{embedded_superset_dashboard.cy.js => superset_dashboard.cy.js} (100%) delete mode 100644 src/components/ConfigureSupersetEmbeddedDashboardModal/index.js rename src/components/{ConfigureSupersetEmbeddedDashboardModal/CreateSupersetEmbeddedDashboard.js => DashboardsBar/ConfigureSupersetDashboard/CreateSupersetDashboardModal.js} (82%) rename src/components/{ConfigureSupersetEmbeddedDashboardModal/SupersetEmbeddedDashboardFields.js => DashboardsBar/ConfigureSupersetDashboard/SupersetDashboardFields.js} (93%) rename src/components/{ConfigureSupersetEmbeddedDashboardModal/UpdateSupersetEmbeddedDashboard.js => DashboardsBar/ConfigureSupersetDashboard/UpdateSupersetDashboardModal.js} (91%) create mode 100644 src/components/DashboardsBar/ConfigureSupersetDashboard/index.js rename src/components/{ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardFields.module.css => DashboardsBar/ConfigureSupersetDashboard/styles/SupersetDashboardFields.module.css} (100%) rename src/components/{ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardModal.module.css => DashboardsBar/ConfigureSupersetDashboard/styles/SupersetDashboardModal.module.css} (100%) rename src/modules/__tests__/{useSupersetEmbeddedDashboardFieldsState.spec.js => useSupersetDashboardFieldsState.spec.js} (99%) rename src/modules/{parseSupersetEmbeddedDashboardFieldValues.js => parseSupersetDashboardFieldValues.js} (85%) rename src/modules/{useSupersetEmbeddedDashboardFieldsState.js => useSupersetDashboardFieldsState.js} (98%) rename src/modules/{useSupersetEmbeddedDashboardMutation.js => useSupersetDashboardMutation.js} (92%) rename src/pages/view/{EmbeddedSupersetDashboard.js => SupersetDashboard.js} (91%) rename src/pages/view/styles/{EmbeddedSupersetDashboard.module.css => SupersetDashboard.module.css} (100%) diff --git a/cypress/e2e/embedded_superset_dashboard.cy.js b/cypress/e2e/superset_dashboard.cy.js similarity index 100% rename from cypress/e2e/embedded_superset_dashboard.cy.js rename to cypress/e2e/superset_dashboard.cy.js diff --git a/i18n/en.pot b/i18n/en.pot index 2bd528481..fc1e5d8af 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: 2025-02-12T13:29:10.634Z\n" -"PO-Revision-Date: 2025-02-12T13:29:10.634Z\n" +"POT-Creation-Date: 2025-02-12T15:35:10.919Z\n" +"PO-Revision-Date: 2025-02-12T15:35:10.920Z\n" msgid "Untitled dashboard" msgstr "Untitled dashboard" @@ -185,6 +185,9 @@ msgstr "Failed to unstar the dashboard" msgid "Failed to star the dashboard" msgstr "Failed to star the dashboard" +msgid "This dashboard is showing data from outside this system" +msgstr "This dashboard is showing data from outside this system" + msgid "External data" msgstr "External data" @@ -658,18 +661,6 @@ msgstr "Requested dashboard not found" msgid "No description" msgstr "No description" -msgid "Fetching external dashboard" -msgstr "Fetching external dashboard" - -msgid "Error" -msgstr "Error" - -msgid "Could not load Superset dashboard" -msgstr "Could not load Superset dashboard" - -msgid "Retry" -msgstr "Retry" - msgid "{{count}} selected" msgid_plural "{{count}} selected" msgstr[0] "{{count}} selected" @@ -711,6 +702,18 @@ msgid_plural "{{count}} filters active" msgstr[0] "{{count}} filter active" msgstr[1] "{{count}} filters active" +msgid "Fetching external dashboard" +msgstr "Fetching external dashboard" + +msgid "Error" +msgstr "Error" + +msgid "Could not load Superset dashboard" +msgstr "Could not load Superset dashboard" + +msgid "Retry" +msgstr "Retry" + msgid "Loading dashboard – {{name}}" msgstr "Loading dashboard – {{name}}" diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/index.js b/src/components/ConfigureSupersetEmbeddedDashboardModal/index.js deleted file mode 100644 index f17157e6b..000000000 --- a/src/components/ConfigureSupersetEmbeddedDashboardModal/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { CreateSupersetEmbeddedDashboard } from './CreateSupersetEmbeddedDashboard.js' -export { UpdateSupersetEmbeddedDashboard } from './UpdateSupersetEmbeddedDashboard.js' diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/CreateSupersetEmbeddedDashboard.js b/src/components/DashboardsBar/ConfigureSupersetDashboard/CreateSupersetDashboardModal.js similarity index 82% rename from src/components/ConfigureSupersetEmbeddedDashboardModal/CreateSupersetEmbeddedDashboard.js rename to src/components/DashboardsBar/ConfigureSupersetDashboard/CreateSupersetDashboardModal.js index a8280e311..86e8d1dfe 100644 --- a/src/components/ConfigureSupersetEmbeddedDashboardModal/CreateSupersetEmbeddedDashboard.js +++ b/src/components/DashboardsBar/ConfigureSupersetDashboard/CreateSupersetDashboardModal.js @@ -12,19 +12,19 @@ import PropTypes from 'prop-types' import React, { useCallback, useState } from 'react' import { useDispatch } from 'react-redux' import { useHistory } from 'react-router-dom' -import { tFetchDashboards } from '../../actions/dashboards.js' -import { parseSupersetEmbeddedDashboardFieldValues } from '../../modules/parseSupersetEmbeddedDashboardFieldValues.js' -import { useSupersetEmbeddedDashboardFieldsState } from '../../modules/useSupersetEmbeddedDashboardFieldsState.js' -import styles from './styles/SupersetEmbeddedDashboardModal.module.css' -import { SupersetEmbeddedDashboardFields } from './SupersetEmbeddedDashboardFields.js' +import { tFetchDashboards } from '../../../actions/dashboards.js' +import { parseSupersetDashboardFieldValues } from '../../../modules/parseSupersetDashboardFieldValues.js' +import { useSupersetDashboardFieldsState } from '../../../modules/useSupersetDashboardFieldsState.js' +import styles from './styles/SupersetDashboardModal.module.css' +import { SupersetDashboardFields } from './SupersetDashboardFields.js' const postDashboardQuery = { resource: 'dashboards', type: 'create', - data: ({ values }) => parseSupersetEmbeddedDashboardFieldValues(values), + data: ({ values }) => parseSupersetDashboardFieldValues(values), } -export const CreateSupersetEmbeddedDashboard = ({ +export const CreateSupersetDashboardModal = ({ backToChooseDashboardModal, closeModal, }) => { @@ -41,7 +41,7 @@ export const CreateSupersetEmbeddedDashboard = ({ values, onChange, onSupersetEmbedIdFieldBlur, - } = useSupersetEmbeddedDashboardFieldsState() + } = useSupersetDashboardFieldsState() const handleSubmit = useCallback( async (event) => { event.preventDefault() @@ -62,7 +62,7 @@ export const CreateSupersetEmbeddedDashboard = ({ {i18n.t('New dashboard: external', { nsSeparator: '###' })} - { +export const UpdateSupersetDashboardModal = ({ closeModal }) => { const { queryLoading, queryHasError, @@ -31,7 +31,7 @@ export const UpdateSupersetEmbeddedDashboard = ({ closeModal }) => { setShowDeleteConfirmDialog, handleUpdate, handleDelete, - } = useSupersetEmbeddedDashboardMutation({ closeModal }) + } = useSupersetDashboardMutation({ closeModal }) const { hasFieldChanges, isSupersetEmbedIdValid, @@ -40,7 +40,7 @@ export const UpdateSupersetEmbeddedDashboard = ({ closeModal }) => { onChange, onSupersetEmbedIdFieldBlur, resetFieldsStateWithNewValues, - } = useSupersetEmbeddedDashboardFieldsState() + } = useSupersetDashboardFieldsState() useEffect(() => { if (dashboard) { @@ -124,7 +124,7 @@ export const UpdateSupersetEmbeddedDashboard = ({ closeModal }) => { )}
)} - { ) } -UpdateSupersetEmbeddedDashboard.propTypes = { +UpdateSupersetDashboardModal.propTypes = { closeModal: PropTypes.func, } diff --git a/src/components/DashboardsBar/ConfigureSupersetDashboard/index.js b/src/components/DashboardsBar/ConfigureSupersetDashboard/index.js new file mode 100644 index 000000000..329d14f01 --- /dev/null +++ b/src/components/DashboardsBar/ConfigureSupersetDashboard/index.js @@ -0,0 +1,2 @@ +export { CreateSupersetDashboardModal } from './CreateSupersetDashboardModal.js' +export { UpdateSupersetDashboardModal } from './UpdateSupersetDashboardModal.js' diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardFields.module.css b/src/components/DashboardsBar/ConfigureSupersetDashboard/styles/SupersetDashboardFields.module.css similarity index 100% rename from src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardFields.module.css rename to src/components/DashboardsBar/ConfigureSupersetDashboard/styles/SupersetDashboardFields.module.css diff --git a/src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardModal.module.css b/src/components/DashboardsBar/ConfigureSupersetDashboard/styles/SupersetDashboardModal.module.css similarity index 100% rename from src/components/ConfigureSupersetEmbeddedDashboardModal/styles/SupersetEmbeddedDashboardModal.module.css rename to src/components/DashboardsBar/ConfigureSupersetDashboard/styles/SupersetDashboardModal.module.css diff --git a/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js b/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js index bf9439c64..f9b78b05b 100644 --- a/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js +++ b/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js @@ -1,8 +1,8 @@ import { Button, IconAdd16 } from '@dhis2/ui' import React, { useCallback, useState } from 'react' import { useHistory } from 'react-router-dom' -import { CreateSupersetEmbeddedDashboard } from '../../ConfigureSupersetEmbeddedDashboardModal/index.js' import { useIsSupersetSupported } from '../../SystemSettingsProvider.js' +import { CreateSupersetDashboardModal } from '../ConfigureSupersetDashboard/index.js' import { ChooseDashboardTypeModal } from './ChooseDashboardTypeModal.js' import styles from './styles/CreateDashboardButton.module.css' @@ -11,7 +11,7 @@ export const CreateDashboardButton = () => { const isSupersetSupported = useIsSupersetSupported() const [isChooseDashboardTypeModalOpen, setIsChooseDashboardTypeModalOpen] = useState(false) - const [isCreateSupersetDashboardOpen, setIsCreateSupersetDashboardOpen] = + const [isCreateSupersetDashboardModalOpen, setIsCreateSupersetDashboardModalOpen] = useState(false) const navigateToNewInternalDashboardView = useCallback(() => { history.push('/new') @@ -41,7 +41,7 @@ export const CreateDashboardButton = () => { }} onSelectSuperset={() => { setIsChooseDashboardTypeModalOpen(false) - setIsCreateSupersetDashboardOpen(true) + setIsCreateSupersetDashboardModalOpen(true) }} onSelectInternal={() => { setIsChooseDashboardTypeModalOpen(false) @@ -49,14 +49,14 @@ export const CreateDashboardButton = () => { }} /> )} - {isCreateSupersetDashboardOpen && ( - { - setIsCreateSupersetDashboardOpen(false) + setIsCreateSupersetDashboardModalOpen(false) setIsChooseDashboardTypeModalOpen(true) }} closeModal={() => { - setIsCreateSupersetDashboardOpen(false) + setIsCreateSupersetDashboardModalOpen(false) }} /> )} diff --git a/src/components/DashboardsBar/InformationBlock/ActionsBar.js b/src/components/DashboardsBar/InformationBlock/ActionsBar.js index af5fe3d35..0a3a83433 100644 --- a/src/components/DashboardsBar/InformationBlock/ActionsBar.js +++ b/src/components/DashboardsBar/InformationBlock/ActionsBar.js @@ -30,7 +30,7 @@ import { sGetSelectedIsEmbedded, } from '../../../reducers/selected.js' import { sGetShowDescription } from '../../../reducers/showDescription.js' -import { UpdateSupersetEmbeddedDashboard } from '../../ConfigureSupersetEmbeddedDashboardModal/UpdateSupersetEmbeddedDashboard.js' +import { UpdateSupersetDashboardModal } from '../ConfigureSupersetDashboard/UpdateSupersetDashboardModal.js' import FilterSelector from './FilterSelector.js' import classes from './styles/ActionsBar.module.css' @@ -315,7 +315,7 @@ const ActionsBar = ({ open={confirmCacheDialogIsOpen} /> {updateEmbeddedDashboardModalIsOpen && ( - setUpdateEmbeddedDashboardModalIsOpen(false) } diff --git a/src/modules/__tests__/useSupersetEmbeddedDashboardFieldsState.spec.js b/src/modules/__tests__/useSupersetDashboardFieldsState.spec.js similarity index 99% rename from src/modules/__tests__/useSupersetEmbeddedDashboardFieldsState.spec.js rename to src/modules/__tests__/useSupersetDashboardFieldsState.spec.js index 3dc902c06..2cec5a873 100644 --- a/src/modules/__tests__/useSupersetEmbeddedDashboardFieldsState.spec.js +++ b/src/modules/__tests__/useSupersetDashboardFieldsState.spec.js @@ -12,7 +12,7 @@ import { reducer, RESET_FIELD_STATE, SUPERSET_FIELD_BLUR, -} from '../useSupersetEmbeddedDashboardFieldsState.js' +} from '../useSupersetDashboardFieldsState.js' const INITIAL_TITLE = 'Initial title' const UPDATED_TITLE = 'Updated title' diff --git a/src/modules/parseSupersetEmbeddedDashboardFieldValues.js b/src/modules/parseSupersetDashboardFieldValues.js similarity index 85% rename from src/modules/parseSupersetEmbeddedDashboardFieldValues.js rename to src/modules/parseSupersetDashboardFieldValues.js index b66ac01aa..38010fdff 100644 --- a/src/modules/parseSupersetEmbeddedDashboardFieldValues.js +++ b/src/modules/parseSupersetDashboardFieldValues.js @@ -1,4 +1,4 @@ -export const parseSupersetEmbeddedDashboardFieldValues = (values) => ({ +export const parseSupersetDashboardFieldValues = (values) => ({ name: values.title || 'Untitled dashboard', description: values.description, code: values.code, diff --git a/src/modules/useSupersetEmbeddedDashboardFieldsState.js b/src/modules/useSupersetDashboardFieldsState.js similarity index 98% rename from src/modules/useSupersetEmbeddedDashboardFieldsState.js rename to src/modules/useSupersetDashboardFieldsState.js index 9c6aea409..5ec1455b3 100644 --- a/src/modules/useSupersetEmbeddedDashboardFieldsState.js +++ b/src/modules/useSupersetDashboardFieldsState.js @@ -65,7 +65,7 @@ export const reducer = (state, { type, payload }) => { } } -export const useSupersetEmbeddedDashboardFieldsState = ( +export const useSupersetDashboardFieldsState = ( initialValues = defaultInitialValues ) => { const [state, dispatch] = useReducer( diff --git a/src/modules/useSupersetEmbeddedDashboardMutation.js b/src/modules/useSupersetDashboardMutation.js similarity index 92% rename from src/modules/useSupersetEmbeddedDashboardMutation.js rename to src/modules/useSupersetDashboardMutation.js index f1bc72138..c1a90ba43 100644 --- a/src/modules/useSupersetEmbeddedDashboardMutation.js +++ b/src/modules/useSupersetDashboardMutation.js @@ -8,7 +8,7 @@ import { acClearSelected, tSetSelectedDashboardById, } from '../actions/selected.js' -import { parseSupersetEmbeddedDashboardFieldValues } from '../modules/parseSupersetEmbeddedDashboardFieldValues.js' +import { parseSupersetDashboardFieldValues } from '../modules/parseSupersetDashboardFieldValues.js' import { sGetSelectedId } from '../reducers/selected.js' const getDashboardQuery = { @@ -24,7 +24,7 @@ const updateDashboardQuery = { resource: 'dashboards', type: 'update', id: ({ id }) => id, - data: ({ values }) => parseSupersetEmbeddedDashboardFieldValues(values), + data: ({ values }) => parseSupersetDashboardFieldValues(values), params: { skipTranslation: true, skipSharing: true, @@ -40,7 +40,7 @@ const parseErrorText = (error) => error?.details?.response?.errorReports[0]?.message ?? i18n.t('An unknown error occurred') -export const useSupersetEmbeddedDashboardMutation = ({ closeModal }) => { +export const useSupersetDashboardMutation = ({ closeModal }) => { const dispatch = useDispatch() const history = useHistory() const id = useSelector(sGetSelectedId) diff --git a/src/pages/view/EmbeddedSupersetDashboard.js b/src/pages/view/SupersetDashboard.js similarity index 91% rename from src/pages/view/EmbeddedSupersetDashboard.js rename to src/pages/view/SupersetDashboard.js index 13a8ab1fb..83e6526bb 100644 --- a/src/pages/view/EmbeddedSupersetDashboard.js +++ b/src/pages/view/SupersetDashboard.js @@ -10,7 +10,7 @@ import { msGetSelectedSupersetEmbedData, sGetSelectedId, } from '../../reducers/selected.js' -import styles from './styles/EmbeddedSupersetDashboard.module.css' +import styles from './styles/SupersetDashboard.module.css' const LOAD_INIT = 'LOAD_INIT' const LOAD_SUCCESS = 'LOAD_SUCCESS' @@ -36,7 +36,7 @@ const reducer = (state, action) => { } } -export const EmbeddedSupersetDashboard = () => { +export const SupersetDashboard = () => { const [{ loading, error, success }, dispatch] = useReducer( reducer, initialLoadState @@ -46,7 +46,7 @@ export const EmbeddedSupersetDashboard = () => { const embedData = useSelector(msGetSelectedSupersetEmbedData) const supersetDomain = useSupersetBaseUrl() const postSupersetGuestToken = usePostSupersetGuestToken(selectedId) - const loadEmbeddedSupersetDashboard = useCallback(async () => { + const loadSupersetDashboard = useCallback(async () => { dispatch({ type: LOAD_INIT }) try { const { id, dashboardUiConfig } = embedData @@ -67,8 +67,8 @@ export const EmbeddedSupersetDashboard = () => { if (loading || success || error) { return } - loadEmbeddedSupersetDashboard() - }, [loading, error, success, loadEmbeddedSupersetDashboard]) + loadSupersetDashboard() + }, [loading, error, success, loadSupersetDashboard]) useEffect(() => { dispatch({ type: LOAD_RESET }) @@ -100,7 +100,7 @@ export const EmbeddedSupersetDashboard = () => { small onClick={() => { dispatch({ type: LOAD_RESET }) - loadEmbeddedSupersetDashboard() + loadSupersetDashboard() }} > {i18n.t('Retry')} diff --git a/src/pages/view/ViewDashboardContent.js b/src/pages/view/ViewDashboardContent.js index de55c4df8..d12042480 100644 --- a/src/pages/view/ViewDashboardContent.js +++ b/src/pages/view/ViewDashboardContent.js @@ -8,10 +8,10 @@ import Notice from '../../components/Notice.js' import { sGetSelectedIsEmbedded } from '../../reducers/selected.js' import { ROUTE_START_PATH } from '../start/index.js' import { Description } from './Description.js' -import { EmbeddedSupersetDashboard } from './EmbeddedSupersetDashboard.js' import FilterBar from './FilterBar/FilterBar.js' import ItemGrid from './ItemGrid.js' import classes from './styles/ViewDashboard.module.css' +import { SupersetDashboard } from './SupersetDashboard.js' export const ViewDashboardContent = ({ loading, @@ -42,7 +42,7 @@ export const ViewDashboardContent = ({ {!isEmbeddedDashboard && } {isEmbeddedDashboard ? ( - + ) : ( )} diff --git a/src/pages/view/styles/EmbeddedSupersetDashboard.module.css b/src/pages/view/styles/SupersetDashboard.module.css similarity index 100% rename from src/pages/view/styles/EmbeddedSupersetDashboard.module.css rename to src/pages/view/styles/SupersetDashboard.module.css From 7ac2f189776ede34e627afb226fb468dd19a791b Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 12 Feb 2025 17:19:37 +0100 Subject: [PATCH 31/37] fix: use logical css property --- .../styles/SupersetDashboardFields.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DashboardsBar/ConfigureSupersetDashboard/styles/SupersetDashboardFields.module.css b/src/components/DashboardsBar/ConfigureSupersetDashboard/styles/SupersetDashboardFields.module.css index 0df42b45d..e58165a69 100644 --- a/src/components/DashboardsBar/ConfigureSupersetDashboard/styles/SupersetDashboardFields.module.css +++ b/src/components/DashboardsBar/ConfigureSupersetDashboard/styles/SupersetDashboardFields.module.css @@ -2,7 +2,7 @@ margin-block-end: var(--spacers-dp12); } .textField textarea { - max-height: 180px; + max-block-size: 180px; } .options { all: unset; From 843c8ec1f9c9c547be36236e306fe224f71af8db Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Thu, 13 Feb 2025 10:29:24 +0100 Subject: [PATCH 32/37] chore: fix formatting issue --- .../CreateDashboardButton/CreateDashboardButton.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js b/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js index f9b78b05b..3eae9c133 100644 --- a/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js +++ b/src/components/DashboardsBar/CreateDashboardButton/CreateDashboardButton.js @@ -11,8 +11,10 @@ export const CreateDashboardButton = () => { const isSupersetSupported = useIsSupersetSupported() const [isChooseDashboardTypeModalOpen, setIsChooseDashboardTypeModalOpen] = useState(false) - const [isCreateSupersetDashboardModalOpen, setIsCreateSupersetDashboardModalOpen] = - useState(false) + const [ + isCreateSupersetDashboardModalOpen, + setIsCreateSupersetDashboardModalOpen, + ] = useState(false) const navigateToNewInternalDashboardView = useCallback(() => { history.push('/new') }, [history]) From 2136513f0efd6dffdb885156378ae6f943d1d4c6 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 25 Feb 2025 12:51:05 +0100 Subject: [PATCH 33/37] fix: prevent flashing iframe error ui and show better error message --- i18n/en.pot | 26 ++++++++++- src/api/supersetGateway.js | 69 ++++++++++++++++++++--------- src/pages/view/SupersetDashboard.js | 11 ++++- 3 files changed, 82 insertions(+), 24 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index fc1e5d8af..6150d6544 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,12 +5,34 @@ 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: 2025-02-12T15:35:10.919Z\n" -"PO-Revision-Date: 2025-02-12T15:35:10.920Z\n" +"POT-Creation-Date: 2025-02-25T11:42:45.699Z\n" +"PO-Revision-Date: 2025-02-25T11:42:45.699Z\n" msgid "Untitled dashboard" msgstr "Untitled dashboard" +msgid "Could not get info from Superset gateway endpoint" +msgstr "Could not get info from Superset gateway endpoint" + +msgid "Dashboard with ID \"{{dashboardId}}\" not found or not accessible" +msgstr "Dashboard with ID \"{{dashboardId}}\" not found or not accessible" + +msgid "Dashboard with ID \"{{dashboardId}}\" is not embedded" +msgstr "Dashboard with ID \"{{dashboardId}}\" is not embedded" + +msgid "Embedded provider must be \"SUPERSET\" for dashboard ID \"{{dashboardId}}\"" +msgstr "Embedded provider must be \"SUPERSET\" for dashboard ID \"{{dashboardId}}\"" + +msgid "Superset Embed UUID not found for dashboard ID \"{{dashboardId}}\"" +msgstr "Superset Embed UUID not found for dashboard ID \"{{dashboardId}}\"" + +msgid "" +"Could not get guest token from Superset Gateway for dashboard ID " +"\"{{dashboardId}}\"" +msgstr "" +"Could not get guest token from Superset Gateway for dashboard ID " +"\"{{dashboardId}}\"" + msgid "New dashboard: external" msgstr "New dashboard: external" diff --git a/src/api/supersetGateway.js b/src/api/supersetGateway.js index 56a359db4..4f1ac7384 100644 --- a/src/api/supersetGateway.js +++ b/src/api/supersetGateway.js @@ -1,4 +1,5 @@ import { useConfig } from '@dhis2/app-service-config' +import i18n from '@dhis2/d2-i18n' import { useCallback, useMemo } from 'react' /* Since the superset gateway is not part of the DHIS2 Core Web API @@ -27,16 +28,16 @@ export const useFetchSupersetBaseUrl = () => { const url = useSupersetGatewayApiUrl('info') // Note that this is an unauthenticated request const fetchSupersetBaseUrl = useCallback(async () => { - const response = await fetch(url) - if (!response.ok) { - console.error(response) + try { + const response = await fetch(url) + const { supersetBaseUrl } = await response.json() + return supersetBaseUrl + } catch (error) { + console.error(error) throw new Error( - `Could not fetch info from the superset gateway: STATUS ${response.status}` + i18n.t('Could not get info from Superset gateway endpoint') ) } - - const data = await response.json() - return data.supersetBaseUrl }, [url]) return fetchSupersetBaseUrl @@ -48,20 +49,46 @@ export const usePostSupersetGuestToken = (dashboardId) => { ) // This is an authenticated request which relies on the cookie set by DHIS2 Core const postSupersetGuestToken = useCallback(async () => { - const response = await fetch(url, { - method: 'POST', - credentials: 'include', - }) - - if (!response.ok) { - console.error(response) - throw new Error( - `Could not POST guest token to superset gateway: STATUS ${response.status}` - ) + try { + const response = await fetch(url, { + method: 'POST', + credentials: 'include', + }) + const { token } = await response.json() + return token + } catch (error) { + throw new Error(parseMessageForErrorCode(dashboardId, error.code)) } - - const data = await response.json() - return data.token - }, [url]) + }, [dashboardId, url]) return postSupersetGuestToken } + +function parseMessageForErrorCode(dashboardId, errorCode) { + switch (errorCode) { + case 'E1001': + return i18n.t( + 'Dashboard with ID "{{dashboardId}}" not found or not accessible', + { dashboardId } + ) + case 'E1002': + return i18n.t( + 'Dashboard with ID "{{dashboardId}}" is not embedded', + { dashboardId } + ) + case 'E1003': + return i18n.t( + 'Embedded provider must be "SUPERSET" for dashboard ID "{{dashboardId}}"', + { dashboardId } + ) + case 'E1004': + return i18n.t( + 'Superset Embed UUID not found for dashboard ID "{{dashboardId}}"', + { dashboardId } + ) + default: + return i18n.t( + 'Could not get guest token from Superset Gateway for dashboard ID "{{dashboardId}}"', + { dashboardId } + ) + } +} diff --git a/src/pages/view/SupersetDashboard.js b/src/pages/view/SupersetDashboard.js index 83e6526bb..a462ad688 100644 --- a/src/pages/view/SupersetDashboard.js +++ b/src/pages/view/SupersetDashboard.js @@ -50,6 +50,13 @@ export const SupersetDashboard = () => { dispatch({ type: LOAD_INIT }) try { const { id, dashboardUiConfig } = embedData + /* We call this manually first, so that if it throws, the embedDashboard + * function will not be called, thus avoiding briefly showing an error UI + * in the iframe. The error has a localised message based on an error code, + * so is informative to the user */ + await postSupersetGuestToken() + /* This could still throw as well and still cause briefly showing the + * error UI in the iframe, but I don't think we can do much about that */ await embedDashboard({ id, supersetDomain, @@ -59,6 +66,7 @@ export const SupersetDashboard = () => { }) dispatch({ type: LOAD_SUCCESS }) } catch (error) { + console.error(error) dispatch({ type: LOAD_ERROR, payload: error }) } }, [embedData, postSupersetGuestToken, supersetDomain]) @@ -93,7 +101,8 @@ export const SupersetDashboard = () => { {error && (

- {i18n.t('Could not load Superset dashboard')} + {error.message ?? + i18n.t('Could not load Superset dashboard')}