mirror of
https://github.com/apache/superset.git
synced 2026-05-11 02:45:46 +00:00
feat: AI-powered TypeScript migration framework with parallel processing (#35045)
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Mehmet Salih Yavuz <salih.yavuz@proton.me> Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>
This commit is contained in:
committed by
GitHub
parent
076e477fd4
commit
ecb3ac68ff
66
superset-frontend/src/utils/DebouncedMessageQueue.test.ts
Normal file
66
superset-frontend/src/utils/DebouncedMessageQueue.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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 DebouncedMessageQueue from './DebouncedMessageQueue';
|
||||
|
||||
describe('DebouncedMessageQueue', () => {
|
||||
it('should create a queue with default options', () => {
|
||||
const queue = new DebouncedMessageQueue();
|
||||
expect(queue).toBeDefined();
|
||||
expect(queue.trigger).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
it('should accept custom configuration options', () => {
|
||||
const mockCallback = jest.fn();
|
||||
const queue = new DebouncedMessageQueue({
|
||||
callback: mockCallback,
|
||||
sizeThreshold: 500,
|
||||
delayThreshold: 2000,
|
||||
});
|
||||
expect(queue).toBeDefined();
|
||||
});
|
||||
|
||||
it('should append items to the queue', () => {
|
||||
const mockCallback = jest.fn();
|
||||
const queue = new DebouncedMessageQueue({ callback: mockCallback });
|
||||
|
||||
const testEvent = { id: 1, message: 'test' };
|
||||
queue.append(testEvent);
|
||||
|
||||
// Verify the append method doesn't throw
|
||||
expect(() => queue.append(testEvent)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle generic types properly', () => {
|
||||
interface TestEvent {
|
||||
id: number;
|
||||
data: string;
|
||||
}
|
||||
|
||||
const mockCallback = jest.fn();
|
||||
const queue = new DebouncedMessageQueue<TestEvent>({
|
||||
callback: mockCallback,
|
||||
});
|
||||
|
||||
const testEvent: TestEvent = { id: 1, data: 'test' };
|
||||
queue.append(testEvent);
|
||||
|
||||
expect(() => queue.append(testEvent)).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -18,26 +18,45 @@
|
||||
*/
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
class DebouncedMessageQueue {
|
||||
export interface DebouncedMessageQueueOptions<T> {
|
||||
callback?: (events: T[]) => void;
|
||||
sizeThreshold?: number;
|
||||
delayThreshold?: number;
|
||||
}
|
||||
|
||||
class DebouncedMessageQueue<T = Record<string, unknown>> {
|
||||
private queue: T[];
|
||||
|
||||
private readonly sizeThreshold: number;
|
||||
|
||||
private readonly delayThreshold: number;
|
||||
|
||||
private readonly callback: (events: T[]) => void;
|
||||
|
||||
public readonly trigger: () => void;
|
||||
|
||||
constructor({
|
||||
callback = () => {},
|
||||
sizeThreshold = 1000,
|
||||
delayThreshold = 1000,
|
||||
}) {
|
||||
}: DebouncedMessageQueueOptions<T> = {}) {
|
||||
this.queue = [];
|
||||
this.sizeThreshold = sizeThreshold;
|
||||
this.delayThreshold = delayThreshold;
|
||||
|
||||
this.trigger = debounce(this.trigger.bind(this), this.delayThreshold);
|
||||
this.callback = callback;
|
||||
|
||||
this.trigger = debounce(
|
||||
this.triggerInternal.bind(this),
|
||||
this.delayThreshold,
|
||||
);
|
||||
}
|
||||
|
||||
append(eventData) {
|
||||
append(eventData: T): void {
|
||||
this.queue.push(eventData);
|
||||
this.trigger();
|
||||
}
|
||||
|
||||
trigger() {
|
||||
private triggerInternal(): void {
|
||||
if (this.queue.length > 0) {
|
||||
const events = this.queue.splice(0, this.sizeThreshold);
|
||||
this.callback.call(null, events);
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const getDatasourceAsSaveableDataset = source => ({
|
||||
columns: source.columns,
|
||||
name: source?.datasource_name || source?.name || 'Untitled',
|
||||
dbId: source?.database?.id || source?.dbId,
|
||||
sql: source?.sql || '',
|
||||
catalog: source?.catalog,
|
||||
schema: source?.schema,
|
||||
templateParams: source?.templateParams,
|
||||
});
|
||||
190
superset-frontend/src/utils/datasourceUtils.test.ts
Normal file
190
superset-frontend/src/utils/datasourceUtils.test.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 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 { ColumnMeta, Metric } from '@superset-ui/chart-controls';
|
||||
import { DatasourceType } from '@superset-ui/core';
|
||||
import type { Datasource } from 'src/explore/types';
|
||||
import type { QueryEditor } from 'src/SqlLab/types';
|
||||
import { getDatasourceAsSaveableDataset } from './datasourceUtils';
|
||||
|
||||
const mockColumnMeta: ColumnMeta = {
|
||||
column_name: 'test_column',
|
||||
type: 'VARCHAR',
|
||||
is_dttm: false,
|
||||
verbose_name: 'Test Column',
|
||||
description: 'A test column',
|
||||
expression: '',
|
||||
filterable: true,
|
||||
groupby: true,
|
||||
id: 1,
|
||||
type_generic: 1,
|
||||
python_date_format: null,
|
||||
optionName: 'test_column',
|
||||
};
|
||||
|
||||
const mockMetric: Metric = {
|
||||
id: 1,
|
||||
uuid: 'metric-1',
|
||||
metric_name: 'count',
|
||||
verbose_name: 'Count',
|
||||
description: 'Count of records',
|
||||
d3format: null,
|
||||
currency: null,
|
||||
warning_text: null,
|
||||
// optionName removed - not part of Metric interface
|
||||
};
|
||||
|
||||
const mockDatasource: Datasource = {
|
||||
id: 1,
|
||||
type: DatasourceType.Table,
|
||||
columns: [mockColumnMeta],
|
||||
metrics: [mockMetric],
|
||||
column_formats: {},
|
||||
verbose_map: {},
|
||||
main_dttm_col: '',
|
||||
order_by_choices: null,
|
||||
datasource_name: 'Test Datasource',
|
||||
name: 'test_table',
|
||||
catalog: 'test_catalog',
|
||||
schema: 'test_schema',
|
||||
description: 'Test datasource',
|
||||
database: {
|
||||
id: 123,
|
||||
database_name: 'test_db',
|
||||
sqlalchemy_uri: 'postgresql://test',
|
||||
},
|
||||
};
|
||||
|
||||
const mockQueryEditor: QueryEditor = {
|
||||
id: 'query-1',
|
||||
immutableId: 'immutable-query-1',
|
||||
version: 1,
|
||||
name: 'Test Query',
|
||||
sql: 'SELECT * FROM users',
|
||||
dbId: 456,
|
||||
autorun: false,
|
||||
remoteId: null,
|
||||
catalog: 'prod_catalog',
|
||||
schema: 'public',
|
||||
templateParams: '{"param1": "value1"}',
|
||||
};
|
||||
|
||||
describe('getDatasourceAsSaveableDataset', () => {
|
||||
test('should convert Datasource object correctly', () => {
|
||||
const result = getDatasourceAsSaveableDataset(mockDatasource);
|
||||
|
||||
expect(result).toEqual({
|
||||
columns: [mockColumnMeta],
|
||||
name: 'Test Datasource',
|
||||
dbId: 123,
|
||||
sql: '',
|
||||
catalog: 'test_catalog',
|
||||
schema: 'test_schema',
|
||||
templateParams: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('should convert QueryEditor object correctly', () => {
|
||||
const queryWithColumns = { ...mockQueryEditor, columns: [mockColumnMeta] };
|
||||
const result = getDatasourceAsSaveableDataset(queryWithColumns);
|
||||
|
||||
expect(result).toEqual({
|
||||
columns: [mockColumnMeta],
|
||||
name: 'Test Query',
|
||||
dbId: 456,
|
||||
sql: 'SELECT * FROM users',
|
||||
catalog: 'prod_catalog',
|
||||
schema: 'public',
|
||||
templateParams: '{"param1": "value1"}',
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle datasource with fallback name from name property', () => {
|
||||
const datasourceWithoutDatasourceName: Datasource = {
|
||||
...mockDatasource,
|
||||
datasource_name: null,
|
||||
name: 'fallback_name',
|
||||
};
|
||||
|
||||
const result = getDatasourceAsSaveableDataset(
|
||||
datasourceWithoutDatasourceName,
|
||||
);
|
||||
|
||||
expect(result.name).toBe('fallback_name');
|
||||
});
|
||||
|
||||
test('should use "Untitled" as fallback when no name is available', () => {
|
||||
const datasourceWithoutName: Datasource = {
|
||||
...mockDatasource,
|
||||
datasource_name: null,
|
||||
name: '',
|
||||
};
|
||||
|
||||
const result = getDatasourceAsSaveableDataset(datasourceWithoutName);
|
||||
|
||||
expect(result.name).toBe('Untitled');
|
||||
});
|
||||
|
||||
test('should handle missing database object', () => {
|
||||
const datasourceWithoutDatabase: Datasource = {
|
||||
...mockDatasource,
|
||||
database: undefined,
|
||||
};
|
||||
|
||||
const result = getDatasourceAsSaveableDataset(datasourceWithoutDatabase);
|
||||
|
||||
expect(result.dbId).toBe(0);
|
||||
});
|
||||
|
||||
test('should handle QueryEditor with missing dbId', () => {
|
||||
const queryEditorWithoutDbId: QueryEditor = {
|
||||
...mockQueryEditor,
|
||||
dbId: undefined,
|
||||
};
|
||||
|
||||
const result = getDatasourceAsSaveableDataset(queryEditorWithoutDbId);
|
||||
|
||||
expect(result.dbId).toBe(0);
|
||||
});
|
||||
|
||||
test('should handle QueryEditor without sql property', () => {
|
||||
const queryEditorWithoutSql: QueryEditor = {
|
||||
...mockQueryEditor,
|
||||
sql: '',
|
||||
};
|
||||
|
||||
const result = getDatasourceAsSaveableDataset(queryEditorWithoutSql);
|
||||
|
||||
expect(result.sql).toBe('');
|
||||
});
|
||||
|
||||
test('should handle null values for optional properties', () => {
|
||||
const minimalQueryEditor: QueryEditor = {
|
||||
...mockQueryEditor,
|
||||
catalog: null,
|
||||
schema: undefined,
|
||||
templateParams: '',
|
||||
};
|
||||
|
||||
const result = getDatasourceAsSaveableDataset(minimalQueryEditor);
|
||||
|
||||
expect(result.catalog).toBe(null);
|
||||
expect(result.schema).toBe(null);
|
||||
expect(result.templateParams).toBe(null);
|
||||
});
|
||||
});
|
||||
57
superset-frontend/src/utils/datasourceUtils.ts
Normal file
57
superset-frontend/src/utils/datasourceUtils.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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 { ColumnMeta } from '@superset-ui/chart-controls';
|
||||
import type { ISaveableDatasource } from 'src/SqlLab/components/SaveDatasetModal';
|
||||
|
||||
// Flexible interface that captures what this function actually needs to work
|
||||
// This allows it to accept various datasource-like objects from different parts of the codebase
|
||||
interface DatasourceInput {
|
||||
// Common properties that all datasource-like objects should have
|
||||
name?: string | null; // Allow null for compatibility
|
||||
|
||||
// Optional properties that may exist on different datasource variants
|
||||
datasource_name?: string | null; // Allow null for compatibility
|
||||
columns?: any[]; // Can be ColumnMeta[], DatasourcePanelColumn[], ISimpleColumn[], etc.
|
||||
database?: { id?: number };
|
||||
dbId?: number;
|
||||
sql?: string | null; // Allow null for compatibility
|
||||
catalog?: string | null;
|
||||
schema?: string | null;
|
||||
templateParams?: string;
|
||||
|
||||
// Type discriminator for QueryEditor-like objects
|
||||
version?: number;
|
||||
}
|
||||
|
||||
export const getDatasourceAsSaveableDataset = (
|
||||
source: DatasourceInput,
|
||||
): ISaveableDatasource => {
|
||||
// Type guard: QueryEditor-like objects have version property
|
||||
const isQueryEditorLike = typeof source.version === 'number';
|
||||
|
||||
return {
|
||||
columns: (source.columns as ColumnMeta[]) || [],
|
||||
name: source.datasource_name || source.name || 'Untitled',
|
||||
dbId: source.database?.id || source.dbId || 0,
|
||||
sql: source.sql || '',
|
||||
catalog: source.catalog || null,
|
||||
schema: source.schema || null,
|
||||
templateParams: isQueryEditorLike ? source.templateParams || null : null,
|
||||
};
|
||||
};
|
||||
101
superset-frontend/src/utils/getControlsForVizType.test.ts
Normal file
101
superset-frontend/src/utils/getControlsForVizType.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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 { getChartControlPanelRegistry, JsonObject } from '@superset-ui/core';
|
||||
import getControlsForVizType from 'src/utils/getControlsForVizType';
|
||||
|
||||
const fakePluginControls: JsonObject = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Fake Control Panel Sections',
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'y_axis_bounds',
|
||||
config: {
|
||||
type: 'BoundsControl',
|
||||
label: 'Value bounds',
|
||||
default: [null, null],
|
||||
description: 'Value bounds for the y axis',
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'adhoc_filters',
|
||||
config: {
|
||||
type: 'AdhocFilterControl',
|
||||
label: 'Fake Filters',
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Fake Control Panel Sections 2',
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'column_collection',
|
||||
config: {
|
||||
type: 'CollectionControl',
|
||||
label: 'Fake Collection Control',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('getControlsForVizType', () => {
|
||||
beforeEach(() => {
|
||||
getChartControlPanelRegistry().registerValue(
|
||||
'chart_controls_inventory_fake',
|
||||
fakePluginControls,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a map of the controls', () => {
|
||||
expect(
|
||||
JSON.stringify(getControlsForVizType('chart_controls_inventory_fake')),
|
||||
).toEqual(
|
||||
JSON.stringify({
|
||||
y_axis_bounds: {
|
||||
type: 'BoundsControl',
|
||||
label: 'Value bounds',
|
||||
default: [null, null],
|
||||
description: 'Value bounds for the y axis',
|
||||
},
|
||||
adhoc_filters: {
|
||||
type: 'AdhocFilterControl',
|
||||
label: 'Fake Filters',
|
||||
default: null,
|
||||
},
|
||||
column_collection: {
|
||||
type: 'CollectionControl',
|
||||
label: 'Fake Collection Control',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
74
superset-frontend/src/utils/getControlsForVizType.ts
Normal file
74
superset-frontend/src/utils/getControlsForVizType.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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 memoizeOne from 'memoize-one';
|
||||
import { isControlPanelSectionConfig } from '@superset-ui/chart-controls';
|
||||
import { getChartControlPanelRegistry, JsonObject } from '@superset-ui/core';
|
||||
import type { ControlMap } from 'src/components/AlteredSliceTag/types';
|
||||
import { controls } from '../explore/controls';
|
||||
|
||||
const memoizedControls = memoizeOne(
|
||||
(vizType: string, controlPanel: JsonObject | undefined): ControlMap => {
|
||||
const controlsMap: ControlMap = {};
|
||||
if (!controlPanel) return controlsMap;
|
||||
|
||||
const sections = controlPanel.controlPanelSections || [];
|
||||
(Array.isArray(sections) ? sections : [])
|
||||
.filter(isControlPanelSectionConfig)
|
||||
.forEach(section => {
|
||||
if (section.controlSetRows && Array.isArray(section.controlSetRows)) {
|
||||
section.controlSetRows.forEach(row => {
|
||||
if (Array.isArray(row)) {
|
||||
row.forEach(control => {
|
||||
if (!control) return;
|
||||
if (typeof control === 'string') {
|
||||
// For now, we have to look in controls.jsx to get the config for some controls.
|
||||
// Once everything is migrated out, delete this if statement.
|
||||
const controlConfig = (controls as any)[control];
|
||||
if (controlConfig) {
|
||||
controlsMap[control] = controlConfig;
|
||||
}
|
||||
} else if (
|
||||
typeof control === 'object' &&
|
||||
control &&
|
||||
'name' in control &&
|
||||
'config' in control
|
||||
) {
|
||||
// condition needed because there are elements, e.g. <hr /> in some control configs (I'm looking at you, FilterBox!)
|
||||
const controlObj = control as {
|
||||
name: string;
|
||||
config: JsonObject;
|
||||
};
|
||||
controlsMap[controlObj.name] = controlObj.config;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return controlsMap;
|
||||
},
|
||||
);
|
||||
|
||||
const getControlsForVizType = (vizType: string): ControlMap => {
|
||||
const controlPanel = getChartControlPanelRegistry().get(vizType);
|
||||
return memoizedControls(vizType, controlPanel);
|
||||
};
|
||||
|
||||
export default getControlsForVizType;
|
||||
58
superset-frontend/src/utils/hostNamesConfig.test.ts
Normal file
58
superset-frontend/src/utils/hostNamesConfig.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 { availableDomains, allowCrossDomain } from './hostNamesConfig';
|
||||
|
||||
describe('hostNamesConfig', () => {
|
||||
beforeEach(() => {
|
||||
// Reset DOM
|
||||
document.body.innerHTML = '';
|
||||
|
||||
// Mock window.location
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
hostname: 'localhost',
|
||||
search: '',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('should export availableDomains as array of strings', () => {
|
||||
expect(Array.isArray(availableDomains)).toBe(true);
|
||||
availableDomains.forEach(domain => {
|
||||
expect(typeof domain).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
test('should export allowCrossDomain as boolean', () => {
|
||||
expect(typeof allowCrossDomain).toBe('boolean');
|
||||
});
|
||||
|
||||
test('should determine allowCrossDomain based on availableDomains length', () => {
|
||||
const expectedValue = availableDomains.length > 1;
|
||||
expect(allowCrossDomain).toBe(expectedValue);
|
||||
});
|
||||
|
||||
test('availableDomains should contain at least the current hostname', () => {
|
||||
// Since we're testing the already computed values, we check they contain localhost
|
||||
// or the configuration returns empty array if app container is missing
|
||||
expect(availableDomains.length >= 0).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -19,7 +19,7 @@
|
||||
import { initFeatureFlags } from '@superset-ui/core';
|
||||
import getBootstrapData from './getBootstrapData';
|
||||
|
||||
function getDomainsConfig() {
|
||||
function getDomainsConfig(): string[] {
|
||||
const appContainer = document.getElementById('app');
|
||||
if (!appContainer) {
|
||||
return [];
|
||||
@@ -42,13 +42,15 @@ function getDomainsConfig() {
|
||||
initFeatureFlags(bootstrapData.common.feature_flags);
|
||||
|
||||
if (bootstrapData?.common?.conf?.SUPERSET_WEBSERVER_DOMAINS) {
|
||||
bootstrapData.common.conf.SUPERSET_WEBSERVER_DOMAINS.forEach(hostName => {
|
||||
const domains = bootstrapData.common.conf
|
||||
.SUPERSET_WEBSERVER_DOMAINS as string[];
|
||||
domains.forEach((hostName: string) => {
|
||||
availableDomains.add(hostName);
|
||||
});
|
||||
}
|
||||
return Array.from(availableDomains);
|
||||
}
|
||||
|
||||
export const availableDomains = getDomainsConfig();
|
||||
export const availableDomains: string[] = getDomainsConfig();
|
||||
|
||||
export const allowCrossDomain = availableDomains.length > 1;
|
||||
export const allowCrossDomain: boolean = availableDomains.length > 1;
|
||||
129
superset-frontend/src/utils/reducerUtils.test.ts
Normal file
129
superset-frontend/src/utils/reducerUtils.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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 {
|
||||
addToObject,
|
||||
alterInObject,
|
||||
alterInArr,
|
||||
removeFromArr,
|
||||
addToArr,
|
||||
} from './reducerUtils';
|
||||
|
||||
interface TestItem {
|
||||
id?: string;
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const mockState = {
|
||||
objects: {
|
||||
'item-1': { id: 'item-1', name: 'Item 1', value: 10 },
|
||||
'item-2': { id: 'item-2', name: 'Item 2', value: 20 },
|
||||
},
|
||||
items: [
|
||||
{ id: 'item-1', name: 'Item 1', value: 10 },
|
||||
{ id: 'item-2', name: 'Item 2', value: 20 },
|
||||
],
|
||||
};
|
||||
|
||||
test('addToObject adds new object to state with generated id', () => {
|
||||
const newItem: TestItem = { name: 'New Item', value: 30 };
|
||||
const result = addToObject(mockState, 'objects', newItem);
|
||||
|
||||
expect(result).not.toBe(mockState);
|
||||
expect(result.objects).not.toBe(mockState.objects);
|
||||
expect(Object.keys(result.objects)).toHaveLength(3);
|
||||
|
||||
const addedItems = Object.values(result.objects).filter(
|
||||
item => (item as TestItem).name === 'New Item',
|
||||
);
|
||||
expect(addedItems).toHaveLength(1);
|
||||
expect((addedItems[0] as TestItem).id).toBeTruthy();
|
||||
});
|
||||
|
||||
test('addToObject adds new object with existing id', () => {
|
||||
const newItem: TestItem = { id: 'item-3', name: 'Item 3', value: 30 };
|
||||
const result = addToObject(mockState, 'objects', newItem);
|
||||
|
||||
expect(result.objects['item-3']).toEqual(newItem);
|
||||
});
|
||||
|
||||
test('alterInObject modifies existing object', () => {
|
||||
const targetItem: TestItem = { id: 'item-1', name: 'Item 1', value: 10 };
|
||||
const alterations = { value: 15 };
|
||||
const result = alterInObject(mockState, 'objects', targetItem, alterations);
|
||||
|
||||
expect(result.objects['item-1'].value).toBe(15);
|
||||
expect(result.objects['item-1'].name).toBe('Item 1');
|
||||
expect(result.objects['item-2']).toBe(mockState.objects['item-2']);
|
||||
});
|
||||
|
||||
test('alterInArr modifies existing array item', () => {
|
||||
const targetItem: TestItem = { id: 'item-1', name: 'Item 1', value: 10 };
|
||||
const alterations = { value: 15 };
|
||||
const result = alterInArr(mockState, 'items', targetItem, alterations);
|
||||
|
||||
expect(result.items[0].value).toBe(15);
|
||||
expect(result.items[0].name).toBe('Item 1');
|
||||
expect(result.items[1]).toBe(mockState.items[1]);
|
||||
});
|
||||
|
||||
test('removeFromArr removes item from array', () => {
|
||||
const targetItem: TestItem = { id: 'item-1', name: 'Item 1', value: 10 };
|
||||
const result = removeFromArr(mockState, 'items', targetItem);
|
||||
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.items[0].id).toBe('item-2');
|
||||
});
|
||||
|
||||
test('removeFromArr with custom idKey', () => {
|
||||
const stateWithCustomKey = {
|
||||
items: [
|
||||
{ customId: 'a', name: 'Item A' },
|
||||
{ customId: 'b', name: 'Item B' },
|
||||
],
|
||||
};
|
||||
const targetItem = { customId: 'a', name: 'Item A' };
|
||||
const result = removeFromArr(
|
||||
stateWithCustomKey,
|
||||
'items',
|
||||
targetItem,
|
||||
'customId',
|
||||
);
|
||||
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.items[0].customId).toBe('b');
|
||||
});
|
||||
|
||||
test('addToArr adds new item to array with generated id', () => {
|
||||
const newItem: TestItem = { name: 'New Item', value: 30 };
|
||||
const result = addToArr(mockState, 'items', newItem);
|
||||
|
||||
expect(result.items).toHaveLength(3);
|
||||
expect(result.items[2].name).toBe('New Item');
|
||||
expect(result.items[2].id).toBeTruthy();
|
||||
});
|
||||
|
||||
test('addToArr adds new item with existing id', () => {
|
||||
const newItem: TestItem = { id: 'item-3', name: 'Item 3', value: 30 };
|
||||
const result = addToArr(mockState, 'items', newItem);
|
||||
|
||||
expect(result.items).toHaveLength(3);
|
||||
expect(result.items[2]).toEqual(newItem);
|
||||
});
|
||||
107
superset-frontend/src/utils/reducerUtils.ts
Normal file
107
superset-frontend/src/utils/reducerUtils.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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 { nanoid } from 'nanoid';
|
||||
|
||||
interface ObjectWithId {
|
||||
id?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface StateWithObject {
|
||||
[key: string]: { [id: string]: ObjectWithId } | any;
|
||||
}
|
||||
|
||||
interface StateWithArray {
|
||||
[key: string]: ObjectWithId[] | any;
|
||||
}
|
||||
|
||||
export function addToObject<T extends ObjectWithId>(
|
||||
state: StateWithObject,
|
||||
arrKey: string,
|
||||
obj: T,
|
||||
): StateWithObject {
|
||||
const newObject = { ...state[arrKey] };
|
||||
const copiedObject = { ...obj };
|
||||
|
||||
if (!copiedObject.id) {
|
||||
copiedObject.id = nanoid();
|
||||
}
|
||||
newObject[copiedObject.id] = copiedObject;
|
||||
return { ...state, [arrKey]: newObject };
|
||||
}
|
||||
|
||||
export function alterInObject<T extends ObjectWithId>(
|
||||
state: StateWithObject,
|
||||
arrKey: string,
|
||||
obj: T,
|
||||
alterations: Partial<T>,
|
||||
): StateWithObject {
|
||||
const newObject = { ...state[arrKey] };
|
||||
newObject[obj.id!] = { ...newObject[obj.id!], ...alterations };
|
||||
return { ...state, [arrKey]: newObject };
|
||||
}
|
||||
|
||||
export function alterInArr<T extends ObjectWithId>(
|
||||
state: StateWithArray,
|
||||
arrKey: string,
|
||||
obj: T,
|
||||
alterations: Partial<T>,
|
||||
): StateWithArray {
|
||||
// Finds an item in an array in the state and replaces it with a
|
||||
// new object with an altered property
|
||||
const idKey = 'id';
|
||||
const newArr: T[] = [];
|
||||
state[arrKey].forEach((arrItem: T) => {
|
||||
if (obj[idKey] === arrItem[idKey]) {
|
||||
newArr.push({ ...arrItem, ...alterations });
|
||||
} else {
|
||||
newArr.push(arrItem);
|
||||
}
|
||||
});
|
||||
return { ...state, [arrKey]: newArr };
|
||||
}
|
||||
|
||||
export function removeFromArr<T extends ObjectWithId>(
|
||||
state: StateWithArray,
|
||||
arrKey: string,
|
||||
obj: T,
|
||||
idKey = 'id',
|
||||
): StateWithArray {
|
||||
const newArr: T[] = [];
|
||||
state[arrKey].forEach((arrItem: T) => {
|
||||
if (!(obj[idKey as keyof T] === arrItem[idKey as keyof T])) {
|
||||
newArr.push(arrItem);
|
||||
}
|
||||
});
|
||||
return { ...state, [arrKey]: newArr };
|
||||
}
|
||||
|
||||
export function addToArr<T extends ObjectWithId>(
|
||||
state: StateWithArray,
|
||||
arrKey: string,
|
||||
obj: T,
|
||||
): StateWithArray {
|
||||
const newObj = { ...obj };
|
||||
if (!newObj.id) {
|
||||
newObj.id = nanoid();
|
||||
}
|
||||
const newState: { [key: string]: T[] } = {};
|
||||
newState[arrKey] = [...state[arrKey], newObj];
|
||||
return { ...state, ...newState };
|
||||
}
|
||||
Reference in New Issue
Block a user