mirror of
https://github.com/apache/superset.git
synced 2026-04-24 18:44:53 +00:00
feat(SIP-95): catalogs in SQL Lab and datasets (#28376)
This commit is contained in:
@@ -74,6 +74,7 @@ const AceEditorWrapper = ({
|
||||
'id',
|
||||
'dbId',
|
||||
'sql',
|
||||
'catalog',
|
||||
'schema',
|
||||
'templateParams',
|
||||
'cursorPosition',
|
||||
@@ -161,6 +162,7 @@ const AceEditorWrapper = ({
|
||||
|
||||
const { data: annotations } = useAnnotations({
|
||||
dbId: queryEditor.dbId,
|
||||
catalog: queryEditor.catalog,
|
||||
schema: queryEditor.schema,
|
||||
sql: currentSql,
|
||||
templateParams: queryEditor.templateParams,
|
||||
@@ -170,6 +172,7 @@ const AceEditorWrapper = ({
|
||||
{
|
||||
queryEditorId,
|
||||
dbId: queryEditor.dbId,
|
||||
catalog: queryEditor.catalog,
|
||||
schema: queryEditor.schema,
|
||||
},
|
||||
!autocomplete,
|
||||
|
||||
@@ -189,7 +189,12 @@ test('returns column keywords among selected tables', async () => {
|
||||
storeWithSqlLab.dispatch(
|
||||
tableApiUtil.upsertQueryData(
|
||||
'tableMetadata',
|
||||
{ dbId: expectDbId, schema: expectSchema, table: expectTable },
|
||||
{
|
||||
dbId: expectDbId,
|
||||
catalog: null,
|
||||
schema: expectSchema,
|
||||
table: expectTable,
|
||||
},
|
||||
{
|
||||
name: expectTable,
|
||||
columns: [
|
||||
@@ -205,7 +210,12 @@ test('returns column keywords among selected tables', async () => {
|
||||
storeWithSqlLab.dispatch(
|
||||
tableApiUtil.upsertQueryData(
|
||||
'tableMetadata',
|
||||
{ dbId: expectDbId, schema: expectSchema, table: unexpectedTable },
|
||||
{
|
||||
dbId: expectDbId,
|
||||
catalog: null,
|
||||
schema: expectSchema,
|
||||
table: unexpectedTable,
|
||||
},
|
||||
{
|
||||
name: unexpectedTable,
|
||||
columns: [
|
||||
@@ -227,6 +237,7 @@ test('returns column keywords among selected tables', async () => {
|
||||
useKeywords({
|
||||
queryEditorId: expectQueryEditorId,
|
||||
dbId: expectDbId,
|
||||
catalog: null,
|
||||
schema: expectSchema,
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -42,6 +42,7 @@ import { SqlLabRootState } from 'src/SqlLab/types';
|
||||
type Params = {
|
||||
queryEditorId: string | number;
|
||||
dbId?: string | number;
|
||||
catalog?: string | null;
|
||||
schema?: string;
|
||||
};
|
||||
|
||||
@@ -58,7 +59,7 @@ const getHelperText = (value: string) =>
|
||||
const extensionsRegistry = getExtensionsRegistry();
|
||||
|
||||
export function useKeywords(
|
||||
{ queryEditorId, dbId, schema }: Params,
|
||||
{ queryEditorId, dbId, catalog, schema }: Params,
|
||||
skip = false,
|
||||
) {
|
||||
const useCustomKeywords = extensionsRegistry.get(
|
||||
@@ -68,6 +69,7 @@ export function useKeywords(
|
||||
const customKeywords = useCustomKeywords?.({
|
||||
queryEditorId: String(queryEditorId),
|
||||
dbId,
|
||||
catalog,
|
||||
schema,
|
||||
});
|
||||
const dispatch = useDispatch();
|
||||
@@ -78,6 +80,7 @@ export function useKeywords(
|
||||
const { data: schemaOptions } = useSchemasQueryState(
|
||||
{
|
||||
dbId,
|
||||
catalog: catalog || undefined,
|
||||
forceRefresh: false,
|
||||
},
|
||||
{ skip: skipFetch || !dbId },
|
||||
@@ -85,6 +88,7 @@ export function useKeywords(
|
||||
const { data: tableData } = useTablesQueryState(
|
||||
{
|
||||
dbId,
|
||||
catalog,
|
||||
schema,
|
||||
forceRefresh: false,
|
||||
},
|
||||
@@ -125,6 +129,7 @@ export function useKeywords(
|
||||
dbId && schema
|
||||
? {
|
||||
dbId,
|
||||
catalog,
|
||||
schema,
|
||||
table,
|
||||
}
|
||||
@@ -137,7 +142,7 @@ export function useKeywords(
|
||||
});
|
||||
});
|
||||
return [...columns];
|
||||
}, [dbId, schema, apiState, tablesForColumnMetadata]);
|
||||
}, [dbId, catalog, schema, apiState, tablesForColumnMetadata]);
|
||||
|
||||
const insertMatch = useEffectEvent((editor: Editor, data: any) => {
|
||||
if (data.meta === 'table') {
|
||||
|
||||
@@ -210,6 +210,38 @@ describe('SaveDatasetModal', () => {
|
||||
expect(createDatasource).toHaveBeenCalledWith({
|
||||
datasourceName: 'my dataset',
|
||||
dbId: 1,
|
||||
catalog: null,
|
||||
schema: 'main',
|
||||
sql: 'SELECT *',
|
||||
templateParams: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('sends the catalog when creating the dataset', async () => {
|
||||
const dummyDispatch = jest.fn().mockResolvedValue({});
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
useSelectorMock.mockReturnValue({ ...user });
|
||||
|
||||
render(
|
||||
<SaveDatasetModal
|
||||
{...mockedProps}
|
||||
datasource={{ ...mockedProps.datasource, catalog: 'public' }}
|
||||
/>,
|
||||
{ useRedux: true },
|
||||
);
|
||||
|
||||
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
|
||||
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
|
||||
|
||||
const saveConfirmationBtn = screen.getByRole('button', {
|
||||
name: /save/i,
|
||||
});
|
||||
userEvent.click(saveConfirmationBtn);
|
||||
|
||||
expect(createDatasource).toHaveBeenCalledWith({
|
||||
datasourceName: 'my dataset',
|
||||
dbId: 1,
|
||||
catalog: 'public',
|
||||
schema: 'main',
|
||||
sql: 'SELECT *',
|
||||
templateParams: undefined,
|
||||
|
||||
@@ -77,6 +77,7 @@ export interface ISaveableDatasource {
|
||||
dbId: number;
|
||||
sql: string;
|
||||
templateParams?: string | object | null;
|
||||
catalog?: string | null;
|
||||
schema?: string | null;
|
||||
database?: Database;
|
||||
}
|
||||
@@ -292,6 +293,7 @@ export const SaveDatasetModal = ({
|
||||
createDatasource({
|
||||
sql: datasource.sql,
|
||||
dbId: datasource.dbId || datasource?.database?.id,
|
||||
catalog: datasource?.catalog,
|
||||
schema: datasource?.schema,
|
||||
templateParams,
|
||||
datasourceName: datasetName,
|
||||
|
||||
@@ -42,6 +42,7 @@ const mockState = {
|
||||
{
|
||||
id: mockedProps.queryEditorId,
|
||||
dbId: 1,
|
||||
catalog: null,
|
||||
schema: 'main',
|
||||
sql: 'SELECT * FROM t',
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ export type QueryPayload = {
|
||||
description?: string;
|
||||
id?: string;
|
||||
remoteId?: number;
|
||||
} & Pick<QueryEditor, 'dbId' | 'schema' | 'sql'>;
|
||||
} & Pick<QueryEditor, 'dbId' | 'catalog' | 'schema' | 'sql'>;
|
||||
|
||||
const Styles = styled.span`
|
||||
span[role='img'] {
|
||||
@@ -78,6 +78,7 @@ const SaveQuery = ({
|
||||
'dbId',
|
||||
'latestQueryId',
|
||||
'queryLimit',
|
||||
'catalog',
|
||||
'schema',
|
||||
'selectedText',
|
||||
'sql',
|
||||
@@ -115,6 +116,7 @@ const SaveQuery = ({
|
||||
description,
|
||||
dbId: query.dbId ?? 0,
|
||||
sql: query.sql,
|
||||
catalog: query.catalog,
|
||||
schema: query.schema,
|
||||
templateParams: query.templateParams,
|
||||
remoteId: query?.remoteId || undefined,
|
||||
|
||||
@@ -44,6 +44,10 @@ const mockedProps = {
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.get('glob:*/api/v1/database/?*', { result: [] });
|
||||
fetchMock.get('glob:*/api/v1/database/*/catalogs/?*', {
|
||||
count: 0,
|
||||
result: [],
|
||||
});
|
||||
fetchMock.get('glob:*/api/v1/database/*/schemas/?*', {
|
||||
count: 2,
|
||||
result: ['main', 'new_schema'],
|
||||
@@ -103,11 +107,14 @@ test('renders a TableElement', async () => {
|
||||
});
|
||||
|
||||
test('table should be visible when expanded is true', async () => {
|
||||
const { container, getByText, getByRole, queryAllByText } =
|
||||
await renderAndWait(mockedProps, undefined, {
|
||||
const { container, getByText, getByRole } = await renderAndWait(
|
||||
mockedProps,
|
||||
undefined,
|
||||
{
|
||||
...initialState,
|
||||
sqlLab: { ...initialState.sqlLab, tables: [table] },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const dbSelect = getByRole('combobox', {
|
||||
name: 'Select database or type to search databases',
|
||||
@@ -115,14 +122,56 @@ test('table should be visible when expanded is true', async () => {
|
||||
const schemaSelect = getByRole('combobox', {
|
||||
name: 'Select schema or type to search schemas',
|
||||
});
|
||||
const dropdown = getByText(/Table/i);
|
||||
const abUser = queryAllByText(/ab_user/i);
|
||||
const dropdown = getByText(/Select table/i);
|
||||
const abUser = getByText(/ab_user/i);
|
||||
|
||||
expect(getByText(/Database/i)).toBeInTheDocument();
|
||||
expect(dbSelect).toBeInTheDocument();
|
||||
expect(schemaSelect).toBeInTheDocument();
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
expect(abUser).toHaveLength(2);
|
||||
expect(abUser).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector('.ant-collapse-content-active'),
|
||||
).toBeInTheDocument();
|
||||
table.columns.forEach(({ name }) => {
|
||||
expect(getByText(name)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('catalog selector should be visible when enabled in the database', async () => {
|
||||
const { container, getByText, getByRole } = await renderAndWait(
|
||||
{
|
||||
...mockedProps,
|
||||
database: {
|
||||
...mockedProps.database,
|
||||
allow_multi_catalog: true,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
...initialState,
|
||||
sqlLab: { ...initialState.sqlLab, tables: [table] },
|
||||
},
|
||||
);
|
||||
|
||||
const dbSelect = getByRole('combobox', {
|
||||
name: 'Select database or type to search databases',
|
||||
});
|
||||
const catalogSelect = getByRole('combobox', {
|
||||
name: 'Select catalog or type to search catalogs',
|
||||
});
|
||||
const schemaSelect = getByRole('combobox', {
|
||||
name: 'Select schema or type to search schemas',
|
||||
});
|
||||
const dropdown = getByText(/Select table/i);
|
||||
const abUser = getByText(/ab_user/i);
|
||||
|
||||
expect(getByText(/Database/i)).toBeInTheDocument();
|
||||
expect(dbSelect).toBeInTheDocument();
|
||||
expect(catalogSelect).toBeInTheDocument();
|
||||
expect(schemaSelect).toBeInTheDocument();
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
expect(abUser).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector('.ant-collapse-content-active'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
removeTables,
|
||||
collapseTable,
|
||||
expandTable,
|
||||
queryEditorSetCatalog,
|
||||
queryEditorSetSchema,
|
||||
setDatabases,
|
||||
addDangerToast,
|
||||
@@ -115,13 +116,17 @@ const SqlEditorLeftBar = ({
|
||||
shallowEqual,
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
const queryEditor = useQueryEditor(queryEditorId, ['dbId', 'schema']);
|
||||
const queryEditor = useQueryEditor(queryEditorId, [
|
||||
'dbId',
|
||||
'catalog',
|
||||
'schema',
|
||||
]);
|
||||
|
||||
const [emptyResultsWithSearch, setEmptyResultsWithSearch] = useState(false);
|
||||
const [userSelectedDb, setUserSelected] = useState<DatabaseObject | null>(
|
||||
null,
|
||||
);
|
||||
const { schema } = queryEditor;
|
||||
const { catalog, schema } = queryEditor;
|
||||
|
||||
useEffect(() => {
|
||||
const bool = querystring.parse(window.location.search).db;
|
||||
@@ -138,9 +143,9 @@ const SqlEditorLeftBar = ({
|
||||
}
|
||||
}, [database]);
|
||||
|
||||
const onEmptyResults = (searchText?: string) => {
|
||||
const onEmptyResults = useCallback((searchText?: string) => {
|
||||
setEmptyResultsWithSearch(!!searchText);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onDbChange = ({ id: dbId }: { id: number }) => {
|
||||
setEmptyState?.(false);
|
||||
@@ -152,7 +157,11 @@ const SqlEditorLeftBar = ({
|
||||
[tables],
|
||||
);
|
||||
|
||||
const onTablesChange = (tableNames: string[], schemaName: string) => {
|
||||
const onTablesChange = (
|
||||
tableNames: string[],
|
||||
catalogName: string | null,
|
||||
schemaName: string,
|
||||
) => {
|
||||
if (!schemaName) {
|
||||
return;
|
||||
}
|
||||
@@ -169,7 +178,7 @@ const SqlEditorLeftBar = ({
|
||||
});
|
||||
|
||||
tablesToAdd.forEach(tableName => {
|
||||
dispatch(addTable(queryEditor, tableName, schemaName));
|
||||
dispatch(addTable(queryEditor, tableName, catalogName, schemaName));
|
||||
});
|
||||
|
||||
dispatch(removeTables(currentTables));
|
||||
@@ -210,6 +219,15 @@ const SqlEditorLeftBar = ({
|
||||
const shouldShowReset = window.location.search === '?reset=1';
|
||||
const tableMetaDataHeight = height - 130; // 130 is the height of the selects above
|
||||
|
||||
const handleCatalogChange = useCallback(
|
||||
(catalog: string | null) => {
|
||||
if (queryEditor) {
|
||||
dispatch(queryEditorSetCatalog(queryEditor, catalog));
|
||||
}
|
||||
},
|
||||
[dispatch, queryEditor],
|
||||
);
|
||||
|
||||
const handleSchemaChange = useCallback(
|
||||
(schema: string) => {
|
||||
if (queryEditor) {
|
||||
@@ -246,9 +264,11 @@ const SqlEditorLeftBar = ({
|
||||
getDbList={handleDbList}
|
||||
handleError={handleError}
|
||||
onDbChange={onDbChange}
|
||||
onCatalogChange={handleCatalogChange}
|
||||
catalog={catalog}
|
||||
onSchemaChange={handleSchemaChange}
|
||||
onTableSelectChange={onTablesChange}
|
||||
schema={schema}
|
||||
onTableSelectChange={onTablesChange}
|
||||
tableValue={selectedTableNames}
|
||||
sqlLabMode
|
||||
/>
|
||||
|
||||
@@ -111,6 +111,7 @@ class TabbedSqlEditors extends React.PureComponent<TabbedSqlEditorsProps> {
|
||||
queryId,
|
||||
dbid,
|
||||
dbname,
|
||||
catalog,
|
||||
schema,
|
||||
autorun,
|
||||
new: isNewQuery,
|
||||
@@ -149,6 +150,7 @@ class TabbedSqlEditors extends React.PureComponent<TabbedSqlEditorsProps> {
|
||||
const newQueryEditor = {
|
||||
name,
|
||||
dbId: databaseId,
|
||||
catalog,
|
||||
schema,
|
||||
autorun,
|
||||
sql,
|
||||
|
||||
@@ -101,7 +101,7 @@ const StyledCollapsePanel = styled(Collapse.Panel)`
|
||||
`;
|
||||
|
||||
const TableElement = ({ table, ...props }: TableElementProps) => {
|
||||
const { dbId, schema, name, expanded } = table;
|
||||
const { dbId, catalog, schema, name, expanded } = table;
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
@@ -112,6 +112,7 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
|
||||
} = useTableMetadataQuery(
|
||||
{
|
||||
dbId,
|
||||
catalog,
|
||||
schema,
|
||||
table: name,
|
||||
},
|
||||
@@ -125,6 +126,7 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
|
||||
} = useTableExtendedMetadataQuery(
|
||||
{
|
||||
dbId,
|
||||
catalog,
|
||||
schema,
|
||||
table: name,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user