mirror of
https://github.com/apache/superset.git
synced 2026-05-08 01:15:46 +00:00
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:
@@ -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',
|
||||
|
||||
@@ -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'>
|
||||
>,
|
||||
);
|
||||
Reference in New Issue
Block a user