mirror of
https://github.com/apache/superset.git
synced 2026-05-02 22:44:28 +00:00
Compare commits
13 Commits
fix-webpac
...
v2021.34.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7aac3ff21 | ||
|
|
f8212ad3df | ||
|
|
9068dd3cb4 | ||
|
|
31ec5b8063 | ||
|
|
961b9da4f4 | ||
|
|
f6bb1cdd9f | ||
|
|
53dea71832 | ||
|
|
4fce06e7a3 | ||
|
|
79cfad10cf | ||
|
|
5bc6ce8325 | ||
|
|
401cb603d8 | ||
|
|
7d9cb04836 | ||
|
|
a235f5b28e |
@@ -66,7 +66,7 @@ Navigate to **Data ‣ Datasets** and select the **+ Dataset** button in the top
|
|||||||
|
|
||||||
A modal window should pop up in front of you. Select your **Database**,
|
A modal window should pop up in front of you. Select your **Database**,
|
||||||
**Schema**, and **Table** using the drop downs that appear. In the following example,
|
**Schema**, and **Table** using the drop downs that appear. In the following example,
|
||||||
we register the **cleaned_sales_data** table from the **examples** database.
|
we register the **Vehicle Sales** table from the **examples** database.
|
||||||
|
|
||||||
<img src="/images/tutorial_09_add_new_table.png" />
|
<img src="/images/tutorial_09_add_new_table.png" />
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
testRegex: '(\\/spec|\\/src)\\/.*(_spec|\\.test)\\.(j|t)sx?$',
|
testRegex: '(\\/spec|\\/src)\\/.*(_spec|\\.test)\\.(j|t)sx?$',
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'\\.(css|less)$': '<rootDir>/spec/__mocks__/styleMock.js',
|
'\\.(css|less|geojson)$': '<rootDir>/spec/__mocks__/mockExportObject.js',
|
||||||
'\\.(gif|ttf|eot|png|jpg)$': '<rootDir>/spec/__mocks__/fileMock.js',
|
'\\.(gif|ttf|eot|png|jpg)$': '<rootDir>/spec/__mocks__/mockExportString.js',
|
||||||
'\\.svg$': '<rootDir>/spec/__mocks__/svgrMock.tsx',
|
'\\.svg$': '<rootDir>/spec/__mocks__/svgrMock.tsx',
|
||||||
'^src/(.*)$': '<rootDir>/src/$1',
|
'^src/(.*)$': '<rootDir>/src/$1',
|
||||||
'^spec/(.*)$': '<rootDir>/spec/$1',
|
'^spec/(.*)$': '<rootDir>/spec/$1',
|
||||||
|
|||||||
@@ -23,3 +23,5 @@ import { configure as configureTestingLibrary } from '@testing-library/react';
|
|||||||
configureTestingLibrary({
|
configureTestingLibrary({
|
||||||
testIdAttribute: 'data-test',
|
testIdAttribute: 'data-test',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.body.innerHTML = '<div id="app" data-bootstrap="{}"></div>';
|
||||||
|
|||||||
@@ -81,13 +81,9 @@ describe('Left Panel Expansion', () => {
|
|||||||
</Provider>
|
</Provider>
|
||||||
</ThemeProvider>,
|
</ThemeProvider>,
|
||||||
);
|
);
|
||||||
const dbSelect = screen.getByRole('combobox', {
|
const dbSelect = screen.getByText(/select a database/i);
|
||||||
name: 'Select a database',
|
const schemaSelect = screen.getByText(/select a schema \(0\)/i);
|
||||||
});
|
const dropdown = screen.getByText(/Select table/i);
|
||||||
const schemaSelect = screen.getByRole('combobox', {
|
|
||||||
name: 'Select a schema',
|
|
||||||
});
|
|
||||||
const dropdown = screen.getByText(/Select a table/i);
|
|
||||||
const abUser = screen.getByText(/ab_user/i);
|
const abUser = screen.getByText(/ab_user/i);
|
||||||
expect(dbSelect).toBeInTheDocument();
|
expect(dbSelect).toBeInTheDocument();
|
||||||
expect(schemaSelect).toBeInTheDocument();
|
expect(schemaSelect).toBeInTheDocument();
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ describe('async actions', () => {
|
|||||||
|
|
||||||
let isFeatureEnabledMock;
|
let isFeatureEnabledMock;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeEach(() => {
|
||||||
isFeatureEnabledMock = jest
|
isFeatureEnabledMock = jest
|
||||||
.spyOn(featureFlags, 'isFeatureEnabled')
|
.spyOn(featureFlags, 'isFeatureEnabled')
|
||||||
.mockImplementation(
|
.mockImplementation(
|
||||||
@@ -407,7 +407,7 @@ describe('async actions', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterEach(() => {
|
||||||
isFeatureEnabledMock.mockRestore();
|
isFeatureEnabledMock.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -612,9 +612,29 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('queryEditorSetSql', () => {
|
describe('queryEditorSetSql', () => {
|
||||||
it('updates the tab state in the backend', () => {
|
describe('with backend persistence flag on', () => {
|
||||||
expect.assertions(2);
|
it('does not update the tab state in the backend', () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const sql = 'SELECT * ';
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
|
return store
|
||||||
|
.dispatch(actions.queryEditorSetSql(queryEditor, sql))
|
||||||
|
.then(() => {
|
||||||
|
expect(store.getActions()).toHaveLength(0);
|
||||||
|
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('with backend persistence flag off', () => {
|
||||||
|
it('updates the tab state in the backend', () => {
|
||||||
|
const backendPersistenceOffMock = jest
|
||||||
|
.spyOn(featureFlags, 'isFeatureEnabled')
|
||||||
|
.mockImplementation(
|
||||||
|
feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
|
||||||
|
);
|
||||||
const sql = 'SELECT * ';
|
const sql = 'SELECT * ';
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
@@ -624,12 +644,12 @@ describe('async actions', () => {
|
|||||||
sql,
|
sql,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return store
|
|
||||||
.dispatch(actions.queryEditorSetSql(queryEditor, sql))
|
store.dispatch(actions.queryEditorSetSql(queryEditor, sql));
|
||||||
.then(() => {
|
|
||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
|
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
|
||||||
});
|
backendPersistenceOffMock.mockRestore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -898,16 +898,11 @@ export function updateSavedQuery(query) {
|
|||||||
|
|
||||||
export function queryEditorSetSql(queryEditor, sql) {
|
export function queryEditorSetSql(queryEditor, sql) {
|
||||||
return function (dispatch) {
|
return function (dispatch) {
|
||||||
const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
|
if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) {
|
||||||
? SupersetClient.put({
|
return SupersetClient.put({
|
||||||
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
|
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
|
||||||
postPayload: { sql, latest_query_id: queryEditor.latestQueryId },
|
postPayload: { sql, latest_query_id: queryEditor.latestQueryId },
|
||||||
})
|
}).catch(() =>
|
||||||
: Promise.resolve();
|
|
||||||
|
|
||||||
return sync
|
|
||||||
.then(() => dispatch({ type: QUERY_EDITOR_SET_SQL, queryEditor, sql }))
|
|
||||||
.catch(() =>
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addDangerToast(
|
addDangerToast(
|
||||||
t(
|
t(
|
||||||
@@ -918,6 +913,8 @@ export function queryEditorSetSql(queryEditor, sql) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return dispatch({ type: QUERY_EDITOR_SET_SQL, queryEditor, sql });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,19 +18,19 @@
|
|||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { t, supersetTheme } from '@superset-ui/core';
|
import { t, supersetTheme } from '@superset-ui/core';
|
||||||
import Icons, { IconType } from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
|
|
||||||
export interface CertifiedIconProps {
|
export interface CertifiedIconProps {
|
||||||
certifiedBy?: string;
|
certifiedBy?: string;
|
||||||
details?: string;
|
details?: string;
|
||||||
size?: IconType['iconSize'];
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CertifiedIcon({
|
function CertifiedIcon({
|
||||||
certifiedBy,
|
certifiedBy,
|
||||||
details,
|
details,
|
||||||
size = 'l',
|
size = 24,
|
||||||
}: CertifiedIconProps) {
|
}: CertifiedIconProps) {
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -48,7 +48,8 @@ function CertifiedIcon({
|
|||||||
>
|
>
|
||||||
<Icons.Certified
|
<Icons.Certified
|
||||||
iconColor={supersetTheme.colors.primary.base}
|
iconColor={supersetTheme.colors.primary.base}
|
||||||
iconSize={size}
|
height={size}
|
||||||
|
width={size}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ import DatabaseSelector from '.';
|
|||||||
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
|
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
|
||||||
|
|
||||||
const createProps = () => ({
|
const createProps = () => ({
|
||||||
db: { id: 1, database_name: 'test', backend: 'postgresql' },
|
dbId: 1,
|
||||||
formMode: false,
|
formMode: false,
|
||||||
isDatabaseSelectEnabled: true,
|
isDatabaseSelectEnabled: true,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
schema: undefined,
|
schema: 'public',
|
||||||
sqlLabMode: true,
|
sqlLabMode: true,
|
||||||
getDbList: jest.fn(),
|
getDbList: jest.fn(),
|
||||||
getTableList: jest.fn(),
|
getTableList: jest.fn(),
|
||||||
@@ -129,7 +129,7 @@ beforeEach(() => {
|
|||||||
changed_on: '2021-03-09T19:02:07.141095',
|
changed_on: '2021-03-09T19:02:07.141095',
|
||||||
changed_on_delta_humanized: 'a day ago',
|
changed_on_delta_humanized: 'a day ago',
|
||||||
created_by: null,
|
created_by: null,
|
||||||
database_name: 'test',
|
database_name: 'examples',
|
||||||
explore_database_id: 1,
|
explore_database_id: 1,
|
||||||
expose_in_sqllab: true,
|
expose_in_sqllab: true,
|
||||||
force_ctas_schema: null,
|
force_ctas_schema: null,
|
||||||
@@ -153,62 +153,50 @@ test('Refresh should work', async () => {
|
|||||||
|
|
||||||
render(<DatabaseSelector {...props} />);
|
render(<DatabaseSelector {...props} />);
|
||||||
|
|
||||||
const select = screen.getByRole('combobox', {
|
|
||||||
name: 'Select a schema',
|
|
||||||
});
|
|
||||||
|
|
||||||
userEvent.click(select);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(SupersetClientGet).toBeCalledTimes(1);
|
|
||||||
expect(props.getDbList).toBeCalledTimes(0);
|
|
||||||
expect(props.getTableList).toBeCalledTimes(0);
|
|
||||||
expect(props.handleError).toBeCalledTimes(0);
|
|
||||||
expect(props.onDbChange).toBeCalledTimes(0);
|
|
||||||
expect(props.onSchemaChange).toBeCalledTimes(0);
|
|
||||||
expect(props.onSchemasLoad).toBeCalledTimes(0);
|
|
||||||
expect(props.onUpdate).toBeCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
userEvent.click(screen.getByRole('button', { name: 'refresh' }));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(SupersetClientGet).toBeCalledTimes(2);
|
expect(SupersetClientGet).toBeCalledTimes(2);
|
||||||
expect(props.getDbList).toBeCalledTimes(0);
|
expect(props.getDbList).toBeCalledTimes(1);
|
||||||
expect(props.getTableList).toBeCalledTimes(0);
|
expect(props.getTableList).toBeCalledTimes(0);
|
||||||
expect(props.handleError).toBeCalledTimes(0);
|
expect(props.handleError).toBeCalledTimes(0);
|
||||||
expect(props.onDbChange).toBeCalledTimes(0);
|
expect(props.onDbChange).toBeCalledTimes(0);
|
||||||
expect(props.onSchemaChange).toBeCalledTimes(0);
|
expect(props.onSchemaChange).toBeCalledTimes(0);
|
||||||
expect(props.onSchemasLoad).toBeCalledTimes(2);
|
expect(props.onSchemasLoad).toBeCalledTimes(1);
|
||||||
expect(props.onUpdate).toBeCalledTimes(0);
|
expect(props.onUpdate).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
userEvent.click(screen.getByRole('button'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(SupersetClientGet).toBeCalledTimes(3);
|
||||||
|
expect(props.getDbList).toBeCalledTimes(1);
|
||||||
|
expect(props.getTableList).toBeCalledTimes(0);
|
||||||
|
expect(props.handleError).toBeCalledTimes(0);
|
||||||
|
expect(props.onDbChange).toBeCalledTimes(1);
|
||||||
|
expect(props.onSchemaChange).toBeCalledTimes(1);
|
||||||
|
expect(props.onSchemasLoad).toBeCalledTimes(2);
|
||||||
|
expect(props.onUpdate).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should database select display options', async () => {
|
test('Should database select display options', async () => {
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
render(<DatabaseSelector {...props} />);
|
render(<DatabaseSelector {...props} />);
|
||||||
const select = screen.getByRole('combobox', {
|
const selector = await screen.findByText('Database:');
|
||||||
name: 'Select a database',
|
expect(selector).toBeInTheDocument();
|
||||||
});
|
expect(selector.parentElement).toHaveTextContent(
|
||||||
expect(select).toBeInTheDocument();
|
'Database:postgresql examples',
|
||||||
userEvent.click(select);
|
);
|
||||||
expect(
|
|
||||||
await screen.findByRole('option', { name: 'postgresql: test' }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should schema select display options', async () => {
|
test('Should schema select display options', async () => {
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
render(<DatabaseSelector {...props} />);
|
render(<DatabaseSelector {...props} />);
|
||||||
const select = screen.getByRole('combobox', {
|
|
||||||
name: 'Select a schema',
|
const selector = await screen.findByText('Schema:');
|
||||||
});
|
expect(selector).toBeInTheDocument();
|
||||||
expect(select).toBeInTheDocument();
|
expect(selector.parentElement).toHaveTextContent('Schema: public');
|
||||||
userEvent.click(select);
|
|
||||||
expect(
|
userEvent.click(screen.getByRole('button'));
|
||||||
await screen.findByRole('option', { name: 'public' }),
|
|
||||||
).toBeInTheDocument();
|
expect(await screen.findByText('Select a schema (2)')).toBeInTheDocument();
|
||||||
expect(
|
|
||||||
await screen.findByRole('option', { name: 'information_schema' }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,51 +16,58 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { ReactNode, useState, useMemo } from 'react';
|
import React, { ReactNode, useEffect, useState } from 'react';
|
||||||
import { styled, SupersetClient, t } from '@superset-ui/core';
|
import { styled, SupersetClient, t } from '@superset-ui/core';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { Select } from 'src/components';
|
import { Select } from 'src/components/Select';
|
||||||
import { FormLabel } from 'src/components/Form';
|
import Label from 'src/components/Label';
|
||||||
import RefreshLabel from 'src/components/RefreshLabel';
|
import RefreshLabel from 'src/components/RefreshLabel';
|
||||||
|
import SupersetAsyncSelect from 'src/components/AsyncSelect';
|
||||||
|
|
||||||
const DatabaseSelectorWrapper = styled.div`
|
const FieldTitle = styled.p`
|
||||||
${({ theme }) => `
|
color: ${({ theme }) => theme.colors.secondary.light2};
|
||||||
.refresh {
|
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||||
display: flex;
|
margin: 20px 0 10px 0;
|
||||||
align-items: center;
|
text-transform: uppercase;
|
||||||
width: 30px;
|
|
||||||
margin-left: ${theme.gridUnit}px;
|
|
||||||
margin-top: ${theme.gridUnit * 5}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > div {
|
|
||||||
margin-bottom: ${theme.gridUnit * 4}px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type DatabaseValue = { label: string; value: number };
|
const DatabaseSelectorWrapper = styled.div`
|
||||||
|
.fa-refresh {
|
||||||
|
padding-left: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
type SchemaValue = { label: string; value: string };
|
.refresh-col {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 30px;
|
||||||
|
margin-left: ${({ theme }) => theme.gridUnit}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding-bottom: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DatabaseOption = styled.span`
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
interface DatabaseSelectorProps {
|
interface DatabaseSelectorProps {
|
||||||
db?: { id: number; database_name: string; backend: string };
|
dbId: number;
|
||||||
formMode?: boolean;
|
formMode?: boolean;
|
||||||
getDbList?: (arg0: any) => {};
|
getDbList?: (arg0: any) => {};
|
||||||
|
getTableList?: (dbId: number, schema: string, force: boolean) => {};
|
||||||
handleError: (msg: string) => void;
|
handleError: (msg: string) => void;
|
||||||
isDatabaseSelectEnabled?: boolean;
|
isDatabaseSelectEnabled?: boolean;
|
||||||
onDbChange?: (db: any) => void;
|
onDbChange?: (db: any) => void;
|
||||||
onSchemaChange?: (schema?: string) => void;
|
onSchemaChange?: (arg0?: any) => {};
|
||||||
onSchemasLoad?: (schemas: Array<object>) => void;
|
onSchemasLoad?: (schemas: Array<object>) => void;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
schema?: string;
|
schema?: string;
|
||||||
@@ -76,9 +83,10 @@ interface DatabaseSelectorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DatabaseSelector({
|
export default function DatabaseSelector({
|
||||||
db,
|
dbId,
|
||||||
formMode = false,
|
formMode = false,
|
||||||
getDbList,
|
getDbList,
|
||||||
|
getTableList,
|
||||||
handleError,
|
handleError,
|
||||||
isDatabaseSelectEnabled = true,
|
isDatabaseSelectEnabled = true,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
@@ -89,189 +97,193 @@ export default function DatabaseSelector({
|
|||||||
schema,
|
schema,
|
||||||
sqlLabMode = false,
|
sqlLabMode = false,
|
||||||
}: DatabaseSelectorProps) {
|
}: DatabaseSelectorProps) {
|
||||||
const [currentDb, setCurrentDb] = useState(
|
const [currentDbId, setCurrentDbId] = useState(dbId);
|
||||||
db
|
const [currentSchema, setCurrentSchema] = useState<string | undefined>(
|
||||||
? { label: `${db.backend}: ${db.database_name}`, value: db.id }
|
schema,
|
||||||
: undefined,
|
|
||||||
);
|
);
|
||||||
const [currentSchema, setCurrentSchema] = useState<SchemaValue | undefined>(
|
const [schemaLoading, setSchemaLoading] = useState(false);
|
||||||
schema ? { label: schema, value: schema } : undefined,
|
const [schemaOptions, setSchemaOptions] = useState([]);
|
||||||
);
|
|
||||||
const [refresh, setRefresh] = useState(0);
|
|
||||||
|
|
||||||
const loadSchemas = useMemo(
|
function fetchSchemas(databaseId: number, forceRefresh = false) {
|
||||||
() => async (): Promise<{
|
const actualDbId = databaseId || dbId;
|
||||||
data: SchemaValue[];
|
if (actualDbId) {
|
||||||
totalCount: number;
|
setSchemaLoading(true);
|
||||||
}> => {
|
const queryParams = rison.encode({
|
||||||
if (currentDb) {
|
force: Boolean(forceRefresh),
|
||||||
const queryParams = rison.encode({ force: refresh > 0 });
|
});
|
||||||
const endpoint = `/api/v1/database/${currentDb.value}/schemas/?q=${queryParams}`;
|
const endpoint = `/api/v1/database/${actualDbId}/schemas/?q=${queryParams}`;
|
||||||
|
return SupersetClient.get({ endpoint })
|
||||||
// TODO: Would be nice to add pagination in a follow-up. Needs endpoint changes.
|
.then(({ json }) => {
|
||||||
return SupersetClient.get({ endpoint }).then(({ json }) => {
|
|
||||||
const options = json.result.map((s: string) => ({
|
const options = json.result.map((s: string) => ({
|
||||||
value: s,
|
value: s,
|
||||||
label: s,
|
label: s,
|
||||||
title: s,
|
title: s,
|
||||||
}));
|
}));
|
||||||
|
setSchemaOptions(options);
|
||||||
|
setSchemaLoading(false);
|
||||||
if (onSchemasLoad) {
|
if (onSchemasLoad) {
|
||||||
onSchemasLoad(options);
|
onSchemasLoad(options);
|
||||||
}
|
}
|
||||||
return {
|
})
|
||||||
data: options,
|
.catch(() => {
|
||||||
totalCount: options.length,
|
setSchemaOptions([]);
|
||||||
};
|
setSchemaLoading(false);
|
||||||
|
handleError(t('Error while fetching schema list'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return {
|
return Promise.resolve();
|
||||||
data: [],
|
}
|
||||||
totalCount: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[currentDb, refresh, onSchemasLoad],
|
|
||||||
);
|
|
||||||
|
|
||||||
function onSelectChange({
|
useEffect(() => {
|
||||||
db,
|
if (currentDbId) {
|
||||||
schema,
|
fetchSchemas(currentDbId);
|
||||||
}: {
|
}
|
||||||
db: DatabaseValue;
|
}, [currentDbId]);
|
||||||
schema?: SchemaValue;
|
|
||||||
}) {
|
function onSelectChange({ dbId, schema }: { dbId: number; schema?: string }) {
|
||||||
setCurrentDb(db);
|
setCurrentDbId(dbId);
|
||||||
setCurrentSchema(schema);
|
setCurrentSchema(schema);
|
||||||
if (onUpdate) {
|
if (onUpdate) {
|
||||||
onUpdate({
|
onUpdate({ dbId, schema, tableName: undefined });
|
||||||
dbId: db.value,
|
|
||||||
schema: schema?.value,
|
|
||||||
tableName: undefined,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeDataBase(selectedValue: DatabaseValue) {
|
function dbMutator(data: any) {
|
||||||
const actualDb = selectedValue || db;
|
if (getDbList) {
|
||||||
|
getDbList(data.result);
|
||||||
|
}
|
||||||
|
if (data.result.length === 0) {
|
||||||
|
handleError(t("It seems you don't have access to any database"));
|
||||||
|
}
|
||||||
|
return data.result.map((row: any) => ({
|
||||||
|
...row,
|
||||||
|
// label is used for the typeahead
|
||||||
|
label: `${row.backend} ${row.database_name}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeDataBase(db: any, force = false) {
|
||||||
|
const dbId = db ? db.id : null;
|
||||||
|
setSchemaOptions([]);
|
||||||
if (onSchemaChange) {
|
if (onSchemaChange) {
|
||||||
onSchemaChange(undefined);
|
onSchemaChange(null);
|
||||||
}
|
}
|
||||||
if (onDbChange) {
|
if (onDbChange) {
|
||||||
onDbChange(db);
|
onDbChange(db);
|
||||||
}
|
}
|
||||||
onSelectChange({ db: actualDb, schema: undefined });
|
fetchSchemas(dbId, force);
|
||||||
|
onSelectChange({ dbId, schema: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeSchema(schema: SchemaValue) {
|
function changeSchema(schemaOpt: any, force = false) {
|
||||||
|
const schema = schemaOpt ? schemaOpt.value : null;
|
||||||
if (onSchemaChange) {
|
if (onSchemaChange) {
|
||||||
onSchemaChange(schema.value);
|
onSchemaChange(schema);
|
||||||
}
|
}
|
||||||
if (currentDb) {
|
setCurrentSchema(schema);
|
||||||
onSelectChange({ db: currentDb, schema });
|
onSelectChange({ dbId: currentDbId, schema });
|
||||||
|
if (getTableList) {
|
||||||
|
getTableList(currentDbId, schema, force);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderDatabaseOption(db: any) {
|
||||||
|
return (
|
||||||
|
<DatabaseOption title={db.database_name}>
|
||||||
|
<Label type="default">{db.backend}</Label> {db.database_name}
|
||||||
|
</DatabaseOption>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
|
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
|
||||||
return (
|
return (
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<span className="select">{select}</span>
|
<span className="select">{select}</span>
|
||||||
<span className="refresh">{refreshBtn}</span>
|
<span className="refresh-col">{refreshBtn}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadDatabases = useMemo(
|
|
||||||
() => async (
|
|
||||||
search: string,
|
|
||||||
page: number,
|
|
||||||
pageSize: number,
|
|
||||||
): Promise<{
|
|
||||||
data: DatabaseValue[];
|
|
||||||
totalCount: number;
|
|
||||||
}> => {
|
|
||||||
const queryParams = rison.encode({
|
|
||||||
order_columns: 'database_name',
|
|
||||||
order_direction: 'asc',
|
|
||||||
page,
|
|
||||||
page_size: pageSize,
|
|
||||||
...(formMode || !sqlLabMode
|
|
||||||
? { filters: [{ col: 'database_name', opr: 'ct', value: search }] }
|
|
||||||
: {
|
|
||||||
filters: [
|
|
||||||
{ col: 'database_name', opr: 'ct', value: search },
|
|
||||||
{
|
|
||||||
col: 'expose_in_sqllab',
|
|
||||||
opr: 'eq',
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const endpoint = `/api/v1/database/?q=${queryParams}`;
|
|
||||||
return SupersetClient.get({ endpoint }).then(({ json }) => {
|
|
||||||
const { result } = json;
|
|
||||||
if (getDbList) {
|
|
||||||
getDbList(result);
|
|
||||||
}
|
|
||||||
if (result.length === 0) {
|
|
||||||
handleError(t("It seems you don't have access to any database"));
|
|
||||||
}
|
|
||||||
const options = result.map(
|
|
||||||
(row: { backend: string; database_name: string; id: number }) => ({
|
|
||||||
label: `${row.backend}: ${row.database_name}`,
|
|
||||||
value: row.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
data: options,
|
|
||||||
totalCount: options.length,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[formMode, getDbList, handleError, sqlLabMode],
|
|
||||||
);
|
|
||||||
|
|
||||||
function renderDatabaseSelect() {
|
function renderDatabaseSelect() {
|
||||||
|
const queryParams = rison.encode({
|
||||||
|
order_columns: 'database_name',
|
||||||
|
order_direction: 'asc',
|
||||||
|
page: 0,
|
||||||
|
page_size: -1,
|
||||||
|
...(formMode || !sqlLabMode
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
col: 'expose_in_sqllab',
|
||||||
|
opr: 'eq',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
return renderSelectRow(
|
return renderSelectRow(
|
||||||
<Select
|
<SupersetAsyncSelect
|
||||||
ariaLabel={t('Select a database')}
|
|
||||||
data-test="select-database"
|
data-test="select-database"
|
||||||
header={<FormLabel>{t('Database')}</FormLabel>}
|
dataEndpoint={`/api/v1/database/?q=${queryParams}`}
|
||||||
onChange={changeDataBase}
|
onChange={(db: any) => changeDataBase(db)}
|
||||||
value={currentDb}
|
onAsyncError={() =>
|
||||||
|
handleError(t('Error while fetching database list'))
|
||||||
|
}
|
||||||
|
clearable={false}
|
||||||
|
value={currentDbId}
|
||||||
|
valueKey="id"
|
||||||
|
valueRenderer={(db: any) => (
|
||||||
|
<div>
|
||||||
|
<span className="text-muted m-r-5">{t('Database:')}</span>
|
||||||
|
{renderDatabaseOption(db)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
optionRenderer={renderDatabaseOption}
|
||||||
|
mutator={dbMutator}
|
||||||
placeholder={t('Select a database')}
|
placeholder={t('Select a database')}
|
||||||
disabled={!isDatabaseSelectEnabled || readOnly}
|
autoSelect
|
||||||
options={loadDatabases}
|
isDisabled={!isDatabaseSelectEnabled || readOnly}
|
||||||
/>,
|
/>,
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSchemaSelect() {
|
function renderSchemaSelect() {
|
||||||
const refreshIcon = !formMode && !readOnly && (
|
const value = schemaOptions.filter(({ value }) => currentSchema === value);
|
||||||
|
const refresh = !formMode && !readOnly && (
|
||||||
<RefreshLabel
|
<RefreshLabel
|
||||||
onClick={() => setRefresh(refresh + 1)}
|
onClick={() => changeDataBase({ id: dbId }, true)}
|
||||||
tooltipContent={t('Force refresh schema list')}
|
tooltipContent={t('Force refresh schema list')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return renderSelectRow(
|
return renderSelectRow(
|
||||||
<Select
|
<Select
|
||||||
ariaLabel={t('Select a schema')}
|
|
||||||
disabled={readOnly}
|
|
||||||
header={<FormLabel>{t('Schema')}</FormLabel>}
|
|
||||||
name="select-schema"
|
name="select-schema"
|
||||||
placeholder={t('Select a schema')}
|
placeholder={t('Select a schema (%s)', schemaOptions.length)}
|
||||||
onChange={item => changeSchema(item as SchemaValue)}
|
options={schemaOptions}
|
||||||
options={loadSchemas}
|
value={value}
|
||||||
value={currentSchema}
|
valueRenderer={o => (
|
||||||
|
<div>
|
||||||
|
<span className="text-muted">{t('Schema:')}</span> {o.label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
isLoading={schemaLoading}
|
||||||
|
autosize={false}
|
||||||
|
onChange={item => changeSchema(item)}
|
||||||
|
isDisabled={readOnly}
|
||||||
/>,
|
/>,
|
||||||
refreshIcon,
|
refresh,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DatabaseSelectorWrapper data-test="DatabaseSelector">
|
<DatabaseSelectorWrapper data-test="DatabaseSelector">
|
||||||
|
{formMode && <FieldTitle>{t('datasource')}</FieldTitle>}
|
||||||
{renderDatabaseSelect()}
|
{renderDatabaseSelect()}
|
||||||
|
{formMode && <FieldTitle>{t('schema')}</FieldTitle>}
|
||||||
{renderSchemaSelect()}
|
{renderSchemaSelect()}
|
||||||
</DatabaseSelectorWrapper>
|
</DatabaseSelectorWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,21 +53,15 @@ export const Icon = (props: IconProps) => {
|
|||||||
const name = fileName.replace('_', '-');
|
const name = fileName.replace('_', '-');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
|
||||||
async function importIcon(): Promise<void> {
|
async function importIcon(): Promise<void> {
|
||||||
ImportedSVG.current = (
|
ImportedSVG.current = (
|
||||||
await import(
|
await import(
|
||||||
`!!@svgr/webpack?-svgo,+titleProp,+ref!images/icons/${fileName}.svg`
|
`!!@svgr/webpack?-svgo,+titleProp,+ref!images/icons/${fileName}.svg`
|
||||||
)
|
)
|
||||||
).default;
|
).default;
|
||||||
if (!cancelled) {
|
setLoaded(true);
|
||||||
setLoaded(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
importIcon();
|
importIcon();
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [fileName, ImportedSVG]);
|
}, [fileName, ImportedSVG]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import OmniContainer from './index';
|
|||||||
jest.mock('src/featureFlags', () => ({
|
jest.mock('src/featureFlags', () => ({
|
||||||
isFeatureEnabled: jest.fn(),
|
isFeatureEnabled: jest.fn(),
|
||||||
FeatureFlag: { OMNIBAR: 'OMNIBAR' },
|
FeatureFlag: { OMNIBAR: 'OMNIBAR' },
|
||||||
|
initFeatureFlags: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test('Do not open Omnibar with the featureflag disabled', () => {
|
test('Do not open Omnibar with the featureflag disabled', () => {
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ type PickedSelectProps = Pick<
|
|||||||
AntdSelectAllProps,
|
AntdSelectAllProps,
|
||||||
| 'allowClear'
|
| 'allowClear'
|
||||||
| 'autoFocus'
|
| 'autoFocus'
|
||||||
| 'value'
|
|
||||||
| 'disabled'
|
| 'disabled'
|
||||||
| 'filterOption'
|
| 'filterOption'
|
||||||
| 'notFoundContent'
|
| 'notFoundContent'
|
||||||
@@ -87,9 +86,12 @@ const StyledContainer = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledSelect = styled(AntdSelect)`
|
const StyledSelect = styled(AntdSelect, {
|
||||||
${({ theme }) => `
|
shouldForwardProp: prop => prop !== 'hasHeader',
|
||||||
|
})<{ hasHeader: boolean }>`
|
||||||
|
${({ theme, hasHeader }) => `
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-top: ${hasHeader ? theme.gridUnit : 0}px;
|
||||||
|
|
||||||
&& .ant-select-selector {
|
&& .ant-select-selector {
|
||||||
border-radius: ${theme.gridUnit}px;
|
border-radius: ${theme.gridUnit}px;
|
||||||
@@ -187,7 +189,6 @@ const Select = ({
|
|||||||
: 'multiple';
|
: 'multiple';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchedQueries.current.clear();
|
|
||||||
setSelectOptions(
|
setSelectOptions(
|
||||||
options && Array.isArray(options) ? options : EMPTY_OPTIONS,
|
options && Array.isArray(options) ? options : EMPTY_OPTIONS,
|
||||||
);
|
);
|
||||||
@@ -365,45 +366,34 @@ const Select = ({
|
|||||||
[options],
|
[options],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnSearch = useMemo(
|
const handleOnSearch = debounce((search: string) => {
|
||||||
() =>
|
const searchValue = search.trim();
|
||||||
debounce((search: string) => {
|
// enables option creation
|
||||||
const searchValue = search.trim();
|
if (allowNewOptions && isSingleMode) {
|
||||||
// enables option creation
|
const firstOption = selectOptions.length > 0 && selectOptions[0].value;
|
||||||
if (allowNewOptions && isSingleMode) {
|
// replaces the last search value entered with the new one
|
||||||
const firstOption =
|
// only when the value wasn't part of the original options
|
||||||
selectOptions.length > 0 && selectOptions[0].value;
|
if (
|
||||||
// replaces the last search value entered with the new one
|
searchValue &&
|
||||||
// only when the value wasn't part of the original options
|
firstOption === searchedValue &&
|
||||||
if (
|
!initialOptions.find(o => o.value === searchedValue)
|
||||||
searchValue &&
|
) {
|
||||||
firstOption === searchedValue &&
|
selectOptions.shift();
|
||||||
!initialOptions.find(o => o.value === searchedValue)
|
setSelectOptions(selectOptions);
|
||||||
) {
|
}
|
||||||
selectOptions.shift();
|
if (searchValue && !hasOption(searchValue, selectOptions)) {
|
||||||
setSelectOptions(selectOptions);
|
const newOption = {
|
||||||
}
|
label: searchValue,
|
||||||
if (searchValue && !hasOption(searchValue, selectOptions)) {
|
value: searchValue,
|
||||||
const newOption = {
|
};
|
||||||
label: searchValue,
|
// adds a custom option
|
||||||
value: searchValue,
|
const newOptions = [...selectOptions, newOption];
|
||||||
};
|
setSelectOptions(newOptions);
|
||||||
// adds a custom option
|
setSelectValue(searchValue);
|
||||||
const newOptions = [...selectOptions, newOption];
|
}
|
||||||
setSelectOptions(newOptions);
|
}
|
||||||
setSelectValue(searchValue);
|
setSearchedValue(searchValue);
|
||||||
}
|
}, DEBOUNCE_TIMEOUT);
|
||||||
}
|
|
||||||
setSearchedValue(searchValue);
|
|
||||||
}, DEBOUNCE_TIMEOUT),
|
|
||||||
[
|
|
||||||
allowNewOptions,
|
|
||||||
initialOptions,
|
|
||||||
isSingleMode,
|
|
||||||
searchedValue,
|
|
||||||
selectOptions,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePagination = (e: UIEvent<HTMLElement>) => {
|
const handlePagination = (e: UIEvent<HTMLElement>) => {
|
||||||
const vScroll = e.currentTarget;
|
const vScroll = e.currentTarget;
|
||||||
@@ -496,6 +486,7 @@ const Select = ({
|
|||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
{header}
|
{header}
|
||||||
<StyledSelect
|
<StyledSelect
|
||||||
|
hasHeader={!!header}
|
||||||
aria-label={ariaLabel || name}
|
aria-label={ariaLabel || name}
|
||||||
dropdownRender={dropdownRender}
|
dropdownRender={dropdownRender}
|
||||||
filterOption={handleFilterOption}
|
filterOption={handleFilterOption}
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
/**
|
||||||
|
* 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 React from 'react';
|
||||||
|
import configureStore from 'redux-mock-store';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||||
|
|
||||||
|
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||||
|
|
||||||
|
import DatabaseSelector from 'src/components/DatabaseSelector';
|
||||||
|
import TableSelector from 'src/components/TableSelector';
|
||||||
|
import { initialState, tables } from 'spec/javascripts/sqllab/fixtures';
|
||||||
|
|
||||||
|
const mockStore = configureStore([thunk]);
|
||||||
|
const store = mockStore(initialState);
|
||||||
|
|
||||||
|
const FETCH_SCHEMAS_ENDPOINT = 'glob:*/api/v1/database/*/schemas/*';
|
||||||
|
const GET_TABLE_ENDPOINT = 'glob:*/superset/tables/1/*/*';
|
||||||
|
const GET_TABLE_NAMES_ENDPOINT = 'glob:*/superset/tables/1/main/*';
|
||||||
|
|
||||||
|
const mockedProps = {
|
||||||
|
clearable: false,
|
||||||
|
database: { id: 1, database_name: 'main' },
|
||||||
|
dbId: 1,
|
||||||
|
formMode: false,
|
||||||
|
getDbList: sinon.stub(),
|
||||||
|
handleError: sinon.stub(),
|
||||||
|
horizontal: false,
|
||||||
|
onChange: sinon.stub(),
|
||||||
|
onDbChange: sinon.stub(),
|
||||||
|
onSchemaChange: sinon.stub(),
|
||||||
|
onTableChange: sinon.stub(),
|
||||||
|
sqlLabMode: true,
|
||||||
|
tableName: '',
|
||||||
|
tableNameSticky: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const schemaOptions = {
|
||||||
|
result: ['main', 'erf', 'superset'],
|
||||||
|
};
|
||||||
|
const selectedSchema = { label: 'main', title: 'main', value: 'main' };
|
||||||
|
const selectedTable = {
|
||||||
|
extra: null,
|
||||||
|
label: 'birth_names',
|
||||||
|
schema: 'main',
|
||||||
|
title: 'birth_names',
|
||||||
|
type: undefined,
|
||||||
|
value: 'birth_names',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function mountAndWait(props = mockedProps) {
|
||||||
|
const mounted = mount(<TableSelector {...props} />, {
|
||||||
|
context: { store },
|
||||||
|
wrappingComponent: ThemeProvider,
|
||||||
|
wrappingComponentProps: { theme: supersetTheme },
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(mounted);
|
||||||
|
|
||||||
|
return mounted;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('TableSelector', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
fetchMock.reset();
|
||||||
|
wrapper = await mountAndWait();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
expect(wrapper.find(TableSelector)).toExist();
|
||||||
|
expect(wrapper.find(DatabaseSelector)).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change database', () => {
|
||||||
|
afterEach(fetchMock.resetHistory);
|
||||||
|
afterAll(fetchMock.reset);
|
||||||
|
|
||||||
|
it('should fetch schemas', async () => {
|
||||||
|
fetchMock.get(FETCH_SCHEMAS_ENDPOINT, { overwriteRoutes: true });
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||||
|
id: 1,
|
||||||
|
database_name: 'main',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(fetchMock.calls(FETCH_SCHEMAS_ENDPOINT)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch schema options', async () => {
|
||||||
|
fetchMock.get(FETCH_SCHEMAS_ENDPOINT, schemaOptions, {
|
||||||
|
overwriteRoutes: true,
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||||
|
id: 1,
|
||||||
|
database_name: 'main',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
wrapper.update();
|
||||||
|
expect(fetchMock.calls(FETCH_SCHEMAS_ENDPOINT)).toHaveLength(1);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapper.find('[name="select-schema"]').first().props().options,
|
||||||
|
).toEqual([
|
||||||
|
{ value: 'main', label: 'main', title: 'main' },
|
||||||
|
{ value: 'erf', label: 'erf', title: 'erf' },
|
||||||
|
{ value: 'superset', label: 'superset', title: 'superset' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear table options', async () => {
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||||
|
id: 1,
|
||||||
|
database_name: 'main',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
const props = wrapper.find('[name="async-select-table"]').first().props();
|
||||||
|
expect(props.isDisabled).toBe(true);
|
||||||
|
expect(props.value).toEqual(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change schema', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
fetchMock.get(FETCH_SCHEMAS_ENDPOINT, schemaOptions, {
|
||||||
|
overwriteRoutes: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(fetchMock.resetHistory);
|
||||||
|
afterAll(fetchMock.reset);
|
||||||
|
|
||||||
|
it('should fetch table', async () => {
|
||||||
|
fetchMock.get(GET_TABLE_NAMES_ENDPOINT, { overwriteRoutes: true });
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||||
|
id: 1,
|
||||||
|
database_name: 'main',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('[name="select-schema"]')
|
||||||
|
.first()
|
||||||
|
.props()
|
||||||
|
.onChange(selectedSchema);
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(fetchMock.calls(GET_TABLE_NAMES_ENDPOINT)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch table options', async () => {
|
||||||
|
fetchMock.get(GET_TABLE_NAMES_ENDPOINT, tables, {
|
||||||
|
overwriteRoutes: true,
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||||
|
id: 1,
|
||||||
|
database_name: 'main',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('[name="select-schema"]')
|
||||||
|
.first()
|
||||||
|
.props()
|
||||||
|
.onChange(selectedSchema);
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(
|
||||||
|
wrapper.find('[name="select-schema"]').first().props().value[0],
|
||||||
|
).toEqual(selectedSchema);
|
||||||
|
expect(fetchMock.calls(GET_TABLE_NAMES_ENDPOINT)).toHaveLength(1);
|
||||||
|
const { options } = wrapper.find('[name="select-table"]').first().props();
|
||||||
|
expect({ options }).toEqual(tables);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change table', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
fetchMock.get(GET_TABLE_NAMES_ENDPOINT, tables, {
|
||||||
|
overwriteRoutes: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change table value', async () => {
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('[name="select-schema"]')
|
||||||
|
.first()
|
||||||
|
.props()
|
||||||
|
.onChange(selectedSchema);
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('[name="select-table"]')
|
||||||
|
.first()
|
||||||
|
.props()
|
||||||
|
.onChange(selectedTable);
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(
|
||||||
|
wrapper.find('[name="select-table"]').first().props().value,
|
||||||
|
).toEqual('birth_names');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onTableChange with schema from table object', async () => {
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('[name="select-schema"]')
|
||||||
|
.first()
|
||||||
|
.props()
|
||||||
|
.onChange(selectedSchema);
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('[name="select-table"]')
|
||||||
|
.first()
|
||||||
|
.props()
|
||||||
|
.onChange(selectedTable);
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(mockedProps.onTableChange.getCall(0).args[0]).toBe('birth_names');
|
||||||
|
expect(mockedProps.onTableChange.getCall(0).args[1]).toBe('main');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTableNamesBySubStr', () => {
|
||||||
|
afterEach(fetchMock.resetHistory);
|
||||||
|
afterAll(fetchMock.reset);
|
||||||
|
|
||||||
|
it('should handle empty', async () => {
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('[name="async-select-table"]')
|
||||||
|
.first()
|
||||||
|
.props()
|
||||||
|
.loadOptions();
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
const props = wrapper.find('[name="async-select-table"]').first().props();
|
||||||
|
expect(props.isDisabled).toBe(true);
|
||||||
|
expect(props.value).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle table name', async () => {
|
||||||
|
wrapper.setProps({ schema: 'main' });
|
||||||
|
fetchMock.get(GET_TABLE_ENDPOINT, tables, {
|
||||||
|
overwriteRoutes: true,
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('[name="async-select-table"]')
|
||||||
|
.first()
|
||||||
|
.props()
|
||||||
|
.loadOptions();
|
||||||
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(fetchMock.calls(GET_TABLE_ENDPOINT)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,91 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
|
||||||
import { SupersetClient } from '@superset-ui/core';
|
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import TableSelector from '.';
|
|
||||||
|
|
||||||
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
|
|
||||||
|
|
||||||
const createProps = () => ({
|
|
||||||
dbId: 1,
|
|
||||||
schema: 'test_schema',
|
|
||||||
handleError: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
SupersetClientGet.mockImplementation(
|
|
||||||
async () =>
|
|
||||||
({
|
|
||||||
json: {
|
|
||||||
options: [
|
|
||||||
{ label: 'table_a', value: 'table_a' },
|
|
||||||
{ label: 'table_b', value: 'table_b' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
} as any),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('renders with default props', async () => {
|
|
||||||
const props = createProps();
|
|
||||||
render(<TableSelector {...props} />);
|
|
||||||
const databaseSelect = screen.getByRole('combobox', {
|
|
||||||
name: 'Select a database',
|
|
||||||
});
|
|
||||||
const schemaSelect = screen.getByRole('combobox', {
|
|
||||||
name: 'Select a database',
|
|
||||||
});
|
|
||||||
const tableSelect = screen.getByRole('combobox', {
|
|
||||||
name: 'Select a table',
|
|
||||||
});
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(databaseSelect).toBeInTheDocument();
|
|
||||||
expect(schemaSelect).toBeInTheDocument();
|
|
||||||
expect(tableSelect).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('renders table options', async () => {
|
|
||||||
const props = createProps();
|
|
||||||
render(<TableSelector {...props} />);
|
|
||||||
const tableSelect = screen.getByRole('combobox', {
|
|
||||||
name: 'Select a table',
|
|
||||||
});
|
|
||||||
userEvent.click(tableSelect);
|
|
||||||
expect(
|
|
||||||
await screen.findByRole('option', { name: 'table_a' }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
await screen.findByRole('option', { name: 'table_b' }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('renders disabled without schema', async () => {
|
|
||||||
const props = createProps();
|
|
||||||
render(<TableSelector {...props} schema={undefined} />);
|
|
||||||
const tableSelect = screen.getByRole('combobox', {
|
|
||||||
name: 'Select a table',
|
|
||||||
});
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(tableSelect).toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -18,49 +18,57 @@
|
|||||||
*/
|
*/
|
||||||
import React, {
|
import React, {
|
||||||
FunctionComponent,
|
FunctionComponent,
|
||||||
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useMemo,
|
|
||||||
useEffect,
|
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { styled, SupersetClient, t } from '@superset-ui/core';
|
import { styled, SupersetClient, t } from '@superset-ui/core';
|
||||||
import { Select } from 'src/components';
|
import { AsyncSelect, CreatableSelect, Select } from 'src/components/Select';
|
||||||
|
|
||||||
import { FormLabel } from 'src/components/Form';
|
import { FormLabel } from 'src/components/Form';
|
||||||
import Icons from 'src/components/Icons';
|
|
||||||
import DatabaseSelector from 'src/components/DatabaseSelector';
|
import DatabaseSelector from 'src/components/DatabaseSelector';
|
||||||
import RefreshLabel from 'src/components/RefreshLabel';
|
import RefreshLabel from 'src/components/RefreshLabel';
|
||||||
import CertifiedIcon from 'src/components/CertifiedIcon';
|
import CertifiedIcon from 'src/components/CertifiedIcon';
|
||||||
import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip';
|
import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip';
|
||||||
|
|
||||||
|
const FieldTitle = styled.p`
|
||||||
|
color: ${({ theme }) => theme.colors.secondary.light2};
|
||||||
|
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||||
|
margin: 20px 0 10px 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
`;
|
||||||
|
|
||||||
const TableSelectorWrapper = styled.div`
|
const TableSelectorWrapper = styled.div`
|
||||||
${({ theme }) => `
|
.fa-refresh {
|
||||||
.refresh {
|
padding-left: 9px;
|
||||||
display: flex;
|
}
|
||||||
align-items: center;
|
|
||||||
width: 30px;
|
|
||||||
margin-left: ${theme.gridUnit}px;
|
|
||||||
margin-top: ${theme.gridUnit * 5}px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
.refresh-col {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
align-items: center;
|
||||||
align-items: center;
|
width: 30px;
|
||||||
}
|
margin-left: ${({ theme }) => theme.gridUnit}px;
|
||||||
|
}
|
||||||
|
|
||||||
.divider {
|
.section {
|
||||||
border-bottom: 1px solid ${theme.colors.secondary.light5};
|
padding-bottom: 5px;
|
||||||
margin: 15px 0;
|
display: flex;
|
||||||
}
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
.table-length {
|
.select {
|
||||||
color: ${theme.colors.grayscale.light1};
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select {
|
.divider {
|
||||||
flex: 1;
|
border-bottom: 1px solid ${({ theme }) => theme.colors.secondary.light5};
|
||||||
}
|
margin: 15px 0;
|
||||||
`}
|
}
|
||||||
|
|
||||||
|
.table-length {
|
||||||
|
color: ${({ theme }) => theme.colors.grayscale.light1};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const TableLabel = styled.span`
|
const TableLabel = styled.span`
|
||||||
@@ -90,15 +98,7 @@ interface TableSelectorProps {
|
|||||||
schema?: string;
|
schema?: string;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
onDbChange?: (
|
onDbChange?: (db: any) => void;
|
||||||
db:
|
|
||||||
| {
|
|
||||||
id: number;
|
|
||||||
database_name: string;
|
|
||||||
backend: string;
|
|
||||||
}
|
|
||||||
| undefined,
|
|
||||||
) => void;
|
|
||||||
onSchemaChange?: (arg0?: any) => {};
|
onSchemaChange?: (arg0?: any) => {};
|
||||||
onSchemasLoad?: () => void;
|
onSchemasLoad?: () => void;
|
||||||
onTableChange?: (tableName: string, schema: string) => void;
|
onTableChange?: (tableName: string, schema: string) => void;
|
||||||
@@ -110,52 +110,6 @@ interface TableSelectorProps {
|
|||||||
tableNameSticky?: boolean;
|
tableNameSticky?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Table {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
type: string;
|
|
||||||
extra?: {
|
|
||||||
certification?: {
|
|
||||||
certified_by: string;
|
|
||||||
details: string;
|
|
||||||
};
|
|
||||||
warning_markdown?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableOption {
|
|
||||||
label: JSX.Element;
|
|
||||||
text: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TableOption = ({ table }: { table: Table }) => {
|
|
||||||
const { label, type, extra } = table;
|
|
||||||
return (
|
|
||||||
<TableLabel title={label}>
|
|
||||||
{type === 'view' ? (
|
|
||||||
<Icons.Eye iconSize="m" />
|
|
||||||
) : (
|
|
||||||
<Icons.Table iconSize="m" />
|
|
||||||
)}
|
|
||||||
{extra?.certification && (
|
|
||||||
<CertifiedIcon
|
|
||||||
certifiedBy={extra.certification.certified_by}
|
|
||||||
details={extra.certification.details}
|
|
||||||
size="l"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{extra?.warning_markdown && (
|
|
||||||
<WarningIconWithTooltip
|
|
||||||
warningMarkdown={extra.warning_markdown}
|
|
||||||
size="l"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{label}
|
|
||||||
</TableLabel>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
||||||
database,
|
database,
|
||||||
dbId,
|
dbId,
|
||||||
@@ -175,187 +129,179 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
|||||||
tableName,
|
tableName,
|
||||||
tableNameSticky = true,
|
tableNameSticky = true,
|
||||||
}) => {
|
}) => {
|
||||||
const [currentDbId, setCurrentDbId] = useState<number | undefined>(dbId);
|
|
||||||
const [currentSchema, setCurrentSchema] = useState<string | undefined>(
|
const [currentSchema, setCurrentSchema] = useState<string | undefined>(
|
||||||
schema,
|
schema,
|
||||||
);
|
);
|
||||||
const [currentTable, setCurrentTable] = useState<TableOption | undefined>();
|
const [currentTableName, setCurrentTableName] = useState<string | undefined>(
|
||||||
const [refresh, setRefresh] = useState(0);
|
tableName,
|
||||||
const [previousRefresh, setPreviousRefresh] = useState(0);
|
|
||||||
|
|
||||||
const loadTable = useMemo(
|
|
||||||
() => async (dbId: number, schema: string, tableName: string) => {
|
|
||||||
const endpoint = encodeURI(
|
|
||||||
`/superset/tables/${dbId}/${schema}/${encodeURIComponent(
|
|
||||||
tableName,
|
|
||||||
)}/false/true`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (previousRefresh !== refresh) {
|
|
||||||
setPreviousRefresh(refresh);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SupersetClient.get({ endpoint }).then(({ json }) => {
|
|
||||||
const options = json.options as Table[];
|
|
||||||
if (options && options.length > 0) {
|
|
||||||
return options[0];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[], // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
);
|
);
|
||||||
|
const [tableLoading, setTableLoading] = useState(false);
|
||||||
|
const [tableOptions, setTableOptions] = useState([]);
|
||||||
|
|
||||||
const loadTables = useMemo(
|
function fetchTables(
|
||||||
() => async (search: string) => {
|
databaseId?: number,
|
||||||
const dbSchema = schema || currentSchema;
|
schema?: string,
|
||||||
if (currentDbId && dbSchema) {
|
forceRefresh = false,
|
||||||
const encodedSchema = encodeURIComponent(dbSchema);
|
substr = 'undefined',
|
||||||
const encodedSubstr = encodeURIComponent(search || 'undefined');
|
) {
|
||||||
const forceRefresh = refresh !== previousRefresh;
|
const dbSchema = schema || currentSchema;
|
||||||
const endpoint = encodeURI(
|
const actualDbId = databaseId || dbId;
|
||||||
`/superset/tables/${currentDbId}/${encodedSchema}/${encodedSubstr}/${forceRefresh}/`,
|
if (actualDbId && dbSchema) {
|
||||||
);
|
const encodedSchema = encodeURIComponent(dbSchema);
|
||||||
|
const encodedSubstr = encodeURIComponent(substr);
|
||||||
if (previousRefresh !== refresh) {
|
setTableLoading(true);
|
||||||
setPreviousRefresh(refresh);
|
setTableOptions([]);
|
||||||
}
|
const endpoint = encodeURI(
|
||||||
|
`/superset/tables/${actualDbId}/${encodedSchema}/${encodedSubstr}/${!!forceRefresh}/`,
|
||||||
return SupersetClient.get({ endpoint }).then(({ json }) => {
|
);
|
||||||
const options = json.options
|
return SupersetClient.get({ endpoint })
|
||||||
.map((table: Table) => ({
|
.then(({ json }) => {
|
||||||
value: table.value,
|
const options = json.options.map((o: any) => ({
|
||||||
label: <TableOption table={table} />,
|
value: o.value,
|
||||||
text: table.label,
|
schema: o.schema,
|
||||||
}))
|
label: o.label,
|
||||||
.sort((a: { text: string }, b: { text: string }) =>
|
title: o.title,
|
||||||
a.text.localeCompare(b.text),
|
type: o.type,
|
||||||
);
|
extra: o?.extra,
|
||||||
|
}));
|
||||||
|
setTableLoading(false);
|
||||||
|
setTableOptions(options);
|
||||||
if (onTablesLoad) {
|
if (onTablesLoad) {
|
||||||
onTablesLoad(json.options);
|
onTablesLoad(json.options);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
return {
|
.catch(() => {
|
||||||
data: options,
|
setTableLoading(false);
|
||||||
totalCount: options.length,
|
setTableOptions([]);
|
||||||
};
|
handleError(t('Error while fetching table list'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { data: [], totalCount: 0 };
|
setTableLoading(false);
|
||||||
},
|
setTableOptions([]);
|
||||||
// We are using the refresh state to re-trigger the query
|
return Promise.resolve();
|
||||||
// previousRefresh should be out of dependencies array
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[currentDbId, currentSchema, onTablesLoad, schema, refresh],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchTable() {
|
if (dbId && schema) {
|
||||||
if (schema && tableName) {
|
fetchTables();
|
||||||
const table = await loadTable(dbId, schema, tableName);
|
|
||||||
if (table) {
|
|
||||||
setCurrentTable({
|
|
||||||
label: <TableOption table={table} />,
|
|
||||||
text: table.label,
|
|
||||||
value: table.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fetchTable();
|
}, [dbId, schema]);
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
function onSelectionChange({
|
function onSelectionChange({
|
||||||
dbId,
|
dbId,
|
||||||
schema,
|
schema,
|
||||||
table,
|
tableName,
|
||||||
}: {
|
}: {
|
||||||
dbId: number;
|
dbId: number;
|
||||||
schema?: string;
|
schema?: string;
|
||||||
table?: TableOption;
|
tableName?: string;
|
||||||
}) {
|
}) {
|
||||||
setCurrentTable(table);
|
setCurrentTableName(tableName);
|
||||||
setCurrentDbId(dbId);
|
|
||||||
setCurrentSchema(schema);
|
setCurrentSchema(schema);
|
||||||
if (onUpdate) {
|
if (onUpdate) {
|
||||||
onUpdate({ dbId, schema, tableName: table?.value });
|
onUpdate({ dbId, schema, tableName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeTable(table: TableOption) {
|
function getTableNamesBySubStr(substr = 'undefined') {
|
||||||
if (!table) {
|
if (!dbId || !substr) {
|
||||||
setCurrentTable(undefined);
|
const options: any[] = [];
|
||||||
|
return Promise.resolve({ options });
|
||||||
|
}
|
||||||
|
const encodedSchema = encodeURIComponent(schema || '');
|
||||||
|
const encodedSubstr = encodeURIComponent(substr);
|
||||||
|
return SupersetClient.get({
|
||||||
|
endpoint: encodeURI(
|
||||||
|
`/superset/tables/${dbId}/${encodedSchema}/${encodedSubstr}`,
|
||||||
|
),
|
||||||
|
}).then(({ json }) => {
|
||||||
|
const options = json.options.map((o: any) => ({
|
||||||
|
value: o.value,
|
||||||
|
schema: o.schema,
|
||||||
|
label: o.label,
|
||||||
|
title: o.title,
|
||||||
|
type: o.type,
|
||||||
|
}));
|
||||||
|
return { options };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeTable(tableOpt: any) {
|
||||||
|
if (!tableOpt) {
|
||||||
|
setCurrentTableName('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tableOptTableName = table.value;
|
const schemaName = tableOpt.schema;
|
||||||
if (currentDbId && tableNameSticky) {
|
const tableOptTableName = tableOpt.value;
|
||||||
|
if (tableNameSticky) {
|
||||||
onSelectionChange({
|
onSelectionChange({
|
||||||
dbId: currentDbId,
|
dbId,
|
||||||
schema: currentSchema,
|
schema: schemaName,
|
||||||
table,
|
tableName: tableOptTableName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (onTableChange && currentSchema) {
|
if (onTableChange) {
|
||||||
onTableChange(tableOptTableName, currentSchema);
|
onTableChange(tableOptTableName, schemaName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRefresh() {
|
function changeSchema(schemaOpt: any, force = false) {
|
||||||
|
const value = schemaOpt ? schemaOpt.value : null;
|
||||||
if (onSchemaChange) {
|
if (onSchemaChange) {
|
||||||
onSchemaChange(currentSchema);
|
onSchemaChange(value);
|
||||||
}
|
}
|
||||||
if (currentDbId && currentSchema) {
|
onSelectionChange({
|
||||||
onSelectionChange({
|
dbId,
|
||||||
dbId: currentDbId,
|
schema: value,
|
||||||
schema: currentSchema,
|
tableName: undefined,
|
||||||
table: currentTable,
|
});
|
||||||
});
|
fetchTables(dbId, currentSchema, force);
|
||||||
}
|
}
|
||||||
setRefresh(refresh + 1);
|
|
||||||
|
function renderTableOption(option: any) {
|
||||||
|
return (
|
||||||
|
<TableLabel title={option.label}>
|
||||||
|
<small className="text-muted">
|
||||||
|
<i className={`fa fa-${option.type === 'view' ? 'eye' : 'table'}`} />
|
||||||
|
</small>
|
||||||
|
{option.extra?.certification && (
|
||||||
|
<CertifiedIcon
|
||||||
|
certifiedBy={option.extra.certification.certified_by}
|
||||||
|
details={option.extra.certification.details}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{option.extra?.warning_markdown && (
|
||||||
|
<WarningIconWithTooltip
|
||||||
|
warningMarkdown={option.extra.warning_markdown}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{option.label}
|
||||||
|
</TableLabel>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
|
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
|
||||||
return (
|
return (
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<span className="select">{select}</span>
|
<span className="select">{select}</span>
|
||||||
<span className="refresh">{refreshBtn}</span>
|
<span className="refresh-col">{refreshBtn}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const internalDbChange = (
|
|
||||||
db:
|
|
||||||
| {
|
|
||||||
id: number;
|
|
||||||
database_name: string;
|
|
||||||
backend: string;
|
|
||||||
}
|
|
||||||
| undefined,
|
|
||||||
) => {
|
|
||||||
setCurrentDbId(db?.id);
|
|
||||||
if (onDbChange) {
|
|
||||||
onDbChange(db);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const internalSchemaChange = (schema?: string) => {
|
|
||||||
setCurrentSchema(schema);
|
|
||||||
if (onSchemaChange) {
|
|
||||||
onSchemaChange(schema);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function renderDatabaseSelector() {
|
function renderDatabaseSelector() {
|
||||||
return (
|
return (
|
||||||
<DatabaseSelector
|
<DatabaseSelector
|
||||||
db={database}
|
dbId={dbId}
|
||||||
formMode={formMode}
|
formMode={formMode}
|
||||||
getDbList={getDbList}
|
getDbList={getDbList}
|
||||||
|
getTableList={fetchTables}
|
||||||
handleError={handleError}
|
handleError={handleError}
|
||||||
onUpdate={onSelectionChange}
|
onUpdate={onSelectionChange}
|
||||||
onDbChange={readOnly ? undefined : internalDbChange}
|
onDbChange={readOnly ? undefined : onDbChange}
|
||||||
onSchemaChange={readOnly ? undefined : internalSchemaChange}
|
onSchemaChange={readOnly ? undefined : onSchemaChange}
|
||||||
onSchemasLoad={onSchemasLoad}
|
onSchemasLoad={onSchemasLoad}
|
||||||
schema={currentSchema}
|
schema={currentSchema}
|
||||||
sqlLabMode={sqlLabMode}
|
sqlLabMode={sqlLabMode}
|
||||||
@@ -365,54 +311,96 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFilterOption = useMemo(
|
|
||||||
() => (search: string, option: TableOption) => {
|
|
||||||
const searchValue = search.trim().toLowerCase();
|
|
||||||
const { text } = option;
|
|
||||||
return text.toLowerCase().includes(searchValue);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
function renderTableSelect() {
|
function renderTableSelect() {
|
||||||
const disabled =
|
const options = tableOptions;
|
||||||
(currentSchema && !formMode && readOnly) ||
|
let select = null;
|
||||||
(!currentSchema && !database?.allow_multi_schema_metadata_fetch);
|
if (currentSchema && !formMode) {
|
||||||
|
// dataset editor
|
||||||
const header = sqlLabMode ? (
|
select = (
|
||||||
<FormLabel>{t('See table schema')}</FormLabel>
|
<Select
|
||||||
) : (
|
name="select-table"
|
||||||
<FormLabel>{t('Table')}</FormLabel>
|
isLoading={tableLoading}
|
||||||
);
|
ignoreAccents={false}
|
||||||
|
placeholder={t('Select table or type table name')}
|
||||||
const select = (
|
autosize={false}
|
||||||
<Select
|
onChange={changeTable}
|
||||||
ariaLabel={t('Select a table')}
|
options={options}
|
||||||
disabled={disabled}
|
// @ts-ignore
|
||||||
filterOption={handleFilterOption}
|
value={currentTableName}
|
||||||
header={header}
|
optionRenderer={renderTableOption}
|
||||||
name="select-table"
|
valueRenderer={renderTableOption}
|
||||||
onChange={changeTable}
|
isDisabled={readOnly}
|
||||||
options={loadTables}
|
/>
|
||||||
placeholder={t('Select a table')}
|
);
|
||||||
value={currentTable}
|
} else if (formMode) {
|
||||||
/>
|
select = (
|
||||||
);
|
<CreatableSelect
|
||||||
|
name="select-table"
|
||||||
|
isLoading={tableLoading}
|
||||||
|
ignoreAccents={false}
|
||||||
|
placeholder={t('Select table or type table name')}
|
||||||
|
autosize={false}
|
||||||
|
onChange={changeTable}
|
||||||
|
options={options}
|
||||||
|
// @ts-ignore
|
||||||
|
value={currentTableName}
|
||||||
|
optionRenderer={renderTableOption}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// sql lab
|
||||||
|
let tableSelectPlaceholder;
|
||||||
|
let tableSelectDisabled = false;
|
||||||
|
if (database && database.allow_multi_schema_metadata_fetch) {
|
||||||
|
tableSelectPlaceholder = t('Type to search ...');
|
||||||
|
} else {
|
||||||
|
tableSelectPlaceholder = t('Select table ');
|
||||||
|
tableSelectDisabled = true;
|
||||||
|
}
|
||||||
|
select = (
|
||||||
|
<AsyncSelect
|
||||||
|
name="async-select-table"
|
||||||
|
placeholder={tableSelectPlaceholder}
|
||||||
|
isDisabled={tableSelectDisabled}
|
||||||
|
autosize={false}
|
||||||
|
onChange={changeTable}
|
||||||
|
// @ts-ignore
|
||||||
|
value={currentTableName}
|
||||||
|
loadOptions={getTableNamesBySubStr}
|
||||||
|
optionRenderer={renderTableOption}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
const refresh = !formMode && !readOnly && (
|
const refresh = !formMode && !readOnly && (
|
||||||
<RefreshLabel
|
<RefreshLabel
|
||||||
onClick={onRefresh}
|
onClick={() => changeSchema({ value: schema }, true)}
|
||||||
tooltipContent={t('Force refresh table list')}
|
tooltipContent={t('Force refresh table list')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return renderSelectRow(select, refresh);
|
return renderSelectRow(select, refresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSeeTableLabel() {
|
||||||
|
return (
|
||||||
|
<div className="section">
|
||||||
|
<FormLabel>
|
||||||
|
{t('See table schema')}{' '}
|
||||||
|
{schema && (
|
||||||
|
<small className="table-length">
|
||||||
|
{tableOptions.length} in {schema}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableSelectorWrapper>
|
<TableSelectorWrapper>
|
||||||
{renderDatabaseSelector()}
|
{renderDatabaseSelector()}
|
||||||
{sqlLabMode && !formMode && <div className="divider" />}
|
{!formMode && <div className="divider" />}
|
||||||
|
{sqlLabMode && renderSeeTableLabel()}
|
||||||
|
{formMode && <FieldTitle>{t('Table')}</FieldTitle>}
|
||||||
{renderTableSelect()}
|
{renderTableSelect()}
|
||||||
</TableSelectorWrapper>
|
</TableSelectorWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,17 +18,16 @@
|
|||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTheme, SafeMarkdown } from '@superset-ui/core';
|
import { useTheme, SafeMarkdown } from '@superset-ui/core';
|
||||||
import Icons, { IconType } from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
|
|
||||||
export interface WarningIconWithTooltipProps {
|
export interface WarningIconWithTooltipProps {
|
||||||
warningMarkdown: string;
|
warningMarkdown: string;
|
||||||
size?: IconType['iconSize'];
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function WarningIconWithTooltip({
|
function WarningIconWithTooltip({
|
||||||
warningMarkdown,
|
warningMarkdown,
|
||||||
size,
|
|
||||||
}: WarningIconWithTooltipProps) {
|
}: WarningIconWithTooltipProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
@@ -38,7 +37,6 @@ function WarningIconWithTooltip({
|
|||||||
>
|
>
|
||||||
<Icons.AlertSolid
|
<Icons.AlertSolid
|
||||||
iconColor={theme.colors.alert.base}
|
iconColor={theme.colors.alert.base}
|
||||||
iconSize={size}
|
|
||||||
css={{ marginRight: theme.gridUnit * 2 }}
|
css={{ marginRight: theme.gridUnit * 2 }}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -775,47 +775,41 @@ class DatasourceEditor extends React.PureComponent {
|
|||||||
<div>
|
<div>
|
||||||
{this.state.isSqla && (
|
{this.state.isSqla && (
|
||||||
<>
|
<>
|
||||||
<Col xs={24} md={12}>
|
<Field
|
||||||
<Field
|
fieldKey="databaseSelector"
|
||||||
fieldKey="databaseSelector"
|
label={t('virtual')}
|
||||||
label={t('virtual')}
|
control={
|
||||||
control={
|
<DatabaseSelector
|
||||||
<div css={{ marginTop: 8 }}>
|
dbId={datasource.database.id}
|
||||||
<DatabaseSelector
|
schema={datasource.schema}
|
||||||
db={datasource?.database}
|
onSchemaChange={schema =>
|
||||||
schema={datasource.schema}
|
this.state.isEditMode &&
|
||||||
onSchemaChange={schema =>
|
this.onDatasourcePropChange('schema', schema)
|
||||||
this.state.isEditMode &&
|
|
||||||
this.onDatasourcePropChange('schema', schema)
|
|
||||||
}
|
|
||||||
onDbChange={database =>
|
|
||||||
this.state.isEditMode &&
|
|
||||||
this.onDatasourcePropChange('database', database)
|
|
||||||
}
|
|
||||||
formMode={false}
|
|
||||||
handleError={this.props.addDangerToast}
|
|
||||||
readOnly={!this.state.isEditMode}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div css={{ width: 'calc(100% - 34px)', marginTop: -16 }}>
|
|
||||||
<Field
|
|
||||||
fieldKey="table_name"
|
|
||||||
label={t('Dataset name')}
|
|
||||||
control={
|
|
||||||
<TextControl
|
|
||||||
controlId="table_name"
|
|
||||||
onChange={table => {
|
|
||||||
this.onDatasourcePropChange('table_name', table);
|
|
||||||
}}
|
|
||||||
placeholder={t('Dataset name')}
|
|
||||||
disabled={!this.state.isEditMode}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
|
onDbChange={database =>
|
||||||
|
this.state.isEditMode &&
|
||||||
|
this.onDatasourcePropChange('database', database)
|
||||||
|
}
|
||||||
|
formMode={false}
|
||||||
|
handleError={this.props.addDangerToast}
|
||||||
|
readOnly={!this.state.isEditMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
</Col>
|
/>
|
||||||
|
<Field
|
||||||
|
fieldKey="table_name"
|
||||||
|
label={t('Dataset name')}
|
||||||
|
control={
|
||||||
|
<TextControl
|
||||||
|
controlId="table_name"
|
||||||
|
onChange={table => {
|
||||||
|
this.onDatasourcePropChange('table_name', table);
|
||||||
|
}}
|
||||||
|
placeholder={t('Dataset name')}
|
||||||
|
disabled={!this.state.isEditMode}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Field
|
<Field
|
||||||
fieldKey="sql"
|
fieldKey="sql"
|
||||||
label={t('SQL')}
|
label={t('SQL')}
|
||||||
@@ -859,39 +853,33 @@ class DatasourceEditor extends React.PureComponent {
|
|||||||
fieldKey="tableSelector"
|
fieldKey="tableSelector"
|
||||||
label={t('Physical')}
|
label={t('Physical')}
|
||||||
control={
|
control={
|
||||||
<div css={{ marginTop: 8 }}>
|
<TableSelector
|
||||||
<TableSelector
|
clearable={false}
|
||||||
clearable={false}
|
dbId={datasource.database.id}
|
||||||
database={datasource.database}
|
handleError={this.props.addDangerToast}
|
||||||
dbId={datasource.database.id}
|
schema={datasource.schema}
|
||||||
handleError={this.props.addDangerToast}
|
sqlLabMode={false}
|
||||||
schema={datasource.schema}
|
tableName={datasource.table_name}
|
||||||
sqlLabMode={false}
|
onSchemaChange={
|
||||||
tableName={datasource.table_name}
|
this.state.isEditMode
|
||||||
onSchemaChange={
|
? schema =>
|
||||||
this.state.isEditMode
|
this.onDatasourcePropChange('schema', schema)
|
||||||
? schema =>
|
: undefined
|
||||||
this.onDatasourcePropChange('schema', schema)
|
}
|
||||||
: undefined
|
onDbChange={
|
||||||
}
|
this.state.isEditMode
|
||||||
onDbChange={
|
? database =>
|
||||||
this.state.isEditMode
|
this.onDatasourcePropChange('database', database)
|
||||||
? database =>
|
: undefined
|
||||||
this.onDatasourcePropChange(
|
}
|
||||||
'database',
|
onTableChange={
|
||||||
database,
|
this.state.isEditMode
|
||||||
)
|
? table =>
|
||||||
: undefined
|
this.onDatasourcePropChange('table_name', table)
|
||||||
}
|
: undefined
|
||||||
onTableChange={
|
}
|
||||||
this.state.isEditMode
|
readOnly={!this.state.isEditMode}
|
||||||
? table =>
|
/>
|
||||||
this.onDatasourcePropChange('table_name', table)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
readOnly={!this.state.isEditMode}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
description={t(
|
description={t(
|
||||||
'The pointer to a physical table (or view). Keep in mind that the chart is ' +
|
'The pointer to a physical table (or view). Keep in mind that the chart is ' +
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export const DataTablesPane = ({
|
|||||||
}, [queryFormData.adhoc_filters, queryFormData.datasource]);
|
}, [queryFormData.adhoc_filters, queryFormData.datasource]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queriesResponse) {
|
if (queriesResponse && chartStatus === 'success') {
|
||||||
const { colnames } = queriesResponse[0];
|
const { colnames } = queriesResponse[0];
|
||||||
setColumnNames([...colnames]);
|
setColumnNames([...colnames]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const createProps = () => ({
|
|||||||
cache_timeout: null,
|
cache_timeout: null,
|
||||||
changed_on: '2021-03-19T16:30:56.750230',
|
changed_on: '2021-03-19T16:30:56.750230',
|
||||||
changed_on_humanized: '3 days ago',
|
changed_on_humanized: '3 days ago',
|
||||||
datasource: 'FCC 2018 Survey',
|
datasource: 'FCC Survey Results',
|
||||||
description: null,
|
description: null,
|
||||||
description_markeddown: '',
|
description_markeddown: '',
|
||||||
edit_url: '/chart/edit/318',
|
edit_url: '/chart/edit/318',
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const createProps = () => ({
|
|||||||
cache_timeout: null,
|
cache_timeout: null,
|
||||||
changed_on: '2021-03-19T16:30:56.750230',
|
changed_on: '2021-03-19T16:30:56.750230',
|
||||||
changed_on_humanized: '7 days ago',
|
changed_on_humanized: '7 days ago',
|
||||||
datasource: 'FCC 2018 Survey',
|
datasource: 'FCC Survey Results',
|
||||||
description: null,
|
description: null,
|
||||||
description_markeddown: '',
|
description_markeddown: '',
|
||||||
edit_url: '/chart/edit/318',
|
edit_url: '/chart/edit/318',
|
||||||
|
|||||||
@@ -227,7 +227,10 @@ class DatasourceControl extends React.PureComponent {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{extra?.warning_markdown && (
|
{extra?.warning_markdown && (
|
||||||
<WarningIconWithTooltip warningMarkdown={extra.warning_markdown} />
|
<WarningIconWithTooltip
|
||||||
|
warningMarkdown={extra.warning_markdown}
|
||||||
|
size={30}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Dropdown
|
<Dropdown
|
||||||
overlay={datasourceMenu}
|
overlay={datasourceMenu}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ test('Get ChartUri when allowDomainSharding:false', () => {
|
|||||||
duplicateQueryParameters: false,
|
duplicateQueryParameters: false,
|
||||||
escapeQuerySpace: true,
|
escapeQuerySpace: true,
|
||||||
fragment: null,
|
fragment: null,
|
||||||
hostname: undefined,
|
hostname: 'localhost',
|
||||||
password: null,
|
password: null,
|
||||||
path: '/path',
|
path: '/path',
|
||||||
port: '',
|
port: '',
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ const createParams = () => ({
|
|||||||
|
|
||||||
test('Get ExploreUrl with default params', () => {
|
test('Get ExploreUrl with default params', () => {
|
||||||
const params = createParams();
|
const params = createParams();
|
||||||
expect(getExploreUrl(params)).toBe('http:///superset/explore/');
|
expect(getExploreUrl(params)).toBe('http://localhost/superset/explore/');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Get ExploreUrl with endpointType:full', () => {
|
test('Get ExploreUrl with endpointType:full', () => {
|
||||||
const params = createParams();
|
const params = createParams();
|
||||||
expect(getExploreUrl({ ...params, endpointType: 'full' })).toBe(
|
expect(getExploreUrl({ ...params, endpointType: 'full' })).toBe(
|
||||||
'http:///superset/explore_json/',
|
'http://localhost/superset/explore_json/',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,5 +47,5 @@ test('Get ExploreUrl with endpointType:full and method:GET', () => {
|
|||||||
const params = createParams();
|
const params = createParams();
|
||||||
expect(
|
expect(
|
||||||
getExploreUrl({ ...params, endpointType: 'full', method: 'GET' }),
|
getExploreUrl({ ...params, endpointType: 'full', method: 'GET' }),
|
||||||
).toBe('http:///superset/explore_json/');
|
).toBe('http://localhost/superset/explore_json/');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
import {
|
import {
|
||||||
AppSection,
|
AppSection,
|
||||||
DataMask,
|
DataMask,
|
||||||
|
DataRecordValue,
|
||||||
ensureIsArray,
|
ensureIsArray,
|
||||||
ExtraFormData,
|
ExtraFormData,
|
||||||
GenericDataType,
|
GenericDataType,
|
||||||
@@ -36,7 +37,11 @@ import { useImmerReducer } from 'use-immer';
|
|||||||
import { FormItemProps } from 'antd/lib/form';
|
import { FormItemProps } from 'antd/lib/form';
|
||||||
import { PluginFilterSelectProps, SelectValue } from './types';
|
import { PluginFilterSelectProps, SelectValue } from './types';
|
||||||
import { StyledFormItem, FilterPluginStyle, StatusMessage } from '../common';
|
import { StyledFormItem, FilterPluginStyle, StatusMessage } from '../common';
|
||||||
import { getDataRecordFormatter, getSelectExtraFormData } from '../../utils';
|
import {
|
||||||
|
formatFilterValue,
|
||||||
|
getDataRecordFormatter,
|
||||||
|
getSelectExtraFormData,
|
||||||
|
} from '../../utils';
|
||||||
|
|
||||||
type DataMaskAction =
|
type DataMaskAction =
|
||||||
| { type: 'ownState'; ownState: JsonObject }
|
| { type: 'ownState'; ownState: JsonObject }
|
||||||
@@ -119,7 +124,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
|||||||
filterState: {
|
filterState: {
|
||||||
...filterState,
|
...filterState,
|
||||||
label: values?.length
|
label: values?.length
|
||||||
? `${(values || []).join(', ')}${suffix}`
|
? `${(values || []).map(formatFilterValue).join(', ')}${suffix}`
|
||||||
: undefined,
|
: undefined,
|
||||||
value:
|
value:
|
||||||
appSection === AppSection.FILTER_CONFIG_MODAL && defaultToFirstItem
|
appSection === AppSection.FILTER_CONFIG_MODAL && defaultToFirstItem
|
||||||
@@ -249,12 +254,12 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
const options: { label: string; value: string | number }[] = [];
|
const options: { label: string; value: DataRecordValue }[] = [];
|
||||||
data.forEach(row => {
|
data.forEach(row => {
|
||||||
const [value] = groupby.map(col => row[col]);
|
const [value] = groupby.map(col => row[col]);
|
||||||
options.push({
|
options.push({
|
||||||
label: labelFormatter(value, datatype),
|
label: labelFormatter(value, datatype),
|
||||||
value: typeof value === 'number' ? value : String(value),
|
value,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return options;
|
return options;
|
||||||
@@ -286,6 +291,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
|||||||
loading={isRefreshing}
|
loading={isRefreshing}
|
||||||
maxTagCount={5}
|
maxTagCount={5}
|
||||||
invertSelection={inverseSelection}
|
invertSelection={inverseSelection}
|
||||||
|
// @ts-ignore
|
||||||
options={options}
|
options={options}
|
||||||
/>
|
/>
|
||||||
</StyledFormItem>
|
</StyledFormItem>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { FALSE_STRING, NULL_STRING, TRUE_STRING } from 'src/utils/common';
|
|||||||
|
|
||||||
export const getSelectExtraFormData = (
|
export const getSelectExtraFormData = (
|
||||||
col: string,
|
col: string,
|
||||||
value?: null | (string | number)[],
|
value?: null | (string | number | boolean | null)[],
|
||||||
emptyFilter = false,
|
emptyFilter = false,
|
||||||
inverseSelection = false,
|
inverseSelection = false,
|
||||||
): ExtraFormData => {
|
): ExtraFormData => {
|
||||||
@@ -46,6 +46,7 @@ export const getSelectExtraFormData = (
|
|||||||
{
|
{
|
||||||
col,
|
col,
|
||||||
op: inverseSelection ? ('NOT IN' as const) : ('IN' as const),
|
op: inverseSelection ? ('NOT IN' as const) : ('IN' as const),
|
||||||
|
// @ts-ignore
|
||||||
val: value,
|
val: value,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -116,3 +117,18 @@ export function getDataRecordFormatter({
|
|||||||
return String(value);
|
return String(value);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatFilterValue(
|
||||||
|
value: string | number | boolean | null,
|
||||||
|
): string {
|
||||||
|
if (value === null) {
|
||||||
|
return NULL_STRING;
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
return value ? TRUE_STRING : FALSE_STRING;
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import Chart from 'src/types/Chart';
|
|||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
import { nativeFilterGate } from 'src/dashboard/components/nativeFilters/utils';
|
import { nativeFilterGate } from 'src/dashboard/components/nativeFilters/utils';
|
||||||
|
import setupPlugins from 'src/setup/setupPlugins';
|
||||||
import ChartCard from './ChartCard';
|
import ChartCard from './ChartCard';
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
@@ -73,6 +74,7 @@ const CONFIRM_OVERWRITE_MESSAGE = t(
|
|||||||
'sure you want to overwrite?',
|
'sure you want to overwrite?',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setupPlugins();
|
||||||
const registry = getChartMetadataRegistry();
|
const registry = getChartMetadataRegistry();
|
||||||
|
|
||||||
const createFetchDatasets = (handleError: (err: Response) => void) => async (
|
const createFetchDatasets = (handleError: (err: Response) => void) => async (
|
||||||
|
|||||||
@@ -243,13 +243,11 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
<CertifiedIcon
|
<CertifiedIcon
|
||||||
certifiedBy={parsedExtra.certification.certified_by}
|
certifiedBy={parsedExtra.certification.certified_by}
|
||||||
details={parsedExtra.certification.details}
|
details={parsedExtra.certification.details}
|
||||||
size="l"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{parsedExtra?.warning_markdown && (
|
{parsedExtra?.warning_markdown && (
|
||||||
<WarningIconWithTooltip
|
<WarningIconWithTooltip
|
||||||
warningMarkdown={parsedExtra.warning_markdown}
|
warningMarkdown={parsedExtra.warning_markdown}
|
||||||
size="l"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{titleLink}
|
{titleLink}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||||||
"url",
|
"url",
|
||||||
"extra",
|
"extra",
|
||||||
]
|
]
|
||||||
show_columns = show_select_columns + ["columns.type_generic", "database.backend"]
|
show_columns = show_select_columns + ["columns.type_generic"]
|
||||||
add_model_schema = DatasetPostSchema()
|
add_model_schema = DatasetPostSchema()
|
||||||
edit_model_schema = DatasetPutSchema()
|
edit_model_schema = DatasetPutSchema()
|
||||||
add_columns = ["database", "schema", "table_name", "owners"]
|
add_columns = ["database", "schema", "table_name", "owners"]
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from .helpers import get_example_data, get_table_connector_registry
|
|||||||
|
|
||||||
|
|
||||||
def load_bart_lines(only_metadata: bool = False, force: bool = False) -> None:
|
def load_bart_lines(only_metadata: bool = False, force: bool = False) -> None:
|
||||||
tbl_name = "bart_lines"
|
tbl_name = "San Franciso BART Lines"
|
||||||
database = get_example_database()
|
database = get_example_database()
|
||||||
table_exists = database.has_table_by_name(tbl_name)
|
table_exists = database.has_table_by_name(tbl_name)
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: FCC 2018 Survey
|
table_name: FCC Survey Results
|
||||||
main_dttm_col: null
|
main_dttm_col: null
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: channel_members
|
table_name: Slack Channels and Members
|
||||||
main_dttm_col: null
|
main_dttm_col: null
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: channels
|
table_name: Slack Channels
|
||||||
main_dttm_col: created
|
main_dttm_col: created
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: cleaned_sales_data
|
table_name: Vehicle Sales
|
||||||
main_dttm_col: OrderDate
|
main_dttm_col: OrderDate
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: covid_vaccines
|
table_name: COVID Vaccines
|
||||||
main_dttm_col: null
|
main_dttm_col: null
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: exported_stats
|
table_name: Slack Exported Metrics
|
||||||
main_dttm_col: Date
|
main_dttm_col: Date
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
|
|||||||
@@ -14,15 +14,15 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: members_channels_2
|
table_name: Slack Members and Channels
|
||||||
main_dttm_col: null
|
main_dttm_col: null
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
offset: 0
|
offset: 0
|
||||||
cache_timeout: null
|
cache_timeout: null
|
||||||
schema: null
|
schema: null
|
||||||
sql: SELECT c.name AS channel_name, u.name AS member_name FROM channel_members cm
|
sql: SELECT c.name AS channel_name, u.name AS member_name FROM "Slack Channels and Members" cm
|
||||||
JOIN channels c ON cm.channel_id = c.id JOIN users u ON cm.user_id = u.id
|
JOIN "Slack Channels" c ON cm.channel_id = c.id JOIN "Slack Users" u ON cm.user_id = u.id
|
||||||
params: null
|
params: null
|
||||||
template_params: null
|
template_params: null
|
||||||
filter_select_enabled: true
|
filter_select_enabled: true
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: messages
|
table_name: Slack Messages
|
||||||
main_dttm_col: bot_profile__updated
|
main_dttm_col: bot_profile__updated
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
|
|||||||
@@ -14,14 +14,14 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: messages_channels
|
table_name: Slack Messages and Channels
|
||||||
main_dttm_col: null
|
main_dttm_col: null
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
offset: 0
|
offset: 0
|
||||||
cache_timeout: null
|
cache_timeout: null
|
||||||
schema: null
|
schema: null
|
||||||
sql: SELECT m.ts, c.name, m.text FROM messages m JOIN channels c ON m.channel_id =
|
sql: SELECT m.ts, c.name, m.text FROM "Slack Messages" m JOIN "Slack Channels" c ON m.channel_id =
|
||||||
c.id
|
c.id
|
||||||
params: null
|
params: null
|
||||||
template_params: null
|
template_params: null
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: new_members_daily
|
table_name: Slack Daily Member Count
|
||||||
main_dttm_col: null
|
main_dttm_col: null
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
@@ -22,7 +22,7 @@ offset: 0
|
|||||||
cache_timeout: null
|
cache_timeout: null
|
||||||
schema: null
|
schema: null
|
||||||
sql: SELECT date, total_membership - lag(total_membership) OVER (ORDER BY date) AS
|
sql: SELECT date, total_membership - lag(total_membership) OVER (ORDER BY date) AS
|
||||||
new_members FROM exported_stats
|
new_members FROM "Slack Exported Metrics"
|
||||||
params: null
|
params: null
|
||||||
template_params: null
|
template_params: null
|
||||||
filter_select_enabled: true
|
filter_select_enabled: true
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: threads
|
table_name: Slack Threads
|
||||||
main_dttm_col: ts
|
main_dttm_col: ts
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: users
|
table_name: Slack Users
|
||||||
main_dttm_col: updated
|
main_dttm_col: updated
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: users_channels-uzooNNtSRO
|
table_name: Slack Channel Combinations and Users
|
||||||
main_dttm_col: null
|
main_dttm_col: null
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
@@ -23,8 +23,8 @@ cache_timeout: null
|
|||||||
schema: null
|
schema: null
|
||||||
sql: >
|
sql: >
|
||||||
SELECT uc1.name as channel_1, uc2.name as channel_2, count(*) AS cnt
|
SELECT uc1.name as channel_1, uc2.name as channel_2, count(*) AS cnt
|
||||||
FROM users_channels uc1
|
FROM "Slack Users and Channels" uc1
|
||||||
JOIN users_channels uc2 ON uc1.user_id = uc2.user_id
|
JOIN "Slack Users and Channels" uc2 ON uc1.user_id = uc2.user_id
|
||||||
GROUP BY uc1.name, uc2.name
|
GROUP BY uc1.name, uc2.name
|
||||||
HAVING uc1.name <> uc2.name
|
HAVING uc1.name <> uc2.name
|
||||||
params: null
|
params: null
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: users_channels
|
table_name: Slack Users and Channels
|
||||||
main_dttm_col: null
|
main_dttm_col: null
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
table_name: video_game_sales
|
table_name: Video Game Sales
|
||||||
main_dttm_col: null
|
main_dttm_col: null
|
||||||
description: null
|
description: null
|
||||||
default_endpoint: null
|
default_endpoint: null
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ def load_deck_dash() -> None:
|
|||||||
print("Loading deck.gl dashboard")
|
print("Loading deck.gl dashboard")
|
||||||
slices = []
|
slices = []
|
||||||
table = get_table_connector_registry()
|
table = get_table_connector_registry()
|
||||||
tbl = db.session.query(table).filter_by(table_name="long_lat").first()
|
tbl = db.session.query(table).filter_by(table_name="Sample Geodata").first()
|
||||||
slice_data = {
|
slice_data = {
|
||||||
"spatial": {"type": "latlong", "lonCol": "LON", "latCol": "LAT"},
|
"spatial": {"type": "latlong", "lonCol": "LON", "latCol": "LAT"},
|
||||||
"color_picker": COLOR_RED,
|
"color_picker": COLOR_RED,
|
||||||
@@ -324,7 +324,9 @@ def load_deck_dash() -> None:
|
|||||||
slices.append(slc)
|
slices.append(slc)
|
||||||
|
|
||||||
polygon_tbl = (
|
polygon_tbl = (
|
||||||
db.session.query(table).filter_by(table_name="sf_population_polygons").first()
|
db.session.query(table)
|
||||||
|
.filter_by(table_name="San Francisco Population Polygons")
|
||||||
|
.first()
|
||||||
)
|
)
|
||||||
slice_data = {
|
slice_data = {
|
||||||
"datasource": "11__table",
|
"datasource": "11__table",
|
||||||
@@ -457,7 +459,7 @@ def load_deck_dash() -> None:
|
|||||||
viz_type="deck_arc",
|
viz_type="deck_arc",
|
||||||
datasource_type="table",
|
datasource_type="table",
|
||||||
datasource_id=db.session.query(table)
|
datasource_id=db.session.query(table)
|
||||||
.filter_by(table_name="flights")
|
.filter_by(table_name="Flights")
|
||||||
.first()
|
.first()
|
||||||
.id,
|
.id,
|
||||||
params=get_slice_json(slice_data),
|
params=get_slice_json(slice_data),
|
||||||
@@ -509,7 +511,7 @@ def load_deck_dash() -> None:
|
|||||||
viz_type="deck_path",
|
viz_type="deck_path",
|
||||||
datasource_type="table",
|
datasource_type="table",
|
||||||
datasource_id=db.session.query(table)
|
datasource_id=db.session.query(table)
|
||||||
.filter_by(table_name="bart_lines")
|
.filter_by(table_name="San Franciso BART Lines")
|
||||||
.first()
|
.first()
|
||||||
.id,
|
.id,
|
||||||
params=get_slice_json(slice_data),
|
params=get_slice_json(slice_data),
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ def load_energy(
|
|||||||
method="multi",
|
method="multi",
|
||||||
)
|
)
|
||||||
|
|
||||||
print("Creating table [wb_health_population] reference")
|
print("Creating table [World Bank Health Data] reference")
|
||||||
table = get_table_connector_registry()
|
table = get_table_connector_registry()
|
||||||
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
|
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
|
||||||
if not tbl:
|
if not tbl:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from .helpers import get_example_data, get_table_connector_registry
|
|||||||
|
|
||||||
def load_flights(only_metadata: bool = False, force: bool = False) -> None:
|
def load_flights(only_metadata: bool = False, force: bool = False) -> None:
|
||||||
"""Loading random time series data from a zip file in the repo"""
|
"""Loading random time series data from a zip file in the repo"""
|
||||||
tbl_name = "flights"
|
tbl_name = "Flights"
|
||||||
database = utils.get_example_database()
|
database = utils.get_example_database()
|
||||||
table_exists = database.has_table_by_name(tbl_name)
|
table_exists = database.has_table_by_name(tbl_name)
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from .helpers import (
|
|||||||
|
|
||||||
def load_long_lat_data(only_metadata: bool = False, force: bool = False) -> None:
|
def load_long_lat_data(only_metadata: bool = False, force: bool = False) -> None:
|
||||||
"""Loading lat/long data from a csv file in the repo"""
|
"""Loading lat/long data from a csv file in the repo"""
|
||||||
tbl_name = "long_lat"
|
tbl_name = "Sample Geodata"
|
||||||
database = utils.get_example_database()
|
database = utils.get_example_database()
|
||||||
table_exists = database.has_table_by_name(tbl_name)
|
table_exists = database.has_table_by_name(tbl_name)
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from .helpers import get_example_data, get_table_connector_registry
|
|||||||
def load_sf_population_polygons(
|
def load_sf_population_polygons(
|
||||||
only_metadata: bool = False, force: bool = False
|
only_metadata: bool = False, force: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
tbl_name = "sf_population_polygons"
|
tbl_name = "San Francisco Population Polygons"
|
||||||
database = utils.get_example_database()
|
database = utils.get_example_database()
|
||||||
table_exists = database.has_table_by_name(tbl_name)
|
table_exists = database.has_table_by_name(tbl_name)
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def load_world_bank_health_n_pop( # pylint: disable=too-many-locals
|
|||||||
only_metadata: bool = False, force: bool = False, sample: bool = False,
|
only_metadata: bool = False, force: bool = False, sample: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Loads the world bank health dataset, slices and a dashboard"""
|
"""Loads the world bank health dataset, slices and a dashboard"""
|
||||||
tbl_name = "wb_health_population"
|
tbl_name = "World Bank Health Data"
|
||||||
database = utils.get_example_database()
|
database = utils.get_example_database()
|
||||||
table_exists = database.has_table_by_name(tbl_name)
|
table_exists = database.has_table_by_name(tbl_name)
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ def load_world_bank_health_n_pop( # pylint: disable=too-many-locals
|
|||||||
index=False,
|
index=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
print("Creating table [wb_health_population] reference")
|
print("Creating table [World Bank Health Data] reference")
|
||||||
table = get_table_connector_registry()
|
table = get_table_connector_registry()
|
||||||
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
|
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
|
||||||
if not tbl:
|
if not tbl:
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ def cast_to_num(value: Optional[Union[float, int, str]]) -> Optional[Union[float
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def cast_to_boolean(value: Any) -> bool:
|
def cast_to_boolean(value: Any) -> Optional[bool]:
|
||||||
"""Casts a value to an int/float
|
"""Casts a value to an int/float
|
||||||
|
|
||||||
>>> cast_to_boolean(1)
|
>>> cast_to_boolean(1)
|
||||||
@@ -473,12 +473,13 @@ def cast_to_boolean(value: Any) -> bool:
|
|||||||
>>> cast_to_boolean('False')
|
>>> cast_to_boolean('False')
|
||||||
False
|
False
|
||||||
>>> cast_to_boolean(None)
|
>>> cast_to_boolean(None)
|
||||||
False
|
|
||||||
|
|
||||||
:param value: value to be converted to boolean representation
|
:param value: value to be converted to boolean representation
|
||||||
:returns: value cast to `bool`. when value is 'true' or value that are not 0
|
:returns: value cast to `bool`. when value is 'true' or value that are not 0
|
||||||
converte into True
|
converted into True. Return `None` if value is `None`
|
||||||
"""
|
"""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
if isinstance(value, (int, float)):
|
if isinstance(value, (int, float)):
|
||||||
return value != 0
|
return value != 0
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
|
|||||||
@@ -1061,14 +1061,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||||||
@event_logger.log_this
|
@event_logger.log_this
|
||||||
@expose("/tables/<int:db_id>/<schema>/<substr>/")
|
@expose("/tables/<int:db_id>/<schema>/<substr>/")
|
||||||
@expose("/tables/<int:db_id>/<schema>/<substr>/<force_refresh>/")
|
@expose("/tables/<int:db_id>/<schema>/<substr>/<force_refresh>/")
|
||||||
@expose("/tables/<int:db_id>/<schema>/<substr>/<force_refresh>/<exact_match>")
|
def tables( # pylint: disable=too-many-locals,no-self-use
|
||||||
def tables( # pylint: disable=too-many-locals,no-self-use,too-many-arguments
|
self, db_id: int, schema: str, substr: str, force_refresh: str = "false"
|
||||||
self,
|
|
||||||
db_id: int,
|
|
||||||
schema: str,
|
|
||||||
substr: str,
|
|
||||||
force_refresh: str = "false",
|
|
||||||
exact_match: str = "false",
|
|
||||||
) -> FlaskResponse:
|
) -> FlaskResponse:
|
||||||
"""Endpoint to fetch the list of tables for given database"""
|
"""Endpoint to fetch the list of tables for given database"""
|
||||||
# Guarantees database filtering by security access
|
# Guarantees database filtering by security access
|
||||||
@@ -1081,7 +1075,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||||||
return json_error_response("Not found", 404)
|
return json_error_response("Not found", 404)
|
||||||
|
|
||||||
force_refresh_parsed = force_refresh.lower() == "true"
|
force_refresh_parsed = force_refresh.lower() == "true"
|
||||||
exact_match_parsed = exact_match.lower() == "true"
|
|
||||||
schema_parsed = utils.parse_js_uri_path_item(schema, eval_undefined=True)
|
schema_parsed = utils.parse_js_uri_path_item(schema, eval_undefined=True)
|
||||||
substr_parsed = utils.parse_js_uri_path_item(substr, eval_undefined=True)
|
substr_parsed = utils.parse_js_uri_path_item(substr, eval_undefined=True)
|
||||||
|
|
||||||
@@ -1123,15 +1116,9 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||||||
ds_name.table if schema_parsed else f"{ds_name.schema}.{ds_name.table}"
|
ds_name.table if schema_parsed else f"{ds_name.schema}.{ds_name.table}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_match(src: str, target: utils.DatasourceName) -> bool:
|
|
||||||
target_label = get_datasource_label(target)
|
|
||||||
if exact_match_parsed:
|
|
||||||
return src == target_label
|
|
||||||
return src in target_label
|
|
||||||
|
|
||||||
if substr_parsed:
|
if substr_parsed:
|
||||||
tables = [tn for tn in tables if is_match(substr_parsed, tn)]
|
tables = [tn for tn in tables if substr_parsed in get_datasource_label(tn)]
|
||||||
views = [vn for vn in views if is_match(substr_parsed, vn)]
|
views = [vn for vn in views if substr_parsed in get_datasource_label(vn)]
|
||||||
|
|
||||||
if not schema_parsed and database.default_schemas:
|
if not schema_parsed and database.default_schemas:
|
||||||
user_schemas = (
|
user_schemas = (
|
||||||
@@ -3039,12 +3026,12 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
databases: Dict[int, Any] = {
|
databases: Dict[int, Any] = {}
|
||||||
database.id: {
|
for database in DatabaseDAO.find_all():
|
||||||
|
databases[database.id] = {
|
||||||
k: v for k, v in database.to_json().items() if k in DATABASE_KEYS
|
k: v for k, v in database.to_json().items() if k in DATABASE_KEYS
|
||||||
}
|
}
|
||||||
for database in DatabaseDAO.find_all()
|
databases[database.id]["backend"] = database.backend
|
||||||
}
|
|
||||||
queries: Dict[str, Any] = {}
|
queries: Dict[str, Any] = {}
|
||||||
|
|
||||||
# These are unnecessary if sqllab backend persistence is disabled
|
# These are unnecessary if sqllab backend persistence is disabled
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import json
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from flask import request
|
from flask import g, request
|
||||||
from flask_appbuilder import expose
|
from flask_appbuilder import expose
|
||||||
from flask_appbuilder.api import rison
|
from flask_appbuilder.api import rison
|
||||||
from flask_appbuilder.security.decorators import has_access_api
|
from flask_appbuilder.security.decorators import has_access_api
|
||||||
@@ -28,6 +28,7 @@ from sqlalchemy.exc import NoSuchTableError
|
|||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
|
||||||
from superset import app, db, event_logger
|
from superset import app, db, event_logger
|
||||||
|
from superset.commands.utils import populate_owners
|
||||||
from superset.connectors.connector_registry import ConnectorRegistry
|
from superset.connectors.connector_registry import ConnectorRegistry
|
||||||
from superset.connectors.sqla.utils import get_physical_table_metadata
|
from superset.connectors.sqla.utils import get_physical_table_metadata
|
||||||
from superset.datasets.commands.exceptions import (
|
from superset.datasets.commands.exceptions import (
|
||||||
@@ -35,6 +36,7 @@ from superset.datasets.commands.exceptions import (
|
|||||||
DatasetNotFoundError,
|
DatasetNotFoundError,
|
||||||
)
|
)
|
||||||
from superset.exceptions import SupersetException, SupersetSecurityException
|
from superset.exceptions import SupersetException, SupersetSecurityException
|
||||||
|
from superset.extensions import security_manager
|
||||||
from superset.models.core import Database
|
from superset.models.core import Database
|
||||||
from superset.typing import FlaskResponse
|
from superset.typing import FlaskResponse
|
||||||
from superset.views.base import (
|
from superset.views.base import (
|
||||||
@@ -79,16 +81,28 @@ class Datasource(BaseSupersetView):
|
|||||||
if "owners" in datasource_dict and orm_datasource.owner_class is not None:
|
if "owners" in datasource_dict and orm_datasource.owner_class is not None:
|
||||||
# Check ownership
|
# Check ownership
|
||||||
if app.config["OLD_API_CHECK_DATASET_OWNERSHIP"]:
|
if app.config["OLD_API_CHECK_DATASET_OWNERSHIP"]:
|
||||||
|
# mimic the behavior of the new dataset command that
|
||||||
|
# checks ownership and ensures that non-admins aren't locked out
|
||||||
|
# of the object
|
||||||
try:
|
try:
|
||||||
check_ownership(orm_datasource)
|
check_ownership(orm_datasource)
|
||||||
except SupersetSecurityException as ex:
|
except SupersetSecurityException as ex:
|
||||||
raise DatasetForbiddenError() from ex
|
raise DatasetForbiddenError() from ex
|
||||||
|
user = security_manager.get_user_by_id(g.user.id)
|
||||||
datasource_dict["owners"] = (
|
datasource_dict["owners"] = populate_owners(
|
||||||
db.session.query(orm_datasource.owner_class)
|
user, datasource_dict["owners"], default_to_user=False
|
||||||
.filter(orm_datasource.owner_class.id.in_(datasource_dict["owners"]))
|
)
|
||||||
.all()
|
else:
|
||||||
)
|
# legacy behavior
|
||||||
|
datasource_dict["owners"] = (
|
||||||
|
db.session.query(orm_datasource.owner_class)
|
||||||
|
.filter(
|
||||||
|
orm_datasource.owner_class.id.in_(
|
||||||
|
datasource_dict["owners"] or []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
duplicates = [
|
duplicates = [
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -222,7 +222,6 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
Dataset API: Test get dataset item
|
Dataset API: Test get dataset item
|
||||||
"""
|
"""
|
||||||
table = self.get_energy_usage_dataset()
|
table = self.get_energy_usage_dataset()
|
||||||
main_db = get_main_database()
|
|
||||||
self.login(username="admin")
|
self.login(username="admin")
|
||||||
uri = f"api/v1/dataset/{table.id}"
|
uri = f"api/v1/dataset/{table.id}"
|
||||||
rv = self.get_assert_metric(uri, "get")
|
rv = self.get_assert_metric(uri, "get")
|
||||||
@@ -230,11 +229,7 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
response = json.loads(rv.data.decode("utf-8"))
|
response = json.loads(rv.data.decode("utf-8"))
|
||||||
expected_result = {
|
expected_result = {
|
||||||
"cache_timeout": None,
|
"cache_timeout": None,
|
||||||
"database": {
|
"database": {"database_name": "examples", "id": 1},
|
||||||
"backend": main_db.backend,
|
|
||||||
"database_name": "examples",
|
|
||||||
"id": 1,
|
|
||||||
},
|
|
||||||
"default_endpoint": None,
|
"default_endpoint": None,
|
||||||
"description": "Energy consumption",
|
"description": "Energy consumption",
|
||||||
"extra": None,
|
"extra": None,
|
||||||
@@ -249,10 +244,9 @@ class TestDatasetApi(SupersetTestCase):
|
|||||||
"table_name": "energy_usage",
|
"table_name": "energy_usage",
|
||||||
"template_params": None,
|
"template_params": None,
|
||||||
}
|
}
|
||||||
if response["result"]["database"]["backend"] not in ("presto", "hive"):
|
assert {
|
||||||
assert {
|
k: v for k, v in response["result"].items() if k in expected_result
|
||||||
k: v for k, v in response["result"].items() if k in expected_result
|
} == expected_result
|
||||||
} == expected_result
|
|
||||||
assert len(response["result"]["columns"]) == 3
|
assert len(response["result"]["columns"]) == 3
|
||||||
assert len(response["result"]["metrics"]) == 2
|
assert len(response["result"]["metrics"]) == 2
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user