diff --git a/dashboard/src/components/panel/panel-render/client-panel-render.tsx b/dashboard/src/components/panel/panel-render/client-panel-render.tsx index a5539f613..67164d8c9 100644 --- a/dashboard/src/components/panel/panel-render/client-panel-render.tsx +++ b/dashboard/src/components/panel/panel-render/client-panel-render.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { useDashboardContext } from '~/contexts'; import { PanelRenderBase } from './panel-render-base'; import { PanelVizFeatures } from './panel-viz-features'; @@ -14,7 +15,7 @@ export interface IClientPanelRenderProps { */ export function ClientPanelRender(props: IClientPanelRenderProps) { const dashboardModel = useDashboardContext(); - const panel = dashboardModel.content.panels.findByID(props.panelId); + const panel = useMemo(() => dashboardModel.content.panels.findByID(props.panelId), [props.panelId]); if (!panel) { return null; } diff --git a/dashboard/src/components/panel/plugin-adaptor.tsx b/dashboard/src/components/panel/plugin-adaptor.tsx index a440553f4..23533e54c 100644 --- a/dashboard/src/components/panel/plugin-adaptor.tsx +++ b/dashboard/src/components/panel/plugin-adaptor.tsx @@ -1,9 +1,12 @@ import { Text } from '@mantine/core'; import { showNotification } from '@mantine/notifications'; import { useAsyncEffect } from 'ahooks'; +import { isEqual } from 'lodash'; import React, { useContext, useEffect, useState } from 'react'; import { MigrationResultType, MigrationStatus } from '~/components/plugins/instance-migrator'; import { useServiceLocator } from '~/components/plugins/service/service-locator/use-service-locator'; +import { LayoutStateContext } from '../..'; +import { AnyObject, IVizConfig } from '../../types'; import { IPanelInfo, tokens } from '../plugins'; import { IConfigComponentProps, @@ -11,8 +14,6 @@ import { VizConfigComponent, VizViewComponent, } from '../plugins/viz-manager/components'; -import { AnyObject, IVizConfig } from '../../types'; -import { LayoutStateContext } from '../..'; function usePluginMigration(onMigrate?: () => void) { const [migrated, setMigrated] = useState(false); @@ -46,8 +47,12 @@ type SetVizConfType = { setVizConf: (val: React.SetStateAction { + if (!isEqual(await instance.instanceData.getItem(null), panel.viz.conf)) { + await instance.instanceData.setItem(null, panel.viz.conf); + } + }, [instance, panel.viz.conf]); useEffect(() => { - instance.instanceData.setItem(null, panel.viz.conf); return instance.instanceData.watchItem(null, (configData) => { setVizConf(configData); }); diff --git a/dashboard/src/components/panel/use-config-viz-instance-service.ts b/dashboard/src/components/panel/use-config-viz-instance-service.ts index 413119995..340fd766f 100644 --- a/dashboard/src/components/panel/use-config-viz-instance-service.ts +++ b/dashboard/src/components/panel/use-config-viz-instance-service.ts @@ -8,6 +8,7 @@ import { NullInteractionManager } from '~/interactions/null-interaction-manager' export function useConfigVizInstanceService(panel: IPanelInfo, withInteraction = true) { const { panel: panelModel } = useRenderPanelContext(); + return useCallback( (services: IServiceLocator) => { const vizManager = services.getRequired(tokens.vizManager); diff --git a/dashboard/src/components/plugins/index.ts b/dashboard/src/components/plugins/index.ts index 0675945bd..15766c45e 100644 --- a/dashboard/src/components/plugins/index.ts +++ b/dashboard/src/components/plugins/index.ts @@ -3,3 +3,4 @@ export * from './plugin-context'; export * from './plugin-data-migrator'; export * from './hooks'; export * from './color-manager'; +export { onVizRendered, notifyVizRendered } from './viz-components/viz-instance-api'; diff --git a/dashboard/src/components/plugins/viz-components/bar-3d-chart/viz-bar-3d-chart.tsx b/dashboard/src/components/plugins/viz-components/bar-3d-chart/viz-bar-3d-chart.tsx index ac76e927f..fbc0bed8f 100644 --- a/dashboard/src/components/plugins/viz-components/bar-3d-chart/viz-bar-3d-chart.tsx +++ b/dashboard/src/components/plugins/viz-components/bar-3d-chart/viz-bar-3d-chart.tsx @@ -2,21 +2,15 @@ import ReactEChartsCore from 'echarts-for-react/lib/core'; import 'echarts-gl'; import * as echarts from 'echarts/core'; import { defaults, get, maxBy, minBy } from 'lodash'; -import { useMemo } from 'react'; +import { useMemo, useRef } from 'react'; import { useStorageData } from '~/components/plugins/hooks'; import { DefaultVizBox, getBoxContentStyle } from '~/styles/viz-box'; import { VizViewProps } from '~/types/plugin'; import { extractFullQueryData, parseDataKey } from '~/utils'; import { DEFAULT_CONFIG, IBar3dChartConf } from './type'; +import { notifyVizRendered } from '~/components/plugins/viz-components/viz-instance-api'; -const paddings = { - top: 16, - right: 16, - bottom: 16, - left: 16, -}; - -export function VizBar3dChart({ context }: VizViewProps) { +export function VizBar3dChart({ context, instance }: VizViewProps) { const { value: conf } = useStorageData(context.instanceData, 'config'); const data = context.data; const { width, height } = context.viewport; @@ -44,6 +38,17 @@ export function VizBar3dChart({ context }: VizViewProps) { }; }, [queryData, z]); + const echartsInstanceRef = useRef(null); + const handleEvents = useMemo( + () => ({ + finished: () => { + const chart = echartsInstanceRef.current?.getEchartsInstance(); + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, + }), + [], + ); const option = { tooltip: {}, backgroundColor: '#fff', @@ -103,9 +108,11 @@ export function VizBar3dChart({ context }: VizViewProps) { diff --git a/dashboard/src/components/plugins/viz-components/boxplot-chart/viz-boxplot-chart.tsx b/dashboard/src/components/plugins/viz-components/boxplot-chart/viz-boxplot-chart.tsx index b53eb7e1c..47fbab2cc 100644 --- a/dashboard/src/components/plugins/viz-components/boxplot-chart/viz-boxplot-chart.tsx +++ b/dashboard/src/components/plugins/viz-components/boxplot-chart/viz-boxplot-chart.tsx @@ -2,7 +2,7 @@ import ReactEChartsCore from 'echarts-for-react/lib/core'; import 'echarts-gl'; import * as echarts from 'echarts/core'; import _, { defaults } from 'lodash'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useRef } from 'react'; import { useStorageData } from '~/components/plugins/hooks'; import { useRowDataMap } from '~/components/plugins/hooks/use-row-data-map'; import { useCurrentInteractionManager, useTriggerSnapshotList } from '~/interactions'; @@ -12,6 +12,7 @@ import { getOption } from './option'; import { ClickBoxplotSeries } from './triggers'; import { DEFAULT_CONFIG, IBoxplotChartConf, IBoxplotDataItem } from './type'; import { useTranslation } from 'react-i18next'; +import { notifyVizRendered } from '~/components/plugins/viz-components/viz-instance-api'; interface IClickBoxplotSeries { type: 'click'; @@ -50,11 +51,19 @@ export function VizBoxplotChart({ context, instance }: VizViewProps) { [rowDataMap, triggers, interactionManager], ); + const echartsInstanceRef = useRef(null); + const handleFinished = useCallback(() => { + const chart = echartsInstanceRef.current?.getEchartsInstance(); + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, []); + const onEvents = useMemo(() => { return { click: handleSeriesClick, + finished: handleFinished, }; - }, [handleSeriesClick]); + }, [handleSeriesClick, handleFinished]); const option = useMemo(() => getOption({ config, data, variables, t }), [config, data, variables, t]); @@ -66,6 +75,7 @@ export function VizBoxplotChart({ context, instance }: VizViewProps) { (null); + const handleFinished = useCallback(() => { + const chart = echartsInstanceRef.current?.getEchartsInstance(); + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, [instance]); const onEvents = useMemo(() => { return { click: handleHeatBlockClick, legendselectchanged: handleLegendSelected, + finished: handleFinished, }; - }, [handleHeatBlockClick]); + }, [handleHeatBlockClick, handleFinished]); const option = React.useMemo(() => { return getOption(conf, data, variables); @@ -81,6 +91,7 @@ function Chart({ { return getOption(conf, data); }, [conf, data]); - return ; + const echartsInstanceRef = React.useRef(null); + const handleFinished = React.useCallback(() => { + const chart = echartsInstanceRef.current?.getEchartsInstance(); + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, [instance]); + const onEvents = useMemo( + () => ({ + finished: handleFinished, + }), + [handleFinished], + ); + + return ( + + ); } -export function VizFunnelChart({ context }: VizViewProps) { +export function VizFunnelChart({ context, instance }: VizViewProps) { const { value: confValue } = useStorageData(context.instanceData, 'config'); const conf = useMemo(() => defaults({}, confValue, DEFAULT_CONFIG), [confValue]); const data = context.data; @@ -28,7 +64,13 @@ export function VizFunnelChart({ context }: VizViewProps) { return ( - + ); } diff --git a/dashboard/src/components/plugins/viz-components/heatmap/viz-heatmap.tsx b/dashboard/src/components/plugins/viz-components/heatmap/viz-heatmap.tsx index e4d4fbb22..abc8ebc63 100644 --- a/dashboard/src/components/plugins/viz-components/heatmap/viz-heatmap.tsx +++ b/dashboard/src/components/plugins/viz-components/heatmap/viz-heatmap.tsx @@ -6,7 +6,7 @@ import { useStorageData } from '~/components/plugins/hooks'; import { useCurrentInteractionManager, useTriggerSnapshotList } from '~/interactions'; import { DefaultVizBox, getBoxContentHeight, getBoxContentWidth } from '~/styles/viz-box'; import { AnyObject } from '~/types'; -import { IVizInteractionManager, VizViewProps } from '~/types/plugin'; +import { IVizInteractionManager, VizInstance, VizViewProps } from '~/types/plugin'; import { ITemplateVariable, parseDataKey } from '~/utils'; import { HeatmapPagination } from './heatmap-pagination'; import { getOption } from './option'; @@ -14,6 +14,7 @@ import { useHeatmapGroupedData } from './render/use-heatmap-grouped-data'; import { SeriesDataItem, useHeatmapSeriesData } from './render/use-heatmap-series-data'; import { ClickHeatBlock } from './triggers'; import { DEFAULT_CONFIG, IHeatmapConf } from './type'; +import { notifyVizRendered } from '~/components/plugins/viz-components/viz-instance-api'; interface IClickHeatBlock { type: 'click'; @@ -34,6 +35,7 @@ function Chart({ height, interactionManager, variables, + instance, }: { conf: IHeatmapConf; data: TPanelData; @@ -42,6 +44,7 @@ function Chart({ height: number; interactionManager: IVizInteractionManager; variables: ITemplateVariable[]; + instance: VizInstance; }) { const rowDataMap = useMemo(() => { const x = parseDataKey(conf.x_axis.data_key); @@ -61,12 +64,19 @@ function Chart({ }, [rowDataMap, triggers, interactionManager], ); + const echartsInstanceRef = React.useRef(null); + const handleFinished = useCallback(() => { + const chart = echartsInstanceRef.current?.getEchartsInstance(); + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, [instance]); const onEvents = useMemo(() => { return { click: handleHeatBlockClick, + finished: handleFinished, }; - }, [handleHeatBlockClick]); + }, [handleHeatBlockClick, handleFinished]); const option = React.useMemo(() => { return getOption(conf, data, seriesData, variables, width, height); @@ -77,6 +87,7 @@ function Chart({ echarts={echarts} option={option} style={{ width, height }} + ref={echartsInstanceRef} onEvents={onEvents} notMerge theme="merico-light" @@ -109,6 +120,7 @@ export function VizHeatmap({ context, instance }: VizViewProps) { )} (null); + const handleFinished = React.useCallback(() => { + const chart = echartsInstanceRef.current?.getEchartsInstance(); + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, [instance]); const onEvents = useMemo(() => { return { click: handleSeriesClick, + finished: handleFinished, }; }, [handleSeriesClick]); @@ -68,6 +78,7 @@ function Chart({ { return getOption(conf, metricKey, data); }, [conf, data, metricKey]); - return ; + const echartsInstanceRef = React.useRef(null); + const handleFinished = React.useCallback(() => { + const chart = echartsInstanceRef.current?.getEchartsInstance(); + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, [instance]); + const onEvents = useMemo(() => { + return { + finished: handleFinished, + }; + }, [handleFinished]); + return ( + + ); } -export function VizMericoEstimationChart({ context }: VizViewProps) { +export function VizMericoEstimationChart({ context, instance }: VizViewProps) { const { value: confValue } = useStorageData(context.instanceData, 'config'); const conf = useMemo(() => defaults({}, confValue, DEFAULT_CONFIG), [confValue]); const data = context.data; @@ -59,7 +83,14 @@ export function VizMericoEstimationChart({ context }: VizViewProps) { sx={{ overflow: 'hidden', height, width }} > - + ); } diff --git a/dashboard/src/components/plugins/viz-components/merico-heatmap/render/index.tsx b/dashboard/src/components/plugins/viz-components/merico-heatmap/render/index.tsx index 36e2fc457..c644f8bda 100644 --- a/dashboard/src/components/plugins/viz-components/merico-heatmap/render/index.tsx +++ b/dashboard/src/components/plugins/viz-components/merico-heatmap/render/index.tsx @@ -6,8 +6,9 @@ import { useStorageData } from '~/components/plugins/hooks'; import { useCurrentInteractionManager, useTriggerSnapshotList } from '~/interactions'; import { DefaultVizBox, getBoxContentHeight, getBoxContentWidth } from '~/styles/viz-box'; import { AnyObject } from '~/types'; -import { IVizInteractionManager, VizViewProps } from '~/types/plugin'; +import { IVizInteractionManager, VizInstance, VizViewProps } from '~/types/plugin'; import { ITemplateVariable, parseDataKey } from '~/utils'; +import { notifyVizRendered } from '../../viz-instance-api'; import { ClickHeatBlock } from '../triggers'; import { DEFAULT_CONFIG, TMericoHeatmapConf } from '../type'; import { getOption } from './option'; @@ -30,6 +31,7 @@ function Chart({ height, interactionManager, variables, + instance, }: { conf: TMericoHeatmapConf; data: TPanelData; @@ -37,6 +39,7 @@ function Chart({ height: number; interactionManager: IVizInteractionManager; variables: ITemplateVariable[]; + instance: VizInstance; }) { const rowDataMap = useMemo(() => { const x = parseDataKey(conf.x_axis.data_key); @@ -57,11 +60,18 @@ function Chart({ [rowDataMap, triggers, interactionManager], ); + const echartsInstanceRef = React.useRef(null); + const handleFinished = useCallback(() => { + const chart = echartsInstanceRef.current?.getEchartsInstance(); + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, [instance]); const onEvents = useMemo(() => { return { click: handleHeatBlockClick, + finished: handleFinished, }; - }, [handleHeatBlockClick]); + }, [handleHeatBlockClick, handleFinished]); const option = React.useMemo(() => { return getOption(conf, data, variables); @@ -71,6 +81,7 @@ function Chart({ (null); + const handleFinished = useCallback(() => { + const chart = echartsInstanceRef.current?.getEchartsInstance(); + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, [instance]); const onEvents = useMemo(() => { return { click: handleSeriesClick, + finished: handleFinished, }; - }, [handleSeriesClick]); + }, [handleSeriesClick, handleFinished]); if (!conf || !width || !height) { return null; @@ -63,6 +71,7 @@ export function VizParetoChart({ context, instance }: VizViewProps) { (null); + const handleFinished = React.useCallback(() => { + const chart = echartsInstanceRef.current?.getEchartsInstance(); + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, [instance]); + const option = React.useMemo(() => { return getOption(conf, data, width); }, [conf, data, width]); @@ -54,8 +65,9 @@ function Chart({ const onEvents = useMemo(() => { return { click: handleSeriesClick, + finished: handleFinished, }; - }, [handleSeriesClick]); + }, [handleSeriesClick, handleFinished]); if (!width || !height || !option.series.name) { return null; @@ -64,6 +76,7 @@ function Chart({ ); } + export function VizPieChart({ context, instance }: VizViewProps) { const interactionManager = useCurrentInteractionManager({ vizManager: context.vizManager, @@ -87,6 +101,7 @@ export function VizPieChart({ context, instance }: VizViewProps) { return ( { const { queryID, columnKey } = parseDataKey(conf.series_name_key); @@ -50,6 +53,13 @@ function Chart({ const triggers = useTriggerSnapshotList(interactionManager.triggerManager, ClickRadarChartSeries.id); + const echartsInstanceRef = React.useRef(null); + const handleFinished = React.useCallback(() => { + const chart = echartsInstanceRef.current?.getEchartsInstance(); + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, [instance]); + const handleRadarSeriesClick = useCallback( (params: IClickRadarSeries) => { const rowData = _.get(rowDataMap, params.name, { error: 'rowData is not found' }); @@ -63,6 +73,7 @@ function Chart({ const onEvents = useMemo(() => { return { click: handleRadarSeriesClick, + finished: handleFinished, }; }, [handleRadarSeriesClick]); @@ -77,6 +88,7 @@ function Chart({ (context.instanceData, 'config'); const { width, height } = context.viewport; @@ -45,6 +46,17 @@ export function VizRegressionChart({ context }: VizViewProps) { const onChartReady = (echartsInstance: EChartsInstance) => { echartsRef.current = echartsInstance; }; + const handleFinished = useCallback(() => { + const chart = echartsRef.current; + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, [instance]); + const onEvents = useMemo( + () => ({ + finished: handleFinished, + }), + [handleFinished], + ); if (!width || !height || !conf) { return null; @@ -56,6 +68,7 @@ export function VizRegressionChart({ context }: VizViewProps) { echarts={echarts} onChartReady={onChartReady} option={option} + onEvents={onEvents} style={getBoxContentStyle(width, height)} notMerge theme="merico-light" diff --git a/dashboard/src/components/plugins/viz-components/scatter-chart/viz-scatter-chart.tsx b/dashboard/src/components/plugins/viz-components/scatter-chart/viz-scatter-chart.tsx index e6b2a1120..df20ebddb 100644 --- a/dashboard/src/components/plugins/viz-components/scatter-chart/viz-scatter-chart.tsx +++ b/dashboard/src/components/plugins/viz-components/scatter-chart/viz-scatter-chart.tsx @@ -7,9 +7,10 @@ import { useRowDataMap } from '~/components/plugins/hooks/use-row-data-map'; import { useCurrentInteractionManager, useTriggerSnapshotList } from '~/interactions'; import { DefaultVizBox, getBoxContentHeight, getBoxContentWidth } from '~/styles/viz-box'; import { AnyObject } from '~/types'; -import { IVizInteractionManager, VizViewProps } from '~/types/plugin'; +import { IVizInteractionManager, VizInstance, VizViewProps } from '~/types/plugin'; import { ITemplateVariable } from '~/utils'; import { StatsAroundViz } from '../../common-echarts-fields/stats-around-viz'; +import { notifyVizRendered } from '../viz-instance-api'; import { getOption } from './option'; import { ClickScatterChartSeries } from './triggers'; import { DEFAULT_CONFIG, IScatterChartConf } from './type'; @@ -32,6 +33,7 @@ function Chart({ height, interactionManager, variables, + instance, }: { conf: IScatterChartConf; data: TPanelData; @@ -39,6 +41,7 @@ function Chart({ height: number; interactionManager: IVizInteractionManager; variables: ITemplateVariable[]; + instance: VizInstance; }) { const rowDataMap = useRowDataMap(data, conf.x_axis.data_key); @@ -57,11 +60,19 @@ function Chart({ [rowDataMap, triggers, interactionManager], ); + const echartsInstanceRef = React.useRef(null); + const handleFinished = React.useCallback(() => { + const chart = echartsInstanceRef.current?.getEchartsInstance(); + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, [instance]); + const onEvents = useMemo(() => { return { click: handleSeriesClick, + finished: handleFinished, }; - }, [handleSeriesClick]); + }, [handleSeriesClick, handleFinished]); const option = React.useMemo(() => { return getOption(conf, data, variables); @@ -71,6 +82,7 @@ function Chart({ (context.instanceData, 'config'); const conf = useMemo(() => defaults({}, confValue, DEFAULT_CONFIG), [confValue]); @@ -17,6 +18,19 @@ export function VizSunburst({ context }: VizViewProps) { const data = context.data; const { width, height } = context.viewport; + const echartsInstanceRef = React.useRef(null); + const handleFinished = React.useCallback(() => { + const chart = echartsInstanceRef.current?.getEchartsInstance(); + if (!chart) return; + notifyVizRendered(instance, chart.getOption()); + }, [instance]); + const onEvents = useMemo( + () => ({ + finished: handleFinished, + }), + [handleFinished], + ); + const option = useMemo(() => getOption(conf, data, variables), [conf, data, variables]); if (!width || !height) { @@ -27,6 +41,8 @@ export function VizSunburst({ context }: VizViewProps) { void) { + instance.messageChannels.getChannel('viz').on('rendered', callback); + return () => { + instance.messageChannels.getChannel('viz').off('rendered', callback); + }; +} diff --git a/dashboard/src/index.ts b/dashboard/src/index.ts index c295fc13f..236dc3b38 100644 --- a/dashboard/src/index.ts +++ b/dashboard/src/index.ts @@ -29,5 +29,5 @@ export interface IDashboardConfig { searchButtonProps: ButtonProps; } -export { pluginManager } from './components/plugins'; +export { pluginManager, onVizRendered, notifyVizRendered } from './components/plugins'; export { type IPanelAddon, type IPanelAddonRenderProps } from './types/plugin'; diff --git a/dashboard/src/model/meta-model/dashboard/content/panel/viz.ts b/dashboard/src/model/meta-model/dashboard/content/panel/viz.ts index 650717885..b78f74181 100644 --- a/dashboard/src/model/meta-model/dashboard/content/panel/viz.ts +++ b/dashboard/src/model/meta-model/dashboard/content/panel/viz.ts @@ -1,3 +1,4 @@ +import { isEqual } from 'lodash'; import { types } from 'mobx-state-tree'; import { AnyObject } from '~/types'; @@ -19,6 +20,7 @@ export const PanelVizMeta = types self.type = type; }, setConf(conf: AnyObject) { + if (isEqual(self.conf, conf)) return; self.conf = conf; }, }));