mirror of
https://github.com/apache/superset.git
synced 2026-04-20 00:24:38 +00:00
feat: home screen mvp (#11206)
* step 1: broken stuff! * first steps * more adding and slicing * step 1: broken stuff! * can now filter dashboards/charts for "Edited" tabs (filter by changed_by o_m) * more updates * update recent card * add icon * Adding Expand Icon to Collapse component * more updates * clean up code * remove lock file * remove consoles * fixing subnav button height shift * lil' ascii arrows * update branch * update test part 1 * remove consoles * fix typescript * add images and update emptystate * add changes * update chart card * fix css issues from rebase * add suggestions * more changes * update tests and clear typescript errors * Update superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx Co-authored-by: Evan Rusackas <evan@preset.io> * update from comments * more updates.. * fix rebase * fix pesky type errors * test fixes * lint fix * Update superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx Co-authored-by: Evan Rusackas <evan@preset.io> * Update superset-frontend/src/views/CRUD/welcome/EmptyState.tsx Co-authored-by: Evan Rusackas <evan@preset.io> * Update superset-frontend/src/components/Menu/SubMenu.tsx Co-authored-by: Evan Rusackas <evan@preset.io> * Update superset-frontend/src/components/ListViewCard/index.tsx Co-authored-by: ʈᵃᵢ <tdupreetan@gmail.com> * Update superset-frontend/src/components/ListViewCard/index.tsx Co-authored-by: ʈᵃᵢ <tdupreetan@gmail.com> * add suggestions * fix lint * remove unused code * toast getrecentActivityobjs * add some suggestions * remove types for now * cypress fix * remove unused type Co-authored-by: Evan Rusackas <evan@preset.io> Co-authored-by: ʈᵃᵢ <tdupreetan@gmail.com>
This commit is contained in:
committed by
GitHub
parent
a8eb3fe8e7
commit
f7051eaade
BIN
superset-frontend/images/empty-charts.png
Normal file
BIN
superset-frontend/images/empty-charts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
superset-frontend/images/empty-dashboard.png
Normal file
BIN
superset-frontend/images/empty-dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
superset-frontend/images/empty-queries.png
Normal file
BIN
superset-frontend/images/empty-queries.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
superset-frontend/images/star-circle.png
Normal file
BIN
superset-frontend/images/star-circle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
BIN
superset-frontend/images/union.png
Normal file
BIN
superset-frontend/images/union.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -24,7 +24,7 @@ import SubMenu from 'src/components/Menu/SubMenu';
|
|||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
name: 'Title',
|
name: 'Title',
|
||||||
children: [
|
tabs: [
|
||||||
{
|
{
|
||||||
name: 'Page1',
|
name: 'Page1',
|
||||||
label: 'Page1',
|
label: 'Page1',
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const mockCharts = [...new Array(3)].map((_, i) => ({
|
|||||||
fetchMock.get(chartsInfoEndpoint, {
|
fetchMock.get(chartsInfoEndpoint, {
|
||||||
permissions: ['can_list', 'can_edit', 'can_delete'],
|
permissions: ['can_list', 'can_edit', 'can_delete'],
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchMock.get(chartssOwnersEndpoint, {
|
fetchMock.get(chartssOwnersEndpoint, {
|
||||||
result: [],
|
result: [],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { styledMount as mount } from 'spec/helpers/theming';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
|
|
||||||
|
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||||
|
import configureStore from 'redux-mock-store';
|
||||||
|
import ActivityTable from 'src/views/CRUD/welcome/ActivityTable';
|
||||||
|
|
||||||
|
const mockStore = configureStore([thunk]);
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
|
const chartsEndpoint = 'glob:*/api/v1/chart/?*';
|
||||||
|
const dashboardEndpoint = 'glob:*/api/v1/dashboard/?*';
|
||||||
|
const savedQueryEndpoint = 'glob:*/api/v1/saved_query/?*';
|
||||||
|
|
||||||
|
fetchMock.get(chartsEndpoint, {
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
slice_name: 'ChartyChart',
|
||||||
|
changed_on_utc: '24 Feb 2014 10:13:14',
|
||||||
|
url: '/fakeUrl/explore',
|
||||||
|
id: '4',
|
||||||
|
table: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchMock.get(dashboardEndpoint, {
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
dashboard_title: 'Dashboard_Test',
|
||||||
|
changed_on_utc: '24 Feb 2014 10:13:14',
|
||||||
|
url: '/fakeUrl/dashboard',
|
||||||
|
id: '3',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchMock.get(savedQueryEndpoint, {
|
||||||
|
result: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ActivityTable', () => {
|
||||||
|
const activityProps = {
|
||||||
|
user: {
|
||||||
|
userId: '1',
|
||||||
|
},
|
||||||
|
activityFilter: 'Edited',
|
||||||
|
};
|
||||||
|
const wrapper = mount(<ActivityTable {...activityProps} />, {
|
||||||
|
context: { store },
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the component renders ', () => {
|
||||||
|
expect(wrapper.find(ActivityTable)).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls batch method and renders ListViewCArd', async () => {
|
||||||
|
const chartCall = fetchMock.calls(/chart\/\?q/);
|
||||||
|
const dashboardCall = fetchMock.calls(/dashboard\/\?q/);
|
||||||
|
expect(chartCall).toHaveLength(2);
|
||||||
|
expect(dashboardCall).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { styledMount as mount } from 'spec/helpers/theming';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
|
import configureStore from 'redux-mock-store';
|
||||||
|
|
||||||
|
import ChartTable from 'src/views/CRUD/welcome/ChartTable';
|
||||||
|
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||||
|
|
||||||
|
const mockStore = configureStore([thunk]);
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
|
const chartsEndpoint = 'glob:*/api/v1/chart/?*';
|
||||||
|
const chartsInfoEndpoint = 'glob:*/api/v1/chart/_info*';
|
||||||
|
|
||||||
|
const mockCharts = [...new Array(3)].map((_, i) => ({
|
||||||
|
changed_on_utc: new Date().toISOString(),
|
||||||
|
created_by: 'super user',
|
||||||
|
id: i,
|
||||||
|
slice_name: `cool chart ${i}`,
|
||||||
|
url: 'url',
|
||||||
|
viz_type: 'bar',
|
||||||
|
datasource_title: `ds${i}`,
|
||||||
|
thumbnail_url: '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
fetchMock.get(chartsEndpoint, {
|
||||||
|
result: mockCharts,
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchMock.get(chartsInfoEndpoint, {
|
||||||
|
permissions: ['can_add', 'can_edit', 'can_delete'],
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ChartTable', () => {
|
||||||
|
const mockedProps = {
|
||||||
|
user: {
|
||||||
|
userId: '2',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wrapper = mount(<ChartTable {...mockedProps} />, {
|
||||||
|
context: { store },
|
||||||
|
});
|
||||||
|
it('it renders', () => {
|
||||||
|
expect(wrapper.find(ChartTable)).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches chart favorites and renders chart cards ', async () => {
|
||||||
|
expect(fetchMock.calls(chartsEndpoint)).toHaveLength(1);
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(wrapper.find('ChartCard')).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('display EmptyState if there is no data', () => {
|
||||||
|
fetchMock.resetHistory();
|
||||||
|
const wrapper = mount(<ChartTable {...mockedProps} />, {
|
||||||
|
context: { store },
|
||||||
|
});
|
||||||
|
expect(wrapper.find('EmptyState')).toExist();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,48 +17,78 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { styledMount as mount } from 'spec/helpers/theming';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
import ListView from 'src/components/ListView';
|
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||||
|
import SubMenu from 'src/components/Menu/SubMenu';
|
||||||
import DashboardTable from 'src/views/CRUD/welcome/DashboardTable';
|
import DashboardTable from 'src/views/CRUD/welcome/DashboardTable';
|
||||||
|
import DashboardCard from 'src/views/CRUD/dashboard/DashboardCard';
|
||||||
|
|
||||||
// store needed for withToasts(DashboardTable)
|
// store needed for withToasts(DashboardTable)
|
||||||
const mockStore = configureStore([thunk]);
|
const mockStore = configureStore([thunk]);
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
|
|
||||||
const dashboardsEndpoint = 'glob:*/api/v1/dashboard/*';
|
const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*';
|
||||||
const mockDashboards = [{ id: 1, url: 'url', dashboard_title: 'title' }];
|
const dashboardInfoEndpoint = 'glob:*/api/v1/dashboard/_info*';
|
||||||
|
const mockDashboards = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
url: 'url',
|
||||||
|
dashboard_title: 'title',
|
||||||
|
changed_on_utc: '24 Feb 2014 10:13:14',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
fetchMock.get(dashboardsEndpoint, { result: mockDashboards });
|
fetchMock.get(dashboardsEndpoint, { result: mockDashboards });
|
||||||
|
fetchMock.get(dashboardInfoEndpoint, {
|
||||||
function setup() {
|
permissions: ['can_list', 'can_edit', 'can_delete'],
|
||||||
// use mount because data fetching is triggered on mount
|
});
|
||||||
return mount(<DashboardTable />, {
|
|
||||||
context: { store },
|
|
||||||
wrappingComponent: ThemeProvider,
|
|
||||||
wrappingComponentProps: { theme: supersetTheme },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('DashboardTable', () => {
|
describe('DashboardTable', () => {
|
||||||
beforeEach(fetchMock.resetHistory);
|
const dashboardProps = {
|
||||||
|
dashboardFilter: 'Favorite',
|
||||||
|
user: {
|
||||||
|
userId: '2',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wrapper = mount(<DashboardTable {...dashboardProps} />, {
|
||||||
|
context: { store },
|
||||||
|
});
|
||||||
|
|
||||||
it('fetches dashboards and renders a ListView', () => {
|
beforeAll(async () => {
|
||||||
return new Promise(done => {
|
await waitForComponentToPaint(wrapper);
|
||||||
const wrapper = setup();
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
it('renders', () => {
|
||||||
expect(fetchMock.calls(dashboardsEndpoint)).toHaveLength(1);
|
expect(wrapper.find(DashboardTable)).toExist();
|
||||||
// there's a delay between response and updating state, so manually set it
|
});
|
||||||
// rather than adding a timeout which could introduce flakiness
|
|
||||||
wrapper.setState({ dashboards: mockDashboards });
|
it('render a submenu with clickable tabs and buttons', async () => {
|
||||||
expect(wrapper.find(ListView)).toExist();
|
expect(wrapper.find(SubMenu)).toExist();
|
||||||
done();
|
expect(wrapper.find('MenuItem')).toHaveLength(2);
|
||||||
});
|
expect(wrapper.find('Button')).toHaveLength(4);
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('MenuItem').at(1).simulate('click');
|
||||||
});
|
});
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(fetchMock.calls(/dashboard\/\?q/)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches dashboards and renders a card', () => {
|
||||||
|
expect(fetchMock.calls(/dashboard\/\?q/)).toHaveLength(1);
|
||||||
|
wrapper.setState({ dashboards: mockDashboards });
|
||||||
|
expect(wrapper.find(DashboardCard)).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('display EmptyState if there is no data', () => {
|
||||||
|
fetchMock.resetHistory();
|
||||||
|
const wrapper = mount(<DashboardTable {...dashboardProps} />, {
|
||||||
|
context: { store },
|
||||||
|
});
|
||||||
|
expect(wrapper.find('EmptyState')).toExist();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { styledMount as mount } from 'spec/helpers/theming';
|
||||||
|
import EmptyState from 'src/views/CRUD/welcome/EmptyState';
|
||||||
|
|
||||||
|
describe('EmptyState', () => {
|
||||||
|
const variants = [
|
||||||
|
{
|
||||||
|
tab: 'Favorite',
|
||||||
|
tableName: 'DASHBOARDS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: 'Mine',
|
||||||
|
tableName: 'DASHBOARDS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: 'Favorite',
|
||||||
|
tableName: 'CHARTS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: 'Mine',
|
||||||
|
tableName: 'CHARTS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: 'Favorite',
|
||||||
|
tableName: 'SAVED_QUERIES',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: 'Mine',
|
||||||
|
tableName: 'SAVED_QUEREIS',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const recents = [
|
||||||
|
{
|
||||||
|
tab: 'Viewed',
|
||||||
|
tableName: 'RECENTS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: 'Edited',
|
||||||
|
tableName: 'RECENTS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: 'Created',
|
||||||
|
tableName: 'RECENTS',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
variants.forEach(variant => {
|
||||||
|
it(`it renders an ${variant.tab} ${variant.tableName} empty state`, () => {
|
||||||
|
const wrapper = mount(<EmptyState {...variant} />);
|
||||||
|
expect(wrapper).toExist();
|
||||||
|
const textContainer = wrapper.find('.ant-empty-description');
|
||||||
|
expect(textContainer.text()).toEqual(
|
||||||
|
variant.tab === 'Favorite'
|
||||||
|
? "You don't have any favorites yet!"
|
||||||
|
: `No ${
|
||||||
|
variant.tableName === 'SAVED_QUERIES'
|
||||||
|
? 'saved queries'
|
||||||
|
: variant.tableName.toLowerCase()
|
||||||
|
} yet`,
|
||||||
|
);
|
||||||
|
expect(wrapper.find('button')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
recents.forEach(recent => {
|
||||||
|
it(`it renders an ${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`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import { styledMount as mount } from 'spec/helpers/theming';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
|
import configureStore from 'redux-mock-store';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
|
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||||
|
import SubMenu from 'src/components/Menu/SubMenu';
|
||||||
|
import SavedQueries from 'src/views/CRUD/welcome/SavedQueries';
|
||||||
|
|
||||||
|
// store needed for withToasts(DashboardTable)
|
||||||
|
const mockStore = configureStore([thunk]);
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
|
const queriesEndpoint = 'glob:*/api/v1/saved_query/?*';
|
||||||
|
const savedQueriesInfo = 'glob:*/api/v1/saved_query/_info';
|
||||||
|
|
||||||
|
const mockqueries = [...new Array(3)].map((_, i) => ({
|
||||||
|
created_by: {
|
||||||
|
id: i,
|
||||||
|
first_name: `user`,
|
||||||
|
last_name: `${i}`,
|
||||||
|
},
|
||||||
|
created_on: `${i}-2020`,
|
||||||
|
database: {
|
||||||
|
database_name: `db ${i}`,
|
||||||
|
id: i,
|
||||||
|
},
|
||||||
|
changed_on_delta_humanized: '1 day ago',
|
||||||
|
db_id: i,
|
||||||
|
description: `SQL for ${i}`,
|
||||||
|
id: i,
|
||||||
|
label: `query ${i}`,
|
||||||
|
schema: 'public',
|
||||||
|
sql: `SELECT ${i} FROM table`,
|
||||||
|
sql_tables: [
|
||||||
|
{
|
||||||
|
catalog: null,
|
||||||
|
schema: null,
|
||||||
|
table: `${i}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
fetchMock.get(queriesEndpoint, {
|
||||||
|
result: mockqueries,
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchMock.get(savedQueriesInfo, {
|
||||||
|
permissions: ['can_list', 'can_edit', 'can_delete'],
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SavedQueries', () => {
|
||||||
|
const savedQueryProps = {
|
||||||
|
user: {
|
||||||
|
userId: '1',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mount(<SavedQueries {...savedQueryProps} />, {
|
||||||
|
context: { store },
|
||||||
|
});
|
||||||
|
beforeAll(async () => {
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is valid', () => {
|
||||||
|
expect(wrapper.find(SavedQueries)).toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it renders a submenu with clickable tables and buttons', async () => {
|
||||||
|
expect(wrapper.find(SubMenu)).toExist();
|
||||||
|
expect(wrapper.find('MenuItem')).toHaveLength(2);
|
||||||
|
expect(wrapper.find('button')).toHaveLength(2);
|
||||||
|
act(() => {
|
||||||
|
wrapper.find('MenuItem').at(1).simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForComponentToPaint(wrapper);
|
||||||
|
expect(fetchMock.calls(/saved_query\/\?q/)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches queries favorites and renders listviewcard cards', () => {
|
||||||
|
expect(fetchMock.calls(/saved_query\/\?q/)).toHaveLength(1);
|
||||||
|
expect(wrapper.find('ListViewCard')).toExist();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,11 +17,14 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Panel, Row, Tab } from 'react-bootstrap';
|
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import configureStore from 'redux-mock-store';
|
||||||
import Welcome from 'src/views/CRUD/welcome/Welcome';
|
import Welcome from 'src/views/CRUD/welcome/Welcome';
|
||||||
|
|
||||||
|
const mockStore = configureStore([thunk]);
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
describe('Welcome', () => {
|
describe('Welcome', () => {
|
||||||
const mockedProps = {
|
const mockedProps = {
|
||||||
user: {
|
user: {
|
||||||
@@ -34,13 +37,15 @@ describe('Welcome', () => {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
it('is valid', () => {
|
const wrapper = shallow(<Welcome {...mockedProps} />, {
|
||||||
expect(React.isValidElement(<Welcome {...mockedProps} />)).toBe(true);
|
context: { store },
|
||||||
});
|
});
|
||||||
it('renders 3 Tab, Panel, and Row components', () => {
|
|
||||||
const wrapper = shallow(<Welcome {...mockedProps} />);
|
it('renders', () => {
|
||||||
expect(wrapper.find(Tab)).toHaveLength(3);
|
expect(wrapper).toExist();
|
||||||
expect(wrapper.find(Panel)).toHaveLength(3);
|
});
|
||||||
expect(wrapper.find(Row)).toHaveLength(3);
|
|
||||||
|
it('renders all panels on the page on page load', () => {
|
||||||
|
expect(wrapper.find('CollapsePanel')).toHaveLength(4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -143,15 +143,20 @@ const paragraphConfig = { rows: 1, width: 150 };
|
|||||||
interface CardProps {
|
interface CardProps {
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
url?: string;
|
url?: string;
|
||||||
imgURL: string;
|
imgURL?: string;
|
||||||
imgFallbackURL: string;
|
imgFallbackURL?: string;
|
||||||
imgPosition?: BackgroundPosition;
|
imgPosition?: BackgroundPosition;
|
||||||
description: string;
|
description: string;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
titleRight?: React.ReactNode;
|
titleRight?: React.ReactNode;
|
||||||
coverLeft?: React.ReactNode;
|
coverLeft?: React.ReactNode;
|
||||||
coverRight?: React.ReactNode;
|
coverRight?: React.ReactNode;
|
||||||
actions: React.ReactNode;
|
actions: React.ReactNode | null;
|
||||||
|
showImg?: boolean;
|
||||||
|
rows?: number | string;
|
||||||
|
avatar?: string;
|
||||||
|
isRecent?: boolean;
|
||||||
|
renderCover?: React.ReactNode | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListViewCard({
|
function ListViewCard({
|
||||||
@@ -162,35 +167,42 @@ function ListViewCard({
|
|||||||
imgFallbackURL,
|
imgFallbackURL,
|
||||||
description,
|
description,
|
||||||
coverLeft,
|
coverLeft,
|
||||||
|
isRecent,
|
||||||
coverRight,
|
coverRight,
|
||||||
actions,
|
actions,
|
||||||
|
avatar,
|
||||||
loading,
|
loading,
|
||||||
imgPosition = 'top',
|
imgPosition = 'top',
|
||||||
|
renderCover,
|
||||||
}: CardProps) {
|
}: CardProps) {
|
||||||
return (
|
return (
|
||||||
<StyledCard
|
<StyledCard
|
||||||
data-test="styled-card"
|
data-test="styled-card"
|
||||||
cover={
|
cover={
|
||||||
<Cover>
|
!isRecent
|
||||||
<a href={url}>
|
? renderCover || (
|
||||||
<div className="gradient-container">
|
<Cover>
|
||||||
<ImageLoader
|
<a href={url}>
|
||||||
src={imgURL}
|
<div className="gradient-container">
|
||||||
fallback={imgFallbackURL}
|
<ImageLoader
|
||||||
isLoading={loading}
|
src={imgURL || ''}
|
||||||
position={imgPosition}
|
fallback={imgFallbackURL || ''}
|
||||||
/>
|
isLoading={loading}
|
||||||
</div>
|
position={imgPosition}
|
||||||
</a>
|
/>
|
||||||
<CoverFooter className="cover-footer">
|
</div>
|
||||||
{!loading && coverLeft && (
|
</a>
|
||||||
<CoverFooterLeft>{coverLeft}</CoverFooterLeft>
|
<CoverFooter className="cover-footer">
|
||||||
)}
|
{!loading && coverLeft && (
|
||||||
{!loading && coverRight && (
|
<CoverFooterLeft>{coverLeft}</CoverFooterLeft>
|
||||||
<CoverFooterRight>{coverRight}</CoverFooterRight>
|
)}
|
||||||
)}
|
{!loading && coverRight && (
|
||||||
</CoverFooter>
|
<CoverFooterRight>{coverRight}</CoverFooterRight>
|
||||||
</Cover>
|
)}
|
||||||
|
</CoverFooter>
|
||||||
|
</Cover>
|
||||||
|
)
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{loading && (
|
{loading && (
|
||||||
@@ -230,6 +242,8 @@ function ListViewCard({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
description={description}
|
description={description}
|
||||||
|
// @ts-ignore
|
||||||
|
avatar={avatar ? <Icon name={avatar} /> : null}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledCard>
|
</StyledCard>
|
||||||
|
|||||||
@@ -53,10 +53,23 @@ const StyledHeader = styled.header`
|
|||||||
li.active > a,
|
li.active > a,
|
||||||
li.active > div,
|
li.active > div,
|
||||||
li > a:hover,
|
li > a:hover,
|
||||||
|
li > a:focus,
|
||||||
li > div:hover {
|
li > div:hover {
|
||||||
background-color: ${({ theme }) => theme.colors.secondary.light4};
|
background: ${({ theme }) => theme.colors.secondary.light4};
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-radius: 4px;
|
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||||
|
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navbar-inverse {
|
||||||
|
.navbar-nav {
|
||||||
|
& > .active > a {
|
||||||
|
background: ${({ theme }) => theme.colors.secondary.light4};
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background: ${({ theme }) => theme.colors.secondary.light4};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -64,8 +77,9 @@ const StyledHeader = styled.header`
|
|||||||
type MenuChild = {
|
type MenuChild = {
|
||||||
label: string;
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url?: string;
|
||||||
usesRouter?: boolean;
|
usesRouter?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ButtonProps {
|
export interface ButtonProps {
|
||||||
@@ -83,8 +97,8 @@ export interface ButtonProps {
|
|||||||
|
|
||||||
export interface SubMenuProps {
|
export interface SubMenuProps {
|
||||||
buttons?: Array<ButtonProps>;
|
buttons?: Array<ButtonProps>;
|
||||||
name: string;
|
name?: string;
|
||||||
children?: MenuChild[];
|
tabs?: MenuChild[];
|
||||||
activeChild?: MenuChild['name'];
|
activeChild?: MenuChild['name'];
|
||||||
/* If usesRouter is true, a react-router <Link> component will be used instead of href.
|
/* If usesRouter is true, a react-router <Link> component will be used instead of href.
|
||||||
* ONLY set usesRouter to true if SubMenu is wrapped in a react-router <Router>;
|
* ONLY set usesRouter to true if SubMenu is wrapped in a react-router <Router>;
|
||||||
@@ -108,16 +122,16 @@ const SubMenu: React.FunctionComponent<SubMenuProps> = props => {
|
|||||||
<Navbar.Brand>{props.name}</Navbar.Brand>
|
<Navbar.Brand>{props.name}</Navbar.Brand>
|
||||||
</Navbar.Header>
|
</Navbar.Header>
|
||||||
<Nav>
|
<Nav>
|
||||||
{props.children &&
|
{props.tabs &&
|
||||||
props.children.map(child => {
|
props.tabs.map(tab => {
|
||||||
if ((props.usesRouter || hasHistory) && !!child.usesRouter) {
|
if ((props.usesRouter || hasHistory) && !!tab.usesRouter) {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={child.name === props.activeChild ? 'active' : ''}
|
className={tab.name === props.activeChild ? 'active' : ''}
|
||||||
key={`${child.label}`}
|
key={`${tab.label}`}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Link to={child.url}>{child.label}</Link>
|
<Link to={tab.url || ''}>{tab.label}</Link>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -126,11 +140,12 @@ const SubMenu: React.FunctionComponent<SubMenuProps> = props => {
|
|||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className="no-router"
|
className="no-router"
|
||||||
active={child.name === props.activeChild}
|
active={tab.name === props.activeChild}
|
||||||
key={`${child.label}`}
|
key={`${tab.label}`}
|
||||||
href={child.url}
|
href={tab.url}
|
||||||
|
onClick={tab.onClick}
|
||||||
>
|
>
|
||||||
{child.label}
|
{tab.label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
138
superset-frontend/src/views/CRUD/chart/ChartCard.tsx
Normal file
138
superset-frontend/src/views/CRUD/chart/ChartCard.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { useFavoriteStatus } from 'src/views/CRUD/hooks';
|
||||||
|
import { t } from '@superset-ui/core';
|
||||||
|
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||||
|
import Icon from 'src/components/Icon';
|
||||||
|
import Chart from 'src/types/Chart';
|
||||||
|
|
||||||
|
import ListViewCard from 'src/components/ListViewCard';
|
||||||
|
import Label from 'src/components/Label';
|
||||||
|
import { Dropdown, Menu } from 'src/common/components';
|
||||||
|
import FaveStar from 'src/components/FaveStar';
|
||||||
|
import FacePile from 'src/components/FacePile';
|
||||||
|
import { handleChartDelete } from '../utils';
|
||||||
|
|
||||||
|
const FAVESTAR_BASE_URL = '/superset/favstar/slice';
|
||||||
|
|
||||||
|
interface ChartCardProps {
|
||||||
|
chart: Chart;
|
||||||
|
hasPerm: (perm: string) => boolean;
|
||||||
|
openChartEditModal: (chart: Chart) => void;
|
||||||
|
bulkSelectEnabled: boolean;
|
||||||
|
addDangerToast: (msg: string) => void;
|
||||||
|
addSuccessToast: (msg: string) => void;
|
||||||
|
refreshData: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChartCard({
|
||||||
|
chart,
|
||||||
|
hasPerm,
|
||||||
|
openChartEditModal,
|
||||||
|
bulkSelectEnabled,
|
||||||
|
addDangerToast,
|
||||||
|
addSuccessToast,
|
||||||
|
refreshData,
|
||||||
|
loading,
|
||||||
|
}: ChartCardProps) {
|
||||||
|
const canEdit = hasPerm('can_edit');
|
||||||
|
const canDelete = hasPerm('can_delete');
|
||||||
|
const [, fetchFaveStar, saveFaveStar, favoriteStatus] = useFavoriteStatus(
|
||||||
|
{},
|
||||||
|
FAVESTAR_BASE_URL,
|
||||||
|
addDangerToast,
|
||||||
|
);
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
{canDelete && (
|
||||||
|
<Menu.Item>
|
||||||
|
<ConfirmStatusChange
|
||||||
|
title={t('Please Confirm')}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
{t('Are you sure you want to delete')} <b>{chart.slice_name}</b>
|
||||||
|
?
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onConfirm={() =>
|
||||||
|
handleChartDelete(
|
||||||
|
chart,
|
||||||
|
addSuccessToast,
|
||||||
|
addDangerToast,
|
||||||
|
refreshData,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{confirmDelete => (
|
||||||
|
<div
|
||||||
|
data-test="chart-list-delete-option"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className="action-button"
|
||||||
|
onClick={confirmDelete}
|
||||||
|
>
|
||||||
|
<ListViewCard.MenuIcon name="trash" /> {t('Delete')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ConfirmStatusChange>
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
{canEdit && (
|
||||||
|
<Menu.Item
|
||||||
|
data-test="chart-list-edit-option"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => openChartEditModal(chart)}
|
||||||
|
>
|
||||||
|
<ListViewCard.MenuIcon name="edit-alt" /> {t('Edit')}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<ListViewCard
|
||||||
|
loading={loading}
|
||||||
|
title={chart.slice_name}
|
||||||
|
url={bulkSelectEnabled ? undefined : chart.url}
|
||||||
|
imgURL={chart.thumbnail_url || ''}
|
||||||
|
imgFallbackURL="/static/assets/images/chart-card-fallback.png"
|
||||||
|
description={t('Last modified %s', chart.changed_on_delta_humanized)}
|
||||||
|
coverLeft={<FacePile users={chart.owners || []} />}
|
||||||
|
coverRight={
|
||||||
|
<Label bsStyle="secondary">{chart.datasource_name_text}</Label>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<ListViewCard.Actions>
|
||||||
|
<FaveStar
|
||||||
|
itemId={chart.id}
|
||||||
|
fetchFaveStar={fetchFaveStar}
|
||||||
|
saveFaveStar={saveFaveStar}
|
||||||
|
isStarred={!!favoriteStatus[chart.id]}
|
||||||
|
/>
|
||||||
|
<Dropdown overlay={menu}>
|
||||||
|
<Icon name="more-horiz" />
|
||||||
|
</Dropdown>
|
||||||
|
</ListViewCard.Actions>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,15 +17,22 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { SupersetClient, getChartMetadataRegistry, t } from '@superset-ui/core';
|
import { SupersetClient, getChartMetadataRegistry, t } from '@superset-ui/core';
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { uniqBy } from 'lodash';
|
import { uniqBy } from 'lodash';
|
||||||
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||||
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
|
import {
|
||||||
import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
|
createFetchRelated,
|
||||||
|
createErrorHandler,
|
||||||
|
handleChartDelete,
|
||||||
|
} from 'src/views/CRUD/utils';
|
||||||
|
import {
|
||||||
|
useListViewResource,
|
||||||
|
useFavoriteStatus,
|
||||||
|
useChartEditModal,
|
||||||
|
} from 'src/views/CRUD/hooks';
|
||||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||||
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
||||||
import FacePile from 'src/components/FacePile';
|
|
||||||
import Icon from 'src/components/Icon';
|
import Icon from 'src/components/Icon';
|
||||||
import FaveStar from 'src/components/FaveStar';
|
import FaveStar from 'src/components/FaveStar';
|
||||||
import ListView, {
|
import ListView, {
|
||||||
@@ -35,11 +42,9 @@ import ListView, {
|
|||||||
} from 'src/components/ListView';
|
} from 'src/components/ListView';
|
||||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||||
import Chart, { Slice } from 'src/types/Chart';
|
import Chart from 'src/types/Chart';
|
||||||
import ListViewCard from 'src/components/ListViewCard';
|
|
||||||
import Label from 'src/components/Label';
|
|
||||||
import { Dropdown, Menu } from 'src/common/components';
|
|
||||||
import TooltipWrapper from 'src/components/TooltipWrapper';
|
import TooltipWrapper from 'src/components/TooltipWrapper';
|
||||||
|
import ChartCard from './ChartCard';
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
const FAVESTAR_BASE_URL = '/superset/favstar/slice';
|
const FAVESTAR_BASE_URL = '/superset/favstar/slice';
|
||||||
@@ -105,51 +110,18 @@ function ChartList(props: ChartListProps) {
|
|||||||
FAVESTAR_BASE_URL,
|
FAVESTAR_BASE_URL,
|
||||||
props.addDangerToast,
|
props.addDangerToast,
|
||||||
);
|
);
|
||||||
const [
|
const {
|
||||||
sliceCurrentlyEditing,
|
sliceCurrentlyEditing,
|
||||||
setSliceCurrentlyEditing,
|
handleChartUpdated,
|
||||||
] = useState<Slice | null>(null);
|
openChartEditModal,
|
||||||
|
closeChartEditModal,
|
||||||
|
} = useChartEditModal(setCharts, charts);
|
||||||
|
|
||||||
const canCreate = hasPerm('can_add');
|
const canCreate = hasPerm('can_add');
|
||||||
const canEdit = hasPerm('can_edit');
|
const canEdit = hasPerm('can_edit');
|
||||||
const canDelete = hasPerm('can_delete');
|
const canDelete = hasPerm('can_delete');
|
||||||
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
|
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
|
||||||
|
|
||||||
function openChartEditModal(chart: Chart) {
|
|
||||||
setSliceCurrentlyEditing({
|
|
||||||
slice_id: chart.id,
|
|
||||||
slice_name: chart.slice_name,
|
|
||||||
description: chart.description,
|
|
||||||
cache_timeout: chart.cache_timeout,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeChartEditModal() {
|
|
||||||
setSliceCurrentlyEditing(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChartUpdated(edits: Chart) {
|
|
||||||
// update the chart in our state with the edited info
|
|
||||||
const newCharts = charts.map(chart =>
|
|
||||||
chart.id === edits.id ? { ...chart, ...edits } : chart,
|
|
||||||
);
|
|
||||||
setCharts(newCharts);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChartDelete({ id, slice_name: sliceName }: Chart) {
|
|
||||||
SupersetClient.delete({
|
|
||||||
endpoint: `/api/v1/chart/${id}`,
|
|
||||||
}).then(
|
|
||||||
() => {
|
|
||||||
refreshData();
|
|
||||||
props.addSuccessToast(t('Deleted: %s', sliceName));
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
props.addDangerToast(t('There was an issue deleting: %s', sliceName));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleBulkChartDelete(chartsToDelete: Chart[]) {
|
function handleBulkChartDelete(chartsToDelete: Chart[]) {
|
||||||
SupersetClient.delete({
|
SupersetClient.delete({
|
||||||
endpoint: `/api/v1/chart/?q=${rison.encode(
|
endpoint: `/api/v1/chart/?q=${rison.encode(
|
||||||
@@ -266,7 +238,13 @@ function ChartList(props: ChartListProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Cell: ({ row: { original } }: any) => {
|
Cell: ({ row: { original } }: any) => {
|
||||||
const handleDelete = () => handleChartDelete(original);
|
const handleDelete = () =>
|
||||||
|
handleChartDelete(
|
||||||
|
original,
|
||||||
|
props.addSuccessToast,
|
||||||
|
props.addDangerToast,
|
||||||
|
refreshData,
|
||||||
|
);
|
||||||
const openEditModal = () => openChartEditModal(original);
|
const openEditModal = () => openChartEditModal(original);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -426,69 +404,17 @@ function ChartList(props: ChartListProps) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function renderCard(chart: Chart & { loading: boolean }) {
|
function renderCard(chart: Chart) {
|
||||||
const menu = (
|
|
||||||
<Menu>
|
|
||||||
{canDelete && (
|
|
||||||
<Menu.Item>
|
|
||||||
<ConfirmStatusChange
|
|
||||||
title={t('Please Confirm')}
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
{t('Are you sure you want to delete')}{' '}
|
|
||||||
<b>{chart.slice_name}</b>?
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
onConfirm={() => handleChartDelete(chart)}
|
|
||||||
>
|
|
||||||
{confirmDelete => (
|
|
||||||
<div
|
|
||||||
data-test="chart-list-delete-option"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
className="action-button"
|
|
||||||
onClick={confirmDelete}
|
|
||||||
>
|
|
||||||
<ListViewCard.MenuIcon name="trash" /> Delete
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ConfirmStatusChange>
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
{canEdit && (
|
|
||||||
<Menu.Item
|
|
||||||
data-test="chart-list-edit-option"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => openChartEditModal(chart)}
|
|
||||||
>
|
|
||||||
<ListViewCard.MenuIcon name="edit-alt" /> Edit
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListViewCard
|
<ChartCard
|
||||||
loading={chart.loading}
|
chart={chart}
|
||||||
title={chart.slice_name}
|
hasPerm={hasPerm}
|
||||||
url={bulkSelectEnabled ? undefined : chart.url}
|
openChartEditModal={openChartEditModal}
|
||||||
imgURL={chart.thumbnail_url ?? ''}
|
bulkSelectEnabled={bulkSelectEnabled}
|
||||||
imgFallbackURL="/static/assets/images/chart-card-fallback.png"
|
addDangerToast={props.addDangerToast}
|
||||||
imgPosition="bottom"
|
addSuccessToast={props.addSuccessToast}
|
||||||
description={t('Last modified %s', chart.changed_on_delta_humanized)}
|
refreshData={refreshData}
|
||||||
coverLeft={<FacePile users={chart.owners || []} />}
|
loading={loading}
|
||||||
coverRight={
|
|
||||||
<Label bsStyle="secondary">{chart.datasource_name_text}</Label>
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<ListViewCard.Actions>
|
|
||||||
{renderFaveStar(chart.id)}
|
|
||||||
<Dropdown data-test="dropdown-options" overlay={menu}>
|
|
||||||
<Icon name="more-horiz" />
|
|
||||||
</Dropdown>
|
|
||||||
</ListViewCard.Actions>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
140
superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx
Normal file
140
superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { t } from '@superset-ui/core';
|
||||||
|
import {
|
||||||
|
handleDashboardDelete,
|
||||||
|
handleBulkDashboardExport,
|
||||||
|
} from 'src/views/CRUD/utils';
|
||||||
|
import { Dropdown, Menu } from 'src/common/components';
|
||||||
|
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||||
|
import ListViewCard from 'src/components/ListViewCard';
|
||||||
|
import Icon from 'src/components/Icon';
|
||||||
|
import Label from 'src/components/Label';
|
||||||
|
import FacePile from 'src/components/FacePile';
|
||||||
|
import FaveStar from 'src/components/FaveStar';
|
||||||
|
import { DashboardCardProps } from 'src/views/CRUD/types';
|
||||||
|
|
||||||
|
import { useFavoriteStatus } from 'src/views/CRUD/hooks';
|
||||||
|
|
||||||
|
const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
|
||||||
|
|
||||||
|
function DashboardCard({
|
||||||
|
dashboard,
|
||||||
|
hasPerm,
|
||||||
|
bulkSelectEnabled,
|
||||||
|
refreshData,
|
||||||
|
addDangerToast,
|
||||||
|
addSuccessToast,
|
||||||
|
openDashboardEditModal,
|
||||||
|
}: DashboardCardProps) {
|
||||||
|
const canEdit = hasPerm('can_edit');
|
||||||
|
const canDelete = hasPerm('can_delete');
|
||||||
|
const canExport = hasPerm('can_mulexport');
|
||||||
|
const [, fetchFaveStar, saveFaveStar, favoriteStatus] = useFavoriteStatus(
|
||||||
|
{},
|
||||||
|
FAVESTAR_BASE_URL,
|
||||||
|
addDangerToast,
|
||||||
|
);
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
{canEdit && openDashboardEditModal && (
|
||||||
|
<Menu.Item
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() =>
|
||||||
|
openDashboardEditModal && openDashboardEditModal(dashboard)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListViewCard.MenuIcon name="edit-alt" /> Edit
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
{canExport && (
|
||||||
|
<Menu.Item
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => handleBulkDashboardExport([dashboard])}
|
||||||
|
>
|
||||||
|
<ListViewCard.MenuIcon name="share" /> Export
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
{canDelete && (
|
||||||
|
<Menu.Item>
|
||||||
|
<ConfirmStatusChange
|
||||||
|
title={t('Please Confirm')}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
{t('Are you sure you want to delete')}{' '}
|
||||||
|
<b>{dashboard.dashboard_title}</b>?
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onConfirm={() =>
|
||||||
|
handleDashboardDelete(
|
||||||
|
dashboard,
|
||||||
|
refreshData,
|
||||||
|
addSuccessToast,
|
||||||
|
addDangerToast,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{confirmDelete => (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className="action-button"
|
||||||
|
onClick={confirmDelete}
|
||||||
|
>
|
||||||
|
<ListViewCard.MenuIcon name="trash" /> Delete
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ConfirmStatusChange>
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<ListViewCard
|
||||||
|
loading={dashboard.loading || false}
|
||||||
|
title={dashboard.dashboard_title}
|
||||||
|
titleRight={<Label>{dashboard.published ? 'published' : 'draft'}</Label>}
|
||||||
|
url={bulkSelectEnabled ? undefined : dashboard.url}
|
||||||
|
imgURL={dashboard.thumbnail_url}
|
||||||
|
imgFallbackURL="/static/assets/images/dashboard-card-fallback.png"
|
||||||
|
description={t('Last modified %s', dashboard.changed_on_delta_humanized)}
|
||||||
|
coverLeft={<FacePile users={dashboard.owners || []} />}
|
||||||
|
actions={
|
||||||
|
<ListViewCard.Actions>
|
||||||
|
<FaveStar
|
||||||
|
itemId={dashboard.id}
|
||||||
|
fetchFaveStar={fetchFaveStar}
|
||||||
|
saveFaveStar={saveFaveStar}
|
||||||
|
isStarred={!!favoriteStatus[dashboard.id]}
|
||||||
|
/>
|
||||||
|
<Dropdown overlay={menu}>
|
||||||
|
<Icon name="more-horiz" />
|
||||||
|
</Dropdown>
|
||||||
|
</ListViewCard.Actions>
|
||||||
|
}
|
||||||
|
showImg
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardCard;
|
||||||
@@ -20,22 +20,27 @@ import { SupersetClient, t } from '@superset-ui/core';
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||||
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
|
import {
|
||||||
|
createFetchRelated,
|
||||||
|
createErrorHandler,
|
||||||
|
handleDashboardDelete,
|
||||||
|
handleBulkDashboardExport,
|
||||||
|
} from 'src/views/CRUD/utils';
|
||||||
import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
|
import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
|
||||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||||
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
||||||
import FacePile from 'src/components/FacePile';
|
|
||||||
import ListView, { ListViewProps, Filters } from 'src/components/ListView';
|
import ListView, { ListViewProps, Filters } from 'src/components/ListView';
|
||||||
import Owner from 'src/types/Owner';
|
import Owner from 'src/types/Owner';
|
||||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
|
import FacePile from 'src/components/FacePile';
|
||||||
import Icon from 'src/components/Icon';
|
import Icon from 'src/components/Icon';
|
||||||
import Label from 'src/components/Label';
|
|
||||||
import FaveStar from 'src/components/FaveStar';
|
import FaveStar from 'src/components/FaveStar';
|
||||||
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
|
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
|
||||||
import ListViewCard from 'src/components/ListViewCard';
|
|
||||||
import { Dropdown, Menu } from 'src/common/components';
|
|
||||||
import TooltipWrapper from 'src/components/TooltipWrapper';
|
import TooltipWrapper from 'src/components/TooltipWrapper';
|
||||||
|
|
||||||
|
import Dashboard from 'src/dashboard/containers/Dashboard';
|
||||||
|
import DashboardCard from './DashboardCard';
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
|
const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
|
||||||
|
|
||||||
@@ -81,7 +86,6 @@ function DashboardList(props: DashboardListProps) {
|
|||||||
FAVESTAR_BASE_URL,
|
FAVESTAR_BASE_URL,
|
||||||
props.addDangerToast,
|
props.addDangerToast,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [dashboardToEdit, setDashboardToEdit] = useState<Dashboard | null>(
|
const [dashboardToEdit, setDashboardToEdit] = useState<Dashboard | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -119,25 +123,6 @@ function DashboardList(props: DashboardListProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDashboardDelete({
|
|
||||||
id,
|
|
||||||
dashboard_title: dashboardTitle,
|
|
||||||
}: Dashboard) {
|
|
||||||
return SupersetClient.delete({
|
|
||||||
endpoint: `/api/v1/dashboard/${id}`,
|
|
||||||
}).then(
|
|
||||||
() => {
|
|
||||||
refreshData();
|
|
||||||
props.addSuccessToast(t('Deleted: %s', dashboardTitle));
|
|
||||||
},
|
|
||||||
createErrorHandler(errMsg =>
|
|
||||||
props.addDangerToast(
|
|
||||||
t('There was an issue deleting %s: %s', dashboardTitle, errMsg),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleBulkDashboardDelete(dashboardsToDelete: Dashboard[]) {
|
function handleBulkDashboardDelete(dashboardsToDelete: Dashboard[]) {
|
||||||
return SupersetClient.delete({
|
return SupersetClient.delete({
|
||||||
endpoint: `/api/v1/dashboard/?q=${rison.encode(
|
endpoint: `/api/v1/dashboard/?q=${rison.encode(
|
||||||
@@ -155,14 +140,6 @@ function DashboardList(props: DashboardListProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBulkDashboardExport(dashboardsToExport: Dashboard[]) {
|
|
||||||
return window.location.assign(
|
|
||||||
`/api/v1/dashboard/export/?q=${rison.encode(
|
|
||||||
dashboardsToExport.map(({ id }) => id),
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderFaveStar(id: number) {
|
function renderFaveStar(id: number) {
|
||||||
return (
|
return (
|
||||||
<FaveStar
|
<FaveStar
|
||||||
@@ -255,7 +232,13 @@ function DashboardList(props: DashboardListProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Cell: ({ row: { original } }: any) => {
|
Cell: ({ row: { original } }: any) => {
|
||||||
const handleDelete = () => handleDashboardDelete(original);
|
const handleDelete = () =>
|
||||||
|
handleDashboardDelete(
|
||||||
|
original,
|
||||||
|
refreshData,
|
||||||
|
props.addSuccessToast,
|
||||||
|
props.addDangerToast,
|
||||||
|
);
|
||||||
const handleEdit = () => openDashboardEditModal(original);
|
const handleEdit = () => openDashboardEditModal(original);
|
||||||
const handleExport = () => handleBulkDashboardExport([original]);
|
const handleExport = () => handleBulkDashboardExport([original]);
|
||||||
|
|
||||||
@@ -418,83 +401,18 @@ function DashboardList(props: DashboardListProps) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function renderCard(dashboard: Dashboard & { loading: boolean }) {
|
function renderCard(dashboard: Dashboard) {
|
||||||
const menu = (
|
|
||||||
<Menu>
|
|
||||||
{canDelete && (
|
|
||||||
<Menu.Item>
|
|
||||||
<ConfirmStatusChange
|
|
||||||
title={t('Please Confirm')}
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
{t('Are you sure you want to delete')}{' '}
|
|
||||||
<b>{dashboard.dashboard_title}</b>?
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
onConfirm={() => handleDashboardDelete(dashboard)}
|
|
||||||
>
|
|
||||||
{confirmDelete => (
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
className="action-button"
|
|
||||||
onClick={confirmDelete}
|
|
||||||
>
|
|
||||||
<ListViewCard.MenuIcon
|
|
||||||
data-test="dashboard-list-view-card-trash-icon"
|
|
||||||
name="trash"
|
|
||||||
/>{' '}
|
|
||||||
Delete
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ConfirmStatusChange>
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
{canExport && (
|
|
||||||
<Menu.Item
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => handleBulkDashboardExport([dashboard])}
|
|
||||||
>
|
|
||||||
<ListViewCard.MenuIcon name="share" /> Export
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
{canEdit && (
|
|
||||||
<Menu.Item
|
|
||||||
data-test="dashboard-list-edit-option"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => openDashboardEditModal(dashboard)}
|
|
||||||
>
|
|
||||||
<ListViewCard.MenuIcon name="edit-alt" /> Edit
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListViewCard
|
<DashboardCard
|
||||||
loading={dashboard.loading}
|
{...{
|
||||||
title={dashboard.dashboard_title}
|
dashboard,
|
||||||
titleRight={
|
hasPerm,
|
||||||
<Label>{dashboard.published ? 'published' : 'draft'}</Label>
|
bulkSelectEnabled,
|
||||||
}
|
refreshData,
|
||||||
url={bulkSelectEnabled ? undefined : dashboard.url}
|
addDangerToast: props.addDangerToast,
|
||||||
imgURL={dashboard.thumbnail_url}
|
addSuccessToast: props.addSuccessToast,
|
||||||
imgFallbackURL="/static/assets/images/dashboard-card-fallback.png"
|
openDashboardEditModal,
|
||||||
description={t(
|
}}
|
||||||
'Last modified %s',
|
|
||||||
dashboard.changed_on_delta_humanized,
|
|
||||||
)}
|
|
||||||
coverLeft={<FacePile users={dashboard.owners || []} />}
|
|
||||||
actions={
|
|
||||||
<ListViewCard.Actions>
|
|
||||||
{renderFaveStar(dashboard.id)}
|
|
||||||
<Dropdown overlay={menu}>
|
|
||||||
<Icon name="more-horiz" />
|
|
||||||
</Dropdown>
|
|
||||||
</ListViewCard.Actions>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { t } from '@superset-ui/core';
|
|||||||
|
|
||||||
export const commonMenuData = {
|
export const commonMenuData = {
|
||||||
name: t('Data'),
|
name: t('Data'),
|
||||||
children: [
|
tabs: [
|
||||||
{
|
{
|
||||||
name: 'Datasets',
|
name: 'Datasets',
|
||||||
label: t('Datasets'),
|
label: t('Datasets'),
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import DeleteModal from 'src/components/DeleteModal';
|
|||||||
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
|
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
|
||||||
import { IconName } from 'src/components/Icon';
|
import { IconName } from 'src/components/Icon';
|
||||||
import { commonMenuData } from 'src/views/CRUD/data/common';
|
import { commonMenuData } from 'src/views/CRUD/data/common';
|
||||||
|
import { SavedQueryObject } from 'src/views/CRUD/types';
|
||||||
import SavedQueryPreviewModal from './SavedQueryPreviewModal';
|
import SavedQueryPreviewModal from './SavedQueryPreviewModal';
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
@@ -48,20 +49,6 @@ interface SavedQueryListProps {
|
|||||||
addSuccessToast: (msg: string) => void;
|
addSuccessToast: (msg: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedQueryObject = {
|
|
||||||
database: {
|
|
||||||
database_name: string;
|
|
||||||
id: number;
|
|
||||||
};
|
|
||||||
db_id: number;
|
|
||||||
description?: string;
|
|
||||||
id: number;
|
|
||||||
label: string;
|
|
||||||
schema: string;
|
|
||||||
sql: string;
|
|
||||||
sql_tables: Array<{ catalog?: string; schema: string; table: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledTableLabel = styled.div`
|
const StyledTableLabel = styled.div`
|
||||||
.count {
|
.count {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { SupersetClient, t } from '@superset-ui/core';
|
|||||||
|
|
||||||
import { createErrorHandler } from 'src/views/CRUD/utils';
|
import { createErrorHandler } from 'src/views/CRUD/utils';
|
||||||
import { FetchDataConfig } from 'src/components/ListView';
|
import { FetchDataConfig } from 'src/components/ListView';
|
||||||
|
import Chart, { Slice } from 'src/types/Chart';
|
||||||
import { FavoriteStatus } from './types';
|
import { FavoriteStatus } from './types';
|
||||||
|
|
||||||
interface ListViewResourceState<D extends object = any> {
|
interface ListViewResourceState<D extends object = any> {
|
||||||
@@ -350,5 +351,87 @@ export function useFavoriteStatus(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return [favoriteStatusRef, fetchFaveStar, saveFaveStar] as const;
|
return [
|
||||||
|
favoriteStatusRef,
|
||||||
|
fetchFaveStar,
|
||||||
|
saveFaveStar,
|
||||||
|
favoriteStatus,
|
||||||
|
] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useChartEditModal = (
|
||||||
|
setCharts: (charts: Array<Chart>) => void,
|
||||||
|
charts: Array<Chart>,
|
||||||
|
) => {
|
||||||
|
const [
|
||||||
|
sliceCurrentlyEditing,
|
||||||
|
setSliceCurrentlyEditing,
|
||||||
|
] = useState<Slice | null>(null);
|
||||||
|
|
||||||
|
function openChartEditModal(chart: Chart) {
|
||||||
|
setSliceCurrentlyEditing({
|
||||||
|
slice_id: chart.id,
|
||||||
|
slice_name: chart.slice_name,
|
||||||
|
description: chart.description,
|
||||||
|
cache_timeout: chart.cache_timeout,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeChartEditModal() {
|
||||||
|
setSliceCurrentlyEditing(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChartUpdated(edits: Chart) {
|
||||||
|
// update the chart in our state with the edited info
|
||||||
|
const newCharts = charts.map((chart: Chart) =>
|
||||||
|
chart.id === edits.id ? { ...chart, ...edits } : chart,
|
||||||
|
);
|
||||||
|
setCharts(newCharts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sliceCurrentlyEditing,
|
||||||
|
handleChartUpdated,
|
||||||
|
openChartEditModal,
|
||||||
|
closeChartEditModal,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const copyQueryLink = (
|
||||||
|
id: number,
|
||||||
|
addDangerToast: (arg0: string) => void,
|
||||||
|
addSuccessToast: (arg0: string) => void,
|
||||||
|
) => {
|
||||||
|
const selection: Selection | null = document.getSelection();
|
||||||
|
|
||||||
|
if (selection) {
|
||||||
|
selection.removeAllRanges();
|
||||||
|
const range = document.createRange();
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = `${window.location.origin}/superset/sqllab?savedQueryId=${id}`;
|
||||||
|
span.style.position = 'fixed';
|
||||||
|
span.style.top = '0';
|
||||||
|
span.style.clip = 'rect(0, 0, 0, 0)';
|
||||||
|
span.style.whiteSpace = 'pre';
|
||||||
|
|
||||||
|
document.body.appendChild(span);
|
||||||
|
range.selectNode(span);
|
||||||
|
selection.addRange(range);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!document.execCommand('copy')) {
|
||||||
|
throw new Error(t('Not successful'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
addDangerToast(t('Sorry, your browser does not support copying.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(span);
|
||||||
|
if (selection.removeRange) {
|
||||||
|
selection.removeRange(range);
|
||||||
|
} else {
|
||||||
|
selection.removeAllRanges();
|
||||||
|
}
|
||||||
|
addSuccessToast(t('Link Copied!'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,7 +16,56 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import { User } from 'src/types/bootstrapTypes';
|
||||||
|
import Owner from 'src/types/Owner';
|
||||||
|
|
||||||
export type FavoriteStatus = {
|
export type FavoriteStatus = {
|
||||||
[id: number]: boolean;
|
[id: number]: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface DashboardTableProps {
|
||||||
|
addDangerToast: (message: string) => void;
|
||||||
|
addSuccessToast: (message: string) => void;
|
||||||
|
search: string;
|
||||||
|
user?: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Dashboard {
|
||||||
|
changed_by_name: string;
|
||||||
|
changed_by_url: string;
|
||||||
|
changed_on_delta_humanized: string;
|
||||||
|
changed_by: string;
|
||||||
|
dashboard_title: string;
|
||||||
|
slice_name?: string;
|
||||||
|
id: number;
|
||||||
|
published: boolean;
|
||||||
|
url: string;
|
||||||
|
thumbnail_url: string;
|
||||||
|
owners: Owner[];
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardCardProps {
|
||||||
|
isChart?: boolean;
|
||||||
|
dashboard: Dashboard;
|
||||||
|
hasPerm: (name: string) => boolean;
|
||||||
|
bulkSelectEnabled: boolean;
|
||||||
|
refreshData: () => void;
|
||||||
|
addDangerToast: (msg: string) => void;
|
||||||
|
addSuccessToast: (msg: string) => void;
|
||||||
|
openDashboardEditModal?: (d: Dashboard) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SavedQueryObject = {
|
||||||
|
database: {
|
||||||
|
database_name: string;
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
db_id: number;
|
||||||
|
description?: string;
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
schema: string;
|
||||||
|
sql: string;
|
||||||
|
sql_tables: Array<{ catalog?: string; schema: string; table: string }>;
|
||||||
|
};
|
||||||
|
|||||||
@@ -17,12 +17,16 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
t,
|
||||||
SupersetClient,
|
SupersetClient,
|
||||||
SupersetClientResponse,
|
SupersetClientResponse,
|
||||||
logging,
|
logging,
|
||||||
|
styled,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
|
import Chart from 'src/types/Chart';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import getClientErrorObject from 'src/utils/getClientErrorObject';
|
import getClientErrorObject from 'src/utils/getClientErrorObject';
|
||||||
|
import { Dashboard } from './types';
|
||||||
|
|
||||||
const createFetchResourceMethod = (method: string) => (
|
const createFetchResourceMethod = (method: string) => (
|
||||||
resource: string,
|
resource: string,
|
||||||
@@ -53,6 +57,102 @@ const createFetchResourceMethod = (method: string) => (
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getRecentAcitivtyObjs = (
|
||||||
|
userId: string | number,
|
||||||
|
recent: string,
|
||||||
|
addDangerToast: (arg0: string, arg1: string) => void,
|
||||||
|
) => {
|
||||||
|
const getParams = (filters?: Array<any>) => {
|
||||||
|
const params = {
|
||||||
|
order_column: 'changed_on_delta_humanized',
|
||||||
|
order_direction: 'desc',
|
||||||
|
page: 0,
|
||||||
|
page_size: 3,
|
||||||
|
filters,
|
||||||
|
};
|
||||||
|
if (!filters) delete params.filters;
|
||||||
|
return rison.encode(params);
|
||||||
|
};
|
||||||
|
const filters = {
|
||||||
|
// chart and dashbaord uses same filters
|
||||||
|
// for edited and created
|
||||||
|
edited: [
|
||||||
|
{
|
||||||
|
col: 'changed_by',
|
||||||
|
opr: 'rel_o_m',
|
||||||
|
value: `${userId}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
created: [
|
||||||
|
{
|
||||||
|
col: 'created_by',
|
||||||
|
opr: 'rel_o_m',
|
||||||
|
value: `${userId}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const baseBatch = [
|
||||||
|
SupersetClient.get({ endpoint: recent }),
|
||||||
|
SupersetClient.get({
|
||||||
|
endpoint: `/api/v1/dashboard/?q=${getParams(filters.edited)}`,
|
||||||
|
}),
|
||||||
|
SupersetClient.get({
|
||||||
|
endpoint: `/api/v1/chart/?q=${getParams(filters.edited)}`,
|
||||||
|
}),
|
||||||
|
SupersetClient.get({
|
||||||
|
endpoint: `/api/v1/dashboard/?q=${getParams(filters.created)}`,
|
||||||
|
}),
|
||||||
|
SupersetClient.get({
|
||||||
|
endpoint: `/api/v1/chart/?q=${getParams(filters.created)}`,
|
||||||
|
}),
|
||||||
|
SupersetClient.get({
|
||||||
|
endpoint: `/api/v1/saved_query/?q=${getParams(filters.created)}`,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return Promise.all(baseBatch).then(
|
||||||
|
([
|
||||||
|
recentsRes,
|
||||||
|
editedDash,
|
||||||
|
editedChart,
|
||||||
|
createdByDash,
|
||||||
|
createdByChart,
|
||||||
|
createdByQuery,
|
||||||
|
]) => {
|
||||||
|
const res: any = {
|
||||||
|
editedDash: editedDash.json?.result.slice(0, 3),
|
||||||
|
editedChart: editedChart.json?.result.slice(0, 3),
|
||||||
|
createdByDash: createdByDash.json?.result.slice(0, 3),
|
||||||
|
createdByChart: createdByChart.json?.result.slice(0, 3),
|
||||||
|
createdByQuery: createdByQuery.json?.result.slice(0, 3),
|
||||||
|
};
|
||||||
|
if (recentsRes.json.length === 0) {
|
||||||
|
const newBatch = [
|
||||||
|
SupersetClient.get({ endpoint: `/api/v1/chart/?q=${getParams()}` }),
|
||||||
|
SupersetClient.get({
|
||||||
|
endpoint: `/api/v1/dashboard/?q=${getParams()}`,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return Promise.all(newBatch)
|
||||||
|
.then(([chartRes, dashboardRes]) => {
|
||||||
|
res.examples = [
|
||||||
|
...chartRes.json.result,
|
||||||
|
...dashboardRes.json.result,
|
||||||
|
];
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch(e =>
|
||||||
|
addDangerToast(
|
||||||
|
'There was an error fetching you recent activity:',
|
||||||
|
e,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
res.viewed = recentsRes.json;
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const createFetchRelated = createFetchResourceMethod('related');
|
export const createFetchRelated = createFetchResourceMethod('related');
|
||||||
export const createFetchDistinct = createFetchResourceMethod('distinct');
|
export const createFetchDistinct = createFetchResourceMethod('distinct');
|
||||||
|
|
||||||
@@ -63,3 +163,100 @@ export function createErrorHandler(handleErrorFunc: (errMsg?: string) => void) {
|
|||||||
handleErrorFunc(parsedError.message || parsedError.error);
|
handleErrorFunc(parsedError.message || parsedError.error);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleChartDelete(
|
||||||
|
{ id, slice_name: sliceName }: Chart,
|
||||||
|
addSuccessToast: (arg0: string) => void,
|
||||||
|
addDangerToast: (arg0: string) => void,
|
||||||
|
refreshData: () => void,
|
||||||
|
) {
|
||||||
|
SupersetClient.delete({
|
||||||
|
endpoint: `/api/v1/chart/${id}`,
|
||||||
|
}).then(
|
||||||
|
() => {
|
||||||
|
refreshData();
|
||||||
|
addSuccessToast(t('Deleted: %s', sliceName));
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
addDangerToast(t('There was an issue deleting: %s', sliceName));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleBulkDashboardExport(dashboardsToExport: Dashboard[]) {
|
||||||
|
return window.location.assign(
|
||||||
|
`/api/v1/dashboard/export/?q=${rison.encode(
|
||||||
|
dashboardsToExport.map(({ id }) => id),
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleDashboardDelete(
|
||||||
|
{ id, dashboard_title: dashboardTitle }: Dashboard,
|
||||||
|
refreshData: () => void,
|
||||||
|
addSuccessToast: (arg0: string) => void,
|
||||||
|
addDangerToast: (arg0: string) => void,
|
||||||
|
) {
|
||||||
|
return SupersetClient.delete({
|
||||||
|
endpoint: `/api/v1/dashboard/${id}`,
|
||||||
|
}).then(
|
||||||
|
() => {
|
||||||
|
refreshData();
|
||||||
|
addSuccessToast(t('Deleted: %s', dashboardTitle));
|
||||||
|
},
|
||||||
|
createErrorHandler(errMsg =>
|
||||||
|
addDangerToast(
|
||||||
|
t('There was an issue deleting %s: %s', dashboardTitle, errMsg),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createChartDeleteFunction(
|
||||||
|
{ id, slice_name: sliceName }: Chart,
|
||||||
|
addSuccessToast: (arg0: string) => void,
|
||||||
|
addDangerToast: (arg0: string) => void,
|
||||||
|
refreshData: () => void,
|
||||||
|
) {
|
||||||
|
SupersetClient.delete({
|
||||||
|
endpoint: `/api/v1/chart/${id}`,
|
||||||
|
}).then(
|
||||||
|
() => {
|
||||||
|
refreshData();
|
||||||
|
addSuccessToast(t('Deleted: %s', sliceName));
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
addDangerToast(t('There was an issue deleting: %s', sliceName));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const breakpoints = [576, 768, 992, 1200];
|
||||||
|
export const mq = breakpoints.map(bp => `@media (max-width: ${bp}px)`);
|
||||||
|
|
||||||
|
export const CardContainer = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(31%, max-content));
|
||||||
|
${[mq[3]]} {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(31%, max-content));
|
||||||
|
}
|
||||||
|
|
||||||
|
${[mq[2]]} {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(48%, max-content));
|
||||||
|
}
|
||||||
|
|
||||||
|
${[mq[1]]} {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(50%, max-content));
|
||||||
|
}
|
||||||
|
grid-gap: ${({ theme }) => theme.gridUnit * 8}px;
|
||||||
|
justify-content: left;
|
||||||
|
padding: ${({ theme }) => theme.gridUnit * 2}px
|
||||||
|
${({ theme }) => theme.gridUnit * 6}px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const IconContainer = styled.div`
|
||||||
|
svg {
|
||||||
|
vertical-align: -7px;
|
||||||
|
color: ${({ theme }) => theme.colors.primary.dark1};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
209
superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
Normal file
209
superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import moment from 'antd/node_modules/moment';
|
||||||
|
import { styled, t } from '@superset-ui/core';
|
||||||
|
|
||||||
|
import ListViewCard from 'src/components/ListViewCard';
|
||||||
|
import { addDangerToast } from 'src/messageToasts/actions';
|
||||||
|
import SubMenu from 'src/components/Menu/SubMenu';
|
||||||
|
import { reject } from 'lodash';
|
||||||
|
import { getRecentAcitivtyObjs, mq } from '../utils';
|
||||||
|
import EmptyState from './EmptyState';
|
||||||
|
|
||||||
|
interface ActivityObjects {
|
||||||
|
action?: string;
|
||||||
|
item_title?: string;
|
||||||
|
slice_name: string;
|
||||||
|
time: string;
|
||||||
|
changed_on_utc: string;
|
||||||
|
url: string;
|
||||||
|
sql: string;
|
||||||
|
dashboard_title: string;
|
||||||
|
label: string;
|
||||||
|
id: string;
|
||||||
|
table: object;
|
||||||
|
item_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivityProps {
|
||||||
|
user: {
|
||||||
|
userId: string | number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivityData {
|
||||||
|
Created?: Array<object>;
|
||||||
|
Edited?: Array<object>;
|
||||||
|
Viewed?: Array<object>;
|
||||||
|
Examples?: Array<object>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActivityContainer = styled.div`
|
||||||
|
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
|
||||||
|
margin-top: ${({ theme }) => theme.gridUnit * -4}px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(31%, max-content));
|
||||||
|
${[mq[3]]} {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(31%, max-content));
|
||||||
|
}
|
||||||
|
${[mq[2]]} {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(42%, max-content));
|
||||||
|
}
|
||||||
|
${[mq[1]]} {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(63%, max-content));
|
||||||
|
}
|
||||||
|
grid-gap: ${({ theme }) => theme.gridUnit * 8}px;
|
||||||
|
justify-content: left;
|
||||||
|
padding: ${({ theme }) => theme.gridUnit * 2}px
|
||||||
|
${({ theme }) => theme.gridUnit * 4}px;
|
||||||
|
.ant-card-meta-avatar {
|
||||||
|
margin-top: ${({ theme }) => theme.gridUnit * 3}px;
|
||||||
|
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
|
||||||
|
}
|
||||||
|
.ant-card-meta-title {
|
||||||
|
font-weight: ${({ theme }) => theme.typography.weights.bold};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function ActivityTable({ user }: ActivityProps) {
|
||||||
|
const [activityData, setActivityData] = useState<ActivityData>({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [activeChild, setActiveChild] = useState('Viewed');
|
||||||
|
// this api uses log for data which in some cases can be empty
|
||||||
|
const recent = `/superset/recent_activity/${user.userId}/?limit=5`;
|
||||||
|
|
||||||
|
const getFilterTitle = (e: ActivityObjects) => {
|
||||||
|
if (e.dashboard_title) return e.dashboard_title;
|
||||||
|
if (e.label) return e.label;
|
||||||
|
if (e.url && !e.table) return e.item_title;
|
||||||
|
if (e.item_title) return e.item_title;
|
||||||
|
return e.slice_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIconName = (e: ActivityObjects) => {
|
||||||
|
if (e.sql) return 'sql';
|
||||||
|
if (e.url?.includes('dashboard')) {
|
||||||
|
return 'nav-dashboard';
|
||||||
|
}
|
||||||
|
if (e.url?.includes('explore') || e.item_url?.includes('explore')) {
|
||||||
|
return 'nav-charts';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: 'Edited',
|
||||||
|
label: t('Edited'),
|
||||||
|
onClick: () => {
|
||||||
|
setActiveChild('Edited');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Created',
|
||||||
|
label: t('Created'),
|
||||||
|
onClick: () => {
|
||||||
|
setActiveChild('Created');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (activityData.Viewed) {
|
||||||
|
tabs.unshift({
|
||||||
|
name: 'Viewed',
|
||||||
|
label: t('Viewed'),
|
||||||
|
onClick: () => {
|
||||||
|
setActiveChild('Viewed');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tabs.unshift({
|
||||||
|
name: 'Examples',
|
||||||
|
label: t('Examples'),
|
||||||
|
onClick: () => {
|
||||||
|
setActiveChild('Examples');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getRecentAcitivtyObjs(user.userId, recent, addDangerToast)
|
||||||
|
.then(res => {
|
||||||
|
const data: any = {
|
||||||
|
Created: [
|
||||||
|
...res.createdByChart,
|
||||||
|
...res.createdByDash,
|
||||||
|
...res.createdByQuery,
|
||||||
|
],
|
||||||
|
Edited: [...res.editedChart, ...res.editedDash],
|
||||||
|
};
|
||||||
|
if (res.viewed) {
|
||||||
|
const filtered = reject(res.viewed, ['item_url', null]).map(r => r);
|
||||||
|
data.Viewed = filtered;
|
||||||
|
setActiveChild('Viewed');
|
||||||
|
} else {
|
||||||
|
data.Examples = res.examples;
|
||||||
|
setActiveChild('Examples');
|
||||||
|
}
|
||||||
|
setActivityData(data);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
setLoading(false);
|
||||||
|
addDangerToast(
|
||||||
|
`There was an issue fetching your recent Acitivity: ${e}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderActivity = () => {
|
||||||
|
return activityData[activeChild].map((e: ActivityObjects) => (
|
||||||
|
<ListViewCard
|
||||||
|
key={`${e.id}`}
|
||||||
|
isRecent
|
||||||
|
loading={loading}
|
||||||
|
url={e.sql ? `/supserset/sqllab?queryId=${e.id}` : e.url}
|
||||||
|
title={getFilterTitle(e)}
|
||||||
|
description={`Last Edited: ${moment(e.changed_on_utc).format(
|
||||||
|
'MM/DD/YYYY HH:mm:ss',
|
||||||
|
)}`}
|
||||||
|
avatar={getIconName(e)}
|
||||||
|
actions={null}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if (loading) return <>loading ...</>;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SubMenu
|
||||||
|
activeChild={activeChild}
|
||||||
|
// eslint-disable-next-line react/no-children-prop
|
||||||
|
tabs={tabs}
|
||||||
|
/>
|
||||||
|
<>
|
||||||
|
{activityData[activeChild]?.length > 0 ? (
|
||||||
|
<ActivityContainer>{renderActivity()}</ActivityContainer>
|
||||||
|
) : (
|
||||||
|
<EmptyState tableName="RECENTS" tab={activeChild} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
167
superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
Normal file
167
superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { t } from '@superset-ui/core';
|
||||||
|
import { useListViewResource, useChartEditModal } from 'src/views/CRUD/hooks';
|
||||||
|
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
|
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||||
|
import { User } from 'src/types/bootstrapTypes';
|
||||||
|
import Icon from 'src/components/Icon';
|
||||||
|
import ChartCard from 'src/views/CRUD/chart/ChartCard';
|
||||||
|
import Chart from 'src/types/Chart';
|
||||||
|
import SubMenu from 'src/components/Menu/SubMenu';
|
||||||
|
import EmptyState from './EmptyState';
|
||||||
|
import { CardContainer, IconContainer } from '../utils';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 3;
|
||||||
|
|
||||||
|
interface ChartTableProps {
|
||||||
|
addDangerToast: (message: string) => void;
|
||||||
|
addSuccessToast: (message: string) => void;
|
||||||
|
search: string;
|
||||||
|
chartFilter?: string;
|
||||||
|
user?: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChartTable({
|
||||||
|
user,
|
||||||
|
addDangerToast,
|
||||||
|
addSuccessToast,
|
||||||
|
}: ChartTableProps) {
|
||||||
|
const {
|
||||||
|
state: { loading, resourceCollection: charts, bulkSelectEnabled },
|
||||||
|
setResourceCollection: setCharts,
|
||||||
|
hasPerm,
|
||||||
|
refreshData,
|
||||||
|
fetchData,
|
||||||
|
} = useListViewResource<Chart>('chart', t('chart'), addDangerToast);
|
||||||
|
const {
|
||||||
|
sliceCurrentlyEditing,
|
||||||
|
openChartEditModal,
|
||||||
|
handleChartUpdated,
|
||||||
|
closeChartEditModal,
|
||||||
|
} = useChartEditModal(setCharts, charts);
|
||||||
|
|
||||||
|
const [chartFilter, setChartFilter] = useState('Mine');
|
||||||
|
|
||||||
|
const getFilters = () => {
|
||||||
|
const filters = [];
|
||||||
|
|
||||||
|
if (chartFilter === 'Mine') {
|
||||||
|
filters.push({
|
||||||
|
id: 'created_by',
|
||||||
|
operator: 'rel_o_m',
|
||||||
|
value: `${user?.userId}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
filters.push({
|
||||||
|
id: 'id',
|
||||||
|
operator: 'chart_is_fav',
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return filters;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
sortBy: [
|
||||||
|
{
|
||||||
|
id: 'changed_on_delta_humanized',
|
||||||
|
desc: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
filters: getFilters(),
|
||||||
|
});
|
||||||
|
}, [chartFilter]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{sliceCurrentlyEditing && (
|
||||||
|
<PropertiesModal
|
||||||
|
onHide={closeChartEditModal}
|
||||||
|
onSave={handleChartUpdated}
|
||||||
|
show
|
||||||
|
slice={sliceCurrentlyEditing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SubMenu
|
||||||
|
activeChild={chartFilter}
|
||||||
|
// eslint-disable-next-line react/no-children-prop
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
name: 'Favorite',
|
||||||
|
label: t('Favorite'),
|
||||||
|
onClick: () => setChartFilter('Favorite'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mine',
|
||||||
|
label: t('Mine'),
|
||||||
|
onClick: () => setChartFilter('Mine'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
buttons={[
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<IconContainer>
|
||||||
|
<Icon name="plus-small" />
|
||||||
|
{t('Chart')}
|
||||||
|
</IconContainer>
|
||||||
|
),
|
||||||
|
buttonStyle: 'tertiary',
|
||||||
|
onClick: () => {
|
||||||
|
window.location.href = '/chart/add';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'View All »',
|
||||||
|
buttonStyle: 'link',
|
||||||
|
onClick: () => {
|
||||||
|
window.location.href = '/chart/list';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{charts?.length ? (
|
||||||
|
<CardContainer>
|
||||||
|
{charts.map(e => (
|
||||||
|
<ChartCard
|
||||||
|
key={`${e.id}`}
|
||||||
|
openChartEditModal={openChartEditModal}
|
||||||
|
loading={loading}
|
||||||
|
chart={e}
|
||||||
|
hasPerm={hasPerm}
|
||||||
|
bulkSelectEnabled={bulkSelectEnabled}
|
||||||
|
refreshData={refreshData}
|
||||||
|
addDangerToast={addDangerToast}
|
||||||
|
addSuccessToast={addSuccessToast}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CardContainer>
|
||||||
|
) : (
|
||||||
|
<EmptyState tableName="CHARTS" tab={chartFilter} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withToasts(ChartTable);
|
||||||
@@ -16,169 +16,176 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { t, SupersetClient } from '@superset-ui/core';
|
import { SupersetClient, t } from '@superset-ui/core';
|
||||||
import { debounce } from 'lodash';
|
import { useListViewResource } from 'src/views/CRUD/hooks';
|
||||||
import ListView, { FetchDataConfig } from 'src/components/ListView';
|
import { Dashboard, DashboardTableProps } from 'src/views/CRUD/types';
|
||||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
import { Dashboard } from 'src/types/bootstrapTypes';
|
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
|
||||||
|
import DashboardCard from 'src/views/CRUD/dashboard/DashboardCard';
|
||||||
|
import SubMenu from 'src/components/Menu/SubMenu';
|
||||||
|
import Icon from 'src/components/Icon';
|
||||||
|
import EmptyState from './EmptyState';
|
||||||
|
import { createErrorHandler, CardContainer, IconContainer } from '../utils';
|
||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 3;
|
||||||
|
|
||||||
interface DashboardTableProps {
|
export interface FilterValue {
|
||||||
addDangerToast: (message: string) => void;
|
col: string;
|
||||||
search?: string;
|
operator: string;
|
||||||
|
value: string | boolean | number | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DashboardTableState {
|
function DashboardTable({
|
||||||
dashboards: Dashboard[];
|
user,
|
||||||
dashboard_count: number;
|
addDangerToast,
|
||||||
loading: boolean;
|
addSuccessToast,
|
||||||
}
|
}: DashboardTableProps) {
|
||||||
|
const {
|
||||||
|
state: { loading, resourceCollection: dashboards, bulkSelectEnabled },
|
||||||
|
setResourceCollection: setDashboards,
|
||||||
|
hasPerm,
|
||||||
|
refreshData,
|
||||||
|
fetchData,
|
||||||
|
} = useListViewResource<Dashboard>(
|
||||||
|
'dashboard',
|
||||||
|
t('dashboard'),
|
||||||
|
addDangerToast,
|
||||||
|
);
|
||||||
|
|
||||||
class DashboardTable extends React.PureComponent<
|
const [editModal, setEditModal] = useState<Dashboard>();
|
||||||
DashboardTableProps,
|
const [dashboardFilter, setDashboardFilter] = useState('Mine');
|
||||||
DashboardTableState
|
|
||||||
> {
|
|
||||||
columns = [
|
|
||||||
{
|
|
||||||
accessor: 'dashboard_title',
|
|
||||||
Header: 'Dashboard',
|
|
||||||
Cell: ({
|
|
||||||
row: {
|
|
||||||
original: { url, dashboard_title: dashboardTitle },
|
|
||||||
},
|
|
||||||
}: {
|
|
||||||
row: {
|
|
||||||
original: {
|
|
||||||
url: string;
|
|
||||||
dashboard_title: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}) => <a href={url}>{dashboardTitle}</a>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: 'changed_by.first_name',
|
|
||||||
Header: 'Modified By',
|
|
||||||
Cell: ({
|
|
||||||
row: {
|
|
||||||
original: { changed_by_name: changedByName, changedByUrl },
|
|
||||||
},
|
|
||||||
}: {
|
|
||||||
row: {
|
|
||||||
original: {
|
|
||||||
changed_by_name: string;
|
|
||||||
changedByUrl: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}) => <a href={changedByUrl}>{changedByName}</a>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: 'changed_on_delta_humanized',
|
|
||||||
Header: 'Modified',
|
|
||||||
Cell: ({
|
|
||||||
row: {
|
|
||||||
original: { changed_on_delta_humanized: changedOn },
|
|
||||||
},
|
|
||||||
}: {
|
|
||||||
row: {
|
|
||||||
original: {
|
|
||||||
changed_on_delta_humanized: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}) => <span className="no-wrap">{changedOn}</span>,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
|
|
||||||
|
|
||||||
constructor(props: DashboardTableProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
dashboards: [],
|
|
||||||
dashboard_count: 0,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: DashboardTableProps) {
|
|
||||||
if (prevProps.search !== this.props.search) {
|
|
||||||
this.fetchDataDebounced({
|
|
||||||
pageSize: PAGE_SIZE,
|
|
||||||
pageIndex: 0,
|
|
||||||
sortBy: this.initialSort,
|
|
||||||
filters: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchData = ({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => {
|
|
||||||
this.setState({ loading: true });
|
|
||||||
const filterExps = Object.keys(filters)
|
|
||||||
.map(fk => ({
|
|
||||||
col: fk,
|
|
||||||
opr: filters[fk].filterId,
|
|
||||||
value: filters[fk].filterValue,
|
|
||||||
}))
|
|
||||||
.concat(
|
|
||||||
this.props.search
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
col: 'dashboard_title',
|
|
||||||
opr: 'ct',
|
|
||||||
value: this.props.search,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
);
|
|
||||||
|
|
||||||
const queryParams = JSON.stringify({
|
|
||||||
order_column: sortBy[0].id,
|
|
||||||
order_direction: sortBy[0].desc ? 'desc' : 'asc',
|
|
||||||
page: pageIndex,
|
|
||||||
page_size: pageSize,
|
|
||||||
...(filterExps.length ? { filters: filterExps } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const handleDashboardEdit = (edits: Dashboard) => {
|
||||||
return SupersetClient.get({
|
return SupersetClient.get({
|
||||||
endpoint: `/api/v1/dashboard/?q=${queryParams}`,
|
endpoint: `/api/v1/dashboard/${edits.id}`,
|
||||||
})
|
}).then(
|
||||||
.then(({ json }) => {
|
({ json = {} }) => {
|
||||||
this.setState({ dashboards: json.result, dashboard_count: json.count });
|
setDashboards(
|
||||||
})
|
dashboards.map(dashboard => {
|
||||||
.catch(response => {
|
if (dashboard.id === json.id) {
|
||||||
if (response.status === 401) {
|
return json.result;
|
||||||
this.props.addDangerToast(
|
}
|
||||||
t(
|
return dashboard;
|
||||||
"You don't have the necessary permissions to load dashboards. Please contact your administrator.",
|
}),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
} else {
|
createErrorHandler(errMsg =>
|
||||||
this.props.addDangerToast(
|
addDangerToast(
|
||||||
t('An error occurred while fetching Dashboards'),
|
t('An error occurred while fetching dashboards: %s', errMsg),
|
||||||
);
|
),
|
||||||
}
|
),
|
||||||
})
|
);
|
||||||
.finally(() => this.setState({ loading: false }));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// sort-comp disabled because of conflict with no-use-before-define rule
|
const getFilters = () => {
|
||||||
// eslint-disable-next-line react/sort-comp
|
const filters = [];
|
||||||
fetchDataDebounced = debounce(this.fetchData, 200);
|
|
||||||
|
|
||||||
render() {
|
if (dashboardFilter === 'Mine') {
|
||||||
return (
|
filters.push({
|
||||||
<ListView
|
id: 'owners',
|
||||||
columns={this.columns}
|
operator: 'rel_m_m',
|
||||||
data={this.state.dashboards}
|
value: `${user?.userId}`,
|
||||||
count={this.state.dashboard_count}
|
});
|
||||||
pageSize={PAGE_SIZE}
|
} else {
|
||||||
fetchData={this.fetchData}
|
filters.push({
|
||||||
loading={this.state.loading}
|
id: 'id',
|
||||||
initialSort={this.initialSort}
|
operator: 'dashboard_is_fav',
|
||||||
/>
|
value: true,
|
||||||
);
|
});
|
||||||
|
}
|
||||||
|
return filters;
|
||||||
|
};
|
||||||
|
const subMenus = [];
|
||||||
|
if (dashboards.length > 0 && dashboardFilter === 'favorite') {
|
||||||
|
subMenus.push({
|
||||||
|
name: 'Favorite',
|
||||||
|
label: t('Favorite'),
|
||||||
|
onClick: () => setDashboardFilter('Favorite'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
sortBy: [
|
||||||
|
{
|
||||||
|
id: 'changed_on_delta_humanized',
|
||||||
|
desc: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
filters: getFilters(),
|
||||||
|
});
|
||||||
|
}, [dashboardFilter]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SubMenu
|
||||||
|
activeChild={dashboardFilter}
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
name: 'Favorite',
|
||||||
|
label: t('Favorite'),
|
||||||
|
onClick: () => setDashboardFilter('Favorite'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mine',
|
||||||
|
label: t('Mine'),
|
||||||
|
onClick: () => setDashboardFilter('Mine'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
buttons={[
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<IconContainer>
|
||||||
|
<Icon name="plus-small" /> Dashboard{' '}
|
||||||
|
</IconContainer>
|
||||||
|
),
|
||||||
|
buttonStyle: 'tertiary',
|
||||||
|
onClick: () => {
|
||||||
|
window.location.href = '/dashboard/new';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'View All »',
|
||||||
|
buttonStyle: 'link',
|
||||||
|
onClick: () => {
|
||||||
|
window.location.href = '/dashboard/list/';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{editModal && (
|
||||||
|
<PropertiesModal
|
||||||
|
dashboardId={editModal?.id}
|
||||||
|
show
|
||||||
|
onHide={() => setEditModal(undefined)}
|
||||||
|
onSubmit={handleDashboardEdit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{dashboards.length > 0 ? (
|
||||||
|
<CardContainer>
|
||||||
|
{dashboards.map(e => (
|
||||||
|
<DashboardCard
|
||||||
|
{...{
|
||||||
|
dashboard: e,
|
||||||
|
hasPerm,
|
||||||
|
bulkSelectEnabled,
|
||||||
|
refreshData,
|
||||||
|
addDangerToast,
|
||||||
|
addSuccessToast,
|
||||||
|
loading,
|
||||||
|
openDashboardEditModal: dashboard => setEditModal(dashboard),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CardContainer>
|
||||||
|
) : (
|
||||||
|
<EmptyState tableName="DASHBOARDS" tab={dashboardFilter} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withToasts(DashboardTable);
|
export default withToasts(DashboardTable);
|
||||||
|
|||||||
144
superset-frontend/src/views/CRUD/welcome/EmptyState.tsx
Normal file
144
superset-frontend/src/views/CRUD/welcome/EmptyState.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'src/components/Button';
|
||||||
|
import { Empty } from 'src/common/components';
|
||||||
|
import { t, styled } from '@superset-ui/core';
|
||||||
|
import Icon from 'src/components/Icon';
|
||||||
|
import { IconContainer } from '../utils';
|
||||||
|
|
||||||
|
interface EmptyStateProps {
|
||||||
|
tableName: string;
|
||||||
|
tab?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonContainer = styled.div`
|
||||||
|
Button {
|
||||||
|
svg {
|
||||||
|
color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function EmptyState({ tableName, tab }: EmptyStateProps) {
|
||||||
|
const mineRedirects = {
|
||||||
|
DASHBOARDS: '/dashboard/new',
|
||||||
|
CHARTS: '/chart/add',
|
||||||
|
SAVED_QUERIES: '/superset/sqllab',
|
||||||
|
};
|
||||||
|
const favRedirects = {
|
||||||
|
DASHBOARDS: '/dashboard/list/',
|
||||||
|
CHARTS: '/chart/list',
|
||||||
|
SAVED_QUERIES: '/savedqueryview/list/',
|
||||||
|
};
|
||||||
|
const tableIcon = {
|
||||||
|
RECENTS: 'union.png',
|
||||||
|
DASHBOARDS: 'empty-dashboard.png',
|
||||||
|
CHARTS: 'empty-charts.png',
|
||||||
|
SAVED_QUERIES: 'empty-queries.png',
|
||||||
|
};
|
||||||
|
const mine = (
|
||||||
|
<div>{`No ${
|
||||||
|
tableName === 'SAVED_QUERIES'
|
||||||
|
? t('saved queries')
|
||||||
|
: t(`${tableName.toLowerCase()}`)
|
||||||
|
} yet`}</div>
|
||||||
|
);
|
||||||
|
const recent = (
|
||||||
|
<div className="no-recents">
|
||||||
|
{(() => {
|
||||||
|
if (tab === 'Viewed') {
|
||||||
|
return t(
|
||||||
|
`Recently viewed charts, dashboards, and saved queries will appear here`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (tab === 'Created') {
|
||||||
|
return t(
|
||||||
|
'Recently created charts, dashboards, and saved queries will appear here',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (tab === 'Examples') {
|
||||||
|
return t(
|
||||||
|
`Recent example charts, dashboards, and saved queries will appear here`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (tab === 'Edited') {
|
||||||
|
return t(
|
||||||
|
`Recently edited charts, dashboards, and saved queries will appear here`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
// Mine and Recent Activity(all tabs) tab empty state
|
||||||
|
if (tab === 'Mine' || tableName === 'RECENTS') {
|
||||||
|
return (
|
||||||
|
<Empty
|
||||||
|
image={`/static/assets/images/${tableIcon[tableName]}`}
|
||||||
|
description={tableName === 'RECENTS' ? recent : mine}
|
||||||
|
>
|
||||||
|
{tableName !== 'RECENTS' && (
|
||||||
|
<ButtonContainer>
|
||||||
|
<Button
|
||||||
|
buttonStyle="primary"
|
||||||
|
onClick={() => {
|
||||||
|
window.location = mineRedirects[tableName];
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconContainer>
|
||||||
|
<Icon name="plus-small" />{' '}
|
||||||
|
{tableName === 'SAVED_QUERIES'
|
||||||
|
? t('SQL QUERY')
|
||||||
|
: t(`${tableName
|
||||||
|
.split('')
|
||||||
|
.slice(0, tableName.length - 1)
|
||||||
|
.join('')}
|
||||||
|
`)}
|
||||||
|
</IconContainer>
|
||||||
|
</Button>
|
||||||
|
</ButtonContainer>
|
||||||
|
)}
|
||||||
|
</Empty>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Favorite tab empty state
|
||||||
|
return (
|
||||||
|
<Empty
|
||||||
|
image="/static/assets/images/star-circle.png"
|
||||||
|
description={
|
||||||
|
<div className="no-favorites">
|
||||||
|
{t("You don't have any favorites yet!")}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
buttonStyle="primary"
|
||||||
|
onClick={() => {
|
||||||
|
window.location = favRedirects[tableName];
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
SEE ALL{' '}
|
||||||
|
{tableName === 'SAVED_QUERIES'
|
||||||
|
? t('SQL LAB QUERIES')
|
||||||
|
: t(`${tableName}`)}
|
||||||
|
</Button>
|
||||||
|
</Empty>
|
||||||
|
);
|
||||||
|
}
|
||||||
260
superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx
Normal file
260
superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { t, SupersetClient, styled } from '@superset-ui/core';
|
||||||
|
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
|
import { Dropdown, Menu } from 'src/common/components';
|
||||||
|
import { useListViewResource, copyQueryLink } from 'src/views/CRUD/hooks';
|
||||||
|
import ListViewCard from 'src/components/ListViewCard';
|
||||||
|
import DeleteModal from 'src/components/DeleteModal';
|
||||||
|
import Icon from 'src/components/Icon';
|
||||||
|
import SubMenu from 'src/components/Menu/SubMenu';
|
||||||
|
import EmptyState from './EmptyState';
|
||||||
|
|
||||||
|
import { IconContainer, CardContainer, createErrorHandler } from '../utils';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 3;
|
||||||
|
|
||||||
|
interface Query {
|
||||||
|
id?: number;
|
||||||
|
sql_tables?: Array<any>;
|
||||||
|
database?: {
|
||||||
|
database_name: string;
|
||||||
|
};
|
||||||
|
rows?: string;
|
||||||
|
description?: string;
|
||||||
|
end_time?: string;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SavedQueriesProps {
|
||||||
|
user: {
|
||||||
|
userId: string | number;
|
||||||
|
};
|
||||||
|
queryFilter: string;
|
||||||
|
addDangerToast: (arg0: string) => void;
|
||||||
|
addSuccessToast: (arg0: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QueryData = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
|
.title {
|
||||||
|
font-weight: ${({ theme }) => theme.typography.weights.normal};
|
||||||
|
color: ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
|
}
|
||||||
|
.holder {
|
||||||
|
margin: ${({ theme }) => theme.gridUnit * 2}px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const SavedQueries = ({
|
||||||
|
user,
|
||||||
|
addDangerToast,
|
||||||
|
addSuccessToast,
|
||||||
|
}: SavedQueriesProps) => {
|
||||||
|
const {
|
||||||
|
state: { loading, resourceCollection: queries },
|
||||||
|
hasPerm,
|
||||||
|
fetchData,
|
||||||
|
refreshData,
|
||||||
|
} = useListViewResource<Query>('saved_query', t('query'), addDangerToast);
|
||||||
|
const [queryFilter, setQueryFilter] = useState('Mine');
|
||||||
|
const [queryDeleteModal, setQueryDeleteModal] = useState(false);
|
||||||
|
const [currentlyEdited, setCurrentlyEdited] = useState<Query>({});
|
||||||
|
|
||||||
|
const canEdit = hasPerm('can_edit');
|
||||||
|
const canDelete = hasPerm('can_delete');
|
||||||
|
|
||||||
|
const handleQueryDelete = ({ id, label }: Query) => {
|
||||||
|
SupersetClient.delete({
|
||||||
|
endpoint: `/api/v1/saved_query/${id}`,
|
||||||
|
}).then(
|
||||||
|
() => {
|
||||||
|
refreshData();
|
||||||
|
setQueryDeleteModal(false);
|
||||||
|
addSuccessToast(t('Deleted: %s', label));
|
||||||
|
},
|
||||||
|
createErrorHandler(errMsg =>
|
||||||
|
addDangerToast(t('There was an issue deleting %s: %s', label, errMsg)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilters = () => {
|
||||||
|
const filters = [];
|
||||||
|
if (queryFilter === '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;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
sortBy: [
|
||||||
|
{
|
||||||
|
id: 'changed_on_delta_humanized',
|
||||||
|
desc: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
filters: getFilters(),
|
||||||
|
});
|
||||||
|
}, [queryFilter]);
|
||||||
|
|
||||||
|
const renderMenu = (query: Query) => (
|
||||||
|
<Menu>
|
||||||
|
{canEdit && (
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => {
|
||||||
|
window.location.href = `/superset/sqllab?savedQueryId=${query.id}`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Edit')}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => {
|
||||||
|
if (query.id)
|
||||||
|
copyQueryLink(query.id, addDangerToast, addSuccessToast);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Share')}
|
||||||
|
</Menu.Item>
|
||||||
|
{canDelete && (
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => {
|
||||||
|
setQueryDeleteModal(true);
|
||||||
|
setCurrentlyEdited(query);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Delete')}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{queryDeleteModal && (
|
||||||
|
<DeleteModal
|
||||||
|
description={t(
|
||||||
|
'This action will permanently delete the saved query.',
|
||||||
|
)}
|
||||||
|
onConfirm={() => {
|
||||||
|
if (queryDeleteModal) {
|
||||||
|
handleQueryDelete(currentlyEdited);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onHide={() => {
|
||||||
|
setQueryDeleteModal(false);
|
||||||
|
}}
|
||||||
|
open
|
||||||
|
title={t('Delete Query?')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SubMenu
|
||||||
|
activeChild={queryFilter}
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
name: 'Favorite',
|
||||||
|
label: t('Favorite'),
|
||||||
|
onClick: () => setQueryFilter('Favorite'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mine',
|
||||||
|
label: t('Mine'),
|
||||||
|
onClick: () => setQueryFilter('Mine'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
buttons={[
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<IconContainer>
|
||||||
|
<Icon name="plus-small" /> SQL Query{' '}
|
||||||
|
</IconContainer>
|
||||||
|
),
|
||||||
|
buttonStyle: 'tertiary',
|
||||||
|
onClick: () => {
|
||||||
|
window.location.href = '/superset/sqllab';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'View All »',
|
||||||
|
buttonStyle: 'link',
|
||||||
|
onClick: () => {
|
||||||
|
window.location.href = '/savedqueryview/list';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{queries.length > 0 ? (
|
||||||
|
<CardContainer>
|
||||||
|
{queries.map(q => (
|
||||||
|
<ListViewCard
|
||||||
|
key={`${q.id}`}
|
||||||
|
imgFallbackURL=""
|
||||||
|
imgURL=""
|
||||||
|
url={`/superset/sqllab?savedQueryId=${q.id}`}
|
||||||
|
title={q.label}
|
||||||
|
rows={q.rows}
|
||||||
|
loading={loading}
|
||||||
|
description={t('Last run ', q.end_time)}
|
||||||
|
showImg={false}
|
||||||
|
renderCover={
|
||||||
|
<QueryData>
|
||||||
|
<div className="holder">
|
||||||
|
<div className="title">{t('Tables')}</div>
|
||||||
|
<div>{q?.sql_tables?.length}</div>
|
||||||
|
</div>
|
||||||
|
<div className="holder">
|
||||||
|
<div className="title">{t('Datasource Name')}</div>
|
||||||
|
<div>{q?.sql_tables && q.sql_tables[0]?.table}</div>
|
||||||
|
</div>
|
||||||
|
</QueryData>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<ListViewCard.Actions>
|
||||||
|
<Dropdown overlay={renderMenu(q)}>
|
||||||
|
<Icon name="more-horiz" />
|
||||||
|
</Dropdown>
|
||||||
|
</ListViewCard.Actions>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CardContainer>
|
||||||
|
) : (
|
||||||
|
<EmptyState tableName="SAVED_QUERIES" tab={queryFilter} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withToasts(SavedQueries);
|
||||||
@@ -16,128 +16,79 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useCallback, useState } from 'react';
|
import React from 'react';
|
||||||
import {
|
import { styled, t } from '@superset-ui/core';
|
||||||
Panel,
|
import { Collapse } from 'src/common/components';
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
Tabs,
|
|
||||||
Tab,
|
|
||||||
FormControl,
|
|
||||||
FormControlProps,
|
|
||||||
} from 'react-bootstrap';
|
|
||||||
import { t } from '@superset-ui/core';
|
|
||||||
import { useQueryParam, StringParam, QueryParamConfig } from 'use-query-params';
|
|
||||||
import { User } from 'src/types/bootstrapTypes';
|
import { User } from 'src/types/bootstrapTypes';
|
||||||
import RecentActivity from 'src/profile/components/RecentActivity';
|
import { mq } from '../utils';
|
||||||
import Favorites from 'src/profile/components/Favorites';
|
import ActivityTable from './ActivityTable';
|
||||||
|
import ChartTable from './ChartTable';
|
||||||
|
import SavedQueries from './SavedQueries';
|
||||||
import DashboardTable from './DashboardTable';
|
import DashboardTable from './DashboardTable';
|
||||||
|
|
||||||
|
const { Panel } = Collapse;
|
||||||
|
|
||||||
interface WelcomeProps {
|
interface WelcomeProps {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useSyncQueryState(
|
const WelcomeContainer = styled.div`
|
||||||
queryParam: string,
|
background-color: ${({ theme }) => theme.colors.grayscale.light4};
|
||||||
queryParamType: QueryParamConfig<
|
nav {
|
||||||
string | null | undefined,
|
margin-top: -15px;
|
||||||
string | undefined
|
background-color: ${({ theme }) => theme.colors.grayscale.light4};
|
||||||
>,
|
&:after {
|
||||||
defaultState: string,
|
content: '';
|
||||||
): [string, (val: string) => void] {
|
display: block;
|
||||||
const [queryState, setQueryState] = useQueryParam(queryParam, queryParamType);
|
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
const [state, setState] = useState(queryState || defaultState);
|
margin: 0px ${({ theme }) => theme.gridUnit * 6}px;
|
||||||
|
position: relative;
|
||||||
const setQueryStateAndState = (val: string) => {
|
${[mq[1]]} {
|
||||||
setQueryState(val);
|
margin-top: 5px;
|
||||||
setState(val);
|
margin: 0px 2px;
|
||||||
};
|
}
|
||||||
|
}
|
||||||
return [state, setQueryStateAndState];
|
.nav.navbar-nav {
|
||||||
}
|
& > li:nth-child(1),
|
||||||
|
& > li:nth-child(2),
|
||||||
|
& > li:nth-child(3) {
|
||||||
|
margin-top: ${({ theme }) => theme.gridUnit * 2}px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 3px 21px;
|
||||||
|
}
|
||||||
|
.navbar-right {
|
||||||
|
position: relative;
|
||||||
|
top: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-card.ant-card-bordered {
|
||||||
|
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
|
}
|
||||||
|
.ant-collapse-header {
|
||||||
|
font-weight: ${({ theme }) => theme.typography.weights.normal};
|
||||||
|
font-size: ${({ theme }) => theme.gridUnit * 4}px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export default function Welcome({ user }: WelcomeProps) {
|
export default function Welcome({ user }: WelcomeProps) {
|
||||||
const [activeTab, setActiveTab] = useSyncQueryState(
|
|
||||||
'activeTab',
|
|
||||||
StringParam,
|
|
||||||
'all',
|
|
||||||
);
|
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useSyncQueryState(
|
|
||||||
'search',
|
|
||||||
StringParam,
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
|
|
||||||
const onFormControlChange = useCallback(
|
|
||||||
(e: React.FormEvent<FormControl & FormControlProps>) => {
|
|
||||||
const { value } = e.currentTarget;
|
|
||||||
setSearchQuery((value as string) ?? '');
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onTabsSelect = useCallback((e: any) => {
|
|
||||||
setActiveTab(e as string);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container welcome">
|
<WelcomeContainer>
|
||||||
<Tabs
|
<Collapse defaultActiveKey={['1', '2', '3', '4']} ghost>
|
||||||
activeKey={activeTab}
|
<Panel header={t('Recents')} key="1">
|
||||||
onSelect={onTabsSelect}
|
<ActivityTable user={user} />
|
||||||
id="uncontrolled-tab-example"
|
</Panel>
|
||||||
>
|
<Panel header={t('Dashboards')} key="2">
|
||||||
<Tab eventKey="all" title={t('Dashboards')}>
|
<DashboardTable user={user} />
|
||||||
<Panel>
|
</Panel>
|
||||||
<Panel.Body>
|
<Panel header={t('Saved Queries')} key="3">
|
||||||
<Row>
|
<SavedQueries user={user} />
|
||||||
<Col md={8}>
|
</Panel>
|
||||||
<h2>{t('Dashboards')}</h2>
|
<Panel header={t('Charts')} key="4">
|
||||||
</Col>
|
<ChartTable user={user} />
|
||||||
<Col md={4}>
|
</Panel>
|
||||||
<FormControl
|
</Collapse>
|
||||||
type="text"
|
</WelcomeContainer>
|
||||||
bsSize="sm"
|
|
||||||
style={{ marginTop: '25px' }}
|
|
||||||
placeholder="Search"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={onFormControlChange}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<hr />
|
|
||||||
<DashboardTable search={searchQuery} />
|
|
||||||
</Panel.Body>
|
|
||||||
</Panel>
|
|
||||||
</Tab>
|
|
||||||
<Tab eventKey="recent" title={t('Recently Viewed')}>
|
|
||||||
<Panel>
|
|
||||||
<Panel.Body>
|
|
||||||
<Row>
|
|
||||||
<Col md={8}>
|
|
||||||
<h2>{t('Recently Viewed')}</h2>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<hr />
|
|
||||||
<RecentActivity user={user} />
|
|
||||||
</Panel.Body>
|
|
||||||
</Panel>
|
|
||||||
</Tab>
|
|
||||||
<Tab eventKey="favorites" title={t('Favorites')}>
|
|
||||||
<Panel>
|
|
||||||
<Panel.Body>
|
|
||||||
<Row>
|
|
||||||
<Col md={8}>
|
|
||||||
<h2>{t('Favorites')}</h2>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<hr />
|
|
||||||
<Favorites user={user} />
|
|
||||||
</Panel.Body>
|
|
||||||
</Panel>
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||||||
]
|
]
|
||||||
search_columns = [
|
search_columns = [
|
||||||
"created_by",
|
"created_by",
|
||||||
|
"changed_by",
|
||||||
"datasource_id",
|
"datasource_id",
|
||||||
"datasource_name",
|
"datasource_name",
|
||||||
"datasource_type",
|
"datasource_type",
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||||||
"owners",
|
"owners",
|
||||||
"published",
|
"published",
|
||||||
"slug",
|
"slug",
|
||||||
|
"changed_by",
|
||||||
)
|
)
|
||||||
search_filters = {
|
search_filters = {
|
||||||
"dashboard_title": [DashboardTitleOrSlugFilter],
|
"dashboard_title": [DashboardTitleOrSlugFilter],
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ class QueryRestApi(BaseSupersetModelRestApi):
|
|||||||
"status",
|
"status",
|
||||||
"start_time",
|
"start_time",
|
||||||
"end_time",
|
"end_time",
|
||||||
|
"rows",
|
||||||
|
"tmp_table_name",
|
||||||
|
"tracking_url",
|
||||||
]
|
]
|
||||||
show_columns = [
|
show_columns = [
|
||||||
"client_id",
|
"client_id",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
|
|||||||
"last_run_delta_humanized",
|
"last_run_delta_humanized",
|
||||||
]
|
]
|
||||||
|
|
||||||
search_columns = ["id", "database", "label", "schema"]
|
search_columns = ["id", "database", "label", "schema", "created_by"]
|
||||||
search_filters = {
|
search_filters = {
|
||||||
"id": [SavedQueryFavoriteFilter],
|
"id": [SavedQueryFavoriteFilter],
|
||||||
"label": [SavedQueryAllTextFilter],
|
"label": [SavedQueryAllTextFilter],
|
||||||
|
|||||||
Reference in New Issue
Block a user