diff --git a/superset-frontend/packages/superset-ui-switchboard/src/switchboard.ts b/superset-frontend/packages/superset-ui-switchboard/src/switchboard.ts index b65ca13586377..f12c9b6482c3d 100644 --- a/superset-frontend/packages/superset-ui-switchboard/src/switchboard.ts +++ b/superset-frontend/packages/superset-ui-switchboard/src/switchboard.ts @@ -23,6 +23,63 @@ export type Params = { debug?: boolean; }; +// Each message we send on the channel specifies an action we want the other side to cooperate with. +enum Actions { + GET = 'get', + REPLY = 'reply', + EMIT = 'emit', + ERROR = 'error', +} + +type Method = (args: A) => R | Promise; + +// helper types/functions for making sure wires don't get crossed + +interface Message { + switchboardAction: Actions; +} + +interface GetMessage extends Message { + switchboardAction: Actions.GET; + method: string; + messageId: string; + args: T; +} + +function isGet(message: Message): message is GetMessage { + return message.switchboardAction === Actions.GET; +} + +interface ReplyMessage extends Message { + switchboardAction: Actions.REPLY; + messageId: string; + result: T; +} + +function isReply(message: Message): message is ReplyMessage { + return message.switchboardAction === Actions.REPLY; +} + +interface EmitMessage extends Message { + switchboardAction: Actions.EMIT; + method: string; + args: T; +} + +function isEmit(message: Message): message is EmitMessage { + return message.switchboardAction === Actions.EMIT; +} + +interface ErrorMessage extends Message { + switchboardAction: Actions.ERROR; + messageId: string; + error: string; +} + +function isError(message: Message): message is ErrorMessage { + return message.switchboardAction === Actions.ERROR; +} + /** * A utility for communications between an iframe and its parent, used by the Superset embedded SDK. * This builds useful patterns on top of the basic functionality offered by MessageChannel. @@ -185,60 +242,3 @@ export class Switchboard { return `m_${this.name}_${this.incrementor++}`; } } - -type Method = (args: A) => R | Promise; - -// Each message we send on the channel specifies an action we want the other side to cooperate with. -enum Actions { - GET = 'get', - REPLY = 'reply', - EMIT = 'emit', - ERROR = 'error', -} - -// helper types/functions for making sure wires don't get crossed - -interface Message { - switchboardAction: Actions; -} - -interface GetMessage extends Message { - switchboardAction: Actions.GET; - method: string; - messageId: string; - args: T; -} - -function isGet(message: Message): message is GetMessage { - return message.switchboardAction === Actions.GET; -} - -interface ReplyMessage extends Message { - switchboardAction: Actions.REPLY; - messageId: string; - result: T; -} - -function isReply(message: Message): message is ReplyMessage { - return message.switchboardAction === Actions.REPLY; -} - -interface EmitMessage extends Message { - switchboardAction: Actions.EMIT; - method: string; - args: T; -} - -function isEmit(message: Message): message is EmitMessage { - return message.switchboardAction === Actions.EMIT; -} - -interface ErrorMessage extends Message { - switchboardAction: Actions.ERROR; - messageId: string; - error: string; -} - -function isError(message: Message): message is ErrorMessage { - return message.switchboardAction === Actions.ERROR; -} diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/components/AdvancedFrame.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/components/AdvancedFrame.tsx index a727a3f261aa2..f865b703a78ab 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/components/AdvancedFrame.tsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/components/AdvancedFrame.tsx @@ -25,12 +25,6 @@ import { FrameComponentProps } from 'src/explore/components/controls/DateFilterC import DateFunctionTooltip from './DateFunctionTooltip'; export function AdvancedFrame(props: FrameComponentProps) { - const advancedRange = getAdvancedRange(props.value || ''); - const [since, until] = advancedRange.split(SEPARATOR); - if (advancedRange !== props.value) { - props.onChange(getAdvancedRange(props.value || '')); - } - function getAdvancedRange(value: string): string { if (value.includes(SEPARATOR)) { return value; @@ -44,6 +38,12 @@ export function AdvancedFrame(props: FrameComponentProps) { return SEPARATOR; } + const advancedRange = getAdvancedRange(props.value || ''); + const [since, until] = advancedRange.split(SEPARATOR); + if (advancedRange !== props.value) { + props.onChange(getAdvancedRange(props.value || '')); + } + function onChange(control: 'since' | 'until', value: string) { if (control === 'since') { props.onChange(`${value}${SEPARATOR}${until}`); diff --git a/superset-frontend/src/middleware/asyncEvent.ts b/superset-frontend/src/middleware/asyncEvent.ts index 638f324a4da25..9d252f99cc4d4 100644 --- a/superset-frontend/src/middleware/asyncEvent.ts +++ b/superset-frontend/src/middleware/asyncEvent.ts @@ -67,46 +67,6 @@ let listenersByJobId: Record; let retriesByJobId: Record; let lastReceivedEventId: string | null | undefined; -export const init = (appConfig?: AppConfig) => { - if (!isFeatureEnabled(FeatureFlag.GLOBAL_ASYNC_QUERIES)) return; - if (pollingTimeoutId) clearTimeout(pollingTimeoutId); - - listenersByJobId = {}; - retriesByJobId = {}; - lastReceivedEventId = null; - - if (appConfig) { - config = appConfig; - } else { - // load bootstrap data from DOM - const appContainer = document.getElementById('app'); - if (appContainer) { - const bootstrapData = JSON.parse( - appContainer?.getAttribute('data-bootstrap') || '{}', - ); - config = bootstrapData?.common?.conf; - } else { - config = {}; - logging.warn('asyncEvent: app config data not found'); - } - } - transport = config.GLOBAL_ASYNC_QUERIES_TRANSPORT || TRANSPORT_POLLING; - pollingDelayMs = config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY || 500; - - try { - lastReceivedEventId = localStorage.getItem(LOCALSTORAGE_KEY); - } catch (err) { - logging.warn('Failed to fetch last event Id from localStorage'); - } - - if (transport === TRANSPORT_POLLING) { - loadEventsFromApi(); - } - if (transport === TRANSPORT_WS) { - wsConnect(); - } -}; - const addListener = (id: string, fn: any) => { listenersByJobId[id] = fn; }; @@ -116,6 +76,24 @@ const removeListener = (id: string) => { delete listenersByJobId[id]; }; +const fetchCachedData = async ( + asyncEvent: AsyncEvent, +): Promise => { + let status = 'success'; + let data; + try { + const { json } = await SupersetClient.get({ + endpoint: String(asyncEvent.result_url), + }); + data = 'result' in json ? json.result : json; + } catch (response) { + status = 'error'; + data = await getClientErrorObject(response); + } + + return { status, data }; +}; + export const waitForAsyncData = async (asyncResponse: AsyncEvent) => new Promise((resolve, reject) => { const jobId = asyncResponse.job_id; @@ -153,24 +131,6 @@ const fetchEvents = makeApi< endpoint: POLLING_URL, }); -const fetchCachedData = async ( - asyncEvent: AsyncEvent, -): Promise => { - let status = 'success'; - let data; - try { - const { json } = await SupersetClient.get({ - endpoint: String(asyncEvent.result_url), - }); - data = 'result' in json ? json.result : json; - } catch (response) { - status = 'error'; - data = await getClientErrorObject(response); - } - - return { status, data }; -}; - const setLastId = (asyncEvent: AsyncEvent) => { lastReceivedEventId = asyncEvent.id; try { @@ -180,22 +140,6 @@ const setLastId = (asyncEvent: AsyncEvent) => { } }; -const loadEventsFromApi = async () => { - const eventArgs = lastReceivedEventId ? { last_id: lastReceivedEventId } : {}; - if (Object.keys(listenersByJobId).length) { - try { - const { result: events } = await fetchEvents(eventArgs); - if (events && events.length) await processEvents(events); - } catch (err) { - logging.warn(err); - } - } - - if (transport === TRANSPORT_POLLING) { - pollingTimeoutId = window.setTimeout(loadEventsFromApi, pollingDelayMs); - } -}; - export const processEvents = async (events: AsyncEvent[]) => { events.forEach((asyncEvent: AsyncEvent) => { const jobId = asyncEvent.job_id; @@ -222,6 +166,22 @@ export const processEvents = async (events: AsyncEvent[]) => { }); }; +const loadEventsFromApi = async () => { + const eventArgs = lastReceivedEventId ? { last_id: lastReceivedEventId } : {}; + if (Object.keys(listenersByJobId).length) { + try { + const { result: events } = await fetchEvents(eventArgs); + if (events && events.length) await processEvents(events); + } catch (err) { + logging.warn(err); + } + } + + if (transport === TRANSPORT_POLLING) { + pollingTimeoutId = window.setTimeout(loadEventsFromApi, pollingDelayMs); + } +}; + const wsConnectMaxRetries = 6; const wsConnectErrorDelay = 2500; let wsConnectRetries = 0; @@ -267,4 +227,44 @@ const wsConnect = (): void => { }); }; +export const init = (appConfig?: AppConfig) => { + if (!isFeatureEnabled(FeatureFlag.GLOBAL_ASYNC_QUERIES)) return; + if (pollingTimeoutId) clearTimeout(pollingTimeoutId); + + listenersByJobId = {}; + retriesByJobId = {}; + lastReceivedEventId = null; + + if (appConfig) { + config = appConfig; + } else { + // load bootstrap data from DOM + const appContainer = document.getElementById('app'); + if (appContainer) { + const bootstrapData = JSON.parse( + appContainer?.getAttribute('data-bootstrap') || '{}', + ); + config = bootstrapData?.common?.conf; + } else { + config = {}; + logging.warn('asyncEvent: app config data not found'); + } + } + transport = config.GLOBAL_ASYNC_QUERIES_TRANSPORT || TRANSPORT_POLLING; + pollingDelayMs = config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY || 500; + + try { + lastReceivedEventId = localStorage.getItem(LOCALSTORAGE_KEY); + } catch (err) { + logging.warn('Failed to fetch last event Id from localStorage'); + } + + if (transport === TRANSPORT_POLLING) { + loadEventsFromApi(); + } + if (transport === TRANSPORT_WS) { + wsConnect(); + } +}; + init(); diff --git a/superset-frontend/src/utils/localStorageHelpers.ts b/superset-frontend/src/utils/localStorageHelpers.ts index 49044a162c6af..482f413372b69 100644 --- a/superset-frontend/src/utils/localStorageHelpers.ts +++ b/superset-frontend/src/utils/localStorageHelpers.ts @@ -66,20 +66,6 @@ export type LocalStorageValues = { explore__data_table_original_formatted_time_columns: Record; }; -export function getItem( - key: K, - defaultValue: LocalStorageValues[K], -): LocalStorageValues[K] { - return dangerouslyGetItemDoNotUse(key, defaultValue); -} - -export function setItem( - key: K, - value: LocalStorageValues[K], -): void { - dangerouslySetItemDoNotUse(key, value); -} - /* * This function should not be used directly, as it doesn't provide any type safety or any * guarantees that the globally namespaced localstorage key is correct. @@ -116,3 +102,17 @@ export function dangerouslySetItemDoNotUse(key: string, value: any): void { // Catch in case localStorage is unavailable } } + +export function getItem( + key: K, + defaultValue: LocalStorageValues[K], +): LocalStorageValues[K] { + return dangerouslyGetItemDoNotUse(key, defaultValue); +} + +export function setItem( + key: K, + value: LocalStorageValues[K], +): void { + dangerouslySetItemDoNotUse(key, value); +} diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx index a56a69b346c11..7d6373b935d35 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx @@ -230,6 +230,14 @@ const DatasetList: FunctionComponent = ({ ), ); + const handleBulkDatasetExport = (datasetsToExport: Dataset[]) => { + const ids = datasetsToExport.map(({ id }) => id); + handleResourceExport('dataset', ids, () => { + setPreparingExport(false); + }); + setPreparingExport(true); + }; + const columns = useMemo( () => [ { @@ -617,14 +625,6 @@ const DatasetList: FunctionComponent = ({ ); }; - const handleBulkDatasetExport = (datasetsToExport: Dataset[]) => { - const ids = datasetsToExport.map(({ id }) => id); - handleResourceExport('dataset', ids, () => { - setPreparingExport(false); - }); - setPreparingExport(true); - }; - return ( <> diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx index a035045318a21..41c25033df61c 100644 --- a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx @@ -106,21 +106,6 @@ function ChartTable({ const [preparingExport, setPreparingExport] = useState(false); const [loaded, setLoaded] = useState(false); - useEffect(() => { - if (loaded || chartFilter === 'Favorite') { - getData(chartFilter); - } - setLoaded(true); - }, [chartFilter]); - - const handleBulkChartExport = (chartsToExport: Chart[]) => { - const ids = chartsToExport.map(({ id }) => id); - handleResourceExport('chart', ids, () => { - setPreparingExport(false); - }); - setPreparingExport(true); - }; - const getFilters = (filterName: string) => { const filters = []; @@ -159,6 +144,21 @@ function ChartTable({ filters: getFilters(filter), }); + useEffect(() => { + if (loaded || chartFilter === 'Favorite') { + getData(chartFilter); + } + setLoaded(true); + }, [chartFilter]); + + const handleBulkChartExport = (chartsToExport: Chart[]) => { + const ids = chartsToExport.map(({ id }) => id); + handleResourceExport('chart', ids, () => { + setPreparingExport(false); + }); + setPreparingExport(true); + }; + const menuTabs = [ { name: 'Favorite', diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx index 4078e23c2dec8..a5078465fc6f8 100644 --- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx @@ -96,6 +96,43 @@ function DashboardTable({ const [preparingExport, setPreparingExport] = useState(false); const [loaded, setLoaded] = useState(false); + const getFilters = (filterName: string) => { + const filters = []; + if (filterName === 'Mine') { + filters.push({ + id: 'owners', + operator: 'rel_m_m', + value: `${user?.userId}`, + }); + } else if (filterName === 'Favorite') { + filters.push({ + id: 'id', + operator: 'dashboard_is_favorite', + value: true, + }); + } else if (filterName === 'Examples') { + filters.push({ + id: 'created_by', + operator: 'rel_o_m', + value: 0, + }); + } + return filters; + }; + + const getData = (filter: string) => + fetchData({ + pageIndex: 0, + pageSize: PAGE_SIZE, + sortBy: [ + { + id: 'changed_on_delta_humanized', + desc: true, + }, + ], + filters: getFilters(filter), + }); + useEffect(() => { if (loaded || dashboardFilter === 'Favorite') { getData(dashboardFilter); @@ -132,30 +169,6 @@ function DashboardTable({ ), ); - const getFilters = (filterName: string) => { - const filters = []; - if (filterName === 'Mine') { - filters.push({ - id: 'owners', - operator: 'rel_m_m', - value: `${user?.userId}`, - }); - } else if (filterName === 'Favorite') { - filters.push({ - id: 'id', - operator: 'dashboard_is_favorite', - value: true, - }); - } else if (filterName === 'Examples') { - filters.push({ - id: 'created_by', - operator: 'rel_o_m', - value: 0, - }); - } - return filters; - }; - const menuTabs = [ { name: 'Favorite', @@ -192,19 +205,6 @@ function DashboardTable({ }); } - const getData = (filter: string) => - fetchData({ - pageIndex: 0, - pageSize: PAGE_SIZE, - sortBy: [ - { - id: 'changed_on_delta_humanized', - desc: true, - }, - ], - filters: getFilters(filter), - }); - if (loading) return ; return ( <>