feat(welcome): make examples tab customizable (#22302)

This commit is contained in:
Ville Brofeldt 2022-12-21 09:28:41 +02:00 committed by GitHub
parent 7a94f3afc6
commit b954f8f560
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 808 additions and 474 deletions

View File

@ -16,6 +16,8 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { BootstrapData, CommonBootstrapData } from './types/bootstrapTypes';
export const DATETIME_WITH_TIME_ZONE = 'YYYY-MM-DD HH:mm:ssZ'; export const DATETIME_WITH_TIME_ZONE = 'YYYY-MM-DD HH:mm:ssZ';
export const TIME_WITH_MS = 'HH:mm:ss.SSS'; export const TIME_WITH_MS = 'HH:mm:ss.SSS';
@ -141,3 +143,55 @@ export const SLOW_DEBOUNCE = 500;
* Display null as `N/A` * Display null as `N/A`
*/ */
export const NULL_DISPLAY = '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,
};

View File

@ -4,7 +4,7 @@
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * 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 * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0

View File

@ -4,7 +4,7 @@
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * 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 * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0

View File

@ -21,7 +21,7 @@ import { FeatureFlagMap, FeatureFlag } from '@superset-ui/core';
export { FeatureFlag } from '@superset-ui/core'; export { FeatureFlag } from '@superset-ui/core';
export type { FeatureFlagMap } from '@superset-ui/core'; export type { FeatureFlagMap } from '@superset-ui/core';
export function initFeatureFlags(featureFlags: FeatureFlagMap) { export function initFeatureFlags(featureFlags?: FeatureFlagMap) {
if (!window.featureFlags) { if (!window.featureFlags) {
window.featureFlags = featureFlags || {}; window.featureFlags = featureFlags || {};
} }

View File

@ -26,22 +26,20 @@ import setupClient from './setup/setupClient';
import setupColors from './setup/setupColors'; import setupColors from './setup/setupColors';
import setupFormatters from './setup/setupFormatters'; import setupFormatters from './setup/setupFormatters';
import setupDashboardComponents from './setup/setupDasboardComponents'; import setupDashboardComponents from './setup/setupDasboardComponents';
import { BootstrapUser, User } from './types/bootstrapTypes'; import { BootstrapData, User } from './types/bootstrapTypes';
import { initFeatureFlags } from './featureFlags'; import { initFeatureFlags } from './featureFlags';
import { DEFAULT_COMMON_BOOTSTRAP_DATA } from './constants';
if (process.env.WEBPACK_MODE === 'development') { if (process.env.WEBPACK_MODE === 'development') {
setHotLoaderConfig({ logLevel: 'debug', trackTailUpdates: false }); setHotLoaderConfig({ logLevel: 'debug', trackTailUpdates: false });
} }
// eslint-disable-next-line import/no-mutable-exports // eslint-disable-next-line import/no-mutable-exports
export let bootstrapData: { export let bootstrapData: BootstrapData = {
user?: BootstrapUser; common: {
common?: any; ...DEFAULT_COMMON_BOOTSTRAP_DATA,
config?: any; },
embedded?: { };
dashboard_id: string;
};
} = {};
// Configure translation // Configure translation
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {

View File

@ -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 { isPlainObject } from 'lodash';
import { FlashMessage } from '../components/FlashProvider';
import { Languages } from '../views/components/LanguagePicker';
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
@ -74,11 +83,78 @@ export type ChartResponse = {
result: ChartData[]; 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 { export interface CommonBootstrapData {
flash_messages: string[][]; flash_messages: FlashMessage[];
conf: JsonObject; conf: JsonObject;
locale: Locale; locale: Locale;
feature_flags: Record<string, boolean>; 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 { export function isUser(user: any): user is User {

View File

@ -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) : {};
}

View File

@ -17,8 +17,7 @@
* under the License. * under the License.
*/ */
import { TableTabTypes } from 'src/views/CRUD/types'; import { TableTab } from 'src/views/CRUD/types';
import { SetTabType } from 'src/views/CRUD/welcome/ActivityTable';
import { DashboardContextForExplore } from 'src/types/DashboardContextForExplore'; import { DashboardContextForExplore } from 'src/types/DashboardContextForExplore';
export enum LocalStorageKeys { export enum LocalStorageKeys {
@ -65,11 +64,11 @@ export type LocalStorageValues = {
controls_width: number; controls_width: number;
datasource_width: number; datasource_width: number;
is_datapanel_open: boolean; is_datapanel_open: boolean;
homepage_chart_filter: TableTabTypes; homepage_chart_filter: TableTab;
homepage_dashboard_filter: TableTabTypes; homepage_dashboard_filter: TableTab;
homepage_collapse_state: string[]; homepage_collapse_state: string[];
datasetname_set_successful: boolean; datasetname_set_successful: boolean;
homepage_activity_filter: SetTabType | null; homepage_activity_filter: TableTab | null;
sqllab__is_autocomplete_enabled: boolean; sqllab__is_autocomplete_enabled: boolean;
explore__data_table_original_formatted_time_columns: Record<string, string[]>; explore__data_table_original_formatted_time_columns: Record<string, string[]>;
dashboard__custom_filter_bar_widths: Record<string, number>; dashboard__custom_filter_bar_widths: Record<string, number>;

View File

@ -4,14 +4,14 @@
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * 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 * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * 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 * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.

View File

@ -4,14 +4,14 @@
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * 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 * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * 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 * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.

View File

@ -38,7 +38,7 @@ import ListView, { FilterOperator, Filters } from 'src/components/ListView';
import handleResourceExport from 'src/utils/export'; import handleResourceExport from 'src/utils/export';
import { ExtentionConfigs } from 'src/views/components/types'; import { ExtentionConfigs } from 'src/views/components/types';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; 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 DatabaseModal from './DatabaseModal';
import { DatabaseObject } from './types'; import { DatabaseObject } from './types';

View File

@ -24,13 +24,16 @@ export type FavoriteStatus = {
[id: number]: boolean; [id: number]: boolean;
}; };
export enum TableTabTypes { export enum TableTab {
FAVORITE = 'Favorite', Favorite = 'Favorite',
MINE = 'Mine', Mine = 'Mine',
EXAMPLES = 'Examples', Other = 'Other',
Viewed = 'Viewed',
Created = 'Created',
Edited = 'Edited',
} }
export type Filters = { export type Filter = {
col: string; col: string;
opr: string; opr: string;
value: string | number; value: string | number;
@ -39,12 +42,12 @@ export type Filters = {
export interface DashboardTableProps { export interface DashboardTableProps {
addDangerToast: (message: string) => void; addDangerToast: (message: string) => void;
addSuccessToast: (message: string) => void; addSuccessToast: (message: string) => void;
search: string;
user?: User; user?: User;
mine: Array<Dashboard>; mine: Array<Dashboard>;
showThumbnails?: boolean; showThumbnails?: boolean;
featureFlag?: boolean; otherTabData: Array<Dashboard>;
examples: Array<Dashboard>; otherTabFilters: Filter[];
otherTabTitle: string;
} }
export interface Dashboard { export interface Dashboard {

View File

@ -18,13 +18,17 @@
*/ */
import rison from 'rison'; import rison from 'rison';
import { import {
isNeedsPassword,
isAlreadyExists,
getPasswordsNeeded,
getAlreadyExists,
hasTerminalValidation,
checkUploadExtensions, checkUploadExtensions,
getAlreadyExists,
getFilterValues,
getPasswordsNeeded,
hasTerminalValidation,
isAlreadyExists,
isNeedsPassword,
} from 'src/views/CRUD/utils'; } from 'src/views/CRUD/utils';
import { User } from 'src/types/bootstrapTypes';
import { Filter, TableTab } from './types';
import { WelcomeTable } from './welcome/types';
const terminalErrors = { const terminalErrors = {
errors: [ errors: [
@ -228,3 +232,165 @@ test('checkUploadExtensions should return valid upload extensions', () => {
checkUploadExtensions(randomExtensionThree, uploadExtensionTest), checkUploadExtensions(randomExtensionThree, uploadExtensionTest),
).toBeFalsy(); ).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<typeof getFilterValues>,
][] = [
[
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,
);
});
});

View File

@ -18,22 +18,24 @@
*/ */
import { import {
t, css,
SupersetClient,
SupersetClientResponse,
logging, logging,
styled, styled,
SupersetClient,
SupersetClientResponse,
SupersetTheme, SupersetTheme,
css, t,
} from '@superset-ui/core'; } from '@superset-ui/core';
import Chart from 'src/types/Chart'; import Chart from 'src/types/Chart';
import { intersection } from 'lodash'; import { intersection } from 'lodash';
import rison from 'rison'; import rison from 'rison';
import { getClientErrorObject } from 'src/utils/getClientErrorObject'; 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 SupersetText from 'src/utils/textUtils';
import { findPermission } from 'src/utils/findPermission'; 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. // 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. // 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; export const PAGE_SIZE = 5;
const getParams = (filters?: Array<Filters>) => { const getParams = (filters?: Filter[]) => {
const params = { const params = {
order_column: 'changed_on_delta_humanized', order_column: 'changed_on_delta_humanized',
order_direction: 'desc', order_direction: 'desc',
@ -164,7 +166,7 @@ export const getEditedObjects = (userId: string | number) => {
export const getUserOwnedObjects = ( export const getUserOwnedObjects = (
userId: string | number, userId: string | number,
resource: string, resource: string,
filters: Array<Filters> = [ filters: Filter[] = [
{ {
col: 'owners', col: 'owners',
opr: 'rel_m_m', opr: 'rel_m_m',
@ -180,16 +182,10 @@ export const getRecentAcitivtyObjs = (
userId: string | number, userId: string | number,
recent: string, recent: string,
addDangerToast: (arg1: string, arg2: any) => any, addDangerToast: (arg1: string, arg2: any) => any,
filters: Filter[],
) => ) =>
SupersetClient.get({ endpoint: recent }).then(recentsRes => { SupersetClient.get({ endpoint: recent }).then(recentsRes => {
const res: any = {}; const res: any = {};
const filters = [
{
col: 'created_by',
opr: 'rel_o_m',
value: 0,
},
];
const newBatch = [ const newBatch = [
SupersetClient.get({ SupersetClient.get({
endpoint: `/api/v1/chart/?q=${getParams(filters)}`, endpoint: `/api/v1/chart/?q=${getParams(filters)}`,
@ -200,7 +196,7 @@ export const getRecentAcitivtyObjs = (
]; ];
return Promise.all(newBatch) return Promise.all(newBatch)
.then(([chartRes, dashboardRes]) => { .then(([chartRes, dashboardRes]) => {
res.examples = [...chartRes.json.result, ...dashboardRes.json.result]; res.other = [...chartRes.json.result, ...dashboardRes.json.result];
res.viewed = recentsRes.json; res.viewed = recentsRes.json;
return res; return res;
}) })
@ -442,3 +438,64 @@ export const uploadUserPerms = (
canUploadData: canUploadCSV || canUploadColumnar || canUploadExcel, 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 [];
}

View File

@ -25,6 +25,7 @@ import fetchMock from 'fetch-mock';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import ActivityTable from 'src/views/CRUD/welcome/ActivityTable'; import ActivityTable from 'src/views/CRUD/welcome/ActivityTable';
import { TableTab } from 'src/views/CRUD/types';
const mockStore = configureStore([thunk]); const mockStore = configureStore([thunk]);
const store = mockStore({}); const store = mockStore({});
@ -33,7 +34,7 @@ const chartsEndpoint = 'glob:*/api/v1/chart/?*';
const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*'; const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*';
const mockData = { const mockData = {
Viewed: [ [TableTab.Viewed]: [
{ {
slice_name: 'ChartyChart', slice_name: 'ChartyChart',
changed_on_utc: '24 Feb 2014 10:13:14', changed_on_utc: '24 Feb 2014 10:13:14',
@ -42,7 +43,7 @@ const mockData = {
table: {}, table: {},
}, },
], ],
Created: [ [TableTab.Created]: [
{ {
dashboard_title: 'Dashboard_Test', dashboard_title: 'Dashboard_Test',
changed_on_utc: '24 Feb 2014 10:13:14', changed_on_utc: '24 Feb 2014 10:13:14',
@ -77,7 +78,7 @@ fetchMock.get(dashboardsEndpoint, {
describe('ActivityTable', () => { describe('ActivityTable', () => {
const activityProps = { const activityProps = {
activeChild: 'Created', activeChild: TableTab.Created,
activityData: mockData, activityData: mockData,
setActiveChild: jest.fn(), setActiveChild: jest.fn(),
user: { userId: '1' }, user: { userId: '1' },
@ -122,7 +123,7 @@ describe('ActivityTable', () => {
}); });
it('show empty state if there is no data', () => { it('show empty state if there is no data', () => {
const activityProps = { const activityProps = {
activeChild: 'Created', activeChild: TableTab.Created,
activityData: {}, activityData: {},
setActiveChild: jest.fn(), setActiveChild: jest.fn(),
user: { userId: '1' }, user: { userId: '1' },

View File

@ -23,6 +23,7 @@ import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import ListViewCard from 'src/components/ListViewCard'; import ListViewCard from 'src/components/ListViewCard';
import SubMenu from 'src/views/components/SubMenu'; 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 { ActivityData, LoadingCards } from 'src/views/CRUD/welcome/Welcome';
import { import {
CardContainer, CardContainer,
@ -30,7 +31,6 @@ import {
getEditedObjects, getEditedObjects,
} from 'src/views/CRUD/utils'; } from 'src/views/CRUD/utils';
import { Chart } from 'src/types/Chart'; import { Chart } from 'src/types/Chart';
import { Dashboard, SavedQueryObject } from 'src/views/CRUD/types';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
@ -57,12 +57,6 @@ interface RecentDashboard extends RecentActivity {
item_type: 'dashboard'; item_type: 'dashboard';
} }
export enum SetTabType {
EDITED = 'Edited',
CREATED = 'Created',
VIEWED = 'Viewed',
EXAMPLE = 'Examples',
}
/** /**
* Recent activity objects fetched by `getRecentAcitivtyObjs`. * Recent activity objects fetched by `getRecentAcitivtyObjs`.
*/ */
@ -148,7 +142,7 @@ export default function ActivityTable({
}; };
useEffect(() => { useEffect(() => {
if (activeChild === 'Edited') { if (activeChild === TableTab.Edited) {
setLoadingState(true); setLoadingState(true);
getEditedCards(); getEditedCards();
} }
@ -156,54 +150,55 @@ export default function ActivityTable({
const tabs = [ const tabs = [
{ {
name: 'Edited', name: TableTab.Edited,
label: t('Edited'), label: t('Edited'),
onClick: () => { onClick: () => {
setActiveChild('Edited'); setActiveChild(TableTab.Edited);
setItem(LocalStorageKeys.homepage_activity_filter, SetTabType.EDITED); setItem(LocalStorageKeys.homepage_activity_filter, TableTab.Edited);
}, },
}, },
{ {
name: 'Created', name: TableTab.Created,
label: t('Created'), label: t('Created'),
onClick: () => { onClick: () => {
setActiveChild('Created'); setActiveChild(TableTab.Created);
setItem(LocalStorageKeys.homepage_activity_filter, SetTabType.CREATED); setItem(LocalStorageKeys.homepage_activity_filter, TableTab.Created);
}, },
}, },
]; ];
if (activityData?.Viewed) { if (activityData?.[TableTab.Viewed]) {
tabs.unshift({ tabs.unshift({
name: 'Viewed', name: TableTab.Viewed,
label: t('Viewed'), label: t('Viewed'),
onClick: () => { onClick: () => {
setActiveChild('Viewed'); setActiveChild(TableTab.Viewed);
setItem(LocalStorageKeys.homepage_activity_filter, SetTabType.VIEWED); setItem(LocalStorageKeys.homepage_activity_filter, TableTab.Viewed);
}, },
}); });
} }
const renderActivity = () => const renderActivity = () =>
(activeChild !== 'Edited' ? activityData[activeChild] : editedObjs).map( (activeChild !== TableTab.Edited
(entity: ActivityObject) => { ? activityData[activeChild]
const url = getEntityUrl(entity); : editedObjs
const lastActionOn = getEntityLastActionOn(entity); ).map((entity: ActivityObject) => {
return ( const url = getEntityUrl(entity);
<CardStyles key={url}> const lastActionOn = getEntityLastActionOn(entity);
<Link to={url}> return (
<ListViewCard <CardStyles key={url}>
cover={<></>} <Link to={url}>
url={url} <ListViewCard
title={getEntityTitle(entity)} cover={<></>}
description={lastActionOn} url={url}
avatar={getEntityIcon(entity)} title={getEntityTitle(entity)}
actions={null} description={lastActionOn}
/> avatar={getEntityIcon(entity)}
</Link> actions={null}
</CardStyles> />
); </Link>
}, </CardStyles>
); );
});
const doneFetching = loadedCount < 3; const doneFetching = loadedCount < 3;
@ -214,7 +209,9 @@ export default function ActivityTable({
<Styles> <Styles>
<SubMenu activeChild={activeChild} tabs={tabs} /> <SubMenu activeChild={activeChild} tabs={tabs} />
{activityData[activeChild]?.length > 0 || {activityData[activeChild]?.length > 0 ||
(activeChild === 'Edited' && editedObjs && editedObjs.length > 0) ? ( (activeChild === TableTab.Edited &&
editedObjs &&
editedObjs.length > 0) ? (
<CardContainer className="recentCards"> <CardContainer className="recentCards">
{renderActivity()} {renderActivity()}
</CardContainer> </CardContainer>

View File

@ -62,6 +62,11 @@ describe('ChartTable', () => {
user: { user: {
userId: '2', userId: '2',
}, },
mine: [],
otherTabData: [],
otherTabFilters: [],
otherTabTitle: 'Other',
showThumbnails: false,
}; };
let wrapper: ReactWrapper; let wrapper: ReactWrapper;
@ -89,13 +94,35 @@ describe('ChartTable', () => {
expect(wrapper.find('ChartCard')).toExist(); expect(wrapper.find('ChartCard')).toExist();
}); });
it('renders other tab by default', async () => {
await act(async () => {
wrapper = mount(
<ChartTable
user={{ userId: '2' }}
mine={[]}
otherTabData={mockCharts}
otherTabFilters={[]}
otherTabTitle="Other"
showThumbnails={false}
store={store}
/>,
);
});
await waitForComponentToPaint(wrapper);
expect(wrapper.find('EmptyState')).not.toExist();
expect(wrapper.find('ChartCard')).toExist();
});
it('display EmptyState if there is no data', async () => { it('display EmptyState if there is no data', async () => {
await act(async () => { await act(async () => {
wrapper = mount( wrapper = mount(
<ChartTable <ChartTable
chartFilter="Mine"
user={{ userId: '2' }} user={{ userId: '2' }}
mine={[]} mine={[]}
otherTabData={[]}
otherTabFilters={[]}
otherTabTitle="Other"
showThumbnails={false}
store={store} store={store}
/>, />,
); );

View File

@ -26,15 +26,19 @@ import {
} from 'src/views/CRUD/hooks'; } from 'src/views/CRUD/hooks';
import { import {
getItem, getItem,
setItem,
LocalStorageKeys, LocalStorageKeys,
setItem,
} from 'src/utils/localStorageHelpers'; } from 'src/utils/localStorageHelpers';
import withToasts from 'src/components/MessageToasts/withToasts'; import withToasts from 'src/components/MessageToasts/withToasts';
import { useHistory } from 'react-router-dom'; 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 PropertiesModal from 'src/explore/components/PropertiesModal';
import { User } from 'src/types/bootstrapTypes'; 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 { LoadingCards } from 'src/views/CRUD/welcome/Welcome';
import ChartCard from 'src/views/CRUD/chart/ChartCard'; import ChartCard from 'src/views/CRUD/chart/ChartCard';
import Chart from 'src/types/Chart'; import Chart from 'src/types/Chart';
@ -48,12 +52,12 @@ import { WelcomeTable } from './types';
interface ChartTableProps { interface ChartTableProps {
addDangerToast: (message: string) => void; addDangerToast: (message: string) => void;
addSuccessToast: (message: string) => void; addSuccessToast: (message: string) => void;
search: string;
chartFilter?: string;
user?: User; user?: User;
mine: Array<any>; mine: Array<any>;
showThumbnails: boolean; showThumbnails: boolean;
examples?: Array<object>; otherTabData?: Array<object>;
otherTabFilters: Filter[];
otherTabTitle: string;
} }
function ChartTable({ function ChartTable({
@ -62,16 +66,17 @@ function ChartTable({
addSuccessToast, addSuccessToast,
mine, mine,
showThumbnails, showThumbnails,
examples, otherTabData,
otherTabFilters,
otherTabTitle,
}: ChartTableProps) { }: ChartTableProps) {
const history = useHistory(); const history = useHistory();
const filterStore = getItem( const initialTab = getItem(
LocalStorageKeys.homepage_chart_filter, 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 { const {
state: { loading, resourceCollection: charts, bulkSelectEnabled }, state: { loading, resourceCollection: charts, bulkSelectEnabled },
@ -84,7 +89,7 @@ function ChartTable({
t('chart'), t('chart'),
addDangerToast, addDangerToast,
true, true,
initialFilter === 'Mine' ? mine : filteredExamples, initialTab === TableTab.Mine ? mine : filteredOtherTabData,
[], [],
false, false,
); );
@ -102,36 +107,11 @@ function ChartTable({
closeChartEditModal, closeChartEditModal,
} = useChartEditModal(setCharts, charts); } = useChartEditModal(setCharts, charts);
const [chartFilter, setChartFilter] = useState(initialFilter); const [activeTab, setActiveTab] = useState(initialTab);
const [preparingExport, setPreparingExport] = useState<boolean>(false); const [preparingExport, setPreparingExport] = useState<boolean>(false);
const [loaded, setLoaded] = useState<boolean>(false); const [loaded, setLoaded] = useState<boolean>(false);
const getFilters = (filterName: string) => { const getData = (tab: TableTab) =>
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) =>
fetchData({ fetchData({
pageIndex: 0, pageIndex: 0,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
@ -141,15 +121,15 @@ function ChartTable({
desc: true, desc: true,
}, },
], ],
filters: getFilters(filter), filters: getFilterValues(tab, WelcomeTable.Charts, user, otherTabFilters),
}); });
useEffect(() => { useEffect(() => {
if (loaded || chartFilter === 'Favorite') { if (loaded || activeTab === TableTab.Favorite) {
getData(chartFilter); getData(activeTab);
} }
setLoaded(true); setLoaded(true);
}, [chartFilter]); }, [activeTab]);
const handleBulkChartExport = (chartsToExport: Chart[]) => { const handleBulkChartExport = (chartsToExport: Chart[]) => {
const ids = chartsToExport.map(({ id }) => id); const ids = chartsToExport.map(({ id }) => id);
@ -161,29 +141,29 @@ function ChartTable({
const menuTabs = [ const menuTabs = [
{ {
name: 'Favorite', name: TableTab.Favorite,
label: t('Favorite'), label: t('Favorite'),
onClick: () => { onClick: () => {
setChartFilter(TableTabTypes.FAVORITE); setActiveTab(TableTab.Favorite);
setItem(LocalStorageKeys.homepage_chart_filter, TableTabTypes.FAVORITE); setItem(LocalStorageKeys.homepage_chart_filter, TableTab.Favorite);
}, },
}, },
{ {
name: 'Mine', name: TableTab.Mine,
label: t('Mine'), label: t('Mine'),
onClick: () => { onClick: () => {
setChartFilter(TableTabTypes.MINE); setActiveTab(TableTab.Mine);
setItem(LocalStorageKeys.homepage_chart_filter, TableTabTypes.MINE); setItem(LocalStorageKeys.homepage_chart_filter, TableTab.Mine);
}, },
}, },
]; ];
if (examples) { if (otherTabData) {
menuTabs.push({ menuTabs.push({
name: 'Examples', name: TableTab.Other,
label: t('Examples'), label: otherTabTitle,
onClick: () => { onClick: () => {
setChartFilter(TableTabTypes.EXAMPLES); setActiveTab(TableTab.Other);
setItem(LocalStorageKeys.homepage_chart_filter, TableTabTypes.EXAMPLES); setItem(LocalStorageKeys.homepage_chart_filter, TableTab.Other);
}, },
}); });
} }
@ -201,7 +181,7 @@ function ChartTable({
)} )}
<SubMenu <SubMenu
activeChild={chartFilter} activeChild={activeTab}
tabs={menuTabs} tabs={menuTabs}
buttons={[ buttons={[
{ {
@ -221,7 +201,7 @@ function ChartTable({
buttonStyle: 'link', buttonStyle: 'link',
onClick: () => { onClick: () => {
const target = const target =
chartFilter === 'Favorite' activeTab === TableTab.Favorite
? `/chart/list/?filters=(favorite:(label:${t( ? `/chart/list/?filters=(favorite:(label:${t(
'Yes', 'Yes',
)},value:!t))` )},value:!t))`
@ -237,7 +217,7 @@ function ChartTable({
<ChartCard <ChartCard
key={`${e.id}`} key={`${e.id}`}
openChartEditModal={openChartEditModal} openChartEditModal={openChartEditModal}
chartFilter={chartFilter} chartFilter={activeTab}
chart={e} chart={e}
userId={user?.userId} userId={user?.userId}
hasPerm={hasPerm} hasPerm={hasPerm}
@ -253,7 +233,11 @@ function ChartTable({
))} ))}
</CardContainer> </CardContainer>
) : ( ) : (
<EmptyState tableName={WelcomeTable.Charts} tab={chartFilter} /> <EmptyState
tableName={WelcomeTable.Charts}
tab={activeTab}
otherTabTitle={otherTabTitle}
/>
)} )}
{preparingExport && <Loading />} {preparingExport && <Loading />}
</ErrorBoundary> </ErrorBoundary>

View File

@ -20,22 +20,19 @@ import React, { useEffect, useMemo, useState } from 'react';
import { SupersetClient, t } from '@superset-ui/core'; import { SupersetClient, t } from '@superset-ui/core';
import { filter } from 'lodash'; import { filter } from 'lodash';
import { useFavoriteStatus, useListViewResource } from 'src/views/CRUD/hooks'; import { useFavoriteStatus, useListViewResource } from 'src/views/CRUD/hooks';
import { import { Dashboard, DashboardTableProps, TableTab } from 'src/views/CRUD/types';
Dashboard,
DashboardTableProps,
TableTabTypes,
} from 'src/views/CRUD/types';
import handleResourceExport from 'src/utils/export'; import handleResourceExport from 'src/utils/export';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { import {
getItem, getItem,
setItem,
LocalStorageKeys, LocalStorageKeys,
setItem,
} from 'src/utils/localStorageHelpers'; } from 'src/utils/localStorageHelpers';
import { LoadingCards } from 'src/views/CRUD/welcome/Welcome'; import { LoadingCards } from 'src/views/CRUD/welcome/Welcome';
import { import {
CardContainer, CardContainer,
createErrorHandler, createErrorHandler,
getFilterValues,
PAGE_SIZE, PAGE_SIZE,
} from 'src/views/CRUD/utils'; } from 'src/views/CRUD/utils';
import withToasts from 'src/components/MessageToasts/withToasts'; import withToasts from 'src/components/MessageToasts/withToasts';
@ -46,28 +43,26 @@ import SubMenu from 'src/views/components/SubMenu';
import EmptyState from './EmptyState'; import EmptyState from './EmptyState';
import { WelcomeTable } from './types'; import { WelcomeTable } from './types';
export interface FilterValue {
col: string;
operator: string;
value: string | boolean | number | null | undefined;
}
function DashboardTable({ function DashboardTable({
user, user,
addDangerToast, addDangerToast,
addSuccessToast, addSuccessToast,
mine, mine,
showThumbnails, showThumbnails,
examples, otherTabData,
otherTabFilters,
otherTabTitle,
}: DashboardTableProps) { }: DashboardTableProps) {
const history = useHistory(); const history = useHistory();
const filterStore = getItem( const defaultTab = getItem(
LocalStorageKeys.homepage_dashboard_filter, 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 { const {
state: { loading, resourceCollection: dashboards }, state: { loading, resourceCollection: dashboards },
@ -80,7 +75,7 @@ function DashboardTable({
t('dashboard'), t('dashboard'),
addDangerToast, addDangerToast,
true, true,
defaultFilter === 'Mine' ? mine : filteredExamples, defaultTab === TableTab.Mine ? mine : filteredOtherTabData,
[], [],
false, false,
); );
@ -92,35 +87,11 @@ function DashboardTable({
); );
const [editModal, setEditModal] = useState<Dashboard>(); const [editModal, setEditModal] = useState<Dashboard>();
const [dashboardFilter, setDashboardFilter] = useState(defaultFilter); const [activeTab, setActiveTab] = useState(defaultTab);
const [preparingExport, setPreparingExport] = useState<boolean>(false); const [preparingExport, setPreparingExport] = useState<boolean>(false);
const [loaded, setLoaded] = useState<boolean>(false); const [loaded, setLoaded] = useState<boolean>(false);
const getFilters = (filterName: string) => { const getData = (tab: TableTab) =>
const filters = [];
if (filterName === 'Mine') {
filters.push({
id: 'owners',
operator: 'rel_m_m',
value: `${user?.userId}`,
});
} else if (filterName === 'Favorite') {
filters.push({
id: 'id',
operator: 'dashboard_is_favorite',
value: true,
});
} else if (filterName === 'Examples') {
filters.push({
id: 'created_by',
operator: 'rel_o_m',
value: 0,
});
}
return filters;
};
const getData = (filter: string) =>
fetchData({ fetchData({
pageIndex: 0, pageIndex: 0,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
@ -130,15 +101,20 @@ function DashboardTable({
desc: true, desc: true,
}, },
], ],
filters: getFilters(filter), filters: getFilterValues(
tab,
WelcomeTable.Dashboards,
user,
otherTabFilters,
),
}); });
useEffect(() => { useEffect(() => {
if (loaded || dashboardFilter === 'Favorite') { if (loaded || activeTab === TableTab.Favorite) {
getData(dashboardFilter); getData(activeTab);
} }
setLoaded(true); setLoaded(true);
}, [dashboardFilter]); }, [activeTab]);
const handleBulkDashboardExport = (dashboardsToExport: Dashboard[]) => { const handleBulkDashboardExport = (dashboardsToExport: Dashboard[]) => {
const ids = dashboardsToExport.map(({ id }) => id); const ids = dashboardsToExport.map(({ id }) => id);
@ -171,36 +147,30 @@ function DashboardTable({
const menuTabs = [ const menuTabs = [
{ {
name: 'Favorite', name: TableTab.Favorite,
label: t('Favorite'), label: t('Favorite'),
onClick: () => { onClick: () => {
setDashboardFilter(TableTabTypes.FAVORITE); setActiveTab(TableTab.Favorite);
setItem( setItem(LocalStorageKeys.homepage_dashboard_filter, TableTab.Favorite);
LocalStorageKeys.homepage_dashboard_filter,
TableTabTypes.FAVORITE,
);
}, },
}, },
{ {
name: 'Mine', name: TableTab.Mine,
label: t('Mine'), label: t('Mine'),
onClick: () => { onClick: () => {
setDashboardFilter(TableTabTypes.MINE); setActiveTab(TableTab.Mine);
setItem(LocalStorageKeys.homepage_dashboard_filter, TableTabTypes.MINE); setItem(LocalStorageKeys.homepage_dashboard_filter, TableTab.Mine);
}, },
}, },
]; ];
if (examples) { if (otherTabData) {
menuTabs.push({ menuTabs.push({
name: 'Examples', name: TableTab.Other,
label: t('Examples'), label: otherTabTitle,
onClick: () => { onClick: () => {
setDashboardFilter(TableTabTypes.EXAMPLES); setActiveTab(TableTab.Other);
setItem( setItem(LocalStorageKeys.homepage_dashboard_filter, TableTab.Other);
LocalStorageKeys.homepage_dashboard_filter,
TableTabTypes.EXAMPLES,
);
}, },
}); });
} }
@ -209,7 +179,7 @@ function DashboardTable({
return ( return (
<> <>
<SubMenu <SubMenu
activeChild={dashboardFilter} activeChild={activeTab}
tabs={menuTabs} tabs={menuTabs}
buttons={[ buttons={[
{ {
@ -229,7 +199,7 @@ function DashboardTable({
buttonStyle: 'link', buttonStyle: 'link',
onClick: () => { onClick: () => {
const target = const target =
dashboardFilter === 'Favorite' activeTab === TableTab.Favorite
? `/dashboard/list/?filters=(favorite:(label:${t( ? `/dashboard/list/?filters=(favorite:(label:${t(
'Yes', 'Yes',
)},value:!t))` )},value:!t))`
@ -256,7 +226,7 @@ function DashboardTable({
hasPerm={hasPerm} hasPerm={hasPerm}
bulkSelectEnabled={false} bulkSelectEnabled={false}
showThumbnails={showThumbnails} showThumbnails={showThumbnails}
dashboardFilter={dashboardFilter} dashboardFilter={activeTab}
refreshData={refreshData} refreshData={refreshData}
addDangerToast={addDangerToast} addDangerToast={addDangerToast}
addSuccessToast={addSuccessToast} addSuccessToast={addSuccessToast}
@ -273,7 +243,7 @@ function DashboardTable({
</CardContainer> </CardContainer>
)} )}
{dashboards.length === 0 && ( {dashboards.length === 0 && (
<EmptyState tableName={WelcomeTable.Dashboards} tab={dashboardFilter} /> <EmptyState tableName={WelcomeTable.Dashboards} tab={activeTab} />
)} )}
{preparingExport && <Loading />} {preparingExport && <Loading />}
</> </>

View File

@ -18,47 +18,48 @@
*/ */
import React from 'react'; import React from 'react';
import { styledMount as mount } from 'spec/helpers/theming'; 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'; import { WelcomeTable } from './types';
describe('EmptyState', () => { describe('EmptyState', () => {
const variants = [ const variants: EmptyStateProps[] = [
{ {
tab: 'Favorite', tab: TableTab.Favorite,
tableName: WelcomeTable.Dashboards, tableName: WelcomeTable.Dashboards,
}, },
{ {
tab: 'Mine', tab: TableTab.Mine,
tableName: WelcomeTable.Dashboards, tableName: WelcomeTable.Dashboards,
}, },
{ {
tab: 'Favorite', tab: TableTab.Favorite,
tableName: WelcomeTable.Charts, tableName: WelcomeTable.Charts,
}, },
{ {
tab: 'Mine', tab: TableTab.Mine,
tableName: WelcomeTable.Charts, tableName: WelcomeTable.Charts,
}, },
{ {
tab: 'Favorite', tab: TableTab.Favorite,
tableName: WelcomeTable.SavedQueries, tableName: WelcomeTable.SavedQueries,
}, },
{ {
tab: 'Mine', tab: TableTab.Mine,
tableName: WelcomeTable.SavedQueries, tableName: WelcomeTable.SavedQueries,
}, },
]; ];
const recents = [ const recents: EmptyStateProps[] = [
{ {
tab: 'Viewed', tab: TableTab.Viewed,
tableName: WelcomeTable.Recents, tableName: WelcomeTable.Recents,
}, },
{ {
tab: 'Edited', tab: TableTab.Edited,
tableName: WelcomeTable.Recents, tableName: WelcomeTable.Recents,
}, },
{ {
tab: 'Created', tab: TableTab.Created,
tableName: WelcomeTable.Recents, tableName: WelcomeTable.Recents,
}, },
]; ];
@ -68,10 +69,10 @@ describe('EmptyState', () => {
expect(wrapper).toExist(); expect(wrapper).toExist();
const textContainer = wrapper.find('.ant-empty-description'); const textContainer = wrapper.find('.ant-empty-description');
expect(textContainer.text()).toEqual( expect(textContainer.text()).toEqual(
variant.tab === 'Favorite' variant.tab === TableTab.Favorite
? "You don't have any favorites yet!" ? "You don't have any favorites yet!"
: `No ${ : `No ${
variant.tableName === 'SAVED_QUERIES' variant.tableName === WelcomeTable.SavedQueries
? 'saved queries' ? 'saved queries'
: variant.tableName.toLowerCase() : variant.tableName.toLowerCase()
} yet`, } yet`,
@ -80,13 +81,13 @@ describe('EmptyState', () => {
}); });
}); });
recents.forEach(recent => { 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(<EmptyState {...recent} />); const wrapper = mount(<EmptyState {...recent} />);
expect(wrapper).toExist(); expect(wrapper).toExist();
const textContainer = wrapper.find('.ant-empty-description'); const textContainer = wrapper.find('.ant-empty-description');
expect(wrapper.find('.ant-empty-image').children()).toHaveLength(1); expect(wrapper.find('.ant-empty-image').children()).toHaveLength(1);
expect(textContainer.text()).toContain( 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`,
); );
}); });
}); });

View File

@ -19,7 +19,8 @@
import React from 'react'; import React from 'react';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import { Empty } from 'src/components'; 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'; import { WelcomeTable } from './types';
const welcomeTableLabels: Record<WelcomeTable, string> = { const welcomeTableLabels: Record<WelcomeTable, string> = {
@ -29,9 +30,10 @@ const welcomeTableLabels: Record<WelcomeTable, string> = {
[WelcomeTable.SavedQueries]: t('saved queries'), [WelcomeTable.SavedQueries]: t('saved queries'),
}; };
interface EmptyStateProps { export interface EmptyStateProps {
tableName: WelcomeTable; tableName: WelcomeTable;
tab?: string; tab?: string;
otherTabTitle?: string;
} }
const EmptyContainer = styled.div` const EmptyContainer = styled.div`
min-height: 200px; min-height: 200px;
@ -52,7 +54,11 @@ type Redirects = Record<
string string
>; >;
export default function EmptyState({ tableName, tab }: EmptyStateProps) { export default function EmptyState({
tableName,
tab,
otherTabTitle,
}: EmptyStateProps) {
const mineRedirects: Redirects = { const mineRedirects: Redirects = {
[WelcomeTable.Charts]: '/chart/add', [WelcomeTable.Charts]: '/chart/add',
[WelcomeTable.Dashboards]: '/dashboard/new', [WelcomeTable.Dashboards]: '/dashboard/new',
@ -77,22 +83,23 @@ export default function EmptyState({ tableName, tab }: EmptyStateProps) {
const recent = ( const recent = (
<span className="no-recents"> <span className="no-recents">
{(() => { {(() => {
if (tab === 'Viewed') { if (tab === TableTab.Viewed) {
return t( return t(
`Recently viewed charts, dashboards, and saved queries will appear here`, `Recently viewed charts, dashboards, and saved queries will appear here`,
); );
} }
if (tab === 'Created') { if (tab === TableTab.Created) {
return t( return t(
'Recently created charts, dashboards, and saved queries will appear here', 'Recently created charts, dashboards, and saved queries will appear here',
); );
} }
if (tab === 'Examples') { if (tab === TableTab.Other) {
return t('Example %(tableName)s will appear here', { return t('%(other)s %(tableName)s will appear here', {
other: otherTabTitle || t('Other'),
tableName: tableName.toLowerCase(), tableName: tableName.toLowerCase(),
}); });
} }
if (tab === 'Edited') { if (tab === TableTab.Edited) {
return t( return t(
`Recently edited charts, dashboards, and saved queries will appear here`, `Recently edited charts, dashboards, and saved queries will appear here`,
); );
@ -101,17 +108,24 @@ export default function EmptyState({ tableName, tab }: EmptyStateProps) {
})()} })()}
</span> </span>
); );
// Mine and Recent Activity(all tabs) tab empty state // 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 ( return (
<EmptyContainer> <EmptyContainer>
<Empty <Empty
image={`/static/assets/images/${tableIcon[tableName]}`} image={`/static/assets/images/${tableIcon[tableName]}`}
description={ description={
tableName === 'RECENTS' || tab === 'Examples' ? recent : mine tableName === WelcomeTable.Recents || tab === TableTab.Other
? recent
: mine
} }
> >
{tableName !== 'RECENTS' && ( {tableName !== WelcomeTable.Recents && (
<ButtonContainer> <ButtonContainer>
<Button <Button
buttonStyle="primary" buttonStyle="primary"
@ -120,7 +134,7 @@ export default function EmptyState({ tableName, tab }: EmptyStateProps) {
}} }}
> >
<i className="fa fa-plus" /> <i className="fa fa-plus" />
{tableName === 'SAVED_QUERIES' {tableName === WelcomeTable.SavedQueries
? t('SQL query') ? t('SQL query')
: tableName : tableName
.split('') .split('')
@ -152,7 +166,7 @@ export default function EmptyState({ tableName, tab }: EmptyStateProps) {
> >
{t('See all %(tableName)s', { {t('See all %(tableName)s', {
tableName: tableName:
tableName === 'SAVED_QUERIES' tableName === WelcomeTable.SavedQueries
? t('SQL Lab queries') ? t('SQL Lab queries')
: welcomeTableLabels[tableName], : welcomeTableLabels[tableName],
})} })}

View File

@ -22,6 +22,7 @@ import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light';
import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql';
import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
import { LoadingCards } from 'src/views/CRUD/welcome/Welcome'; import { LoadingCards } from 'src/views/CRUD/welcome/Welcome';
import { TableTab } from 'src/views/CRUD/types';
import withToasts from 'src/components/MessageToasts/withToasts'; import withToasts from 'src/components/MessageToasts/withToasts';
import { AntdDropdown } from 'src/components'; import { AntdDropdown } from 'src/components';
import { Menu } from 'src/components/Menu'; import { Menu } from 'src/components/Menu';
@ -30,10 +31,12 @@ import ListViewCard from 'src/components/ListViewCard';
import DeleteModal from 'src/components/DeleteModal'; import DeleteModal from 'src/components/DeleteModal';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
import SubMenu from 'src/views/components/SubMenu'; import SubMenu from 'src/views/components/SubMenu';
import { User } from 'src/types/bootstrapTypes';
import EmptyState from './EmptyState'; import EmptyState from './EmptyState';
import { import {
CardContainer, CardContainer,
createErrorHandler, createErrorHandler,
getFilterValues,
PAGE_SIZE, PAGE_SIZE,
shortenSQL, shortenSQL,
} from '../utils'; } from '../utils';
@ -56,9 +59,7 @@ interface Query {
} }
interface SavedQueriesProps { interface SavedQueriesProps {
user: { user: User;
userId: string | number;
};
queryFilter: string; queryFilter: string;
addDangerToast: (arg0: string) => void; addDangerToast: (arg0: string) => void;
addSuccessToast: (arg0: string) => void; addSuccessToast: (arg0: string) => void;
@ -134,7 +135,7 @@ const SavedQueries = ({
[], [],
false, false,
); );
const [queryFilter, setQueryFilter] = useState('Mine'); const [activeTab, setActiveTab] = useState(TableTab.Mine);
const [queryDeleteModal, setQueryDeleteModal] = useState(false); const [queryDeleteModal, setQueryDeleteModal] = useState(false);
const [currentlyEdited, setCurrentlyEdited] = useState<Query>({}); const [currentlyEdited, setCurrentlyEdited] = useState<Query>({});
const [ifMine, setMine] = useState(true); const [ifMine, setMine] = useState(true);
@ -149,13 +150,11 @@ const SavedQueries = ({
}).then( }).then(
() => { () => {
const queryParams = { const queryParams = {
filters: [ filters: getFilterValues(
{ TableTab.Created,
id: 'created_by', WelcomeTable.SavedQueries,
operator: 'rel_o_m', user,
value: `${user?.userId}`, ),
},
],
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
sortBy: [ sortBy: [
{ {
@ -178,25 +177,7 @@ const SavedQueries = ({
); );
}; };
const getFilters = (filterName: string) => { const getData = (tab: TableTab) =>
const filters = [];
if (filterName === 'Mine') {
filters.push({
id: 'created_by',
operator: 'rel_o_m',
value: `${user?.userId}`,
});
} else {
filters.push({
id: 'id',
operator: 'saved_query_is_fav',
value: true,
});
}
return filters;
};
const getData = (filter: string) =>
fetchData({ fetchData({
pageIndex: 0, pageIndex: 0,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
@ -206,7 +187,7 @@ const SavedQueries = ({
desc: true, desc: true,
}, },
], ],
filters: getFilters(filter), filters: getFilterValues(tab, WelcomeTable.SavedQueries, user),
}); });
const renderMenu = (query: Query) => ( const renderMenu = (query: Query) => (
@ -263,21 +244,13 @@ const SavedQueries = ({
/> />
)} )}
<SubMenu <SubMenu
activeChild={queryFilter} activeChild={activeTab}
tabs={[ tabs={[
/* @TODO uncomment when fav functionality is implemented
{ {
name: 'Favorite', name: TableTab.Mine,
label: t('Favorite'),
onClick: () => {
getData('Favorite').then(() => setQueryFilter('Favorite'));
},
},
*/
{
name: 'Mine',
label: t('Mine'), label: t('Mine'),
onClick: () => getData('Mine').then(() => setQueryFilter('Mine')), onClick: () =>
getData(TableTab.Mine).then(() => setActiveTab(TableTab.Mine)),
}, },
]} ]}
buttons={[ buttons={[
@ -366,7 +339,7 @@ const SavedQueries = ({
))} ))}
</CardContainer> </CardContainer>
) : ( ) : (
<EmptyState tableName={WelcomeTable.SavedQueries} tab={queryFilter} /> <EmptyState tableName={WelcomeTable.SavedQueries} tab={activeTab} />
)} )}
</> </>
); );

View File

@ -16,8 +16,13 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React, { useEffect, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { styled, t, getExtensionsRegistry } from '@superset-ui/core'; import {
getExtensionsRegistry,
JsonObject,
styled,
t,
} from '@superset-ui/core';
import Collapse from 'src/components/Collapse'; import Collapse from 'src/components/Collapse';
import { User } from 'src/types/bootstrapTypes'; import { User } from 'src/types/bootstrapTypes';
import { reject } from 'lodash'; import { reject } from 'lodash';
@ -40,7 +45,9 @@ import {
} from 'src/views/CRUD/utils'; } from 'src/views/CRUD/utils';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { AntdSwitch } from 'src/components'; import { AntdSwitch } from 'src/components';
import getBootstrapData from 'src/utils/getBootstrapData';
import { TableTab } from 'src/views/CRUD/types';
import { WelcomePageLastTab } from './types';
import ActivityTable from './ActivityTable'; import ActivityTable from './ActivityTable';
import ChartTable from './ChartTable'; import ChartTable from './ChartTable';
import SavedQueries from './SavedQueries'; import SavedQueries from './SavedQueries';
@ -54,10 +61,10 @@ interface WelcomeProps {
} }
export interface ActivityData { export interface ActivityData {
Created?: Array<object>; [TableTab.Created]?: JsonObject[];
Edited?: Array<object>; [TableTab.Edited]?: JsonObject[];
Viewed?: Array<object>; [TableTab.Viewed]?: JsonObject[];
Examples?: Array<object>; [TableTab.Other]?: JsonObject[];
} }
interface LoadingProps { interface LoadingProps {
@ -138,6 +145,8 @@ const WelcomeNav = styled.div`
`} `}
`; `;
const bootstrapData = getBootstrapData();
export const LoadingCards = ({ cover }: LoadingProps) => ( export const LoadingCards = ({ cover }: LoadingProps) => (
<CardContainer showThumbnails={cover} className="loading-cards"> <CardContainer showThumbnails={cover} className="loading-cards">
{[...new Array(loadingCardCount)].map((_, index) => ( {[...new Array(loadingCardCount)].map((_, index) => (
@ -185,28 +194,58 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
'welcome.main.replacement', 'welcome.main.replacement',
); );
const [otherTabTitle, otherTabFilters] = useMemo(() => {
const lastTab = bootstrapData.common?.conf
.WELCOME_PAGE_LAST_TAB as WelcomePageLastTab;
const [customTitle, customFilter] = Array.isArray(lastTab)
? lastTab
: [undefined, undefined];
if (customTitle && customFilter) {
return [t(customTitle), customFilter];
}
if (lastTab === 'all') {
return [t('All'), []];
}
return [
t('Examples'),
[
{
col: 'created_by',
opr: 'rel_o_m',
value: 0,
},
],
];
}, []);
useEffect(() => { useEffect(() => {
if (!otherTabFilters) {
return;
}
const activeTab = getItem(LocalStorageKeys.homepage_activity_filter, null); const activeTab = getItem(LocalStorageKeys.homepage_activity_filter, null);
setActiveState(collapseState.length > 0 ? collapseState : DEFAULT_TAB_ARR); setActiveState(collapseState.length > 0 ? collapseState : DEFAULT_TAB_ARR);
getRecentAcitivtyObjs(user.userId!, recent, addDangerToast) getRecentAcitivtyObjs(user.userId!, recent, addDangerToast, otherTabFilters)
.then(res => { .then(res => {
const data: ActivityData | null = {}; const data: ActivityData | null = {};
data.Examples = res.examples; data[TableTab.Other] = res.other;
if (res.viewed) { if (res.viewed) {
const filtered = reject(res.viewed, ['item_url', null]).map(r => r); const filtered = reject(res.viewed, ['item_url', null]).map(r => r);
data.Viewed = filtered; data[TableTab.Viewed] = filtered;
if (!activeTab && data.Viewed) { if (!activeTab && data[TableTab.Viewed]) {
setActiveChild('Viewed'); setActiveChild(TableTab.Viewed);
} else if (!activeTab && !data.Viewed) { } else if (!activeTab && !data[TableTab.Viewed]) {
setActiveChild('Created'); setActiveChild(TableTab.Created);
} else setActiveChild(activeTab || 'Created'); } else setActiveChild(activeTab || TableTab.Created);
} else if (!activeTab) setActiveChild('Created'); } else if (!activeTab) setActiveChild(TableTab.Created);
else setActiveChild(activeTab); else setActiveChild(activeTab);
setActivityData(activityData => ({ ...activityData, ...data })); setActivityData(activityData => ({ ...activityData, ...data }));
}) })
.catch( .catch(
createErrorHandler((errMsg: unknown) => { createErrorHandler((errMsg: unknown) => {
setActivityData(activityData => ({ ...activityData, Viewed: [] })); setActivityData(activityData => ({
...activityData,
[TableTab.Viewed]: [],
}));
addDangerToast( addDangerToast(
t('There was an issue fetching your recent activity: %s', errMsg), t('There was an issue fetching your recent activity: %s', errMsg),
); );
@ -255,7 +294,7 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
t('There was an issues fetching your saved queries: %s', err), t('There was an issues fetching your saved queries: %s', err),
); );
}); });
}, []); }, [otherTabFilters]);
const handleToggle = () => { const handleToggle = () => {
setChecked(!checked); setChecked(!checked);
@ -277,13 +316,13 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
}, [chartData, queryData, dashboardData]); }, [chartData, queryData, dashboardData]);
useEffect(() => { useEffect(() => {
if (!collapseState && activityData?.Viewed?.length) { if (!collapseState && activityData?.[TableTab.Viewed]?.length) {
setActiveState(activeState => ['1', ...activeState]); setActiveState(activeState => ['1', ...activeState]);
} }
}, [activityData]); }, [activityData]);
const isRecentActivityLoading = const isRecentActivityLoading =
!activityData?.Examples && !activityData?.Viewed; !activityData?.[TableTab.Other] && !activityData?.[TableTab.Viewed];
return ( return (
<WelcomeContainer> <WelcomeContainer>
{WelcomeMessageExtension && <WelcomeMessageExtension />} {WelcomeMessageExtension && <WelcomeMessageExtension />}
@ -308,9 +347,9 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
> >
<Collapse.Panel header={t('Recents')} key="1"> <Collapse.Panel header={t('Recents')} key="1">
{activityData && {activityData &&
(activityData.Viewed || (activityData[TableTab.Viewed] ||
activityData.Examples || activityData[TableTab.Other] ||
activityData.Created) && activityData[TableTab.Created]) &&
activeChild !== 'Loading' ? ( activeChild !== 'Loading' ? (
<ActivityTable <ActivityTable
user={{ userId: user.userId! }} // user is definitely not a guest user on this page user={{ userId: user.userId! }} // user is definitely not a guest user on this page
@ -331,7 +370,9 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
user={user} user={user}
mine={dashboardData} mine={dashboardData}
showThumbnails={checked} showThumbnails={checked}
examples={activityData?.Examples} otherTabData={activityData?.[TableTab.Other]}
otherTabFilters={otherTabFilters}
otherTabTitle={otherTabTitle}
/> />
)} )}
</Collapse.Panel> </Collapse.Panel>
@ -343,7 +384,9 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
showThumbnails={checked} showThumbnails={checked}
user={user} user={user}
mine={chartData} mine={chartData}
examples={activityData?.Examples} otherTabData={activityData?.[TableTab.Other]}
otherTabFilters={otherTabFilters}
otherTabTitle={otherTabTitle}
/> />
)} )}
</Collapse.Panel> </Collapse.Panel>

View File

@ -17,9 +17,13 @@
* under the License. * under the License.
*/ */
import { Filter } from '../types';
export enum WelcomeTable { export enum WelcomeTable {
Charts = 'CHARTS', Charts = 'CHARTS',
Dashboards = 'DASHBOARDS', Dashboards = 'DASHBOARDS',
Recents = 'RECENTS', Recents = 'RECENTS',
SavedQueries = 'SAVED_QUERIES', SavedQueries = 'SAVED_QUERIES',
} }
export type WelcomePageLastTab = 'examples' | 'all' | [string, Filter[]];

View File

@ -29,65 +29,18 @@ import { GenericLink } from 'src/components/GenericLink/GenericLink';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
import { useUiConfig } from 'src/components/UiConfigContext'; import { useUiConfig } from 'src/components/UiConfigContext';
import { URL_PARAMS } from 'src/constants'; import { URL_PARAMS } from 'src/constants';
import {
MenuObjectChildProps,
MenuObjectProps,
MenuData,
} from 'src/types/bootstrapTypes';
import RightMenu from './RightMenu'; import RightMenu from './RightMenu';
import { Languages } from './LanguagePicker';
interface BrandProps { interface MenuProps {
path: string; data: MenuData;
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 MenuProps {
data: {
menu: MenuObjectProps[];
brand: BrandProps;
navbar_right: NavBarProps;
settings: MenuObjectProps[];
environment_tag: {
text: string;
color: string;
};
};
isFrontendRoute?: (path?: string) => boolean; isFrontendRoute?: (path?: string) => boolean;
} }
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;
}
const StyledHeader = styled.header` const StyledHeader = styled.header`
${({ theme }) => ` ${({ theme }) => `
background-color: ${theme.colors.grayscale.light5}; background-color: ${theme.colors.grayscale.light5};

View File

@ -38,7 +38,10 @@ import Icons from 'src/components/Icons';
import Label from 'src/components/Label'; import Label from 'src/components/Label';
import { findPermission } from 'src/utils/findPermission'; import { findPermission } from 'src/utils/findPermission';
import { isUserAdmin } from 'src/dashboard/util/permissionUtils'; import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; import {
MenuObjectProps,
UserWithPermissionsAndRoles,
} from 'src/types/bootstrapTypes';
import { RootState } from 'src/dashboard/types'; import { RootState } from 'src/dashboard/types';
import LanguagePicker from './LanguagePicker'; import LanguagePicker from './LanguagePicker';
import DatabaseModal from '../CRUD/data/database/DatabaseModal'; import DatabaseModal from '../CRUD/data/database/DatabaseModal';
@ -48,7 +51,6 @@ import {
GlobalMenuDataOptions, GlobalMenuDataOptions,
RightMenuProps, RightMenuProps,
} from './types'; } from './types';
import { MenuObjectProps } from './Menu';
import AddDatasetModal from '../CRUD/data/dataset/AddDatasetModal'; import AddDatasetModal from '../CRUD/data/dataset/AddDatasetModal';
const extensionsRegistry = getExtensionsRegistry(); const extensionsRegistry = getExtensionsRegistry();

View File

@ -26,7 +26,7 @@ import { Row } from 'src/components';
import { Menu, MenuMode, MainNav as DropdownMenu } from 'src/components/Menu'; import { Menu, MenuMode, MainNav as DropdownMenu } from 'src/components/Menu';
import Button, { OnClickHandler } from 'src/components/Button'; import Button, { OnClickHandler } from 'src/components/Button';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
import { MenuObjectProps } from './Menu'; import { MenuObjectProps } from 'src/types/bootstrapTypes';
const StyledHeader = styled.div` const StyledHeader = styled.div`
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px; margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;

View File

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import { NavBarProps, MenuObjectProps } from './Menu'; import { NavBarProps, MenuObjectProps } from 'src/types/bootstrapTypes';
export interface ExtentionConfigs { export interface ExtentionConfigs {
ALLOWED_EXTENSIONS: Array<any>; ALLOWED_EXTENSIONS: Array<any>;

View File

@ -41,6 +41,7 @@ from typing import (
Literal, Literal,
Optional, Optional,
Set, Set,
Tuple,
Type, Type,
TYPE_CHECKING, TYPE_CHECKING,
Union, Union,
@ -1452,6 +1453,17 @@ ADVANCED_DATA_TYPES: Dict[str, AdvancedDataType] = {
"port": internet_port, "port": internet_port,
} }
# By default, the Welcome page features example charts and dashboards. This can be
# changed to show all charts/dashboards the user has access to, or a custom view
# by providing the title and a FAB filter:
# WELCOME_PAGE_LAST_TAB = (
# "Xyz",
# [{"col": 'created_by', "opr": 'rel_o_m', "value": 10}],
# )
WELCOME_PAGE_LAST_TAB: Union[
Literal["examples", "all"], Tuple[str, List[Dict[str, Any]]]
] = "examples"
# Configuration for environment tag shown on the navbar. Setting 'text' to '' will hide the tag. # Configuration for environment tag shown on the navbar. Setting 'text' to '' will hide the tag.
# 'color' can either be a hex color code, or a dot-indexed theme color (e.g. error.base) # 'color' can either be a hex color code, or a dot-indexed theme color (e.g. error.base)
ENVIRONMENT_TAG_CONFIG = { ENVIRONMENT_TAG_CONFIG = {

View File

@ -19,13 +19,11 @@ import textwrap
from typing import Dict, List, Tuple, Union from typing import Dict, List, Tuple, Union
import pandas as pd import pandas as pd
from flask_appbuilder.security.sqla.models import User
from sqlalchemy import DateTime, inspect, String from sqlalchemy import DateTime, inspect, String
from sqlalchemy.sql import column from sqlalchemy.sql import column
from superset import app, db, security_manager from superset import app, db
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
from superset.exceptions import NoDataException
from superset.models.core import Database from superset.models.core import Database
from superset.models.dashboard import Dashboard from superset.models.dashboard import Dashboard
from superset.models.slice import Slice from superset.models.slice import Slice
@ -42,17 +40,6 @@ from .helpers import (
) )
def get_admin_user() -> User:
admin = security_manager.find_user("admin")
if admin is None:
raise NoDataException(
"Admin user does not exist. "
"Please, check if test users are properly loaded "
"(`superset load_test_users`)."
)
return admin
def gen_filter( def gen_filter(
subject: str, comparator: str, operator: str = "==" subject: str, comparator: str, operator: str = "=="
) -> Dict[str, Union[bool, str]]: ) -> Dict[str, Union[bool, str]]:
@ -125,7 +112,7 @@ def load_birth_names(
db.session.commit() db.session.commit()
slices, _ = create_slices(obj, admin_owner=True) slices, _ = create_slices(obj)
create_dashboard(slices) create_dashboard(slices)
@ -165,7 +152,7 @@ def _add_table_metrics(datasource: SqlaTable) -> None:
datasource.metrics = metrics datasource.metrics = metrics
def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[Slice]]: def create_slices(tbl: SqlaTable) -> Tuple[List[Slice], List[Slice]]:
metrics = [ metrics = [
{ {
"expressionType": "SIMPLE", "expressionType": "SIMPLE",
@ -206,26 +193,16 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
], ],
} }
admin = get_admin_user() slice_kwargs = {
if admin_owner: "datasource_id": tbl.id,
slice_props = dict( "datasource_type": DatasourceType.TABLE,
datasource_id=tbl.id, "owners": [],
datasource_type=DatasourceType.TABLE, }
owners=[admin],
created_by=admin,
)
else:
slice_props = dict(
datasource_id=tbl.id,
datasource_type=DatasourceType.TABLE,
owners=[],
created_by=admin,
)
print("Creating some slices") print("Creating some slices")
slices = [ slices = [
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Participants", slice_name="Participants",
viz_type="big_number", viz_type="big_number",
params=get_slice_json( params=get_slice_json(
@ -238,7 +215,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Genders", slice_name="Genders",
viz_type="pie", viz_type="pie",
params=get_slice_json( params=get_slice_json(
@ -246,7 +223,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Trends", slice_name="Trends",
viz_type="line", viz_type="line",
params=get_slice_json( params=get_slice_json(
@ -260,7 +237,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Genders by State", slice_name="Genders by State",
viz_type="dist_bar", viz_type="dist_bar",
params=get_slice_json( params=get_slice_json(
@ -296,7 +273,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Girls", slice_name="Girls",
viz_type="table", viz_type="table",
params=get_slice_json( params=get_slice_json(
@ -309,7 +286,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Girl Name Cloud", slice_name="Girl Name Cloud",
viz_type="word_cloud", viz_type="word_cloud",
params=get_slice_json( params=get_slice_json(
@ -325,7 +302,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Boys", slice_name="Boys",
viz_type="table", viz_type="table",
params=get_slice_json( params=get_slice_json(
@ -338,7 +315,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Boy Name Cloud", slice_name="Boy Name Cloud",
viz_type="word_cloud", viz_type="word_cloud",
params=get_slice_json( params=get_slice_json(
@ -354,7 +331,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Top 10 Girl Name Share", slice_name="Top 10 Girl Name Share",
viz_type="area", viz_type="area",
params=get_slice_json( params=get_slice_json(
@ -371,7 +348,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Top 10 Boy Name Share", slice_name="Top 10 Boy Name Share",
viz_type="area", viz_type="area",
params=get_slice_json( params=get_slice_json(
@ -388,7 +365,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Pivot Table v2", slice_name="Pivot Table v2",
viz_type="pivot_table_v2", viz_type="pivot_table_v2",
params=get_slice_json( params=get_slice_json(
@ -411,7 +388,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
] ]
misc_slices = [ misc_slices = [
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Average and Sum Trends", slice_name="Average and Sum Trends",
viz_type="dual_line", viz_type="dual_line",
params=get_slice_json( params=get_slice_json(
@ -430,13 +407,13 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Num Births Trend", slice_name="Num Births Trend",
viz_type="line", viz_type="line",
params=get_slice_json(defaults, viz_type="line", metrics=metrics), params=get_slice_json(defaults, viz_type="line", metrics=metrics),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Daily Totals", slice_name="Daily Totals",
viz_type="table", viz_type="table",
params=get_slice_json( params=get_slice_json(
@ -449,7 +426,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Number of California Births", slice_name="Number of California Births",
viz_type="big_number_total", viz_type="big_number_total",
params=get_slice_json( params=get_slice_json(
@ -468,7 +445,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Top 10 California Names Timeseries", slice_name="Top 10 California Names Timeseries",
viz_type="line", viz_type="line",
params=get_slice_json( params=get_slice_json(
@ -500,7 +477,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Names Sorted by Num in California", slice_name="Names Sorted by Num in California",
viz_type="table", viz_type="table",
params=get_slice_json( params=get_slice_json(
@ -520,7 +497,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Number of Girls", slice_name="Number of Girls",
viz_type="big_number_total", viz_type="big_number_total",
params=get_slice_json( params=get_slice_json(
@ -533,7 +510,7 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Pivot Table", slice_name="Pivot Table",
viz_type="pivot_table", viz_type="pivot_table",
params=get_slice_json( params=get_slice_json(
@ -557,12 +534,10 @@ def create_slices(tbl: SqlaTable, admin_owner: bool) -> Tuple[List[Slice], List[
def create_dashboard(slices: List[Slice]) -> Dashboard: def create_dashboard(slices: List[Slice]) -> Dashboard:
print("Creating a dashboard") print("Creating a dashboard")
admin = get_admin_user()
dash = db.session.query(Dashboard).filter_by(slug="births").first() dash = db.session.query(Dashboard).filter_by(slug="births").first()
if not dash: if not dash:
dash = Dashboard() dash = Dashboard()
dash.owners = [admin] dash.owners = []
dash.created_by = admin
db.session.add(dash) db.session.add(dash)
dash.published = True dash.published = True

View File

@ -23,7 +23,7 @@ from typing import List
from sqlalchemy import inspect from sqlalchemy import inspect
from superset import db, security_manager from superset import db
from superset.connectors.sqla.models import SqlaTable from superset.connectors.sqla.models import SqlaTable
from superset.models.dashboard import Dashboard from superset.models.dashboard import Dashboard
from superset.models.slice import Slice from superset.models.slice import Slice
@ -41,13 +41,11 @@ DASH_SLUG = "supported_charts_dash"
def create_slices(tbl: SqlaTable) -> List[Slice]: def create_slices(tbl: SqlaTable) -> List[Slice]:
admin = security_manager.find_user("admin") slice_kwargs = {
slice_props = dict( "datasource_id": tbl.id,
datasource_id=tbl.id, "datasource_type": DatasourceType.TABLE,
datasource_type=DatasourceType.TABLE, "owners": [],
owners=[admin], }
created_by=admin,
)
defaults = { defaults = {
"limit": "25", "limit": "25",
@ -62,7 +60,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
# TIER 1 # TIER 1
# --------------------- # ---------------------
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Big Number", slice_name="Big Number",
viz_type="big_number_total", viz_type="big_number_total",
params=get_slice_json( params=get_slice_json(
@ -72,7 +70,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Big Number with Trendline", slice_name="Big Number with Trendline",
viz_type="big_number", viz_type="big_number",
params=get_slice_json( params=get_slice_json(
@ -82,7 +80,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Table", slice_name="Table",
viz_type="table", viz_type="table",
params=get_slice_json( params=get_slice_json(
@ -93,7 +91,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Pivot Table", slice_name="Pivot Table",
viz_type="pivot_table_v2", viz_type="pivot_table_v2",
params=get_slice_json( params=get_slice_json(
@ -105,7 +103,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Time-Series Line Chart", slice_name="Time-Series Line Chart",
viz_type="echarts_timeseries_line", viz_type="echarts_timeseries_line",
params=get_slice_json( params=get_slice_json(
@ -116,7 +114,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Time-Series Area Chart", slice_name="Time-Series Area Chart",
viz_type="echarts_area", viz_type="echarts_area",
params=get_slice_json( params=get_slice_json(
@ -127,7 +125,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Time-Series Bar Chart V2", slice_name="Time-Series Bar Chart V2",
viz_type="echarts_timeseries_bar", viz_type="echarts_timeseries_bar",
params=get_slice_json( params=get_slice_json(
@ -138,7 +136,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Time-Series Scatter Chart", slice_name="Time-Series Scatter Chart",
viz_type="echarts_timeseries_scatter", viz_type="echarts_timeseries_scatter",
params=get_slice_json( params=get_slice_json(
@ -149,7 +147,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Pie Chart", slice_name="Pie Chart",
viz_type="pie", viz_type="pie",
params=get_slice_json( params=get_slice_json(
@ -161,7 +159,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Bar Chart", slice_name="Bar Chart",
viz_type="dist_bar", viz_type="dist_bar",
params=get_slice_json( params=get_slice_json(
@ -175,7 +173,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
# TIER 2 # TIER 2
# --------------------- # ---------------------
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Box Plot Chart", slice_name="Box Plot Chart",
viz_type="box_plot", viz_type="box_plot",
params=get_slice_json( params=get_slice_json(
@ -187,7 +185,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Bubble Chart", slice_name="Bubble Chart",
viz_type="bubble", viz_type="bubble",
params=get_slice_json( params=get_slice_json(
@ -217,7 +215,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Calendar Heatmap", slice_name="Calendar Heatmap",
viz_type="cal_heatmap", viz_type="cal_heatmap",
params=get_slice_json( params=get_slice_json(
@ -228,7 +226,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Chord Chart", slice_name="Chord Chart",
viz_type="chord", viz_type="chord",
params=get_slice_json( params=get_slice_json(
@ -240,7 +238,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Time-Series Percent Change Chart", slice_name="Time-Series Percent Change Chart",
viz_type="compare", viz_type="compare",
params=get_slice_json( params=get_slice_json(
@ -251,7 +249,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Time-Series Generic Chart", slice_name="Time-Series Generic Chart",
viz_type="echarts_timeseries", viz_type="echarts_timeseries",
params=get_slice_json( params=get_slice_json(
@ -262,7 +260,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Time-Series Smooth Line Chart", slice_name="Time-Series Smooth Line Chart",
viz_type="echarts_timeseries_smooth", viz_type="echarts_timeseries_smooth",
params=get_slice_json( params=get_slice_json(
@ -273,7 +271,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Time-Series Step Line Chart", slice_name="Time-Series Step Line Chart",
viz_type="echarts_timeseries_step", viz_type="echarts_timeseries_step",
params=get_slice_json( params=get_slice_json(
@ -284,7 +282,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Funnel Chart", slice_name="Funnel Chart",
viz_type="funnel", viz_type="funnel",
params=get_slice_json( params=get_slice_json(
@ -295,7 +293,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Gauge Chart", slice_name="Gauge Chart",
viz_type="gauge_chart", viz_type="gauge_chart",
params=get_slice_json( params=get_slice_json(
@ -306,7 +304,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Heatmap Chart", slice_name="Heatmap Chart",
viz_type="heatmap", viz_type="heatmap",
params=get_slice_json( params=get_slice_json(
@ -318,7 +316,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Line Chart", slice_name="Line Chart",
viz_type="line", viz_type="line",
params=get_slice_json( params=get_slice_json(
@ -329,7 +327,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Mixed Chart", slice_name="Mixed Chart",
viz_type="mixed_timeseries", viz_type="mixed_timeseries",
params=get_slice_json( params=get_slice_json(
@ -342,7 +340,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Partition Chart", slice_name="Partition Chart",
viz_type="partition", viz_type="partition",
params=get_slice_json( params=get_slice_json(
@ -353,7 +351,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Radar Chart", slice_name="Radar Chart",
viz_type="radar", viz_type="radar",
params=get_slice_json( params=get_slice_json(
@ -376,7 +374,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Nightingale Chart", slice_name="Nightingale Chart",
viz_type="rose", viz_type="rose",
params=get_slice_json( params=get_slice_json(
@ -387,7 +385,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Sankey Chart", slice_name="Sankey Chart",
viz_type="sankey", viz_type="sankey",
params=get_slice_json( params=get_slice_json(
@ -398,7 +396,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Sunburst Chart", slice_name="Sunburst Chart",
viz_type="sunburst", viz_type="sunburst",
params=get_slice_json( params=get_slice_json(
@ -409,7 +407,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Treemap Chart", slice_name="Treemap Chart",
viz_type="treemap", viz_type="treemap",
params=get_slice_json( params=get_slice_json(
@ -420,7 +418,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Treemap V2 Chart", slice_name="Treemap V2 Chart",
viz_type="treemap_v2", viz_type="treemap_v2",
params=get_slice_json( params=get_slice_json(
@ -431,7 +429,7 @@ def create_slices(tbl: SqlaTable) -> List[Slice]:
), ),
), ),
Slice( Slice(
**slice_props, **slice_kwargs,
slice_name="Word Cloud Chart", slice_name="Word Cloud Chart",
viz_type="word_cloud", viz_type="word_cloud",
params=get_slice_json( params=get_slice_json(

View File

@ -114,6 +114,7 @@ FRONTEND_CONF_KEYS = (
"DEFAULT_TIME_FILTER", "DEFAULT_TIME_FILTER",
"HTML_SANITIZATION", "HTML_SANITIZATION",
"HTML_SANITIZATION_SCHEMA_EXTENSIONS", "HTML_SANITIZATION_SCHEMA_EXTENSIONS",
"WELCOME_PAGE_LAST_TAB",
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -68,7 +68,7 @@ def _create_dashboards():
from superset.examples.birth_names import create_dashboard, create_slices from superset.examples.birth_names import create_dashboard, create_slices
slices, _ = create_slices(table, admin_owner=False) slices, _ = create_slices(table)
dash = create_dashboard(slices) dash = create_dashboard(slices)
slices_ids_to_delete = [slice.id for slice in slices] slices_ids_to_delete = [slice.id for slice in slices]
dash_id_to_delete = dash.id dash_id_to_delete = dash.id