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
* 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,
};

View File

@ -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

View File

@ -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

View File

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

View File

@ -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') {

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 { 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<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 {

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.
*/
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<string, string[]>;
dashboard__custom_filter_bar_widths: Record<string, number>;

View File

@ -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.

View File

@ -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.

View File

@ -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';

View File

@ -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<Dashboard>;
showThumbnails?: boolean;
featureFlag?: boolean;
examples: Array<Dashboard>;
otherTabData: Array<Dashboard>;
otherTabFilters: Filter[];
otherTabTitle: string;
}
export interface Dashboard {

View File

@ -18,13 +18,17 @@
*/
import rison from 'rison';
import {
isNeedsPassword,
isAlreadyExists,
getPasswordsNeeded,
getAlreadyExists,
hasTerminalValidation,
checkUploadExtensions,
getAlreadyExists,
getFilterValues,
getPasswordsNeeded,
hasTerminalValidation,
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<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 {
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<Filters>) => {
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> = [
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 [];
}

View File

@ -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' },

View File

@ -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,36 +150,38 @@ 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) => {
(activeChild !== TableTab.Edited
? activityData[activeChild]
: editedObjs
).map((entity: ActivityObject) => {
const url = getEntityUrl(entity);
const lastActionOn = getEntityLastActionOn(entity);
return (
@ -202,8 +198,7 @@ export default function ActivityTable({
</Link>
</CardStyles>
);
},
);
});
const doneFetching = loadedCount < 3;
@ -214,7 +209,9 @@ export default function ActivityTable({
<Styles>
<SubMenu activeChild={activeChild} tabs={tabs} />
{activityData[activeChild]?.length > 0 ||
(activeChild === 'Edited' && editedObjs && editedObjs.length > 0) ? (
(activeChild === TableTab.Edited &&
editedObjs &&
editedObjs.length > 0) ? (
<CardContainer className="recentCards">
{renderActivity()}
</CardContainer>

View File

@ -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(
<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 () => {
await act(async () => {
wrapper = mount(
<ChartTable
chartFilter="Mine"
user={{ userId: '2' }}
mine={[]}
otherTabData={[]}
otherTabFilters={[]}
otherTabTitle="Other"
showThumbnails={false}
store={store}
/>,
);

View File

@ -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<any>;
showThumbnails: boolean;
examples?: Array<object>;
otherTabData?: Array<object>;
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<boolean>(false);
const [loaded, setLoaded] = useState<boolean>(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({
)}
<SubMenu
activeChild={chartFilter}
activeChild={activeTab}
tabs={menuTabs}
buttons={[
{
@ -221,7 +201,7 @@ function ChartTable({
buttonStyle: 'link',
onClick: () => {
const target =
chartFilter === 'Favorite'
activeTab === TableTab.Favorite
? `/chart/list/?filters=(favorite:(label:${t(
'Yes',
)},value:!t))`
@ -237,7 +217,7 @@ function ChartTable({
<ChartCard
key={`${e.id}`}
openChartEditModal={openChartEditModal}
chartFilter={chartFilter}
chartFilter={activeTab}
chart={e}
userId={user?.userId}
hasPerm={hasPerm}
@ -253,7 +233,11 @@ function ChartTable({
))}
</CardContainer>
) : (
<EmptyState tableName={WelcomeTable.Charts} tab={chartFilter} />
<EmptyState
tableName={WelcomeTable.Charts}
tab={activeTab}
otherTabTitle={otherTabTitle}
/>
)}
{preparingExport && <Loading />}
</ErrorBoundary>

View File

@ -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<Dashboard>();
const [dashboardFilter, setDashboardFilter] = useState(defaultFilter);
const [activeTab, setActiveTab] = useState(defaultTab);
const [preparingExport, setPreparingExport] = useState<boolean>(false);
const [loaded, setLoaded] = useState<boolean>(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 (
<>
<SubMenu
activeChild={dashboardFilter}
activeChild={activeTab}
tabs={menuTabs}
buttons={[
{
@ -229,7 +199,7 @@ function DashboardTable({
buttonStyle: 'link',
onClick: () => {
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({
</CardContainer>
)}
{dashboards.length === 0 && (
<EmptyState tableName={WelcomeTable.Dashboards} tab={dashboardFilter} />
<EmptyState tableName={WelcomeTable.Dashboards} tab={activeTab} />
)}
{preparingExport && <Loading />}
</>

View File

@ -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(<EmptyState {...recent} />);
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`,
);
});
});

View File

@ -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<WelcomeTable, string> = {
@ -29,9 +30,10 @@ const welcomeTableLabels: Record<WelcomeTable, string> = {
[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 = (
<span className="no-recents">
{(() => {
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) {
})()}
</span>
);
// 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 (
<EmptyContainer>
<Empty
image={`/static/assets/images/${tableIcon[tableName]}`}
description={
tableName === 'RECENTS' || tab === 'Examples' ? recent : mine
tableName === WelcomeTable.Recents || tab === TableTab.Other
? recent
: mine
}
>
{tableName !== 'RECENTS' && (
{tableName !== WelcomeTable.Recents && (
<ButtonContainer>
<Button
buttonStyle="primary"
@ -120,7 +134,7 @@ export default function EmptyState({ tableName, tab }: EmptyStateProps) {
}}
>
<i className="fa fa-plus" />
{tableName === 'SAVED_QUERIES'
{tableName === WelcomeTable.SavedQueries
? t('SQL query')
: tableName
.split('')
@ -152,7 +166,7 @@ export default function EmptyState({ tableName, tab }: EmptyStateProps) {
>
{t('See all %(tableName)s', {
tableName:
tableName === 'SAVED_QUERIES'
tableName === WelcomeTable.SavedQueries
? t('SQL Lab queries')
: 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 github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
import { LoadingCards } from 'src/views/CRUD/welcome/Welcome';
import { TableTab } from 'src/views/CRUD/types';
import withToasts from 'src/components/MessageToasts/withToasts';
import { AntdDropdown } from 'src/components';
import { Menu } from 'src/components/Menu';
@ -30,10 +31,12 @@ import ListViewCard from 'src/components/ListViewCard';
import DeleteModal from 'src/components/DeleteModal';
import Icons from 'src/components/Icons';
import SubMenu from 'src/views/components/SubMenu';
import { User } from 'src/types/bootstrapTypes';
import EmptyState from './EmptyState';
import {
CardContainer,
createErrorHandler,
getFilterValues,
PAGE_SIZE,
shortenSQL,
} from '../utils';
@ -56,9 +59,7 @@ interface Query {
}
interface SavedQueriesProps {
user: {
userId: string | number;
};
user: User;
queryFilter: string;
addDangerToast: (arg0: string) => void;
addSuccessToast: (arg0: string) => void;
@ -134,7 +135,7 @@ const SavedQueries = ({
[],
false,
);
const [queryFilter, setQueryFilter] = useState('Mine');
const [activeTab, setActiveTab] = useState(TableTab.Mine);
const [queryDeleteModal, setQueryDeleteModal] = useState(false);
const [currentlyEdited, setCurrentlyEdited] = useState<Query>({});
const [ifMine, setMine] = useState(true);
@ -149,13 +150,11 @@ const SavedQueries = ({
}).then(
() => {
const queryParams = {
filters: [
{
id: 'created_by',
operator: 'rel_o_m',
value: `${user?.userId}`,
},
],
filters: getFilterValues(
TableTab.Created,
WelcomeTable.SavedQueries,
user,
),
pageSize: PAGE_SIZE,
sortBy: [
{
@ -178,25 +177,7 @@ const SavedQueries = ({
);
};
const getFilters = (filterName: string) => {
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) =>
const getData = (tab: TableTab) =>
fetchData({
pageIndex: 0,
pageSize: PAGE_SIZE,
@ -206,7 +187,7 @@ const SavedQueries = ({
desc: true,
},
],
filters: getFilters(filter),
filters: getFilterValues(tab, WelcomeTable.SavedQueries, user),
});
const renderMenu = (query: Query) => (
@ -263,21 +244,13 @@ const SavedQueries = ({
/>
)}
<SubMenu
activeChild={queryFilter}
activeChild={activeTab}
tabs={[
/* @TODO uncomment when fav functionality is implemented
{
name: 'Favorite',
label: t('Favorite'),
onClick: () => {
getData('Favorite').then(() => setQueryFilter('Favorite'));
},
},
*/
{
name: 'Mine',
name: TableTab.Mine,
label: t('Mine'),
onClick: () => getData('Mine').then(() => setQueryFilter('Mine')),
onClick: () =>
getData(TableTab.Mine).then(() => setActiveTab(TableTab.Mine)),
},
]}
buttons={[
@ -366,7 +339,7 @@ const SavedQueries = ({
))}
</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
* under the License.
*/
import React, { useEffect, useState } from 'react';
import { styled, t, getExtensionsRegistry } from '@superset-ui/core';
import React, { useEffect, useMemo, useState } from 'react';
import {
getExtensionsRegistry,
JsonObject,
styled,
t,
} from '@superset-ui/core';
import Collapse from 'src/components/Collapse';
import { User } from 'src/types/bootstrapTypes';
import { reject } from 'lodash';
@ -40,7 +45,9 @@ import {
} from 'src/views/CRUD/utils';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
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 ChartTable from './ChartTable';
import SavedQueries from './SavedQueries';
@ -54,10 +61,10 @@ interface WelcomeProps {
}
export interface ActivityData {
Created?: Array<object>;
Edited?: Array<object>;
Viewed?: Array<object>;
Examples?: Array<object>;
[TableTab.Created]?: JsonObject[];
[TableTab.Edited]?: JsonObject[];
[TableTab.Viewed]?: JsonObject[];
[TableTab.Other]?: JsonObject[];
}
interface LoadingProps {
@ -138,6 +145,8 @@ const WelcomeNav = styled.div`
`}
`;
const bootstrapData = getBootstrapData();
export const LoadingCards = ({ cover }: LoadingProps) => (
<CardContainer showThumbnails={cover} className="loading-cards">
{[...new Array(loadingCardCount)].map((_, index) => (
@ -185,28 +194,58 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
'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(() => {
if (!otherTabFilters) {
return;
}
const activeTab = getItem(LocalStorageKeys.homepage_activity_filter, null);
setActiveState(collapseState.length > 0 ? collapseState : DEFAULT_TAB_ARR);
getRecentAcitivtyObjs(user.userId!, recent, addDangerToast)
getRecentAcitivtyObjs(user.userId!, recent, addDangerToast, otherTabFilters)
.then(res => {
const data: ActivityData | null = {};
data.Examples = res.examples;
data[TableTab.Other] = res.other;
if (res.viewed) {
const filtered = reject(res.viewed, ['item_url', null]).map(r => r);
data.Viewed = filtered;
if (!activeTab && data.Viewed) {
setActiveChild('Viewed');
} else if (!activeTab && !data.Viewed) {
setActiveChild('Created');
} else setActiveChild(activeTab || 'Created');
} else if (!activeTab) setActiveChild('Created');
data[TableTab.Viewed] = filtered;
if (!activeTab && data[TableTab.Viewed]) {
setActiveChild(TableTab.Viewed);
} else if (!activeTab && !data[TableTab.Viewed]) {
setActiveChild(TableTab.Created);
} else setActiveChild(activeTab || TableTab.Created);
} else if (!activeTab) setActiveChild(TableTab.Created);
else setActiveChild(activeTab);
setActivityData(activityData => ({ ...activityData, ...data }));
})
.catch(
createErrorHandler((errMsg: unknown) => {
setActivityData(activityData => ({ ...activityData, Viewed: [] }));
setActivityData(activityData => ({
...activityData,
[TableTab.Viewed]: [],
}));
addDangerToast(
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),
);
});
}, []);
}, [otherTabFilters]);
const handleToggle = () => {
setChecked(!checked);
@ -277,13 +316,13 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
}, [chartData, queryData, dashboardData]);
useEffect(() => {
if (!collapseState && activityData?.Viewed?.length) {
if (!collapseState && activityData?.[TableTab.Viewed]?.length) {
setActiveState(activeState => ['1', ...activeState]);
}
}, [activityData]);
const isRecentActivityLoading =
!activityData?.Examples && !activityData?.Viewed;
!activityData?.[TableTab.Other] && !activityData?.[TableTab.Viewed];
return (
<WelcomeContainer>
{WelcomeMessageExtension && <WelcomeMessageExtension />}
@ -308,9 +347,9 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
>
<Collapse.Panel header={t('Recents')} key="1">
{activityData &&
(activityData.Viewed ||
activityData.Examples ||
activityData.Created) &&
(activityData[TableTab.Viewed] ||
activityData[TableTab.Other] ||
activityData[TableTab.Created]) &&
activeChild !== 'Loading' ? (
<ActivityTable
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}
mine={dashboardData}
showThumbnails={checked}
examples={activityData?.Examples}
otherTabData={activityData?.[TableTab.Other]}
otherTabFilters={otherTabFilters}
otherTabTitle={otherTabTitle}
/>
)}
</Collapse.Panel>
@ -343,7 +384,9 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
showThumbnails={checked}
user={user}
mine={chartData}
examples={activityData?.Examples}
otherTabData={activityData?.[TableTab.Other]}
otherTabFilters={otherTabFilters}
otherTabTitle={otherTabTitle}
/>
)}
</Collapse.Panel>

View File

@ -17,9 +17,13 @@
* under the License.
*/
import { Filter } from '../types';
export enum WelcomeTable {
Charts = 'CHARTS',
Dashboards = 'DASHBOARDS',
Recents = 'RECENTS',
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 { useUiConfig } from 'src/components/UiConfigContext';
import { URL_PARAMS } from 'src/constants';
import {
MenuObjectChildProps,
MenuObjectProps,
MenuData,
} from 'src/types/bootstrapTypes';
import RightMenu from './RightMenu';
import { Languages } from './LanguagePicker';
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 MenuProps {
data: {
menu: MenuObjectProps[];
brand: BrandProps;
navbar_right: NavBarProps;
settings: MenuObjectProps[];
environment_tag: {
text: string;
color: string;
};
};
interface MenuProps {
data: MenuData;
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`
${({ theme }) => `
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 { findPermission } from 'src/utils/findPermission';
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 LanguagePicker from './LanguagePicker';
import DatabaseModal from '../CRUD/data/database/DatabaseModal';
@ -48,7 +51,6 @@ import {
GlobalMenuDataOptions,
RightMenuProps,
} from './types';
import { MenuObjectProps } from './Menu';
import AddDatasetModal from '../CRUD/data/dataset/AddDatasetModal';
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 Button, { OnClickHandler } from 'src/components/Button';
import Icons from 'src/components/Icons';
import { MenuObjectProps } from './Menu';
import { MenuObjectProps } from 'src/types/bootstrapTypes';
const StyledHeader = styled.div`
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;

View File

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

View File

@ -41,6 +41,7 @@ from typing import (
Literal,
Optional,
Set,
Tuple,
Type,
TYPE_CHECKING,
Union,
@ -1452,6 +1453,17 @@ ADVANCED_DATA_TYPES: Dict[str, AdvancedDataType] = {
"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.
# 'color' can either be a hex color code, or a dot-indexed theme color (e.g. error.base)
ENVIRONMENT_TAG_CONFIG = {

View File

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

View File

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

View File

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

View File

@ -68,7 +68,7 @@ def _create_dashboards():
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)
slices_ids_to_delete = [slice.id for slice in slices]
dash_id_to_delete = dash.id