diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 3130369143409..38e0138b15bc0 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -135,6 +135,7 @@ "rimraf": "^3.0.2", "rison": "^0.1.1", "scroll-into-view-if-needed": "^3.1.0", + "shortid": "^2.2.6", "tinycolor2": "^1.4.2", "urijs": "^1.19.8", "use-event-callback": "^0.1.0", @@ -53305,6 +53306,20 @@ "deprecated": "core-js@<3.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", "hasInstallScript": true }, + "node_modules/react-jsonschema-form/node_modules/nanoid": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + }, + "node_modules/react-jsonschema-form/node_modules/shortid": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", + "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dependencies": { + "nanoid": "^2.1.0" + } + }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", @@ -56557,19 +56572,10 @@ "peer": true }, "node_modules/shortid": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", - "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dependencies": { - "nanoid": "^2.1.0" - } - }, - "node_modules/shortid/node_modules/nanoid": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", - "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==", - "license": "MIT" + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.6.tgz", + "integrity": "sha512-I8FMxCNdsfwqrnnOdGwP/or3HoRHeTNGc2wk5sAuxC/ROJTWf+jQmOEXp1P6VHwXHubrA5uWfxMlkrO6eaRu7g==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info." }, "node_modules/side-channel": { "version": "1.0.6", @@ -107422,6 +107428,19 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" + }, + "nanoid": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + }, + "shortid": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", + "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "requires": { + "nanoid": "^2.1.0" + } } } }, @@ -109839,19 +109858,9 @@ "peer": true }, "shortid": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", - "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", - "requires": { - "nanoid": "^2.1.0" - }, - "dependencies": { - "nanoid": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", - "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" - } - } + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.6.tgz", + "integrity": "sha512-I8FMxCNdsfwqrnnOdGwP/or3HoRHeTNGc2wk5sAuxC/ROJTWf+jQmOEXp1P6VHwXHubrA5uWfxMlkrO6eaRu7g==" }, "side-channel": { "version": "1.0.6", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 8625d4b5e4c02..53dd419ad8d2d 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -200,6 +200,7 @@ "rimraf": "^3.0.2", "rison": "^0.1.1", "scroll-into-view-if-needed": "^3.1.0", + "shortid": "^2.2.6", "tinycolor2": "^1.4.2", "urijs": "^1.19.8", "use-event-callback": "^0.1.0", @@ -234,8 +235,8 @@ "@storybook/addon-essentials": "^8.1.11", "@storybook/addon-links": "^8.1.11", "@storybook/addon-mdx-gfm": "^8.1.11", - "@storybook/preview-api": "^8.1.11", "@storybook/components": "^8.1.11", + "@storybook/preview-api": "^8.1.11", "@storybook/react": "^8.1.11", "@storybook/react-webpack5": "^8.1.11", "@svgr/webpack": "^8.0.1", diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts index ac4b19cae55ae..230b1e6fdcbd0 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts @@ -1,21 +1,4 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ +// DODO was here import { AdhocFilter, DataMask } from '@superset-ui/core'; @@ -54,6 +37,23 @@ export type DataMaskState = { [id: string]: DataMask }; export type DataMaskWithId = { id: string } & DataMask; export type DataMaskStateWithId = { [filterId: string]: DataMaskWithId }; +// DODO added start 44211751 +type FilterSetDodoExtended = { + isPrimary: boolean; +}; + +export type FilterSet = { + id: number; + name: string; + nativeFilters: Filters; + dataMask: DataMaskStateWithId; +} & FilterSetDodoExtended; + +export type FilterSets = { + [filtersSetId: string]: FilterSet; +}; +// DODO added stop 44211751 + export type Filter = { cascadeParentIds: string[]; defaultDataMask: DataMask; @@ -156,11 +156,15 @@ export type PartialFilters = { [filterId: string]: Partial; }; +type NativeFiltersStateDodoExtended = { + filterSets: FilterSets; // DODO added 44211751 + pendingFilterSetId?: number; // DODO added 44211751 +}; export type NativeFiltersState = { filters: Filters; focusedFilterId?: string; hoveredFilterId?: string; -}; +} & NativeFiltersStateDodoExtended; export type DashboardComponentMetadata = { nativeFilters: NativeFiltersState; diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts index be28944a9178d..8112d7f346001 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts @@ -62,6 +62,7 @@ export enum FeatureFlag { SlackEnableAvatars = 'SLACK_ENABLE_AVATARS', EnableDashboardScreenshotEndpoints = 'ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS', EnableDashboardDownloadWebDriverScreenshot = 'ENABLE_DASHBOARD_DOWNLOAD_WEBDRIVER_SCREENSHOT', + DashboardNativeFiltersSet = 'DASHBOARD_NATIVE_FILTERS_SET', // DODO added 44211751 } export type ScheduleQueriesProps = { diff --git a/superset-frontend/spec/fixtures/mockNativeFilters.ts b/superset-frontend/spec/fixtures/mockNativeFilters.ts index b83cdcc8dccdd..ff4861eddf024 100644 --- a/superset-frontend/spec/fixtures/mockNativeFilters.ts +++ b/superset-frontend/spec/fixtures/mockNativeFilters.ts @@ -1,21 +1,4 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ +// DODO was here import { DataMaskStateWithId, ExtraFormData, @@ -24,6 +7,7 @@ import { } from '@superset-ui/core'; export const nativeFilters: NativeFiltersState = { + filterSets: {}, // DODO added 44211751 filters: { 'NATIVE_FILTER-e7Q8zKixx': { id: 'NATIVE_FILTER-e7Q8zKixx', diff --git a/superset-frontend/src/DodoExtensions/FilterSets/EditSection.test.tsx b/superset-frontend/src/DodoExtensions/FilterSets/EditSection.test.tsx new file mode 100644 index 0000000000000..883cb2864d8d6 --- /dev/null +++ b/superset-frontend/src/DodoExtensions/FilterSets/EditSection.test.tsx @@ -0,0 +1,112 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { render, screen } from 'spec/helpers/testing-library'; +import userEvent from '@testing-library/user-event'; +import { mockStore } from 'spec/fixtures/mockStore'; +import { Provider } from 'react-redux'; +import EditSection, { EditSectionProps } from './EditSection'; + +const createProps = () => ({ + filterSetId: 1, + dataMaskSelected: { + DefaultsID: { + filterState: { + value: 'value', + }, + }, + }, + onCancel: jest.fn(), + disabled: false, +}); + +const setup = (props: EditSectionProps) => ( + + + +); + +test('should render', () => { + const mockedProps = createProps(); + const { container } = render(setup(mockedProps)); + expect(container).toBeInTheDocument(); +}); + +test('should render the title', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + expect(screen.getByText('Editing filter set:')).toBeInTheDocument(); +}); + +test('should render the set name', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + expect(screen.getByText('Set name')).toBeInTheDocument(); +}); + +test('should render a textbox', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + expect(screen.getByRole('textbox')).toBeInTheDocument(); +}); + +test('should change the set name', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + const textbox = screen.getByRole('textbox'); + userEvent.clear(textbox); + userEvent.type(textbox, 'New name'); + expect(textbox).toHaveValue('New name'); +}); + +test('should render the enter icon', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + expect(screen.getByRole('img', { name: 'enter' })).toBeInTheDocument(); +}); + +test('should render the Cancel button', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + expect(screen.getByText('Cancel')).toBeInTheDocument(); +}); + +test('should cancel', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + const cancelBtn = screen.getByText('Cancel'); + expect(mockedProps.onCancel).not.toHaveBeenCalled(); + userEvent.click(cancelBtn); + expect(mockedProps.onCancel).toHaveBeenCalled(); +}); + +test('should render the Save button', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + expect(screen.getByText('Save')).toBeInTheDocument(); +}); + +test('should render the Save button as disabled', () => { + const mockedProps = createProps(); + const saveDisabledProps = { + ...mockedProps, + disabled: true, + }; + render(setup(saveDisabledProps)); + expect(screen.getByText('Save').parentElement).toBeDisabled(); +}); diff --git a/superset-frontend/src/DodoExtensions/FilterSets/EditSection.tsx b/superset-frontend/src/DodoExtensions/FilterSets/EditSection.tsx new file mode 100644 index 0000000000000..94efd77e30432 --- /dev/null +++ b/superset-frontend/src/DodoExtensions/FilterSets/EditSection.tsx @@ -0,0 +1,176 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { FC, useMemo, useState } from 'react'; +import { DataMaskState, HandlerFunction, styled, t } from '@superset-ui/core'; +import { Typography, AntdTooltip } from 'src/components'; +import { useDispatch } from 'react-redux'; +import Button from 'src/components/Button'; +import { updateFilterSet } from 'src/dashboard/actions/nativeFilters'; +import Icons from 'src/components/Icons'; +import { + useNativeFiltersDataMask, + useFilters, + useFilterSets, +} from 'src/dashboard/components/nativeFilters/FilterBar/state'; +import { getFilterBarTestId } from 'src/dashboard/components/nativeFilters/FilterBar/utils'; +import { ActionButtons } from './Footer'; +import { APPLY_FILTERS_HINT, findExistingFilterSet } from './utils'; +import { useFilterSetNameDuplicated } from './state'; + +const Wrapper = styled.div` + display: grid; + grid-template-columns: 1fr; + align-items: flex-start; + justify-content: flex-start; + grid-gap: ${({ theme }) => theme.gridUnit}px; + background: ${({ theme }) => theme.colors.primary.light4}; + padding: ${({ theme }) => theme.gridUnit * 2}px; +`; + +const Title = styled(Typography.Text)` + color: ${({ theme }) => theme.colors.primary.dark2}; +`; + +const Warning = styled(Typography.Text)` + font-size: ${({ theme }) => theme.typography.sizes.s}px; + & .anticon { + padding: ${({ theme }) => theme.gridUnit}px; + } +`; + +const ActionButton = styled.div<{ disabled?: boolean }>` + display: flex; + & button { + ${({ disabled }) => `pointer-events: ${disabled ? 'none' : 'all'}`}; + flex: 1; + } +`; + +export type EditSectionProps = { + filterSetId: number; + dataMaskSelected: DataMaskState; + onCancel: HandlerFunction; + disabled: boolean; +}; + +const EditSection: FC = ({ + filterSetId, + onCancel, + dataMaskSelected, + disabled, +}) => { + const dataMaskApplied = useNativeFiltersDataMask(); + const dispatch = useDispatch(); + const filterSets = useFilterSets(); + const filters = useFilters(); + const filterSetFilterValues = Object.values(filterSets); + + const [filterSetName, setFilterSetName] = useState( + filterSets[filterSetId].name, + ); + + const isFilterSetNameDuplicated = useFilterSetNameDuplicated( + filterSetName, + filterSets[filterSetId].name, + ); + + const handleSave = () => { + dispatch( + updateFilterSet({ + id: filterSetId, + name: filterSetName, + nativeFilters: filters, + dataMask: { ...dataMaskApplied }, + isPrimary: filterSets[filterSetId].isPrimary, + }), + ); + onCancel(); + }; + + const foundFilterSet = useMemo( + () => + findExistingFilterSet({ + dataMaskSelected, + filterSetFilterValues, + }), + [dataMaskSelected, filterSetFilterValues], + ); + + const isDuplicateFilterSet = + foundFilterSet && foundFilterSet.id !== filterSetId; + + const resultDisabled = + disabled || isDuplicateFilterSet || isFilterSetNameDuplicated; + + return ( + + {t('Editing filter set:')} + , + onChange: setFilterSetName, + }} + > + {filterSetName} + + + + + + + + + + {isDuplicateFilterSet && ( + + + {t('This filter set is identical to: "%s"', foundFilterSet?.name)} + + )} + + ); +}; + +export default EditSection; diff --git a/superset-frontend/src/DodoExtensions/FilterSets/FilterSetUnit.test.tsx b/superset-frontend/src/DodoExtensions/FilterSets/FilterSetUnit.test.tsx new file mode 100644 index 0000000000000..781ef7a531e34 --- /dev/null +++ b/superset-frontend/src/DodoExtensions/FilterSets/FilterSetUnit.test.tsx @@ -0,0 +1,99 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { render, screen } from 'spec/helpers/testing-library'; +import { mockStore } from 'spec/fixtures/mockStore'; +import { Provider } from 'react-redux'; +import userEvent from '@testing-library/user-event'; +import FilterSetUnit, { FilterSetUnitProps } from './FilterSetUnit'; + +const createProps = () => ({ + editMode: true, + setFilterSetName: jest.fn(), + onDelete: jest.fn(), + onEdit: jest.fn(), + onRebuild: jest.fn(), +}); + +function openDropdown() { + const dropdownIcon = screen.getAllByRole('img', { name: '' })[0]; + userEvent.click(dropdownIcon); +} + +const setup = (props: FilterSetUnitProps) => ( + + + +); + +test('should render', () => { + const mockedProps = createProps(); + const { container } = render(setup(mockedProps)); + expect(container).toBeInTheDocument(); +}); + +test('should render the edit button', () => { + const mockedProps = createProps(); + const editModeOffProps = { + ...mockedProps, + editMode: false, + }; + render(setup(editModeOffProps)); + expect(screen.getByRole('button', { name: 'Edit' })).toBeInTheDocument(); +}); + +test('should render the menu', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + openDropdown(); + expect(screen.getByRole('menu')).toBeInTheDocument(); + expect(screen.getAllByRole('menuitem')).toHaveLength(3); + expect(screen.getByText('Edit')).toBeInTheDocument(); + expect(screen.getByText('Rebuild')).toBeInTheDocument(); + expect(screen.getByText('Delete')).toBeInTheDocument(); +}); + +test('should edit', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + openDropdown(); + const editBtn = screen.getByText('Edit'); + expect(mockedProps.onEdit).not.toHaveBeenCalled(); + userEvent.click(editBtn); + expect(mockedProps.onEdit).toHaveBeenCalled(); +}); + +test('should delete', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + openDropdown(); + const deleteBtn = screen.getByText('Delete'); + expect(mockedProps.onDelete).not.toHaveBeenCalled(); + userEvent.click(deleteBtn); + expect(mockedProps.onDelete).toHaveBeenCalled(); +}); + +test('should rebuild', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + openDropdown(); + const rebuildBtn = screen.getByText('Rebuild'); + expect(mockedProps.onRebuild).not.toHaveBeenCalled(); + userEvent.click(rebuildBtn); + expect(mockedProps.onRebuild).toHaveBeenCalled(); +}); diff --git a/superset-frontend/src/DodoExtensions/FilterSets/FilterSetUnit.tsx b/superset-frontend/src/DodoExtensions/FilterSets/FilterSetUnit.tsx new file mode 100644 index 0000000000000..f5303f2c07426 --- /dev/null +++ b/superset-frontend/src/DodoExtensions/FilterSets/FilterSetUnit.tsx @@ -0,0 +1,176 @@ +// DODO was here +import { AntdDropdown, Typography } from 'src/components'; +import { Menu } from 'src/components/Menu'; +import { FC } from 'react'; +import { + DataMaskState, + FilterSet, + HandlerFunction, + styled, + useTheme, + t, +} from '@superset-ui/core'; +import Icons from 'src/components/Icons'; +import Button from 'src/components/Button'; +import { Tooltip } from 'src/components/Tooltip'; +import CheckboxControl from 'src/explore/components/controls/CheckboxControl'; +import Loading from 'src/components/Loading'; +import { getFilterBarTestId } from 'src/dashboard/components/nativeFilters/FilterBar/utils'; +import FiltersHeader from './FiltersHeader'; + +const HeaderButton = styled(Button)` + padding: 0; +`; + +const TitleText = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const IconsBlock = styled.div` + display: flex; + justify-content: flex-end; + align-items: center; + & > *, + & > button.superset-button { + ${({ theme }) => `margin-left: ${theme.gridUnit * 2}px`}; + } +`; + +type FilterSetUnitPropsDodoExtended = { + onSetPrimary?: HandlerFunction; + isPrimary?: boolean; + isFilterSetPrimary?: boolean; + setIsFilterSetPrimary?: (value: boolean) => void; + isInPending?: boolean; +}; + +export type FilterSetUnitProps = { + editMode?: boolean; + isApplied?: boolean; + filterSet?: FilterSet; + filterSetName?: string; + dataMaskSelected?: DataMaskState; + setFilterSetName?: (name: string) => void; + onDelete?: HandlerFunction; + onEdit?: HandlerFunction; + onRebuild?: HandlerFunction; +} & FilterSetUnitPropsDodoExtended; + +const FilterSetUnit: FC = ({ + editMode, + setFilterSetName, + onDelete, + onEdit, + filterSetName, + dataMaskSelected, + filterSet, + isApplied, + onRebuild, + // DODO added start 38080573 + isPrimary, + onSetPrimary, + isFilterSetPrimary, + setIsFilterSetPrimary, + isInPending, + // DODO added stop 38080573 +}) => { + const theme = useTheme(); + + const menu = ( + + {t('Edit')} + {/* DODO added start 38080573 */} + + + {t('Set as primary')} + + + {/* DODO added stop 38080573 */} + + + {t('Rebuild')} + + + + {t('Delete')} + + + ); + + return ( + <> + {/* DODO added 38080573 */} + {isInPending && } + + + , + onChange: setFilterSetName, + }} + > + {filterSet?.name ?? filterSetName} + + + {isPrimary && ( + + )} + {isApplied && ( + + )} + {onDelete && ( + + { + e.stopPropagation(); + e.preventDefault(); + }} + {...getFilterBarTestId('filter-set-menu-button')} + buttonStyle="link" + buttonSize="xsmall" + > + + + + )} + + + {/* DODO added start 38080573 */} + {editMode && setIsFilterSetPrimary && ( + + )} + {/* DODO added stop 38080573 */} + + + ); +}; + +export default FilterSetUnit; diff --git a/superset-frontend/src/DodoExtensions/FilterSets/FilterSets.test.tsx b/superset-frontend/src/DodoExtensions/FilterSets/FilterSets.test.tsx new file mode 100644 index 0000000000000..4138649b37022 --- /dev/null +++ b/superset-frontend/src/DodoExtensions/FilterSets/FilterSets.test.tsx @@ -0,0 +1,67 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { render, screen } from 'spec/helpers/testing-library'; +import { mockStore } from 'spec/fixtures/mockStore'; +import { Provider } from 'react-redux'; +import { TabIds } from 'src/dashboard/components/nativeFilters/FilterBar/types'; +import FilterSets, { FilterSetsProps } from '.'; + +const createProps = () => ({ + disabled: false, + tab: TabIds.FilterSets, + dataMaskSelected: { + DefaultsID: { + filterState: { + value: 'value', + }, + }, + }, + onEditFilterSet: jest.fn(), + onFilterSelectionChange: jest.fn(), +}); + +const setup = (props: FilterSetsProps) => ( + + + +); + +test('should render', () => { + const mockedProps = createProps(); + const { container } = render(setup(mockedProps)); + expect(container).toBeInTheDocument(); +}); + +test('should render the default title', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + expect(screen.getByText('New filter set')).toBeInTheDocument(); +}); + +test('should render the right number of filters', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + expect(screen.getByText('Filters (1)')).toBeInTheDocument(); +}); + +test('should render the filters', () => { + const mockedProps = createProps(); + render(setup(mockedProps)); + expect(screen.getByText('Set name')).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/DodoExtensions/FilterSets/FiltersHeader.test.tsx b/superset-frontend/src/DodoExtensions/FilterSets/FiltersHeader.test.tsx new file mode 100644 index 0000000000000..8108335aeca4c --- /dev/null +++ b/superset-frontend/src/DodoExtensions/FilterSets/FiltersHeader.test.tsx @@ -0,0 +1,53 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { render, screen } from 'spec/helpers/testing-library'; +import { mockStore } from 'spec/fixtures/mockStore'; +import { Provider } from 'react-redux'; +import FiltersHeader, { FiltersHeaderProps } from './FiltersHeader'; + +const mockedProps = { + dataMask: { + DefaultsID: { + filterState: { + value: 'value', + }, + }, + }, +}; +const setup = (props: FiltersHeaderProps) => ( + + + +); + +test('should render', () => { + const { container } = render(setup(mockedProps)); + expect(container).toBeInTheDocument(); +}); + +test('should render the right number of filters', () => { + render(setup(mockedProps)); + expect(screen.getByText('Filters (1)')).toBeInTheDocument(); +}); + +test('should render the name and value', () => { + render(setup(mockedProps)); + expect(screen.getByText('test:')).toBeInTheDocument(); + expect(screen.getByText('value')).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/DodoExtensions/FilterSets/FiltersHeader.tsx b/superset-frontend/src/DodoExtensions/FilterSets/FiltersHeader.tsx new file mode 100644 index 0000000000000..11423aed037c7 --- /dev/null +++ b/superset-frontend/src/DodoExtensions/FilterSets/FiltersHeader.tsx @@ -0,0 +1,156 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { FC } from 'react'; +import { + DataMaskState, + FilterSet, + isNativeFilter, + styled, + t, + useTheme, +} from '@superset-ui/core'; +import { Typography, AntdTooltip, AntdCollapse } from 'src/components'; +import Icons from 'src/components/Icons'; +import { areObjectsEqual } from 'src/reduxUtils'; +import { useFilters } from 'src/dashboard/components/nativeFilters/FilterBar/state'; +import { getFilterBarTestId } from 'src/dashboard/components/nativeFilters/FilterBar/utils'; +import { getFilterValueForDisplay } from './utils'; + +const FilterHeader = styled.div` + display: flex; + align-items: center; + font-size: ${({ theme }) => theme.typography.sizes.s}px; +`; + +const StyledCollapse = styled(AntdCollapse)` + &.ant-collapse-ghost > .ant-collapse-item { + & > .ant-collapse-content > .ant-collapse-content-box { + padding: 0; + padding-top: 0; + padding-bottom: 0; + font-size: ${({ theme }) => theme.typography.sizes.s}px; + } + & > .ant-collapse-header { + padding: 0; + display: flex; + align-items: center; + flex-direction: row-reverse; + justify-content: flex-end; + max-width: max-content; + & .ant-collapse-arrow { + position: static; + padding-left: ${({ theme }) => theme.gridUnit}px; + } + } +`; + +const StyledFilterRow = styled.div` + padding-top: ${({ theme }) => theme.gridUnit}px; + padding-bottom: ${({ theme }) => theme.gridUnit}px; +`; + +export type FiltersHeaderProps = { + dataMask?: DataMaskState; + filterSet?: FilterSet; +}; + +const FiltersHeader: FC = ({ dataMask, filterSet }) => { + const theme = useTheme(); + const filters = useFilters(); + const filterValues = Object.values(filters).filter(isNativeFilter); + + let resultFilters = filterValues ?? []; + if (filterSet?.nativeFilters) { + resultFilters = Object.values(filterSet?.nativeFilters).filter( + isNativeFilter, + ); + } + + const getFiltersHeader = () => ( + + + {t('Filters (%d)', resultFilters.length)} + + + ); + + const getFilterRow = ({ id, name }: { id: string; name: string }) => { + const changedFilter = + filterSet && + !areObjectsEqual( + filters[id]?.controlValues, + filterSet?.nativeFilters?.[id]?.controlValues, + { + ignoreUndefined: true, + }, + ); + const removedFilter = !Object.keys(filters).includes(id); + + return ( + + + + {name}:  + + + {getFilterValueForDisplay(dataMask?.[id]?.filterState?.value) || ( + {t('None')} + )} + + + + ); + }; + + const getExpandIcon = ({ isActive }: { isActive: boolean }) => { + const color = theme.colors.grayscale.base; + const Icon = isActive ? Icons.CaretUpOutlined : Icons.CaretDownOutlined; + return ; + }; + + return ( + + + {resultFilters.map(getFilterRow)} + + + ); +}; + +export default FiltersHeader; diff --git a/superset-frontend/src/DodoExtensions/FilterSets/Footer.test.tsx b/superset-frontend/src/DodoExtensions/FilterSets/Footer.test.tsx new file mode 100644 index 0000000000000..d07b95e7de7c1 --- /dev/null +++ b/superset-frontend/src/DodoExtensions/FilterSets/Footer.test.tsx @@ -0,0 +1,93 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import userEvent from '@testing-library/user-event'; +import { render, screen } from 'spec/helpers/testing-library'; +import Footer from './Footer'; + +const createProps = () => ({ + filterSetName: 'Set name', + disabled: false, + editMode: false, + onCancel: jest.fn(), + onEdit: jest.fn(), + onCreate: jest.fn(), +}); + +const editModeProps = { + ...createProps(), + editMode: true, +}; + +test('should render', () => { + const mockedProps = createProps(); + const { container } = render(