mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat(deckgl): add auto zoom option in deck gl multi layer (#37221)
This commit is contained in:
committed by
GitHub
parent
429d9b27f6
commit
baaa8c5f54
@@ -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) => (
|
||||
<div
|
||||
data-test="deckgl-container"
|
||||
data-viewport={JSON.stringify(viewport)}
|
||||
data-layers-count={layers?.length || 0}
|
||||
>
|
||||
DeckGL Container Mock
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// 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(
|
||||
<Provider store={mockStore}>
|
||||
<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
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(<DeckMulti {...props} />);
|
||||
|
||||
// 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(<DeckMulti {...props} />);
|
||||
|
||||
// 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(<DeckMulti {...props} />);
|
||||
|
||||
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(<DeckMulti {...props} />);
|
||||
|
||||
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(<DeckMulti {...props} />);
|
||||
|
||||
// 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(<DeckMulti {...props} />);
|
||||
|
||||
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(<DeckMulti {...props} />);
|
||||
|
||||
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(<DeckMulti {...props} />);
|
||||
|
||||
// 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(<DeckMulti {...props} />);
|
||||
|
||||
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(<DeckMulti {...baseMockProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('deckgl-container')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass correct props to DeckGLContainer', async () => {
|
||||
renderWithProviders(<DeckMulti {...baseMockProps} />);
|
||||
|
||||
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(<DeckMulti {...baseMockProps} />);
|
||||
|
||||
// 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(
|
||||
<Provider store={mockStore}>
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<DeckMulti {...updatedProps} />
|
||||
</ThemeProvider>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user