From 8bf6d801553a13552831be0fe8ed41bf5171824c Mon Sep 17 00:00:00 2001 From: wanghong1314 <498213175@qq.com> Date: Tue, 10 Jan 2023 22:52:07 +0800 Subject: [PATCH 01/15] fix: Stop query in SQL Lab with impala engine (#22635) --- superset/config.py | 4 +- superset/db_engine_specs/hive.py | 10 +++- superset/db_engine_specs/impala.py | 90 ++++++++++++++++++++++++++++++ superset/views/core.py | 4 ++ 4 files changed, 105 insertions(+), 3 deletions(-) diff --git a/superset/config.py b/superset/config.py index 8a1b5220db92..a3fc3df00ef5 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1131,8 +1131,8 @@ def CSV_TO_HIVE_UPLOAD_DIRECTORY_FUNC( # pylint: disable=invalid-name TRACKING_URL_TRANSFORMER = lambda url: url -# Interval between consecutive polls when using Hive Engine -HIVE_POLL_INTERVAL = int(timedelta(seconds=5).total_seconds()) +# customize the polling time of each engine +DB_POLL_INTERVAL_SECONDS: Dict[str, int] = {} # Interval between consecutive polls when using Presto Engine # See here: https://github.com/dropbox/PyHive/blob/8eb0aeab8ca300f3024655419b93dad926c1a351/pyhive/presto.py#L93 # pylint: disable=line-too-long,useless-suppression diff --git a/superset/db_engine_specs/hive.py b/superset/db_engine_specs/hive.py index c69908976728..1d27978e9d23 100644 --- a/superset/db_engine_specs/hive.py +++ b/superset/db_engine_specs/hive.py @@ -375,7 +375,15 @@ def handle_cursor( # pylint: disable=too-many-locals last_log_line = len(log_lines) if needs_commit: session.commit() - time.sleep(current_app.config["HIVE_POLL_INTERVAL"]) + if sleep_interval := current_app.config.get("HIVE_POLL_INTERVAL"): + logger.warning( + "HIVE_POLL_INTERVAL is deprecated and will be removed in 3.0. Please use DB_POLL_INTERVAL_SECONDS instead" + ) + else: + sleep_interval = current_app.config["DB_POLL_INTERVAL_SECONDS"].get( + cls.engine, 5 + ) + time.sleep(sleep_interval) polled = cursor.poll() @classmethod diff --git a/superset/db_engine_specs/impala.py b/superset/db_engine_specs/impala.py index 048588c046fd..177a9728fe0f 100644 --- a/superset/db_engine_specs/impala.py +++ b/superset/db_engine_specs/impala.py @@ -14,14 +14,25 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +import logging +import re +import time from datetime import datetime from typing import Any, Dict, List, Optional +from flask import current_app from sqlalchemy.engine.reflection import Inspector +from sqlalchemy.orm import Session +from superset.constants import QUERY_EARLY_CANCEL_KEY from superset.db_engine_specs.base import BaseEngineSpec +from superset.models.sql_lab import Query from superset.utils import core as utils +logger = logging.getLogger(__name__) +# Query 5543ffdf692b7d02:f78a944000000000: 3% Complete (17 out of 547) +QUERY_PROGRESS_REGEX = re.compile(r"Query.*: (?P[0-9]+)%") + class ImpalaEngineSpec(BaseEngineSpec): """Engine spec for Cloudera's Impala""" @@ -63,3 +74,82 @@ def get_schema_names(cls, inspector: Inspector) -> List[str]: if not row[0].startswith("_") ] return schemas + + @classmethod + def has_implicit_cancel(cls) -> bool: + """ + Return True if the live cursor handles the implicit cancelation of the query, + False otherise. + + :return: Whether the live cursor implicitly cancels the query + :see: handle_cursor + """ + + return True + + @classmethod + def execute( + cls, + cursor: Any, + query: str, + **kwargs: Any, # pylint: disable=unused-argument + ) -> None: + try: + cursor.execute_async(query) + except Exception as ex: + raise cls.get_dbapi_mapped_exception(ex) + + @classmethod + def handle_cursor(cls, cursor: Any, query: Query, session: Session) -> None: + """Stop query and updates progress information""" + + query_id = query.id + unfinished_states = ( + "INITIALIZED_STATE", + "RUNNING_STATE", + ) + + try: + status = cursor.status() + while status in unfinished_states: + session.refresh(query) + query = session.query(Query).filter_by(id=query_id).one() + # if query cancelation was requested prior to the handle_cursor call, but + # the query was still executed + # modified in stop_query in views / core.py is reflected here. + # stop query + if query.extra.get(QUERY_EARLY_CANCEL_KEY): + cursor.cancel_operation() + cursor.close_operation() + cursor.close() + break + + # updates progress info by log + try: + log = cursor.get_log() or "" + except Exception: # pylint: disable=broad-except + logger.warning("Call to GetLog() failed") + log = "" + + if log: + match = QUERY_PROGRESS_REGEX.match(log) + if match: + progress = int(match.groupdict()["query_progress"]) + logger.debug( + "Query %s: Progress total: %s", str(query_id), str(progress) + ) + needs_commit = False + if progress > query.progress: + query.progress = progress + needs_commit = True + + if needs_commit: + session.commit() + sleep_interval = current_app.config["DB_POLL_INTERVAL_SECONDS"].get( + cls.engine, 5 + ) + time.sleep(sleep_interval) + status = cursor.status() + except Exception: # pylint: disable=broad-except + logger.debug("Call to status() failed ") + return diff --git a/superset/views/core.py b/superset/views/core.py index d0db5e9b2e94..34e59947e7dd 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -67,6 +67,7 @@ SqlMetric, TableColumn, ) +from superset.constants import QUERY_EARLY_CANCEL_KEY from superset.dashboards.commands.importers.v0 import ImportDashboardsCommand from superset.dashboards.dao import DashboardDAO from superset.dashboards.permalink.commands.get import GetDashboardPermalinkCommand @@ -2318,6 +2319,9 @@ def stop_query(self) -> FlaskResponse: raise SupersetCancelQueryException("Could not cancel query") query.status = QueryStatus.STOPPED + # Add the stop identity attribute because the sqlalchemy thread is unsafe + # because of multiple updates to the status in the query table + query.set_extra_json_key(QUERY_EARLY_CANCEL_KEY, True) query.end_time = now_as_float() db.session.commit() From 399f6e3ddc8bb21fd7b39cdf850510b2692fbe12 Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Tue, 10 Jan 2023 16:48:51 +0100 Subject: [PATCH 02/15] feat(dashboard): Display a loading spinner while dashboard is being saved (#22588) --- .../src/dashboard/actions/dashboardState.js | 14 ++++++++++++++ .../src/dashboard/actions/dashboardState.test.js | 8 +++++--- superset-frontend/src/dashboard/actions/hydrate.js | 1 + .../DashboardBuilder/DashboardBuilder.test.tsx | 14 ++++++++++++++ .../DashboardBuilder/DashboardBuilder.tsx | 12 ++++++++++++ .../src/dashboard/reducers/dashboardState.js | 14 ++++++++++++++ superset-frontend/src/dashboard/types.ts | 1 + 7 files changed, 61 insertions(+), 3 deletions(-) diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js index 9bbe618c88da..518dd7b5dc31 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.js @@ -203,9 +203,20 @@ export function setOverrideConfirm(overwriteConfirmMetadata) { }; } +export const SAVE_DASHBOARD_STARTED = 'SAVE_DASHBOARD_STARTED'; +export function saveDashboardStarted() { + return { type: SAVE_DASHBOARD_STARTED }; +} + +export const SAVE_DASHBOARD_FINISHED = 'SAVE_DASHBOARD_FINISHED'; +export function saveDashboardFinished() { + return { type: SAVE_DASHBOARD_FINISHED }; +} + export function saveDashboardRequest(data, id, saveType) { return (dispatch, getState) => { dispatch({ type: UPDATE_COMPONENTS_PARENTS_LIST }); + dispatch(saveDashboardStarted()); const { dashboardFilters, dashboardLayout } = getState(); const layout = dashboardLayout.present; @@ -291,6 +302,7 @@ export function saveDashboardRequest(data, id, saveType) { const chartConfiguration = handleChartConfiguration(); dispatch(setChartConfiguration(chartConfiguration)); } + dispatch(saveDashboardFinished()); dispatch(addSuccessToast(t('This dashboard was saved successfully.'))); return response; }; @@ -322,6 +334,7 @@ export function saveDashboardRequest(data, id, saveType) { if (lastModifiedTime) { dispatch(saveDashboardRequestSuccess(lastModifiedTime)); } + dispatch(saveDashboardFinished()); // redirect to the new slug or id window.history.pushState( { event: 'dashboard_properties_changed' }, @@ -347,6 +360,7 @@ export function saveDashboardRequest(data, id, saveType) { if (typeof message === 'string' && message === 'Forbidden') { errorText = t('You do not have permission to edit this dashboard'); } + dispatch(saveDashboardFinished()); dispatch(addDangerToast(errorText)); }; diff --git a/superset-frontend/src/dashboard/actions/dashboardState.test.js b/superset-frontend/src/dashboard/actions/dashboardState.test.js index 25aa54ed6638..00b358bc4397 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.test.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.test.js @@ -22,6 +22,7 @@ import { waitFor } from '@testing-library/react'; import { removeSliceFromDashboard, + SAVE_DASHBOARD_STARTED, saveDashboardRequest, SET_OVERRIDE_CONFIRM, } from 'src/dashboard/actions/dashboardState'; @@ -104,10 +105,11 @@ describe('dashboardState actions', () => { }); const thunk = saveDashboardRequest(newDashboardData, 1, 'save_dash'); thunk(dispatch, getState); - expect(dispatch.callCount).toBe(1); + expect(dispatch.callCount).toBe(2); expect(dispatch.getCall(0).args[0].type).toBe( UPDATE_COMPONENTS_PARENTS_LIST, ); + expect(dispatch.getCall(1).args[0].type).toBe(SAVE_DASHBOARD_STARTED); }); it('should post dashboard data with updated redux state', () => { @@ -162,10 +164,10 @@ describe('dashboardState actions', () => { expect(getStub.callCount).toBe(1); expect(postStub.callCount).toBe(0); await waitFor(() => - expect(dispatch.getCall(1).args[0].type).toBe(SET_OVERRIDE_CONFIRM), + expect(dispatch.getCall(2).args[0].type).toBe(SET_OVERRIDE_CONFIRM), ); expect( - dispatch.getCall(1).args[0].overwriteConfirmMetadata.dashboardId, + dispatch.getCall(2).args[0].overwriteConfirmMetadata.dashboardId, ).toBe(id); }); diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 917492bd7b20..ed359a8cee10 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -454,6 +454,7 @@ export const hydrateDashboard = editMode: canEdit && editMode, isPublished: dashboard.published, hasUnsavedChanges: false, + dashboardIsSaving: false, maxUndoHistoryExceeded: false, lastModifiedTime: dashboard.changed_on, isRefreshing: false, diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx index 65969e2ca6d7..b5f249b89602 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.test.tsx @@ -249,6 +249,20 @@ describe('DashboardBuilder', () => { (setDirectPathToChild as jest.Mock).mockReset(); }); + it('should not display a loading spinner when saving is not in progress', () => { + const { queryByAltText } = setup(); + + expect(queryByAltText('Loading...')).not.toBeInTheDocument(); + }); + + it('should display a loading spinner when saving is in progress', async () => { + const { findByAltText } = setup({ + dashboardState: { dashboardIsSaving: true }, + }); + + expect(await findByAltText('Loading...')).toBeVisible(); + }); + describe('when nativeFiltersEnabled', () => { beforeEach(() => { (isFeatureEnabled as jest.Mock).mockImplementation( diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index d2ce6a2f1a3e..ab14c4d93205 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -246,6 +246,9 @@ const DashboardBuilder: FC = () => { const canEdit = useSelector( ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, ); + const dashboardIsSaving = useSelector( + ({ dashboardState }) => dashboardState.dashboardIsSaving, + ); const nativeFilters = useSelector((state: RootState) => state.nativeFilters); const focusedFilterId = nativeFilters?.focusedFilterId; const fullSizeChartId = useSelector( @@ -533,6 +536,15 @@ const DashboardBuilder: FC = () => { + {dashboardIsSaving && ( + + )} ); }; diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js index 1c339001d17d..5d81cd8ac11f 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.js @@ -43,6 +43,8 @@ import { ON_FILTERS_REFRESH_SUCCESS, SET_DATASETS_STATUS, SET_OVERRIDE_CONFIRM, + SAVE_DASHBOARD_STARTED, + SAVE_DASHBOARD_FINISHED, } from '../actions/dashboardState'; import { HYDRATE_DASHBOARD } from '../actions/hydrate'; @@ -111,6 +113,18 @@ export default function dashboardStateReducer(state = {}, action) { [ON_CHANGE]() { return { ...state, hasUnsavedChanges: true }; }, + [SAVE_DASHBOARD_STARTED]() { + return { + ...state, + dashboardIsSaving: true, + }; + }, + [SAVE_DASHBOARD_FINISHED]() { + return { + ...state, + dashboardIsSaving: false, + }; + }, [ON_SAVE]() { return { ...state, diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index 1bdd1c14a1c7..e24356be6191 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -70,6 +70,7 @@ export type DashboardState = { isRefreshing: boolean; isFiltersRefreshing: boolean; hasUnsavedChanges: boolean; + dashboardIsSaving: boolean; colorScheme: string; sliceIds: number[]; directPathLastUpdated: number; From 1e3746be215e5c2060b00d4c3196518f7c71697a Mon Sep 17 00:00:00 2001 From: Artem Shumeiko <53895552+artemonsh@users.noreply.github.com> Date: Tue, 10 Jan 2023 19:10:46 +0300 Subject: [PATCH 03/15] fix(dockerfile): fix "unhealthy" container state (#22663) --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index a0aed973e5f0..a30eaaa165dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,6 +58,7 @@ RUN mkdir -p ${PYTHONPATH} \ && apt-get update -y \ && apt-get install -y --no-install-recommends \ build-essential \ + curl \ default-libmysqlclient-dev \ libsasl2-dev \ libsasl2-modules-gssapi-mit \ From 08f45ef207fb159bf0de49dd0a90f423c77965a7 Mon Sep 17 00:00:00 2001 From: Ville Brofeldt <33317356+villebro@users.noreply.github.com> Date: Tue, 10 Jan 2023 19:08:30 +0200 Subject: [PATCH 04/15] fix(viz-gallery): respect denylist in viz gallery (#22658) --- .../spec/helpers/reducerIndex.ts | 8 ++--- superset-frontend/spec/helpers/setup.ts | 2 +- superset-frontend/src/SqlLab/App.jsx | 4 +-- .../components/ScheduleQueryButton/index.tsx | 6 ++-- .../src/SqlLab/components/SqlEditor/index.jsx | 6 ++-- .../components/TabbedSqlEditors/index.jsx | 8 ++--- .../src/addSlice/AddSliceContainer.test.tsx | 2 +- .../src/addSlice/AddSliceContainer.tsx | 5 +++ .../src/dashboard/components/Dashboard.jsx | 4 +-- superset-frontend/src/embedded/api.tsx | 4 ++- superset-frontend/src/embedded/index.tsx | 3 +- .../VizTypeControl/VizTypeControl.test.tsx | 2 +- .../VizTypeControl/VizTypeGallery.tsx | 2 ++ .../controls/VizTypeControl/index.tsx | 4 +++ .../src/middleware/asyncEvent.ts | 17 ++-------- superset-frontend/src/preamble.ts | 31 ++++++------------- superset-frontend/src/profile/App.tsx | 8 ++--- .../src/profile/components/App.tsx | 4 +-- .../src/profile/components/CreatedContent.tsx | 6 ++-- .../src/profile/components/Favorites.tsx | 7 ++--- .../src/profile/components/RecentActivity.tsx | 6 ++-- .../src/profile/components/Security.tsx | 13 ++++---- .../src/profile/components/UserInfo.tsx | 19 ++++++------ .../src/showSavedQuery/index.jsx | 5 ++- .../src/utils/getBootstrapData.ts | 3 +- .../src/utils/hostNamesConfig.js | 5 +-- superset-frontend/src/views/App.tsx | 15 ++++----- .../src/views/CRUD/chart/ChartList.tsx | 6 ++-- .../views/CRUD/dashboard/DashboardList.tsx | 4 ++- .../src/views/RootContextProviders.tsx | 6 ++-- superset-frontend/src/views/menu.tsx | 7 ++--- superset-frontend/src/views/store.ts | 11 ++++--- superset/views/base.py | 1 + 33 files changed, 111 insertions(+), 123 deletions(-) diff --git a/superset-frontend/spec/helpers/reducerIndex.ts b/superset-frontend/spec/helpers/reducerIndex.ts index edfaf7bb5c8c..459a112e2201 100644 --- a/superset-frontend/spec/helpers/reducerIndex.ts +++ b/superset-frontend/spec/helpers/reducerIndex.ts @@ -31,13 +31,13 @@ import explore from 'src/explore/reducers/exploreReducer'; import sqlLab from 'src/SqlLab/reducers/sqlLab'; import localStorageUsageInKilobytes from 'src/SqlLab/reducers/localStorageUsage'; import reports from 'src/reports/reducers/reports'; +import getBootstrapData from 'src/utils/getBootstrapData'; const impressionId = (state = '') => state; -const container = document.getElementById('app'); -const bootstrap = JSON.parse(container?.getAttribute('data-bootstrap') ?? '{}'); -const common = { ...bootstrap.common }; -const user = { ...bootstrap.user }; +const bootstrapData = getBootstrapData(); +const common = { ...bootstrapData.common }; +const user = { ...bootstrapData.user }; const noopReducer = (initialState: unknown) => diff --git a/superset-frontend/spec/helpers/setup.ts b/superset-frontend/spec/helpers/setup.ts index bd2961e23cec..281ab4ae4bb1 100644 --- a/superset-frontend/spec/helpers/setup.ts +++ b/superset-frontend/spec/helpers/setup.ts @@ -25,5 +25,5 @@ configureTestingLibrary({ testIdAttribute: 'data-test', }); -document.body.innerHTML = '
'; +document.body.innerHTML = '
'; expect.extend(matchers); diff --git a/superset-frontend/src/SqlLab/App.jsx b/superset-frontend/src/SqlLab/App.jsx index 812202eec20f..4a300958031d 100644 --- a/superset-frontend/src/SqlLab/App.jsx +++ b/superset-frontend/src/SqlLab/App.jsx @@ -30,6 +30,7 @@ import { FeatureFlag, } from 'src/featureFlags'; import setupExtensions from 'src/setup/setupExtensions'; +import getBootstrapData from 'src/utils/getBootstrapData'; import getInitialState from './reducers/getInitialState'; import rootReducer from './reducers/index'; import { initEnhancer } from '../reduxUtils'; @@ -48,8 +49,7 @@ import { theme } from '../preamble'; setupApp(); setupExtensions(); -const appContainer = document.getElementById('app'); -const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap')); +const bootstrapData = getBootstrapData(); initFeatureFlags(bootstrapData.common.feature_flags); diff --git a/superset-frontend/src/SqlLab/components/ScheduleQueryButton/index.tsx b/superset-frontend/src/SqlLab/components/ScheduleQueryButton/index.tsx index 780bc4d96d70..c78e2bd32d0b 100644 --- a/superset-frontend/src/SqlLab/components/ScheduleQueryButton/index.tsx +++ b/superset-frontend/src/SqlLab/components/ScheduleQueryButton/index.tsx @@ -25,11 +25,9 @@ import * as chrono from 'chrono-node'; import ModalTrigger, { ModalTriggerRef } from 'src/components/ModalTrigger'; import { Form, FormItem } from 'src/components/Form'; import Button from 'src/components/Button'; +import getBootstrapData from 'src/utils/getBootstrapData'; -const appContainer = document.getElementById('app'); -const bootstrapData = JSON.parse( - appContainer?.getAttribute('data-bootstrap') || '{}', -); +const bootstrapData = getBootstrapData(); const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES; const validators = { diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx index 8e48cbefbe69..e27f6c1e0be2 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx @@ -74,6 +74,7 @@ import { } from 'src/utils/localStorageHelpers'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { EmptyStateBig } from 'src/components/EmptyState'; +import getBootstrapData from 'src/utils/getBootstrapData'; import { isEmpty } from 'lodash'; import TemplateParamsEditor from '../TemplateParamsEditor'; import SouthPane from '../SouthPane'; @@ -86,10 +87,7 @@ import AceEditorWrapper from '../AceEditorWrapper'; import RunQueryActionButton from '../RunQueryActionButton'; import QueryLimitSelect from '../QueryLimitSelect'; -const appContainer = document.getElementById('app'); -const bootstrapData = JSON.parse( - appContainer.getAttribute('data-bootstrap') || '{}', -); +const bootstrapData = getBootstrapData(); const validatorMap = bootstrapData?.common?.conf?.SQL_VALIDATORS_BY_ENGINE || {}; const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES; diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx index 63c7cc862caf..ca53fed70947 100644 --- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx +++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx @@ -28,6 +28,7 @@ import { Tooltip } from 'src/components/Tooltip'; import { detectOS } from 'src/utils/common'; import * as Actions from 'src/SqlLab/actions/sqlLab'; import { EmptyStateBig } from 'src/components/EmptyState'; +import getBootstrapData from 'src/utils/getBootstrapData'; import SqlEditor from '../SqlEditor'; import SqlEditorTabHeader from '../SqlEditorTabHeader'; @@ -106,13 +107,10 @@ class TabbedSqlEditors extends React.PureComponent { } // merge post form data with GET search params - // Hack: this data should be comming from getInitialState + // Hack: this data should be coming from getInitialState // but for some reason this data isn't being passed properly through // the reducer. - const appContainer = document.getElementById('app'); - const bootstrapData = JSON.parse( - appContainer?.getAttribute('data-bootstrap') || '{}', - ); + const bootstrapData = getBootstrapData(); const query = { ...bootstrapData.requested_query, ...URI(window.location).search(true), diff --git a/superset-frontend/src/addSlice/AddSliceContainer.test.tsx b/superset-frontend/src/addSlice/AddSliceContainer.test.tsx index e5f62fd258eb..1e26c849955d 100644 --- a/superset-frontend/src/addSlice/AddSliceContainer.test.tsx +++ b/superset-frontend/src/addSlice/AddSliceContainer.test.tsx @@ -84,7 +84,7 @@ async function getWrapper(user = mockUser) { return wrapper; } -test('renders a select and a VizTypeControl', async () => { +test('renders a select and a VizTypeGallery', async () => { const wrapper = await getWrapper(); expect(wrapper.find(AsyncSelect)).toExist(); expect(wrapper.find(VizTypeGallery)).toExist(); diff --git a/superset-frontend/src/addSlice/AddSliceContainer.tsx b/superset-frontend/src/addSlice/AddSliceContainer.tsx index f63c53f3370e..c50bcad9a2a5 100644 --- a/superset-frontend/src/addSlice/AddSliceContainer.tsx +++ b/superset-frontend/src/addSlice/AddSliceContainer.tsx @@ -39,6 +39,7 @@ import VizTypeGallery, { } from 'src/explore/components/controls/VizTypeControl/VizTypeGallery'; import { findPermission } from 'src/utils/findPermission'; import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; +import getBootstrapData from 'src/utils/getBootstrapData'; type Dataset = { id: number; @@ -62,6 +63,9 @@ export type AddSliceContainerState = { const ESTIMATED_NAV_HEIGHT = 56; const ELEMENTS_EXCEPT_VIZ_GALLERY = ESTIMATED_NAV_HEIGHT + 250; +const bootstrapData = getBootstrapData(); +const denyList: string[] = bootstrapData.common.conf.VIZ_TYPE_DENYLIST || []; + const StyledContainer = styled.div` ${({ theme }) => ` flex: 1 1 auto; @@ -395,6 +399,7 @@ export class AddSliceContainer extends React.PureComponent< description={ 0, diff --git a/superset-frontend/src/embedded/api.tsx b/superset-frontend/src/embedded/api.tsx index 675e97ac542f..9d37daf2e01b 100644 --- a/superset-frontend/src/embedded/api.tsx +++ b/superset-frontend/src/embedded/api.tsx @@ -16,10 +16,12 @@ * specific language governing permissions and limitations * under the License. */ +import getBootstrapData from 'src/utils/getBootstrapData'; import { store } from '../views/store'; -import { bootstrapData } from '../preamble'; import { getDashboardPermalink as getDashboardPermalinkUtil } from '../utils/urlUtils'; +const bootstrapData = getBootstrapData(); + type Size = { width: number; height: number; diff --git a/superset-frontend/src/embedded/index.tsx b/superset-frontend/src/embedded/index.tsx index 832c76a767ca..50c026fba8f9 100644 --- a/superset-frontend/src/embedded/index.tsx +++ b/superset-frontend/src/embedded/index.tsx @@ -21,7 +21,7 @@ import ReactDOM from 'react-dom'; import { BrowserRouter as Router, Route } from 'react-router-dom'; import { makeApi, t, logging } from '@superset-ui/core'; import Switchboard from '@superset-ui/switchboard'; -import { bootstrapData } from 'src/preamble'; +import getBootstrapData from 'src/utils/getBootstrapData'; import setupClient from 'src/setup/setupClient'; import { RootContextProviders } from 'src/views/RootContextProviders'; import { store, USER_LOADED } from 'src/views/store'; @@ -33,6 +33,7 @@ import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; import { embeddedApi } from './api'; const debugMode = process.env.WEBPACK_MODE === 'development'; +const bootstrapData = getBootstrapData(); function log(...info: unknown[]) { if (debugMode) { diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx index 2ee9bfab0e56..034b339cda94 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx @@ -40,7 +40,7 @@ import { } from '@superset-ui/plugin-chart-echarts'; import TableChartPlugin from '@superset-ui/plugin-chart-table'; import { LineChartPlugin } from '@superset-ui/preset-chart-xy'; -import TimeTableChartPlugin from '../../../../visualizations/TimeTable'; +import TimeTableChartPlugin from 'src/visualizations/TimeTable'; import VizTypeControl, { VIZ_TYPE_CONTROL_TEST_ID } from './index'; jest.useFakeTimers(); diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx index c11ae9f94804..6b1466007512 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx @@ -50,6 +50,7 @@ interface VizTypeGalleryProps { onDoubleClick: () => void; selectedViz: string | null; className?: string; + denyList: string[]; } type VizEntry = { @@ -506,6 +507,7 @@ export default function VizTypeGallery(props: VizTypeGalleryProps) { const chartMetadata: VizEntry[] = useMemo(() => { const result = Object.entries(mountedPluginMetadata) .map(([key, value]) => ({ key, value })) + .filter(({ key }) => !props.denyList.includes(key)) .filter( ({ value }) => nativeFilterGate(value.behaviors || []) && !value.deprecated, diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx index 2059e1138b38..802c04f8a995 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx @@ -27,6 +27,7 @@ import { import { usePluginContext } from 'src/components/DynamicPlugins'; import Modal from 'src/components/Modal'; import { noOp } from 'src/utils/common'; +import getBootstrapData from 'src/utils/getBootstrapData'; import VizTypeGallery, { MAX_ADVISABLE_VIZ_GALLERY_WIDTH, } from './VizTypeGallery'; @@ -41,6 +42,8 @@ interface VizTypeControlProps { isModalOpenInit?: boolean; } +const bootstrapData = getBootstrapData(); +const denyList: string[] = bootstrapData.common.conf.VIZ_TYPE_DENYLIST || []; const metadataRegistry = getChartMetadataRegistry(); export const VIZ_TYPE_CONTROL_TEST_ID = 'viz-type-control'; @@ -141,6 +144,7 @@ const VizTypeControl = ({ selectedViz={selectedViz} onChange={setSelectedViz} onDoubleClick={onSubmit} + denyList={denyList} /> diff --git a/superset-frontend/src/middleware/asyncEvent.ts b/superset-frontend/src/middleware/asyncEvent.ts index f76b0a713fbb..f1eee02ab4d5 100644 --- a/superset-frontend/src/middleware/asyncEvent.ts +++ b/superset-frontend/src/middleware/asyncEvent.ts @@ -23,6 +23,7 @@ import { logging, } from '@superset-ui/core'; import { SupersetError } from 'src/components/ErrorMessage/types'; +import getBootstrapData from 'src/utils/getBootstrapData'; import { FeatureFlag, isFeatureEnabled } from '../featureFlags'; import { getClientErrorObject, @@ -235,21 +236,7 @@ export const init = (appConfig?: AppConfig) => { 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'); - } - } + config = appConfig || getBootstrapData().config; transport = config.GLOBAL_ASYNC_QUERIES_TRANSPORT || TRANSPORT_POLLING; pollingDelayMs = config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY || 500; diff --git a/superset-frontend/src/preamble.ts b/superset-frontend/src/preamble.ts index c5026cdb8b79..1d5c2ae10c50 100644 --- a/superset-frontend/src/preamble.ts +++ b/superset-frontend/src/preamble.ts @@ -26,47 +26,34 @@ import setupClient from './setup/setupClient'; import setupColors from './setup/setupColors'; import setupFormatters from './setup/setupFormatters'; import setupDashboardComponents from './setup/setupDasboardComponents'; -import { BootstrapData, User } from './types/bootstrapTypes'; +import { User } from './types/bootstrapTypes'; import { initFeatureFlags } from './featureFlags'; -import { DEFAULT_COMMON_BOOTSTRAP_DATA } from './constants'; +import getBootstrapData from './utils/getBootstrapData'; if (process.env.WEBPACK_MODE === 'development') { setHotLoaderConfig({ logLevel: 'debug', trackTailUpdates: false }); } // eslint-disable-next-line import/no-mutable-exports -export let bootstrapData: BootstrapData = { - common: { - ...DEFAULT_COMMON_BOOTSTRAP_DATA, - }, -}; +const bootstrapData = getBootstrapData(); // Configure translation if (typeof window !== 'undefined') { - const root = document.getElementById('app'); - bootstrapData = root - ? JSON.parse(root.getAttribute('data-bootstrap') || '{}') - : {}; - if (bootstrapData?.common?.language_pack) { - const languagePack = bootstrapData.common.language_pack; - configure({ languagePack }); - moment.locale(bootstrapData.common.locale); - } else { - configure(); - } + configure({ languagePack: bootstrapData.common.language_pack }); + moment.locale(bootstrapData.common.locale); } else { configure(); } // Configure feature flags -initFeatureFlags(bootstrapData?.common?.feature_flags); +initFeatureFlags(bootstrapData.common.feature_flags); // Setup SupersetClient setupClient(); setupColors( - bootstrapData?.common?.extra_categorical_color_schemes, - bootstrapData?.common?.extra_sequential_color_schemes, + bootstrapData.common.extra_categorical_color_schemes, + bootstrapData.common.extra_sequential_color_schemes, ); // Setup number formatters @@ -76,7 +63,7 @@ setupDashboardComponents(); export const theme = merge( supersetTheme, - bootstrapData?.common?.theme_overrides ?? {}, + bootstrapData.common.theme_overrides ?? {}, ); const getMe = makeApi({ diff --git a/superset-frontend/src/profile/App.tsx b/superset-frontend/src/profile/App.tsx index 3704dcb4b5f4..ed331ce73725 100644 --- a/superset-frontend/src/profile/App.tsx +++ b/superset-frontend/src/profile/App.tsx @@ -30,14 +30,12 @@ import setupApp from 'src/setup/setupApp'; import setupExtensions from 'src/setup/setupExtensions'; import { theme } from 'src/preamble'; import ToastContainer from 'src/components/MessageToasts/ToastContainer'; +import getBootstrapData from 'src/utils/getBootstrapData'; setupApp(); setupExtensions(); -const profileViewContainer = document.getElementById('app'); -const bootstrap = JSON.parse( - profileViewContainer?.getAttribute('data-bootstrap') ?? '{}', -); +const bootstrapData = getBootstrapData(); const store = createStore( combineReducers({ @@ -51,7 +49,7 @@ const Application = () => ( - + diff --git a/superset-frontend/src/profile/components/App.tsx b/superset-frontend/src/profile/components/App.tsx index e2c910abd2f5..08130176fe61 100644 --- a/superset-frontend/src/profile/components/App.tsx +++ b/superset-frontend/src/profile/components/App.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { t, styled } from '@superset-ui/core'; import { Row, Col } from 'src/components'; import Tabs from 'src/components/Tabs'; -import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; +import { BootstrapUser } from 'src/types/bootstrapTypes'; import Favorites from './Favorites'; import UserInfo from './UserInfo'; import Security from './Security'; @@ -28,7 +28,7 @@ import RecentActivity from './RecentActivity'; import CreatedContent from './CreatedContent'; interface AppProps { - user: UserWithPermissionsAndRoles; + user: BootstrapUser; } const StyledTabPane = styled(Tabs.TabPane)` diff --git a/superset-frontend/src/profile/components/CreatedContent.tsx b/superset-frontend/src/profile/components/CreatedContent.tsx index e32fde86b1d2..a972ea5db90f 100644 --- a/superset-frontend/src/profile/components/CreatedContent.tsx +++ b/superset-frontend/src/profile/components/CreatedContent.tsx @@ -22,13 +22,13 @@ import { t } from '@superset-ui/core'; import TableLoader from 'src/components/TableLoader'; import { - User, - DashboardResponse, + BootstrapUser, ChartResponse, + DashboardResponse, } from 'src/types/bootstrapTypes'; interface CreatedContentProps { - user: User; + user: BootstrapUser; } class CreatedContent extends React.PureComponent { diff --git a/superset-frontend/src/profile/components/Favorites.tsx b/superset-frontend/src/profile/components/Favorites.tsx index 1a7daa750f06..1e28cd989aab 100644 --- a/superset-frontend/src/profile/components/Favorites.tsx +++ b/superset-frontend/src/profile/components/Favorites.tsx @@ -20,13 +20,12 @@ import React from 'react'; import rison from 'rison'; import moment from 'moment'; import { t } from '@superset-ui/core'; - +import { DashboardResponse, BootstrapUser } from 'src/types/bootstrapTypes'; import TableLoader from '../../components/TableLoader'; import { Slice } from '../types'; -import { User, DashboardResponse } from '../../types/bootstrapTypes'; interface FavoritesProps { - user: User; + user: BootstrapUser; } export default class Favorites extends React.PureComponent { @@ -40,7 +39,7 @@ export default class Favorites extends React.PureComponent { })); return ( ); diff --git a/superset-frontend/src/profile/components/Security.tsx b/superset-frontend/src/profile/components/Security.tsx index 078fc42e4c74..a102e5599287 100644 --- a/superset-frontend/src/profile/components/Security.tsx +++ b/superset-frontend/src/profile/components/Security.tsx @@ -21,10 +21,10 @@ import Badge from 'src/components/Badge'; import { t } from '@superset-ui/core'; import Label from 'src/components/Label'; -import { UserWithPermissionsAndRoles } from '../../types/bootstrapTypes'; +import { BootstrapUser } from 'src/types/bootstrapTypes'; interface SecurityProps { - user: UserWithPermissionsAndRoles; + user: BootstrapUser; } export default function Security({ user }: SecurityProps) { @@ -32,15 +32,16 @@ export default function Security({ user }: SecurityProps) {

- {t('Roles')} + {t('Roles')}{' '} +

- {Object.keys(user.roles).map(role => ( + {Object.keys(user?.roles || {}).map(role => ( ))}
- {user.permissions.database_access && ( + {user?.permissions.database_access && (

{t('Databases')}{' '} @@ -54,7 +55,7 @@ export default function Security({ user }: SecurityProps) { )}

- {user.permissions.datasource_access && ( + {user?.permissions.datasource_access && (

{t('Datasets')}{' '} diff --git a/superset-frontend/src/profile/components/UserInfo.tsx b/superset-frontend/src/profile/components/UserInfo.tsx index 887b4ec50c13..6b44e35caeaf 100644 --- a/superset-frontend/src/profile/components/UserInfo.tsx +++ b/superset-frontend/src/profile/components/UserInfo.tsx @@ -20,10 +20,10 @@ import React from 'react'; import Gravatar from 'react-gravatar'; import moment from 'moment'; import { t, styled } from '@superset-ui/core'; -import { UserWithPermissionsAndRoles } from '../../types/bootstrapTypes'; +import { BootstrapUser } from 'src/types/bootstrapTypes'; interface UserInfoProps { - user: UserWithPermissionsAndRoles; + user: BootstrapUser; } const StyledContainer = styled.div` @@ -37,7 +37,7 @@ export default function UserInfo({ user }: UserInfoProps) {

- {user.firstName} {user.lastName} + {user?.firstName} {user?.lastName}

- {user.username} + {user?.username}


{' '} - {t('joined')} {moment(user.createdOn, 'YYYYMMDD').fromNow()} + {t('joined')} {moment(user?.createdOn, 'YYYYMMDD').fromNow()}

- {user.email} + {user?.email}

- {Object.keys(user.roles).join(', ')} + {' '} + {Object.keys(user?.roles || {}).join(', ')}

  {t('id:')}  - {user.userId} + {user?.userId}

diff --git a/superset-frontend/src/showSavedQuery/index.jsx b/superset-frontend/src/showSavedQuery/index.jsx index 6b8adcd60d7a..10c09703f720 100644 --- a/superset-frontend/src/showSavedQuery/index.jsx +++ b/superset-frontend/src/showSavedQuery/index.jsx @@ -21,11 +21,10 @@ import ReactDom from 'react-dom'; import Form from 'react-jsonschema-form'; import { interpolate } from 'src/showSavedQuery/utils'; import { styled } from '@superset-ui/core'; +import getBootstrapData from 'src/utils/getBootstrapData'; const scheduleInfoContainer = document.getElementById('schedule-info'); -const bootstrapData = JSON.parse( - scheduleInfoContainer.getAttribute('data-bootstrap'), -); +const bootstrapData = getBootstrapData(); const config = bootstrapData.common.conf.SCHEDULED_QUERIES; const { query } = bootstrapData.common; const scheduleInfo = query.extra_json.schedule_info; diff --git a/superset-frontend/src/utils/getBootstrapData.ts b/superset-frontend/src/utils/getBootstrapData.ts index e8d708e40dc4..e426498e5738 100644 --- a/superset-frontend/src/utils/getBootstrapData.ts +++ b/superset-frontend/src/utils/getBootstrapData.ts @@ -18,9 +18,10 @@ */ import { BootstrapData } from 'src/types/bootstrapTypes'; +import { DEFAULT_BOOTSTRAP_DATA } from 'src/constants'; export default function getBootstrapData(): BootstrapData { const appContainer = document.getElementById('app'); const dataBootstrap = appContainer?.getAttribute('data-bootstrap'); - return dataBootstrap ? JSON.parse(dataBootstrap) : {}; + return dataBootstrap ? JSON.parse(dataBootstrap) : DEFAULT_BOOTSTRAP_DATA; } diff --git a/superset-frontend/src/utils/hostNamesConfig.js b/superset-frontend/src/utils/hostNamesConfig.js index f2a1638cc775..2fda95c46ab6 100644 --- a/superset-frontend/src/utils/hostNamesConfig.js +++ b/superset-frontend/src/utils/hostNamesConfig.js @@ -21,6 +21,7 @@ import { isFeatureEnabled, FeatureFlag, } from 'src/featureFlags'; +import getBootstrapData from './getBootstrapData'; function getDomainsConfig() { const appContainer = document.getElementById('app'); @@ -38,11 +39,11 @@ function getDomainsConfig() { return Array.from(availableDomains); } - const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap')); + const bootstrapData = getBootstrapData(); // this module is a little special, it may be loaded before index.jsx, // where window.featureFlags get initialized // eslint-disable-next-line camelcase - initFeatureFlags(bootstrapData?.common?.feature_flags); + initFeatureFlags(bootstrapData.common.feature_flags); if ( isFeatureEnabled(FeatureFlag.ALLOW_DASHBOARD_DOMAIN_SHARDING) && diff --git a/superset-frontend/src/views/App.tsx b/superset-frontend/src/views/App.tsx index e780242dae35..cacfaa145528 100644 --- a/superset-frontend/src/views/App.tsx +++ b/superset-frontend/src/views/App.tsx @@ -29,7 +29,7 @@ import { GlobalStyles } from 'src/GlobalStyles'; import ErrorBoundary from 'src/components/ErrorBoundary'; import Loading from 'src/components/Loading'; import Menu from 'src/views/components/Menu'; -import { bootstrapData } from 'src/preamble'; +import getBootstrapData from 'src/utils/getBootstrapData'; import ToastContainer from 'src/components/MessageToasts/ToastContainer'; import setupApp from 'src/setup/setupApp'; import setupPlugins from 'src/setup/setupPlugins'; @@ -46,10 +46,8 @@ setupApp(); setupPlugins(); setupExtensions(); -const user = { ...bootstrapData.user }; -const menu = { - ...bootstrapData.common.menu_data, -}; +const bootstrapData = getBootstrapData(); + let lastLocationPathname: string; const boundActions = bindActionCreators({ logEvent }, store.dispatch); @@ -78,13 +76,16 @@ const App = () => ( - + {routes.map(({ path, Component, props = {}, Fallback = Loading }) => ( }> - + diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx index 1bbaddb41191..d449321686bc 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx +++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx @@ -65,7 +65,7 @@ import setupPlugins from 'src/setup/setupPlugins'; import InfoTooltip from 'src/components/InfoTooltip'; import CertifiedBadge from 'src/components/CertifiedBadge'; import { GenericLink } from 'src/components/GenericLink/GenericLink'; -import { bootstrapData } from 'src/preamble'; +import getBootstrapData from 'src/utils/getBootstrapData'; import Owner from 'src/types/Owner'; import ChartCard from './ChartCard'; @@ -157,6 +157,8 @@ const Actions = styled.div` color: ${({ theme }) => theme.colors.grayscale.base}; `; +const bootstrapData = getBootstrapData(); + function ChartList(props: ChartListProps) { const { addDangerToast, @@ -224,7 +226,7 @@ function ChartList(props: ChartListProps) { hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; const enableBroadUserAccess = - bootstrapData?.common?.conf?.ENABLE_BROAD_ACTIVITY_ACCESS; + bootstrapData.common.conf.ENABLE_BROAD_ACTIVITY_ACCESS; const handleBulkChartExport = (chartsToExport: Chart[]) => { const ids = chartsToExport.map(({ id }) => id); handleResourceExport('chart', ids, () => { diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx index 23b9543423b8..6fe5bf2b202b 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx @@ -49,7 +49,7 @@ import ImportModelsModal from 'src/components/ImportModal/index'; import Dashboard from 'src/dashboard/containers/Dashboard'; import CertifiedBadge from 'src/components/CertifiedBadge'; -import { bootstrapData } from 'src/preamble'; +import getBootstrapData from 'src/utils/getBootstrapData'; import DashboardCard from './DashboardCard'; import { DashboardStatus } from './types'; @@ -95,6 +95,8 @@ const Actions = styled.div` color: ${({ theme }) => theme.colors.grayscale.base}; `; +const bootstrapData = getBootstrapData(); + function DashboardList(props: DashboardListProps) { const { addDangerToast, diff --git a/superset-frontend/src/views/RootContextProviders.tsx b/superset-frontend/src/views/RootContextProviders.tsx index 99f620611b53..795ea7c5ee25 100644 --- a/superset-frontend/src/views/RootContextProviders.tsx +++ b/superset-frontend/src/views/RootContextProviders.tsx @@ -24,14 +24,14 @@ import { Provider as ReduxProvider } from 'react-redux'; import { QueryParamProvider } from 'use-query-params'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; - +import getBootstrapData from 'src/utils/getBootstrapData'; import { store } from './store'; import FlashProvider from '../components/FlashProvider'; -import { bootstrapData, theme } from '../preamble'; +import { theme } from '../preamble'; import { EmbeddedUiConfigProvider } from '../components/UiConfigContext'; import { DynamicPluginProvider } from '../components/DynamicPlugins'; -const common = { ...bootstrapData.common }; +const { common } = getBootstrapData(); const extensionsRegistry = getExtensionsRegistry(); diff --git a/superset-frontend/src/views/menu.tsx b/superset-frontend/src/views/menu.tsx index 4d27e3d6a46d..287634a29b60 100644 --- a/superset-frontend/src/views/menu.tsx +++ b/superset-frontend/src/views/menu.tsx @@ -26,6 +26,7 @@ import createCache from '@emotion/cache'; import { ThemeProvider } from '@superset-ui/core'; import Menu from 'src/views/components/Menu'; import { theme } from 'src/preamble'; +import getBootstrapData from 'src/utils/getBootstrapData'; import { Provider } from 'react-redux'; import { setupStore } from './store'; @@ -33,10 +34,8 @@ import { setupStore } from './store'; // Disable connecting to redux debugger so that the React app injected // Below the menu like SqlLab or Explore can connect its redux store to the debugger const store = setupStore(true); -const container = document.getElementById('app'); -const bootstrapJson = container?.getAttribute('data-bootstrap') ?? '{}'; -const bootstrap = JSON.parse(bootstrapJson); -const menu = { ...bootstrap.common.menu_data }; +const bootstrapData = getBootstrapData(); +const menu = { ...bootstrapData.common.menu_data }; const emotionCache = createCache({ key: 'menu', diff --git a/superset-frontend/src/views/store.ts b/superset-frontend/src/views/store.ts index 251feeec515b..a143deb7379f 100644 --- a/superset-frontend/src/views/store.ts +++ b/superset-frontend/src/views/store.ts @@ -48,10 +48,12 @@ import { import shortid from 'shortid'; import { BootstrapUser, + UndefinedUser, UserWithPermissionsAndRoles, } from 'src/types/bootstrapTypes'; import { AnyDatasourcesAction } from 'src/explore/actions/datasourcesActions'; import { HydrateExplore } from 'src/explore/actions/hydrateExplore'; +import getBootstrapData from 'src/utils/getBootstrapData'; import { Dataset } from '@superset-ui/chart-controls'; // Some reducers don't do anything, and redux is just used to reference the initial "state". @@ -61,8 +63,7 @@ const noopReducer = (state: STATE = initialState) => state; -const container = document.getElementById('app'); -const bootstrap = JSON.parse(container?.getAttribute('data-bootstrap') ?? '{}'); +const bootstrapData = getBootstrapData(); export const USER_LOADED = 'USER_LOADED'; @@ -72,9 +73,9 @@ export type UserLoadedAction = { }; const userReducer = ( - user: BootstrapUser = bootstrap.user || {}, + user = bootstrapData.user || {}, action: UserLoadedAction, -): BootstrapUser => { +): BootstrapUser | UndefinedUser => { if (action.type === USER_LOADED) { return action.user; } @@ -104,7 +105,7 @@ const CombinedDatasourceReducers = ( // exported for tests export const rootReducer = combineReducers({ messageToasts: messageToastReducer, - common: noopReducer(bootstrap.common || {}), + common: noopReducer(bootstrapData.common), user: userReducer, impressionId: noopReducer(shortid.generate()), charts, diff --git a/superset/views/base.py b/superset/views/base.py index 74a2e2a2d7e6..d16baecc735c 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -115,6 +115,7 @@ "HTML_SANITIZATION", "HTML_SANITIZATION_SCHEMA_EXTENSIONS", "WELCOME_PAGE_LAST_TAB", + "VIZ_TYPE_DENYLIST", ) logger = logging.getLogger(__name__) From 0b22287ad9f3908ce62f51e2a17de8975beafed2 Mon Sep 17 00:00:00 2001 From: Cemre Mengu Date: Tue, 10 Jan 2023 21:52:54 +0300 Subject: [PATCH 05/15] feat: make CTA text in Alerts & Reports mails configurable (#19779) --- superset/config.py | 3 +++ superset/reports/notifications/email.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/superset/config.py b/superset/config.py index a3fc3df00ef5..64ff09a5c838 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1259,6 +1259,9 @@ def EMAIL_HEADER_MUTATOR( # pylint: disable=invalid-name,unused-argument # A custom prefix to use on all Alerts & Reports emails EMAIL_REPORTS_SUBJECT_PREFIX = "[Report] " +# The text for call-to-action link in Alerts & Reports emails +EMAIL_REPORTS_CTA = "Explore in Superset" + # Slack API token for the superset reports, either string or callable SLACK_API_TOKEN: Optional[Union[Callable[[], str], str]] = None SLACK_PROXY = None diff --git a/superset/reports/notifications/email.py b/superset/reports/notifications/email.py index f8b38cc3eae2..4b061c8ef6b6 100644 --- a/superset/reports/notifications/email.py +++ b/superset/reports/notifications/email.py @@ -128,7 +128,7 @@ def _get_content(self) -> EmailContent: else: html_table = "" - call_to_action = __("Explore in Superset") + call_to_action = __(app.config["EMAIL_REPORTS_CTA"]) url = ( modify_url_query(self._content.url, standalone="0") if self._content.url is not None From 73e53fab7a5141881711a0269740627fd0527d30 Mon Sep 17 00:00:00 2001 From: Ville Brofeldt <33317356+villebro@users.noreply.github.com> Date: Tue, 10 Jan 2023 22:14:51 +0200 Subject: [PATCH 06/15] fix(bootstrap-data): always check flashes (#22659) --- superset/views/base.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/superset/views/base.py b/superset/views/base.py index d16baecc735c..41a27b4139e9 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -388,13 +388,12 @@ def menu_data(user: User) -> Dict[str, Any]: @cache_manager.cache.memoize(timeout=60) -def common_bootstrap_payload(user: User) -> Dict[str, Any]: +def cached_common_bootstrap_data(user: User) -> Dict[str, Any]: """Common data always sent to the client - The function is memoized as the return value only changes based - on configuration and feature flag values. + The function is memoized as the return value only changes when user permissions + or configuration values change. """ - messages = get_flashed_messages(with_categories=True) locale = str(get_locale()) # should not expose API TOKEN to frontend @@ -418,7 +417,6 @@ def common_bootstrap_payload(user: User) -> Dict[str, Any]: frontend_config["HAS_GSHEETS_INSTALLED"] = bool(available_specs[GSheetsEngineSpec]) bootstrap_data = { - "flash_messages": messages, "conf": frontend_config, "locale": locale, "language_pack": get_language_pack(locale), @@ -432,6 +430,13 @@ def common_bootstrap_payload(user: User) -> Dict[str, Any]: return bootstrap_data +def common_bootstrap_payload(user: User) -> Dict[str, Any]: + return { + **(cached_common_bootstrap_data(user)), + "flash_messages": get_flashed_messages(with_categories=True), + } + + def get_error_level_from_status_code( # pylint: disable=invalid-name status: int, ) -> ErrorLevel: From c0aeb2a75a06694402be0f15d1eb64631d3b9ef9 Mon Sep 17 00:00:00 2001 From: Ville Brofeldt <33317356+villebro@users.noreply.github.com> Date: Wed, 11 Jan 2023 11:42:46 +0200 Subject: [PATCH 07/15] chore(embedded): bump package versions (#22676) --- superset-embedded-sdk/package.json | 2 +- superset-frontend/packages/superset-ui-switchboard/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/superset-embedded-sdk/package.json b/superset-embedded-sdk/package.json index fea75d7af58c..055f44191a7e 100644 --- a/superset-embedded-sdk/package.json +++ b/superset-embedded-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@superset-ui/embedded-sdk", - "version": "0.1.0-alpha.7", + "version": "0.1.0-alpha.8", "description": "SDK for embedding resources from Superset into your own application", "access": "public", "keywords": [ diff --git a/superset-frontend/packages/superset-ui-switchboard/package.json b/superset-frontend/packages/superset-ui-switchboard/package.json index f7e6c69a1b50..1e49d7eb9cde 100644 --- a/superset-frontend/packages/superset-ui-switchboard/package.json +++ b/superset-frontend/packages/superset-ui-switchboard/package.json @@ -1,6 +1,6 @@ { "name": "@superset-ui/switchboard", - "version": "0.18.26-0", + "version": "0.18.26-1", "description": "Switchboard is a library to make it easier to communicate across browser windows using the MessageChannel API", "sideEffects": false, "main": "lib/index.js", From 8f98c469fd16caa74ba4d9bf0b6a0f0e5ded7f1c Mon Sep 17 00:00:00 2001 From: SamraHanifCareem <106157603+SamraHanifCareem@users.noreply.github.com> Date: Wed, 11 Jan 2023 18:10:15 +0500 Subject: [PATCH 08/15] docs: Add Careem to the user's list (#22669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: “SamraHanifCareem” <“samra.hanif@careem.com”> --- RESOURCES/INTHEWILD.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RESOURCES/INTHEWILD.md b/RESOURCES/INTHEWILD.md index 39ea878bd57a..deb6e91e478d 100644 --- a/RESOURCES/INTHEWILD.md +++ b/RESOURCES/INTHEWILD.md @@ -72,6 +72,7 @@ Join our growing community! - [Apollo GraphQL](https://www.apollographql.com/) [@evans] - [Astronomer](https://www.astronomer.io) [@ryw] - [Avesta Technologies](https://avestatechnologies.com/) [@TheRum] +- [Careem](https://www.careem.com/) [@SamraHanifCareem] - [Cloudsmith](https://cloudsmith.io) [@alancarson] - [CnOvit](http://www.cnovit.com/) [@xieshaohu] - [Deepomatic](https://deepomatic.com/) [@Zanoellia] From 1fe0290a60773f958fd65857cbe853d99617e218 Mon Sep 17 00:00:00 2001 From: "Byungjin Park (Claud)" Date: Thu, 12 Jan 2023 01:20:51 +0900 Subject: [PATCH 09/15] chore: Add KarrotPay in INTHEWILD.md (#22666) --- RESOURCES/INTHEWILD.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RESOURCES/INTHEWILD.md b/RESOURCES/INTHEWILD.md index deb6e91e478d..193899b814d6 100644 --- a/RESOURCES/INTHEWILD.md +++ b/RESOURCES/INTHEWILD.md @@ -40,6 +40,7 @@ Join our growing community! - [Cape Crypto](https://capecrypto.com) - [Capital Service S.A.](http://capitalservice.pl) [@pkonarzewski] - [Clark.de](http://clark.de/) +- [KarrotPay](https://www.daangnpay.com/) - [Wise](https://wise.com) [@koszti] - [Xendit](http://xendit.co/) [@LieAlbertTriAdrian] From 44c9cf4de52e764b2d3c5e875bee517635f1ba1c Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Wed, 11 Jan 2023 13:22:41 -0300 Subject: [PATCH 10/15] chore: Migrate /superset/search_queries to API v1 (#22579) --- .../QuerySearch/QuerySearch.test.jsx | 139 --------- .../SqlLab/components/QuerySearch/index.tsx | 289 ------------------ .../translations/de/LC_MESSAGES/messages.po | 30 -- .../translations/en/LC_MESSAGES/messages.po | 30 -- .../translations/es/LC_MESSAGES/messages.po | 30 -- .../translations/fr/LC_MESSAGES/messages.po | 30 -- .../translations/it/LC_MESSAGES/messages.po | 30 -- .../translations/ja/LC_MESSAGES/messages.po | 30 -- .../translations/ko/LC_MESSAGES/messages.po | 30 -- superset/translations/messages.pot | 30 -- .../translations/nl/LC_MESSAGES/messages.po | 30 -- .../translations/pt/LC_MESSAGES/message.po | 30 -- .../pt_BR/LC_MESSAGES/messages.po | 30 -- .../translations/ru/LC_MESSAGES/messages.po | 30 -- .../translations/sk/LC_MESSAGES/messages.po | 30 -- .../translations/sl/LC_MESSAGES/messages.po | 33 -- .../translations/zh/LC_MESSAGES/messages.po | 30 -- superset/views/core.py | 1 + 18 files changed, 1 insertion(+), 881 deletions(-) delete mode 100644 superset-frontend/src/SqlLab/components/QuerySearch/QuerySearch.test.jsx delete mode 100644 superset-frontend/src/SqlLab/components/QuerySearch/index.tsx diff --git a/superset-frontend/src/SqlLab/components/QuerySearch/QuerySearch.test.jsx b/superset-frontend/src/SqlLab/components/QuerySearch/QuerySearch.test.jsx deleted file mode 100644 index 2a891d34af84..000000000000 --- a/superset-frontend/src/SqlLab/components/QuerySearch/QuerySearch.test.jsx +++ /dev/null @@ -1,139 +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 thunk from 'redux-thunk'; -import configureStore from 'redux-mock-store'; -import fetchMock from 'fetch-mock'; -import QuerySearch from 'src/SqlLab/components/QuerySearch'; -import { Provider } from 'react-redux'; -import { supersetTheme, ThemeProvider } from '@superset-ui/core'; -import { fireEvent, render, screen, act } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; -import userEvent from '@testing-library/user-event'; -import { user } from 'src/SqlLab/fixtures'; - -const mockStore = configureStore([thunk]); -const store = mockStore({ - sqlLab: user, -}); - -const SEARCH_ENDPOINT = 'glob:*/superset/search_queries?*'; -const USER_ENDPOINT = 'glob:*/api/v1/query/related/user'; -const DATABASE_ENDPOINT = 'glob:*/api/v1/database/?*'; - -fetchMock.get(SEARCH_ENDPOINT, []); -fetchMock.get(USER_ENDPOINT, []); -fetchMock.get(DATABASE_ENDPOINT, []); - -describe('QuerySearch', () => { - const mockedProps = { - displayLimit: 50, - }; - - it('is valid', () => { - expect( - React.isValidElement( - - - - - , - ), - ).toBe(true); - }); - - beforeEach(async () => { - // You need this await function in order to change state in the app. In fact you need it everytime you re-render. - await act(async () => { - render( - - - - - , - ); - }); - }); - - it('should have three Selects', () => { - expect(screen.getByText(/28 days ago/i)).toBeInTheDocument(); - expect(screen.getByText(/now/i)).toBeInTheDocument(); - expect(screen.getByText(/success/i)).toBeInTheDocument(); - }); - - it('updates fromTime on user selects from time', () => { - const role = screen.getByText(/28 days ago/i); - fireEvent.keyDown(role, { key: 'ArrowDown', keyCode: 40 }); - userEvent.click(screen.getByText(/1 hour ago/i)); - expect(screen.getByText(/1 hour ago/i)).toBeInTheDocument(); - }); - - it('updates toTime on user selects on time', () => { - const role = screen.getByText(/now/i); - fireEvent.keyDown(role, { key: 'ArrowDown', keyCode: 40 }); - userEvent.click(screen.getByText(/1 hour ago/i)); - expect(screen.getByText(/1 hour ago/i)).toBeInTheDocument(); - }); - - it('updates status on user selects status', () => { - const role = screen.getByText(/success/i); - fireEvent.keyDown(role, { key: 'ArrowDown', keyCode: 40 }); - userEvent.click(screen.getByText(/failed/i)); - expect(screen.getByText(/failed/i)).toBeInTheDocument(); - }); - - it('should have one input for searchText', () => { - expect( - screen.getByPlaceholderText(/Query search string/i), - ).toBeInTheDocument(); - }); - - it('updates search text on user inputs search text', () => { - const search = screen.getByPlaceholderText(/Query search string/i); - userEvent.type(search, 'text'); - expect(search.value).toBe('text'); - }); - - it('should have one Button', () => { - const button = screen.getAllByRole('button'); - expect(button.length).toEqual(1); - }); - - it('should call API when search button is pressed', async () => { - fetchMock.resetHistory(); - const button = screen.getByRole('button'); - await act(async () => { - userEvent.click(button); - }); - expect(fetchMock.calls(SEARCH_ENDPOINT)).toHaveLength(1); - }); - - it('should call API when (only)enter key is pressed', async () => { - fetchMock.resetHistory(); - const search = screen.getByPlaceholderText(/Query search string/i); - await act(async () => { - userEvent.type(search, 'a'); - }); - expect(fetchMock.calls(SEARCH_ENDPOINT)).toHaveLength(0); - await act(async () => { - userEvent.type(search, '{enter}'); - }); - expect(fetchMock.calls(SEARCH_ENDPOINT)).toHaveLength(1); - }); -}); diff --git a/superset-frontend/src/SqlLab/components/QuerySearch/index.tsx b/superset-frontend/src/SqlLab/components/QuerySearch/index.tsx deleted file mode 100644 index 3018ff192458..000000000000 --- a/superset-frontend/src/SqlLab/components/QuerySearch/index.tsx +++ /dev/null @@ -1,289 +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, { useState, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; - -import { setDatabases, addDangerToast } from 'src/SqlLab/actions/sqlLab'; -import Button from 'src/components/Button'; -import Select from 'src/components/DeprecatedSelect'; -import { styled, t, SupersetClient, QueryResponse } from '@superset-ui/core'; -import { debounce } from 'lodash'; -import Loading from 'src/components/Loading'; -import { - now, - epochTimeXHoursAgo, - epochTimeXDaysAgo, - epochTimeXYearsAgo, -} from 'src/utils/dates'; -import AsyncSelect from 'src/components/AsyncSelect'; -import { STATUS_OPTIONS, TIME_OPTIONS } from 'src/SqlLab/constants'; -import QueryTable from '../QueryTable'; - -interface QuerySearchProps { - displayLimit: number; -} - -interface UserMutatorProps { - value: number; - text: string; -} - -interface DbMutatorProps { - id: number; - database_name: string; -} - -const TableWrapper = styled.div` - display: flex; - flex-direction: column; - flex: 1; - height: 100%; -`; - -const TableStyles = styled.div` - table { - background-color: ${({ theme }) => theme.colors.grayscale.light4}; - } - - .table > thead > tr > th { - border-bottom: ${({ theme }) => theme.gridUnit / 2}px solid - ${({ theme }) => theme.colors.grayscale.light2}; - background: ${({ theme }) => theme.colors.grayscale.light4}; - } -`; - -const StyledTableStylesContainer = styled.div` - overflow: auto; -`; - -const QuerySearch = ({ displayLimit }: QuerySearchProps) => { - const dispatch = useDispatch(); - - const [databaseId, setDatabaseId] = useState(''); - const [userId, setUserId] = useState(''); - const [searchText, setSearchText] = useState(''); - const [from, setFrom] = useState('28 days ago'); - const [to, setTo] = useState('now'); - const [status, setStatus] = useState('success'); - const [queriesArray, setQueriesArray] = useState([]); - const [queriesLoading, setQueriesLoading] = useState(true); - - const getTimeFromSelection = (selection: string) => { - switch (selection) { - case 'now': - return now(); - case '1 hour ago': - return epochTimeXHoursAgo(1); - case '1 day ago': - return epochTimeXDaysAgo(1); - case '7 days ago': - return epochTimeXDaysAgo(7); - case '28 days ago': - return epochTimeXDaysAgo(28); - case '90 days ago': - return epochTimeXDaysAgo(90); - case '1 year ago': - return epochTimeXYearsAgo(1); - default: - return null; - } - }; - - const insertParams = (baseUrl: string, params: string[]) => { - const validParams = params.filter(function (p) { - return p !== ''; - }); - return `${baseUrl}?${validParams.join('&')}`; - }; - - const refreshQueries = async () => { - setQueriesLoading(true); - const params = [ - userId && `user_id=${userId}`, - databaseId && `database_id=${databaseId}`, - searchText && `search_text=${searchText}`, - status && `status=${status}`, - from && `from=${getTimeFromSelection(from)}`, - to && `to=${getTimeFromSelection(to)}`, - ]; - - try { - const response = await SupersetClient.get({ - endpoint: insertParams('/superset/search_queries', params), - }); - const queries = Object.values(response.json); - setQueriesArray(queries); - } catch (err) { - dispatch(addDangerToast(t('An error occurred when refreshing queries'))); - } finally { - setQueriesLoading(false); - } - }; - useEffect(() => { - refreshQueries(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const onUserClicked = (userId: string) => { - setUserId(userId); - refreshQueries(); - }; - - const onDbClicked = (dbId: string) => { - setDatabaseId(dbId); - refreshQueries(); - }; - - const onKeyDown = (event: React.KeyboardEvent) => { - if (event.keyCode === 13) { - refreshQueries(); - } - }; - - const onChange = (e: React.ChangeEvent) => { - e.persist(); - const handleChange = debounce(e => { - setSearchText(e.target.value); - }, 200); - handleChange(e); - }; - - const userMutator = ({ result }: { result: UserMutatorProps[] }) => - result.map(({ value, text }: UserMutatorProps) => ({ - label: text, - value, - })); - - const dbMutator = ({ result }: { result: DbMutatorProps[] }) => { - const options = result.map(({ id, database_name }: DbMutatorProps) => ({ - value: id, - label: database_name, - })); - dispatch(setDatabases(result)); - if (result.length === 0) { - dispatch( - addDangerToast(t("It seems you don't have access to any database")), - ); - } - return options; - }; - - return ( - -
-
- setUserId(selected?.value)} - placeholder={t('Filter by user')} - /> -
-
- setDatabaseId(db?.value)} - dataEndpoint="/api/v1/database/?q=(filters:!((col:expose_in_sqllab,opr:eq,value:!t)))" - value={databaseId} - mutator={dbMutator} - placeholder={t('Filter by database')} - /> -
-
- -
-
- ({ value: xt, label: xt }))} - value={{ value: to, label: to }} - autosize={false} - onChange={(selected: any) => setTo(selected?.value)} - /> - - , )} {this.formRow( - 'Tooltip', - 'Column header tooltip', + t('Tooltip'), + t('Column header tooltip'), 'col-tooltip', , )} {this.formRow( - 'Type', - 'Type of comparison, value difference or percentage', + t('Type'), + t('Type of comparison, value difference or percentage'), 'col-type', , )} {this.state.colType === 'spark' && this.formRow( - 'Height', - 'Height of the sparkline', + t('Height'), + t('Height of the sparkline'), 'spark-width', , )} {['time', 'avg'].indexOf(this.state.colType) >= 0 && this.formRow( - 'Time lag', - 'Number of periods to compare against', + t('Time lag'), + t('Number of periods to compare against'), 'time-lag', , )} {['spark'].indexOf(this.state.colType) >= 0 && this.formRow( - 'Time ratio', - 'Number of periods to ratio against', + t('Time ratio'), + t('Number of periods to ratio against'), 'time-ratio', , )} {this.state.colType === 'time' && this.formRow( - 'Type', - 'Type of comparison, value difference or percentage', + t('Type'), + t('Type of comparison, value difference or percentage'), 'comp-type', , )} {this.state.colType === 'spark' && this.formRow( - 'Date format', - 'Optional d3 date format string', + t('Date format'), + t('Optional d3 date format string'), 'date-format', , )} @@ -356,7 +358,7 @@ export default class TimeSeriesColumnControl extends React.Component { diff --git a/superset-frontend/src/explore/components/controls/ViewportControl.jsx b/superset-frontend/src/explore/components/controls/ViewportControl.jsx index 631a8c550878..ff9ebef2042e 100644 --- a/superset-frontend/src/explore/components/controls/ViewportControl.jsx +++ b/superset-frontend/src/explore/components/controls/ViewportControl.jsx @@ -17,6 +17,7 @@ * under the License. */ import React from 'react'; +import { t } from '@superset-ui/core'; import PropTypes from 'prop-types'; import Popover from 'src/components/Popover'; import { decimal2sexagesimal } from 'geolib'; @@ -107,7 +108,7 @@ export default class ViewportControl extends React.Component { trigger="click" placement="right" content={this.renderPopover()} - title="Viewport" + title={t('Viewport')} > diff --git a/superset-frontend/src/explore/controlPanels/sections.tsx b/superset-frontend/src/explore/controlPanels/sections.tsx index 4b0d641274fe..fe384c6f39ce 100644 --- a/superset-frontend/src/explore/controlPanels/sections.tsx +++ b/superset-frontend/src/explore/controlPanels/sections.tsx @@ -19,7 +19,6 @@ import React from 'react'; import { t } from '@superset-ui/core'; import { ControlPanelSectionConfig } from '@superset-ui/chart-controls'; -import { formatSelectOptions } from 'src/explore/exploreUtils'; export const datasourceAndVizType: ControlPanelSectionConfig = { controlSetRows: [ @@ -81,7 +80,7 @@ export const annotations: ControlPanelSectionConfig = { type: 'AnnotationLayerControl', label: '', default: [], - description: 'Annotation layers', + description: t('Annotation layers'), renderTrigger: true, tabOverride: 'data', }, @@ -131,13 +130,13 @@ export const NVD3TimeSeries: ControlPanelSectionConfig[] = [ type: 'SelectControl', label: t('Rolling function'), default: 'None', - choices: formatSelectOptions([ - 'None', - 'mean', - 'sum', - 'std', - 'cumsum', - ]), + choices: [ + ['None', t('None')], + ['mean', t('mean')], + ['sum', t('sum')], + ['std', t('std')], + ['cumsum', t('cumsum')], + ], description: t( 'Defines a rolling window function to apply, works along ' + 'with the [Periods] text box', @@ -181,20 +180,18 @@ export const NVD3TimeSeries: ControlPanelSectionConfig[] = [ multi: true, freeForm: true, label: t('Time shift'), - choices: formatSelectOptions([ - '1 day', - '1 week', - '28 days', - '30 days', - '52 weeks', - '1 year', - '104 weeks', - '2 years', - '156 weeks', - '3 years', - '208 weeks', - '4 years', - ]), + choices: [ + ['1 day', t('1 day')], + ['1 week', t('1 week')], + ['28 days', t('28 days')], + ['30 days', t('30 days')], + ['52 weeks', t('52 weeks')], + ['1 year', t('1 year')], + ['104 weeks', t('104 weeks')], + ['2 years', t('2 years')], + ['156 weeks', t('156 weeks')], + ['3 years', t('3 years')], + ], description: t( 'Overlay one or more timeseries from a ' + 'relative time period. Expects relative time deltas ' + @@ -210,10 +207,10 @@ export const NVD3TimeSeries: ControlPanelSectionConfig[] = [ label: t('Calculation type'), default: 'values', choices: [ - ['values', 'Actual values'], - ['absolute', 'Difference'], - ['percentage', 'Percentage change'], - ['ratio', 'Ratio'], + ['values', t('Actual values')], + ['absolute', t('Difference')], + ['percentage', t('Percentage change')], + ['ratio', t('Ratio')], ], description: t( 'How to display time shifts: as individual lines; as the ' + @@ -232,7 +229,14 @@ export const NVD3TimeSeries: ControlPanelSectionConfig[] = [ freeForm: true, label: t('Rule'), default: null, - choices: formatSelectOptions(['1T', '1H', '1D', '7D', '1M', '1AS']), + choices: [ + ['1T', t('1T')], + ['1H', t('1H')], + ['1D', t('1D')], + ['7D', t('7D')], + ['1M', t('1M')], + ['1AS', t('1AS')], + ], description: t('Pandas resample rule'), }, }, @@ -243,14 +247,14 @@ export const NVD3TimeSeries: ControlPanelSectionConfig[] = [ freeForm: true, label: t('Method'), default: null, - choices: formatSelectOptions([ - 'asfreq', - 'bfill', - 'ffill', - 'median', - 'mean', - 'sum', - ]), + choices: [ + ['asfreq', t('asfreq')], + ['bfill', t('bfill')], + ['ffill', t('ffill')], + ['median', t('median')], + ['mean', t('mean')], + ['sum', t('sum')], + ], description: t('Pandas resample method'), }, }, diff --git a/superset-frontend/src/explore/controls.jsx b/superset-frontend/src/explore/controls.jsx index 6b325b27c968..8cacba4890c4 100644 --- a/superset-frontend/src/explore/controls.jsx +++ b/superset-frontend/src/explore/controls.jsx @@ -75,8 +75,8 @@ export const PRIMARY_COLOR = { r: 0, g: 122, b: 135, a: 1 }; // input choices & options export const D3_FORMAT_OPTIONS = [ - ['SMART_NUMBER', 'Adaptive formatting'], - ['~g', 'Original value'], + ['SMART_NUMBER', t('Adaptive formatting')], + ['~g', t('Original value')], [',d', ',d (12345.432 => 12,345)'], ['.1s', '.1s (12345.432 => 10k)'], ['.3s', '.3s (12345.432 => 12.3k)'], @@ -86,8 +86,8 @@ export const D3_FORMAT_OPTIONS = [ [',.3f', ',.3f (12345.432 => 12,345.432)'], ['+,', '+, (12345.432 => +12,345.432)'], ['$,.2f', '$,.2f (12345.432 => $12,345.43)'], - ['DURATION', 'Duration in ms (66000 => 1m 6s)'], - ['DURATION_SUB', 'Duration in ms (100.40008 => 100ms 400µs 80ns)'], + ['DURATION', t('Duration in ms (66000 => 1m 6s)')], + ['DURATION_SUB', t('Duration in ms (100.40008 => 100ms 400µs 80ns)')], ]; const ROW_LIMIT_OPTIONS = [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000]; @@ -98,7 +98,7 @@ export const D3_FORMAT_DOCS = 'D3 format syntax: https://github.com/d3/d3-format'; export const D3_TIME_FORMAT_OPTIONS = [ - ['smart_date', 'Adaptive formatting'], + ['smart_date', t('Adaptive formatting')], ['%d/%m/%Y', '%d/%m/%Y | 14/01/2019'], ['%m/%d/%Y', '%m/%d/%Y | 01/14/2019'], ['%Y-%m-%d', '%Y-%m-%d | 2019-01-14'], @@ -251,22 +251,22 @@ export const controls = { label: TIME_FILTER_LABELS.granularity, default: 'P1D', choices: [ - [null, 'all'], - ['PT5S', '5 seconds'], - ['PT30S', '30 seconds'], - ['PT1M', '1 minute'], - ['PT5M', '5 minutes'], - ['PT30M', '30 minutes'], - ['PT1H', '1 hour'], - ['PT6H', '6 hour'], - ['P1D', '1 day'], - ['P7D', '7 days'], - ['P1W', 'week'], - ['week_starting_sunday', 'week starting Sunday'], - ['week_ending_saturday', 'week ending Saturday'], - ['P1M', 'month'], - ['P3M', 'quarter'], - ['P1Y', 'year'], + [null, t('all')], + ['PT5S', t('5 seconds')], + ['PT30S', t('30 seconds')], + ['PT1M', t('1 minute')], + ['PT5M', t('5 minutes')], + ['PT30M', t('30 minutes')], + ['PT1H', t('1 hour')], + ['PT6H', t('6 hour')], + ['P1D', t('1 day')], + ['P7D', t('7 days')], + ['P1W', t('week')], + ['week_starting_sunday', t('week starting Sunday')], + ['week_ending_saturday', t('week ending Saturday')], + ['P1M', t('month')], + ['P3M', t('quarter')], + ['P1Y', t('year')], ], description: t( 'The time granularity for the visualization. Note that you ' + diff --git a/superset-frontend/src/explore/fixtures.tsx b/superset-frontend/src/explore/fixtures.tsx index d7c1276247d5..351e6f26b126 100644 --- a/superset-frontend/src/explore/fixtures.tsx +++ b/superset-frontend/src/explore/fixtures.tsx @@ -58,9 +58,9 @@ export const controlPanelSectionsChartOptions: (ControlPanelSectionConfig | null label: t('Stacked Style'), renderTrigger: true, choices: [ - ['stack', 'stack'], - ['stream', 'stream'], - ['expand', 'expand'], + ['stack', t('stack')], + ['stream', t('stream')], + ['expand', t('expand')], ], default: 'stack', description: '', diff --git a/superset-frontend/src/filters/components/GroupBy/controlPanel.ts b/superset-frontend/src/filters/components/GroupBy/controlPanel.ts index fbc61bf6e4a3..5b26f9cdcfaa 100644 --- a/superset-frontend/src/filters/components/GroupBy/controlPanel.ts +++ b/superset-frontend/src/filters/components/GroupBy/controlPanel.ts @@ -39,7 +39,7 @@ const config: ControlPanelConfig = { name: 'groupby', config: { ...sharedControls.groupby, - label: 'Columns to show', + label: t('Columns to show'), multiple: true, required: false, }, diff --git a/superset-frontend/src/filters/components/Range/buildQuery.ts b/superset-frontend/src/filters/components/Range/buildQuery.ts index 8a76e5a28c16..72e2fa549247 100644 --- a/superset-frontend/src/filters/components/Range/buildQuery.ts +++ b/superset-frontend/src/filters/components/Range/buildQuery.ts @@ -20,6 +20,7 @@ import { buildQueryContext, GenericDataType, QueryFormData, + t, } from '@superset-ui/core'; /** @@ -54,7 +55,7 @@ export default function buildQuery(formData: QueryFormData) { }, expressionType: 'SIMPLE', hasCustomLabel: true, - label: 'min', + label: t('min'), }, { aggregate: 'MAX', @@ -65,7 +66,7 @@ export default function buildQuery(formData: QueryFormData) { }, expressionType: 'SIMPLE', hasCustomLabel: true, - label: 'max', + label: t('max'), }, ], }, diff --git a/superset-frontend/src/filters/components/Range/controlPanel.ts b/superset-frontend/src/filters/components/Range/controlPanel.ts index 076d5a44aa0d..2a38f5733cb1 100644 --- a/superset-frontend/src/filters/components/Range/controlPanel.ts +++ b/superset-frontend/src/filters/components/Range/controlPanel.ts @@ -36,7 +36,7 @@ const config: ControlPanelConfig = { name: 'groupby', config: { ...sharedControls.groupby, - label: 'Column', + label: t('Column'), required: true, }, }, diff --git a/superset-frontend/src/filters/components/Select/controlPanel.ts b/superset-frontend/src/filters/components/Select/controlPanel.ts index 6b1549d5359c..12bc36a7ade4 100644 --- a/superset-frontend/src/filters/components/Select/controlPanel.ts +++ b/superset-frontend/src/filters/components/Select/controlPanel.ts @@ -46,7 +46,7 @@ const config: ControlPanelConfig = { name: 'groupby', config: { ...sharedControls.groupby, - label: 'Column', + label: t('Column'), required: true, }, }, diff --git a/superset-frontend/src/filters/components/Time/controlPanel.ts b/superset-frontend/src/filters/components/Time/controlPanel.ts index b54909f70b96..2dbded365160 100644 --- a/superset-frontend/src/filters/components/Time/controlPanel.ts +++ b/superset-frontend/src/filters/components/Time/controlPanel.ts @@ -34,7 +34,7 @@ const config: ControlPanelConfig = { name: 'groupby', config: { ...sharedControls.groupby, - label: 'Column', + label: t('Column'), required: true, }, }, diff --git a/superset-frontend/src/profile/components/UserInfo.tsx b/superset-frontend/src/profile/components/UserInfo.tsx index 6b44e35caeaf..b79f1fb97cb6 100644 --- a/superset-frontend/src/profile/components/UserInfo.tsx +++ b/superset-frontend/src/profile/components/UserInfo.tsx @@ -73,7 +73,7 @@ export default function UserInfo({ user }: UserInfoProps) {

  - {t('id:')}  + {t('id')}:  {user?.userId}

diff --git a/superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx b/superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx index 37f3e7c2add6..85e248c470bc 100644 --- a/superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx +++ b/superset-frontend/src/views/CRUD/alert/ExecutionLog.tsx @@ -168,7 +168,7 @@ function ExecutionLog({ addDangerToast, isReportEnabled }: ExecutionLogProps) { {alertResource?.type} {alertResource?.name} - Back to all + {t('Back to all')} } diff --git a/superset-frontend/src/views/CRUD/alert/components/AlertReportCronScheduler.tsx b/superset-frontend/src/views/CRUD/alert/components/AlertReportCronScheduler.tsx index 5418842aeaaa..9a7fa6214b8e 100644 --- a/superset-frontend/src/views/CRUD/alert/components/AlertReportCronScheduler.tsx +++ b/superset-frontend/src/views/CRUD/alert/components/AlertReportCronScheduler.tsx @@ -80,7 +80,7 @@ export const AlertReportCronScheduler: React.FC =
- CRON Schedule + {t('CRON Schedule')} {t('Annotation Layer %s', annotationLayerName)} {hasHistory ? ( - Back to all + {t('Back to all')} ) : ( - Back to all + {t('Back to all')} )} diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx index d426cf4cdfe7..be60ac0308d7 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx @@ -66,7 +66,7 @@ export const portField = ({ errorMessage={validationErrors?.port} placeholder={t('e.g. 5432')} className="form-group-w-50" - label="Port" + label={t('Port')} onChange={changeMethods.onParametersChange} /> diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx index 82eeea05a77c..6077c288a598 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx @@ -70,9 +70,9 @@ const ExtraOptions = ({ -

SQL Lab

+

{t('SQL Lab')}

- Adjust how this database will interact with SQL Lab. + {t('Adjust how this database will interact with SQL Lab.')}

} @@ -216,7 +216,7 @@ const ExtraOptions = ({ -

Performance

+

{t('Performance')}

Adjust performance settings of this database.

@@ -326,8 +326,8 @@ const ExtraOptions = ({ -

Security

-

Add extra connection information.

+

{t('Security')}

+

{t('Add extra connection information.')}

} key="3" @@ -440,8 +440,8 @@ const ExtraOptions = ({ -

Other

-

Additional settings.

+

{t('Other')}

+

{t('Additional settings.')}

} key="4" diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ModalHeader.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ModalHeader.tsx index 6947ea2808b0..9825489a7b80 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ModalHeader.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ModalHeader.tsx @@ -89,16 +89,21 @@ const ModalHeader = ({ const useSqlAlchemyFormHeader = ( -

STEP 2 OF 2

-

Enter Primary Credentials

+

+ {t('STEP %(stepCurr)s OF %(stepLast)s', { + stepCurr: 2, + stepLast: 2, + })} +

+

{t('Enter Primary Credentials')}

- Need help? Learn how to connect your database{' '} + {t('Need help? Learn how to connect your database')}{' '} - here + {t('here')} .

@@ -108,8 +113,13 @@ const ModalHeader = ({ const hasConnectedDbHeader = ( -

STEP 3 OF 3

-

Database connected

+

+ {t('STEP %(stepCurr)s OF %(stepLast)s', { + stepCurr: 3, + stepLast: 3, + })} +

+

{t('Database connected')}

{t(`Create a dataset to begin visualizing your data as a chart or go to SQL Lab to query your data.`)} @@ -121,16 +131,26 @@ const ModalHeader = ({ const hasDbHeader = ( -

STEP 2 OF 3

-

Enter the required {dbModel.name} credentials

+

+ {t('STEP %(stepCurr)s OF %(stepLast)s', { + stepCurr: 2, + stepLast: 3, + })} +

+

+ {t('Enter the required %(dbModelName)s credentials', { + dbModelName: dbModel.name, + })} +

- Need help? Learn more about{' '} + {t('Need help? Learn more about')}{' '} - connecting to {dbModel.name}. + {t('connecting to %(dbModelName)s.', { dbModelName: dbModel.name })} + .

@@ -140,7 +160,12 @@ const ModalHeader = ({ const noDbHeader = (
-

STEP 1 OF 3

+

+ {t('STEP %(stepCurr)s OF %(stepLast)s', { + stepCurr: 1, + stepLast: 3, + })} +

{t('Select a database to connect')}

@@ -149,8 +174,17 @@ const ModalHeader = ({ const importDbHeader = ( -

STEP 2 OF 2

-

Enter the required {dbModel.name} credentials

+

+ {t('STEP %(stepCurr)s OF %(stepLast)s', { + stepCurr: 2, + stepLast: 2, + })} +

+

+ {t('Enter the required %(dbModelName)s credentials', { + dbModelName: dbModel.name, + })} +

{fileCheck ? fileList[0].name : ''}

diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Footer/index.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Footer/index.tsx index 8fa4d6976fb4..5aecec0bb892 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Footer/index.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Footer/index.tsx @@ -118,7 +118,7 @@ function Footer({ return ( <> - + - - -{% endif %} -{% if documentation_url %} -
  • - - {% if documentation_icon %} - {{ documentation_text }} - {% else %} -   - {% endif %} - -
  • -{% endif %} -{% if bug_report_url %} -
  • - -   - -
  • -{% endif %} -{% if languages.keys()|length > 1 %} - -{% endif %} - -{% if not current_user.is_anonymous %} - -{% else %} -
  • - {{_("Login")}}
  • -{% endif %} diff --git a/superset/views/base.py b/superset/views/base.py index 41a27b4139e9..515384082299 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -367,7 +367,11 @@ def menu_data(user: User) -> Dict[str, Any]: # show the watermark if the default app icon has been overriden "show_watermark": ("superset-logo-horiz" not in appbuilder.app_icon), "bug_report_url": appbuilder.app.config["BUG_REPORT_URL"], + "bug_report_icon": appbuilder.app.config["BUG_REPORT_ICON"], + "bug_report_text": appbuilder.app.config["BUG_REPORT_TEXT"], "documentation_url": appbuilder.app.config["DOCUMENTATION_URL"], + "documentation_icon": appbuilder.app.config["DOCUMENTATION_ICON"], + "documentation_text": appbuilder.app.config["DOCUMENTATION_TEXT"], "version_string": appbuilder.app.config["VERSION_STRING"], "version_sha": appbuilder.app.config["VERSION_SHA"], "build_number": build_number, From a8f3a4fb6e90f061a9b87e7366f7f7c7184ca629 Mon Sep 17 00:00:00 2001 From: "JUST.in DO IT" Date: Wed, 11 Jan 2023 09:38:59 -0800 Subject: [PATCH 13/15] fix(sqllab): Overflow bigint in json-tree view (#22609) --- .../FilterableTable/FilterableTable.test.tsx | 17 +++++++++++++++++ .../src/components/FilterableTable/index.tsx | 17 +++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/components/FilterableTable/FilterableTable.test.tsx b/superset-frontend/src/components/FilterableTable/FilterableTable.test.tsx index 494af6033072..3135377cf7c2 100644 --- a/superset-frontend/src/components/FilterableTable/FilterableTable.test.tsx +++ b/superset-frontend/src/components/FilterableTable/FilterableTable.test.tsx @@ -21,6 +21,7 @@ import { ReactWrapper } from 'enzyme'; import { styledMount as mount } from 'spec/helpers/theming'; import FilterableTable, { MAX_COLUMNS_FOR_TABLE, + renderBigIntStrToNumber, } from 'src/components/FilterableTable'; import { render, screen } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; @@ -331,3 +332,19 @@ describe('FilterableTable sorting - RTL', () => { expect(gridCells[6]).toHaveTextContent('2022-01-02'); }); }); + +test('renders bigInt value in a number format', () => { + expect(renderBigIntStrToNumber('123')).toBe('123'); + expect(renderBigIntStrToNumber('some string value')).toBe( + 'some string value', + ); + expect(renderBigIntStrToNumber('{ a: 123 }')).toBe('{ a: 123 }'); + expect(renderBigIntStrToNumber('"Not a Number"')).toBe('"Not a Number"'); + // trim quotes for bigint string format + expect(renderBigIntStrToNumber('"-12345678901234567890"')).toBe( + '-12345678901234567890', + ); + expect(renderBigIntStrToNumber('"12345678901234567890"')).toBe( + '12345678901234567890', + ); +}); diff --git a/superset-frontend/src/components/FilterableTable/index.tsx b/superset-frontend/src/components/FilterableTable/index.tsx index 731514cca6cb..4d9098b2c2a4 100644 --- a/superset-frontend/src/components/FilterableTable/index.tsx +++ b/superset-frontend/src/components/FilterableTable/index.tsx @@ -53,7 +53,7 @@ function safeJsonObjectParse( // We know `data` is a string starting with '{' or '[', so try to parse it as a valid object try { - const jsonData = JSON.parse(data); + const jsonData = JSONbig({ storeAsString: true }).parse(data); if (jsonData && typeof jsonData === 'object') { return jsonData; } @@ -63,6 +63,13 @@ function safeJsonObjectParse( } } +export function renderBigIntStrToNumber(value: string) { + if (typeof value === 'string' && /^"-?\d+"$/.test(value)) { + return value.substring(1, value.length - 1); + } + return value; +} + const GRID_POSITION_ADJUSTMENT = 4; const SCROLL_BAR_HEIGHT = 15; // This regex handles all possible number formats in javascript, including ints, floats, @@ -405,7 +412,13 @@ const FilterableTable = ({ jsonString: CellDataType, ) => ( } + modalBody={ + + } modalFooter={