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..32cdc853b --- /dev/null +++ b/src/components/Item/VisualizationItem/Visualization/DataVisualizerPlugin.js @@ -0,0 +1,38 @@ +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( + /* webpackChunkName: "data-visualizer-plugin" */ /* webpackPrefetch: true */ '@dhis2/data-visualizer-plugin' + ) +) + +const DataVisualizerPlugin = props => { + const d2 = useD2() + const { userSettings } = useUserSettings() + const [visualizationLoaded, setVisualizationLoaded] = useState(false) + + return ( + }> + {!visualizationLoaded && } + setVisualizationLoaded(true)} + {...props} + /> + + ) +} + +DataVisualizerPlugin.propTypes = { + style: PropTypes.object, +} + +export default DataVisualizerPlugin 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 38452eccc..78184f7ec 100644 --- a/src/components/Item/VisualizationItem/Visualization/Visualization.js +++ b/src/components/Item/VisualizationItem/Visualization/Visualization.js @@ -1,13 +1,11 @@ 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' -import LoadingMask from './LoadingMask' +import DataVisualizerPlugin from './DataVisualizerPlugin' import NoVisualizationMessage from './NoVisualizationMessage' import getFilteredVisualization from './getFilteredVisualization' @@ -21,14 +19,9 @@ import { import { getVisualizationId } from '../../../../modules/item' import memoizeOne from '../../../../modules/memoizeOne' import { sGetVisualization } from '../../../../reducers/visualizations' -import { UserSettingsCtx } from '../../../UserSettingsProvider' import { pluginIsAvailable } from './plugin' class Visualization extends React.Component { - state = { - pluginLoaded: false, - } - constructor(props) { super(props) @@ -38,10 +31,6 @@ class Visualization extends React.Component { this.memoizedGetVisualizationConfig = memoizeOne(getVisualizationConfig) } - onLoadingComplete = () => { - this.setState({ pluginLoaded: true }) - } - render() { const { visualization, @@ -82,32 +71,13 @@ class Visualization extends React.Component { case CHART: case REPORT_TABLE: { return ( - <> - {!this.state.pluginLoaded && ( -
- -
+ - {({ d2 }) => ( - - )} - - + style={pluginProps.style} + /> ) } case MAP: { @@ -140,8 +110,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 ec2e6c9df..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', () => ({ - D2Shim: ({ children }) => children({ d2: {} }), -})) - 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 6cd520d30..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 @@ -36,33 +36,6 @@ exports[`renders a MapPlugin when activeType is MAP 1`] = ` exports[`renders a VisualizationPlugin for CHART 1`] = `
-
-
-
- - - -
-
-
@@ -71,33 +44,6 @@ exports[`renders a VisualizationPlugin for CHART 1`] = ` exports[`renders a VisualizationPlugin for REPORT_TABLE 1`] = `
-
-
-
- - - -
-
-
@@ -114,33 +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`] = `
-
-
-
- - - -
-
-
diff --git a/src/components/Item/VisualizationItem/Visualization/plugin.js b/src/components/Item/VisualizationItem/Visualization/plugin.js index fe19c24c2..07e677e4c 100644 --- a/src/components/Item/VisualizationItem/Visualization/plugin.js +++ b/src/components/Item/VisualizationItem/Visualization/plugin.js @@ -8,27 +8,71 @@ 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 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) { + 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.js') + scripts.push('./vendor/jquery-3.3.1.js') + scripts.push('./vendor/jquery-migrate-3.0.1.js') + } + } + + scripts.push(baseUrl + itemTypeToScriptPath[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 => + 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 +104,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 = (id, type, isFullscreen = false) => { 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) + }) +}