From e5fe97be002bc28d7f56cd30da3bb0f71201189f Mon Sep 17 00:00:00 2001 From: Igor Dykhta Date: Tue, 22 Oct 2024 02:01:43 +0300 Subject: [PATCH] [feat] Updated time filter sync style (#2705) * [feat] Updated time filter sync style Signed-off-by: Ihor Dykhta --- src/components/src/common/field-selector.tsx | 3 +- .../common/item-selector/item-selector.tsx | 3 +- .../filter-synced-dataset-panel.tsx | 188 +++++++++++++++++ .../filter-panels/time-range-filter-panel.tsx | 190 +++++------------- .../time-synced-field-selector.tsx | 34 ++++ src/components/src/index.ts | 2 + .../common/source-data-selector-content.tsx | 60 ++++++ .../common/source-data-selector.tsx | 72 +++---- .../src/side-panel/common/source-selector.tsx | 77 +++++++ src/components/src/side-panel/common/types.ts | 1 + .../filter-panel/filter-panel-header.tsx | 4 +- src/components/src/types.ts | 6 +- src/localization/src/translations/en.ts | 2 +- 13 files changed, 448 insertions(+), 194 deletions(-) create mode 100644 src/components/src/filters/filter-panels/filter-synced-dataset-panel.tsx create mode 100644 src/components/src/filters/filter-panels/time-synced-field-selector.tsx create mode 100644 src/components/src/side-panel/common/source-data-selector-content.tsx create mode 100644 src/components/src/side-panel/common/source-selector.tsx diff --git a/src/components/src/common/field-selector.tsx b/src/components/src/common/field-selector.tsx index ecf51ba065..2b05645485 100644 --- a/src/components/src/common/field-selector.tsx +++ b/src/components/src/common/field-selector.tsx @@ -125,7 +125,8 @@ function FieldSelectorFactory( multiSelect: false, closeOnSelect: true, showToken: true, - placeholder: 'placeholder.selectField' + placeholder: 'placeholder.selectField', + className: '' }; fieldsSelector = props => props.fields; diff --git a/src/components/src/common/item-selector/item-selector.tsx b/src/components/src/common/item-selector/item-selector.tsx index 0d229f8c39..7ea57e5bc8 100644 --- a/src/components/src/common/item-selector/item-selector.tsx +++ b/src/components/src/common/item-selector/item-selector.tsx @@ -152,7 +152,8 @@ class ItemSelectorUnmemoized extends Component { DropDownRenderComponent: DropdownList, DropDownLineItemRenderComponent: ListItem, DropDownWrapperComponent: DropdownWrapper, - reorderItems: undefined + reorderItems: undefined, + className: '' }; state = { diff --git a/src/components/src/filters/filter-panels/filter-synced-dataset-panel.tsx b/src/components/src/filters/filter-panels/filter-synced-dataset-panel.tsx new file mode 100644 index 0000000000..fb60ce5056 --- /dev/null +++ b/src/components/src/filters/filter-panels/filter-synced-dataset-panel.tsx @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT +// Copyright contributors to the kepler.gl project + +import React, {useCallback, useMemo} from 'react'; +import styled from 'styled-components'; + +import {FormattedMessage} from '@kepler.gl/localization'; +import {ALL_FIELD_TYPES} from '@kepler.gl/constants'; + +import {Button} from '../../common/styled-components'; +import {Add} from '../../common/icons'; +import TippyTooltip from '../../common/tippy-tooltip'; +import FilterPanelHeaderFactory from '../../side-panel/filter-panel/filter-panel-header'; +import SourceSelectorFactory from '../../side-panel/common/source-selector'; +import SourceDataSelectorFactory from '../../side-panel/common/source-data-selector'; + +const TIME_FIELD_ANALYZER_TYPES = ['DATE', 'TIME', 'DATETIME']; + +const SyncedDatasetsArea = styled.div` + display: grid; + align-items: center; + grid-auto-rows: min-content; + grid-auto-flow: row; +`; + +const StyledContentTitle = styled.div` + color: ${props => props.theme.subtextColor}; + margin-bottom: 8px; +`; + +const StyledSeparator = styled.div` + border-left: 1px dashed ${props => props.theme.subtextColor}; + height: 16px; + margin-left: 8px; +`; + +const StyledButton = styled(Button)` + padding: 2px; +`; + +function getDatasetsWithTimeField(datasets) { + const rv = {}; + for (const id of Object.keys(datasets)) { + // TODO: change to + if (datasets[id].fields.some(f => f.type === ALL_FIELD_TYPES.timestamp)) { + rv[id] = datasets[id]; + } + } + return rv; +} + +function getTimeFields(dataset) { + return dataset.fields.filter(f => TIME_FIELD_ANALYZER_TYPES.includes(f.analyzerType)); +} + +FilterSyncedDatasetPanelFactory.deps = [ + SourceSelectorFactory, + FilterPanelHeaderFactory, + SourceDataSelectorFactory +]; + +function FilterSyncedDatasetPanelFactory(SourceSelector, FilterPanelHeader, SourceDataSelector) { + const StyledFilterPanelHeader = styled(FilterPanelHeader)` + display: flex; + border: none; + height: unset; + padding: 2px 0; + background: none; + align-items: baseline; + `; + + const StyledSourceSelector = styled(SourceSelector)` + flex: 1; + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + border-radius: 8px; + background-color: transparent; + `; + + const FilterSyncedDatasetPanel = ({ + datasets, + filter, + setFilter, + idx, + supportedFields, + onFieldSelector, + onSourceDataSelector + }) => { + const datasetsWithTime = useMemo(() => getDatasetsWithTimeField(datasets), [datasets]); + const filterDatasetsNum = useMemo(() => filter.dataId.length, [filter.dataId]); + const datasetsWithTimeNum = useMemo(() => Object.keys(datasetsWithTime).length, [ + datasetsWithTime + ]); + + const onRemoveSyncedFilter = useCallback( + valueIndex => { + setFilter(idx, 'dataId', null, valueIndex); + }, + [idx, setFilter] + ); + + const onSelectSyncedDataset = useCallback( + (datasetId, valueIndex) => { + setFilter(idx, 'dataId', datasetId, valueIndex); + }, + [setFilter, idx] + ); + + const onAddSyncedFilter = useCallback(() => { + const nextId = Object.keys(datasetsWithTime).find(id => !filter.dataId.includes(id)); + if (!nextId) return; + const timeFieldNames = getTimeFields(datasets[nextId])?.map(f => f.name); + if (!timeFieldNames || timeFieldNames.length < 1) return; + const nextName = timeFieldNames.includes(filter.name[0]) ? filter.name[0] : timeFieldNames[0]; + setFilter(idx, ['dataId', 'name'], [nextId, nextName], filter.dataId.length); + }, [setFilter, idx, datasetsWithTime, datasets, filter.dataId, filter.name]); + + return ( + + {filter.dataId.length > 1 ? ( + <> + Datasets + {filter.dataId.map((dataId, index, list) => { + return ( +
+ onRemoveSyncedFilter(index)} + > + = datasetsWithTimeNum} + dataId={dataId} + onSelectDataset={datasetId => onSelectSyncedDataset(datasetId, index)} + fields={getTimeFields(datasets[dataId])} + fieldValue={filter.name[index]} + onFieldSelector={field => onFieldSelector(field, index)} + /> + + {index + 1 < list.length && } +
+ ); + })} + + ) : ( + <> + + + + )} + {filterDatasetsNum < datasetsWithTimeNum && ( +
+ ( +
+ +
+ )} + > + + + + +
+
+ )} +
+ ); + }; + + return FilterSyncedDatasetPanel; +} + +export default FilterSyncedDatasetPanelFactory; diff --git a/src/components/src/filters/filter-panels/time-range-filter-panel.tsx b/src/components/src/filters/filter-panels/time-range-filter-panel.tsx index 002a39f2c0..4e7cba0185 100644 --- a/src/components/src/filters/filter-panels/time-range-filter-panel.tsx +++ b/src/components/src/filters/filter-panels/time-range-filter-panel.tsx @@ -2,9 +2,8 @@ // Copyright contributors to the kepler.gl project import React, {useCallback, useMemo} from 'react'; -import styled from 'styled-components'; import TimeRangeFilterFactory from '../time-range-filter'; -import {Add, Clock} from '../../common/icons'; +import {Clock} from '../../common/icons'; import FieldPanelWithFieldSelectFactory from './filter-panel-with-field-select'; import {TimeRangeFilterPanelComponent} from './types'; import {isSideFilter, getTimelineFromFilter} from '@kepler.gl/utils'; @@ -12,30 +11,13 @@ import FilterPanelHeaderFactory from '../../side-panel/filter-panel/filter-panel import PanelHeaderActionFactory from '../../side-panel/panel-header-action'; import SourceDataSelectorFactory from '../../side-panel/common/source-data-selector'; import FieldSelectorFactory from '../../common/field-selector'; -import {FormattedMessage} from '@kepler.gl/localization'; -import {Button, StyledFilterContent} from '../../common/styled-components'; +import {StyledFilterContent} from '../../common/styled-components'; import {getSupportedFilterFields} from './new-filter-panel'; -import {ALL_FIELD_TYPES} from '@kepler.gl/constants'; -import TippyTooltip from '../../common/tippy-tooltip'; +import {FILTER_TYPES} from '@kepler.gl/constants'; +import TimeSyncedFieldSelectorFactory from './time-synced-field-selector'; +import FilterSyncedDatasetPanelFactory from './filter-synced-dataset-panel'; -const SyncedDatasetsArea = styled.div` - display: flex; - flex-direction: column; - & > * + * { - margin-top: 10px; - } -`; - -const SyncedDatasetFieldArea = styled.div` - padding: 10px; - border: ${props => props.theme.panelBorder}; - .side-panel-section { - margin-bottom: 0; - } - & > * + * { - margin-top: 10px; - } -`; +const SYNC_FILTER_ID_LENGTH = 2; TimeRangeFilterPanelFactory.deps = [ FieldPanelWithFieldSelectFactory, @@ -43,7 +25,9 @@ TimeRangeFilterPanelFactory.deps = [ FilterPanelHeaderFactory, SourceDataSelectorFactory, FieldSelectorFactory, - PanelHeaderActionFactory + PanelHeaderActionFactory, + TimeSyncedFieldSelectorFactory, + FilterSyncedDatasetPanelFactory ]; function TimeRangeFilterPanelFactory( @@ -52,7 +36,9 @@ function TimeRangeFilterPanelFactory( FilterPanelHeader: ReturnType, SourceDataSelector: ReturnType, FieldSelector: ReturnType, - PanelHeaderAction: ReturnType + PanelHeaderAction: ReturnType, + TimeSyncedFieldSelector: ReturnType, + FilterSyncedDatasetPanel: ReturnType ) { const TimeRangeFilterPanel: TimeRangeFilterPanelComponent = React.memo( ({ @@ -72,12 +58,7 @@ function TimeRangeFilterPanelFactory( ); const totalDatasetsNum = useMemo(() => Object.keys(datasets).length, [datasets]); - const datasetsWithTime = useMemo(() => getDatasetsWithTimeField(datasets), [datasets]); - const datasetsWithTimeNum = useMemo( - () => Object.keys(datasetsWithTime).length, - [datasetsWithTime] - ); - const filterDatasetsNum = useMemo(() => filter.dataId.length, [filter]); + const onSetFilterPlot = useCallback( (newProp, valueIndex) => setFilterPlot(idx, newProp, valueIndex), [idx, setFilterPlot] @@ -98,26 +79,10 @@ function TimeRangeFilterPanelFactory( [filter.id, isEnlarged, enlargeFilter] ); - const onAddSyncedFilter = () => { - const nextId = Object.keys(datasetsWithTime).find(id => !filter.dataId.includes(id)); - if (!nextId) return; - const timeFieldNames = getTimeFields(datasets[nextId])?.map(f => f.name); - if (!timeFieldNames || timeFieldNames.length < 1) return; - const nextName = timeFieldNames.includes(filter.name[0]) - ? filter.name[0] - : timeFieldNames[0]; - setFilter(idx, ['dataId', 'name'], [nextId, nextName], filter.dataId.length); - }; - - const onRemoveSyncedFilter = valueIndex => { - setFilter(idx, 'dataId', null, valueIndex); - }; - - const onSelectSyncedDataset = (datasetId, valueIndex) => { - setFilter(idx, 'dataId', datasetId, valueIndex); - }; - - const onFieldSelector = (field, valueIndex) => setFilter(idx, 'name', field.name, valueIndex); + const onFieldSelector = useCallback( + (field, valueIndex) => setFilter(idx, 'name', field.name, valueIndex), + [setFilter, idx] + ); const onSourceDataSelector = useCallback( value => setFilter(idx, 'dataId', value, 0), @@ -130,6 +95,17 @@ function TimeRangeFilterPanelFactory( [dataset.supportedFilterTypes, allAvailableFields] ); + const isSynced = useMemo(() => { + return ( + filter.dataId.length >= SYNC_FILTER_ID_LENGTH && filter.type === FILTER_TYPES.timeRange + ); + }, [filter.dataId, filter.type]); + + const isHistogramVisible = useMemo( + () => filter.type && !isEnlarged, + [filter.type, isEnlarged] + ); + const timeline = getTimelineFromFilter(filter); return ( @@ -141,13 +117,17 @@ function TimeRangeFilterPanelFactory( filter={filter} removeFilter={removeFilter} > - onFieldSelector(field, 0)} - /> + {isSynced ? ( + + ) : ( + onFieldSelector(field, 0)} + /> + )} {panelActions.map(panelAction => ( {totalDatasetsNum > 1 && ( - - {/* Only shon when there fiter is not synced */} - - - {filter.dataId.slice(1).map((dataId, index) => { - const filteredDatasets = {...datasets}; - for (const id of filter.dataId) { - // remove others which are already in the filter - if (id !== dataId) delete filteredDatasets[id]; - } - const valueIndex = index + 1; - return ( - - onRemoveSyncedFilter(valueIndex)} - > - onFieldSelector(field, valueIndex)} - /> - - - = datasetsWithTimeNum} - dataId={dataId} - onSelect={datasetId => onSelectSyncedDataset(datasetId, valueIndex)} - /> - - ); - })} - - {filterDatasetsNum < datasetsWithTimeNum && ( -
- ( -
- -
- )} - > - -
-
- )} -
+ )} - {filter.type && !isEnlarged && ( + {isHistogramVisible && (
f.type === ALL_FIELD_TYPES.timestamp)) { - rv[id] = datasets[id]; - } - } - return rv; -} - -function getTimeFields(dataset) { - return dataset.fields.filter(f => TIME_FIELD_ANALYZER_TYPES.includes(f.analyzerType)); -} diff --git a/src/components/src/filters/filter-panels/time-synced-field-selector.tsx b/src/components/src/filters/filter-panels/time-synced-field-selector.tsx new file mode 100644 index 0000000000..f7f8f7d2f0 --- /dev/null +++ b/src/components/src/filters/filter-panels/time-synced-field-selector.tsx @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +// Copyright contributors to the kepler.gl project + +import React from 'react'; +import styled from 'styled-components'; + +import {ALL_FIELD_TYPES} from '@kepler.gl/constants'; +import FieldTokenFactory from '../../common/field-token'; + +const StyledSyncTimeHeader = styled.div` + color: ${props => props.theme.subtextColor}; + flex: 1; + cursor: auto; + display: grid; + align-items: center; + grid-column-gap: 8px; + grid-auto-columns: min-content; + grid-auto-flow: column; +`; + +TimeSyncedFieldSelectorFactory.deps = [FieldTokenFactory]; + +function TimeSyncedFieldSelectorFactory(FieldToken) { + const TimeSyncedFieldSelector = () => ( + + + Synced + + ); + + return TimeSyncedFieldSelector; +} + +export default TimeSyncedFieldSelectorFactory; diff --git a/src/components/src/index.ts b/src/components/src/index.ts index f4bb919b73..dab80b487d 100644 --- a/src/components/src/index.ts +++ b/src/components/src/index.ts @@ -206,12 +206,14 @@ export {default as TimeWidgetTopFactory} from './filters/time-widget-top'; export {default as SingleSelectFilterFactory} from './filters/single-select-filter'; export {default as MultiSelectFilterFactory} from './filters/multi-select-filter'; export {default as NewFilterPanelFactory} from './filters/filter-panels/new-filter-panel'; +export {default as TimeRangeFilterPanelFactory} from './filters/filter-panels/time-range-filter-panel'; export { timeRangeSliderFieldsSelector, default as TimeRangeFilterFactory } from './filters/time-range-filter'; export {default as RangeFilterFactory} from './filters/range-filter'; + // // Editor Factory export {default as EditorFactory} from './editor/editor'; export { diff --git a/src/components/src/side-panel/common/source-data-selector-content.tsx b/src/components/src/side-panel/common/source-data-selector-content.tsx new file mode 100644 index 0000000000..49155ca6cb --- /dev/null +++ b/src/components/src/side-panel/common/source-data-selector-content.tsx @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// Copyright contributors to the kepler.gl project + +import React, {useMemo} from 'react'; + +import ItemSelector from '../../common/item-selector/item-selector'; +import DatasetTagFactory from './dataset-tag'; +import {SourceDataSelectorProps} from './types'; + +SourceDataSelectorContentFactory.deps = [DatasetTagFactory]; + +function SourceDataSelectorContentFactory(DatasetTag) { + const DatasetItem = ({value}) => ; + + const SourceDataSelectorContent = ({ + className, + datasets, + dataId, + inputTheme, + onSelect, + defaultValue, + disabled + }: SourceDataSelectorProps) => { + const dsOptions = useMemo( + () => + Object.values(datasets).map(ds => ({ + label: ds.label, + value: ds.id, + color: ds.color + })), + [datasets] + ); + + const selectedItems = useMemo( + () => (dataId ? ((Array.isArray(dataId) && dataId) || [dataId]).map(id => datasets[id]) : []), + [dataId, datasets] + ); + + return ( + + ); + }; + + return SourceDataSelectorContent; +} + +export default SourceDataSelectorContentFactory; diff --git a/src/components/src/side-panel/common/source-data-selector.tsx b/src/components/src/side-panel/common/source-data-selector.tsx index 8e95d0adcf..a94d754d14 100644 --- a/src/components/src/side-panel/common/source-data-selector.tsx +++ b/src/components/src/side-panel/common/source-data-selector.tsx @@ -1,64 +1,44 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import React, {useMemo} from 'react'; -import {PanelLabel, SidePanelSection} from '../../common/styled-components'; -import ItemSelector from '../../common/item-selector/item-selector'; -import DatasetTagFactory from './dataset-tag'; +import React from 'react'; + import {FormattedMessage} from '@kepler.gl/localization'; -import {DatasetItemProps, SourceDataSelectorProps} from './types'; -SourceDataSelectorFactory.deps = [DatasetTagFactory]; +import {PanelLabel, SidePanelSection} from '../../common/styled-components'; +import SourceDataSelectorContentFactory from './source-data-selector-content'; +import {SourceDataSelectorProps} from './types'; + +SourceDataSelectorFactory.deps = [SourceDataSelectorContentFactory]; export default function SourceDataSelectorFactory( - DatasetTag: ReturnType + DataSourceSelectorContent: ReturnType ): React.FC { - const DatasetItem = ({value}: DatasetItemProps) => ; - - const SourceDataSelector: React.FC = ({ - dataId, - datasets, - disabled, - onSelect, - defaultValue = 'Select A Dataset', - inputTheme - }: SourceDataSelectorProps) => { - const dsOptions = useMemo( - () => - Object.values(datasets).map(ds => ({ - label: ds.label, - value: ds.id, - color: ds.color - })), - [datasets] - ); - - const selectedItems = useMemo( - () => (dataId ? ((Array.isArray(dataId) && dataId) || [dataId]).map(id => datasets[id]) : []), - [dataId, datasets] - ); - - return ( + const SourceDataSelector: React.FC = React.memo( + ({ + dataId, + datasets, + disabled, + onSelect, + defaultValue = 'Select A Dataset', + inputTheme + }: SourceDataSelectorProps) => ( - - ); - }; + ) + ); + SourceDataSelector.displayName = 'SourceDataSelector'; return SourceDataSelector; } diff --git a/src/components/src/side-panel/common/source-selector.tsx b/src/components/src/side-panel/common/source-selector.tsx new file mode 100644 index 0000000000..7948f9ce78 --- /dev/null +++ b/src/components/src/side-panel/common/source-selector.tsx @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +// Copyright contributors to the kepler.gl project + +import React, {useMemo} from 'react'; +import styled from 'styled-components'; + +import SourceDataSelectorContentFactory from './source-data-selector-content'; +import FieldSelectorFactory from '../../common/field-selector'; + +const StyledSourceContainer = styled.div` + background-color: #1c2233; +`; + +SourceSelectorFactory.deps = [SourceDataSelectorContentFactory, FieldSelectorFactory]; + +function SourceSelectorFactory(SourceDataSelectorContent, FieldSelector) { + const StyledFieldSelector = styled(FieldSelector)` + -webkit-border-bottom-right-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -moz-border-radius-bottomleft: 4px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + `; + + const StyledSourceDataSelectorContent = styled(SourceDataSelectorContent)` + -webkit-border-top-left-radius: 4px; + -webkit-border-top-right-radius: 4px; + -moz-border-radius-topleft: 4px; + -moz-border-radius-topright: 4px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + `; + + const SourceSelector = ({ + className, + datasets, + dataId, + disabled, + onSelectDataset, + fields, + fieldValue, + onFieldSelector, + inputTheme + }) => { + const datasetFields = useMemo(() => { + return fields || datasets[dataId]?.fields; + }, [dataId, datasets, fields]); + + return ( + + + {dataId && ( + + )} + + ); + }; + + SourceSelector.displayName = 'SourceSelector'; + + return SourceSelector; +} + +export default SourceSelectorFactory; diff --git a/src/components/src/side-panel/common/types.ts b/src/components/src/side-panel/common/types.ts index 3a919d6e95..0ac41ab2a0 100644 --- a/src/components/src/side-panel/common/types.ts +++ b/src/components/src/side-panel/common/types.ts @@ -57,4 +57,5 @@ export type SourceDataSelectorProps = { | object | null ) => void; + className?: string; }; diff --git a/src/components/src/side-panel/filter-panel/filter-panel-header.tsx b/src/components/src/side-panel/filter-panel/filter-panel-header.tsx index 21572ed8ad..18f3471510 100644 --- a/src/components/src/side-panel/filter-panel/filter-panel-header.tsx +++ b/src/components/src/side-panel/filter-panel/filter-panel-header.tsx @@ -3,6 +3,7 @@ import React, {ComponentType} from 'react'; import styled from 'styled-components'; +import classnames from 'classnames'; import PanelHeaderActionFactory from '../../side-panel/panel-header-action'; import {Trash} from '../../common/icons'; import {createLinearGradient} from '@kepler.gl/utils'; @@ -59,13 +60,14 @@ function FilterPanelHeaderFactory( }; const FilterPanelHeader: React.FC = ({ children, + className = '', datasets, filter, removeFilter, actionIcons = defaultActionIcons }: FilterPanelHeaderProps) => ( d.color)} > {children} diff --git a/src/components/src/types.ts b/src/components/src/types.ts index 3ab25c07be..ded46faed4 100644 --- a/src/components/src/types.ts +++ b/src/components/src/types.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import {ComponentType} from 'react'; +import React from 'react'; import {MapStyle} from '@kepler.gl/reducers'; import {Layer, LayerClassesType} from '@kepler.gl/layers'; import {Filter, InteractionConfig, UiState} from '@kepler.gl/types'; @@ -17,8 +17,8 @@ import {Datasets} from '@kepler.gl/table'; export type SidePanelItem = { id: string; label: string; - iconComponent: ComponentType; - component: ComponentType; + iconComponent: React.ComponentType; + component: React.ComponentType; }; export type SidePanelProps = { diff --git a/src/localization/src/translations/en.ts b/src/localization/src/translations/en.ts index 8aadbcc63a..92ae7e7c34 100644 --- a/src/localization/src/translations/en.ts +++ b/src/localization/src/translations/en.ts @@ -195,7 +195,7 @@ export default { }, filterManager: { addFilter: 'Add Filter', - timeFilterSync: 'Sync with…', + timeFilterSync: 'Synced datasets', column: 'Column' }, datasetTitle: {