diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.test.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.test.tsx new file mode 100644 index 00000000000..eac5d13df7c --- /dev/null +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.test.tsx @@ -0,0 +1,524 @@ +/** + * 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, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { supersetTheme, ThemeProvider } from '@apache-superset/core/ui'; +import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; +import { DatasourceType, SupersetClient } from '@superset-ui/core'; +import DeckMulti from './Multi'; +import * as fitViewportModule from '../utils/fitViewport'; + +// Mock DeckGLContainer +jest.mock('../DeckGLContainer', () => ({ + DeckGLContainerStyledWrapper: ({ viewport, layers }: any) => ( +
+ DeckGL Container Mock +
+ ), +})); + +// Mock SupersetClient +jest.mock('@superset-ui/core', () => ({ + ...jest.requireActual('@superset-ui/core'), + SupersetClient: { + get: jest.fn(), + }, +})); + +const mockStore = configureStore({ + reducer: { + dataMask: () => ({}), + }, +}); + +const baseMockProps = { + formData: { + datasource: 'test_datasource', + viz_type: 'deck_multi', + deck_slices: [1, 2], + autozoom: false, + mapbox_style: 'mapbox://styles/mapbox/light-v9', + }, + payload: { + data: { + slices: [ + { + slice_id: 1, + form_data: { + viz_type: 'deck_scatter', + datasource: 'test_datasource', + }, + }, + { + slice_id: 2, + form_data: { + viz_type: 'deck_polygon', + datasource: 'test_datasource', + }, + }, + ], + features: { + deck_scatter: [{ position: [0, 0] }], + deck_polygon: [ + { + polygon: [ + [1, 1], + [2, 2], + ], + }, + ], + deck_path: [], + deck_grid: [], + deck_contour: [], + deck_heatmap: [], + deck_hex: [], + deck_arc: [], + deck_geojson: [], + deck_screengrid: [], + }, + mapboxApiKey: 'test-key', + }, + }, + setControlValue: jest.fn(), + viewport: { longitude: 0, latitude: 0, zoom: 1 }, + onAddFilter: jest.fn(), + height: 600, + width: 800, + datasource: { + id: 1, + type: DatasourceType.Table, + name: 'test_datasource', + columns: [], + metrics: [], + columnFormats: {}, + currencyFormats: {}, + verboseMap: {}, + }, + onSelect: jest.fn(), +}; + +const renderWithProviders = (component: React.ReactElement) => + render( + + {component} + , + ); + +describe('DeckMulti Autozoom Functionality', () => { + beforeEach(() => { + jest.clearAllMocks(); + (SupersetClient.get as jest.Mock).mockResolvedValue({ + json: { + data: { + features: [], + }, + }, + }); + }); + + it('should NOT apply autozoom when autozoom is false', () => { + const fitViewportSpy = jest.spyOn(fitViewportModule, 'default'); + + const props = { + ...baseMockProps, + formData: { + ...baseMockProps.formData, + autozoom: false, + }, + }; + + renderWithProviders(); + + // fitViewport should not be called when autozoom is false + expect(fitViewportSpy).not.toHaveBeenCalled(); + + fitViewportSpy.mockRestore(); + }); + + it('should apply autozoom when autozoom is true', () => { + const fitViewportSpy = jest.spyOn(fitViewportModule, 'default'); + fitViewportSpy.mockReturnValue({ + longitude: -122.4, + latitude: 37.8, + zoom: 10, + }); + + const props = { + ...baseMockProps, + formData: { + ...baseMockProps.formData, + autozoom: true, + }, + }; + + renderWithProviders(); + + // fitViewport should be called with the points from all layers + expect(fitViewportSpy).toHaveBeenCalledWith( + expect.objectContaining({ + longitude: 0, + latitude: 0, + zoom: 1, + }), + expect.objectContaining({ + width: 800, + height: 600, + points: expect.any(Array), + }), + ); + + fitViewportSpy.mockRestore(); + }); + + it('should use adjusted viewport when autozoom is enabled', async () => { + const fitViewportSpy = jest.spyOn(fitViewportModule, 'default'); + const adjustedViewport = { + longitude: -122.4, + latitude: 37.8, + zoom: 12, + }; + fitViewportSpy.mockReturnValue(adjustedViewport); + + const props = { + ...baseMockProps, + formData: { + ...baseMockProps.formData, + autozoom: true, + }, + }; + + renderWithProviders(); + + await waitFor(() => { + const container = screen.getByTestId('deckgl-container'); + const viewportData = JSON.parse( + container.getAttribute('data-viewport') || '{}', + ); + + expect(viewportData.longitude).toBe(adjustedViewport.longitude); + expect(viewportData.latitude).toBe(adjustedViewport.latitude); + expect(viewportData.zoom).toBe(adjustedViewport.zoom); + }); + + fitViewportSpy.mockRestore(); + }); + + it('should set zoom to 0 when calculated zoom is negative', async () => { + const fitViewportSpy = jest.spyOn(fitViewportModule, 'default'); + fitViewportSpy.mockReturnValue({ + longitude: 0, + latitude: 0, + zoom: -5, // negative zoom + }); + + const props = { + ...baseMockProps, + formData: { + ...baseMockProps.formData, + autozoom: true, + }, + }; + + renderWithProviders(); + + await waitFor(() => { + const container = screen.getByTestId('deckgl-container'); + const viewportData = JSON.parse( + container.getAttribute('data-viewport') || '{}', + ); + + // Zoom should be 0, not negative + expect(viewportData.zoom).toBe(0); + }); + + fitViewportSpy.mockRestore(); + }); + + it('should handle empty features gracefully when autozoom is enabled', () => { + const fitViewportSpy = jest.spyOn(fitViewportModule, 'default'); + + const props = { + ...baseMockProps, + formData: { + ...baseMockProps.formData, + autozoom: true, + }, + payload: { + ...baseMockProps.payload, + data: { + ...baseMockProps.payload.data, + features: { + deck_scatter: [], + deck_polygon: [], + deck_path: [], + deck_grid: [], + deck_contour: [], + deck_heatmap: [], + deck_hex: [], + deck_arc: [], + deck_geojson: [], + deck_screengrid: [], + }, + }, + }, + }; + + renderWithProviders(); + + // fitViewport should not be called when there are no points + expect(fitViewportSpy).not.toHaveBeenCalled(); + + fitViewportSpy.mockRestore(); + }); + + it('should collect points from all layer types when autozoom is enabled', () => { + const fitViewportSpy = jest.spyOn(fitViewportModule, 'default'); + fitViewportSpy.mockReturnValue({ + longitude: 0, + latitude: 0, + zoom: 10, + }); + + const props = { + ...baseMockProps, + formData: { + ...baseMockProps.formData, + autozoom: true, + }, + payload: { + ...baseMockProps.payload, + data: { + ...baseMockProps.payload.data, + features: { + deck_scatter: [{ position: [1, 1] }, { position: [2, 2] }], + deck_polygon: [ + { + polygon: [ + [3, 3], + [4, 4], + ], + }, + ], + deck_arc: [{ sourcePosition: [5, 5], targetPosition: [6, 6] }], + deck_path: [], + deck_grid: [], + deck_contour: [], + deck_heatmap: [], + deck_hex: [], + deck_geojson: [], + deck_screengrid: [], + }, + }, + }, + }; + + renderWithProviders(); + + expect(fitViewportSpy).toHaveBeenCalled(); + const callArgs = fitViewportSpy.mock.calls[0]; + const { points } = callArgs[1]; + + // Should have points from scatter (2), polygon (2), and arc (2) = 6 points total + expect(points.length).toBeGreaterThan(0); + + fitViewportSpy.mockRestore(); + }); + + it('should use original viewport when autozoom is disabled', async () => { + const fitViewportSpy = jest.spyOn(fitViewportModule, 'default'); + + const originalViewport = { longitude: -100, latitude: 40, zoom: 5 }; + const props = { + ...baseMockProps, + viewport: originalViewport, + formData: { + ...baseMockProps.formData, + autozoom: false, + }, + }; + + renderWithProviders(); + + await waitFor(() => { + const container = screen.getByTestId('deckgl-container'); + const viewportData = JSON.parse( + container.getAttribute('data-viewport') || '{}', + ); + + // Should use original viewport without modification + expect(viewportData.longitude).toBe(originalViewport.longitude); + expect(viewportData.latitude).toBe(originalViewport.latitude); + expect(viewportData.zoom).toBe(originalViewport.zoom); + }); + + // fitViewport should not have been called + expect(fitViewportSpy).not.toHaveBeenCalled(); + + fitViewportSpy.mockRestore(); + }); + + it('should apply autozoom when autozoom is undefined (backward compatibility)', () => { + const fitViewportSpy = jest.spyOn(fitViewportModule, 'default'); + fitViewportSpy.mockReturnValue({ + longitude: -122.4, + latitude: 37.8, + zoom: 10, + }); + + const props = { + ...baseMockProps, + formData: { + ...baseMockProps.formData, + autozoom: undefined, // Simulating existing charts created before this feature + }, + }; + + renderWithProviders(); + + // fitViewport should be called for backward compatibility with existing charts + expect(fitViewportSpy).toHaveBeenCalledWith( + expect.objectContaining({ + longitude: 0, + latitude: 0, + zoom: 1, + }), + expect.objectContaining({ + width: 800, + height: 600, + points: expect.any(Array), + }), + ); + + fitViewportSpy.mockRestore(); + }); + + it('should use adjusted viewport when autozoom is undefined', async () => { + const fitViewportSpy = jest.spyOn(fitViewportModule, 'default'); + const adjustedViewport = { + longitude: -122.4, + latitude: 37.8, + zoom: 12, + }; + fitViewportSpy.mockReturnValue(adjustedViewport); + + const props = { + ...baseMockProps, + formData: { + ...baseMockProps.formData, + autozoom: undefined, // Simulating existing charts + }, + }; + + renderWithProviders(); + + await waitFor(() => { + const container = screen.getByTestId('deckgl-container'); + const viewportData = JSON.parse( + container.getAttribute('data-viewport') || '{}', + ); + + // Should use adjusted viewport for backward compatibility + expect(viewportData.longitude).toBe(adjustedViewport.longitude); + expect(viewportData.latitude).toBe(adjustedViewport.latitude); + expect(viewportData.zoom).toBe(adjustedViewport.zoom); + }); + + fitViewportSpy.mockRestore(); + }); +}); + +describe('DeckMulti Component Rendering', () => { + beforeEach(() => { + jest.clearAllMocks(); + (SupersetClient.get as jest.Mock).mockResolvedValue({ + json: { + data: { + features: [], + }, + }, + }); + }); + + it('should render DeckGLContainer', async () => { + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByTestId('deckgl-container')).toBeInTheDocument(); + }); + }); + + it('should pass correct props to DeckGLContainer', async () => { + renderWithProviders(); + + await waitFor(() => { + const container = screen.getByTestId('deckgl-container'); + const viewportData = JSON.parse( + container.getAttribute('data-viewport') || '{}', + ); + + expect(viewportData).toMatchObject({ + longitude: baseMockProps.viewport.longitude, + latitude: baseMockProps.viewport.latitude, + zoom: baseMockProps.viewport.zoom, + }); + }); + }); + + it('should handle viewport changes', async () => { + const { rerender } = renderWithProviders(); + + // Wait for initial render + await waitFor(() => { + expect(screen.getByTestId('deckgl-container')).toBeInTheDocument(); + }); + + const newViewport = { longitude: 10, latitude: 20, zoom: 8 }; + const updatedProps = { + ...baseMockProps, + viewport: newViewport, + formData: { + ...baseMockProps.formData, + deck_slices: [1, 2, 3], // Change deck_slices to trigger loadLayers + }, + }; + + rerender( + + + + + , + ); + + await waitFor(() => { + const container = screen.getByTestId('deckgl-container'); + const viewportData = JSON.parse( + container.getAttribute('data-viewport') || '{}', + ); + + expect(viewportData.longitude).toBe(newViewport.longitude); + expect(viewportData.latitude).toBe(newViewport.latitude); + }); + }); +}); diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx index c6d2b48e320..1ee7ee12707 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx @@ -115,26 +115,33 @@ const DeckMulti = (props: DeckMultiProps) => { const getAdjustedViewport = useCallback(() => { let viewport = { ...props.viewport }; - const points = [ - ...getPointsPolygon(props.payload.data.features.deck_polygon || []), - ...getPointsPath(props.payload.data.features.deck_path || []), - ...getPointsGrid(props.payload.data.features.deck_grid || []), - ...getPointsScatter(props.payload.data.features.deck_scatter || []), - ...getPointsContour(props.payload.data.features.deck_contour || []), - ...getPointsHeatmap(props.payload.data.features.deck_heatmap || []), - ...getPointsHex(props.payload.data.features.deck_hex || []), - ...getPointsArc(props.payload.data.features.deck_arc || []), - ...getPointsGeojson(props.payload.data.features.deck_geojson || []), - ...getPointsScreengrid(props.payload.data.features.deck_screengrid || []), - ]; - if (props.formData) { - viewport = fitViewport(viewport, { - width: props.width, - height: props.height, - points, - }); + // Default to autozoom enabled for backward compatibility (undefined treated as true) + if (props.formData.autozoom !== false) { + const points = [ + ...getPointsPolygon(props.payload.data.features.deck_polygon || []), + ...getPointsPath(props.payload.data.features.deck_path || []), + ...getPointsGrid(props.payload.data.features.deck_grid || []), + ...getPointsScatter(props.payload.data.features.deck_scatter || []), + ...getPointsContour(props.payload.data.features.deck_contour || []), + ...getPointsHeatmap(props.payload.data.features.deck_heatmap || []), + ...getPointsHex(props.payload.data.features.deck_hex || []), + ...getPointsArc(props.payload.data.features.deck_arc || []), + ...getPointsGeojson(props.payload.data.features.deck_geojson || []), + ...getPointsScreengrid( + props.payload.data.features.deck_screengrid || [], + ), + ]; + + if (props.formData && points.length > 0) { + viewport = fitViewport(viewport, { + width: props.width, + height: props.height, + points, + }); + } } + if (viewport.zoom < 0) { viewport.zoom = 0; } diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.test.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.test.ts new file mode 100644 index 00000000000..dc37d54db3b --- /dev/null +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.test.ts @@ -0,0 +1,351 @@ +/** + * 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 type { + ControlPanelSectionConfig, + ControlSetRow, + ControlSetItem, +} from '@superset-ui/chart-controls'; +import controlPanel from './controlPanel'; + +test('controlPanel should have Map section', () => { + const mapSection = controlPanel.controlPanelSections.find( + ( + section: ControlPanelSectionConfig | null, + ): section is ControlPanelSectionConfig => + section !== null && section.label === 'Map', + ); + + expect(mapSection).toBeDefined(); + expect(mapSection?.expanded).toBe(true); +}); + +test('controlPanel should have Query section', () => { + const querySection = controlPanel.controlPanelSections.find( + ( + section: ControlPanelSectionConfig | null, + ): section is ControlPanelSectionConfig => + section !== null && section.label === 'Query', + ); + + expect(querySection).toBeDefined(); + expect(querySection?.expanded).toBe(true); +}); + +test('controlPanel Map section should include viewport control', () => { + const mapSection = controlPanel.controlPanelSections.find( + ( + section: ControlPanelSectionConfig | null, + ): section is ControlPanelSectionConfig => + section !== null && section.label === 'Map', + ); + + // viewport is imported from Shared_DeckGL and included in controlSetRows + expect(mapSection?.controlSetRows).toBeDefined(); + expect(mapSection?.controlSetRows.length).toBeGreaterThan(0); +}); + +test('controlPanel Map section should include autozoom control', () => { + const mapSection = controlPanel.controlPanelSections.find( + ( + section: ControlPanelSectionConfig | null, + ): section is ControlPanelSectionConfig => + section !== null && section.label === 'Map', + ); + + // autozoom is imported from Shared_DeckGL and included in controlSetRows + expect(mapSection?.controlSetRows).toBeDefined(); + expect(mapSection?.controlSetRows.length).toBeGreaterThan(0); +}); + +test('controlPanel should include deck_slices control with validation', () => { + const mapSection = controlPanel.controlPanelSections.find( + ( + section: ControlPanelSectionConfig | null, + ): section is ControlPanelSectionConfig => + section !== null && section.label === 'Map', + ); + + const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) => + row.some( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ), + ); + + const deckSlicesControl = deckSlicesRow?.find( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ) as any; + + expect(deckSlicesControl).toBeDefined(); + expect(deckSlicesControl?.config?.validators).toBeDefined(); + expect(deckSlicesControl?.config?.validators.length).toBeGreaterThan(0); + expect(deckSlicesControl?.config?.multi).toBe(true); + expect(deckSlicesControl?.config?.type).toBe('SelectAsyncControl'); +}); + +test('deck_slices control should have correct label and description', () => { + const mapSection = controlPanel.controlPanelSections.find( + ( + section: ControlPanelSectionConfig | null, + ): section is ControlPanelSectionConfig => + section !== null && section.label === 'Map', + ); + + const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) => + row.some( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ), + ); + + const deckSlicesControl = deckSlicesRow?.find( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ) as any; + + expect(deckSlicesControl?.config?.label).toBeDefined(); + expect(deckSlicesControl?.config?.description).toBeDefined(); + expect(deckSlicesControl?.config?.placeholder).toBeDefined(); +}); + +test('deck_slices control should have correct API endpoint', () => { + const mapSection = controlPanel.controlPanelSections.find( + ( + section: ControlPanelSectionConfig | null, + ): section is ControlPanelSectionConfig => + section !== null && section.label === 'Map', + ); + + const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) => + row.some( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ), + ); + + const deckSlicesControl = deckSlicesRow?.find( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ) as any; + + expect(deckSlicesControl?.config?.dataEndpoint).toBe( + 'api/v1/chart/?q=(filters:!((col:viz_type,opr:sw,value:deck)))', + ); +}); + +test('deck_slices mutator should add index labels to selected charts', () => { + const mapSection = controlPanel.controlPanelSections.find( + ( + section: ControlPanelSectionConfig | null, + ): section is ControlPanelSectionConfig => + section !== null && section.label === 'Map', + ); + + const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) => + row.some( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ), + ); + + const deckSlicesControl = deckSlicesRow?.find( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ) as any; + + const mockData = { + result: [ + { id: 1, slice_name: 'Chart A' }, + { id: 2, slice_name: 'Chart B' }, + { id: 3, slice_name: 'Chart C' }, + ], + }; + + const selectedIds = [2, 3]; + const result = deckSlicesControl.config.mutator(mockData, selectedIds); + + expect(result).toEqual([ + { value: 1, label: 'Chart A' }, + { value: 2, label: 'Chart B [1]' }, + { value: 3, label: 'Chart C [2]' }, + ]); +}); + +test('deck_slices mutator should handle empty result', () => { + const mapSection = controlPanel.controlPanelSections.find( + ( + section: ControlPanelSectionConfig | null, + ): section is ControlPanelSectionConfig => + section !== null && section.label === 'Map', + ); + + const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) => + row.some( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ), + ); + + const deckSlicesControl = deckSlicesRow?.find( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ) as any; + + const mockData = { + result: undefined, + }; + + const result = deckSlicesControl.config.mutator(mockData, []); + + expect(result).toEqual([]); +}); + +test('deck_slices mutator should handle undefined selected values', () => { + const mapSection = controlPanel.controlPanelSections.find( + ( + section: ControlPanelSectionConfig | null, + ): section is ControlPanelSectionConfig => + section !== null && section.label === 'Map', + ); + + const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) => + row.some( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ), + ); + + const deckSlicesControl = deckSlicesRow?.find( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ) as any; + + const mockData = { + result: [ + { id: 1, slice_name: 'Chart A' }, + { id: 2, slice_name: 'Chart B' }, + ], + }; + + const result = deckSlicesControl.config.mutator(mockData, undefined); + + expect(result).toEqual([ + { value: 1, label: 'Chart A' }, + { value: 2, label: 'Chart B' }, + ]); +}); + +test('deck_slices mutator should preserve order based on selection', () => { + const mapSection = controlPanel.controlPanelSections.find( + ( + section: ControlPanelSectionConfig | null, + ): section is ControlPanelSectionConfig => + section !== null && section.label === 'Map', + ); + + const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) => + row.some( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ), + ); + + const deckSlicesControl = deckSlicesRow?.find( + (control: ControlSetItem) => + control && + typeof control === 'object' && + 'name' in control && + control.name === 'deck_slices', + ) as any; + + const mockData = { + result: [ + { id: 1, slice_name: 'Chart A' }, + { id: 2, slice_name: 'Chart B' }, + { id: 3, slice_name: 'Chart C' }, + { id: 4, slice_name: 'Chart D' }, + ], + }; + + // Select in specific order: 3, 1, 4 + const selectedIds = [3, 1, 4]; + const result = deckSlicesControl.config.mutator(mockData, selectedIds); + + expect(result).toEqual([ + { value: 1, label: 'Chart A [2]' }, // second in selection + { value: 2, label: 'Chart B' }, // not selected + { value: 3, label: 'Chart C [1]' }, // first in selection + { value: 4, label: 'Chart D [3]' }, // third in selection + ]); +}); + +test('Query section should include adhoc_filters control', () => { + const querySection = controlPanel.controlPanelSections.find( + ( + section: ControlPanelSectionConfig | null, + ): section is ControlPanelSectionConfig => + section !== null && section.label === 'Query', + ); + + const hasAdhocFilters = querySection?.controlSetRows.some( + (row: ControlSetRow) => row.includes('adhoc_filters'), + ); + + expect(hasAdhocFilters).toBe(true); +}); diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.ts index 1ad60394d7c..3bf2613e8a5 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.ts +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.ts @@ -18,7 +18,7 @@ */ import { t } from '@apache-superset/core'; import { validateNonEmpty } from '@superset-ui/core'; -import { viewport, mapboxStyle } from '../utilities/Shared_DeckGL'; +import { viewport, mapboxStyle, autozoom } from '../utilities/Shared_DeckGL'; export default { controlPanelSections: [ @@ -28,6 +28,7 @@ export default { controlSetRows: [ [mapboxStyle], [viewport], + [autozoom], [ { name: 'deck_slices',