setEditModal(undefined)}
+ onSubmit={handleDashboardEdit}
+ />
+ )}
+ {dashboards.length > 0 ? (
+
+ {dashboards.map(e => (
+ setEditModal(dashboard),
+ }}
+ />
+ ))}
+
+ ) : (
+
+ )}
+ >
+ );
}
export default withToasts(DashboardTable);
diff --git a/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx b/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx
new file mode 100644
index 000000000000..f145dfe79ca0
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx
@@ -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 = (
+ {`No ${
+ tableName === 'SAVED_QUERIES'
+ ? t('saved queries')
+ : t(`${tableName.toLowerCase()}`)
+ } yet`}
+ );
+ const recent = (
+
+ {(() => {
+ 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;
+ })()}
+
+ );
+ // Mine and Recent Activity(all tabs) tab empty state
+ if (tab === 'Mine' || tableName === 'RECENTS') {
+ return (
+
+ {tableName !== 'RECENTS' && (
+
+
+
+ )}
+
+ );
+ }
+ // Favorite tab empty state
+ return (
+
+ {t("You don't have any favorites yet!")}
+
+ }
+ >
+
+
+ );
+}
diff --git a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx
new file mode 100644
index 000000000000..dc0590bacbda
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx
@@ -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;
+ 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('saved_query', t('query'), addDangerToast);
+ const [queryFilter, setQueryFilter] = useState('Mine');
+ const [queryDeleteModal, setQueryDeleteModal] = useState(false);
+ const [currentlyEdited, setCurrentlyEdited] = useState({});
+
+ 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) => (
+
+ );
+ return (
+ <>
+ {queryDeleteModal && (
+ {
+ if (queryDeleteModal) {
+ handleQueryDelete(currentlyEdited);
+ }
+ }}
+ onHide={() => {
+ setQueryDeleteModal(false);
+ }}
+ open
+ title={t('Delete Query?')}
+ />
+ )}
+ setQueryFilter('Favorite'),
+ },
+ {
+ name: 'Mine',
+ label: t('Mine'),
+ onClick: () => setQueryFilter('Mine'),
+ },
+ ]}
+ buttons={[
+ {
+ name: (
+
+ SQL Query{' '}
+
+ ),
+ buttonStyle: 'tertiary',
+ onClick: () => {
+ window.location.href = '/superset/sqllab';
+ },
+ },
+ {
+ name: 'View All »',
+ buttonStyle: 'link',
+ onClick: () => {
+ window.location.href = '/savedqueryview/list';
+ },
+ },
+ ]}
+ />
+ {queries.length > 0 ? (
+
+ {queries.map(q => (
+
+
+
{t('Tables')}
+
{q?.sql_tables?.length}
+
+
+
{t('Datasource Name')}
+
{q?.sql_tables && q.sql_tables[0]?.table}
+
+
+ }
+ actions={
+
+
+
+
+
+ }
+ />
+ ))}
+
+ ) : (
+
+ )}
+ >
+ );
+};
+
+export default withToasts(SavedQueries);
diff --git a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
index 256fe3a9e4fa..f35effcbf975 100644
--- a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
@@ -16,128 +16,79 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useCallback, useState } from 'react';
-import {
- Panel,
- Row,
- Col,
- Tabs,
- Tab,
- FormControl,
- FormControlProps,
-} from 'react-bootstrap';
-import { t } from '@superset-ui/core';
-import { useQueryParam, StringParam, QueryParamConfig } from 'use-query-params';
+import React from 'react';
+import { styled, t } from '@superset-ui/core';
+import { Collapse } from 'src/common/components';
import { User } from 'src/types/bootstrapTypes';
-import RecentActivity from 'src/profile/components/RecentActivity';
-import Favorites from 'src/profile/components/Favorites';
+import { mq } from '../utils';
+import ActivityTable from './ActivityTable';
+import ChartTable from './ChartTable';
+import SavedQueries from './SavedQueries';
import DashboardTable from './DashboardTable';
+const { Panel } = Collapse;
+
interface WelcomeProps {
user: User;
}
-function useSyncQueryState(
- queryParam: string,
- queryParamType: QueryParamConfig<
- string | null | undefined,
- string | undefined
- >,
- defaultState: string,
-): [string, (val: string) => void] {
- const [queryState, setQueryState] = useQueryParam(queryParam, queryParamType);
- const [state, setState] = useState(queryState || defaultState);
-
- const setQueryStateAndState = (val: string) => {
- setQueryState(val);
- setState(val);
- };
-
- return [state, setQueryStateAndState];
-}
+const WelcomeContainer = styled.div`
+ background-color: ${({ theme }) => theme.colors.grayscale.light4};
+ nav {
+ margin-top: -15px;
+ background-color: ${({ theme }) => theme.colors.grayscale.light4};
+ &:after {
+ content: '';
+ display: block;
+ border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
+ margin: 0px ${({ theme }) => theme.gridUnit * 6}px;
+ position: relative;
+ ${[mq[1]]} {
+ margin-top: 5px;
+ margin: 0px 2px;
+ }
+ }
+ .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) {
- const [activeTab, setActiveTab] = useSyncQueryState(
- 'activeTab',
- StringParam,
- 'all',
- );
-
- const [searchQuery, setSearchQuery] = useSyncQueryState(
- 'search',
- StringParam,
- '',
- );
-
- const onFormControlChange = useCallback(
- (e: React.FormEvent) => {
- const { value } = e.currentTarget;
- setSearchQuery((value as string) ?? '');
- },
- [],
- );
-
- const onTabsSelect = useCallback((e: any) => {
- setActiveTab(e as string);
- }, []);
-
return (
-
-
-
-
-
-
-
- {t('Dashboards')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {t('Recently Viewed')}
-
-
-
-
-
-
-
-
-
-
-
-
- {t('Favorites')}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/superset/charts/api.py b/superset/charts/api.py
index 3ef27d097ee6..6ebe3cb26b36 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -144,6 +144,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
]
search_columns = [
"created_by",
+ "changed_by",
"datasource_id",
"datasource_name",
"datasource_type",
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index 8f65f35fdb8b..cba494a4567c 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -157,6 +157,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"owners",
"published",
"slug",
+ "changed_by",
)
search_filters = {
"dashboard_title": [DashboardTitleOrSlugFilter],
diff --git a/superset/queries/api.py b/superset/queries/api.py
index 0f368d396094..ce4fca56b76a 100644
--- a/superset/queries/api.py
+++ b/superset/queries/api.py
@@ -42,6 +42,9 @@ class QueryRestApi(BaseSupersetModelRestApi):
"status",
"start_time",
"end_time",
+ "rows",
+ "tmp_table_name",
+ "tracking_url",
]
show_columns = [
"client_id",
diff --git a/superset/queries/saved_queries/api.py b/superset/queries/saved_queries/api.py
index 794ab257259a..b37fc7dd61e1 100644
--- a/superset/queries/saved_queries/api.py
+++ b/superset/queries/saved_queries/api.py
@@ -106,7 +106,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
"last_run_delta_humanized",
]
- search_columns = ["id", "database", "label", "schema"]
+ search_columns = ["id", "database", "label", "schema", "created_by"]
search_filters = {
"id": [SavedQueryFavoriteFilter],
"label": [SavedQueryAllTextFilter],