mirror of https://github.com/apache/superset.git
feat: SIP-34 table list view for databases (#10705)
This commit is contained in:
parent
5a4370012b
commit
7bccb38a60
|
@ -19,19 +19,57 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
import { styledMount as mount } from 'spec/helpers/theming';
|
import { styledMount as mount } from 'spec/helpers/theming';
|
||||||
|
|
||||||
import DatabaseList from 'src/views/CRUD/data/database/DatabaseList';
|
import DatabaseList from 'src/views/CRUD/data/database/DatabaseList';
|
||||||
import DatabaseModal from 'src/views/CRUD/data/database/DatabaseModal';
|
import DatabaseModal from 'src/views/CRUD/data/database/DatabaseModal';
|
||||||
import SubMenu from 'src/components/Menu/SubMenu';
|
import SubMenu from 'src/components/Menu/SubMenu';
|
||||||
|
import ListView from 'src/components/ListView';
|
||||||
|
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
// store needed for withToasts(DatabaseList)
|
// store needed for withToasts(DatabaseList)
|
||||||
const mockStore = configureStore([thunk]);
|
const mockStore = configureStore([thunk]);
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
|
|
||||||
|
const databasesInfoEndpoint = 'glob:*/api/v1/database/_info*';
|
||||||
|
const databasesEndpoint = 'glob:*/api/v1/database/?*';
|
||||||
|
const databaseEndpoint = 'glob:*/api/v1/database/*';
|
||||||
|
|
||||||
|
const mockdatabases = [...new Array(3)].map((_, i) => ({
|
||||||
|
changed_by: {
|
||||||
|
first_name: `user`,
|
||||||
|
last_name: `${i}`,
|
||||||
|
},
|
||||||
|
database_name: `db ${i}`,
|
||||||
|
backend: 'postgresql',
|
||||||
|
allow_run_async: true,
|
||||||
|
allow_dml: false,
|
||||||
|
allow_csv_upload: true,
|
||||||
|
expose_in_sqllab: false,
|
||||||
|
changed_on_delta_humanized: `${i} day(s) ago`,
|
||||||
|
changed_on: new Date().toISOString,
|
||||||
|
id: i,
|
||||||
|
}));
|
||||||
|
|
||||||
|
fetchMock.get(databasesInfoEndpoint, {
|
||||||
|
permissions: ['can_delete'],
|
||||||
|
});
|
||||||
|
fetchMock.get(databasesEndpoint, {
|
||||||
|
result: mockdatabases,
|
||||||
|
database_count: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchMock.delete(databaseEndpoint, {});
|
||||||
|
|
||||||
describe('DatabaseList', () => {
|
describe('DatabaseList', () => {
|
||||||
const wrapper = mount(<DatabaseList />, { context: { store } });
|
const wrapper = mount(<DatabaseList />, { context: { store } });
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
});
|
||||||
|
|
||||||
it('renders', () => {
|
it('renders', () => {
|
||||||
expect(wrapper.find(DatabaseList)).toExist();
|
expect(wrapper.find(DatabaseList)).toExist();
|
||||||
});
|
});
|
||||||
|
@ -43,4 +81,39 @@ describe('DatabaseList', () => {
|
||||||
it('renders a DatabaseModal', () => {
|
it('renders a DatabaseModal', () => {
|
||||||
expect(wrapper.find(DatabaseModal)).toExist();
|
expect(wrapper.find(DatabaseModal)).toExist();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders a ListView', () => {
|
||||||
|
expect(wrapper.find(ListView)).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches Databases', () => {
|
||||||
|
const callsD = fetchMock.calls(/database\/\?q/);
|
||||||
|
expect(callsD).toHaveLength(1);
|
||||||
|
expect(callsD[0][0]).toMatchInlineSnapshot(
|
||||||
|
`"http://localhost/api/v1/database/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes', async () => {
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('[data-test="database-delete"]').first().props().onClick();
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('#delete')
|
||||||
|
.first()
|
||||||
|
.props()
|
||||||
|
.onChange({ target: { value: 'DELETE' } });
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('button').last().props().onClick();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
|
||||||
|
expect(fetchMock.calls(/database\/0/, 'DELETE')).toHaveLength(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,9 +38,20 @@ export default function ConfirmStatusChange({
|
||||||
|
|
||||||
const showConfirm = (...callbackArgs: any[]) => {
|
const showConfirm = (...callbackArgs: any[]) => {
|
||||||
// check if any args are DOM events, if so, call persist
|
// check if any args are DOM events, if so, call persist
|
||||||
callbackArgs.forEach(
|
callbackArgs.forEach(arg => {
|
||||||
arg => arg && typeof arg.persist === 'function' && arg.persist(),
|
if (!arg) {
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
if (typeof arg.persist === 'function') {
|
||||||
|
arg.persist();
|
||||||
|
}
|
||||||
|
if (typeof arg.preventDefault === 'function') {
|
||||||
|
arg.preventDefault();
|
||||||
|
}
|
||||||
|
if (typeof arg.stopPropagation === 'function') {
|
||||||
|
arg.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
setCurrentCallbackArgs(callbackArgs);
|
setCurrentCallbackArgs(callbackArgs);
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,52 +17,72 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { SupersetClient } from '@superset-ui/connection';
|
import { SupersetClient } from '@superset-ui/connection';
|
||||||
|
import styled from '@superset-ui/style';
|
||||||
import { t } from '@superset-ui/translation';
|
import { t } from '@superset-ui/translation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { useListViewResource } from 'src/views/CRUD/hooks';
|
||||||
import { createErrorHandler } from 'src/views/CRUD/utils';
|
import { createErrorHandler } from 'src/views/CRUD/utils';
|
||||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
|
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||||
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
||||||
|
import TooltipWrapper from 'src/components/TooltipWrapper';
|
||||||
|
import Icon from 'src/components/Icon';
|
||||||
|
import ListView, { Filters } from 'src/components/ListView';
|
||||||
import { commonMenuData } from 'src/views/CRUD/data/common';
|
import { commonMenuData } from 'src/views/CRUD/data/common';
|
||||||
import DatabaseModal, { DatabaseObject } from './DatabaseModal';
|
import DatabaseModal from './DatabaseModal';
|
||||||
|
import { DatabaseObject } from './types';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 25;
|
||||||
|
|
||||||
interface DatabaseListProps {
|
interface DatabaseListProps {
|
||||||
addDangerToast: (msg: string) => void;
|
addDangerToast: (msg: string) => void;
|
||||||
addSuccessToast: (msg: string) => void;
|
addSuccessToast: (msg: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const IconBlack = styled(Icon)`
|
||||||
|
color: ${({ theme }) => theme.colors.grayscale.dark1};
|
||||||
|
`;
|
||||||
|
|
||||||
|
function BooleanDisplay(value: any) {
|
||||||
|
return value ? <IconBlack name="check" /> : <IconBlack name="cancel-x" />;
|
||||||
|
}
|
||||||
|
|
||||||
function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
||||||
|
const {
|
||||||
|
state: {
|
||||||
|
loading,
|
||||||
|
resourceCount: databaseCount,
|
||||||
|
resourceCollection: databases,
|
||||||
|
},
|
||||||
|
hasPerm,
|
||||||
|
fetchData,
|
||||||
|
refreshData,
|
||||||
|
} = useListViewResource<DatabaseObject>(
|
||||||
|
'database',
|
||||||
|
t('database'),
|
||||||
|
addDangerToast,
|
||||||
|
);
|
||||||
const [databaseModalOpen, setDatabaseModalOpen] = useState<boolean>(false);
|
const [databaseModalOpen, setDatabaseModalOpen] = useState<boolean>(false);
|
||||||
const [currentDatabase, setCurrentDatabase] = useState<DatabaseObject | null>(
|
const [currentDatabase, setCurrentDatabase] = useState<DatabaseObject | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [permissions, setPermissions] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const fetchDatasetInfo = () => {
|
function handleDatabaseDelete({ id, database_name: dbName }: DatabaseObject) {
|
||||||
SupersetClient.get({
|
SupersetClient.delete({
|
||||||
endpoint: `/api/v1/dataset/_info`,
|
endpoint: `/api/v1/database/${id}`,
|
||||||
}).then(
|
}).then(
|
||||||
({ json: infoJson = {} }) => {
|
() => {
|
||||||
setPermissions(infoJson.permissions);
|
refreshData();
|
||||||
|
addSuccessToast(t('Deleted: %s', dbName));
|
||||||
},
|
},
|
||||||
createErrorHandler(errMsg =>
|
createErrorHandler(errMsg =>
|
||||||
addDangerToast(t('An error occurred while fetching datasets', errMsg)),
|
addDangerToast(t('There was an issue deleting %s: %s', dbName, errMsg)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDatasetInfo();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const hasPerm = (perm: string) => {
|
|
||||||
if (!permissions.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Boolean(permissions.find(p => p === perm));
|
|
||||||
};
|
|
||||||
|
|
||||||
const canCreate = hasPerm('can_add');
|
const canCreate = hasPerm('can_add');
|
||||||
|
const canDelete = hasPerm('can_delete');
|
||||||
|
|
||||||
const menuData: SubMenuProps = {
|
const menuData: SubMenuProps = {
|
||||||
activeChild: 'Databases',
|
activeChild: 'Databases',
|
||||||
|
@ -85,6 +105,148 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessor: 'database_name',
|
||||||
|
Header: t('Database'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'backend',
|
||||||
|
Header: t('Backend'),
|
||||||
|
size: 'xxl',
|
||||||
|
disableSortBy: true, // TODO: api support for sorting by 'backend'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'allow_run_async',
|
||||||
|
Header: (
|
||||||
|
<TooltipWrapper
|
||||||
|
label="allow-run-async-header"
|
||||||
|
tooltip={t('Asynchronous Query Execution')}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<span>{t('AQE')}</span>
|
||||||
|
</TooltipWrapper>
|
||||||
|
),
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { allow_run_async: allowRunAsync },
|
||||||
|
},
|
||||||
|
}: any) => <BooleanDisplay value={allowRunAsync} />,
|
||||||
|
size: 'md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'allow_dml',
|
||||||
|
Header: (
|
||||||
|
<TooltipWrapper
|
||||||
|
label="allow-dml-header"
|
||||||
|
tooltip={t('Allow Data Danipulation Language')}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<span>{t('DML')}</span>
|
||||||
|
</TooltipWrapper>
|
||||||
|
),
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { allow_dml: allowDML },
|
||||||
|
},
|
||||||
|
}: any) => <BooleanDisplay value={allowDML} />,
|
||||||
|
size: 'md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'allow_csv_upload',
|
||||||
|
Header: t('CSV Upload'),
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { allow_csv_upload: allowCSVUpload },
|
||||||
|
},
|
||||||
|
}: any) => <BooleanDisplay value={allowCSVUpload} />,
|
||||||
|
size: 'xl',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'expose_in_sqllab',
|
||||||
|
Header: t('Expose in SQL Lab'),
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { expose_in_sqllab: exposeInSqllab },
|
||||||
|
},
|
||||||
|
}: any) => <BooleanDisplay value={exposeInSqllab} />,
|
||||||
|
size: 'xxl',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'created_by',
|
||||||
|
disableSortBy: true,
|
||||||
|
Header: t('Created By'),
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { created_by: createdBy },
|
||||||
|
},
|
||||||
|
}: any) =>
|
||||||
|
createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
|
||||||
|
size: 'xl',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { changed_on_delta_humanized: changedOn },
|
||||||
|
},
|
||||||
|
}: any) => changedOn,
|
||||||
|
Header: t('Last Modified'),
|
||||||
|
accessor: 'changed_on_delta_humanized',
|
||||||
|
size: 'xl',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Cell: ({ row: { original } }: any) => {
|
||||||
|
const handleDelete = () => handleDatabaseDelete(original);
|
||||||
|
if (!canDelete) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className="actions">
|
||||||
|
{canDelete && (
|
||||||
|
<ConfirmStatusChange
|
||||||
|
title={t('Please Confirm')}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
{t('Are you sure you want to delete')}{' '}
|
||||||
|
<b>{original.database_name}</b>?
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
>
|
||||||
|
{confirmDelete => (
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className="action-button"
|
||||||
|
data-test="database-delete"
|
||||||
|
onClick={confirmDelete}
|
||||||
|
>
|
||||||
|
<TooltipWrapper
|
||||||
|
label="delete-action"
|
||||||
|
tooltip={t('Delete database')}
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<Icon name="trash" />
|
||||||
|
</TooltipWrapper>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ConfirmStatusChange>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Header: t('Actions'),
|
||||||
|
id: 'actions',
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[canDelete, canCreate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const filters: Filters = [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SubMenu {...menuData} />
|
<SubMenu {...menuData} />
|
||||||
|
@ -96,6 +258,18 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
||||||
/* TODO: add database logic here */
|
/* TODO: add database logic here */
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ListView<DatabaseObject>
|
||||||
|
className="database-list-view"
|
||||||
|
columns={columns}
|
||||||
|
count={databaseCount}
|
||||||
|
data={databases}
|
||||||
|
fetchData={fetchData}
|
||||||
|
filters={filters}
|
||||||
|
initialSort={initialSort}
|
||||||
|
loading={loading}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,13 +23,7 @@ import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
import Icon from 'src/components/Icon';
|
import Icon from 'src/components/Icon';
|
||||||
import Modal from 'src/common/components/Modal';
|
import Modal from 'src/common/components/Modal';
|
||||||
import Tabs from 'src/common/components/Tabs';
|
import Tabs from 'src/common/components/Tabs';
|
||||||
|
import { DatabaseObject } from './types';
|
||||||
export type DatabaseObject = {
|
|
||||||
id?: number;
|
|
||||||
name: string;
|
|
||||||
uri: string;
|
|
||||||
// TODO: add more props
|
|
||||||
};
|
|
||||||
|
|
||||||
interface DatabaseModalProps {
|
interface DatabaseModalProps {
|
||||||
addDangerToast: (msg: string) => void;
|
addDangerToast: (msg: string) => void;
|
||||||
|
@ -90,7 +84,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
// const [disableSave, setDisableSave] = useState(true);
|
// const [disableSave, setDisableSave] = useState(true);
|
||||||
const [disableSave] = useState<boolean>(true);
|
const [disableSave] = useState<boolean>(true);
|
||||||
const [db, setDB] = useState<DatabaseObject | null>(null);
|
const [db, setDB] = useState<Partial<DatabaseObject> | null>(null);
|
||||||
const [isHidden, setIsHidden] = useState<boolean>(true);
|
const [isHidden, setIsHidden] = useState<boolean>(true);
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
|
@ -110,7 +104,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||||
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
const data = {
|
const data = {
|
||||||
name: db ? db.name : '',
|
database_name: db ? db.database_name : '',
|
||||||
uri: db ? db.uri : '',
|
uri: db ? db.uri : '',
|
||||||
...db,
|
...db,
|
||||||
};
|
};
|
||||||
|
@ -130,7 +124,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||||
setDB(database);
|
setDB(database);
|
||||||
} else if (!isEditMode && (!db || db.id || (isHidden && show))) {
|
} else if (!isEditMode && (!db || db.id || (isHidden && show))) {
|
||||||
setDB({
|
setDB({
|
||||||
name: '',
|
database_name: '',
|
||||||
uri: '',
|
uri: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -175,7 +169,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
name="name"
|
||||||
value={db ? db.name : ''}
|
value={db ? db.database_name : ''}
|
||||||
placeholder={t('Name your datasource')}
|
placeholder={t('Name your datasource')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
type DatabaseUser = {
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DatabaseObject = {
|
||||||
|
id: number;
|
||||||
|
database_name: string;
|
||||||
|
backend: string;
|
||||||
|
allow_run_async: boolean;
|
||||||
|
allow_dml: boolean;
|
||||||
|
allow_csv_upload: boolean;
|
||||||
|
expose_in_sqllab: boolean;
|
||||||
|
created_by: null | DatabaseUser;
|
||||||
|
changed_on_delta_humanized: string;
|
||||||
|
changed_on: string;
|
||||||
|
|
||||||
|
uri: string;
|
||||||
|
// TODO: add more props
|
||||||
|
};
|
|
@ -60,6 +60,6 @@ export function createErrorHandler(handleErrorFunc: (errMsg?: string) => void) {
|
||||||
return async (e: SupersetClientResponse | string) => {
|
return async (e: SupersetClientResponse | string) => {
|
||||||
const parsedError = await getClientErrorObject(e);
|
const parsedError = await getClientErrorObject(e);
|
||||||
logging.error(e);
|
logging.error(e);
|
||||||
handleErrorFunc(parsedError.error);
|
handleErrorFunc(parsedError.message || parsedError.error);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,22 +87,26 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
||||||
"sqlalchemy_uri",
|
"sqlalchemy_uri",
|
||||||
]
|
]
|
||||||
list_columns = [
|
list_columns = [
|
||||||
"id",
|
"allow_csv_upload",
|
||||||
"database_name",
|
|
||||||
"expose_in_sqllab",
|
|
||||||
"allow_ctas",
|
"allow_ctas",
|
||||||
"allow_cvas",
|
"allow_cvas",
|
||||||
"force_ctas_schema",
|
|
||||||
"allow_run_async",
|
|
||||||
"allow_dml",
|
"allow_dml",
|
||||||
"allow_multi_schema_metadata_fetch",
|
"allow_multi_schema_metadata_fetch",
|
||||||
"allow_csv_upload",
|
"allow_run_async",
|
||||||
"allows_subquery",
|
|
||||||
"allows_cost_estimate",
|
"allows_cost_estimate",
|
||||||
|
"allows_subquery",
|
||||||
"allows_virtual_table_explore",
|
"allows_virtual_table_explore",
|
||||||
"explore_database_id",
|
|
||||||
"backend",
|
"backend",
|
||||||
|
"changed_on",
|
||||||
|
"changed_on_delta_humanized",
|
||||||
|
"created_by.first_name",
|
||||||
|
"created_by.last_name",
|
||||||
|
"database_name",
|
||||||
|
"explore_database_id",
|
||||||
|
"expose_in_sqllab",
|
||||||
|
"force_ctas_schema",
|
||||||
"function_names",
|
"function_names",
|
||||||
|
"id",
|
||||||
]
|
]
|
||||||
add_columns = [
|
add_columns = [
|
||||||
"database_name",
|
"database_name",
|
||||||
|
@ -124,6 +128,16 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
||||||
edit_columns = add_columns
|
edit_columns = add_columns
|
||||||
|
|
||||||
list_select_columns = list_columns + ["extra", "sqlalchemy_uri", "password"]
|
list_select_columns = list_columns + ["extra", "sqlalchemy_uri", "password"]
|
||||||
|
order_columns = [
|
||||||
|
"allow_csv_upload",
|
||||||
|
"allow_dml",
|
||||||
|
"allow_run_async",
|
||||||
|
"changed_on",
|
||||||
|
"changed_on_delta_humanized",
|
||||||
|
"created_by.first_name",
|
||||||
|
"database_name",
|
||||||
|
"expose_in_sqllab",
|
||||||
|
]
|
||||||
# Removes the local limit for the page size
|
# Removes the local limit for the page size
|
||||||
max_page_size = -1
|
max_page_size = -1
|
||||||
add_model_schema = DatabasePostSchema()
|
add_model_schema = DatabasePostSchema()
|
||||||
|
|
|
@ -72,6 +72,9 @@ class TestDatabaseApi(SupersetTestCase):
|
||||||
"allows_subquery",
|
"allows_subquery",
|
||||||
"allows_virtual_table_explore",
|
"allows_virtual_table_explore",
|
||||||
"backend",
|
"backend",
|
||||||
|
"changed_on",
|
||||||
|
"changed_on_delta_humanized",
|
||||||
|
"created_by",
|
||||||
"database_name",
|
"database_name",
|
||||||
"explore_database_id",
|
"explore_database_id",
|
||||||
"expose_in_sqllab",
|
"expose_in_sqllab",
|
||||||
|
|
|
@ -515,6 +515,7 @@ class TestSqlLab(SupersetTestCase):
|
||||||
"page_size": -1,
|
"page_size": -1,
|
||||||
}
|
}
|
||||||
url = f"api/v1/database/?q={prison.dumps(arguments)}"
|
url = f"api/v1/database/?q={prison.dumps(arguments)}"
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{"examples", "fake_db_100", "main"},
|
{"examples", "fake_db_100", "main"},
|
||||||
{r.get("database_name") for r in self.get_json_resp(url)["result"]},
|
{r.get("database_name") for r in self.get_json_resp(url)["result"]},
|
||||||
|
|
Loading…
Reference in New Issue