chore(frontend): migrate SqlLab and explore JS/JSX files to TypeScript (#36760)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-01-06 10:52:58 -08:00
committed by GitHub
parent aaa174f820
commit 9aff89c1b4
69 changed files with 3272 additions and 1482 deletions

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import type React from 'react';
import { Route } from 'react-router-dom';
import fetchMock from 'fetch-mock';
import { DatasourceType, JsonObject, SupersetClient } from '@superset-ui/core';
@@ -28,7 +29,7 @@ import {
waitFor,
} from 'spec/helpers/testing-library';
import { fallbackExploreInitialData } from 'src/explore/fixtures';
import type { DatasetObject, ColumnObject } from 'src/features/datasets/types';
import type { ColumnObject } from 'src/features/datasets/types';
import DatasourceControl from '.';
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
@@ -46,20 +47,35 @@ afterEach(() => {
jest.clearAllMocks(); // Clears mock history but keeps spy in place
});
type TestDatasource = Omit<
Partial<DatasetObject>,
'columns' | 'main_dttm_col'
> & {
interface TestDatasource {
id?: number;
name: string;
database: { name: string };
datasource_name?: string;
database: {
id: number;
database_name: string;
name?: string;
backend?: string;
};
columns?: Partial<ColumnObject>[];
type?: DatasourceType;
main_dttm_col?: string | null;
};
owners?: Array<{
first_name: string;
last_name: string;
id: number;
username?: string;
}>;
sql?: string;
metrics?: Array<{ id: number; metric_name: string }>;
[key: string]: unknown;
}
const mockDatasource: TestDatasource = {
id: 25,
database: {
id: 1,
database_name: 'examples',
name: 'examples',
},
name: 'channels',
@@ -69,39 +85,50 @@ const mockDatasource: TestDatasource = {
owners: [{ first_name: 'john', last_name: 'doe', id: 1, username: 'jd' }],
sql: 'SELECT * FROM mock_datasource_sql',
};
const createProps = (overrides: JsonObject = {}) => ({
hovered: false,
type: 'DatasourceControl',
label: 'Datasource',
default: null,
description: null,
value: '25__table',
form_data: {},
datasource: mockDatasource,
validationErrors: [],
name: 'datasource',
actions: {
changeDatasource: jest.fn(),
setControlValue: jest.fn(),
},
isEditable: true,
user: {
createdOn: '2021-04-27T18:12:38.952304',
email: 'admin',
firstName: 'admin',
isActive: true,
lastName: 'admin',
permissions: {},
roles: { Admin: Array(173) },
userId: 1,
username: 'admin',
},
onChange: jest.fn(),
onDatasourceSave: jest.fn(),
...overrides,
});
async function openAndSaveChanges(datasource: TestDatasource) {
// Use type assertion for test props since the component is wrapped with withTheme
// The withTheme HOC makes the props type complex, so we cast through unknown to bypass type check
type DatasourceControlComponentProps = React.ComponentProps<
typeof DatasourceControl
>;
const createProps = (
overrides: JsonObject = {},
): DatasourceControlComponentProps =>
({
hovered: false,
type: 'DatasourceControl',
label: 'Datasource',
default: null,
description: null,
value: '25__table',
form_data: {},
datasource: mockDatasource,
validationErrors: [],
name: 'datasource',
actions: {
changeDatasource: jest.fn(),
setControlValue: jest.fn(),
},
isEditable: true,
user: {
createdOn: '2021-04-27T18:12:38.952304',
email: 'admin',
firstName: 'admin',
isActive: true,
lastName: 'admin',
permissions: {},
roles: { Admin: Array(173) },
userId: 1,
username: 'admin',
},
onChange: jest.fn(),
onDatasourceSave: jest.fn(),
...overrides,
}) as unknown as DatasourceControlComponentProps;
async function openAndSaveChanges(
datasource: TestDatasource | Record<string, unknown>,
) {
fetchMock.get(
'glob:*/api/v1/database/?q=*',
{ result: [] },
@@ -259,7 +286,6 @@ test('Click on Edit dataset', async () => {
test('Edit dataset should be disabled when user is not admin', async () => {
const props = createProps();
// @ts-expect-error
props.user.roles = {};
props.datasource.owners = [];
SupersetClientGet.mockImplementationOnce(
@@ -458,11 +484,11 @@ test('should not set the temporal column', async () => {
const overrideProps = {
...props,
form_data: {
granularity_sqla: null,
granularity_sqla: undefined,
},
datasource: {
...props.datasource,
main_dttm_col: null,
main_dttm_col: undefined,
columns: [
{
column_name: 'test-col',

View File

@@ -18,10 +18,20 @@
* under the License.
*/
import { PureComponent } from 'react';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { DatasourceType, SupersetClient, t } from '@superset-ui/core';
import { css, styled, withTheme } from '@apache-superset/core/ui';
import {
DatasourceType,
SupersetClient,
t,
Datasource,
} from '@superset-ui/core';
import {
css,
styled,
withTheme,
type SupersetTheme,
} from '@apache-superset/core/ui';
import { getTemporalColumns } from '@superset-ui/chart-controls';
import { getUrlParam } from 'src/utils/urlUtils';
import {
@@ -51,6 +61,68 @@ import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
import { safeStringify } from 'src/utils/safeStringify';
import { Link } from 'react-router-dom';
// Extended Datasource interface with all properties used in this component
interface ExtendedDatasource extends Datasource {
sql?: string;
select_star?: string;
owners?: Array<{
id: number;
first_name: string;
last_name: string;
value?: number;
}>;
extra?: string;
health_check_message?: string;
database?: {
id: number;
database_name: string;
backend?: string;
};
}
interface User {
userId?: number;
username?: string;
roles?: Record<string, unknown[]>;
}
interface DatasourceControlActions {
changeDatasource: (datasource: ExtendedDatasource) => void;
setControlValue: (name: string, value: unknown) => void;
}
interface FormData {
granularity_sqla?: string;
[key: string]: unknown;
}
interface DatasourceControlProps {
actions: DatasourceControlActions;
onChange?: () => void;
value?: string | null;
datasource: ExtendedDatasource;
form_data?: FormData;
isEditable?: boolean;
onDatasourceSave?: ((datasource: ExtendedDatasource) => void) | null;
theme: SupersetTheme;
user: User;
// ControlHeader-related props
hovered?: boolean;
type?: string;
label?: string;
default?: unknown;
description?: string | null;
validationErrors?: string[];
name?: string;
}
interface DatasourceControlState {
showEditDatasourceModal: boolean;
showChangeDatasourceModal: boolean;
showSaveDatasetModal: boolean;
showDatasource?: boolean;
}
const propTypes = {
actions: PropTypes.object.isRequired,
onChange: PropTypes.func,
@@ -59,6 +131,15 @@ const propTypes = {
form_data: PropTypes.object.isRequired,
isEditable: PropTypes.bool,
onDatasourceSave: PropTypes.func,
user: PropTypes.object.isRequired,
// ControlHeader-related props
hovered: PropTypes.bool,
type: PropTypes.string,
label: PropTypes.string,
default: PropTypes.any,
description: PropTypes.string,
validationErrors: PropTypes.array,
name: PropTypes.string,
};
const defaultProps = {
@@ -68,7 +149,7 @@ const defaultProps = {
isEditable: true,
};
const getDatasetType = datasource => {
const getDatasetType = (datasource: ExtendedDatasource): string => {
if (datasource.type === 'query') {
return 'query';
}
@@ -139,15 +220,18 @@ const SAVE_AS_DATASET = 'save_as_dataset';
const VISIBLE_TITLE_LENGTH = 25;
// Assign icon for each DatasourceType. If no icon assignment is found in the lookup, no icon will render
export const datasourceIconLookup = {
export const datasourceIconLookup: Record<string, React.ReactNode> = {
query: <Icons.ConsoleSqlOutlined className="datasource-svg" />,
physical_dataset: <Icons.TableOutlined className="datasource-svg" />,
virtual_dataset: <Icons.ConsoleSqlOutlined className="datasource-svg" />,
};
// Render title for datasource with tooltip only if text is longer than VISIBLE_TITLE_LENGTH
export const renderDatasourceTitle = (displayString, tooltip) =>
displayString?.length > VISIBLE_TITLE_LENGTH ? (
export const renderDatasourceTitle = (
displayString: string | undefined,
tooltip: string,
) =>
displayString?.length && displayString.length > VISIBLE_TITLE_LENGTH ? (
// Add a tooltip only for long names that will be visually truncated
<Tooltip title={tooltip}>
<span className="title-select">{displayString}</span>
@@ -159,12 +243,14 @@ export const renderDatasourceTitle = (displayString, tooltip) =>
);
// Different data source types use different attributes for the display title
export const getDatasourceTitle = datasource => {
if (datasource?.type === 'query') return datasource?.sql;
export const getDatasourceTitle = (
datasource: ExtendedDatasource | null | undefined,
): string => {
if (datasource?.type === 'query') return datasource?.sql || '';
return datasource?.name || '';
};
const preventRouterLinkWhileMetaClicked = evt => {
const preventRouterLinkWhileMetaClicked = (evt: React.MouseEvent) => {
if (evt.metaKey) {
evt.preventDefault();
} else {
@@ -172,8 +258,15 @@ const preventRouterLinkWhileMetaClicked = evt => {
}
};
class DatasourceControl extends PureComponent {
constructor(props) {
class DatasourceControl extends PureComponent<
DatasourceControlProps,
DatasourceControlState
> {
static propTypes = propTypes;
static defaultProps = defaultProps;
constructor(props: DatasourceControlProps) {
super(props);
this.state = {
showEditDatasourceModal: false,
@@ -182,10 +275,13 @@ class DatasourceControl extends PureComponent {
};
}
onDatasourceSave = datasource => {
this.props.actions.changeDatasource(datasource);
const { temporalColumns, defaultTemporalColumn } =
getTemporalColumns(datasource);
onDatasourceSave = (datasource: Datasource) => {
// Cast to ExtendedDatasource for the component's internal use
this.props.actions.changeDatasource(datasource as ExtendedDatasource);
// Cast datasource for getTemporalColumns which expects Dataset | QueryResponse
const { temporalColumns, defaultTemporalColumn } = getTemporalColumns(
datasource as Parameters<typeof getTemporalColumns>[0],
);
const { columns } = datasource;
// the current granularity_sqla might not be a temporal column anymore
const timeCol = this.props.form_data?.granularity_sqla;
@@ -238,7 +334,7 @@ class DatasourceControl extends PureComponent {
}));
};
handleMenuItemClick = ({ key }) => {
handleMenuItemClick = ({ key }: { key: string }) => {
switch (key) {
case CHANGE_DATASET:
this.toggleChangeDatasourceModal();
@@ -371,12 +467,17 @@ class DatasourceControl extends PureComponent {
modalBody={
<ViewQuery
sql={datasource?.sql || datasource?.select_star || ''}
datasource={`${datasource.id}__${datasource.type}`}
/>
}
modalFooter={
<ViewQueryModalFooter
changeDatasource={this.toggleSaveDatasetModal}
datasource={datasource}
datasource={{
id: String(datasource.id),
sql: datasource.sql || '',
type: datasource.type,
}}
/>
}
draggable={false}
@@ -406,7 +507,7 @@ class DatasourceControl extends PureComponent {
queryDatasourceMenuItems.push({
key: SAVE_AS_DATASET,
label: t('Save as dataset'),
label: <span>{t('Save as dataset')}</span>,
});
const queryDatasourceMenu = (
@@ -464,8 +565,8 @@ class DatasourceControl extends PureComponent {
{isMissingDatasource && isMissingParams && (
<div className="error-alert">
<ErrorAlert
level="warning"
errorType={t('Missing URL parameters')}
type="warning"
message={t('Missing URL parameters')}
description={t(
'The URL is missing the dataset_id or slice_id parameters.',
)}
@@ -486,7 +587,7 @@ class DatasourceControl extends PureComponent {
) : (
<ErrorAlert
type="warning"
errorType={t('Missing dataset')}
message={t('Missing dataset')}
descriptionPre={false}
descriptionDetailsCollapsed={false}
descriptionDetails={
@@ -498,7 +599,7 @@ class DatasourceControl extends PureComponent {
</p>
<p>
<Button
buttonStyle="warning"
buttonStyle="primary"
onClick={() =>
this.handleMenuItemClick({ key: CHANGE_DATASET })
}
@@ -547,7 +648,9 @@ class DatasourceControl extends PureComponent {
}
}
DatasourceControl.propTypes = propTypes;
DatasourceControl.defaultProps = defaultProps;
export default withTheme(DatasourceControl);
// withTheme injects the theme prop, so we need to cast the component type
export default withTheme(
DatasourceControl as React.ComponentType<
Omit<DatasourceControlProps, 'theme'>
>,
);