feat: Oauth2 in DatabaseSelector (#30082)

This commit is contained in:
Beto Dealmeida 2024-09-03 20:11:37 -04:00 committed by GitHub
parent 0415ed34ce
commit 09dfe2f2ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 50 additions and 11 deletions

View File

@ -24,10 +24,11 @@ import {
useRef, useRef,
useCallback, useCallback,
} from 'react'; } from 'react';
import { styled, SupersetClient, t } from '@superset-ui/core'; import { styled, SupersetClient, SupersetError, t } from '@superset-ui/core';
import type { LabeledValue as AntdLabeledValue } from 'antd/lib/select'; import type { LabeledValue as AntdLabeledValue } from 'antd/lib/select';
import rison from 'rison'; import rison from 'rison';
import { AsyncSelect, Select } from 'src/components'; import { AsyncSelect, Select } from 'src/components';
import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace';
import Label from 'src/components/Label'; import Label from 'src/components/Label';
import { FormLabel } from 'src/components/Form'; import { FormLabel } from 'src/components/Form';
import RefreshLabel from 'src/components/RefreshLabel'; import RefreshLabel from 'src/components/RefreshLabel';
@ -154,6 +155,7 @@ export default function DatabaseSelector({
}: DatabaseSelectorProps) { }: DatabaseSelectorProps) {
const showCatalogSelector = !!db?.allow_multi_catalog; const showCatalogSelector = !!db?.allow_multi_catalog;
const [currentDb, setCurrentDb] = useState<DatabaseValue | undefined>(); const [currentDb, setCurrentDb] = useState<DatabaseValue | undefined>();
const [errorPayload, setErrorPayload] = useState<SupersetError | null>();
const [currentCatalog, setCurrentCatalog] = useState< const [currentCatalog, setCurrentCatalog] = useState<
CatalogOption | null | undefined CatalogOption | null | undefined
>(catalog ? { label: catalog, value: catalog, title: catalog } : undefined); >(catalog ? { label: catalog, value: catalog, title: catalog } : undefined);
@ -267,6 +269,7 @@ export default function DatabaseSelector({
dbId: currentDb?.value, dbId: currentDb?.value,
catalog: currentCatalog?.value, catalog: currentCatalog?.value,
onSuccess: (schemas, isFetched) => { onSuccess: (schemas, isFetched) => {
setErrorPayload(null);
if (schemas.length === 1) { if (schemas.length === 1) {
changeSchema(schemas[0]); changeSchema(schemas[0]);
} else if ( } else if (
@ -279,7 +282,13 @@ export default function DatabaseSelector({
addSuccessToast('List refreshed'); addSuccessToast('List refreshed');
} }
}, },
onError: () => handleError(t('There was an error loading the schemas')), onError: error => {
if (error?.errors) {
setErrorPayload(error?.errors?.[0]);
} else {
handleError(t('There was an error loading the schemas'));
}
},
}); });
const schemaOptions = schemaData || EMPTY_SCHEMA_OPTIONS; const schemaOptions = schemaData || EMPTY_SCHEMA_OPTIONS;
@ -299,6 +308,7 @@ export default function DatabaseSelector({
} = useCatalogs({ } = useCatalogs({
dbId: showCatalogSelector ? currentDb?.value : undefined, dbId: showCatalogSelector ? currentDb?.value : undefined,
onSuccess: (catalogs, isFetched) => { onSuccess: (catalogs, isFetched) => {
setErrorPayload(null);
if (!showCatalogSelector) { if (!showCatalogSelector) {
changeCatalog(null); changeCatalog(null);
} else if (catalogs.length === 1) { } else if (catalogs.length === 1) {
@ -315,9 +325,13 @@ export default function DatabaseSelector({
addSuccessToast('List refreshed'); addSuccessToast('List refreshed');
} }
}, },
onError: () => { onError: error => {
if (showCatalogSelector) { if (showCatalogSelector) {
handleError(t('There was an error loading the catalogs')); if (error?.errors) {
setErrorPayload(error?.errors?.[0]);
} else {
handleError(t('There was an error loading the catalogs'));
}
} }
}, },
}); });
@ -423,9 +437,16 @@ export default function DatabaseSelector({
); );
} }
function renderError() {
return errorPayload ? (
<ErrorMessageWithStackTrace error={errorPayload} source="crud" />
) : null;
}
return ( return (
<DatabaseSelectorWrapper data-test="DatabaseSelector"> <DatabaseSelectorWrapper data-test="DatabaseSelector">
{renderDatabaseSelect()} {renderDatabaseSelect()}
{renderError()}
{showCatalogSelector && renderCatalogSelect()} {showCatalogSelector && renderCatalogSelect()}
{renderSchemaSelect()} {renderSchemaSelect()}
</DatabaseSelectorWrapper> </DatabaseSelectorWrapper>

View File

@ -162,7 +162,7 @@ function OAuth2RedirectMessage({
> >
provide authorization provide authorization
</a>{' '} </a>{' '}
in order to run this query. in order to run this operation.
</> </>
); );

View File

@ -22,16 +22,19 @@ import { FieldPropTypes } from '../../types';
const FIELD_TEXT_MAP = { const FIELD_TEXT_MAP = {
account: { account: {
label: 'Account',
helpText: t( helpText: t(
'Copy the identifier of the account you are trying to connect to.', 'Copy the identifier of the account you are trying to connect to.',
), ),
placeholder: t('e.g. xy12345.us-east-2.aws'), placeholder: t('e.g. xy12345.us-east-2.aws'),
}, },
warehouse: { warehouse: {
label: 'Warehouse',
placeholder: t('e.g. compute_wh'), placeholder: t('e.g. compute_wh'),
className: 'form-group-w-50', className: 'form-group-w-50',
}, },
role: { role: {
label: 'Role',
placeholder: t('e.g. AccountAdmin'), placeholder: t('e.g. AccountAdmin'),
className: 'form-group-w-50', className: 'form-group-w-50',
}, },
@ -54,7 +57,7 @@ export const validatedInputField = ({
errorMessage={validationErrors?.[field]} errorMessage={validationErrors?.[field]}
placeholder={FIELD_TEXT_MAP[field].placeholder} placeholder={FIELD_TEXT_MAP[field].placeholder}
helpText={FIELD_TEXT_MAP[field].helpText} helpText={FIELD_TEXT_MAP[field].helpText}
label={field} label={FIELD_TEXT_MAP[field].label || field}
onChange={changeMethods.onParametersChange} onChange={changeMethods.onParametersChange}
className={FIELD_TEXT_MAP[field].className || field} className={FIELD_TEXT_MAP[field].className || field}
/> />

View File

@ -17,6 +17,7 @@
* under the License. * under the License.
*/ */
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { ClientErrorObject } from '@superset-ui/core';
import useEffectEvent from 'src/hooks/useEffectEvent'; import useEffectEvent from 'src/hooks/useEffectEvent';
import { api, JsonResponse } from './queryApi'; import { api, JsonResponse } from './queryApi';
@ -30,7 +31,7 @@ export type FetchCatalogsQueryParams = {
dbId?: string | number; dbId?: string | number;
forceRefresh: boolean; forceRefresh: boolean;
onSuccess?: (data: CatalogOption[], isRefetched: boolean) => void; onSuccess?: (data: CatalogOption[], isRefetched: boolean) => void;
onError?: () => void; onError?: (error: ClientErrorObject) => void;
}; };
type Params = Omit<FetchCatalogsQueryParams, 'forceRefresh'>; type Params = Omit<FetchCatalogsQueryParams, 'forceRefresh'>;
@ -77,6 +78,12 @@ export function useCatalogs(options: Params) {
}, },
); );
useEffect(() => {
if (result.isError) {
onError?.(result.error as ClientErrorObject);
}
}, [result.isError, result.error, onError]);
const fetchData = useEffectEvent( const fetchData = useEffectEvent(
(dbId: FetchCatalogsQueryParams['dbId'], forceRefresh = false) => { (dbId: FetchCatalogsQueryParams['dbId'], forceRefresh = false) => {
if (dbId && (!result.currentData || forceRefresh)) { if (dbId && (!result.currentData || forceRefresh)) {
@ -85,7 +92,7 @@ export function useCatalogs(options: Params) {
onSuccess?.(data || EMPTY_CATALOGS, forceRefresh); onSuccess?.(data || EMPTY_CATALOGS, forceRefresh);
} }
if (isError) { if (isError) {
onError?.(); onError?.(result.error as ClientErrorObject);
} }
}); });
} }

View File

@ -82,7 +82,7 @@ test('supersetClientQuery should return error when unsuccessful', async () => {
getBaseQueryApiMock(store), getBaseQueryApiMock(store),
{}, {},
); );
expect(result.error).toEqual({ error: expectedError }); expect(result.error).toEqual({ error: expectedError, errors: [] });
}); });
test('supersetClientQuery should return parsed response by parseMethod', async () => { test('supersetClientQuery should return parsed response by parseMethod', async () => {

View File

@ -64,6 +64,7 @@ export const supersetClientQuery: BaseQueryFn<
getClientErrorObject(response).then(errorObj => ({ getClientErrorObject(response).then(errorObj => ({
error: { error: {
error: errorObj?.message || errorObj?.error || response.statusText, error: errorObj?.message || errorObj?.error || response.statusText,
errors: errorObj?.errors || [], // used by <ErrorMessageWithStackTrace />
status: response.status, status: response.status,
}, },
})), })),

View File

@ -17,6 +17,7 @@
* under the License. * under the License.
*/ */
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { ClientErrorObject } from '@superset-ui/core';
import useEffectEvent from 'src/hooks/useEffectEvent'; import useEffectEvent from 'src/hooks/useEffectEvent';
import { api, JsonResponse } from './queryApi'; import { api, JsonResponse } from './queryApi';
@ -31,7 +32,7 @@ export type FetchSchemasQueryParams = {
catalog?: string; catalog?: string;
forceRefresh: boolean; forceRefresh: boolean;
onSuccess?: (data: SchemaOption[], isRefetched: boolean) => void; onSuccess?: (data: SchemaOption[], isRefetched: boolean) => void;
onError?: () => void; onError?: (error: ClientErrorObject) => void;
}; };
type Params = Omit<FetchSchemasQueryParams, 'forceRefresh'>; type Params = Omit<FetchSchemasQueryParams, 'forceRefresh'>;
@ -81,6 +82,12 @@ export function useSchemas(options: Params) {
}, },
); );
useEffect(() => {
if (result.isError) {
onError?.(result.error as ClientErrorObject);
}
}, [result.isError, result.error, onError]);
const fetchData = useEffectEvent( const fetchData = useEffectEvent(
( (
dbId: FetchSchemasQueryParams['dbId'], dbId: FetchSchemasQueryParams['dbId'],
@ -94,7 +101,7 @@ export function useSchemas(options: Params) {
onSuccess?.(data || EMPTY_SCHEMAS, forceRefresh); onSuccess?.(data || EMPTY_SCHEMAS, forceRefresh);
} }
if (isError) { if (isError) {
onError?.(); onError?.(result.error as ClientErrorObject);
} }
}, },
); );