diff --git a/superset-frontend/src/constants.ts b/superset-frontend/src/constants.ts index 6a8da3ffe9f5..dcef70da7e8a 100644 --- a/superset-frontend/src/constants.ts +++ b/superset-frontend/src/constants.ts @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import { BootstrapData, CommonBootstrapData } from './types/bootstrapTypes'; + export const DATETIME_WITH_TIME_ZONE = 'YYYY-MM-DD HH:mm:ssZ'; export const TIME_WITH_MS = 'HH:mm:ss.SSS'; @@ -141,3 +143,55 @@ export const SLOW_DEBOUNCE = 500; * Display null as `N/A` */ export const NULL_DISPLAY = 'N/A'; + +export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = { + flash_messages: [], + conf: {}, + locale: 'en', + feature_flags: {}, + language_pack: { + domain: '', + locale_data: { + superset: { + '': { + domain: 'superset', + lang: 'en', + plural_forms: '', + }, + }, + }, + }, + extra_categorical_color_schemes: [], + extra_sequential_color_schemes: [], + theme_overrides: {}, + menu_data: { + menu: [], + brand: { + path: '', + icon: '', + alt: '', + tooltip: '', + text: '', + }, + navbar_right: { + show_watermark: true, + languages: {}, + show_language_picker: true, + user_is_anonymous: false, + user_info_url: '', + user_login_url: '', + user_logout_url: '', + user_profile_url: '', + locale: '', + }, + settings: [], + environment_tag: { + text: '', + color: '', + }, + }, +}; + +export const DEFAULT_BOOTSTRAP_DATA: BootstrapData = { + common: DEFAULT_COMMON_BOOTSTRAP_DATA, +}; diff --git a/superset-frontend/src/dashboard/util/findTabIndexByComponentId.test.js b/superset-frontend/src/dashboard/util/findTabIndexByComponentId.test.js index 3a3672c030b8..c41ea486335f 100644 --- a/superset-frontend/src/dashboard/util/findTabIndexByComponentId.test.js +++ b/superset-frontend/src/dashboard/util/findTabIndexByComponentId.test.js @@ -4,7 +4,7 @@ * 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 + * "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 diff --git a/superset-frontend/src/dashboard/util/updateComponentParentsList.test.js b/superset-frontend/src/dashboard/util/updateComponentParentsList.test.js index 4a788ad35de1..c5a15d72901b 100644 --- a/superset-frontend/src/dashboard/util/updateComponentParentsList.test.js +++ b/superset-frontend/src/dashboard/util/updateComponentParentsList.test.js @@ -4,7 +4,7 @@ * 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 + * "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 diff --git a/superset-frontend/src/featureFlags.ts b/superset-frontend/src/featureFlags.ts index ba902f07df79..e71ec8207410 100644 --- a/superset-frontend/src/featureFlags.ts +++ b/superset-frontend/src/featureFlags.ts @@ -21,7 +21,7 @@ import { FeatureFlagMap, FeatureFlag } from '@superset-ui/core'; export { FeatureFlag } from '@superset-ui/core'; export type { FeatureFlagMap } from '@superset-ui/core'; -export function initFeatureFlags(featureFlags: FeatureFlagMap) { +export function initFeatureFlags(featureFlags?: FeatureFlagMap) { if (!window.featureFlags) { window.featureFlags = featureFlags || {}; } diff --git a/superset-frontend/src/preamble.ts b/superset-frontend/src/preamble.ts index 09ef06dafe63..c5026cdb8b79 100644 --- a/superset-frontend/src/preamble.ts +++ b/superset-frontend/src/preamble.ts @@ -26,22 +26,20 @@ import setupClient from './setup/setupClient'; import setupColors from './setup/setupColors'; import setupFormatters from './setup/setupFormatters'; import setupDashboardComponents from './setup/setupDasboardComponents'; -import { BootstrapUser, User } from './types/bootstrapTypes'; +import { BootstrapData, User } from './types/bootstrapTypes'; import { initFeatureFlags } from './featureFlags'; +import { DEFAULT_COMMON_BOOTSTRAP_DATA } from './constants'; if (process.env.WEBPACK_MODE === 'development') { setHotLoaderConfig({ logLevel: 'debug', trackTailUpdates: false }); } // eslint-disable-next-line import/no-mutable-exports -export let bootstrapData: { - user?: BootstrapUser; - common?: any; - config?: any; - embedded?: { - dashboard_id: string; - }; -} = {}; +export let bootstrapData: BootstrapData = { + common: { + ...DEFAULT_COMMON_BOOTSTRAP_DATA, + }, +}; // Configure translation if (typeof window !== 'undefined') { diff --git a/superset-frontend/src/types/bootstrapTypes.ts b/superset-frontend/src/types/bootstrapTypes.ts index 6d4605270ad0..b667b9a80c11 100644 --- a/superset-frontend/src/types/bootstrapTypes.ts +++ b/superset-frontend/src/types/bootstrapTypes.ts @@ -1,5 +1,14 @@ -import { JsonObject, Locale } from '@superset-ui/core'; +import { + ColorSchemeConfig, + FeatureFlagMap, + JsonObject, + LanguagePack, + Locale, + SequentialSchemeConfig, +} from '@superset-ui/core'; import { isPlainObject } from 'lodash'; +import { FlashMessage } from '../components/FlashProvider'; +import { Languages } from '../views/components/LanguagePicker'; /** * Licensed to the Apache Software Foundation (ASF) under one @@ -74,11 +83,78 @@ export type ChartResponse = { result: ChartData[]; }; +export interface BrandProps { + path: string; + icon: string; + alt: string; + tooltip: string; + text: string; +} + +export interface NavBarProps { + show_watermark: boolean; + bug_report_url?: string; + version_string?: string; + version_sha?: string; + build_number?: string; + documentation_url?: string; + languages: Languages; + show_language_picker: boolean; + user_is_anonymous: boolean; + user_info_url: string; + user_login_url: string; + user_logout_url: string; + user_profile_url: string | null; + locale: string; +} + +export interface MenuObjectChildProps { + label: string; + name?: string; + icon?: string; + index?: number; + url?: string; + isFrontendRoute?: boolean; + perm?: string | boolean; + view?: string; + disable?: boolean; +} + +export interface MenuObjectProps extends MenuObjectChildProps { + childs?: (MenuObjectChildProps | string)[]; + isHeader?: boolean; +} + +export interface MenuData { + menu: MenuObjectProps[]; + brand: BrandProps; + navbar_right: NavBarProps; + settings: MenuObjectProps[]; + environment_tag: { + text: string; + color: string; + }; +} + export interface CommonBootstrapData { - flash_messages: string[][]; + flash_messages: FlashMessage[]; conf: JsonObject; locale: Locale; - feature_flags: Record; + feature_flags: FeatureFlagMap; + language_pack: LanguagePack; + extra_categorical_color_schemes: ColorSchemeConfig[]; + extra_sequential_color_schemes: SequentialSchemeConfig[]; + theme_overrides: JsonObject; + menu_data: MenuData; +} + +export interface BootstrapData { + user?: BootstrapUser; + common: CommonBootstrapData; + config?: any; + embedded?: { + dashboard_id: string; + }; } export function isUser(user: any): user is User { diff --git a/superset-frontend/src/utils/getBootstrapData.ts b/superset-frontend/src/utils/getBootstrapData.ts new file mode 100644 index 000000000000..e8d708e40dc4 --- /dev/null +++ b/superset-frontend/src/utils/getBootstrapData.ts @@ -0,0 +1,26 @@ +/** + * 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 { BootstrapData } from 'src/types/bootstrapTypes'; + +export default function getBootstrapData(): BootstrapData { + const appContainer = document.getElementById('app'); + const dataBootstrap = appContainer?.getAttribute('data-bootstrap'); + return dataBootstrap ? JSON.parse(dataBootstrap) : {}; +} diff --git a/superset-frontend/src/utils/localStorageHelpers.ts b/superset-frontend/src/utils/localStorageHelpers.ts index b6dad501f5af..3dceaba86ece 100644 --- a/superset-frontend/src/utils/localStorageHelpers.ts +++ b/superset-frontend/src/utils/localStorageHelpers.ts @@ -17,8 +17,7 @@ * under the License. */ -import { TableTabTypes } from 'src/views/CRUD/types'; -import { SetTabType } from 'src/views/CRUD/welcome/ActivityTable'; +import { TableTab } from 'src/views/CRUD/types'; import { DashboardContextForExplore } from 'src/types/DashboardContextForExplore'; export enum LocalStorageKeys { @@ -65,11 +64,11 @@ export type LocalStorageValues = { controls_width: number; datasource_width: number; is_datapanel_open: boolean; - homepage_chart_filter: TableTabTypes; - homepage_dashboard_filter: TableTabTypes; + homepage_chart_filter: TableTab; + homepage_dashboard_filter: TableTab; homepage_collapse_state: string[]; datasetname_set_successful: boolean; - homepage_activity_filter: SetTabType | null; + homepage_activity_filter: TableTab | null; sqllab__is_autocomplete_enabled: boolean; explore__data_table_original_formatted_time_columns: Record; dashboard__custom_filter_bar_widths: Record; diff --git a/superset-frontend/src/utils/urlUtils.test.ts b/superset-frontend/src/utils/urlUtils.test.ts index 5f413ac59034..19e2a470d182 100644 --- a/superset-frontend/src/utils/urlUtils.test.ts +++ b/superset-frontend/src/utils/urlUtils.test.ts @@ -4,14 +4,14 @@ * 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 + * "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 + * "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. diff --git a/superset-frontend/src/utils/urlUtils.ts b/superset-frontend/src/utils/urlUtils.ts index 24442a2f485b..07c8ad550074 100644 --- a/superset-frontend/src/utils/urlUtils.ts +++ b/superset-frontend/src/utils/urlUtils.ts @@ -4,14 +4,14 @@ * 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 + * "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 + * "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. diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx index 1c58fa3a599d..744edb51b18e 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx @@ -38,7 +38,7 @@ import ListView, { FilterOperator, Filters } from 'src/components/ListView'; import handleResourceExport from 'src/utils/export'; import { ExtentionConfigs } from 'src/views/components/types'; import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; -import type { MenuObjectProps } from 'src/views/components/Menu'; +import type { MenuObjectProps } from 'src/types/bootstrapTypes'; import DatabaseModal from './DatabaseModal'; import { DatabaseObject } from './types'; diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts index 0090697747ac..86784c9704b8 100644 --- a/superset-frontend/src/views/CRUD/types.ts +++ b/superset-frontend/src/views/CRUD/types.ts @@ -24,13 +24,16 @@ export type FavoriteStatus = { [id: number]: boolean; }; -export enum TableTabTypes { - FAVORITE = 'Favorite', - MINE = 'Mine', - EXAMPLES = 'Examples', +export enum TableTab { + Favorite = 'Favorite', + Mine = 'Mine', + Other = 'Other', + Viewed = 'Viewed', + Created = 'Created', + Edited = 'Edited', } -export type Filters = { +export type Filter = { col: string; opr: string; value: string | number; @@ -39,12 +42,12 @@ export type Filters = { export interface DashboardTableProps { addDangerToast: (message: string) => void; addSuccessToast: (message: string) => void; - search: string; user?: User; mine: Array; showThumbnails?: boolean; - featureFlag?: boolean; - examples: Array; + otherTabData: Array; + otherTabFilters: Filter[]; + otherTabTitle: string; } export interface Dashboard { diff --git a/superset-frontend/src/views/CRUD/utils.test.tsx b/superset-frontend/src/views/CRUD/utils.test.tsx index e727a0c896bd..fa41455d8508 100644 --- a/superset-frontend/src/views/CRUD/utils.test.tsx +++ b/superset-frontend/src/views/CRUD/utils.test.tsx @@ -18,13 +18,17 @@ */ import rison from 'rison'; import { - isNeedsPassword, - isAlreadyExists, - getPasswordsNeeded, + checkUploadExtensions, getAlreadyExists, + getFilterValues, + getPasswordsNeeded, hasTerminalValidation, - checkUploadExtensions, + isAlreadyExists, + isNeedsPassword, } from 'src/views/CRUD/utils'; +import { User } from 'src/types/bootstrapTypes'; +import { Filter, TableTab } from './types'; +import { WelcomeTable } from './welcome/types'; const terminalErrors = { errors: [ @@ -228,3 +232,165 @@ test('checkUploadExtensions should return valid upload extensions', () => { checkUploadExtensions(randomExtensionThree, uploadExtensionTest), ).toBeFalsy(); }); + +test('getFilterValues', () => { + const userId = 1234; + const mockUser: User = { + firstName: 'foo', + lastName: 'bar', + username: 'baz', + userId, + isActive: true, + isAnonymous: false, + }; + const testCases: [ + TableTab, + WelcomeTable, + User | undefined, + Filter[] | undefined, + ReturnType, + ][] = [ + [ + TableTab.Mine, + WelcomeTable.SavedQueries, + mockUser, + undefined, + [ + { + id: 'created_by', + operator: 'rel_o_m', + value: `${userId}`, + }, + ], + ], + [ + TableTab.Favorite, + WelcomeTable.SavedQueries, + mockUser, + undefined, + [ + { + id: 'id', + operator: 'saved_query_is_fav', + value: true, + }, + ], + ], + [ + TableTab.Created, + WelcomeTable.Charts, + mockUser, + undefined, + [ + { + id: 'created_by', + operator: 'rel_o_m', + value: `${userId}`, + }, + ], + ], + [ + TableTab.Created, + WelcomeTable.Dashboards, + mockUser, + undefined, + [ + { + id: 'created_by', + operator: 'rel_o_m', + value: `${userId}`, + }, + ], + ], + [ + TableTab.Created, + WelcomeTable.Recents, + mockUser, + undefined, + [ + { + id: 'created_by', + operator: 'rel_o_m', + value: `${userId}`, + }, + ], + ], + [ + TableTab.Mine, + WelcomeTable.Charts, + mockUser, + undefined, + [ + { + id: 'owners', + operator: 'rel_m_m', + value: `${userId}`, + }, + ], + ], + [ + TableTab.Mine, + WelcomeTable.Dashboards, + mockUser, + undefined, + [ + { + id: 'owners', + operator: 'rel_m_m', + value: `${userId}`, + }, + ], + ], + [ + TableTab.Favorite, + WelcomeTable.Dashboards, + mockUser, + undefined, + [ + { + id: 'id', + operator: 'dashboard_is_favorite', + value: true, + }, + ], + ], + [ + TableTab.Favorite, + WelcomeTable.Charts, + mockUser, + undefined, + [ + { + id: 'id', + operator: 'chart_is_favorite', + value: true, + }, + ], + ], + [ + TableTab.Other, + WelcomeTable.Charts, + mockUser, + [ + { + col: 'created_by', + opr: 'rel_o_m', + value: 0, + }, + ], + [ + { + id: 'created_by', + operator: 'rel_o_m', + value: 0, + }, + ], + ], + ]; + testCases.forEach(testCase => { + const [tab, welcomeTable, user, otherTabFilters, expectedValue] = testCase; + expect(getFilterValues(tab, welcomeTable, user, otherTabFilters)).toEqual( + expectedValue, + ); + }); +}); diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx index 8d9c1cea83da..bbb985609d4b 100644 --- a/superset-frontend/src/views/CRUD/utils.tsx +++ b/superset-frontend/src/views/CRUD/utils.tsx @@ -18,22 +18,24 @@ */ import { - t, - SupersetClient, - SupersetClientResponse, + css, logging, styled, + SupersetClient, + SupersetClientResponse, SupersetTheme, - css, + t, } from '@superset-ui/core'; import Chart from 'src/types/Chart'; import { intersection } from 'lodash'; import rison from 'rison'; import { getClientErrorObject } from 'src/utils/getClientErrorObject'; -import { FetchDataConfig } from 'src/components/ListView'; +import { FetchDataConfig, FilterValue } from 'src/components/ListView'; import SupersetText from 'src/utils/textUtils'; import { findPermission } from 'src/utils/findPermission'; -import { Dashboard, Filters } from './types'; +import { User } from 'src/types/bootstrapTypes'; +import { Dashboard, Filter, TableTab } from './types'; +import { WelcomeTable } from './welcome/types'; // Modifies the rison encoding slightly to match the backend's rison encoding/decoding. Applies globally. // Code pulled from rison.js (https://github.com/Nanonid/rison), rison is licensed under the MIT license. @@ -120,7 +122,7 @@ const createFetchResourceMethod = }; export const PAGE_SIZE = 5; -const getParams = (filters?: Array) => { +const getParams = (filters?: Filter[]) => { const params = { order_column: 'changed_on_delta_humanized', order_direction: 'desc', @@ -164,7 +166,7 @@ export const getEditedObjects = (userId: string | number) => { export const getUserOwnedObjects = ( userId: string | number, resource: string, - filters: Array = [ + filters: Filter[] = [ { col: 'owners', opr: 'rel_m_m', @@ -180,16 +182,10 @@ export const getRecentAcitivtyObjs = ( userId: string | number, recent: string, addDangerToast: (arg1: string, arg2: any) => any, + filters: Filter[], ) => SupersetClient.get({ endpoint: recent }).then(recentsRes => { const res: any = {}; - const filters = [ - { - col: 'created_by', - opr: 'rel_o_m', - value: 0, - }, - ]; const newBatch = [ SupersetClient.get({ endpoint: `/api/v1/chart/?q=${getParams(filters)}`, @@ -200,7 +196,7 @@ export const getRecentAcitivtyObjs = ( ]; return Promise.all(newBatch) .then(([chartRes, dashboardRes]) => { - res.examples = [...chartRes.json.result, ...dashboardRes.json.result]; + res.other = [...chartRes.json.result, ...dashboardRes.json.result]; res.viewed = recentsRes.json; return res; }) @@ -442,3 +438,64 @@ export const uploadUserPerms = ( canUploadData: canUploadCSV || canUploadColumnar || canUploadExcel, }; }; + +export function getFilterValues( + tab: TableTab, + welcomeTable: WelcomeTable, + user?: User, + otherTabFilters?: Filter[], +): FilterValue[] { + if ( + tab === TableTab.Created || + (welcomeTable === WelcomeTable.SavedQueries && tab === TableTab.Mine) + ) { + return [ + { + id: 'created_by', + operator: 'rel_o_m', + value: `${user?.userId}`, + }, + ]; + } + if (welcomeTable === WelcomeTable.SavedQueries && tab === TableTab.Favorite) { + return [ + { + id: 'id', + operator: 'saved_query_is_fav', + value: true, + }, + ]; + } + if (tab === TableTab.Mine && user) { + return [ + { + id: 'owners', + operator: 'rel_m_m', + value: `${user.userId}`, + }, + ]; + } + if ( + tab === TableTab.Favorite && + [WelcomeTable.Dashboards, WelcomeTable.Charts].includes(welcomeTable) + ) { + return [ + { + id: 'id', + operator: + welcomeTable === WelcomeTable.Dashboards + ? 'dashboard_is_favorite' + : 'chart_is_favorite', + value: true, + }, + ]; + } + if (tab === TableTab.Other) { + return (otherTabFilters || []).map(flt => ({ + id: flt.col, + operator: flt.opr, + value: flt.value, + })); + } + return []; +} diff --git a/superset-frontend/src/views/CRUD/welcome/ActivityTable.test.tsx b/superset-frontend/src/views/CRUD/welcome/ActivityTable.test.tsx index 71067b817a3e..53f637e4e6ea 100644 --- a/superset-frontend/src/views/CRUD/welcome/ActivityTable.test.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.test.tsx @@ -25,6 +25,7 @@ import fetchMock from 'fetch-mock'; import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; import ActivityTable from 'src/views/CRUD/welcome/ActivityTable'; +import { TableTab } from 'src/views/CRUD/types'; const mockStore = configureStore([thunk]); const store = mockStore({}); @@ -33,7 +34,7 @@ const chartsEndpoint = 'glob:*/api/v1/chart/?*'; const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*'; const mockData = { - Viewed: [ + [TableTab.Viewed]: [ { slice_name: 'ChartyChart', changed_on_utc: '24 Feb 2014 10:13:14', @@ -42,7 +43,7 @@ const mockData = { table: {}, }, ], - Created: [ + [TableTab.Created]: [ { dashboard_title: 'Dashboard_Test', changed_on_utc: '24 Feb 2014 10:13:14', @@ -77,7 +78,7 @@ fetchMock.get(dashboardsEndpoint, { describe('ActivityTable', () => { const activityProps = { - activeChild: 'Created', + activeChild: TableTab.Created, activityData: mockData, setActiveChild: jest.fn(), user: { userId: '1' }, @@ -122,7 +123,7 @@ describe('ActivityTable', () => { }); it('show empty state if there is no data', () => { const activityProps = { - activeChild: 'Created', + activeChild: TableTab.Created, activityData: {}, setActiveChild: jest.fn(), user: { userId: '1' }, diff --git a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx index d4f4fb4a8be2..677f9e51bfa3 100644 --- a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx @@ -23,6 +23,7 @@ import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers'; import { Link } from 'react-router-dom'; import ListViewCard from 'src/components/ListViewCard'; import SubMenu from 'src/views/components/SubMenu'; +import { Dashboard, SavedQueryObject, TableTab } from 'src/views/CRUD/types'; import { ActivityData, LoadingCards } from 'src/views/CRUD/welcome/Welcome'; import { CardContainer, @@ -30,7 +31,6 @@ import { getEditedObjects, } from 'src/views/CRUD/utils'; import { Chart } from 'src/types/Chart'; -import { Dashboard, SavedQueryObject } from 'src/views/CRUD/types'; import Icons from 'src/components/Icons'; @@ -57,12 +57,6 @@ interface RecentDashboard extends RecentActivity { item_type: 'dashboard'; } -export enum SetTabType { - EDITED = 'Edited', - CREATED = 'Created', - VIEWED = 'Viewed', - EXAMPLE = 'Examples', -} /** * Recent activity objects fetched by `getRecentAcitivtyObjs`. */ @@ -148,7 +142,7 @@ export default function ActivityTable({ }; useEffect(() => { - if (activeChild === 'Edited') { + if (activeChild === TableTab.Edited) { setLoadingState(true); getEditedCards(); } @@ -156,54 +150,55 @@ export default function ActivityTable({ const tabs = [ { - name: 'Edited', + name: TableTab.Edited, label: t('Edited'), onClick: () => { - setActiveChild('Edited'); - setItem(LocalStorageKeys.homepage_activity_filter, SetTabType.EDITED); + setActiveChild(TableTab.Edited); + setItem(LocalStorageKeys.homepage_activity_filter, TableTab.Edited); }, }, { - name: 'Created', + name: TableTab.Created, label: t('Created'), onClick: () => { - setActiveChild('Created'); - setItem(LocalStorageKeys.homepage_activity_filter, SetTabType.CREATED); + setActiveChild(TableTab.Created); + setItem(LocalStorageKeys.homepage_activity_filter, TableTab.Created); }, }, ]; - if (activityData?.Viewed) { + if (activityData?.[TableTab.Viewed]) { tabs.unshift({ - name: 'Viewed', + name: TableTab.Viewed, label: t('Viewed'), onClick: () => { - setActiveChild('Viewed'); - setItem(LocalStorageKeys.homepage_activity_filter, SetTabType.VIEWED); + setActiveChild(TableTab.Viewed); + setItem(LocalStorageKeys.homepage_activity_filter, TableTab.Viewed); }, }); } const renderActivity = () => - (activeChild !== 'Edited' ? activityData[activeChild] : editedObjs).map( - (entity: ActivityObject) => { - const url = getEntityUrl(entity); - const lastActionOn = getEntityLastActionOn(entity); - return ( - - - } - url={url} - title={getEntityTitle(entity)} - description={lastActionOn} - avatar={getEntityIcon(entity)} - actions={null} - /> - - - ); - }, - ); + (activeChild !== TableTab.Edited + ? activityData[activeChild] + : editedObjs + ).map((entity: ActivityObject) => { + const url = getEntityUrl(entity); + const lastActionOn = getEntityLastActionOn(entity); + return ( + + + } + url={url} + title={getEntityTitle(entity)} + description={lastActionOn} + avatar={getEntityIcon(entity)} + actions={null} + /> + + + ); + }); const doneFetching = loadedCount < 3; @@ -214,7 +209,9 @@ export default function ActivityTable({ {activityData[activeChild]?.length > 0 || - (activeChild === 'Edited' && editedObjs && editedObjs.length > 0) ? ( + (activeChild === TableTab.Edited && + editedObjs && + editedObjs.length > 0) ? ( {renderActivity()} diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.test.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.test.tsx index cfa9230328c0..45e3483026cc 100644 --- a/superset-frontend/src/views/CRUD/welcome/ChartTable.test.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.test.tsx @@ -62,6 +62,11 @@ describe('ChartTable', () => { user: { userId: '2', }, + mine: [], + otherTabData: [], + otherTabFilters: [], + otherTabTitle: 'Other', + showThumbnails: false, }; let wrapper: ReactWrapper; @@ -89,13 +94,35 @@ describe('ChartTable', () => { expect(wrapper.find('ChartCard')).toExist(); }); + it('renders other tab by default', async () => { + await act(async () => { + wrapper = mount( + , + ); + }); + await waitForComponentToPaint(wrapper); + expect(wrapper.find('EmptyState')).not.toExist(); + expect(wrapper.find('ChartCard')).toExist(); + }); + it('display EmptyState if there is no data', async () => { await act(async () => { wrapper = mount( , ); diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx index 41c25033df61..b5d016d42140 100644 --- a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx @@ -26,15 +26,19 @@ import { } from 'src/views/CRUD/hooks'; import { getItem, - setItem, LocalStorageKeys, + setItem, } from 'src/utils/localStorageHelpers'; import withToasts from 'src/components/MessageToasts/withToasts'; import { useHistory } from 'react-router-dom'; -import { TableTabTypes } from 'src/views/CRUD/types'; +import { Filter, TableTab } from 'src/views/CRUD/types'; import PropertiesModal from 'src/explore/components/PropertiesModal'; import { User } from 'src/types/bootstrapTypes'; -import { CardContainer, PAGE_SIZE } from 'src/views/CRUD/utils'; +import { + CardContainer, + getFilterValues, + PAGE_SIZE, +} from 'src/views/CRUD/utils'; import { LoadingCards } from 'src/views/CRUD/welcome/Welcome'; import ChartCard from 'src/views/CRUD/chart/ChartCard'; import Chart from 'src/types/Chart'; @@ -48,12 +52,12 @@ import { WelcomeTable } from './types'; interface ChartTableProps { addDangerToast: (message: string) => void; addSuccessToast: (message: string) => void; - search: string; - chartFilter?: string; user?: User; mine: Array; showThumbnails: boolean; - examples?: Array; + otherTabData?: Array; + otherTabFilters: Filter[]; + otherTabTitle: string; } function ChartTable({ @@ -62,16 +66,17 @@ function ChartTable({ addSuccessToast, mine, showThumbnails, - examples, + otherTabData, + otherTabFilters, + otherTabTitle, }: ChartTableProps) { const history = useHistory(); - const filterStore = getItem( + const initialTab = getItem( LocalStorageKeys.homepage_chart_filter, - TableTabTypes.EXAMPLES, + TableTab.Other, ); - const initialFilter = filterStore; - const filteredExamples = filter(examples, obj => 'viz_type' in obj); + const filteredOtherTabData = filter(otherTabData, obj => 'viz_type' in obj); const { state: { loading, resourceCollection: charts, bulkSelectEnabled }, @@ -84,7 +89,7 @@ function ChartTable({ t('chart'), addDangerToast, true, - initialFilter === 'Mine' ? mine : filteredExamples, + initialTab === TableTab.Mine ? mine : filteredOtherTabData, [], false, ); @@ -102,36 +107,11 @@ function ChartTable({ closeChartEditModal, } = useChartEditModal(setCharts, charts); - const [chartFilter, setChartFilter] = useState(initialFilter); + const [activeTab, setActiveTab] = useState(initialTab); 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: 'chart_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) => + const getData = (tab: TableTab) => fetchData({ pageIndex: 0, pageSize: PAGE_SIZE, @@ -141,15 +121,15 @@ function ChartTable({ desc: true, }, ], - filters: getFilters(filter), + filters: getFilterValues(tab, WelcomeTable.Charts, user, otherTabFilters), }); useEffect(() => { - if (loaded || chartFilter === 'Favorite') { - getData(chartFilter); + if (loaded || activeTab === TableTab.Favorite) { + getData(activeTab); } setLoaded(true); - }, [chartFilter]); + }, [activeTab]); const handleBulkChartExport = (chartsToExport: Chart[]) => { const ids = chartsToExport.map(({ id }) => id); @@ -161,29 +141,29 @@ function ChartTable({ const menuTabs = [ { - name: 'Favorite', + name: TableTab.Favorite, label: t('Favorite'), onClick: () => { - setChartFilter(TableTabTypes.FAVORITE); - setItem(LocalStorageKeys.homepage_chart_filter, TableTabTypes.FAVORITE); + setActiveTab(TableTab.Favorite); + setItem(LocalStorageKeys.homepage_chart_filter, TableTab.Favorite); }, }, { - name: 'Mine', + name: TableTab.Mine, label: t('Mine'), onClick: () => { - setChartFilter(TableTabTypes.MINE); - setItem(LocalStorageKeys.homepage_chart_filter, TableTabTypes.MINE); + setActiveTab(TableTab.Mine); + setItem(LocalStorageKeys.homepage_chart_filter, TableTab.Mine); }, }, ]; - if (examples) { + if (otherTabData) { menuTabs.push({ - name: 'Examples', - label: t('Examples'), + name: TableTab.Other, + label: otherTabTitle, onClick: () => { - setChartFilter(TableTabTypes.EXAMPLES); - setItem(LocalStorageKeys.homepage_chart_filter, TableTabTypes.EXAMPLES); + setActiveTab(TableTab.Other); + setItem(LocalStorageKeys.homepage_chart_filter, TableTab.Other); }, }); } @@ -201,7 +181,7 @@ function ChartTable({ )} { const target = - chartFilter === 'Favorite' + activeTab === TableTab.Favorite ? `/chart/list/?filters=(favorite:(label:${t( 'Yes', )},value:!t))` @@ -237,7 +217,7 @@ function ChartTable({ ) : ( - + )} {preparingExport && } diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx index a5078465fc6f..0a13dde2cfdd 100644 --- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx @@ -20,22 +20,19 @@ import React, { useEffect, useMemo, useState } from 'react'; import { SupersetClient, t } from '@superset-ui/core'; import { filter } from 'lodash'; import { useFavoriteStatus, useListViewResource } from 'src/views/CRUD/hooks'; -import { - Dashboard, - DashboardTableProps, - TableTabTypes, -} from 'src/views/CRUD/types'; +import { Dashboard, DashboardTableProps, TableTab } from 'src/views/CRUD/types'; import handleResourceExport from 'src/utils/export'; import { useHistory } from 'react-router-dom'; import { getItem, - setItem, LocalStorageKeys, + setItem, } from 'src/utils/localStorageHelpers'; import { LoadingCards } from 'src/views/CRUD/welcome/Welcome'; import { CardContainer, createErrorHandler, + getFilterValues, PAGE_SIZE, } from 'src/views/CRUD/utils'; import withToasts from 'src/components/MessageToasts/withToasts'; @@ -46,28 +43,26 @@ import SubMenu from 'src/views/components/SubMenu'; import EmptyState from './EmptyState'; import { WelcomeTable } from './types'; -export interface FilterValue { - col: string; - operator: string; - value: string | boolean | number | null | undefined; -} - function DashboardTable({ user, addDangerToast, addSuccessToast, mine, showThumbnails, - examples, + otherTabData, + otherTabFilters, + otherTabTitle, }: DashboardTableProps) { const history = useHistory(); - const filterStore = getItem( + const defaultTab = getItem( LocalStorageKeys.homepage_dashboard_filter, - TableTabTypes.EXAMPLES, + TableTab.Other, ); - const defaultFilter = filterStore; - const filteredExamples = filter(examples, obj => !('viz_type' in obj)); + const filteredOtherTabData = filter( + otherTabData, + obj => !('viz_type' in obj), + ); const { state: { loading, resourceCollection: dashboards }, @@ -80,7 +75,7 @@ function DashboardTable({ t('dashboard'), addDangerToast, true, - defaultFilter === 'Mine' ? mine : filteredExamples, + defaultTab === TableTab.Mine ? mine : filteredOtherTabData, [], false, ); @@ -92,35 +87,11 @@ function DashboardTable({ ); const [editModal, setEditModal] = useState(); - const [dashboardFilter, setDashboardFilter] = useState(defaultFilter); + const [activeTab, setActiveTab] = useState(defaultTab); 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) => + const getData = (tab: TableTab) => fetchData({ pageIndex: 0, pageSize: PAGE_SIZE, @@ -130,15 +101,20 @@ function DashboardTable({ desc: true, }, ], - filters: getFilters(filter), + filters: getFilterValues( + tab, + WelcomeTable.Dashboards, + user, + otherTabFilters, + ), }); useEffect(() => { - if (loaded || dashboardFilter === 'Favorite') { - getData(dashboardFilter); + if (loaded || activeTab === TableTab.Favorite) { + getData(activeTab); } setLoaded(true); - }, [dashboardFilter]); + }, [activeTab]); const handleBulkDashboardExport = (dashboardsToExport: Dashboard[]) => { const ids = dashboardsToExport.map(({ id }) => id); @@ -171,36 +147,30 @@ function DashboardTable({ const menuTabs = [ { - name: 'Favorite', + name: TableTab.Favorite, label: t('Favorite'), onClick: () => { - setDashboardFilter(TableTabTypes.FAVORITE); - setItem( - LocalStorageKeys.homepage_dashboard_filter, - TableTabTypes.FAVORITE, - ); + setActiveTab(TableTab.Favorite); + setItem(LocalStorageKeys.homepage_dashboard_filter, TableTab.Favorite); }, }, { - name: 'Mine', + name: TableTab.Mine, label: t('Mine'), onClick: () => { - setDashboardFilter(TableTabTypes.MINE); - setItem(LocalStorageKeys.homepage_dashboard_filter, TableTabTypes.MINE); + setActiveTab(TableTab.Mine); + setItem(LocalStorageKeys.homepage_dashboard_filter, TableTab.Mine); }, }, ]; - if (examples) { + if (otherTabData) { menuTabs.push({ - name: 'Examples', - label: t('Examples'), + name: TableTab.Other, + label: otherTabTitle, onClick: () => { - setDashboardFilter(TableTabTypes.EXAMPLES); - setItem( - LocalStorageKeys.homepage_dashboard_filter, - TableTabTypes.EXAMPLES, - ); + setActiveTab(TableTab.Other); + setItem(LocalStorageKeys.homepage_dashboard_filter, TableTab.Other); }, }); } @@ -209,7 +179,7 @@ function DashboardTable({ return ( <> { const target = - dashboardFilter === 'Favorite' + activeTab === TableTab.Favorite ? `/dashboard/list/?filters=(favorite:(label:${t( 'Yes', )},value:!t))` @@ -256,7 +226,7 @@ function DashboardTable({ hasPerm={hasPerm} bulkSelectEnabled={false} showThumbnails={showThumbnails} - dashboardFilter={dashboardFilter} + dashboardFilter={activeTab} refreshData={refreshData} addDangerToast={addDangerToast} addSuccessToast={addSuccessToast} @@ -273,7 +243,7 @@ function DashboardTable({ )} {dashboards.length === 0 && ( - + )} {preparingExport && } diff --git a/superset-frontend/src/views/CRUD/welcome/EmptyState.test.tsx b/superset-frontend/src/views/CRUD/welcome/EmptyState.test.tsx index 908ebed6c4f8..fb8ae48ee16b 100644 --- a/superset-frontend/src/views/CRUD/welcome/EmptyState.test.tsx +++ b/superset-frontend/src/views/CRUD/welcome/EmptyState.test.tsx @@ -18,47 +18,48 @@ */ import React from 'react'; import { styledMount as mount } from 'spec/helpers/theming'; -import EmptyState from 'src/views/CRUD/welcome/EmptyState'; +import { TableTab } from 'src/views/CRUD/types'; +import EmptyState, { EmptyStateProps } from 'src/views/CRUD/welcome/EmptyState'; import { WelcomeTable } from './types'; describe('EmptyState', () => { - const variants = [ + const variants: EmptyStateProps[] = [ { - tab: 'Favorite', + tab: TableTab.Favorite, tableName: WelcomeTable.Dashboards, }, { - tab: 'Mine', + tab: TableTab.Mine, tableName: WelcomeTable.Dashboards, }, { - tab: 'Favorite', + tab: TableTab.Favorite, tableName: WelcomeTable.Charts, }, { - tab: 'Mine', + tab: TableTab.Mine, tableName: WelcomeTable.Charts, }, { - tab: 'Favorite', + tab: TableTab.Favorite, tableName: WelcomeTable.SavedQueries, }, { - tab: 'Mine', + tab: TableTab.Mine, tableName: WelcomeTable.SavedQueries, }, ]; - const recents = [ + const recents: EmptyStateProps[] = [ { - tab: 'Viewed', + tab: TableTab.Viewed, tableName: WelcomeTable.Recents, }, { - tab: 'Edited', + tab: TableTab.Edited, tableName: WelcomeTable.Recents, }, { - tab: 'Created', + tab: TableTab.Created, tableName: WelcomeTable.Recents, }, ]; @@ -68,10 +69,10 @@ describe('EmptyState', () => { expect(wrapper).toExist(); const textContainer = wrapper.find('.ant-empty-description'); expect(textContainer.text()).toEqual( - variant.tab === 'Favorite' + variant.tab === TableTab.Favorite ? "You don't have any favorites yet!" : `No ${ - variant.tableName === 'SAVED_QUERIES' + variant.tableName === WelcomeTable.SavedQueries ? 'saved queries' : variant.tableName.toLowerCase() } yet`, @@ -80,13 +81,13 @@ describe('EmptyState', () => { }); }); recents.forEach(recent => { - it(`it renders an ${recent.tab} ${recent.tableName} empty state`, () => { + it(`it renders a ${recent.tab} ${recent.tableName} empty state`, () => { const wrapper = mount(); expect(wrapper).toExist(); const textContainer = wrapper.find('.ant-empty-description'); expect(wrapper.find('.ant-empty-image').children()).toHaveLength(1); expect(textContainer.text()).toContain( - `Recently ${recent.tab.toLowerCase()} charts, dashboards, and saved queries will appear here`, + `Recently ${recent.tab?.toLowerCase()} charts, dashboards, and saved queries will appear here`, ); }); }); diff --git a/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx b/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx index 525c9ef62e80..702833c39433 100644 --- a/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx +++ b/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx @@ -19,7 +19,8 @@ import React from 'react'; import Button from 'src/components/Button'; import { Empty } from 'src/components'; -import { t, styled } from '@superset-ui/core'; +import { TableTab } from 'src/views/CRUD/types'; +import { styled, t } from '@superset-ui/core'; import { WelcomeTable } from './types'; const welcomeTableLabels: Record = { @@ -29,9 +30,10 @@ const welcomeTableLabels: Record = { [WelcomeTable.SavedQueries]: t('saved queries'), }; -interface EmptyStateProps { +export interface EmptyStateProps { tableName: WelcomeTable; tab?: string; + otherTabTitle?: string; } const EmptyContainer = styled.div` min-height: 200px; @@ -52,7 +54,11 @@ type Redirects = Record< string >; -export default function EmptyState({ tableName, tab }: EmptyStateProps) { +export default function EmptyState({ + tableName, + tab, + otherTabTitle, +}: EmptyStateProps) { const mineRedirects: Redirects = { [WelcomeTable.Charts]: '/chart/add', [WelcomeTable.Dashboards]: '/dashboard/new', @@ -77,22 +83,23 @@ export default function EmptyState({ tableName, tab }: EmptyStateProps) { const recent = ( {(() => { - if (tab === 'Viewed') { + if (tab === TableTab.Viewed) { return t( `Recently viewed charts, dashboards, and saved queries will appear here`, ); } - if (tab === 'Created') { + if (tab === TableTab.Created) { return t( 'Recently created charts, dashboards, and saved queries will appear here', ); } - if (tab === 'Examples') { - return t('Example %(tableName)s will appear here', { + if (tab === TableTab.Other) { + return t('%(other)s %(tableName)s will appear here', { + other: otherTabTitle || t('Other'), tableName: tableName.toLowerCase(), }); } - if (tab === 'Edited') { + if (tab === TableTab.Edited) { return t( `Recently edited charts, dashboards, and saved queries will appear here`, ); @@ -101,17 +108,24 @@ export default function EmptyState({ tableName, tab }: EmptyStateProps) { })()} ); + // Mine and Recent Activity(all tabs) tab empty state - if (tab === 'Mine' || tableName === 'RECENTS' || tab === 'Examples') { + if ( + tab === TableTab.Mine || + tableName === WelcomeTable.Recents || + tab === TableTab.Other + ) { return ( - {tableName !== 'RECENTS' && ( + {tableName !== WelcomeTable.Recents && (