From 0cf8fae87f41956ebd9e8c63d52b42b920a7a4ef Mon Sep 17 00:00:00 2001 From: Austin McGee <947888+amcgee@users.noreply.github.com> Date: Thu, 18 Feb 2021 00:26:01 +0100 Subject: [PATCH 1/6] feat: dynamically load plugin scripts and dependencies when needed --- d2.config.js | 1 + public/index.html | 90 ------------------- .../Visualization/DataVisualizerPlugin.js | 17 ++++ .../Visualization/LoadingMask.js | 9 +- .../Visualization/Visualization.js | 28 +++--- .../VisualizationItem/Visualization/plugin.js | 50 +++++++++-- src/modules/loadExternalScript.js | 47 ++++++++++ 7 files changed, 124 insertions(+), 118 deletions(-) delete mode 100644 public/index.html create mode 100644 src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js create mode 100644 src/modules/loadExternalScript.js diff --git a/d2.config.js b/d2.config.js index af8003238..aa0478838 100644 --- a/d2.config.js +++ b/d2.config.js @@ -1,4 +1,5 @@ const config = { + name: 'dashboard', type: 'app', coreApp: true, title: 'Dashboard', diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 35b218a54..000000000 --- a/public/index.html +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - %REACT_APP_DHIS2_APP_NAME% | DHIS2 - - - - - <% if (process.env.NODE_ENV === 'production') { %> - - - - <% } %> <% if (process.env.NODE_ENV !== 'production') { %> - - - - <% } %> - - - - - - - - - - -
- - - diff --git a/src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js b/src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js new file mode 100644 index 000000000..446eabe09 --- /dev/null +++ b/src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js @@ -0,0 +1,17 @@ +import React, { Suspense } from 'react' +import { useD2 } from '@dhis2/app-runtime-adapter-d2' + +const VisualizationPlugin = React.lazy(() => + import( + /* webpackChunkName: "data-visualizer-plugin" */ /* webpackPrefetch: true */ '@dhis2/data-visualizer-plugin' + ) +) + +export const DataVisualizerPlugin = props => { + const d2 = useD2({}) + return ( + }> + + + ) +} diff --git a/src/components/Item/VisualizationItem/Visualization/LoadingMask.js b/src/components/Item/VisualizationItem/Visualization/LoadingMask.js index 3218f70d8..450d706e6 100644 --- a/src/components/Item/VisualizationItem/Visualization/LoadingMask.js +++ b/src/components/Item/VisualizationItem/Visualization/LoadingMask.js @@ -1,14 +1,19 @@ import React from 'react' +import PropTypes from 'prop-types' import { CircularLoader } from '@dhis2/ui' import classes from './styles/LoadingMask.module.css' -const LoadingMask = () => { +const LoadingMask = ({ style }) => { return ( -
+
) } +LoadingMask.propTypes = { + style: PropTypes.object, +} + export default LoadingMask diff --git a/src/components/Item/VisualizationItem/Visualization/Visualization.js b/src/components/Item/VisualizationItem/Visualization/Visualization.js index 81edde976..22b313363 100644 --- a/src/components/Item/VisualizationItem/Visualization/Visualization.js +++ b/src/components/Item/VisualizationItem/Visualization/Visualization.js @@ -1,9 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import VisualizationPlugin from '@dhis2/data-visualizer-plugin' import i18n from '@dhis2/d2-i18n' -import { D2Shim } from '@dhis2/app-runtime-adapter-d2' import DefaultPlugin from './DefaultPlugin' import MapPlugin from './MapPlugin' @@ -22,6 +20,7 @@ import { getVisualizationId } from '../../../../modules/item' import memoizeOne from '../../../../modules/memoizeOne' import { sGetVisualization } from '../../../../reducers/visualizations' import { pluginIsAvailable } from './plugin' +import { DataVisualizerPlugin } from './DataVisualizerPlugin' class Visualization extends React.Component { state = { @@ -83,24 +82,17 @@ class Visualization extends React.Component { return ( <> {!this.state.pluginLoaded && ( -
- -
+ )} - - {({ d2 }) => ( - + + onLoadingComplete={this.onLoadingComplete} + forDashboard={true} + style={pluginProps.style} + /> ) } diff --git a/src/components/Item/VisualizationItem/Visualization/plugin.js b/src/components/Item/VisualizationItem/Visualization/plugin.js index 3ea4b294b..c60f158b6 100644 --- a/src/components/Item/VisualizationItem/Visualization/plugin.js +++ b/src/components/Item/VisualizationItem/Visualization/plugin.js @@ -8,27 +8,63 @@ import { } from '../../../../modules/itemTypes' import { getVisualizationId } from '../../../../modules/item' import getGridItemDomId from '../../../../modules/getGridItemDomId' +import { loadExternalScript } from '../../../../modules/loadExternalScript' //external plugins -const itemTypeToExternalPlugin = { +const itemTypeToGlobalVariable = { [MAP]: 'mapPlugin', [EVENT_REPORT]: 'eventReportPlugin', [EVENT_CHART]: 'eventChartPlugin', } + +const itemTypeToScriptPath = { + [MAP]: '/dhis-web-maps/map.js', + [EVENT_REPORT]: '/dhis-web-event-reports/eventreport.js', + [EVENT_CHART]: '/dhis-web-event-visualizer/eventchart.js', +} + const hasIntegratedPlugin = type => [CHART, REPORT_TABLE].includes(type) -const getPlugin = type => { +const getPlugin = async type => { if (hasIntegratedPlugin(type)) { return true } - const pluginName = itemTypeToExternalPlugin[type] + const pluginName = itemTypeToGlobalVariable[type] return global[pluginName] } -export const pluginIsAvailable = type => !!getPlugin(type) +const fetchPlugin = async (type, baseUrl) => { + const scripts = [] + + if (type === EVENT_REPORT || type === EVENT_CHART) { + if (process.env.NODE_ENV === 'production') { + scripts.push('./vendor/babel-polyfill-6.26.0.min.js') + scripts.push('./vendor/jquery-3.3.1.min.js') + scripts.push('./vendor/jquery-migrate-3.0.1.min.js') + } else { + scripts.push('./vendor/babel-polyfill-6.26.0.min.js') + scripts.push('./vendor/jquery-3.3.1.min.js') + scripts.push('./vendor/jquery-migrate-3.0.1.min.js') + } + } + + scripts.push(baseUrl + itemTypeToScriptPath[type]) + + await Promise.all(scripts.map(loadExternalScript)) + return getPlugin(type) +} + +export const pluginIsAvailable = type => + hasIntegratedPlugin(type) || itemTypeToGlobalVariable[type] + +export const loadPlugin = async (type, config, credentials) => { + if (!pluginIsAvailable(type)) { + return + } + + const plugin = await fetchPlugin(type, credentials.baseUrl) -export const loadPlugin = (plugin, config, credentials) => { if (!(plugin && plugin.load)) { return } @@ -60,9 +96,7 @@ export const load = async ( } const type = activeType || item.type - const plugin = getPlugin(type) - - loadPlugin(plugin, config, credentials) + await loadPlugin(type, config, credentials) } export const resize = (item, isFullscreen) => { diff --git a/src/modules/loadExternalScript.js b/src/modules/loadExternalScript.js new file mode 100644 index 000000000..aa6bb1442 --- /dev/null +++ b/src/modules/loadExternalScript.js @@ -0,0 +1,47 @@ +const isRelative = path => path.startsWith('./') +const normalizeRelativePath = path => + [process.env.PUBLIC_URL, path.replace(/^\.\//, '')].join('/') + +const isScriptLoaded = src => + document.querySelector('script[src="' + src + '"]') ? true : false + +export const loadExternalScript = src => { + if (isRelative(src)) { + src = normalizeRelativePath(src) + } + + return new Promise((resolve, reject) => { + if (isScriptLoaded(src)) { + return resolve() + } + + const element = document.createElement('script') + + element.src = src + element.type = 'text/javascript' + element.async = false + + const cleanup = () => { + console.log(`Dynamic Script Removed: ${src}`) + document.head.removeChild(element) + } + + element.onload = () => { + console.log(`Dynamic Script Loaded: ${src}`) + try { + resolve() + } catch (e) { + cleanup() + reject() + } + } + + element.onerror = () => { + console.error(`Dynamic Script Error: ${src}`) + cleanup() + reject() + } + + document.head.appendChild(element) + }) +} From 758ca989ed74c3c1af54e4f3bf335bb16ef781b1 Mon Sep 17 00:00:00 2001 From: Austin McGee <947888+amcgee@users.noreply.github.com> Date: Thu, 18 Feb 2021 00:34:47 +0100 Subject: [PATCH 2/6] chore: fix some tests --- .../__tests__/Visualization.spec.js | 2 +- .../__snapshots__/Visualization.spec.js.snap | 109 ++++++++---------- 2 files changed, 50 insertions(+), 61 deletions(-) diff --git a/src/components/Item/VisualizationItem/Visualization/__tests__/Visualization.spec.js b/src/components/Item/VisualizationItem/Visualization/__tests__/Visualization.spec.js index 01b9f2635..883787479 100644 --- a/src/components/Item/VisualizationItem/Visualization/__tests__/Visualization.spec.js +++ b/src/components/Item/VisualizationItem/Visualization/__tests__/Visualization.spec.js @@ -6,7 +6,7 @@ import configureMockStore from 'redux-mock-store' import Visualization from '../Visualization' jest.mock('@dhis2/app-runtime-adapter-d2', () => ({ - D2Shim: ({ children }) => children({ d2: {} }), + useD2: () => ({}), })) jest.mock( diff --git a/src/components/Item/VisualizationItem/Visualization/__tests__/__snapshots__/Visualization.spec.js.snap b/src/components/Item/VisualizationItem/Visualization/__tests__/__snapshots__/Visualization.spec.js.snap index 6cd520d30..a626504cf 100644 --- a/src/components/Item/VisualizationItem/Visualization/__tests__/__snapshots__/Visualization.spec.js.snap +++ b/src/components/Item/VisualizationItem/Visualization/__tests__/__snapshots__/Visualization.spec.js.snap @@ -37,65 +37,57 @@ exports[`renders a MapPlugin when activeType is MAP 1`] = ` exports[`renders a VisualizationPlugin for CHART 1`] = `
-
- - - -
+ +
-
+
`; exports[`renders a VisualizationPlugin for REPORT_TABLE 1`] = `
-
- - - -
+ +
-
- - - -
+ +
Date: Thu, 18 Feb 2021 01:09:56 +0100 Subject: [PATCH 3/6] fix: wait for in-flight fetches of the same plugin type --- .../VisualizationItem/Visualization/plugin.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/Item/VisualizationItem/Visualization/plugin.js b/src/components/Item/VisualizationItem/Visualization/plugin.js index c60f158b6..bfcefda26 100644 --- a/src/components/Item/VisualizationItem/Visualization/plugin.js +++ b/src/components/Item/VisualizationItem/Visualization/plugin.js @@ -35,6 +35,11 @@ const getPlugin = async type => { } const fetchPlugin = async (type, baseUrl) => { + const globalName = itemTypeToGlobalVariable[type] + if (global[globalName]) { + return global[globalName] // Will be a promise if fetch is in progress + } + const scripts = [] if (type === EVENT_REPORT || type === EVENT_CHART) { @@ -43,16 +48,19 @@ const fetchPlugin = async (type, baseUrl) => { scripts.push('./vendor/jquery-3.3.1.min.js') scripts.push('./vendor/jquery-migrate-3.0.1.min.js') } else { - scripts.push('./vendor/babel-polyfill-6.26.0.min.js') - scripts.push('./vendor/jquery-3.3.1.min.js') - scripts.push('./vendor/jquery-migrate-3.0.1.min.js') + scripts.push('./vendor/babel-polyfill-6.26.0.js') + scripts.push('./vendor/jquery-3.3.1.js') + scripts.push('./vendor/jquery-migrate-3.0.1.js') } } scripts.push(baseUrl + itemTypeToScriptPath[type]) - await Promise.all(scripts.map(loadExternalScript)) - return getPlugin(type) + const scriptsPromise = Promise.all(scripts.map(loadExternalScript)).then( + () => global[globalName] // At this point, has been replaced with the real thing + ) + global[globalName] = scriptsPromise + return await scriptsPromise } export const pluginIsAvailable = type => From 2a723fb4852df52305da239b8dfc82149668ad09 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 11 Mar 2021 10:49:53 +0100 Subject: [PATCH 4/6] fix: move visualizer props to DataVisualizerPlugin --- .../Visualization/DataVisualizerPlugin.js | 29 ++- .../Visualization/Visualization.js | 29 +-- .../__tests__/Visualization.spec.js | 183 ++++++++---------- .../__snapshots__/Visualization.spec.js.snap | 74 +------ 4 files changed, 111 insertions(+), 204 deletions(-) diff --git a/src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js b/src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js index 446eabe09..db7866c7f 100644 --- a/src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js +++ b/src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js @@ -1,5 +1,8 @@ -import React, { Suspense } from 'react' +import React, { Suspense, useState } from 'react' +import PropTypes from 'prop-types' import { useD2 } from '@dhis2/app-runtime-adapter-d2' +import { useUserSettings } from '../../../UserSettingsProvider' +import LoadingMask from './LoadingMask' const VisualizationPlugin = React.lazy(() => import( @@ -7,11 +10,29 @@ const VisualizationPlugin = React.lazy(() => ) ) -export const DataVisualizerPlugin = props => { - const d2 = useD2({}) +const DataVisualizerPlugin = props => { + const d2 = useD2() + const { userSettings } = useUserSettings() + const [pluginLoaded, setPluginLoaded] = useState(false) + return ( }> - + {!pluginLoaded && } + setPluginLoaded(true)} + {...props} + /> ) } + +DataVisualizerPlugin.propTypes = { + style: PropTypes.object, +} + +export default DataVisualizerPlugin diff --git a/src/components/Item/VisualizationItem/Visualization/Visualization.js b/src/components/Item/VisualizationItem/Visualization/Visualization.js index dba469ca9..6546037d1 100644 --- a/src/components/Item/VisualizationItem/Visualization/Visualization.js +++ b/src/components/Item/VisualizationItem/Visualization/Visualization.js @@ -5,7 +5,7 @@ import i18n from '@dhis2/d2-i18n' import DefaultPlugin from './DefaultPlugin' import MapPlugin from './MapPlugin' -import LoadingMask from './LoadingMask' +import DataVisualizerPlugin from './DataVisualizerPlugin' import NoVisualizationMessage from './NoVisualizationMessage' import getFilteredVisualization from './getFilteredVisualization' @@ -19,9 +19,7 @@ import { import { getVisualizationId } from '../../../../modules/item' import memoizeOne from '../../../../modules/memoizeOne' import { sGetVisualization } from '../../../../reducers/visualizations' -import { UserSettingsCtx } from '../../../UserSettingsProvider' import { pluginIsAvailable } from './plugin' -import { DataVisualizerPlugin } from './DataVisualizerPlugin' class Visualization extends React.Component { state = { @@ -81,24 +79,13 @@ class Visualization extends React.Component { case CHART: case REPORT_TABLE: { return ( - <> - {!this.state.pluginLoaded && ( - + - + style={pluginProps.style} + /> ) } case MAP: { @@ -131,8 +118,6 @@ class Visualization extends React.Component { } } -Visualization.contextType = UserSettingsCtx - Visualization.propTypes = { activeType: PropTypes.string, availableHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), diff --git a/src/components/Item/VisualizationItem/Visualization/__tests__/Visualization.spec.js b/src/components/Item/VisualizationItem/Visualization/__tests__/Visualization.spec.js index 1afad314c..9d986529e 100644 --- a/src/components/Item/VisualizationItem/Visualization/__tests__/Visualization.spec.js +++ b/src/components/Item/VisualizationItem/Visualization/__tests__/Visualization.spec.js @@ -2,16 +2,11 @@ import React from 'react' import { render } from '@testing-library/react' import { Provider } from 'react-redux' import configureMockStore from 'redux-mock-store' -import UserSettingsProvider from '../../../../UserSettingsProvider' import Visualization from '../Visualization' -jest.mock('@dhis2/app-runtime-adapter-d2', () => ({ - useD2: () => ({}), -})) - jest.mock( - '@dhis2/data-visualizer-plugin', + '../DataVisualizerPlugin', () => function MockVisualizationPlugin() { return
@@ -38,29 +33,19 @@ const DEFAULT_STORE_WITH_ONE_ITEM = { visualizations: { rainbowVis: { rows: [], columns: [], filters: [] } }, } -global.eventChartPlugin = {} -global.eventReportPlugin = {} -global.fetch = jest.fn(() => - Promise.resolve({ - userSettings: { keyAnalysisDisplayPropert: 'name' }, - }) -) - test('renders a MapPlugin when activeType is MAP', () => { const { container } = render( - - - + ) expect(container).toMatchSnapshot() @@ -69,18 +54,16 @@ test('renders a MapPlugin when activeType is MAP', () => { test('renders a VisualizationPlugin for CHART', () => { const { container } = render( - - - + ) expect(container).toMatchSnapshot() @@ -89,18 +72,16 @@ test('renders a VisualizationPlugin for CHART', () => { test('renders a VisualizationPlugin for REPORT_TABLE', () => { const { container } = render( - - - + ) expect(container).toMatchSnapshot() @@ -109,18 +90,16 @@ test('renders a VisualizationPlugin for REPORT_TABLE', () => { test('renders active type MAP rather than original type REPORT_TABLE', () => { const { container } = render( - - - + ) expect(container).toMatchSnapshot() @@ -129,18 +108,16 @@ test('renders active type MAP rather than original type REPORT_TABLE', () => { test('renders active type REPORT_TABLE rather than original type MAP', () => { const { container } = render( - - - + ) expect(container).toMatchSnapshot() @@ -149,18 +126,16 @@ test('renders active type REPORT_TABLE rather than original type MAP', () => { test('renders a DefaultPlugin when activeType is EVENT_CHART', () => { const { container } = render( - - - + ) expect(container).toMatchSnapshot() @@ -169,18 +144,16 @@ test('renders a DefaultPlugin when activeType is EVENT_CHART', () => { test('renders a DefaultPlugin when activeType is EVENT_REPORT', () => { const { container } = render( - - - + ) expect(container).toMatchSnapshot() @@ -192,14 +165,12 @@ test('renders NoVisMessage when no visualization', () => { } const { container } = render( - - - + ) expect(container).toMatchSnapshot() diff --git a/src/components/Item/VisualizationItem/Visualization/__tests__/__snapshots__/Visualization.spec.js.snap b/src/components/Item/VisualizationItem/Visualization/__tests__/__snapshots__/Visualization.spec.js.snap index a626504cf..07c9ea066 100644 --- a/src/components/Item/VisualizationItem/Visualization/__tests__/__snapshots__/Visualization.spec.js.snap +++ b/src/components/Item/VisualizationItem/Visualization/__tests__/__snapshots__/Visualization.spec.js.snap @@ -37,59 +37,13 @@ exports[`renders a MapPlugin when activeType is MAP 1`] = ` exports[`renders a VisualizationPlugin for CHART 1`] = `
-
- - - -
-
-
+ class="visualization-plugin" + />
`; exports[`renders a VisualizationPlugin for REPORT_TABLE 1`] = `
-
-
- - - -
-
@@ -106,30 +60,6 @@ exports[`renders active type MAP rather than original type REPORT_TABLE 1`] = ` exports[`renders active type REPORT_TABLE rather than original type MAP 1`] = `
-
-
- - - -
-
From 21bb3c3f452e12c2c1e0af3ba6c9246b874880dd Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 11 Mar 2021 10:54:56 +0100 Subject: [PATCH 5/6] fix: remove unused state --- .../Item/VisualizationItem/Visualization/Visualization.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/Item/VisualizationItem/Visualization/Visualization.js b/src/components/Item/VisualizationItem/Visualization/Visualization.js index 6546037d1..78184f7ec 100644 --- a/src/components/Item/VisualizationItem/Visualization/Visualization.js +++ b/src/components/Item/VisualizationItem/Visualization/Visualization.js @@ -22,10 +22,6 @@ import { sGetVisualization } from '../../../../reducers/visualizations' import { pluginIsAvailable } from './plugin' class Visualization extends React.Component { - state = { - pluginLoaded: false, - } - constructor(props) { super(props) @@ -35,10 +31,6 @@ class Visualization extends React.Component { this.memoizedGetVisualizationConfig = memoizeOne(getVisualizationConfig) } - onLoadingComplete = () => { - this.setState({ pluginLoaded: true }) - } - render() { const { visualization, From 401d3f021de73411bf207bdcdbad4ae11f02f542 Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Thu, 11 Mar 2021 10:59:49 +0100 Subject: [PATCH 6/6] fix: change var name to avoid confusion between plugin and vis loading --- .../VisualizationItem/Visualization/DataVisualizerPlugin.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js b/src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js index db7866c7f..32cdc853b 100644 --- a/src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js +++ b/src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js @@ -13,18 +13,18 @@ const VisualizationPlugin = React.lazy(() => const DataVisualizerPlugin = props => { const d2 = useD2() const { userSettings } = useUserSettings() - const [pluginLoaded, setPluginLoaded] = useState(false) + const [visualizationLoaded, setVisualizationLoaded] = useState(false) return ( }> - {!pluginLoaded && } + {!visualizationLoaded && } setPluginLoaded(true)} + onLoadingComplete={() => setVisualizationLoaded(true)} {...props} />