fix(home): missing key and invalid dates in Recents cards (#13291)
diff --git a/superset-frontend/src/types/Chart.ts b/superset-frontend/src/types/Chart.ts
index 5148d32..cf78dab 100644
--- a/superset-frontend/src/types/Chart.ts
+++ b/superset-frontend/src/types/Chart.ts
@@ -23,17 +23,18 @@
import Owner from './Owner';
-export default interface Chart {
+export interface Chart {
id: number;
url: string;
viz_type: string;
slice_name: string;
creator: string;
changed_on: string;
+ changed_on_delta_humanized?: string;
+ changed_on_utc?: string;
description: string | null;
cache_timeout: number | null;
thumbnail_url?: string;
- changed_on_delta_humanized?: string;
owners?: Owner[];
datasource_name_text?: string;
}
@@ -44,4 +45,7 @@
slice_name: string;
description: string | null;
cache_timeout: number | null;
+ url?: string;
};
+
+export default Chart;
diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts
index 4a9a2c5..8e54bbd 100644
--- a/superset-frontend/src/views/CRUD/types.ts
+++ b/superset-frontend/src/views/CRUD/types.ts
@@ -34,7 +34,8 @@
export interface Dashboard {
changed_by_name: string;
changed_by_url: string;
- changed_on_delta_humanized: string;
+ changed_on_delta_humanized?: string;
+ changed_on_utc?: string;
changed_by: string;
dashboard_title: string;
slice_name?: string;
@@ -47,13 +48,15 @@
}
export type SavedQueryObject = {
+ id: number;
+ changed_on: string;
+ changed_on_delta_humanized: string;
database: {
database_name: string;
id: number;
};
db_id: number;
description?: string;
- id: number;
label: string;
schema: string;
sql: string | null;
diff --git a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
index 5e5cc13..b52dfe2 100644
--- a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
@@ -23,25 +23,43 @@
import Loading from 'src/components/Loading';
import ListViewCard from 'src/components/ListViewCard';
import SubMenu from 'src/components/Menu/SubMenu';
+import { Chart } from 'src/types/Chart';
+import { Dashboard, SavedQueryObject } from 'src/views/CRUD/types';
+import { mq, CardStyles } from 'src/views/CRUD/utils';
+
import { ActivityData } from './Welcome';
-import { mq, CardStyles } 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;
+/**
+ * Return result from /superset/recent_activity/{user_id}
+ */
+interface RecentActivity {
+ action: string;
+ item_type: 'slice' | 'dashboard';
item_url: string;
+ item_title: string;
+ time: number;
+ time_delta_humanized?: string;
}
+interface RecentSlice extends RecentActivity {
+ item_type: 'slice';
+}
+
+interface RecentDashboard extends RecentActivity {
+ item_type: 'dashboard';
+}
+
+/**
+ * Recent activity objects fetched by `getRecentAcitivtyObjs`.
+ */
+type ActivityObject =
+ | RecentSlice
+ | RecentDashboard
+ | Chart
+ | Dashboard
+ | SavedQueryObject;
+
interface ActivityProps {
user: {
userId: string | number;
@@ -79,31 +97,70 @@
}
`;
+const UNTITLED = t('[Untitled]');
+const UNKNOWN_TIME = t('Unknown');
+
+const getEntityTitle = (entity: ActivityObject) => {
+ if ('dashboard_title' in entity) return entity.dashboard_title || UNTITLED;
+ if ('slice_name' in entity) return entity.slice_name || UNTITLED;
+ if ('label' in entity) return entity.label || UNTITLED;
+ return entity.item_title || UNTITLED;
+};
+
+const getEntityIconName = (entity: ActivityObject) => {
+ if ('sql' in entity) return 'sql';
+ const url = 'item_url' in entity ? entity.item_url : entity.url;
+ if (url?.includes('dashboard')) {
+ return 'nav-dashboard';
+ }
+ if (url?.includes('explore')) {
+ return 'nav-charts';
+ }
+ return '';
+};
+
+const getEntityUrl = (entity: ActivityObject) => {
+ if ('sql' in entity) return `/superset/sqllab?savedQueryId=${entity.id}`;
+ if ('url' in entity) return entity.url;
+ return entity.item_url;
+};
+
+const getEntityLastActionOn = (entity: ActivityObject) => {
+ // translation keys for last action on
+ const LAST_VIEWED = `Last viewed %s`;
+ const LAST_MODIFIED = `Last modified %s`;
+
+ // for Recent viewed items
+ if ('time_delta_humanized' in entity) {
+ return t(LAST_VIEWED, entity.time_delta_humanized);
+ }
+
+ if ('changed_on_delta_humanized' in entity) {
+ return t(LAST_MODIFIED, entity.changed_on_delta_humanized);
+ }
+
+ let time: number | string | undefined | null;
+ let translationKey = LAST_MODIFIED;
+ if ('time' in entity) {
+ // eslint-disable-next-line prefer-destructuring
+ time = entity.time;
+ translationKey = LAST_VIEWED;
+ }
+ if ('changed_on' in entity) time = entity.changed_on;
+ if ('changed_on_utc' in entity) time = entity.changed_on_utc;
+
+ return t(
+ translationKey,
+ time == null ? UNKNOWN_TIME : moment(time).fromNow(),
+ );
+};
+
export default function ActivityTable({
loading,
activeChild,
setActiveChild,
activityData,
}: ActivityProps) {
- 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') || e.item_url?.includes('dashboard')) {
- return 'nav-dashboard';
- }
- if (e.url?.includes('explore') || e.item_url?.includes('explore')) {
- return 'nav-charts';
- }
- return '';
- };
-
const tabs = [
{
name: 'Edited',
@@ -139,35 +196,30 @@
});
}
- const renderActivity = () => {
- const getRecentRef = (e: ActivityObjects) => {
- if (activeChild === 'Viewed') {
- return e.item_url;
- }
- return e.sql ? `/superset/sqllab?savedQueryId=${e.id}` : e.url;
- };
- return activityData[activeChild].map((e: ActivityObjects) => (
- <CardStyles
- onClick={() => {
- window.location.href = getRecentRef(e);
- }}
- key={e.id}
- >
- <ListViewCard
- loading={loading}
- cover={<></>}
- url={e.sql ? `/superset/sqllab?savedQueryId=${e.id}` : e.url}
- title={getFilterTitle(e)}
- description={`Last Edited: ${moment(
- e.changed_on_utc,
- 'MM/DD/YYYY HH:mm:ss',
- )}`}
- avatar={getIconName(e)}
- actions={null}
- />
- </CardStyles>
- ));
- };
+ const renderActivity = () =>
+ activityData[activeChild].map((entity: ActivityObject) => {
+ const url = getEntityUrl(entity);
+ const lastActionOn = getEntityLastActionOn(entity);
+ return (
+ <CardStyles
+ onClick={() => {
+ window.location.href = url;
+ }}
+ key={url}
+ >
+ <ListViewCard
+ loading={loading}
+ cover={<></>}
+ url={url}
+ title={getEntityTitle(entity)}
+ description={lastActionOn}
+ avatar={getEntityIconName(entity)}
+ actions={null}
+ />
+ </CardStyles>
+ );
+ });
+
if (loading) return <Loading position="inline" />;
return (
<>
diff --git a/superset/views/core.py b/superset/views/core.py
index 8cc0b22..62b1b49 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -23,6 +23,7 @@
from urllib import parse
import backoff
+import humanize
import pandas as pd
import simplejson as json
from flask import abort, flash, g, Markup, redirect, render_template, request, Response
@@ -1395,6 +1396,9 @@
"item_url": item_url,
"item_title": item_title,
"time": log.dttm,
+ "time_delta_humanized": humanize.naturaltime(
+ datetime.now() - log.dttm
+ ),
}
)
return json_success(json.dumps(payload, default=utils.json_int_dttm_ser))