From 9ab3d6998ff9d11b7af9143eec4a1b24f6719fb9 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 24 Jul 2024 09:14:02 +0200 Subject: [PATCH 01/22] refactor: Set the ground for dashboards data filters --- app/config-types.ts | 10 +++++- .../components/add-dataset-dialog.mock.ts | 4 +++ .../components/layout-configurator.tsx | 14 +++++++-- .../configurator-state/initial.tsx | 4 +++ app/configurator/configurator-state/mocks.ts | 8 +++++ app/docs/charts.stories.tsx | 8 +++++ app/docs/fixtures.ts | 4 +++ app/docs/lines.stories.tsx | 4 +++ app/stores/interactive-filters.tsx | 10 +++++- app/utils/chart-config/versioning.ts | 31 ++++++++++++++++++- 10 files changed, 91 insertions(+), 6 deletions(-) diff --git a/app/config-types.ts b/app/config-types.ts index e71b84055..2f87f93ea 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -1150,13 +1150,21 @@ const DashboardTimeRangeFilter = t.type({ to: t.string, }), }); - export type DashboardTimeRangeFilter = t.TypeOf< typeof DashboardTimeRangeFilter >; +const DashboardDataFiltersConfig = t.type({ + active: t.boolean, + componentIris: t.array(t.string), +}); +export type DashboardDataFiltersConfig = t.TypeOf< + typeof DashboardDataFiltersConfig +>; + const DashboardFiltersConfig = t.type({ timeRange: DashboardTimeRangeFilter, + dataFilters: DashboardDataFiltersConfig, }); export type DashboardFiltersConfig = t.TypeOf; diff --git a/app/configurator/components/add-dataset-dialog.mock.ts b/app/configurator/components/add-dataset-dialog.mock.ts index c36b35ed4..adc8ced89 100644 --- a/app/configurator/components/add-dataset-dialog.mock.ts +++ b/app/configurator/components/add-dataset-dialog.mock.ts @@ -109,5 +109,9 @@ export const photovoltaikChartStateMock: ConfiguratorStateConfiguringChart = { to: "", }, }, + dataFilters: { + active: false, + componentIris: [], + }, }, }; diff --git a/app/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index 2124f383e..c5eddfe3c 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -116,8 +116,12 @@ const LayoutLayoutConfigurator = () => { const LayoutSharedFiltersConfigurator = () => { const [state, dispatch] = useConfiguratorState(isLayouting); const { layout } = state; - const { timeRange, potentialTimeRangeFilterIris } = - useDashboardInteractiveFilters(); + const { + timeRange, + potentialTimeRangeFilterIris, + dataFilters, + potentialDataFilterIris, + } = useDashboardInteractiveFilters(); const locale = useLocale(); const [{ data }] = useConfigsCubeComponents({ @@ -224,7 +228,11 @@ const LayoutSharedFiltersConfigurator = () => { switch (layout.type) { case "tab": case "dashboard": - if (!timeRange || potentialTimeRangeFilterIris.length === 0) { + if ( + !timeRange || + (potentialTimeRangeFilterIris.length === 0 && + (!dataFilters || potentialDataFilterIris.length === 0)) + ) { return null; } diff --git a/app/configurator/configurator-state/initial.tsx b/app/configurator/configurator-state/initial.tsx index a2a060f0d..ebc591514 100644 --- a/app/configurator/configurator-state/initial.tsx +++ b/app/configurator/configurator-state/initial.tsx @@ -46,6 +46,10 @@ export const getInitialConfiguringConfigBasedOnCube = (props: { to: "", }, }, + dataFilters: { + active: false, + componentIris: [], + }, }, }; }; diff --git a/app/configurator/configurator-state/mocks.ts b/app/configurator/configurator-state/mocks.ts index c3d53a91b..024a3fb10 100644 --- a/app/configurator/configurator-state/mocks.ts +++ b/app/configurator/configurator-state/mocks.ts @@ -70,6 +70,10 @@ export const configStateMock = { to: "", }, }, + dataFilters: { + active: false, + componentIris: [], + }, }, }, groupedColumnChart: { @@ -213,6 +217,10 @@ export const configStateMock = { to: "", }, }, + dataFilters: { + active: false, + componentIris: [], + }, }, }, } satisfies Record; diff --git a/app/docs/charts.stories.tsx b/app/docs/charts.stories.tsx index 87119057f..38c5215d1 100644 --- a/app/docs/charts.stories.tsx +++ b/app/docs/charts.stories.tsx @@ -77,6 +77,10 @@ const ColumnsStory = { to: "", }, }, + dataFilters: { + active: false, + componentIris: [], + }, }, }} > @@ -136,6 +140,10 @@ const ScatterplotStory = { to: "", }, }, + dataFilters: { + active: false, + componentIris: [], + }, }, }} > diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts index 73b8f5b13..e949c3352 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -97,6 +97,10 @@ export const states: ConfiguratorState[] = [ to: "", }, }, + dataFilters: { + active: false, + componentIris: [], + }, }, }, ]; diff --git a/app/docs/lines.stories.tsx b/app/docs/lines.stories.tsx index 3c5380ccb..5df697309 100644 --- a/app/docs/lines.stories.tsx +++ b/app/docs/lines.stories.tsx @@ -55,6 +55,10 @@ const LineChartStory = () => ( to: "", }, }, + dataFilters: { + active: false, + componentIris: [], + }, }, }} > diff --git a/app/stores/interactive-filters.tsx b/app/stores/interactive-filters.tsx index 3659bcc46..6d4554809 100644 --- a/app/stores/interactive-filters.tsx +++ b/app/stores/interactive-filters.tsx @@ -5,6 +5,7 @@ import { getChartSpec } from "@/charts/chart-config-ui-options"; import { CalculationType, ChartConfig, + DashboardDataFiltersConfig, DashboardTimeRangeFilter, FilterValueSingle, hasChartConfigs, @@ -146,6 +147,8 @@ const InteractiveFiltersContext = createContext< | { potentialTimeRangeFilterIris: string[]; timeRange: DashboardTimeRangeFilter | undefined; + potentialDataFilterIris: string[]; + dataFilters: DashboardDataFiltersConfig | undefined; stores: Record; } | undefined @@ -222,9 +225,14 @@ export const InteractiveFiltersProvider = ({ const ctxValue = useMemo( () => ({ - stores, potentialTimeRangeFilterIris, timeRange, + potentialDataFilterIris: [], + dataFilters: { + active: false, + componentIris: [], + }, + stores, }), [stores, potentialTimeRangeFilterIris, timeRange] ); diff --git a/app/utils/chart-config/versioning.ts b/app/utils/chart-config/versioning.ts index 9aa78f40a..732a260cf 100644 --- a/app/utils/chart-config/versioning.ts +++ b/app/utils/chart-config/versioning.ts @@ -931,7 +931,7 @@ export const migrateChartConfig = makeMigrate( } ); -export const CONFIGURATOR_STATE_VERSION = "3.5.0"; +export const CONFIGURATOR_STATE_VERSION = "3.6.0"; export const configuratorStateMigrations: Migration[] = [ { @@ -1284,6 +1284,35 @@ export const configuratorStateMigrations: Migration[] = [ return newConfig; }, }, + { + description: "ALL (modify dashboardFilters)", + from: "3.5.0", + to: "3.6.0", + up: (config) => { + const newConfig = { + ...config, + version: "3.6.0", + dashboardFilters: { + ...config.dashboardFilters, + dataFilters: { + active: false, + componentIris: [], + }, + }, + }; + return newConfig; + }, + down: (config) => { + const newConfig = { + ...config, + version: "3.5.0", + dashboardFilters: { + timeRange: config.dashboardFilters.timeRange, + }, + }; + return newConfig; + }, + }, ]; export const migrateConfiguratorState = makeMigrate( From e246b8aadac920757f460cb72ff7da412f599dee Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 24 Jul 2024 10:51:49 +0200 Subject: [PATCH 02/22] feat: Display dashboard data filters in the options panel --- app/config-types.ts | 1 - .../components/add-dataset-dialog.mock.ts | 1 - .../components/layout-configurator.tsx | 151 ++++++++++++------ .../configurator-state/actions.tsx | 12 ++ .../configurator-state/initial.tsx | 1 - app/configurator/configurator-state/mocks.ts | 2 - .../configurator-state/reducer.tsx | 34 ++++ app/docs/charts.stories.tsx | 2 - app/docs/fixtures.ts | 1 - app/docs/lines.stories.tsx | 1 - app/stores/interactive-filters.tsx | 34 +++- app/utils/chart-config/versioning.ts | 1 - 12 files changed, 172 insertions(+), 69 deletions(-) diff --git a/app/config-types.ts b/app/config-types.ts index 2f87f93ea..b0c6828d7 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -1155,7 +1155,6 @@ export type DashboardTimeRangeFilter = t.TypeOf< >; const DashboardDataFiltersConfig = t.type({ - active: t.boolean, componentIris: t.array(t.string), }); export type DashboardDataFiltersConfig = t.TypeOf< diff --git a/app/configurator/components/add-dataset-dialog.mock.ts b/app/configurator/components/add-dataset-dialog.mock.ts index adc8ced89..9bd54d840 100644 --- a/app/configurator/components/add-dataset-dialog.mock.ts +++ b/app/configurator/components/add-dataset-dialog.mock.ts @@ -110,7 +110,6 @@ export const photovoltaikChartStateMock: ConfiguratorStateConfiguringChart = { }, }, dataFilters: { - active: false, componentIris: [], }, }, diff --git a/app/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index c5eddfe3c..bfa04c947 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -1,7 +1,6 @@ import { t, Trans } from "@lingui/macro"; import { Box, - FormControlLabel, Stack, Switch, SwitchProps, @@ -189,7 +188,7 @@ const LayoutSharedFiltersConfigurator = () => { potentialTimeRangeFilterIris, ]); - const handleToggle: SwitchProps["onChange"] = useEventCallback( + const handleTimeRangeFilterToggle: SwitchProps["onChange"] = useEventCallback( (_, checked) => { if (checked) { const options = getTimeFilterOptions({ @@ -225,13 +224,32 @@ const LayoutSharedFiltersConfigurator = () => { } ); + const handleDataFiltersToggle = useEventCallback( + (checked: boolean, componentIri: string) => { + if (checked) { + dispatch({ + type: "DASHBOARD_DATA_FILTER_ADD", + value: { + dimensionIri: componentIri, + }, + }); + } else { + dispatch({ + type: "DASHBOARD_DATA_FILTER_REMOVE", + value: { + dimensionIri: componentIri, + }, + }); + } + } + ); + switch (layout.type) { case "tab": case "dashboard": if ( - !timeRange || - (potentialTimeRangeFilterIris.length === 0 && - (!dataFilters || potentialDataFilterIris.length === 0)) + (!timeRange || potentialTimeRangeFilterIris.length === 0) && + (!dataFilters || potentialDataFilterIris.length === 0) ) { return null; } @@ -251,39 +269,66 @@ const LayoutSharedFiltersConfigurator = () => { - - - {combinedDimension.label} - - + - - Shared - + {combinedDimension.label} - } - control={ - } - /> - - {timeRange.active ? ( - + + {timeRange.active ? ( + + ) : null} + + ) : null} + {dataFilters ? ( + <> + {potentialDataFilterIris.map((componentIri) => { + const dimension = data?.dataCubesComponents.dimensions.find( + (dimension) => dimension.iri === componentIri + ); + if (!dimension) { + return null; + } + return ( + + + {dimension.label} + + { + handleDataFiltersToggle(checked, dimension.iri); + }} + /> + + ); + })} + ) : null} @@ -296,35 +341,35 @@ const LayoutSharedFiltersConfigurator = () => { const DashboardFiltersOptions = ({ timeRangeFilter, - dimension, + timeRangeCombinedDimension, }: { timeRangeFilter: DashboardTimeRangeFilter | undefined; - dimension: Dimension; + timeRangeCombinedDimension: Dimension; }) => { if (!timeRangeFilter) { return null; } if ( - !canDimensionBeTimeFiltered(dimension) || - !canRenderDatePickerField(dimension.timeUnit) + !canDimensionBeTimeFiltered(timeRangeCombinedDimension) || + !canRenderDatePickerField(timeRangeCombinedDimension.timeUnit) ) { return null; } return ( ); }; const DashboardTimeRangeFilterOptions = ({ - timeRangeFilter, + filter, dimension, }: { - timeRangeFilter: DashboardTimeRangeFilter; + filter: DashboardTimeRangeFilter; dimension: TemporalDimension | TemporalEntityDimension; }) => { const { timeUnit, timeFormat } = dimension; @@ -383,9 +428,9 @@ const DashboardTimeRangeFilterOptions = ({ dispatch({ type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", value: { - ...timeRangeFilter, + ...filter, presets: { - ...timeRangeFilter.presets, + ...filter.presets, from: formatDate(newDate), }, }, @@ -399,9 +444,9 @@ const DashboardTimeRangeFilterOptions = ({ dispatch({ type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", value: { - ...timeRangeFilter, + ...filter, presets: { - ...timeRangeFilter.presets, + ...filter.presets, from: ev.target.value as string, }, }, @@ -420,9 +465,9 @@ const DashboardTimeRangeFilterOptions = ({ dispatch({ type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", value: { - ...timeRangeFilter, + ...filter, presets: { - ...timeRangeFilter.presets, + ...filter.presets, to: formatDate(newDate), }, }, @@ -436,9 +481,9 @@ const DashboardTimeRangeFilterOptions = ({ dispatch({ type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", value: { - ...timeRangeFilter, + ...filter, presets: { - ...timeRangeFilter.presets, + ...filter.presets, to: ev.target.value as string, }, }, @@ -464,7 +509,7 @@ const DashboardTimeRangeFilterOptions = ({ {canRenderDatePickerField(timeUnit) ? ( !optionValues.includes(formatDate(date))} timeUnit={timeUnit} @@ -478,14 +523,14 @@ const DashboardTimeRangeFilterOptions = ({ id="dashboard-time-range-filter-from" label={t({ id: "controls.filters.select.from", message: "From" })} options={options} - value={timeRangeFilter.presets.from} + value={filter.presets.from} onChange={handleChangeFromGeneric} /> )} {canRenderDatePickerField(timeUnit) ? ( !optionValues.includes(formatDate(date))} timeUnit={timeUnit} @@ -499,7 +544,7 @@ const DashboardTimeRangeFilterOptions = ({ id="dashboard-time-range-filter-to" label={t({ id: "controls.filters.select.to", message: "To" })} options={options} - value={timeRangeFilter.presets.to} + value={filter.presets.to} onChange={handleChangeToGeneric} /> )} diff --git a/app/configurator/configurator-state/actions.tsx b/app/configurator/configurator-state/actions.tsx index 61fec710e..d08d4a5eb 100644 --- a/app/configurator/configurator-state/actions.tsx +++ b/app/configurator/configurator-state/actions.tsx @@ -275,4 +275,16 @@ export type ConfiguratorStateAction = } | { type: "DASHBOARD_TIME_RANGE_FILTER_REMOVE"; + } + | { + type: "DASHBOARD_DATA_FILTER_ADD"; + value: { + dimensionIri: string; + }; + } + | { + type: "DASHBOARD_DATA_FILTER_REMOVE"; + value: { + dimensionIri: string; + }; }; diff --git a/app/configurator/configurator-state/initial.tsx b/app/configurator/configurator-state/initial.tsx index ebc591514..c780e641e 100644 --- a/app/configurator/configurator-state/initial.tsx +++ b/app/configurator/configurator-state/initial.tsx @@ -47,7 +47,6 @@ export const getInitialConfiguringConfigBasedOnCube = (props: { }, }, dataFilters: { - active: false, componentIris: [], }, }, diff --git a/app/configurator/configurator-state/mocks.ts b/app/configurator/configurator-state/mocks.ts index 024a3fb10..168201c85 100644 --- a/app/configurator/configurator-state/mocks.ts +++ b/app/configurator/configurator-state/mocks.ts @@ -71,7 +71,6 @@ export const configStateMock = { }, }, dataFilters: { - active: false, componentIris: [], }, }, @@ -218,7 +217,6 @@ export const configStateMock = { }, }, dataFilters: { - active: false, componentIris: [], }, }, diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index 040edce4e..601f17df0 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -1085,6 +1085,40 @@ const reducer_: Reducer = ( } return draft; + case "DASHBOARD_DATA_FILTER_ADD": + if (isLayouting(draft) && draft.dashboardFilters) { + const { dimensionIri } = action.value; + const newFilters = { + ...draft.dashboardFilters, + dataFilters: { + ...draft.dashboardFilters.dataFilters, + componentIris: [ + ...draft.dashboardFilters.dataFilters.componentIris, + dimensionIri, + ], + }, + }; + draft.dashboardFilters = newFilters; + } + return draft; + + case "DASHBOARD_DATA_FILTER_REMOVE": + if (isLayouting(draft) && draft.dashboardFilters) { + const { dimensionIri } = action.value; + const newFilters = { + ...draft.dashboardFilters, + dataFilters: { + ...draft.dashboardFilters.dataFilters, + componentIris: + draft.dashboardFilters.dataFilters.componentIris.filter( + (d) => d !== dimensionIri + ), + }, + }; + draft.dashboardFilters = newFilters; + } + return draft; + default: throw unreachableError(action); } diff --git a/app/docs/charts.stories.tsx b/app/docs/charts.stories.tsx index 38c5215d1..76fcfaead 100644 --- a/app/docs/charts.stories.tsx +++ b/app/docs/charts.stories.tsx @@ -78,7 +78,6 @@ const ColumnsStory = { }, }, dataFilters: { - active: false, componentIris: [], }, }, @@ -141,7 +140,6 @@ const ScatterplotStory = { }, }, dataFilters: { - active: false, componentIris: [], }, }, diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts index e949c3352..ec33a0659 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -98,7 +98,6 @@ export const states: ConfiguratorState[] = [ }, }, dataFilters: { - active: false, componentIris: [], }, }, diff --git a/app/docs/lines.stories.tsx b/app/docs/lines.stories.tsx index 5df697309..f860b9cb5 100644 --- a/app/docs/lines.stories.tsx +++ b/app/docs/lines.stories.tsx @@ -56,7 +56,6 @@ const LineChartStory = () => ( }, }, dataFilters: { - active: false, componentIris: [], }, }, diff --git a/app/stores/interactive-filters.tsx b/app/stores/interactive-filters.tsx index 6d4554809..5a86e8213 100644 --- a/app/stores/interactive-filters.tsx +++ b/app/stores/interactive-filters.tsx @@ -1,3 +1,4 @@ +import uniq from "lodash/uniq"; import React, { createContext, useContext, useMemo, useRef } from "react"; import create, { StateCreator, StoreApi, UseBoundStore } from "zustand"; @@ -187,6 +188,20 @@ const getPotentialTimeRangeFilterIris = (chartConfigs: ChartConfig[]) => { return temporalDimensions.map((dimension) => dimension.componentIri); }; +const getPotentialDataFilterIris = (chartConfigs: ChartConfig[]) => { + return uniq( + chartConfigs.flatMap((config) => { + return config.cubes + .map((cube) => cube.filters) + .flatMap((filters) => { + return Object.entries(filters) + .filter(([_, filter]) => filter.type === "single") + .map(([dimensionIri]) => dimensionIri); + }); + }) + ); +}; + /** * Creates and provides all the interactive filters stores for the given chartConfigs. */ @@ -202,6 +217,9 @@ export const InteractiveFiltersProvider = ({ const potentialTimeRangeFilterIris = useMemo(() => { return getPotentialTimeRangeFilterIris(chartConfigs); }, [chartConfigs]); + const potentialDataFilterIris = useMemo(() => { + return getPotentialDataFilterIris(chartConfigs); + }, [chartConfigs]); const stores = useMemo< Record @@ -222,19 +240,23 @@ export const InteractiveFiltersProvider = ({ }, [chartConfigs]); const timeRange = state.dashboardFilters?.timeRange; + const dataFilters = state.dashboardFilters?.dataFilters; const ctxValue = useMemo( () => ({ potentialTimeRangeFilterIris, timeRange, - potentialDataFilterIris: [], - dataFilters: { - active: false, - componentIris: [], - }, + potentialDataFilterIris, + dataFilters, stores, }), - [stores, potentialTimeRangeFilterIris, timeRange] + [ + potentialTimeRangeFilterIris, + timeRange, + potentialDataFilterIris, + dataFilters, + stores, + ] ); return ( diff --git a/app/utils/chart-config/versioning.ts b/app/utils/chart-config/versioning.ts index 732a260cf..5867ee1d5 100644 --- a/app/utils/chart-config/versioning.ts +++ b/app/utils/chart-config/versioning.ts @@ -1295,7 +1295,6 @@ export const configuratorStateMigrations: Migration[] = [ dashboardFilters: { ...config.dashboardFilters, dataFilters: { - active: false, componentIris: [], }, }, From 7fca689f3045c3ed6410782a8e25d4451d0d562d Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 24 Jul 2024 11:27:58 +0200 Subject: [PATCH 03/22] refactor: Do not mirror configurator dashboard filters in dashboard filters context --- app/charts/column/chart-column.tsx | 7 ++--- app/charts/combo/chart-combo-line-column.tsx | 7 ++--- app/charts/combo/chart-combo-line-dual.tsx | 7 ++--- app/charts/combo/chart-combo-line-single.tsx | 7 ++--- app/charts/line/chart-lines.tsx | 6 ++--- app/charts/shared/chart-dimensions.tsx | 8 +++--- app/charts/shared/chart-state.ts | 13 +++++---- .../dashboard-interactive-filters.tsx | 27 +++++++++---------- .../components/layout-configurator.tsx | 9 +++---- .../interactive-filters/time-slider.tsx | 12 ++++----- app/stores/interactive-filters.tsx | 20 +------------- 11 files changed, 50 insertions(+), 73 deletions(-) diff --git a/app/charts/column/chart-column.tsx b/app/charts/column/chart-column.tsx index eef1ae59e..5e8528a9f 100644 --- a/app/charts/column/chart-column.tsx +++ b/app/charts/column/chart-column.tsx @@ -25,8 +25,9 @@ import { import { Tooltip } from "@/charts/shared/interaction/tooltip"; import { LegendColor } from "@/charts/shared/legend-color"; import { ColumnConfig, useChartConfigFilters } from "@/config-types"; +import { hasChartConfigs } from "@/configurator"; import { TimeSlider } from "@/configurator/interactive-filters/time-slider"; -import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; +import { useConfiguratorState } from "@/src"; import { ChartProps, VisualizationProps } from "../shared/ChartProps"; @@ -40,10 +41,10 @@ const ChartColumns = memo((props: ChartProps) => { const { chartConfig, dimensions } = props; const { fields, interactiveFiltersConfig } = chartConfig; const filters = useChartConfigFilters(chartConfig); - const dashboardFilters = useDashboardInteractiveFilters(); + const [{ dashboardFilters }] = useConfiguratorState(hasChartConfigs); const showTimeBrush = shouldShowBrush( interactiveFiltersConfig, - dashboardFilters.timeRange + dashboardFilters?.timeRange ); return ( <> diff --git a/app/charts/combo/chart-combo-line-column.tsx b/app/charts/combo/chart-combo-line-column.tsx index bd701e012..f6687b931 100644 --- a/app/charts/combo/chart-combo-line-column.tsx +++ b/app/charts/combo/chart-combo-line-column.tsx @@ -12,7 +12,8 @@ import { HoverDotMultiple } from "@/charts/shared/interaction/hover-dots-multipl import { Ruler } from "@/charts/shared/interaction/ruler"; import { Tooltip } from "@/charts/shared/interaction/tooltip"; import { ComboLineColumnConfig } from "@/config-types"; -import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; +import { hasChartConfigs } from "@/configurator"; +import { useConfiguratorState } from "@/src"; import { ChartProps, VisualizationProps } from "../shared/ChartProps"; @@ -26,7 +27,7 @@ const ChartComboLineColumn = memo( (props: ChartProps) => { const { chartConfig } = props; const { interactiveFiltersConfig } = chartConfig; - const dashboardFilters = useDashboardInteractiveFilters(); + const [{ dashboardFilters }] = useConfiguratorState(hasChartConfigs); return ( @@ -38,7 +39,7 @@ const ChartComboLineColumn = memo( {shouldShowBrush( interactiveFiltersConfig, - dashboardFilters.timeRange + dashboardFilters?.timeRange ) && } diff --git a/app/charts/combo/chart-combo-line-dual.tsx b/app/charts/combo/chart-combo-line-dual.tsx index 1203f2fb2..a1c831c07 100644 --- a/app/charts/combo/chart-combo-line-dual.tsx +++ b/app/charts/combo/chart-combo-line-dual.tsx @@ -12,7 +12,8 @@ import { Ruler } from "@/charts/shared/interaction/ruler"; import { Tooltip } from "@/charts/shared/interaction/tooltip"; import { InteractionHorizontal } from "@/charts/shared/overlay-horizontal"; import { ComboLineDualConfig } from "@/config-types"; -import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; +import { hasChartConfigs } from "@/configurator"; +import { useConfiguratorState } from "@/src"; import { ChartProps, VisualizationProps } from "../shared/ChartProps"; @@ -25,7 +26,7 @@ export const ChartComboLineDualVisualization = ( const ChartComboLineDual = memo((props: ChartProps) => { const { chartConfig } = props; const { interactiveFiltersConfig } = chartConfig; - const dashboardFilters = useDashboardInteractiveFilters(); + const [{ dashboardFilters }] = useConfiguratorState(hasChartConfigs); return ( @@ -37,7 +38,7 @@ const ChartComboLineDual = memo((props: ChartProps) => { {shouldShowBrush( interactiveFiltersConfig, - dashboardFilters.timeRange + dashboardFilters?.timeRange ) && } diff --git a/app/charts/combo/chart-combo-line-single.tsx b/app/charts/combo/chart-combo-line-single.tsx index 19c53722e..bd50ffdc7 100644 --- a/app/charts/combo/chart-combo-line-single.tsx +++ b/app/charts/combo/chart-combo-line-single.tsx @@ -12,7 +12,8 @@ import { Ruler } from "@/charts/shared/interaction/ruler"; import { Tooltip } from "@/charts/shared/interaction/tooltip"; import { InteractionHorizontal } from "@/charts/shared/overlay-horizontal"; import { ComboLineSingleConfig } from "@/config-types"; -import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; +import { hasChartConfigs } from "@/configurator"; +import { useConfiguratorState } from "@/src"; import { ChartProps, VisualizationProps } from "../shared/ChartProps"; @@ -26,7 +27,7 @@ const ChartComboLineSingle = memo( (props: ChartProps) => { const { chartConfig } = props; const { interactiveFiltersConfig } = chartConfig; - const dashboardFilters = useDashboardInteractiveFilters(); + const [{ dashboardFilters }] = useConfiguratorState(hasChartConfigs); return ( @@ -36,7 +37,7 @@ const ChartComboLineSingle = memo( {shouldShowBrush( interactiveFiltersConfig, - dashboardFilters.timeRange + dashboardFilters?.timeRange ) && } diff --git a/app/charts/line/chart-lines.tsx b/app/charts/line/chart-lines.tsx index 308b1a484..510f8418d 100644 --- a/app/charts/line/chart-lines.tsx +++ b/app/charts/line/chart-lines.tsx @@ -17,7 +17,7 @@ import { Tooltip } from "@/charts/shared/interaction/tooltip"; import { LegendColor } from "@/charts/shared/legend-color"; import { InteractionHorizontal } from "@/charts/shared/overlay-horizontal"; import { LineConfig } from "@/config-types"; -import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; +import { hasChartConfigs, useConfiguratorState } from "@/configurator"; import { ChartProps, VisualizationProps } from "../shared/ChartProps"; @@ -30,7 +30,7 @@ export const ChartLinesVisualization = ( const ChartLines = memo((props: ChartProps) => { const { chartConfig } = props; const { fields, interactiveFiltersConfig } = chartConfig; - const dashboardFilters = useDashboardInteractiveFilters(); + const [{ dashboardFilters }] = useConfiguratorState(hasChartConfigs); return ( @@ -41,7 +41,7 @@ const ChartLines = memo((props: ChartProps) => { {shouldShowBrush( interactiveFiltersConfig, - dashboardFilters.timeRange + dashboardFilters?.timeRange ) && } diff --git a/app/charts/shared/chart-dimensions.tsx b/app/charts/shared/chart-dimensions.tsx index 206c1653b..93f93e05a 100644 --- a/app/charts/shared/chart-dimensions.tsx +++ b/app/charts/shared/chart-dimensions.tsx @@ -9,11 +9,11 @@ import { Bounds, Margins } from "@/charts/shared/use-size"; import { CHART_GRID_MIN_HEIGHT } from "@/components/react-grid"; import { ChartConfig, + DashboardFiltersConfig, hasChartConfigs, isLayoutingFreeCanvas, useConfiguratorState, } from "@/configurator"; -import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; import { getTextWidth } from "@/utils/get-text-width"; type ComputeChartPaddingProps = { @@ -29,7 +29,7 @@ type ComputeChartPaddingProps = { const computeChartPadding = ( props: ComputeChartPaddingProps & { - dashboardFilters: ReturnType; + dashboardFilters: DashboardFiltersConfig | undefined; } ) => { const { @@ -61,7 +61,7 @@ const computeChartPadding = ( ); let bottom = - (!dashboardFilters.timeRange?.active && + (!dashboardFilters?.timeRange.active && !!interactiveFiltersConfig?.timeRange.active) || animationPresent ? BRUSH_BOTTOM_SPACE @@ -87,7 +87,7 @@ export const useChartPadding = (props: ComputeChartPaddingProps) => { bandDomain, normalize, } = props; - const dashboardFilters = useDashboardInteractiveFilters(); + const [{ dashboardFilters }] = useConfiguratorState(hasChartConfigs); return useMemo(() => { return computeChartPadding({ yScale, diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts index 66fb03403..3caf00321 100644 --- a/app/charts/shared/chart-state.ts +++ b/app/charts/shared/chart-state.ts @@ -32,6 +32,8 @@ import { GenericField, InteractiveFiltersConfig, getAnimationField, + hasChartConfigs, + useConfiguratorState, } from "@/configurator"; import { parseDate, @@ -57,10 +59,7 @@ import { } from "@/domain/data"; import { Has } from "@/domain/types"; import { ScaleType, TimeUnit } from "@/graphql/resolver-types"; -import { - useChartInteractiveFilters, - useDashboardInteractiveFilters, -} from "@/stores/interactive-filters"; +import { useChartInteractiveFilters } from "@/stores/interactive-filters"; export type ChartState = | AreasState @@ -508,13 +507,13 @@ export const useChartData = ( // interactive time range const interactiveFromTime = timeRange.from?.getTime(); const interactiveToTime = timeRange.to?.getTime(); - const dashboardFilters = useDashboardInteractiveFilters(); + const [{ dashboardFilters }] = useConfiguratorState(hasChartConfigs); const interactiveTimeRangeFilters = useMemo(() => { const interactiveTimeRangeFilter: ValuePredicate | null = getXAsDate && interactiveFromTime && interactiveToTime && - (interactiveTimeRange?.active || dashboardFilters.timeRange?.active) + (interactiveTimeRange?.active || dashboardFilters?.timeRange.active) ? (d: Observation) => { const time = getXAsDate(d).getTime(); return time >= interactiveFromTime && time <= interactiveToTime; @@ -523,7 +522,7 @@ export const useChartData = ( return interactiveTimeRangeFilter ? [interactiveTimeRangeFilter] : []; }, [ - dashboardFilters.timeRange, + dashboardFilters?.timeRange, getXAsDate, interactiveFromTime, interactiveToTime, diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index f0f186559..9e0ebdf57 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -1,16 +1,13 @@ -import { - Collapse, - Slider, - sliderClasses, - useEventCallback, -} from "@mui/material"; +import { Slider, sliderClasses, useEventCallback } from "@mui/material"; import { Theme } from "@mui/material/styles"; import { makeStyles } from "@mui/styles"; import { useEffect, useMemo, useState } from "react"; import { DashboardTimeRangeFilter, + hasChartConfigs, InteractiveFiltersTimeRange, + useConfiguratorState, } from "@/configurator"; import { timeUnitToFormatter, @@ -181,17 +178,17 @@ const DashboardTimeRangeSlider = ({ }; export const DashboardInteractiveFilters = () => { - const { timeRange } = useDashboardInteractiveFilters(); - return timeRange?.active ? ( - -
+ const [{ dashboardFilters }] = useConfiguratorState(hasChartConfigs); + return ( +
+ {dashboardFilters?.timeRange.active ? ( -
- - ) : null; + ) : null} +
+ ); }; function stepFromTimeUnit(timeUnit: TimeUnit | undefined) { diff --git a/app/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index bfa04c947..b3ac2425e 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -115,12 +115,9 @@ const LayoutLayoutConfigurator = () => { const LayoutSharedFiltersConfigurator = () => { const [state, dispatch] = useConfiguratorState(isLayouting); const { layout } = state; - const { - timeRange, - potentialTimeRangeFilterIris, - dataFilters, - potentialDataFilterIris, - } = useDashboardInteractiveFilters(); + const { potentialTimeRangeFilterIris, potentialDataFilterIris } = + useDashboardInteractiveFilters(); + const { timeRange, dataFilters } = state.dashboardFilters ?? {}; const locale = useLocale(); const [{ data }] = useConfigsCubeComponents({ diff --git a/app/configurator/interactive-filters/time-slider.tsx b/app/configurator/interactive-filters/time-slider.tsx index e2433b5e6..9e9f93465 100644 --- a/app/configurator/interactive-filters/time-slider.tsx +++ b/app/configurator/interactive-filters/time-slider.tsx @@ -16,6 +16,7 @@ import { TableChartState } from "@/charts/table/table-state"; import { Slider as GenericSlider } from "@/components/form"; import { AnimationField, Filters, SortingField } from "@/config-types"; import { parseDate } from "@/configurator/components/ui-helpers"; +import { hasChartConfigs } from "@/configurator/configurator-state"; import { Dimension, isTemporalDimension, @@ -25,10 +26,8 @@ import { import { truthy } from "@/domain/types"; import { useTimeFormatUnit } from "@/formatters"; import { Icon } from "@/icons"; -import { - useChartInteractiveFilters, - useDashboardInteractiveFilters, -} from "@/stores/interactive-filters"; +import { useConfiguratorState } from "@/src"; +import { useChartInteractiveFilters } from "@/stores/interactive-filters"; import { Timeline, TimelineProps } from "@/utils/observables"; import { getSortingOrders, @@ -67,6 +66,7 @@ export const TimeSlider = (props: TimeSliderProps) => { dimensions, } = props; + const [state] = useConfiguratorState(hasChartConfigs); const dimension = useMemo(() => { return dimensions.find((d) => d.iri === componentIri); }, [componentIri, dimensions]); @@ -74,8 +74,6 @@ export const TimeSlider = (props: TimeSliderProps) => { const temporalEntity = isTemporalEntityDimension(dimension); const temporalOrdinal = isTemporalOrdinalDimension(dimension); - const dashboardFilters = useDashboardInteractiveFilters(); - if (!(temporal || temporalEntity || temporalOrdinal)) { throw new Error("You can only use TimeSlider with temporal dimensions!"); } @@ -144,7 +142,7 @@ export const TimeSlider = (props: TimeSliderProps) => { return new Timeline(timelineProps); }, [timelineProps]); - if (dashboardFilters.timeRange?.active) { + if (state.dashboardFilters?.timeRange.active) { return null; } diff --git a/app/stores/interactive-filters.tsx b/app/stores/interactive-filters.tsx index 5a86e8213..126021ede 100644 --- a/app/stores/interactive-filters.tsx +++ b/app/stores/interactive-filters.tsx @@ -6,11 +6,7 @@ import { getChartSpec } from "@/charts/chart-config-ui-options"; import { CalculationType, ChartConfig, - DashboardDataFiltersConfig, - DashboardTimeRangeFilter, FilterValueSingle, - hasChartConfigs, - useConfiguratorState, } from "@/configurator"; import { truthy } from "@/domain/types"; import { getOriginalIris, isJoinById } from "@/graphql/join"; @@ -147,9 +143,7 @@ type InteractiveFiltersContextValue = [ const InteractiveFiltersContext = createContext< | { potentialTimeRangeFilterIris: string[]; - timeRange: DashboardTimeRangeFilter | undefined; potentialDataFilterIris: string[]; - dataFilters: DashboardDataFiltersConfig | undefined; stores: Record; } | undefined @@ -211,7 +205,6 @@ export const InteractiveFiltersProvider = ({ }: React.PropsWithChildren<{ chartConfigs: ChartConfig[]; }>) => { - const [state] = useConfiguratorState(hasChartConfigs); const storeRefs = useRef>>({}); const potentialTimeRangeFilterIris = useMemo(() => { @@ -239,24 +232,13 @@ export const InteractiveFiltersProvider = ({ ); }, [chartConfigs]); - const timeRange = state.dashboardFilters?.timeRange; - const dataFilters = state.dashboardFilters?.dataFilters; - const ctxValue = useMemo( () => ({ potentialTimeRangeFilterIris, - timeRange, potentialDataFilterIris, - dataFilters, stores, }), - [ - potentialTimeRangeFilterIris, - timeRange, - potentialDataFilterIris, - dataFilters, - stores, - ] + [potentialTimeRangeFilterIris, potentialDataFilterIris, stores] ); return ( From 42a5eead32307b5dde73b1e3f8832e8f38c5aec1 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 25 Jul 2024 10:59:45 +0200 Subject: [PATCH 04/22] feat: Add initial dashboard data filters logic --- app/browse/datatable.tsx | 14 +- app/charts/shared/chart-data-filters.tsx | 13 +- app/charts/shared/chart-helpers.spec.tsx | 5 +- app/charts/shared/chart-helpers.tsx | 29 ++- app/components/chart-filters-list.tsx | 4 + app/components/chart-footnotes.tsx | 4 + app/components/chart-preview.tsx | 7 +- app/components/chart-published.tsx | 6 +- app/components/chart-shared.tsx | 14 +- app/components/chart-with-filters.tsx | 11 +- .../dashboard-interactive-filters.tsx | 242 ++++++++++++++++-- app/components/metadata-panel.tsx | 18 +- app/components/select-tree.tsx | 2 +- .../components/chart-configurator.tsx | 9 +- .../components/chart-options-selector.tsx | 5 +- 15 files changed, 335 insertions(+), 48 deletions(-) diff --git a/app/browse/datatable.tsx b/app/browse/datatable.tsx index b4df070ea..8dc0f4a4d 100644 --- a/app/browse/datatable.tsx +++ b/app/browse/datatable.tsx @@ -20,7 +20,11 @@ import { } from "@/charts/shared/chart-helpers"; import { Loading } from "@/components/hint"; import { OpenMetadataPanelWrapper } from "@/components/metadata-panel"; -import { ChartConfig, DataSource } from "@/config-types"; +import { + ChartConfig, + DashboardFiltersConfig, + DataSource, +} from "@/config-types"; import { Component, Dimension, @@ -229,10 +233,12 @@ export const DataSetPreviewTable = ({ export const DataSetTable = ({ dataSource, chartConfig, + dashboardFilters, sx, }: { dataSource: DataSource; chartConfig: ChartConfig; + dashboardFilters: DashboardFiltersConfig | undefined; sx?: SxProps; }) => { const locale = useLocale(); @@ -269,7 +275,11 @@ export const DataSetTable = ({ ...componentsData.dataCubesComponents.measures, ]); }, [componentsData?.dataCubesComponents]); - const queryFilters = useQueryFilters({ chartConfig, componentIris }); + const queryFilters = useQueryFilters({ + chartConfig, + dashboardFilters, + componentIris, + }); const [{ data: observationsData }] = useDataCubesObservationsQuery({ variables: { ...commonQueryVariables, diff --git a/app/charts/shared/chart-data-filters.tsx b/app/charts/shared/chart-data-filters.tsx index 2412a98ac..a9e6265c3 100644 --- a/app/charts/shared/chart-data-filters.tsx +++ b/app/charts/shared/chart-data-filters.tsx @@ -18,6 +18,7 @@ import { OpenMetadataPanelWrapper } from "@/components/metadata-panel"; import SelectTree from "@/components/select-tree"; import { ChartConfig, + DashboardFiltersConfig, DataSource, Filters, getFiltersByMappingStatus, @@ -66,9 +67,11 @@ type PreparedFilter = { export const useChartDataFiltersState = ({ dataSource, chartConfig, + dashboardFilters, }: { dataSource: DataSource; chartConfig: ChartConfig; + dashboardFilters: DashboardFiltersConfig | undefined; }) => { const componentIris = chartConfig.interactiveFiltersConfig?.dataFilters.componentIris; @@ -81,6 +84,7 @@ export const useChartDataFiltersState = ({ const { loading } = useLoadingState(); const queryFilters = useQueryFilters({ chartConfig, + dashboardFilters, allowNoneValues: true, componentIris, }); @@ -107,6 +111,7 @@ export const useChartDataFiltersState = ({ }; }); }, [chartConfig, componentIris, queryFilters]); + // TODO: disable when dashboard filters are active? const { error } = useEnsurePossibleInteractiveFilters({ dataSource, chartConfig, @@ -371,7 +376,9 @@ export type DataFilterGenericDimensionProps = { disabled: boolean; }; -const DataFilterGenericDimension = (props: DataFilterGenericDimensionProps) => { +export const DataFilterGenericDimension = ( + props: DataFilterGenericDimensionProps +) => { const { dimension, value, onChange, options: propOptions, disabled } = props; const { label, isKeyDimension } = dimension; const noneLabel = t({ @@ -420,7 +427,7 @@ type DataFilterHierarchyDimensionProps = { disabled: boolean; }; -const DataFilterHierarchyDimension = ( +export const DataFilterHierarchyDimension = ( props: DataFilterHierarchyDimensionProps ) => { const { dimension, value, onChange, hierarchy, disabled } = props; @@ -475,7 +482,7 @@ const DataFilterHierarchyDimension = ( ); }; -const DataFilterTemporalDimension = ({ +export const DataFilterTemporalDimension = ({ dimension, value, onChange, diff --git a/app/charts/shared/chart-helpers.spec.tsx b/app/charts/shared/chart-helpers.spec.tsx index 67c8ce39c..3ff57c38c 100644 --- a/app/charts/shared/chart-helpers.spec.tsx +++ b/app/charts/shared/chart-helpers.spec.tsx @@ -93,6 +93,7 @@ describe("useQueryFilters", () => { line1Fixture.data.chartConfig.chartType as ChartType, line1Fixture.data.chartConfig.filters as Filters, commonInteractiveFiltersConfig, + undefined, commonInteractiveFiltersState.dataFilters ); expect(queryFilters[col("3")]).toEqual({ @@ -110,6 +111,7 @@ describe("useQueryFilters", () => { active: true, }, }), + undefined, commonInteractiveFiltersState.dataFilters ); @@ -128,6 +130,7 @@ describe("useQueryFilters", () => { active: true, }, }), + undefined, merge({}, commonInteractiveFiltersState, { dataFilters: { [col("3")]: { @@ -171,7 +174,7 @@ describe("useQueryFilters", () => { >( (props: Parameters[0]) => useQueryFilters(props), { - initialProps: { chartConfig }, + initialProps: { chartConfig, dashboardFilters: undefined }, } ); diff --git a/app/charts/shared/chart-helpers.tsx b/app/charts/shared/chart-helpers.tsx index b214422f9..14f5f8729 100644 --- a/app/charts/shared/chart-helpers.tsx +++ b/app/charts/shared/chart-helpers.tsx @@ -23,6 +23,7 @@ import { import { CategoricalColorField, ComboChartConfig, + DashboardFiltersConfig, GenericField, NumericalColorField, getChartConfigFilters, @@ -54,14 +55,20 @@ export const prepareCubeQueryFilters = ( chartType: ChartType, cubeFilters: Filters, interactiveFiltersConfig: InteractiveFiltersConfig, + dashboardFiltersConfig: DashboardFiltersConfig | undefined, cubeDataFilters: InteractiveFiltersState["dataFilters"], allowNoneValues = false ): Filters => { const queryFilters = { ...cubeFilters }; - if (chartType !== "table" && interactiveFiltersConfig?.dataFilters.active) { + if (chartType !== "table") { for (const [k, v] of Object.entries(cubeDataFilters)) { - queryFilters[k] = v; + if ( + interactiveFiltersConfig?.dataFilters.active || + dashboardFiltersConfig?.dataFilters.componentIris.includes(k) + ) { + queryFilters[k] = v; + } } } @@ -75,16 +82,16 @@ export const prepareCubeQueryFilters = ( export const useQueryFilters = ({ chartConfig, + dashboardFilters, allowNoneValues, componentIris, }: { chartConfig: ChartConfig; + dashboardFilters: DashboardFiltersConfig | undefined; allowNoneValues?: boolean; componentIris?: string[]; }): DataCubeObservationFilter[] => { - const allInteractiveDataFilters = useChartInteractiveFilters( - (d) => d.dataFilters - ); + const chartDataFilters = useChartInteractiveFilters((d) => d.dataFilters); return useMemo(() => { return chartConfig.cubes.map((cube) => { const cubeFilters = getChartConfigFilters(chartConfig.cubes, { @@ -95,8 +102,8 @@ export const useQueryFilters = ({ // This is a bigger issue we should address in the future, probably by keeping // track of interactive data filters per cube. // Only include data filters that are part of the chart config. - const cubeDataInteractiveFilters = Object.fromEntries( - Object.entries(allInteractiveDataFilters).filter(([key]) => + const cubeDataFilters = Object.fromEntries( + Object.entries(chartDataFilters).filter(([key]) => cubeFiltersKeys.includes(key) ) ); @@ -108,7 +115,8 @@ export const useQueryFilters = ({ chartConfig.chartType, cubeFilters, chartConfig.interactiveFiltersConfig, - cubeDataInteractiveFilters, + dashboardFilters, + cubeDataFilters, allowNoneValues ), joinBy: cube.joinBy, @@ -118,9 +126,10 @@ export const useQueryFilters = ({ chartConfig.cubes, chartConfig.chartType, chartConfig.interactiveFiltersConfig, - allInteractiveDataFilters, - allowNoneValues, + chartDataFilters, componentIris, + dashboardFilters, + allowNoneValues, ]); }; diff --git a/app/components/chart-filters-list.tsx b/app/components/chart-filters-list.tsx index 85075282c..e8f5edeb6 100644 --- a/app/components/chart-filters-list.tsx +++ b/app/components/chart-filters-list.tsx @@ -9,6 +9,7 @@ import { import { OpenMetadataPanelWrapper } from "@/components/metadata-panel"; import { ChartConfig, + DashboardFiltersConfig, DataSource, FilterValue, getAnimationField, @@ -28,11 +29,13 @@ export const ChartFiltersList = ({ cubeIri, dataSource, chartConfig, + dashboardFilters, components, }: { cubeIri: string; dataSource: DataSource; chartConfig: ChartConfig; + dashboardFilters: DashboardFiltersConfig | undefined; components: Component[]; }) => { const locale = useLocale(); @@ -41,6 +44,7 @@ export const ChartFiltersList = ({ const animationField = getAnimationField(chartConfig); const queryFilters = useQueryFilters({ chartConfig, + dashboardFilters, componentIris: extractChartConfigComponentIris({ chartConfig }), }); const cubeQueryFilters = useMemo(() => { diff --git a/app/components/chart-footnotes.tsx b/app/components/chart-footnotes.tsx index 449a50672..d744f37dc 100644 --- a/app/components/chart-footnotes.tsx +++ b/app/components/chart-footnotes.tsx @@ -12,6 +12,7 @@ import { ComboLineColumnConfig, ComboLineDualConfig, ComboLineSingleConfig, + DashboardFiltersConfig, DataSource, } from "@/configurator"; import { Component, Measure } from "@/domain/data"; @@ -43,11 +44,13 @@ export const useFootnotesStyles = makeStyles( export const ChartFootnotes = ({ dataSource, chartConfig, + dashboardFilters, components, showVisualizeLink = false, }: { dataSource: DataSource; chartConfig: ChartConfig; + dashboardFilters: DashboardFiltersConfig | undefined; components: Component[]; showVisualizeLink?: boolean; }) => { @@ -89,6 +92,7 @@ export const ChartFootnotes = ({ diff --git a/app/components/chart-preview.tsx b/app/components/chart-preview.tsx index e72642f68..5473aacfc 100644 --- a/app/components/chart-preview.tsx +++ b/app/components/chart-preview.tsx @@ -89,9 +89,10 @@ export const ChartPreview = (props: ChartPreviewProps) => { // we switch tabs in the configurator, otherwise we end up with the wrong // data in the downstream hooks (useDataCubesMetadataQuery, etc.) <> - {state.state !== "CONFIGURING_CHART" ? ( + {!isConfiguring(state) ? ( x.key).join(",")} + sx={{ mb: 4 }} /> ) : null} @@ -531,6 +532,7 @@ const ChartPreviewInner = (props: ChartPreviewInnerProps) => { { ) : ( @@ -556,12 +559,14 @@ const ChartPreviewInner = (props: ChartPreviewInnerProps) => { dataSource={dataSource} componentIris={componentIris} chartConfig={chartConfig} + dashboardFilters={state.dashboardFilters} /> )} {/* Wrap in div for subgrid layout */} diff --git a/app/components/chart-published.tsx b/app/components/chart-published.tsx index 0507a5a0b..0503aba78 100644 --- a/app/components/chart-published.tsx +++ b/app/components/chart-published.tsx @@ -152,7 +152,7 @@ export const ChartPublished = (props: ChartPublishedProps) => { )} - + { { ) : ( @@ -393,12 +395,14 @@ const ChartPublishedInnerImpl = (props: ChartPublishInnerProps) => { dataSource={dataSource} componentIris={componentIris} chartConfig={chartConfig} + dashboardFilters={state.dashboardFilters} /> )} diff --git a/app/components/chart-shared.tsx b/app/components/chart-shared.tsx index 34c371ef7..6b4bc2a8d 100644 --- a/app/components/chart-shared.tsx +++ b/app/components/chart-shared.tsx @@ -15,6 +15,7 @@ import { MenuActionItem } from "@/components/menu-action-item"; import { MetadataPanel } from "@/components/metadata-panel"; import { ChartConfig, + DashboardFiltersConfig, DataSource, getChartConfig, hasChartConfigs, @@ -50,19 +51,27 @@ export const useChartStyles = makeStyles((theme) => ({ export const ChartControls = ({ dataSource, chartConfig, + dashboardFilters, metadataPanelProps, }: { dataSource: DataSource; chartConfig: ChartConfig; + dashboardFilters: DashboardFiltersConfig | undefined; metadataPanelProps: Omit< ComponentProps, - "dataSource" | "chartConfig" + "dataSource" | "chartConfig" | "dashboardFilters" >; }) => { - const showFilters = chartConfig.interactiveFiltersConfig?.dataFilters.active; + const showFilters = + chartConfig.interactiveFiltersConfig?.dataFilters.active && + chartConfig.interactiveFiltersConfig.dataFilters.componentIris.some( + (componentIri) => + !dashboardFilters?.dataFilters.componentIris.includes(componentIri) + ); const chartFiltersState = useChartDataFiltersState({ dataSource, chartConfig, + dashboardFilters, }); return ( diff --git a/app/components/chart-with-filters.tsx b/app/components/chart-with-filters.tsx index 09bb714a8..8629c365b 100644 --- a/app/components/chart-with-filters.tsx +++ b/app/components/chart-with-filters.tsx @@ -5,7 +5,11 @@ import { forwardRef } from "react"; import { useQueryFilters } from "@/charts/shared/chart-helpers"; import { Observer } from "@/charts/shared/use-size"; import useSyncInteractiveFilters from "@/charts/shared/use-sync-interactive-filters"; -import { ChartConfig, DataSource } from "@/configurator"; +import { + ChartConfig, + DashboardFiltersConfig, + DataSource, +} from "@/configurator"; const ChartAreasVisualization = dynamic( import("@/charts/area/chart-area").then( @@ -72,12 +76,14 @@ type GenericChartProps = { dataSource: DataSource; componentIris: string[] | undefined; chartConfig: ChartConfig; + dashboardFilters: DashboardFiltersConfig | undefined; }; const GenericChart = (props: GenericChartProps) => { - const { dataSource, componentIris, chartConfig } = props; + const { dataSource, componentIris, chartConfig, dashboardFilters } = props; const observationQueryFilters = useQueryFilters({ chartConfig, + dashboardFilters, componentIris, }); const commonProps = { @@ -150,6 +156,7 @@ type ChartWithFiltersProps = { dataSource: DataSource; componentIris: string[] | undefined; chartConfig: ChartConfig; + dashboardFilters: DashboardFiltersConfig | undefined; }; const useStyles = makeStyles(() => ({ diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index 9e0ebdf57..ec0799d57 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -1,8 +1,21 @@ -import { Slider, sliderClasses, useEventCallback } from "@mui/material"; +import { + Box, + BoxProps, + SelectChangeEvent, + Slider, + sliderClasses, + useEventCallback, +} from "@mui/material"; import { Theme } from "@mui/material/styles"; import { makeStyles } from "@mui/styles"; -import { useEffect, useMemo, useState } from "react"; +import uniq from "lodash/uniq"; +import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react"; +import { + DataFilterGenericDimension, + DataFilterHierarchyDimension, + DataFilterTemporalDimension, +} from "@/charts/shared/chart-data-filters"; import { DashboardTimeRangeFilter, hasChartConfigs, @@ -13,18 +26,44 @@ import { timeUnitToFormatter, timeUnitToParser, } from "@/configurator/components/ui-helpers"; +import { isTemporalDimension } from "@/domain/data"; +import { useDataCubesComponentsQuery } from "@/graphql/hooks"; import { TimeUnit } from "@/graphql/query-hooks"; -import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; +import { useLocale } from "@/locales/use-locale"; +import { + InteractiveFiltersState, + useDashboardInteractiveFilters, +} from "@/stores/interactive-filters"; import { useTransitionStore } from "@/stores/transition"; import { assert } from "@/utils/assert"; +import useEvent from "@/utils/use-event"; import { useTimeout } from "../hooks/use-timeout"; -const useStyles = makeStyles((theme: Theme) => ({ +export const DashboardInteractiveFilters = (props: BoxProps) => { + const [state] = useConfiguratorState(hasChartConfigs); + const { dashboardFilters } = state; + return ( + + {dashboardFilters?.timeRange.active ? ( + + ) : null} + {dashboardFilters?.dataFilters.componentIris.length ? ( + + ) : null} + + ); +}; + +const useTimeRangeRangeStyles = makeStyles((theme: Theme) => ({ slider: { maxWidth: 800, margin: theme.spacing(6, 4, 2), - [`& .${sliderClasses.track}`]: { height: 1, }, @@ -75,7 +114,7 @@ const DashboardTimeRangeSlider = ({ filter: DashboardTimeRangeFilter; mounted: boolean; }) => { - const classes = useStyles(); + const classes = useTimeRangeRangeStyles(); const dashboardInteractiveFilters = useDashboardInteractiveFilters(); const setEnableTransition = useTransitionStore((state) => state.setEnable); const presets = filter.presets; @@ -177,20 +216,6 @@ const DashboardTimeRangeSlider = ({ ); }; -export const DashboardInteractiveFilters = () => { - const [{ dashboardFilters }] = useConfiguratorState(hasChartConfigs); - return ( -
- {dashboardFilters?.timeRange.active ? ( - - ) : null} -
- ); -}; - function stepFromTimeUnit(timeUnit: TimeUnit | undefined) { if (!timeUnit) { return 0; @@ -213,3 +238,180 @@ function stepFromTimeUnit(timeUnit: TimeUnit | undefined) { return 1; } } + +const useDataFilterStyles = makeStyles((theme: Theme) => ({ + wrapper: { + display: "grid", + gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", + gap: theme.spacing(2), + }, + filter: { + display: "flex", + flex: "1 1 100%", + width: "100%", + marginRight: theme.spacing(3), + "&:last-of-type": { + marginRight: 0, + }, + "& > div": { + width: "100%", + }, + }, +})); + +const DashboardDataFilters = ({ + componentIris, +}: { + componentIris: string[]; +}) => { + const classes = useDataFilterStyles(); + return ( +
+ {componentIris.map((componentIri) => ( + + ))} +
+ ); +}; + +const DataFilter = ({ componentIri }: { componentIri: string }) => { + const locale = useLocale(); + const classes = useDataFilterStyles(); + const [state] = useConfiguratorState(hasChartConfigs); + const dashboardInteractiveFilters = useDashboardInteractiveFilters(); + const chartConfigs = state.chartConfigs.filter((config) => + config.cubes.some((cube) => + Object.keys(cube.filters).includes(componentIri) + ) + ); + const cubeIris = uniq( + state.chartConfigs.flatMap((config) => + config.cubes + .filter((cube) => Object.keys(cube.filters).includes(componentIri)) + .map((cube) => cube.iri) + ) + ); + + if (cubeIris.length > 1) { + console.error( + `Data filter ${componentIri} is used in multiple cubes: ${cubeIris.join(", ")}` + ); + } + + const cubeIri = cubeIris[0]; + + const [{ data }] = useDataCubesComponentsQuery({ + variables: { + sourceType: state.dataSource.type, + sourceUrl: state.dataSource.url, + locale, + cubeFilters: [ + { iri: cubeIri, componentIris: [componentIri], loadValues: true }, + ], + }, + keepPreviousData: true, + }); + + const dimension = data?.dataCubesComponents.dimensions[0]; + + const [value, setValue] = useState(); + const handleChange = useEvent( + ( + e: + | SelectChangeEvent + | ChangeEvent + | { target: { value: string } } + ) => { + const newValue = e.target.value as string; + setValue(newValue); + + for (const [chartKey, [_getState, _useStore, store]] of Object.entries( + dashboardInteractiveFilters.stores + )) { + if (chartConfigs.map((config) => config.key).includes(chartKey)) { + store.setState({ + dataFilters: { + ...store.getState().dataFilters, + [componentIri]: { + type: "single", + value: newValue, + }, + }, + }); + } + } + } + ); + + // Store the state of the stores to restore it when the component is unmounted + const storesRef = useRef<{ [chartKey: string]: InteractiveFiltersState }>(); + useEffect(() => { + storesRef.current = Object.fromEntries( + Object.entries(dashboardInteractiveFilters.stores).map( + ([key, [_getState, _useStore, store]]) => { + return [key, store.getState()]; + } + ) + ); + + if (dimension?.values.length) { + handleChange({ + target: { value: dimension.values[0].value as string }, + } as ChangeEvent); + } + + const storesRefCurrent = storesRef.current; + + return () => { + for (const [chartKey, [_getState, _useStore, store]] of Object.entries( + dashboardInteractiveFilters.stores + )) { + if (chartConfigs.map((config) => config.key).includes(chartKey)) { + const dataFilters = store.getState().dataFilters; + const refStore = storesRefCurrent[chartKey]; + if (refStore) { + const refDataFilters = refStore.dataFilters; + if (refDataFilters[componentIri]) { + dataFilters[componentIri] = refDataFilters[componentIri]; + store.setState({ dataFilters }); + } else { + delete dataFilters[componentIri]; + store.setState({ dataFilters }); + } + } + } + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dimension]); + const disabled = !dimension?.values.length; + const hierarchy = dimension?.hierarchy; + + return dimension && value ? ( +
+ {isTemporalDimension(dimension) ? ( + + ) : hierarchy ? ( + + ) : ( + + )} +
+ ) : null; +}; diff --git a/app/components/metadata-panel.tsx b/app/components/metadata-panel.tsx index 60183dfca..880b56d1f 100644 --- a/app/components/metadata-panel.tsx +++ b/app/components/metadata-panel.tsx @@ -45,7 +45,12 @@ import { useMetadataPanelStoreActions, } from "@/components/metadata-panel-store"; import { MotionBox } from "@/components/presence"; -import { BackButton, ChartConfig, DataSource } from "@/configurator"; +import { + BackButton, + ChartConfig, + DashboardFiltersConfig, + DataSource, +} from "@/configurator"; import { DRAWER_WIDTH } from "@/configurator/components/drawer"; import { getComponentDescription, @@ -229,6 +234,7 @@ export const OpenMetadataPanelWrapper = ({ export const MetadataPanel = ({ chartConfig, + dashboardFilters, dataSource, components, container, @@ -237,6 +243,7 @@ export const MetadataPanel = ({ renderToggle = true, }: { chartConfig: ChartConfig; + dashboardFilters: DashboardFiltersConfig | undefined; dataSource: DataSource; components: Component[]; container?: HTMLDivElement | null; @@ -327,7 +334,11 @@ export const MetadataPanel = ({ {activeSection === "general" ? ( - + ) : activeSection === "data" ? ( @@ -380,9 +391,11 @@ const Header = ({ onClose }: { onClose: () => void }) => { const CubesPanel = ({ dataSource, chartConfig, + dashboardFilters, }: { dataSource: DataSource; chartConfig: ChartConfig; + dashboardFilters: DashboardFiltersConfig | undefined; }) => { const classes = useOtherStyles(); const locale = useLocale(); @@ -404,6 +417,7 @@ const CubesPanel = ({ const cubesMetadata = dataCubesMetadataData?.dataCubesMetadata; const queryFilters = useQueryFilters({ chartConfig, + dashboardFilters, componentIris: extractChartConfigComponentIris({ chartConfig }), }); const [ diff --git a/app/components/select-tree.tsx b/app/components/select-tree.tsx index aae3508c6..27827b8e0 100644 --- a/app/components/select-tree.tsx +++ b/app/components/select-tree.tsx @@ -269,7 +269,7 @@ export type SelectTreeProps = { value: NodeId | undefined; topControls?: React.ReactNode; sideControls?: React.ReactNode; - onChange: (ev: { target: { value: NodeId } }) => void; + onChange: (e: { target: { value: NodeId } }) => void; disabled?: boolean; label?: React.ReactNode; onOpen?: () => void; diff --git a/app/configurator/components/chart-configurator.tsx b/app/configurator/components/chart-configurator.tsx index 6963c3d29..5178e793d 100644 --- a/app/configurator/components/chart-configurator.tsx +++ b/app/configurator/components/chart-configurator.tsx @@ -40,6 +40,7 @@ import { ChartConfig, ConfiguratorStateConfiguringChart, ConfiguratorStatePublishing, + DashboardFiltersConfig, DataSource, Filters, getChartConfig, @@ -629,6 +630,7 @@ export const ChartConfigurator = ({ @@ -755,6 +757,7 @@ export const ChartConfigurator = ({ { - const { dataSource, chartConfig, dimensions, measures } = props; + const { dataSource, chartConfig, dashboardFilters, dimensions, measures } = + props; const components = [...(dimensions ?? []), ...(measures ?? [])]; - const queryFilters = useQueryFilters({ chartConfig }); + const queryFilters = useQueryFilters({ chartConfig, dashboardFilters }); const locale = useLocale(); const [{ data: observationsData }] = useDataCubesObservationsQuery({ variables: { diff --git a/app/configurator/components/chart-options-selector.tsx b/app/configurator/components/chart-options-selector.tsx index 8f9b5d170..f134f79ba 100644 --- a/app/configurator/components/chart-options-selector.tsx +++ b/app/configurator/components/chart-options-selector.tsx @@ -121,7 +121,10 @@ export const ChartOptionsSelector = ({ }); const dimensions = componentsData?.dataCubesComponents.dimensions; const measures = componentsData?.dataCubesComponents.measures; - const queryFilters = useQueryFilters({ chartConfig }); + const queryFilters = useQueryFilters({ + chartConfig, + dashboardFilters: state.dashboardFilters, + }); const [{ data: observationsData, fetching: fetchingObservations }] = useDataCubesObservationsQuery({ variables: { From bec48ee0ce3437fe3edaed7132d627bc59e6a6d3 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 25 Jul 2024 11:00:03 +0200 Subject: [PATCH 05/22] fix: Accessing with value --- app/configurator/components/layout-configurator.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index b3ac2425e..00265b8f7 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -194,7 +194,7 @@ const LayoutSharedFiltersConfigurator = () => { timeFormatUnit, }); - const from = options.sortedOptions[0].date; + const from = options.sortedOptions[0]?.date; const to = options.sortedOptions.at(-1)?.date; const formatDate = timeUnitToFormatter[combinedDimension.timeUnit]; @@ -266,7 +266,8 @@ const LayoutSharedFiltersConfigurator = () => { - {timeRange ? ( + {/* TODO: allow TemporalOrdinalDimensions to work here */} + {timeRange && combinedDimension.values.length ? ( <> Date: Thu, 25 Jul 2024 11:00:11 +0200 Subject: [PATCH 06/22] chore: Remove console.log --- app/utils/uniqueMapBy.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/utils/uniqueMapBy.ts b/app/utils/uniqueMapBy.ts index 39c815a88..16d453721 100644 --- a/app/utils/uniqueMapBy.ts +++ b/app/utils/uniqueMapBy.ts @@ -2,9 +2,7 @@ export const uniqueMapBy = (arr: T[], keyFn: (t: T) => K) => { const res = new Map(); for (const item of arr) { const key = keyFn(item); - if (res.has(key)) { - console.log(`uniqueMapBy: duplicate detected ${key}, ignoring it`); - } else { + if (!res.has(key)) { res.set(key, item); } } From 1c42987bb211d8306ee42793e0129a6d4d9b6a92 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 25 Jul 2024 11:25:09 +0200 Subject: [PATCH 07/22] fix: useSyncInteractiveFilters --- .../use-sync-interactive-filters.spec.tsx | 5 ++- .../shared/use-sync-interactive-filters.tsx | 34 ++++++++++++------- app/components/chart-with-filters.tsx | 3 +- .../dashboard-interactive-filters.tsx | 2 +- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/app/charts/shared/use-sync-interactive-filters.spec.tsx b/app/charts/shared/use-sync-interactive-filters.spec.tsx index 8b1e917ca..f9b88fb81 100644 --- a/app/charts/shared/use-sync-interactive-filters.spec.tsx +++ b/app/charts/shared/use-sync-interactive-filters.spec.tsx @@ -101,7 +101,10 @@ const setup = ({ calculation: d.calculation, })); const [useModified, setUseModified] = useState(false); - useSyncInteractiveFilters(useModified ? modifiedChartConfig : chartConfig); + useSyncInteractiveFilters( + useModified ? modifiedChartConfig : chartConfig, + undefined + ); return (
diff --git a/app/charts/shared/use-sync-interactive-filters.tsx b/app/charts/shared/use-sync-interactive-filters.tsx index d4c5c37c7..0b80e02a6 100644 --- a/app/charts/shared/use-sync-interactive-filters.tsx +++ b/app/charts/shared/use-sync-interactive-filters.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo } from "react"; import { ChartConfig, + DashboardFiltersConfig, FilterValueSingle, isSegmentInConfig, useChartConfigFilters, @@ -18,7 +19,10 @@ import { useChartInteractiveFilters } from "@/stores/interactive-filters"; * inside the interactive filters * */ -const useSyncInteractiveFilters = (chartConfig: ChartConfig) => { +const useSyncInteractiveFilters = ( + chartConfig: ChartConfig, + dashboardFilters: DashboardFiltersConfig | undefined +) => { const { interactiveFiltersConfig } = chartConfig; const filters = useChartConfigFilters(chartConfig); const resetCategories = useChartInteractiveFilters((d) => d.resetCategories); @@ -52,23 +56,27 @@ const useSyncInteractiveFilters = (chartConfig: ChartConfig) => { // Data Filters const componentIris = interactiveFiltersConfig?.dataFilters.componentIris; + const dashboardComponentIris = dashboardFilters?.dataFilters.componentIris; useEffect(() => { if (componentIris) { // If dimension is already in use as interactive filter, use it, - // otherwise, default to editor config filter dimension value. - const newInteractiveDataFilters = componentIris.reduce<{ - [key: string]: FilterValueSingle; - }>((obj, iri) => { - const configFilter = filters[iri]; + // otherwise, default to editor config filter dimension value (only + // if dashboard filters are not set). + const newInteractiveDataFilters = componentIris + .concat(dashboardComponentIris ?? []) + .reduce<{ + [key: string]: FilterValueSingle; + }>((obj, iri) => { + const configFilter = filters[iri]; - if (Object.keys(dataFilters).includes(iri)) { - obj[iri] = dataFilters[iri]; - } else if (configFilter?.type === "single") { - obj[iri] = configFilter; - } + if (Object.keys(dataFilters).includes(iri)) { + obj[iri] = dataFilters[iri]; + } else if (configFilter?.type === "single") { + obj[iri] = configFilter; + } - return obj; - }, {}); + return obj; + }, {}); setDataFilters(newInteractiveDataFilters); } diff --git a/app/components/chart-with-filters.tsx b/app/components/chart-with-filters.tsx index 8629c365b..b1d778725 100644 --- a/app/components/chart-with-filters.tsx +++ b/app/components/chart-with-filters.tsx @@ -170,9 +170,8 @@ export const ChartWithFilters = forwardRef< HTMLDivElement, ChartWithFiltersProps >((props, ref) => { - useSyncInteractiveFilters(props.chartConfig); + useSyncInteractiveFilters(props.chartConfig, props.dashboardFilters); const classes = useStyles(); - return (
diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index ec0799d57..2f9bde21c 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -383,7 +383,7 @@ const DataFilter = ({ componentIri }: { componentIri: string }) => { } }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dimension]); + }, [dimension?.values]); const disabled = !dimension?.values.length; const hierarchy = dimension?.hierarchy; From 24df881f0243961c4d1a9b0ce699961a154a9aa5 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 25 Jul 2024 11:29:00 +0200 Subject: [PATCH 08/22] feat: Disable reloading of interactive filters when dashboard filters are active --- app/charts/shared/chart-data-filters.tsx | 26 ++++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/app/charts/shared/chart-data-filters.tsx b/app/charts/shared/chart-data-filters.tsx index a9e6265c3..ad61a5e3f 100644 --- a/app/charts/shared/chart-data-filters.tsx +++ b/app/charts/shared/chart-data-filters.tsx @@ -116,6 +116,7 @@ export const useChartDataFiltersState = ({ dataSource, chartConfig, preparedFilters, + dashboardFilters, }); return { open, @@ -539,20 +540,19 @@ export const DataFilterTemporalDimension = ({ ); }; -type EnsurePossibleInteractiveFiltersProps = { - dataSource: DataSource; - chartConfig: ChartConfig; - preparedFilters?: PreparedFilter[]; -}; - /** * This runs every time the state changes and it ensures that the selected interactive * filters return at least 1 observation. Otherwise they are reloaded. + * + * This behavior is disabled when the dashboard filters are active. */ -const useEnsurePossibleInteractiveFilters = ( - props: EnsurePossibleInteractiveFiltersProps -) => { - const { dataSource, chartConfig, preparedFilters } = props; +const useEnsurePossibleInteractiveFilters = (props: { + dataSource: DataSource; + chartConfig: ChartConfig; + dashboardFilters: DashboardFiltersConfig | undefined; + preparedFilters?: PreparedFilter[]; +}) => { + const { dataSource, chartConfig, dashboardFilters, preparedFilters } = props; const [, dispatch] = useConfiguratorState(); const loadingState = useLoadingState(); const [error, setError] = useState(); @@ -569,7 +569,10 @@ const useEnsurePossibleInteractiveFilters = ( useEffect(() => { const run = async () => { - if (!filtersByCubeIri) { + if ( + !filtersByCubeIri || + dashboardFilters?.dataFilters.componentIris.length + ) { return; } @@ -667,6 +670,7 @@ const useEnsurePossibleInteractiveFilters = ( loadingState, filtersByCubeIri, getInteractiveFiltersState, + dashboardFilters?.dataFilters.componentIris.length, ]); return { error }; From 966810b3e07f3d2c884e05f3c43ae22a64b69072 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 25 Jul 2024 12:07:10 +0200 Subject: [PATCH 09/22] feat: Sort dashboard categorical filters --- .../components/layout-configurator.tsx | 17 +++++++++++++++-- app/configurator/configurator-state/actions.tsx | 6 ++++++ app/configurator/configurator-state/reducer.tsx | 6 ++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/app/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index 00265b8f7..4908ada4e 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -225,9 +225,22 @@ const LayoutSharedFiltersConfigurator = () => { (checked: boolean, componentIri: string) => { if (checked) { dispatch({ - type: "DASHBOARD_DATA_FILTER_ADD", + type: "DASHBOARD_DATA_FILTERS_SET", value: { - dimensionIri: componentIri, + componentIris: dataFilters?.componentIris + ? [...dataFilters.componentIris, componentIri].sort((a, b) => { + const dimensions = data?.dataCubesComponents.dimensions ?? []; + const aIndex = + dimensions.find((d) => d.iri === a)?.order ?? + dimensions.findIndex((d) => d.iri === a) ?? + 0; + const bIndex = + dimensions.find((d) => d.iri === b)?.order ?? + dimensions.findIndex((d) => d.iri === b) ?? + 0; + return aIndex - bIndex; + }) + : [componentIri], }, }); } else { diff --git a/app/configurator/configurator-state/actions.tsx b/app/configurator/configurator-state/actions.tsx index d08d4a5eb..6a92ab8fc 100644 --- a/app/configurator/configurator-state/actions.tsx +++ b/app/configurator/configurator-state/actions.tsx @@ -282,6 +282,12 @@ export type ConfiguratorStateAction = dimensionIri: string; }; } + | { + type: "DASHBOARD_DATA_FILTERS_SET"; + value: { + componentIris: string[]; + }; + } | { type: "DASHBOARD_DATA_FILTER_REMOVE"; value: { diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index 601f17df0..9baf2148f 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -1102,6 +1102,12 @@ const reducer_: Reducer = ( } return draft; + case "DASHBOARD_DATA_FILTERS_SET": + if (isLayouting(draft) && draft.dashboardFilters) { + draft.dashboardFilters.dataFilters = action.value; + } + return draft; + case "DASHBOARD_DATA_FILTER_REMOVE": if (isLayouting(draft) && draft.dashboardFilters) { const { dimensionIri } = action.value; From 5a7f5acb5a6ea3ce82b4b826cd91818f0c59ec94 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Mon, 29 Jul 2024 12:59:41 +0200 Subject: [PATCH 10/22] feat: Add a way to set override dashboard filters --- .../shared/use-sync-interactive-filters.tsx | 12 ++- .../dashboard-interactive-filters.tsx | 37 ++++++-- app/config-types.ts | 1 + .../components/add-dataset-dialog.mock.ts | 1 + .../components/chart-configurator.tsx | 31 ++++--- app/configurator/components/field.tsx | 37 ++++---- .../components/layout-configurator.tsx | 85 +++++++++++++------ app/configurator/config-form.tsx | 16 ++-- .../configurator-state/actions.tsx | 8 +- .../configurator-state/initial.tsx | 1 + app/configurator/configurator-state/mocks.ts | 2 + .../configurator-state/reducer.tsx | 19 ++++- app/docs/charts.stories.tsx | 2 + app/docs/fixtures.ts | 1 + app/docs/lines.stories.tsx | 1 + app/utils/chart-config/versioning.ts | 1 + 16 files changed, 171 insertions(+), 84 deletions(-) diff --git a/app/charts/shared/use-sync-interactive-filters.tsx b/app/charts/shared/use-sync-interactive-filters.tsx index 0b80e02a6..d7196ddcb 100644 --- a/app/charts/shared/use-sync-interactive-filters.tsx +++ b/app/charts/shared/use-sync-interactive-filters.tsx @@ -68,8 +68,11 @@ const useSyncInteractiveFilters = ( [key: string]: FilterValueSingle; }>((obj, iri) => { const configFilter = filters[iri]; + const dashboardFilter = dashboardFilters?.dataFilters.filters[iri]; - if (Object.keys(dataFilters).includes(iri)) { + if (dashboardFilter?.type === "single") { + obj[iri] = dashboardFilter; + } else if (Object.keys(dataFilters).includes(iri)) { obj[iri] = dataFilters[iri]; } else if (configFilter?.type === "single") { obj[iri] = configFilter; @@ -81,7 +84,12 @@ const useSyncInteractiveFilters = ( setDataFilters(newInteractiveDataFilters); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [componentIris, setDataFilters]); + }, [ + componentIris, + dashboardComponentIris, + dashboardFilters?.dataFilters.filters, + setDataFilters, + ]); const changes = useFilterChanges(filters); useEffect(() => { diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index 2f9bde21c..9ec1e6916 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -277,15 +277,16 @@ const DashboardDataFilters = ({ const DataFilter = ({ componentIri }: { componentIri: string }) => { const locale = useLocale(); const classes = useDataFilterStyles(); - const [state] = useConfiguratorState(hasChartConfigs); + const [{ chartConfigs, dataSource, dashboardFilters }] = + useConfiguratorState(hasChartConfigs); const dashboardInteractiveFilters = useDashboardInteractiveFilters(); - const chartConfigs = state.chartConfigs.filter((config) => + const relevantChartConfigs = chartConfigs.filter((config) => config.cubes.some((cube) => Object.keys(cube.filters).includes(componentIri) ) ); const cubeIris = uniq( - state.chartConfigs.flatMap((config) => + chartConfigs.flatMap((config) => config.cubes .filter((cube) => Object.keys(cube.filters).includes(componentIri)) .map((cube) => cube.iri) @@ -302,8 +303,8 @@ const DataFilter = ({ componentIri }: { componentIri: string }) => { const [{ data }] = useDataCubesComponentsQuery({ variables: { - sourceType: state.dataSource.type, - sourceUrl: state.dataSource.url, + sourceType: dataSource.type, + sourceUrl: dataSource.url, locale, cubeFilters: [ { iri: cubeIri, componentIris: [componentIri], loadValues: true }, @@ -328,7 +329,9 @@ const DataFilter = ({ componentIri }: { componentIri: string }) => { for (const [chartKey, [_getState, _useStore, store]] of Object.entries( dashboardInteractiveFilters.stores )) { - if (chartConfigs.map((config) => config.key).includes(chartKey)) { + if ( + relevantChartConfigs.map((config) => config.key).includes(chartKey) + ) { store.setState({ dataFilters: { ...store.getState().dataFilters, @@ -343,6 +346,16 @@ const DataFilter = ({ componentIri }: { componentIri: string }) => { } ); + // Sync the interactive filter value with the config value + useEffect(() => { + const value = dashboardFilters?.dataFilters.filters[componentIri].value as + | string + | undefined; + if (value) { + handleChange({ target: { value } }); + } + }, [componentIri, handleChange, dashboardFilters?.dataFilters.filters]); + // Store the state of the stores to restore it when the component is unmounted const storesRef = useRef<{ [chartKey: string]: InteractiveFiltersState }>(); useEffect(() => { @@ -354,7 +367,13 @@ const DataFilter = ({ componentIri }: { componentIri: string }) => { ) ); - if (dimension?.values.length) { + const value = dashboardFilters?.dataFilters.filters[componentIri]?.value as + | string + | undefined; + + if (value) { + handleChange({ target: { value } } as ChangeEvent); + } else if (dimension?.values.length) { handleChange({ target: { value: dimension.values[0].value as string }, } as ChangeEvent); @@ -366,7 +385,9 @@ const DataFilter = ({ componentIri }: { componentIri: string }) => { for (const [chartKey, [_getState, _useStore, store]] of Object.entries( dashboardInteractiveFilters.stores )) { - if (chartConfigs.map((config) => config.key).includes(chartKey)) { + if ( + relevantChartConfigs.map((config) => config.key).includes(chartKey) + ) { const dataFilters = store.getState().dataFilters; const refStore = storesRefCurrent[chartKey]; if (refStore) { diff --git a/app/config-types.ts b/app/config-types.ts index b0c6828d7..69a38ca9f 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -1156,6 +1156,7 @@ export type DashboardTimeRangeFilter = t.TypeOf< const DashboardDataFiltersConfig = t.type({ componentIris: t.array(t.string), + filters: SingleFilters, }); export type DashboardDataFiltersConfig = t.TypeOf< typeof DashboardDataFiltersConfig diff --git a/app/configurator/components/add-dataset-dialog.mock.ts b/app/configurator/components/add-dataset-dialog.mock.ts index 9bd54d840..0695372f6 100644 --- a/app/configurator/components/add-dataset-dialog.mock.ts +++ b/app/configurator/components/add-dataset-dialog.mock.ts @@ -111,6 +111,7 @@ export const photovoltaikChartStateMock: ConfiguratorStateConfiguringChart = { }, dataFilters: { componentIris: [], + filters: {}, }, }, }; diff --git a/app/configurator/components/chart-configurator.tsx b/app/configurator/components/chart-configurator.tsx index 5178e793d..696e0be89 100644 --- a/app/configurator/components/chart-configurator.tsx +++ b/app/configurator/components/chart-configurator.tsx @@ -99,24 +99,23 @@ import useEvent from "@/utils/use-event"; import { FiltersBadge } from "./badges"; import { DatasetsControlSection } from "./dataset-control-section"; -type DataFilterSelectGenericProps = { +export const DataFilterSelectGeneric = ({ + rawDimension, + filterDimensionIris, + index, + disabled, + onRemove, + sideControls, + disableLabel, +}: { rawDimension: Dimension; filterDimensionIris: string[]; index: number; disabled?: boolean; - onRemove: () => void; + onRemove?: () => void; sideControls?: React.ReactNode; -}; - -const DataFilterSelectGeneric = (props: DataFilterSelectGenericProps) => { - const { - rawDimension, - filterDimensionIris, - index, - disabled, - onRemove, - sideControls, - } = props; + disableLabel?: boolean; +}) => { const locale = useLocale(); const [state] = useConfiguratorState(); const chartConfig = getChartConfig(state); @@ -157,7 +156,7 @@ const DataFilterSelectGeneric = (props: DataFilterSelectGenericProps) => { const sharedProps = { dimension, - label: ( + label: disableLabel ? null : ( {`${index + 1}. ${dimension.label}`} @@ -423,7 +422,7 @@ const useFilterReorder = ({ onAddDimensionFilter?.(); const filterValue = dimension.values[0]; dispatch({ - type: "CHART_CONFIG_FILTER_SET_SINGLE", + type: "FILTER_SET_SINGLE", value: { filters: dimensionToFieldProps(dimension), value: `${filterValue.value}`, @@ -433,7 +432,7 @@ const useFilterReorder = ({ const handleRemoveDimensionFilter = useEvent((dimension: Dimension) => { dispatch({ - type: "CHART_CONFIG_FILTER_REMOVE_SINGLE", + type: "FILTER_REMOVE_SINGLE", value: { filters: dimensionToFieldProps(dimension), }, diff --git a/app/configurator/components/field.tsx b/app/configurator/components/field.tsx index 3777000c5..b8975c1fb 100644 --- a/app/configurator/components/field.tsx +++ b/app/configurator/components/field.tsx @@ -335,16 +335,6 @@ export const MostRecentDateSwitch = (props: MostRecentDateSwitchProps) => { ); }; -type DataFilterTemporalProps = { - label: React.ReactNode; - dimension: TemporalDimension; - timeUnit: DatePickerTimeUnit; - disabled?: boolean; - isOptional?: boolean; - topControls?: React.ReactNode; - sideControls?: React.ReactNode; -}; - export const dimensionToFieldProps = (dim: Component) => { return isJoinByComponent(dim) ? dim.originalIris.map((o) => pick(o, ["cubeIri", "dimensionIri"])) @@ -356,16 +346,23 @@ export const dimensionToFieldProps = (dim: Component) => { ]; }; -export const DataFilterTemporal = (props: DataFilterTemporalProps) => { - const { - label: _label, - dimension, - timeUnit, - disabled, - isOptional, - topControls, - sideControls, - } = props; +export const DataFilterTemporal = ({ + label: _label, + dimension, + timeUnit, + disabled, + isOptional, + topControls, + sideControls, +}: { + label: React.ReactNode; + dimension: TemporalDimension; + timeUnit: DatePickerTimeUnit; + disabled?: boolean; + isOptional?: boolean; + topControls?: React.ReactNode; + sideControls?: React.ReactNode; +}) => { const { values, timeFormat } = dimension; const formatLocale = useTimeFormatLocale(); const formatDate = formatLocale.format(timeFormat); diff --git a/app/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index 4908ada4e..9ece9f65f 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -23,6 +23,7 @@ import { LayoutDashboard, } from "@/config-types"; import { LayoutAnnotator } from "@/configurator/components/annotators"; +import { DataFilterSelectGeneric } from "@/configurator/components/chart-configurator"; import { ControlSection, ControlSectionContent, @@ -120,18 +121,21 @@ const LayoutSharedFiltersConfigurator = () => { const { timeRange, dataFilters } = state.dashboardFilters ?? {}; const locale = useLocale(); - const [{ data }] = useConfigsCubeComponents({ + const [{ data, fetching }] = useConfigsCubeComponents({ variables: { state, locale: locale, }, }); + const dimensions = useMemo( + () => data?.dataCubesComponents.dimensions ?? [], + [data?.dataCubesComponents.dimensions] + ); const formatLocale = useTimeFormatLocale(); const timeFormatUnit = useTimeFormatUnit(); const combinedDimension = useMemo(() => { - const dimensions = data?.dataCubesComponents.dimensions ?? []; const timeUnitDimensions = dimensions.filter( (dimension) => isTemporalDimensionWithTimeUnit(dimension) && @@ -179,11 +183,7 @@ const LayoutSharedFiltersConfigurator = () => { }; return combinedDimension; - }, [ - data?.dataCubesComponents.dimensions, - formatLocale, - potentialTimeRangeFilterIris, - ]); + }, [dimensions, formatLocale, potentialTimeRangeFilterIris]); const handleTimeRangeFilterToggle: SwitchProps["onChange"] = useEventCallback( (_, checked) => { @@ -229,7 +229,6 @@ const LayoutSharedFiltersConfigurator = () => { value: { componentIris: dataFilters?.componentIris ? [...dataFilters.componentIris, componentIri].sort((a, b) => { - const dimensions = data?.dataCubesComponents.dimensions ?? []; const aIndex = dimensions.find((d) => d.iri === a)?.order ?? dimensions.findIndex((d) => d.iri === a) ?? @@ -241,6 +240,17 @@ const LayoutSharedFiltersConfigurator = () => { return aIndex - bIndex; }) : [componentIri], + filters: dataFilters?.filters ?? {}, + }, + }); + const value = dimensions.find((d) => d.iri === componentIri)?.values[0] + .value as string; + dispatch({ + type: "FILTER_SET_SINGLE", + value: { + // FIXME: shared filters should be scoped per cube + filters: [{ cubeIri: "", dimensionIri: componentIri }], + value, }, }); } else { @@ -250,6 +260,13 @@ const LayoutSharedFiltersConfigurator = () => { dimensionIri: componentIri, }, }); + dispatch({ + type: "FILTER_REMOVE_SINGLE", + value: { + // FIXME: shared filters should be scoped per cube + filters: [{ cubeIri: "", dimensionIri: componentIri }], + }, + }); } } ); @@ -308,34 +325,50 @@ const LayoutSharedFiltersConfigurator = () => { ) : null} {dataFilters ? ( <> - {potentialDataFilterIris.map((componentIri) => { - const dimension = data?.dataCubesComponents.dimensions.find( + {potentialDataFilterIris.map((componentIri, i) => { + const dimension = dimensions.find( (dimension) => dimension.iri === componentIri ); if (!dimension) { return null; } + const checked = dataFilters.componentIris.includes( + dimension.iri + ); return ( - - {dimension.label} - - { - handleDataFiltersToggle(checked, dimension.iri); + + > + + {dimension.label} + + { + handleDataFiltersToggle(checked, dimension.iri); + }} + /> + + {checked ? ( + + + + ) : null} ); })} diff --git a/app/configurator/config-form.tsx b/app/configurator/config-form.tsx index 8e1b47283..5acd25a73 100644 --- a/app/configurator/config-form.tsx +++ b/app/configurator/config-form.tsx @@ -451,7 +451,7 @@ export const useAddOrEditChartType = ( // Used in the configurator filters export const useSingleFilterSelect = ( - filters: GetConfiguratorStateAction<"CHART_CONFIG_FILTER_SET_SINGLE">["value"]["filters"] + filters: GetConfiguratorStateAction<"FILTER_SET_SINGLE">["value"]["filters"] ) => { const [state, dispatch] = useConfiguratorState(); const onChange = useCallback< @@ -466,14 +466,14 @@ export const useSingleFilterSelect = ( if (value === FIELD_VALUE_NONE) { dispatch({ - type: "CHART_CONFIG_FILTER_REMOVE_SINGLE", + type: "FILTER_REMOVE_SINGLE", value: { filters, }, }); } else { dispatch({ - type: "CHART_CONFIG_FILTER_SET_SINGLE", + type: "FILTER_SET_SINGLE", value: { filters, value: value === "" ? FIELD_VALUE_NONE : value, @@ -496,6 +496,12 @@ export const useSingleFilterSelect = ( value = get(cube, ["filters", dimensionIri, "value"], FIELD_VALUE_NONE); } } + } else if (isLayouting(state)) { + value = get( + state.dashboardFilters, + ["dataFilters", "filters", filters[0].dimensionIri, "value"], + FIELD_VALUE_NONE + ) as string; } return { @@ -509,14 +515,14 @@ export const useSingleFilterField = ({ filters, value, }: { - filters: GetConfiguratorStateAction<"CHART_CONFIG_FILTER_SET_SINGLE">["value"]["filters"]; + filters: GetConfiguratorStateAction<"FILTER_SET_SINGLE">["value"]["filters"]; value: string; }): FieldProps => { const [state, dispatch] = useConfiguratorState(); const onChange = useCallback<(e: ChangeEvent) => void>( (e) => { dispatch({ - type: "CHART_CONFIG_FILTER_SET_SINGLE", + type: "FILTER_SET_SINGLE", value: { filters: filters, value: e.currentTarget.value, diff --git a/app/configurator/configurator-state/actions.tsx b/app/configurator/configurator-state/actions.tsx index 6a92ab8fc..db6755326 100644 --- a/app/configurator/configurator-state/actions.tsx +++ b/app/configurator/configurator-state/actions.tsx @@ -139,7 +139,7 @@ export type ConfiguratorStateAction = }; } | { - type: "CHART_CONFIG_FILTER_SET_SINGLE"; + type: "FILTER_SET_SINGLE"; value: { filters: { cubeIri: string; @@ -149,7 +149,7 @@ export type ConfiguratorStateAction = }; } | { - type: "CHART_CONFIG_FILTER_REMOVE_SINGLE"; + type: "FILTER_REMOVE_SINGLE"; value: { filters: { cubeIri: string; @@ -284,9 +284,7 @@ export type ConfiguratorStateAction = } | { type: "DASHBOARD_DATA_FILTERS_SET"; - value: { - componentIris: string[]; - }; + value: DashboardFiltersConfig["dataFilters"]; } | { type: "DASHBOARD_DATA_FILTER_REMOVE"; diff --git a/app/configurator/configurator-state/initial.tsx b/app/configurator/configurator-state/initial.tsx index c780e641e..6a86296e8 100644 --- a/app/configurator/configurator-state/initial.tsx +++ b/app/configurator/configurator-state/initial.tsx @@ -48,6 +48,7 @@ export const getInitialConfiguringConfigBasedOnCube = (props: { }, dataFilters: { componentIris: [], + filters: {}, }, }, }; diff --git a/app/configurator/configurator-state/mocks.ts b/app/configurator/configurator-state/mocks.ts index 168201c85..9ac06b778 100644 --- a/app/configurator/configurator-state/mocks.ts +++ b/app/configurator/configurator-state/mocks.ts @@ -72,6 +72,7 @@ export const configStateMock = { }, dataFilters: { componentIris: [], + filters: {}, }, }, }, @@ -218,6 +219,7 @@ export const configStateMock = { }, dataFilters: { componentIris: [], + filters: {}, }, }, }, diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index 9baf2148f..ce745c50c 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -740,7 +740,7 @@ const reducer_: Reducer = ( return draft; - case "CHART_CONFIG_FILTER_SET_SINGLE": + case "FILTER_SET_SINGLE": if (isConfiguring(draft)) { const { filters, value } = action.value; const chartConfig = getChartConfig(draft); @@ -765,11 +765,20 @@ const reducer_: Reducer = ( ); } } + } else if (isLayouting(draft)) { + const { filters, value } = action.value; + const { dimensionIri } = filters[0]; + if (draft.dashboardFilters) { + draft.dashboardFilters.dataFilters.filters[dimensionIri] = { + type: "single", + value, + }; + } } return draft; - case "CHART_CONFIG_FILTER_REMOVE_SINGLE": + case "FILTER_REMOVE_SINGLE": if (isConfiguring(draft)) { const { filters } = action.value; const chartConfig = getChartConfig(draft); @@ -788,6 +797,12 @@ const reducer_: Reducer = ( chartConfig.interactiveFiltersConfig = newIFConfig; } } + } else if (isLayouting(draft)) { + const { filters } = action.value; + const { dimensionIri } = filters[0]; + if (draft.dashboardFilters) { + delete draft.dashboardFilters.dataFilters.filters[dimensionIri]; + } } return draft; diff --git a/app/docs/charts.stories.tsx b/app/docs/charts.stories.tsx index 76fcfaead..970c46171 100644 --- a/app/docs/charts.stories.tsx +++ b/app/docs/charts.stories.tsx @@ -79,6 +79,7 @@ const ColumnsStory = { }, dataFilters: { componentIris: [], + filters: {}, }, }, }} @@ -141,6 +142,7 @@ const ScatterplotStory = { }, dataFilters: { componentIris: [], + filters: {}, }, }, }} diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts index ec33a0659..7f80b9883 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -99,6 +99,7 @@ export const states: ConfiguratorState[] = [ }, dataFilters: { componentIris: [], + filters: {}, }, }, }, diff --git a/app/docs/lines.stories.tsx b/app/docs/lines.stories.tsx index f860b9cb5..2853da089 100644 --- a/app/docs/lines.stories.tsx +++ b/app/docs/lines.stories.tsx @@ -57,6 +57,7 @@ const LineChartStory = () => ( }, dataFilters: { componentIris: [], + filters: {}, }, }, }} diff --git a/app/utils/chart-config/versioning.ts b/app/utils/chart-config/versioning.ts index 5867ee1d5..94d24f4ac 100644 --- a/app/utils/chart-config/versioning.ts +++ b/app/utils/chart-config/versioning.ts @@ -1296,6 +1296,7 @@ export const configuratorStateMigrations: Migration[] = [ ...config.dashboardFilters, dataFilters: { componentIris: [], + filters: {}, }, }, }; From 940c2872910fede158f23ade8300b8e17397eb28 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Mon, 29 Jul 2024 13:16:25 +0200 Subject: [PATCH 11/22] fix: Maintaining the interactive filters state in tab layout --- .../shared/use-sync-interactive-filters.tsx | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/app/charts/shared/use-sync-interactive-filters.tsx b/app/charts/shared/use-sync-interactive-filters.tsx index d7196ddcb..71fc4a7b6 100644 --- a/app/charts/shared/use-sync-interactive-filters.tsx +++ b/app/charts/shared/use-sync-interactive-filters.tsx @@ -57,40 +57,59 @@ const useSyncInteractiveFilters = ( // Data Filters const componentIris = interactiveFiltersConfig?.dataFilters.componentIris; const dashboardComponentIris = dashboardFilters?.dataFilters.componentIris; - useEffect(() => { + const newPotentialInteractiveDataFilters = useMemo(() => { if (componentIris) { // If dimension is already in use as interactive filter, use it, // otherwise, default to editor config filter dimension value (only // if dashboard filters are not set). - const newInteractiveDataFilters = componentIris - .concat(dashboardComponentIris ?? []) - .reduce<{ - [key: string]: FilterValueSingle; - }>((obj, iri) => { - const configFilter = filters[iri]; - const dashboardFilter = dashboardFilters?.dataFilters.filters[iri]; - - if (dashboardFilter?.type === "single") { - obj[iri] = dashboardFilter; - } else if (Object.keys(dataFilters).includes(iri)) { - obj[iri] = dataFilters[iri]; - } else if (configFilter?.type === "single") { - obj[iri] = configFilter; - } + return componentIris.concat(dashboardComponentIris ?? []); + } + }, [componentIris, dashboardComponentIris]); - return obj; - }, {}); + useEffect(() => { + if (newPotentialInteractiveDataFilters) { + const newInteractiveDataFilters = + newPotentialInteractiveDataFilters.reduce( + (obj, iri) => { + const dashboardFilter = dashboardFilters?.dataFilters.filters[iri]; + if (dashboardFilter?.type === "single") { + obj[iri] = dashboardFilter; + } + return obj; + }, + {} as { [key: string]: FilterValueSingle } + ); setDataFilters(newInteractiveDataFilters); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - componentIris, + newPotentialInteractiveDataFilters, dashboardComponentIris, dashboardFilters?.dataFilters.filters, - setDataFilters, ]); + useEffect(() => { + if (newPotentialInteractiveDataFilters) { + const newInteractiveDataFilters = + newPotentialInteractiveDataFilters.reduce( + (obj, iri) => { + const configFilter = filters[iri]; + if (Object.keys(dataFilters).includes(iri)) { + obj[iri] = dataFilters[iri]; + } else if (configFilter?.type === "single") { + obj[iri] = configFilter; + } + return obj; + }, + {} as { [key: string]: FilterValueSingle } + ); + + setDataFilters(newInteractiveDataFilters); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [newPotentialInteractiveDataFilters, setDataFilters]); + const changes = useFilterChanges(filters); useEffect(() => { if (changes.length !== 1) { From b55a51e2bdc14c07ec3b5a4cba3a6f70ae58f78d Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Mon, 29 Jul 2024 13:27:28 +0200 Subject: [PATCH 12/22] fix: Opening of metadata panel from global interactive filters --- app/components/chart-preview.tsx | 320 ++++++++++----------- app/components/chart-published.tsx | 448 +++++++++++++++-------------- 2 files changed, 391 insertions(+), 377 deletions(-) diff --git a/app/components/chart-preview.tsx b/app/components/chart-preview.tsx index 5473aacfc..a5ec1d715 100644 --- a/app/components/chart-preview.tsx +++ b/app/components/chart-preview.tsx @@ -79,32 +79,39 @@ export const ChartPreview = (props: ChartPreviewProps) => { const [state] = useConfiguratorState(hasChartConfigs); const editing = isConfiguring(state); const { layout } = state; + const metadataPanelStore = useMemo(() => { + return createMetadataPanelStore(); + }, []); - return layout.type === "dashboard" && !editing ? ( - - ) : layout.type === "singleURLs" && !editing ? ( - - ) : ( - // Important to keep the key here to force re-rendering of the chart when - // we switch tabs in the configurator, otherwise we end up with the wrong - // data in the downstream hooks (useDataCubesMetadataQuery, etc.) - <> - {!isConfiguring(state) ? ( - x.key).join(",")} - sx={{ mb: 4 }} - /> - ) : null} - - - - - - + return ( + + {layout.type === "dashboard" && !editing ? ( + + ) : layout.type === "singleURLs" && !editing ? ( + + ) : ( + // Important to keep the key here to force re-rendering of the chart when + // we switch tabs in the configurator, otherwise we end up with the wrong + // data in the downstream hooks (useDataCubesMetadataQuery, etc.) + <> + {!isConfiguring(state) ? ( + x.key).join(",")} + sx={{ mb: 4 }} + /> + ) : null} + + + + + + + )} + ); }; @@ -425,86 +432,47 @@ const ChartPreviewInner = (props: ChartPreviewInnerProps) => { return [...dimensions, ...measures]; }, [dimensions, measures]); - const metadataPanelStore = useMemo(() => { - return createMetadataPanelStore(); - }, []); return ( - - - {props.children} - - {hasChartConfigs(state) && ( - <> - - - {!chartConfig.meta.title[locale] - ? // FIXME: adapt to design - metadata?.dataCubesMetadata.map((d) => d.title).join(", ") - : chartConfig.meta.title[locale]}{" "} - - visualize.admin.ch - - - - + {props.children} + + {hasChartConfigs(state) && ( + <> + + + {!chartConfig.meta.title[locale] + ? // FIXME: adapt to design + metadata?.dataCubesMetadata.map((d) => d.title).join(", ") + : chartConfig.meta.title[locale]}{" "} + - visualize.admin.ch + + + + + - - {configuring || chartConfig.meta.title[locale] ? ( - - dispatch({ - type: "CHART_ACTIVE_FIELD_CHANGED", - value: "title", - }) - : undefined - } - /> - ) : ( - // We need to have a span here to keep the space between the - // title and the chart (subgrid layout) - <span style={{ height: 1 }} /> - )} - <Box - sx={{ - display: "flex", - alignItems: "center", - gap: 2, - mt: "-0.33rem", - }} - > - <ChartMoreButton chartKey={chartConfig.key} /> - {actionElementSlot} - </Box> - </Flex> - {configuring || chartConfig.meta.description[locale] ? ( - <Description - text={chartConfig.meta.description[locale]} + {configuring || chartConfig.meta.title[locale] ? ( + <Title + text={chartConfig.meta.title[locale]} lighterColor smaller={state.layout.type === "dashboard"} onClick={ configuring - ? () => { + ? () => dispatch({ type: "CHART_ACTIVE_FIELD_CHANGED", - value: "description", - }); - } + value: "title", + }) : undefined } /> @@ -513,72 +481,104 @@ const ChartPreviewInner = (props: ChartPreviewInnerProps) => { // title and the chart (subgrid layout) <span style={{ height: 1 }} /> )} - <Box sx={{ mt: 4 }}> - {metadata?.dataCubesMetadata.some( - (d) => - d.publicationStatus === DataCubePublicationStatus.Draft - ) && ( - <Box sx={{ mb: 4 }}> - <HintYellow> - <Trans id="dataset.publicationStatus.draft.warning"> - Careful, this dataset is only a draft. - <br /> - <strong>Don't use for reporting!</strong> - </Trans> - </HintYellow> - </Box> - )} - </Box> - <ChartControls - dataSource={dataSource} - chartConfig={chartConfig} - dashboardFilters={state.dashboardFilters} - metadataPanelProps={{ - components: allComponents, - top: BANNER_MARGIN_TOP, - }} - /> - <div - ref={containerRef} - style={{ - minWidth: 0, - height: containerHeight, - paddingTop: 16, - flexGrow: 1, + <Box + sx={{ + display: "flex", + alignItems: "center", + gap: 2, + mt: "-0.33rem", }} > - {isTable ? ( - <DataSetTable - dataSource={dataSource} - chartConfig={chartConfig} - dashboardFilters={state.dashboardFilters} - sx={{ width: "100%", maxHeight: "100%" }} - /> - ) : ( - <ChartWithFilters - dataSource={dataSource} - componentIris={componentIris} - chartConfig={chartConfig} - dashboardFilters={state.dashboardFilters} - /> - )} - </div> - <ChartFootnotes - dataSource={dataSource} - chartConfig={chartConfig} - dashboardFilters={state.dashboardFilters} - components={allComponents} + <ChartMoreButton chartKey={chartConfig.key} /> + {actionElementSlot} + </Box> + </Flex> + {configuring || chartConfig.meta.description[locale] ? ( + <Description + text={chartConfig.meta.description[locale]} + lighterColor + smaller={state.layout.type === "dashboard"} + onClick={ + configuring + ? () => { + dispatch({ + type: "CHART_ACTIVE_FIELD_CHANGED", + value: "description", + }); + } + : undefined + } /> - {/* Wrap in div for subgrid layout */} - <div className="debug-panel"> - <DebugPanel configurator interactiveFilters /> - </div> - </InteractiveFiltersChartProvider> - </LoadingStateProvider> - </> - )} - </ChartErrorBoundary> - </Box> - </MetadataPanelStoreContext.Provider> + ) : ( + // We need to have a span here to keep the space between the + // title and the chart (subgrid layout) + <span style={{ height: 1 }} /> + )} + <Box sx={{ mt: 4 }}> + {metadata?.dataCubesMetadata.some( + (d) => + d.publicationStatus === DataCubePublicationStatus.Draft + ) && ( + <Box sx={{ mb: 4 }}> + <HintYellow> + <Trans id="dataset.publicationStatus.draft.warning"> + Careful, this dataset is only a draft. + <br /> + <strong>Don't use for reporting!</strong> + </Trans> + </HintYellow> + </Box> + )} + </Box> + <ChartControls + dataSource={dataSource} + chartConfig={chartConfig} + dashboardFilters={state.dashboardFilters} + metadataPanelProps={{ + components: allComponents, + top: BANNER_MARGIN_TOP, + }} + /> + <div + ref={containerRef} + style={{ + minWidth: 0, + height: containerHeight, + paddingTop: 16, + flexGrow: 1, + }} + > + {isTable ? ( + <DataSetTable + dataSource={dataSource} + chartConfig={chartConfig} + dashboardFilters={state.dashboardFilters} + sx={{ width: "100%", maxHeight: "100%" }} + /> + ) : ( + <ChartWithFilters + dataSource={dataSource} + componentIris={componentIris} + chartConfig={chartConfig} + dashboardFilters={state.dashboardFilters} + /> + )} + </div> + <ChartFootnotes + dataSource={dataSource} + chartConfig={chartConfig} + dashboardFilters={state.dashboardFilters} + components={allComponents} + /> + {/* Wrap in div for subgrid layout */} + <div className="debug-panel"> + <DebugPanel configurator interactiveFilters /> + </div> + </InteractiveFiltersChartProvider> + </LoadingStateProvider> + </> + )} + </ChartErrorBoundary> + </Box> ); }; diff --git a/app/components/chart-published.tsx b/app/components/chart-published.tsx index 0503aba78..7e46e7a82 100644 --- a/app/components/chart-published.tsx +++ b/app/components/chart-published.tsx @@ -63,35 +63,50 @@ type ChartPublishedIndividualChartProps = ChartPublishInnerProps; const ChartPublishedIndividualChart = forwardRef< HTMLDivElement, ChartPublishedIndividualChartProps ->(({ dataSource, state, chartConfig, configKey, children, ...rest }, ref) => { - return ( - <ChartTablePreviewProvider key={chartConfig.key}> - <ChartWrapper - key={chartConfig.key} - layoutType={state.layout.type} - ref={ref} - chartKey={chartConfig.key} - {...rest} - > - <ChartPublishedInner +>( + ( + { + dataSource, + state, + chartConfig, + configKey, + metadataPanelStore, + children, + ...rest + }, + ref + ) => { + return ( + <ChartTablePreviewProvider key={chartConfig.key}> + <ChartWrapper key={chartConfig.key} - dataSource={dataSource} - state={state} - chartConfig={chartConfig} - configKey={configKey} + layoutType={state.layout.type} + ref={ref} + chartKey={chartConfig.key} + {...rest} > - {children} - </ChartPublishedInner> - </ChartWrapper> - </ChartTablePreviewProvider> - ); -}); + <ChartPublishedInner + key={chartConfig.key} + dataSource={dataSource} + state={state} + chartConfig={chartConfig} + configKey={configKey} + metadataPanelStore={metadataPanelStore} + > + {children} + </ChartPublishedInner> + </ChartWrapper> + </ChartTablePreviewProvider> + ); + } +); export const ChartPublished = (props: ChartPublishedProps) => { const { configKey } = props; const [state] = useConfiguratorState(isPublished); const { dataSource } = state; const locale = useLocale(); + const metadataPanelStore = useMemo(() => createMetadataPanelStore(), []); const renderChart = useCallback( (chartConfig: ChartConfig) => ( <ChartPublishedIndividualChart @@ -100,74 +115,78 @@ export const ChartPublished = (props: ChartPublishedProps) => { state={state} chartConfig={chartConfig} configKey={configKey} + metadataPanelStore={metadataPanelStore} /> ), - [configKey, dataSource, state] + [configKey, dataSource, metadataPanelStore, state] ); return ( - <InteractiveFiltersProvider chartConfigs={state.chartConfigs}> - {state.layout.type === "dashboard" ? ( - <> - <Box - sx={{ - mb: - state.layout.meta.title[locale] || - state.layout.meta.description[locale] - ? 4 - : 0, - }} - > - {state.layout.meta.title[locale] && ( - <Title text={state.layout.meta.title[locale]} /> - )} - {state.layout.meta.description[locale] && ( - <Description text={state.layout.meta.description[locale]} /> - )} - </Box> + <MetadataPanelStoreContext.Provider value={metadataPanelStore}> + <InteractiveFiltersProvider chartConfigs={state.chartConfigs}> + {state.layout.type === "dashboard" ? ( + <> + <Box + sx={{ + mb: + state.layout.meta.title[locale] || + state.layout.meta.description[locale] + ? 4 + : 0, + }} + > + {state.layout.meta.title[locale] && ( + <Title text={state.layout.meta.title[locale]} /> + )} + {state.layout.meta.description[locale] && ( + <Description text={state.layout.meta.description[locale]} /> + )} + </Box> - <ChartPanelLayout - layoutType={state.layout.layout} - chartConfigs={state.chartConfigs} - renderChart={renderChart} - /> - </> - ) : ( - <> - <Flex - sx={{ - flexDirection: "column", - mb: - state.layout.meta.title[locale] || - state.layout.meta.description[locale] - ? 4 - : 0, - }} - > - {state.layout.meta.title[locale] && ( - <Title text={state.layout.meta.title[locale]} /> - )} - {state.layout.meta.description[locale] && ( - <Description text={state.layout.meta.description[locale]} /> - )} - </Flex> - <ChartTablePreviewProvider> - <DashboardInteractiveFilters sx={{ mb: 4 }} /> - <ChartWrapper - layoutType={state.layout.type} - chartKey={state.activeChartKey} + <ChartPanelLayout + layoutType={state.layout.layout} + chartConfigs={state.chartConfigs} + renderChart={renderChart} + /> + </> + ) : ( + <> + <Flex + sx={{ + flexDirection: "column", + mb: + state.layout.meta.title[locale] || + state.layout.meta.description[locale] + ? 4 + : 0, + }} > - <ChartPublishedInner - dataSource={dataSource} - state={state} - chartConfig={getChartConfig(state)} - configKey={configKey} - /> - </ChartWrapper> - </ChartTablePreviewProvider> - </> - )} - </InteractiveFiltersProvider> + {state.layout.meta.title[locale] && ( + <Title text={state.layout.meta.title[locale]} /> + )} + {state.layout.meta.description[locale] && ( + <Description text={state.layout.meta.description[locale]} /> + )} + </Flex> + <ChartTablePreviewProvider> + <DashboardInteractiveFilters sx={{ mb: 4 }} /> + <ChartWrapper + layoutType={state.layout.type} + chartKey={state.activeChartKey} + > + <ChartPublishedInner + dataSource={dataSource} + state={state} + chartConfig={getChartConfig(state)} + configKey={configKey} + metadataPanelStore={metadataPanelStore} + /> + </ChartWrapper> + </ChartTablePreviewProvider> + </> + )} + </InteractiveFiltersProvider> + </MetadataPanelStoreContext.Provider> ); }; @@ -190,6 +209,7 @@ type ChartPublishInnerProps = { configKey: string | undefined; className?: string; children?: React.ReactNode; + metadataPanelStore: ReturnType<typeof createMetadataPanelStore>; }; const ChartPublishedInnerImpl = (props: ChartPublishInnerProps) => { @@ -200,12 +220,12 @@ const ChartPublishedInnerImpl = (props: ChartPublishInnerProps) => { configKey, className, children, + metadataPanelStore, } = props; const { meta } = chartConfig; const rootRef = useRef<HTMLDivElement>(null); const { isTable, containerRef, containerHeight, computeContainerHeight } = useChartTablePreview(); - const metadataPanelStore = useMemo(() => createMetadataPanelStore(), []); const metadataPanelOpen = useStore(metadataPanelStore, (state) => state.open); const shouldShrink = useMemo(() => { const rootWidth = rootRef.current?.getBoundingClientRect().width; @@ -268,93 +288,71 @@ const ChartPublishedInnerImpl = (props: ChartPublishInnerProps) => { }, [dimensions, measures]); return ( - <MetadataPanelStoreContext.Provider value={metadataPanelStore}> - <Box - className={clsx( - chartClasses.root, - publishedChartClasses.root, - className - )} - ref={rootRef} - > - {children} - <ChartErrorBoundary resetKeys={[chartConfig]}> - <div> - {metadata?.some( - (d) => d.publicationStatus === DataCubePublicationStatus.Draft - ) && ( - <Box sx={{ mb: 4 }}> - <HintRed> - <Trans id="dataset.publicationStatus.draft.warning"> - Careful, this dataset is only a draft. - <br /> - <strong>Don't use for reporting!</strong> - </Trans> - </HintRed> - </Box> - )} - {metadata?.some((d) => d.expires) && ( - <Box sx={{ mb: 4 }}> - <HintRed> - <Trans id="dataset.publicationStatus.expires.warning"> - Careful, the data for this chart has expired. - <br /> - <strong>Don't use for reporting!</strong> - </Trans> - </HintRed> - </Box> - )} - {!isTrustedDataSource && ( - <Box sx={{ mb: 4 }}> - <HintYellow> - <Trans id="data.source.notTrusted"> - This chart is not using a trusted data source. - </Trans> - </HintYellow> - </Box> - )} - {isUsingImputation(chartConfig) && ( - <Box sx={{ mb: 4 }}> - <HintBlue> - <Trans id="dataset.hasImputedValues"> - Some data in this dataset is missing and has been - interpolated to fill the gaps. - </Trans> - </HintBlue> - </Box> - )} - </div> - <LoadingStateProvider> - <InteractiveFiltersChartProvider chartConfigKey={chartConfig.key}> - <Flex - sx={{ - height: "fit-content", - justifyContent: meta.title[locale] - ? "space-between" - : "flex-end", - gap: 2, - }} - > - {meta.title[locale] ? ( - <Title - text={meta.title[locale]} - smaller={state.layout.type === "dashboard"} - /> - ) : ( - // We need to have a span here to keep the space between the - // title and the chart (subgrid layout) - <span style={{ height: 1 }} /> - )} - <Box sx={{ mt: "-0.33rem" }}> - <ChartMoreButton - configKey={configKey} - chartKey={chartConfig.key} - /> - </Box> - </Flex> - {meta.description[locale] ? ( - <Description - text={meta.description[locale]} + <Box + className={clsx(chartClasses.root, publishedChartClasses.root, className)} + ref={rootRef} + > + {children} + <ChartErrorBoundary resetKeys={[chartConfig]}> + <div> + {metadata?.some( + (d) => d.publicationStatus === DataCubePublicationStatus.Draft + ) && ( + <Box sx={{ mb: 4 }}> + <HintRed> + <Trans id="dataset.publicationStatus.draft.warning"> + Careful, this dataset is only a draft. + <br /> + <strong>Don't use for reporting!</strong> + </Trans> + </HintRed> + </Box> + )} + {metadata?.some((d) => d.expires) && ( + <Box sx={{ mb: 4 }}> + <HintRed> + <Trans id="dataset.publicationStatus.expires.warning"> + Careful, the data for this chart has expired. + <br /> + <strong>Don't use for reporting!</strong> + </Trans> + </HintRed> + </Box> + )} + {!isTrustedDataSource && ( + <Box sx={{ mb: 4 }}> + <HintYellow> + <Trans id="data.source.notTrusted"> + This chart is not using a trusted data source. + </Trans> + </HintYellow> + </Box> + )} + {isUsingImputation(chartConfig) && ( + <Box sx={{ mb: 4 }}> + <HintBlue> + <Trans id="dataset.hasImputedValues"> + Some data in this dataset is missing and has been interpolated + to fill the gaps. + </Trans> + </HintBlue> + </Box> + )} + </div> + <LoadingStateProvider> + <InteractiveFiltersChartProvider chartConfigKey={chartConfig.key}> + <Flex + sx={{ + height: "fit-content", + justifyContent: meta.title[locale] + ? "space-between" + : "flex-end", + gap: 2, + }} + > + {meta.title[locale] ? ( + <Title + text={meta.title[locale]} smaller={state.layout.type === "dashboard"} /> ) : ( @@ -362,55 +360,71 @@ const ChartPublishedInnerImpl = (props: ChartPublishInnerProps) => { // title and the chart (subgrid layout) <span style={{ height: 1 }} /> )} - <ChartControls - dataSource={dataSource} - chartConfig={chartConfig} - dashboardFilters={state.dashboardFilters} - metadataPanelProps={{ - components: allComponents, - container: rootRef.current, - allowMultipleOpen: true, - }} + <Box sx={{ mt: "-0.33rem" }}> + <ChartMoreButton + configKey={configKey} + chartKey={chartConfig.key} + /> + </Box> + </Flex> + {meta.description[locale] ? ( + <Description + text={meta.description[locale]} + smaller={state.layout.type === "dashboard"} /> + ) : ( + // We need to have a span here to keep the space between the + // title and the chart (subgrid layout) + <span style={{ height: 1 }} /> + )} + <ChartControls + dataSource={dataSource} + chartConfig={chartConfig} + dashboardFilters={state.dashboardFilters} + metadataPanelProps={{ + components: allComponents, + container: rootRef.current, + allowMultipleOpen: true, + }} + /> - <div - ref={containerRef} - style={{ - // TODO before merging, Align with chart-preview - minWidth: 0, - height: containerHeight, - marginTop: 16, - flexGrow: 1, - }} - > - {isTable ? ( - <DataSetTable - dataSource={dataSource} - chartConfig={chartConfig} - dashboardFilters={state.dashboardFilters} - sx={{ maxHeight: "100%" }} - /> - ) : ( - <ChartWithFilters - dataSource={dataSource} - componentIris={componentIris} - chartConfig={chartConfig} - dashboardFilters={state.dashboardFilters} - /> - )} - </div> - <ChartFootnotes - dataSource={dataSource} - chartConfig={chartConfig} - dashboardFilters={state.dashboardFilters} - components={allComponents} - showVisualizeLink={state.chartConfigs.length === 1} - /> - </InteractiveFiltersChartProvider> - </LoadingStateProvider> - </ChartErrorBoundary> - </Box> - </MetadataPanelStoreContext.Provider> + <div + ref={containerRef} + style={{ + // TODO before merging, Align with chart-preview + minWidth: 0, + height: containerHeight, + marginTop: 16, + flexGrow: 1, + }} + > + {isTable ? ( + <DataSetTable + dataSource={dataSource} + chartConfig={chartConfig} + dashboardFilters={state.dashboardFilters} + sx={{ maxHeight: "100%" }} + /> + ) : ( + <ChartWithFilters + dataSource={dataSource} + componentIris={componentIris} + chartConfig={chartConfig} + dashboardFilters={state.dashboardFilters} + /> + )} + </div> + <ChartFootnotes + dataSource={dataSource} + chartConfig={chartConfig} + dashboardFilters={state.dashboardFilters} + components={allComponents} + showVisualizeLink={state.chartConfigs.length === 1} + /> + </InteractiveFiltersChartProvider> + </LoadingStateProvider> + </ChartErrorBoundary> + </Box> ); }; From e6f63789c249bb88ea2ea6f2f20c6da7eb2d8f98 Mon Sep 17 00:00:00 2001 From: Patrick Browne <pt.browne@gmail.com> Date: Fri, 2 Aug 2024 11:12:49 +0200 Subject: [PATCH 13/22] refactor: Extract helper --- app/charts/shared/chart-data-filters.tsx | 10 +++++----- app/config-types.ts | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/charts/shared/chart-data-filters.tsx b/app/charts/shared/chart-data-filters.tsx index ad61a5e3f..cdb8d91b1 100644 --- a/app/charts/shared/chart-data-filters.tsx +++ b/app/charts/shared/chart-data-filters.tsx @@ -17,6 +17,7 @@ import { Loading } from "@/components/hint"; import { OpenMetadataPanelWrapper } from "@/components/metadata-panel"; import SelectTree from "@/components/select-tree"; import { + areDataFiltersActive, ChartConfig, DashboardFiltersConfig, DataSource, @@ -567,12 +568,11 @@ const useEnsurePossibleInteractiveFilters = (props: { }, {}); }, [preparedFilters]); + const dataFiltersActive = areDataFiltersActive(dashboardFilters); + useEffect(() => { const run = async () => { - if ( - !filtersByCubeIri || - dashboardFilters?.dataFilters.componentIris.length - ) { + if (!filtersByCubeIri || dataFiltersActive) { return; } @@ -670,7 +670,7 @@ const useEnsurePossibleInteractiveFilters = (props: { loadingState, filtersByCubeIri, getInteractiveFiltersState, - dashboardFilters?.dataFilters.componentIris.length, + dataFiltersActive, ]); return { error }; diff --git a/app/config-types.ts b/app/config-types.ts index 69a38ca9f..f1cf91560 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -1168,6 +1168,12 @@ const DashboardFiltersConfig = t.type({ }); export type DashboardFiltersConfig = t.TypeOf<typeof DashboardFiltersConfig>; +export const areDataFiltersActive = ( + dashboardFilters: DashboardFiltersConfig | undefined +) => { + return dashboardFilters?.dataFilters.componentIris.length; +}; + const Config = t.intersection([ t.type( { From daf43d9798c886877994dae03edf06d87f814cc1 Mon Sep 17 00:00:00 2001 From: Patrick Browne <pt.browne@gmail.com> Date: Fri, 2 Aug 2024 11:20:02 +0200 Subject: [PATCH 14/22] refactor: Use fromEntries techniques to create new obj --- .../shared/use-sync-interactive-filters.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/charts/shared/use-sync-interactive-filters.tsx b/app/charts/shared/use-sync-interactive-filters.tsx index 71fc4a7b6..eb25d880f 100644 --- a/app/charts/shared/use-sync-interactive-filters.tsx +++ b/app/charts/shared/use-sync-interactive-filters.tsx @@ -10,6 +10,7 @@ import { import { parseDate } from "@/configurator/components/ui-helpers"; import { FIELD_VALUE_NONE } from "@/configurator/constants"; import useFilterChanges from "@/configurator/use-filter-changes"; +import { truthy } from "@/domain/types"; import { useChartInteractiveFilters } from "@/stores/interactive-filters"; /** @@ -68,17 +69,16 @@ const useSyncInteractiveFilters = ( useEffect(() => { if (newPotentialInteractiveDataFilters) { - const newInteractiveDataFilters = - newPotentialInteractiveDataFilters.reduce( - (obj, iri) => { + const newInteractiveDataFilters = Object.fromEntries( + Object.entries(newPotentialInteractiveDataFilters) + .map(([iri]) => { const dashboardFilter = dashboardFilters?.dataFilters.filters[iri]; - if (dashboardFilter?.type === "single") { - obj[iri] = dashboardFilter; - } - return obj; - }, - {} as { [key: string]: FilterValueSingle } - ); + return dashboardFilter?.type === "single" + ? ([iri, dashboardFilter] as const) + : null; + }) + .filter(truthy) + ); setDataFilters(newInteractiveDataFilters); } From bb9c7baf8b07324d0f2b494510adfe99146b2a21 Mon Sep 17 00:00:00 2001 From: Patrick Browne <pt.browne@gmail.com> Date: Mon, 5 Aug 2024 09:50:17 +0200 Subject: [PATCH 15/22] refactor --- .../configurator-state/context.tsx | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/app/configurator/configurator-state/context.tsx b/app/configurator/configurator-state/context.tsx index da1cebcca..344c1ba6a 100644 --- a/app/configurator/configurator-state/context.tsx +++ b/app/configurator/configurator-state/context.tsx @@ -1,7 +1,9 @@ +import { ParsedUrlQuery } from "querystring"; + import { PUBLISHED_STATE } from "@prisma/client"; import { NextRouter, useRouter } from "next/router"; import { Dispatch, createContext, useContext, useEffect, useMemo } from "react"; -import { useClient } from "urql"; +import { Client, useClient } from "urql"; import { useImmerReducer } from "use-immer"; import { @@ -155,6 +157,30 @@ const handlePublishSuccess = async ( }); }; +async function initializeChartState( + chartId: string, + query: ParsedUrlQuery, + client: Client, + dataSource: { type: "sql" | "sparql"; url: string }, + locale: string +) { + if (chartId === "new") { + if (query.copy && typeof query.copy === "string") { + return initChartStateFromChartCopy(client, query.copy); + } else if (query.edit && typeof query.edit === "string") { + return initChartStateFromChartEdit( + client, + query.edit, + typeof query.state === "string" ? query.state : undefined + ); + } else if (query.cube && typeof query.cube === "string") { + return initChartStateFromCube(client, query.cube, dataSource, locale); + } + } else if (chartId !== "published") { + return initChartStateFromLocalStorage(client, chartId); + } +} + export async function publishState( user: ReturnType<typeof useUser>, key: string, @@ -260,35 +286,17 @@ const ConfiguratorStateProviderInternal = ( let stateToInitialize = initialStateWithDataSource; const initialize = async () => { try { - let newChartState; - - if (chartId === "new") { - if (query.copy && typeof query.copy === "string") { - newChartState = await initChartStateFromChartCopy( - client, - query.copy - ); - } else if (query.edit && typeof query.edit === "string") { - newChartState = await initChartStateFromChartEdit( - client, - query.edit, - typeof query.state === "string" ? query.state : undefined - ); - } else if (query.cube && typeof query.cube === "string") { - newChartState = await initChartStateFromCube( - client, - query.cube, - dataSource, - locale - ); - } - } else if (chartId !== "published") { - newChartState = await initChartStateFromLocalStorage(client, chartId); - if (!newChartState && allowDefaultRedirect) { - replace(`/create/new`); - } - } + const newChartState = await initializeChartState( + chartId, + query, + client, + dataSource, + locale + ); + if (!newChartState && allowDefaultRedirect && chartId !== "published") { + replace(`/create/new`); + } stateToInitialize = newChartState ?? stateToInitialize; } finally { dispatch({ type: "INITIALIZED", value: stateToInitialize }); From 2db20b373c79c545e775550f85b28f8989941bf3 Mon Sep 17 00:00:00 2001 From: Patrick Browne <pt.browne@gmail.com> Date: Mon, 5 Aug 2024 15:21:50 +0200 Subject: [PATCH 16/22] refactor: Extract snapshot function --- .../dashboard-interactive-filters.tsx | 75 +++++++++++-------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index 9ec1e6916..ea58b3d44 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -9,7 +9,7 @@ import { import { Theme } from "@mui/material/styles"; import { makeStyles } from "@mui/styles"; import uniq from "lodash/uniq"; -import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react"; +import { ChangeEvent, useEffect, useMemo, useState } from "react"; import { DataFilterGenericDimension, @@ -17,6 +17,7 @@ import { DataFilterTemporalDimension, } from "@/charts/shared/chart-data-filters"; import { + ChartConfig, DashboardTimeRangeFilter, hasChartConfigs, InteractiveFiltersTimeRange, @@ -259,6 +260,44 @@ const useDataFilterStyles = makeStyles((theme: Theme) => ({ }, })); +type StoreSnapshot = { [chartKey: string]: InteractiveFiltersState }; +type Stores = ReturnType<typeof useDashboardInteractiveFilters>["stores"]; + +const saveSnapshot = (stores: Stores): StoreSnapshot => { + return Object.fromEntries( + Object.entries(stores).map(([key, [_getState, _useStore, store]]) => { + const state = store.getState(); + return [key, state]; + }) + ); +}; + +const restoreSnapshot = ( + chartConfigs: ChartConfig[], + stores: Stores, + storeSnapshot: StoreSnapshot, + componentIri: string +) => { + for (const [chartKey, [_getState, _useStore, store]] of Object.entries( + stores + )) { + if (chartConfigs.map((config) => config.key).includes(chartKey)) { + const dataFilters = store.getState().dataFilters; + const refStore = storeSnapshot[chartKey]; + if (refStore) { + const refDataFilters = refStore.dataFilters; + if (refDataFilters[componentIri]) { + dataFilters[componentIri] = refDataFilters[componentIri]; + store.setState({ dataFilters }); + } else { + delete dataFilters[componentIri]; + store.setState({ dataFilters }); + } + } + } + } +}; + const DashboardDataFilters = ({ componentIris, }: { @@ -356,16 +395,9 @@ const DataFilter = ({ componentIri }: { componentIri: string }) => { } }, [componentIri, handleChange, dashboardFilters?.dataFilters.filters]); - // Store the state of the stores to restore it when the component is unmounted - const storesRef = useRef<{ [chartKey: string]: InteractiveFiltersState }>(); useEffect(() => { - storesRef.current = Object.fromEntries( - Object.entries(dashboardInteractiveFilters.stores).map( - ([key, [_getState, _useStore, store]]) => { - return [key, store.getState()]; - } - ) - ); + const stores = dashboardInteractiveFilters.stores; + const snapshot = saveSnapshot(dashboardInteractiveFilters.stores); const value = dashboardFilters?.dataFilters.filters[componentIri]?.value as | string @@ -379,28 +411,9 @@ const DataFilter = ({ componentIri }: { componentIri: string }) => { } as ChangeEvent<HTMLSelectElement>); } - const storesRefCurrent = storesRef.current; - return () => { - for (const [chartKey, [_getState, _useStore, store]] of Object.entries( - dashboardInteractiveFilters.stores - )) { - if ( - relevantChartConfigs.map((config) => config.key).includes(chartKey) - ) { - const dataFilters = store.getState().dataFilters; - const refStore = storesRefCurrent[chartKey]; - if (refStore) { - const refDataFilters = refStore.dataFilters; - if (refDataFilters[componentIri]) { - dataFilters[componentIri] = refDataFilters[componentIri]; - store.setState({ dataFilters }); - } else { - delete dataFilters[componentIri]; - store.setState({ dataFilters }); - } - } - } + if (snapshot) { + restoreSnapshot(relevantChartConfigs, stores, snapshot, componentIri); } }; // eslint-disable-next-line react-hooks/exhaustive-deps From c88732f239e713418b80311fd717c4b33a866749 Mon Sep 17 00:00:00 2001 From: Patrick Browne <pt.browne@gmail.com> Date: Mon, 5 Aug 2024 15:28:02 +0200 Subject: [PATCH 17/22] refactor: Save snapshot directly returns restore function --- .../dashboard-interactive-filters.tsx | 56 +++++++++---------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index ea58b3d44..cac22f2b0 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -31,10 +31,7 @@ import { isTemporalDimension } from "@/domain/data"; import { useDataCubesComponentsQuery } from "@/graphql/hooks"; import { TimeUnit } from "@/graphql/query-hooks"; import { useLocale } from "@/locales/use-locale"; -import { - InteractiveFiltersState, - useDashboardInteractiveFilters, -} from "@/stores/interactive-filters"; +import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; import { useTransitionStore } from "@/stores/transition"; import { assert } from "@/utils/assert"; import useEvent from "@/utils/use-event"; @@ -260,34 +257,30 @@ const useDataFilterStyles = makeStyles((theme: Theme) => ({ }, })); -type StoreSnapshot = { [chartKey: string]: InteractiveFiltersState }; type Stores = ReturnType<typeof useDashboardInteractiveFilters>["stores"]; -const saveSnapshot = (stores: Stores): StoreSnapshot => { - return Object.fromEntries( +const saveDataFiltersSnapshot = ( + chartConfigs: ChartConfig[], + stores: Stores, + componentIri: string +) => { + const snapshot = Object.fromEntries( Object.entries(stores).map(([key, [_getState, _useStore, store]]) => { const state = store.getState(); - return [key, state]; + const filterValue = state.dataFilters[componentIri]; + return [key, filterValue]; }) ); -}; -const restoreSnapshot = ( - chartConfigs: ChartConfig[], - stores: Stores, - storeSnapshot: StoreSnapshot, - componentIri: string -) => { - for (const [chartKey, [_getState, _useStore, store]] of Object.entries( - stores - )) { - if (chartConfigs.map((config) => config.key).includes(chartKey)) { - const dataFilters = store.getState().dataFilters; - const refStore = storeSnapshot[chartKey]; - if (refStore) { - const refDataFilters = refStore.dataFilters; - if (refDataFilters[componentIri]) { - dataFilters[componentIri] = refDataFilters[componentIri]; + return () => { + for (const [chartKey, [_getState, _useStore, store]] of Object.entries( + stores + )) { + if (chartConfigs.map((config) => config.key).includes(chartKey)) { + const dataFilters = store.getState().dataFilters; + const filterValue = snapshot[chartKey]; + if (filterValue) { + dataFilters[componentIri] = filterValue; store.setState({ dataFilters }); } else { delete dataFilters[componentIri]; @@ -295,7 +288,7 @@ const restoreSnapshot = ( } } } - } + }; }; const DashboardDataFilters = ({ @@ -396,8 +389,11 @@ const DataFilter = ({ componentIri }: { componentIri: string }) => { }, [componentIri, handleChange, dashboardFilters?.dataFilters.filters]); useEffect(() => { - const stores = dashboardInteractiveFilters.stores; - const snapshot = saveSnapshot(dashboardInteractiveFilters.stores); + const restoreSnapshot = saveDataFiltersSnapshot( + relevantChartConfigs, + dashboardInteractiveFilters.stores, + componentIri + ); const value = dashboardFilters?.dataFilters.filters[componentIri]?.value as | string @@ -412,9 +408,7 @@ const DataFilter = ({ componentIri }: { componentIri: string }) => { } return () => { - if (snapshot) { - restoreSnapshot(relevantChartConfigs, stores, snapshot, componentIri); - } + restoreSnapshot(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [dimension?.values]); From 5b82112b3c3fb5fb83bed65b2c7427ba5c8d5148 Mon Sep 17 00:00:00 2001 From: Patrick Browne <pt.browne@gmail.com> Date: Mon, 5 Aug 2024 15:44:53 +0200 Subject: [PATCH 18/22] test: Add test for restorer --- .../dashboard-interactive-filters.spec.tsx | 112 ++++++++++++++++++ .../dashboard-interactive-filters.tsx | 6 +- app/stores/interactive-filters.tsx | 4 +- 3 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 app/components/dashboard-interactive-filters.spec.tsx diff --git a/app/components/dashboard-interactive-filters.spec.tsx b/app/components/dashboard-interactive-filters.spec.tsx new file mode 100644 index 000000000..b211525a3 --- /dev/null +++ b/app/components/dashboard-interactive-filters.spec.tsx @@ -0,0 +1,112 @@ +import { saveDataFiltersSnapshot } from "@/components/dashboard-interactive-filters"; +import { ChartConfig } from "@/configurator"; +import { InteractiveFiltersContextValue } from "@/stores/interactive-filters"; + +class MockState<T = unknown> { + private _state: any; + + constructor(state: T) { + this._state = state; + } + + getState() { + return this._state; + } + + setState(newState: any) { + Object.assign(this._state, newState); + } +} +const mockDashboardInteractiveFiltersContextValue = <T extends unknown>( + state: T +) => { + return [ + null, + null, + new MockState(state), + ] as unknown as InteractiveFiltersContextValue; +}; +test("Save snapshot, modify the store manually, then restore and check result", () => { + // Mock chartConfigs + const chartConfigs: ChartConfig[] = [ + { key: "chart1" } as ChartConfig, + { key: "chart2" } as ChartConfig, + { key: "chart3" } as ChartConfig, + ]; + + // Mock stores + const stores = { + chart1: mockDashboardInteractiveFiltersContextValue({ + unrelated: 1, + dataFilters: { component1: { type: "single", value: "filter1" } }, + }), + chart2: mockDashboardInteractiveFiltersContextValue({ + dataFilters: { component2: { type: "single", value: "filter2" } }, + }), + chart3: mockDashboardInteractiveFiltersContextValue({ + dataFilters: { component3: { type: "single", value: "filter3" } }, + }), + }; + + // Save the snapshot + const restoreComponent1 = saveDataFiltersSnapshot( + chartConfigs, + stores, + "component1" + ); + + const setDataFilter = ( + store: InteractiveFiltersContextValue["2"], + key: string, + value: string + ) => { + store.setState({ + dataFilters: { + ...store.getState().dataFilters, + [key]: { type: "single", value }, + }, + }); + }; + + // Manually modify the store + setDataFilter(stores.chart1[2], "component1", "modified filter"); + setDataFilter(stores.chart1[2], "component2", "modified filter 2"); + + const restoreComponent3 = saveDataFiltersSnapshot( + chartConfigs, + stores, + "component3" + ); + + setDataFilter(stores.chart1[2], "component3", "modified filter 3"); + + // Restore the snapshot + restoreComponent1(); + + // Check if the store is restored correctly + expect(stores.chart1[2].getState()).toEqual({ + unrelated: 1, + dataFilters: { + // Filter 1 is restored + component1: { type: "single", value: "filter1" }, + // Filter 2 is not restored + component2: { type: "single", value: "modified filter 2" }, + // Filter 3 is not yet restored + component3: { type: "single", value: "modified filter 3" }, + }, + }); + + restoreComponent3(); + + // Check if the store is restored correctly + expect(stores.chart1[2].getState()).toEqual({ + unrelated: 1, + dataFilters: { + // Filter 1 is restored + component1: { type: "single", value: "filter1" }, + // Filter 2 is not restored + component2: { type: "single", value: "modified filter 2" }, + // Filter 3 is not there anymore + }, + }); +}); diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index cac22f2b0..00817c8b9 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -257,9 +257,11 @@ const useDataFilterStyles = makeStyles((theme: Theme) => ({ }, })); -type Stores = ReturnType<typeof useDashboardInteractiveFilters>["stores"]; +export type Stores = ReturnType< + typeof useDashboardInteractiveFilters +>["stores"]; -const saveDataFiltersSnapshot = ( +export const saveDataFiltersSnapshot = ( chartConfigs: ChartConfig[], stores: Stores, componentIri: string diff --git a/app/stores/interactive-filters.tsx b/app/stores/interactive-filters.tsx index 126021ede..3448682cf 100644 --- a/app/stores/interactive-filters.tsx +++ b/app/stores/interactive-filters.tsx @@ -11,9 +11,9 @@ import { import { truthy } from "@/domain/types"; import { getOriginalIris, isJoinById } from "@/graphql/join"; import { + createBoundUseStoreWithSelector, ExtractState, UseBoundStoreWithSelector, - createBoundUseStoreWithSelector, } from "@/stores/utils"; import { assert } from "@/utils/assert"; @@ -134,7 +134,7 @@ const interactiveFiltersStoreCreator: StateCreator<State> = (set) => { }; }; -type InteractiveFiltersContextValue = [ +export type InteractiveFiltersContextValue = [ UseBoundStore<StoreApi<State>>["getState"], UseBoundStoreWithSelector<StoreApi<State>>, StoreApi<State>, From 0c100f91e0e931ea0dc3727c8d9298e8dc08bcf1 Mon Sep 17 00:00:00 2001 From: Patrick Browne <pt.browne@gmail.com> Date: Mon, 5 Aug 2024 15:59:19 +0200 Subject: [PATCH 19/22] refactor: Extract function closer to domain --- .../dashboard-interactive-filters.spec.tsx | 18 ++++-------------- .../dashboard-interactive-filters.tsx | 15 +++++---------- app/stores/interactive-filters.tsx | 16 +++++++++++++++- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/app/components/dashboard-interactive-filters.spec.tsx b/app/components/dashboard-interactive-filters.spec.tsx index b211525a3..a0e843ba5 100644 --- a/app/components/dashboard-interactive-filters.spec.tsx +++ b/app/components/dashboard-interactive-filters.spec.tsx @@ -1,6 +1,9 @@ import { saveDataFiltersSnapshot } from "@/components/dashboard-interactive-filters"; import { ChartConfig } from "@/configurator"; -import { InteractiveFiltersContextValue } from "@/stores/interactive-filters"; +import { + InteractiveFiltersContextValue, + setDataFilter, +} from "@/stores/interactive-filters"; class MockState<T = unknown> { private _state: any; @@ -55,19 +58,6 @@ test("Save snapshot, modify the store manually, then restore and check result", "component1" ); - const setDataFilter = ( - store: InteractiveFiltersContextValue["2"], - key: string, - value: string - ) => { - store.setState({ - dataFilters: { - ...store.getState().dataFilters, - [key]: { type: "single", value }, - }, - }); - }; - // Manually modify the store setDataFilter(stores.chart1[2], "component1", "modified filter"); setDataFilter(stores.chart1[2], "component2", "modified filter 2"); diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index 00817c8b9..5dea38c0d 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -31,7 +31,10 @@ import { isTemporalDimension } from "@/domain/data"; import { useDataCubesComponentsQuery } from "@/graphql/hooks"; import { TimeUnit } from "@/graphql/query-hooks"; import { useLocale } from "@/locales/use-locale"; -import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; +import { + setDataFilter, + useDashboardInteractiveFilters, +} from "@/stores/interactive-filters"; import { useTransitionStore } from "@/stores/transition"; import { assert } from "@/utils/assert"; import useEvent from "@/utils/use-event"; @@ -366,15 +369,7 @@ const DataFilter = ({ componentIri }: { componentIri: string }) => { if ( relevantChartConfigs.map((config) => config.key).includes(chartKey) ) { - store.setState({ - dataFilters: { - ...store.getState().dataFilters, - [componentIri]: { - type: "single", - value: newValue, - }, - }, - }); + setDataFilter(store, componentIri, newValue); } } } diff --git a/app/stores/interactive-filters.tsx b/app/stores/interactive-filters.tsx index 3448682cf..09dd6c1e1 100644 --- a/app/stores/interactive-filters.tsx +++ b/app/stores/interactive-filters.tsx @@ -134,10 +134,11 @@ const interactiveFiltersStoreCreator: StateCreator<State> = (set) => { }; }; +export type InteractiveFiltersStore = StoreApi<State>; export type InteractiveFiltersContextValue = [ UseBoundStore<StoreApi<State>>["getState"], UseBoundStoreWithSelector<StoreApi<State>>, - StoreApi<State>, + InteractiveFiltersStore, ]; const InteractiveFiltersContext = createContext< @@ -315,3 +316,16 @@ export const useDashboardInteractiveFilters = () => { return ctx; }; + +export const setDataFilter = ( + store: InteractiveFiltersStore, + key: string, + value: string +) => { + store.setState({ + dataFilters: { + ...store.getState().dataFilters, + [key]: { type: "single", value }, + }, + }); +}; From ec5a130b6bb7c1c39f21d8558733cf4a4df7318a Mon Sep 17 00:00:00 2001 From: Patrick Browne <pt.browne@gmail.com> Date: Mon, 12 Aug 2024 10:53:34 +0200 Subject: [PATCH 20/22] refactor: rename for clarity --- app/charts/shared/chart-helpers.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/charts/shared/chart-helpers.tsx b/app/charts/shared/chart-helpers.tsx index 14f5f8729..7b64251c0 100644 --- a/app/charts/shared/chart-helpers.tsx +++ b/app/charts/shared/chart-helpers.tsx @@ -25,9 +25,9 @@ import { ComboChartConfig, DashboardFiltersConfig, GenericField, - NumericalColorField, getChartConfigFilters, isComboChartConfig, + NumericalColorField, } from "@/configurator"; import { parseDate } from "@/configurator/components/ui-helpers"; import { FIELD_VALUE_NONE } from "@/configurator/constants"; @@ -35,9 +35,9 @@ import { Component, Dimension, DimensionValue, + getTemporalEntityValue, Observation, ObservationValue, - getTemporalEntityValue, } from "@/domain/data"; import { truthy } from "@/domain/types"; import { getOriginalIris, isJoinById } from "@/graphql/join"; @@ -91,7 +91,9 @@ export const useQueryFilters = ({ allowNoneValues?: boolean; componentIris?: string[]; }): DataCubeObservationFilter[] => { - const chartDataFilters = useChartInteractiveFilters((d) => d.dataFilters); + const chartInteractiveFilters = useChartInteractiveFilters( + (d) => d.dataFilters + ); return useMemo(() => { return chartConfig.cubes.map((cube) => { const cubeFilters = getChartConfigFilters(chartConfig.cubes, { @@ -103,7 +105,7 @@ export const useQueryFilters = ({ // track of interactive data filters per cube. // Only include data filters that are part of the chart config. const cubeDataFilters = Object.fromEntries( - Object.entries(chartDataFilters).filter(([key]) => + Object.entries(chartInteractiveFilters).filter(([key]) => cubeFiltersKeys.includes(key) ) ); @@ -126,7 +128,7 @@ export const useQueryFilters = ({ chartConfig.cubes, chartConfig.chartType, chartConfig.interactiveFiltersConfig, - chartDataFilters, + chartInteractiveFilters, componentIris, dashboardFilters, allowNoneValues, From f86a7c290e3a09b62e0f8c9d14c5ebcb648b8029 Mon Sep 17 00:00:00 2001 From: Patrick Browne <pt.browne@gmail.com> Date: Mon, 12 Aug 2024 10:53:48 +0200 Subject: [PATCH 21/22] refactor: Import order --- app/components/chart-preview.tsx | 6 +++--- app/configurator/config-form.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/components/chart-preview.tsx b/app/components/chart-preview.tsx index a5ec1d715..aad545527 100644 --- a/app/components/chart-preview.tsx +++ b/app/components/chart-preview.tsx @@ -11,8 +11,8 @@ import { Box } from "@mui/material"; import { makeStyles } from "@mui/styles"; import Head from "next/head"; import React, { - ReactNode, forwardRef, + ReactNode, useCallback, useMemo, useState, @@ -45,17 +45,17 @@ import Flex from "@/components/flex"; import { Checkbox } from "@/components/form"; import { HintYellow } from "@/components/hint"; import { - MetadataPanelStoreContext, createMetadataPanelStore, + MetadataPanelStoreContext, } from "@/components/metadata-panel-store"; import { BANNER_MARGIN_TOP } from "@/components/presence"; import { ChartConfig, DataSource, - Layout, getChartConfig, hasChartConfigs, isConfiguring, + Layout, useConfiguratorState, } from "@/configurator"; import { Description, Title } from "@/configurator/components/annotators"; diff --git a/app/configurator/config-form.tsx b/app/configurator/config-form.tsx index 5acd25a73..c1f5dcbcb 100644 --- a/app/configurator/config-form.tsx +++ b/app/configurator/config-form.tsx @@ -2,8 +2,8 @@ import { SelectChangeEvent, SelectProps } from "@mui/material"; import get from "lodash/get"; import React, { ChangeEvent, - InputHTMLAttributes, createContext, + InputHTMLAttributes, useCallback, useContext, useMemo, @@ -20,8 +20,8 @@ import { isComboChartConfig, } from "@/config-types"; import { - GetConfiguratorStateAction, getChartOptionField, + GetConfiguratorStateAction, getFilterValue, isConfiguring, isLayouting, @@ -33,8 +33,8 @@ import { Dimension, DimensionValue, HierarchyValue, - Measure, isMeasure, + Measure, } from "@/domain/data"; import { useLocale } from "@/locales/use-locale"; import { bfs } from "@/utils/bfs"; From e620968b657342b1eb83056a352d7d1b569d54f6 Mon Sep 17 00:00:00 2001 From: Patrick Browne <pt.browne@gmail.com> Date: Mon, 12 Aug 2024 15:23:01 +0200 Subject: [PATCH 22/22] fix: When changing dashboard config default value, cube query filters was incorrect and would still reference the previous value --- app/charts/shared/chart-helpers.spec.tsx | 79 ++++++++++++++++++- app/charts/shared/chart-helpers.tsx | 39 ++++++--- app/components/chart-with-filters.tsx | 1 + .../dashboard-interactive-filters.tsx | 2 +- 4 files changed, 106 insertions(+), 15 deletions(-) diff --git a/app/charts/shared/chart-helpers.spec.tsx b/app/charts/shared/chart-helpers.spec.tsx index 3ff57c38c..d56df31ad 100644 --- a/app/charts/shared/chart-helpers.spec.tsx +++ b/app/charts/shared/chart-helpers.spec.tsx @@ -14,7 +14,10 @@ import { } from "@/configurator"; import { FIELD_VALUE_NONE } from "@/configurator/constants"; import { mkJoinById } from "@/graphql/join"; -import { InteractiveFiltersState } from "@/stores/interactive-filters"; +import { + InteractiveFiltersState, + useChartInteractiveFilters, +} from "@/stores/interactive-filters"; import dualLine1Fixture from "@/test/__fixtures/config/dev/chartConfig-photovoltaik-und-gebaudeprogramm.json"; import map1Fixture from "@/test/__fixtures/config/int/map-nfi.json"; import line1Fixture from "@/test/__fixtures/config/prod/line-1.json"; @@ -196,6 +199,80 @@ describe("useQueryFilters", () => { }, ]); }); + + it("should handle correctly dashboard filters", () => { + (useChartInteractiveFilters as jest.Mock).mockImplementation(() => ({})); + + const chartConfig = { + chartType: "line", + interactiveFiltersConfig: { + dataFilters: { + active: true, + }, + }, + cubes: [ + { + iri: "A", + filters: { + A_1: { type: "single", value: "A_1_5" }, + A_2: { type: "single", value: "A_2_1" }, + }, + }, + { + iri: "B", + filters: { + B_1: { type: "single", value: "B_1_1" }, + }, + }, + ], + } as any as ChartConfig; + const { result: queryFilters } = renderHook< + ReturnType<typeof useQueryFilters>, + Parameters<typeof useQueryFilters>[0] + >( + (props: Parameters<typeof useQueryFilters>[0]) => useQueryFilters(props), + { + initialProps: { + chartConfig, + dashboardFilters: { + timeRange: { + active: false, + timeUnit: "ms", + presets: { + from: "2021-01-01", + to: "2021-12-31", + }, + }, + dataFilters: { + componentIris: ["A_1"], + filters: { + A_1: { type: "single", value: "A_1_Data_Filter" }, + A_2: { type: "single", value: "A_2_3" }, + }, + }, + }, + }, + } + ); + + expect(queryFilters.current).toEqual([ + { + iri: "A", + componentIris: undefined, + filters: { + A_1: { type: "single", value: "A_1_Data_Filter" }, + A_2: { type: "single", value: "A_2_1" }, + }, + joinBy: undefined, + }, + { + iri: "B", + componentIris: undefined, + filters: { B_1: { type: "single", value: "B_1_1" } }, + joinBy: undefined, + }, + ]); + }); }); describe("getChartConfigComponentIris", () => { diff --git a/app/charts/shared/chart-helpers.tsx b/app/charts/shared/chart-helpers.tsx index 7b64251c0..7616b9680 100644 --- a/app/charts/shared/chart-helpers.tsx +++ b/app/charts/shared/chart-helpers.tsx @@ -48,7 +48,7 @@ import { } from "@/stores/interactive-filters"; // Prepare filters used in data query: -// - merges publisher data filters and interactive data filters (user-defined), +// - merges publisher data filters, interactive data filters, and dashboard filters // if applicable // - removes none values since they should not be sent as part of the GraphQL query export const prepareCubeQueryFilters = ( @@ -56,16 +56,26 @@ export const prepareCubeQueryFilters = ( cubeFilters: Filters, interactiveFiltersConfig: InteractiveFiltersConfig, dashboardFiltersConfig: DashboardFiltersConfig | undefined, - cubeDataFilters: InteractiveFiltersState["dataFilters"], + interactiveDataFilters: InteractiveFiltersState["dataFilters"], allowNoneValues = false ): Filters => { const queryFilters = { ...cubeFilters }; if (chartType !== "table") { - for (const [k, v] of Object.entries(cubeDataFilters)) { + for (const [k, v] of Object.entries( + dashboardFiltersConfig?.dataFilters.filters ?? {} + )) { + if ( + k in cubeFilters && + dashboardFiltersConfig?.dataFilters.componentIris?.includes(k) + ) { + queryFilters[k] = v; + } + } + for (const [k, v] of Object.entries(interactiveDataFilters)) { if ( interactiveFiltersConfig?.dataFilters.active || - dashboardFiltersConfig?.dataFilters.componentIris.includes(k) + dashboardFiltersConfig?.dataFilters.componentIris?.includes(k) ) { queryFilters[k] = v; } @@ -94,6 +104,7 @@ export const useQueryFilters = ({ const chartInteractiveFilters = useChartInteractiveFilters( (d) => d.dataFilters ); + return useMemo(() => { return chartConfig.cubes.map((cube) => { const cubeFilters = getChartConfigFilters(chartConfig.cubes, { @@ -104,23 +115,25 @@ export const useQueryFilters = ({ // This is a bigger issue we should address in the future, probably by keeping // track of interactive data filters per cube. // Only include data filters that are part of the chart config. - const cubeDataFilters = Object.fromEntries( + const cubeInteractiveDataFilters = Object.fromEntries( Object.entries(chartInteractiveFilters).filter(([key]) => cubeFiltersKeys.includes(key) ) ); + const preparedFilters = prepareCubeQueryFilters( + chartConfig.chartType, + cubeFilters, + chartConfig.interactiveFiltersConfig, + dashboardFilters, + cubeInteractiveDataFilters, + allowNoneValues + ); + return { iri: cube.iri, componentIris, - filters: prepareCubeQueryFilters( - chartConfig.chartType, - cubeFilters, - chartConfig.interactiveFiltersConfig, - dashboardFilters, - cubeDataFilters, - allowNoneValues - ), + filters: preparedFilters, joinBy: cube.joinBy, }; }); diff --git a/app/components/chart-with-filters.tsx b/app/components/chart-with-filters.tsx index b1d778725..3ee25c806 100644 --- a/app/components/chart-with-filters.tsx +++ b/app/components/chart-with-filters.tsx @@ -86,6 +86,7 @@ const GenericChart = (props: GenericChartProps) => { dashboardFilters, componentIris, }); + const commonProps = { dataSource, observationQueryFilters, diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index 5dea38c0d..42a0a3df0 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -375,7 +375,7 @@ const DataFilter = ({ componentIri }: { componentIri: string }) => { } ); - // Sync the interactive filter value with the config value + // Syncs the interactive filter value with the config value useEffect(() => { const value = dashboardFilters?.dataFilters.filters[componentIri].value as | string