diff --git a/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx b/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx index a385fafede..695b74eb9d 100644 --- a/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx +++ b/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx @@ -105,7 +105,7 @@ describe('ChartList', () => { const callsD = fetchMock.calls(/chart\/\?q/); expect(callsD).toHaveLength(1); expect(callsD[0][0]).toMatchInlineSnapshot( - `"http://localhost/api/v1/chart/?q=(order_column:changed_on,order_direction:desc,page:0,page_size:25)"`, + `"http://localhost/api/v1/chart/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`, ); }); }); diff --git a/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx b/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx index ca35c5dd48..5de6ba3426 100644 --- a/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx +++ b/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx @@ -43,7 +43,8 @@ const mockDashboards = [...new Array(3)].map((_, i) => ({ changed_by_url: 'changed_by_url', changed_by_fk: 1, published: true, - changed_on: new Date().toISOString(), + changed_on_utc: new Date().toISOString(), + changed_on_delta_humanized: '5 minutes ago', owners: [{ first_name: 'admin', last_name: 'admin_user' }], })); @@ -95,7 +96,7 @@ describe('DashboardList', () => { const callsD = fetchMock.calls(/dashboard\/\?q/); expect(callsD).toHaveLength(1); expect(callsD[0][0]).toMatchInlineSnapshot( - `"http://localhost/api/v1/dashboard/?q=(order_column:changed_on,order_direction:desc,page:0,page_size:25)"`, + `"http://localhost/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`, ); }); it('edits', () => { diff --git a/superset-frontend/src/views/chartList/ChartList.tsx b/superset-frontend/src/views/chartList/ChartList.tsx index 57cfd829c9..fc5f99271e 100644 --- a/superset-frontend/src/views/chartList/ChartList.tsx +++ b/superset-frontend/src/views/chartList/ChartList.tsx @@ -19,7 +19,6 @@ import { SupersetClient } from '@superset-ui/connection'; import { t } from '@superset-ui/translation'; import { getChartMetadataRegistry } from '@superset-ui/chart'; -import moment from 'moment'; import PropTypes from 'prop-types'; import React from 'react'; import rison from 'rison'; @@ -108,7 +107,7 @@ class ChartList extends React.PureComponent { return isFeatureEnabled(FeatureFlag.LIST_VIEWS_SIP34_FILTER_UI); } - initialSort = [{ id: 'changed_on', desc: true }]; + initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; columns = [ { @@ -153,11 +152,11 @@ class ChartList extends React.PureComponent { { Cell: ({ row: { - original: { changed_on: changedOn }, + original: { changed_on_delta_humanized: changedOn }, }, - }: any) => {moment(changedOn).fromNow()}, + }: any) => {changedOn}, Header: t('Last Modified'), - accessor: 'changed_on', + accessor: 'changed_on_delta_humanized', }, { accessor: 'description', diff --git a/superset-frontend/src/views/dashboardList/DashboardList.tsx b/superset-frontend/src/views/dashboardList/DashboardList.tsx index 438c964ad2..95de66a422 100644 --- a/superset-frontend/src/views/dashboardList/DashboardList.tsx +++ b/superset-frontend/src/views/dashboardList/DashboardList.tsx @@ -18,7 +18,6 @@ */ import { SupersetClient } from '@superset-ui/connection'; import { t } from '@superset-ui/translation'; -import moment from 'moment'; import PropTypes from 'prop-types'; import React from 'react'; import rison from 'rison'; @@ -60,7 +59,7 @@ interface Dashboard { changed_by: string; changed_by_name: string; changed_by_url: string; - changed_on: string; + changed_on_delta_humanized: string; dashboard_title: string; published: boolean; url: string; @@ -123,7 +122,7 @@ class DashboardList extends React.PureComponent { return isFeatureEnabled(FeatureFlag.LIST_VIEWS_SIP34_FILTER_UI); } - initialSort = [{ id: 'changed_on', desc: true }]; + initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; columns = [ { @@ -181,11 +180,11 @@ class DashboardList extends React.PureComponent { { Cell: ({ row: { - original: { changed_on: changedOn }, + original: { changed_on_delta_humanized: changedOn }, }, - }: any) => {moment(changedOn).fromNow()}, + }: any) => {changedOn}, Header: t('Modified'), - accessor: 'changed_on', + accessor: 'changed_on_delta_humanized', }, { accessor: 'slug', diff --git a/superset-frontend/src/welcome/DashboardTable.tsx b/superset-frontend/src/welcome/DashboardTable.tsx index 704cdc3020..430548a3aa 100644 --- a/superset-frontend/src/welcome/DashboardTable.tsx +++ b/superset-frontend/src/welcome/DashboardTable.tsx @@ -19,7 +19,6 @@ import React from 'react'; import { t } from '@superset-ui/translation'; import { SupersetClient } from '@superset-ui/connection'; -import moment from 'moment'; import { debounce } from 'lodash'; import ListView from 'src/components/ListView/ListView'; import withToasts from 'src/messageToasts/enhancers/withToasts'; @@ -94,23 +93,23 @@ class DashboardTable extends React.PureComponent< }) => {changedByName}, }, { - accessor: 'changed_on', + accessor: 'changed_on_delta_humanized', Header: 'Modified', Cell: ({ row: { - original: { changed_on: changedOn }, + original: { changed_on_delta_humanized: changedOn }, }, }: { row: { original: { - changed_on: string; + changed_on_delta_humanized: string; }; }; - }) => {moment(changedOn).fromNow()}, + }) => {changedOn}, }, ]; - initialSort = [{ id: 'changed_on', desc: true }]; + initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; fetchData = ({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => { this.setState({ loading: true }); diff --git a/superset/charts/api.py b/superset/charts/api.py index 52c2a1c1a0..9f5f92e3b5 100644 --- a/superset/charts/api.py +++ b/superset/charts/api.py @@ -101,7 +101,6 @@ class ChartRestApi(BaseSupersetModelRestApi): "cache_timeout", ] show_select_columns = show_columns + ["table.id"] - list_columns = [ "id", "slice_name", @@ -113,7 +112,8 @@ class ChartRestApi(BaseSupersetModelRestApi): "changed_by_url", "changed_by.first_name", "changed_by.last_name", - "changed_on", + "changed_on_utc", + "changed_on_delta_humanized", "datasource_id", "datasource_type", "datasource_name_text", @@ -124,13 +124,13 @@ class ChartRestApi(BaseSupersetModelRestApi): "params", "cache_timeout", ] - + list_select_columns = list_columns + ["changed_on"] order_columns = [ "slice_name", "viz_type", "datasource_name", "changed_by_fk", - "changed_on", + "changed_on_delta_humanized", ] search_columns = ( "slice_name", diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index cba1fa74e8..a62f67ed75 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -96,7 +96,6 @@ class DashboardRestApi(BaseSupersetModelRestApi): "table_names", "thumbnail_url", ] - order_columns = ["dashboard_title", "changed_on", "published", "changed_by_fk"] list_columns = [ "id", "published", @@ -112,13 +111,22 @@ class DashboardRestApi(BaseSupersetModelRestApi): "changed_by.id", "changed_by_name", "changed_by_url", - "changed_on", + "changed_on_utc", + "changed_on_delta_humanized", "dashboard_title", "owners.id", "owners.username", "owners.first_name", "owners.last_name", ] + list_select_columns = list_columns + ["changed_on"] + order_columns = [ + "dashboard_title", + "changed_on_delta_humanized", + "published", + "changed_by_fk", + ] + add_columns = [ "dashboard_title", "slug", diff --git a/superset/models/helpers.py b/superset/models/helpers.py index d003f55e47..969669c916 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -25,6 +25,7 @@ from typing import Any, Dict, List, Optional, Set, Union # pylint: disable=ungrouped-imports import humanize import pandas as pd +import pytz import sqlalchemy as sa import yaml from flask import escape, g, Markup @@ -381,6 +382,15 @@ class AuditMixinNullable(AuditMixin): def changed_on_(self) -> Markup: return Markup(f'{self.changed_on}') + @renders("changed_on") + def changed_on_delta_humanized(self) -> str: + return self.changed_on_humanized + + @renders("changed_on") + def changed_on_utc(self) -> str: + # Convert naive datetime to UTC + return self.changed_on.astimezone(pytz.utc).strftime("%Y-%m-%dT%H:%M:%S.%f%z") + @property def changed_on_humanized(self) -> str: return humanize.naturaltime(datetime.now() - self.changed_on) diff --git a/tests/charts/api_tests.py b/tests/charts/api_tests.py index 909b8b13eb..c115b1d9c7 100644 --- a/tests/charts/api_tests.py +++ b/tests/charts/api_tests.py @@ -18,9 +18,11 @@ """Unit tests for Superset""" import json from typing import List, Optional +from datetime import datetime from unittest import mock import prison +import humanize from sqlalchemy.sql import func from tests.test_app import app @@ -543,6 +545,34 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin): data = json.loads(rv.data.decode("utf-8")) self.assertEqual(data["count"], 33) + def test_get_charts_changed_on(self): + """ + Dashboard API: Test get charts changed on + """ + admin = self.get_user("admin") + start_changed_on = datetime.now() + chart = self.insert_chart("foo_a", [admin.id], 1, description="ZY_bar") + + self.login(username="admin") + + arguments = { + "order_column": "changed_on_delta_humanized", + "order_direction": "desc", + } + uri = f"api/v1/chart/?q={prison.dumps(arguments)}" + + rv = self.get_assert_metric(uri, "get_list") + self.assertEqual(rv.status_code, 200) + data = json.loads(rv.data.decode("utf-8")) + self.assertEqual( + data["result"][0]["changed_on_delta_humanized"], + humanize.naturaltime(datetime.now() - start_changed_on), + ) + + # rollback changes + db.session.delete(chart) + db.session.commit() + def test_get_charts_filter(self): """ Chart API: Test get charts filter diff --git a/tests/dashboards/api_tests.py b/tests/dashboards/api_tests.py index 5637e06d64..6645e802fc 100644 --- a/tests/dashboards/api_tests.py +++ b/tests/dashboards/api_tests.py @@ -18,8 +18,10 @@ """Unit tests for Superset""" import json from typing import List, Optional +from datetime import datetime import prison +import humanize from sqlalchemy.sql import func import tests.test_app @@ -111,7 +113,7 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin): data = json.loads(rv.data.decode("utf-8")) self.assertIn("changed_on", data["result"]) for key, value in data["result"].items(): - # We can't assert timestamp + # We can't assert timestamp values if key != "changed_on": self.assertEqual(value, expected_result[key]) # rollback changes @@ -152,6 +154,37 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin): db.session.delete(dashboard) db.session.commit() + def test_get_dashboards_changed_on(self): + """ + Dashboard API: Test get dashboards changed on + """ + from datetime import datetime + import humanize + + admin = self.get_user("admin") + start_changed_on = datetime.now() + dashboard = self.insert_dashboard("title", "slug1", [admin.id]) + + self.login(username="admin") + + arguments = { + "order_column": "changed_on_delta_humanized", + "order_direction": "desc", + } + uri = f"api/v1/dashboard/?q={prison.dumps(arguments)}" + + rv = self.get_assert_metric(uri, "get_list") + self.assertEqual(rv.status_code, 200) + data = json.loads(rv.data.decode("utf-8")) + self.assertEqual( + data["result"][0]["changed_on_delta_humanized"], + humanize.naturaltime(datetime.now() - start_changed_on), + ) + + # rollback changes + db.session.delete(dashboard) + db.session.commit() + def test_get_dashboards_filter(self): """ Dashboard API: Test get dashboards filter @@ -214,9 +247,9 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin): self.assertEqual(data["count"], 3) expected_response = [ - {"slug": "ZY_bar", "dashboard_title": "foo_a",}, - {"slug": "slug1zy_", "dashboard_title": "foo_b",}, - {"slug": "slug1", "dashboard_title": "zy_foo",}, + {"slug": "ZY_bar", "dashboard_title": "foo_a"}, + {"slug": "slug1zy_", "dashboard_title": "foo_b"}, + {"slug": "slug1", "dashboard_title": "zy_foo"}, ] for index, item in enumerate(data["result"]): self.assertEqual(item["slug"], expected_response[index]["slug"])