diff --git a/superset-frontend/cypress-base/cypress/e2e/alerts_and_reports/alerts.test.ts b/superset-frontend/cypress-base/cypress/e2e/alerts_and_reports/alerts.test.ts
index a695541ceecd9..b677507a4602f 100644
--- a/superset-frontend/cypress-base/cypress/e2e/alerts_and_reports/alerts.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/alerts_and_reports/alerts.test.ts
@@ -29,10 +29,9 @@ describe('Alert list view', () => {
cy.getBySel('sort-header').eq(2).contains('Name');
cy.getBySel('sort-header').eq(3).contains('Schedule');
cy.getBySel('sort-header').eq(4).contains('Notification method');
- cy.getBySel('sort-header').eq(5).contains('Created by');
- cy.getBySel('sort-header').eq(6).contains('Owners');
- cy.getBySel('sort-header').eq(7).contains('Modified');
- cy.getBySel('sort-header').eq(8).contains('Active');
+ cy.getBySel('sort-header').eq(5).contains('Owners');
+ cy.getBySel('sort-header').eq(6).contains('Last modified');
+ cy.getBySel('sort-header').eq(7).contains('Active');
// TODO Cypress won't recognize the Actions column
// cy.getBySel('sort-header').eq(9).contains('Actions');
});
diff --git a/superset-frontend/cypress-base/cypress/e2e/alerts_and_reports/reports.test.ts b/superset-frontend/cypress-base/cypress/e2e/alerts_and_reports/reports.test.ts
index e267d76f6f7ed..a227fa03d7da7 100644
--- a/superset-frontend/cypress-base/cypress/e2e/alerts_and_reports/reports.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/alerts_and_reports/reports.test.ts
@@ -29,10 +29,9 @@ describe('Report list view', () => {
cy.getBySel('sort-header').eq(2).contains('Name');
cy.getBySel('sort-header').eq(3).contains('Schedule');
cy.getBySel('sort-header').eq(4).contains('Notification method');
- cy.getBySel('sort-header').eq(5).contains('Created by');
- cy.getBySel('sort-header').eq(6).contains('Owners');
- cy.getBySel('sort-header').eq(7).contains('Modified');
- cy.getBySel('sort-header').eq(8).contains('Active');
+ cy.getBySel('sort-header').eq(5).contains('Owners');
+ cy.getBySel('sort-header').eq(6).contains('Last modified');
+ cy.getBySel('sort-header').eq(7).contains('Active');
// TODO Cypress won't recognize the Actions column
// cy.getBySel('sort-header').eq(9).contains('Actions');
});
diff --git a/superset-frontend/cypress-base/cypress/e2e/chart_list/filter.test.ts b/superset-frontend/cypress-base/cypress/e2e/chart_list/filter.test.ts
index acd11669bea18..00b09e2fb8d0f 100644
--- a/superset-frontend/cypress-base/cypress/e2e/chart_list/filter.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/chart_list/filter.test.ts
@@ -35,14 +35,14 @@ describe('Charts filters', () => {
setFilter('Owner', 'admin user');
});
- it('should allow filtering by "Created by" correctly', () => {
- setFilter('Created by', 'alpha user');
- setFilter('Created by', 'admin user');
+ it('should allow filtering by "Modified by" correctly', () => {
+ setFilter('Modified by', 'alpha user');
+ setFilter('Modified by', 'admin user');
});
- it('should allow filtering by "Chart type" correctly', () => {
- setFilter('Chart type', 'Area Chart (legacy)');
- setFilter('Chart type', 'Bubble Chart');
+ it('should allow filtering by "Type" correctly', () => {
+ setFilter('Type', 'Area Chart (legacy)');
+ setFilter('Type', 'Bubble Chart');
});
it('should allow filtering by "Dataset" correctly', () => {
@@ -51,7 +51,7 @@ describe('Charts filters', () => {
});
it('should allow filtering by "Dashboards" correctly', () => {
- setFilter('Dashboards', 'Unicode Test');
- setFilter('Dashboards', 'Tabbed Dashboard');
+ setFilter('Dashboard', 'Unicode Test');
+ setFilter('Dashboard', 'Tabbed Dashboard');
});
});
diff --git a/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts b/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts
index 6664281abe9b9..44f348edc50f5 100644
--- a/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts
@@ -109,14 +109,12 @@ describe('Charts list', () => {
it('should load rows in list mode', () => {
cy.getBySel('listview-table').should('be.visible');
- cy.getBySel('sort-header').eq(1).contains('Chart');
- cy.getBySel('sort-header').eq(2).contains('Visualization type');
+ cy.getBySel('sort-header').eq(1).contains('Name');
+ cy.getBySel('sort-header').eq(2).contains('Type');
cy.getBySel('sort-header').eq(3).contains('Dataset');
- // cy.getBySel('sort-header').eq(4).contains('Dashboards added to');
- cy.getBySel('sort-header').eq(4).contains('Modified by');
+ cy.getBySel('sort-header').eq(4).contains('Owners');
cy.getBySel('sort-header').eq(5).contains('Last modified');
- cy.getBySel('sort-header').eq(6).contains('Created by');
- cy.getBySel('sort-header').eq(7).contains('Actions');
+ cy.getBySel('sort-header').eq(6).contains('Actions');
});
it('should sort correctly in list mode', () => {
diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard_list/filter.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard_list/filter.test.ts
index 4654b3b5c2634..854ea541c74e3 100644
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard_list/filter.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/dashboard_list/filter.test.ts
@@ -35,9 +35,9 @@ describe('Dashboards filters', () => {
setFilter('Owner', 'admin user');
});
- it('should allow filtering by "Created by" correctly', () => {
- setFilter('Created by', 'alpha user');
- setFilter('Created by', 'admin user');
+ it('should allow filtering by "Modified by" correctly', () => {
+ setFilter('Modified by', 'alpha user');
+ setFilter('Modified by', 'admin user');
});
it('should allow filtering by "Status" correctly', () => {
diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard_list/list.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard_list/list.test.ts
index 9bc6eed224578..7dfb7cd673d7f 100644
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard_list/list.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/dashboard_list/list.test.ts
@@ -54,13 +54,11 @@ describe('Dashboards list', () => {
it('should load rows in list mode', () => {
cy.getBySel('listview-table').should('be.visible');
- cy.getBySel('sort-header').eq(1).contains('Title');
- cy.getBySel('sort-header').eq(2).contains('Modified by');
- cy.getBySel('sort-header').eq(3).contains('Status');
- cy.getBySel('sort-header').eq(4).contains('Modified');
- cy.getBySel('sort-header').eq(5).contains('Created by');
- cy.getBySel('sort-header').eq(6).contains('Owners');
- cy.getBySel('sort-header').eq(7).contains('Actions');
+ cy.getBySel('sort-header').eq(1).contains('Name');
+ cy.getBySel('sort-header').eq(2).contains('Status');
+ cy.getBySel('sort-header').eq(3).contains('Owners');
+ cy.getBySel('sort-header').eq(4).contains('Last modified');
+ cy.getBySel('sort-header').eq(5).contains('Actions');
});
it('should sort correctly in list mode', () => {
diff --git a/superset-frontend/src/components/AuditInfo/ModifiedInfo.test.tsx b/superset-frontend/src/components/AuditInfo/ModifiedInfo.test.tsx
new file mode 100644
index 0000000000000..af9d6913d80d9
--- /dev/null
+++ b/superset-frontend/src/components/AuditInfo/ModifiedInfo.test.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import '@testing-library/jest-dom';
+import userEvent from '@testing-library/user-event';
+
+import { ModifiedInfo } from '.';
+
+const TEST_DATE = '2023-11-20';
+const USER = {
+ id: 1,
+ first_name: 'Foo',
+ last_name: 'Bar',
+};
+
+test('should render a tooltip when user is provided', async () => {
+ render();
+
+ const dateElement = screen.getByTestId('audit-info-date');
+ expect(dateElement).toBeInTheDocument();
+ expect(screen.getByText(TEST_DATE)).toBeInTheDocument();
+ expect(screen.queryByText('Modified by: Foo Bar')).not.toBeInTheDocument();
+ userEvent.hover(dateElement);
+ const tooltip = await screen.findByRole('tooltip');
+ expect(tooltip).toBeInTheDocument();
+ expect(screen.getByText('Modified by: Foo Bar')).toBeInTheDocument();
+});
+
+test('should render only the date if username is not provided', async () => {
+ render();
+
+ const dateElement = screen.getByTestId('audit-info-date');
+ expect(dateElement).toBeInTheDocument();
+ expect(screen.getByText(TEST_DATE)).toBeInTheDocument();
+ userEvent.hover(dateElement);
+ await waitFor(
+ () => {
+ const tooltip = screen.queryByRole('tooltip');
+ expect(tooltip).not.toBeInTheDocument();
+ },
+ { timeout: 1000 },
+ );
+});
diff --git a/superset-frontend/src/components/AuditInfo/index.tsx b/superset-frontend/src/components/AuditInfo/index.tsx
new file mode 100644
index 0000000000000..24223a1554a31
--- /dev/null
+++ b/superset-frontend/src/components/AuditInfo/index.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+
+import Owner from 'src/types/Owner';
+import { Tooltip } from 'src/components/Tooltip';
+import getOwnerName from 'src/utils/getOwnerName';
+import { t } from '@superset-ui/core';
+
+export type ModifiedInfoProps = {
+ user?: Owner;
+ date: string;
+};
+
+export const ModifiedInfo = ({ user, date }: ModifiedInfoProps) => {
+ const dateSpan = (
+
+ {date}
+
+ );
+
+ if (user) {
+ const userName = getOwnerName(user);
+ const title = t('Modified by: %s', userName);
+ return (
+
+ {dateSpan}
+
+ );
+ }
+ return dateSpan;
+};
diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
index 86b5c2277723f..751001297a92a 100644
--- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
@@ -1114,7 +1114,7 @@ class DatasourceEditor extends React.PureComponent {
-
+
= ({
- {t('Annotation name')}
+ {t('Name')}
*
= ({
const update_id = currentCssTemplate.id;
delete currentCssTemplate.id;
delete currentCssTemplate.created_by;
+ delete currentCssTemplate.changed_by;
+ delete currentCssTemplate.changed_on_delta_humanized;
+
updateResource(update_id, currentCssTemplate).then(response => {
if (!response) {
return;
@@ -235,7 +238,7 @@ const CssTemplateModal: FunctionComponent = ({
- {t('CSS template name')}
+ {t('Name')}
*
{
changed_on_delta_humanized: '',
created_on_delta_humanized: '',
created_by: {
+ id: 1,
first_name: 'joe',
last_name: 'smith',
},
changed_by: {
+ id: 2,
first_name: 'tom',
last_name: 'brown',
},
diff --git a/superset-frontend/src/pages/AlertReportList/index.tsx b/superset-frontend/src/pages/AlertReportList/index.tsx
index b0cd0a46226db..c6d14d186f100 100644
--- a/superset-frontend/src/pages/AlertReportList/index.tsx
+++ b/superset-frontend/src/pages/AlertReportList/index.tsx
@@ -53,6 +53,8 @@ import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
import Owner from 'src/types/Owner';
import AlertReportModal from 'src/features/alerts/AlertReportModal';
import { AlertObject, AlertState } from 'src/features/alerts/types';
+import { ModifiedInfo } from 'src/components/AuditInfo';
+import { QueryObjectColumns } from 'src/views/CRUD/types';
const extensionsRegistry = getExtensionsRegistry();
@@ -303,18 +305,6 @@ function AlertList({
disableSortBy: true,
size: 'xl',
},
- {
- Cell: ({
- row: {
- original: { created_by },
- },
- }: any) =>
- created_by ? `${created_by.first_name} ${created_by.last_name}` : '',
- Header: t('Created by'),
- id: 'created_by',
- disableSortBy: true,
- size: 'xl',
- },
{
Cell: ({
row: {
@@ -329,10 +319,13 @@ function AlertList({
{
Cell: ({
row: {
- original: { changed_on_delta_humanized: changedOn },
+ original: {
+ changed_on_delta_humanized: changedOn,
+ changed_by: changedBy,
+ },
},
- }: any) => {changedOn},
- Header: t('Modified'),
+ }: any) => ,
+ Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
@@ -407,6 +400,10 @@ function AlertList({
disableSortBy: true,
size: 'xl',
},
+ {
+ accessor: QueryObjectColumns.changed_by,
+ hidden: true,
+ },
],
[canDelete, canEdit, isReportEnabled, toggleActive],
);
@@ -448,6 +445,13 @@ function AlertList({
const filters: Filters = useMemo(
() => [
+ {
+ Header: t('Name'),
+ key: 'search',
+ id: 'name',
+ input: 'search',
+ operator: FilterOperator.contains,
+ },
{
Header: t('Owner'),
key: 'owner',
@@ -465,23 +469,6 @@ function AlertList({
),
paginate: true,
},
- {
- Header: t('Created by'),
- key: 'created_by',
- id: 'created_by',
- input: 'select',
- operator: FilterOperator.relationOneMany,
- unfilteredLabel: 'All',
- fetchSelects: createFetchRelated(
- 'report',
- 'created_by',
- createErrorHandler(errMsg =>
- t('An error occurred while fetching created by values: %s', errMsg),
- ),
- user,
- ),
- paginate: true,
- },
{
Header: t('Status'),
key: 'status',
@@ -504,11 +491,24 @@ function AlertList({
],
},
{
- Header: t('Search'),
- key: 'search',
- id: 'name',
- input: 'search',
- operator: FilterOperator.contains,
+ Header: t('Modified by'),
+ key: 'changed_by',
+ id: 'changed_by',
+ input: 'select',
+ operator: FilterOperator.relationOneMany,
+ unfilteredLabel: t('All'),
+ fetchSelects: createFetchRelated(
+ 'report',
+ 'changed_by',
+ createErrorHandler(errMsg =>
+ t(
+ 'An error occurred while fetching dataset datasource values: %s',
+ errMsg,
+ ),
+ ),
+ user,
+ ),
+ paginate: true,
},
],
[],
diff --git a/superset-frontend/src/pages/AllEntities/index.tsx b/superset-frontend/src/pages/AllEntities/index.tsx
index a1e2c52fe4934..b94cab846dfe4 100644
--- a/superset-frontend/src/pages/AllEntities/index.tsx
+++ b/superset-frontend/src/pages/AllEntities/index.tsx
@@ -35,6 +35,7 @@ import TagModal from 'src/features/tags/TagModal';
import withToasts, { useToasts } from 'src/components/MessageToasts/withToasts';
import { fetchObjectsByTagIds, fetchSingleTag } from 'src/features/tags/tags';
import Loading from 'src/components/Loading';
+import getOwnerName from 'src/utils/getOwnerName';
interface TaggedObject {
id: number;
@@ -132,7 +133,7 @@ function AllEntities() {
const owner: Owner = {
type: MetadataType.OWNER,
- createdBy: `${tag?.created_by.first_name} ${tag?.created_by.last_name}`,
+ createdBy: getOwnerName(tag?.created_by),
createdOn: tag?.created_on_delta_humanized || '',
};
items.push(owner);
@@ -140,7 +141,7 @@ function AllEntities() {
const lastModified: LastModified = {
type: MetadataType.LAST_MODIFIED,
value: tag?.changed_on_delta_humanized || '',
- modifiedBy: `${tag?.changed_by.first_name} ${tag?.changed_by.last_name}`,
+ modifiedBy: getOwnerName(tag?.changed_by),
};
items.push(lastModified);
diff --git a/superset-frontend/src/pages/AnnotationLayerList/index.tsx b/superset-frontend/src/pages/AnnotationLayerList/index.tsx
index fc909538c0d94..fff5743b5ab90 100644
--- a/superset-frontend/src/pages/AnnotationLayerList/index.tsx
+++ b/superset-frontend/src/pages/AnnotationLayerList/index.tsx
@@ -21,7 +21,6 @@ import React, { useMemo, useState } from 'react';
import rison from 'rison';
import { t, SupersetClient } from '@superset-ui/core';
import { Link, useHistory } from 'react-router-dom';
-import moment from 'moment';
import { useListViewResource } from 'src/views/CRUD/hooks';
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
import withToasts from 'src/components/MessageToasts/withToasts';
@@ -36,9 +35,10 @@ import DeleteModal from 'src/components/DeleteModal';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import AnnotationLayerModal from 'src/features/annotationLayers/AnnotationLayerModal';
import { AnnotationLayerObject } from 'src/features/annotationLayers/types';
+import { ModifiedInfo } from 'src/components/AuditInfo';
+import { QueryObjectColumns } from 'src/views/CRUD/types';
const PAGE_SIZE = 25;
-const MOMENT_FORMAT = 'MMM DD, YYYY';
interface AnnotationLayersListProps {
addDangerToast: (msg: string) => void;
@@ -156,65 +156,16 @@ function AnnotationLayersList({
{
Cell: ({
row: {
- original: { changed_on: changedOn },
+ original: {
+ changed_on_delta_humanized: changedOn,
+ changed_by: changedBy,
+ },
},
- }: any) => {
- const date = new Date(changedOn);
- const utc = new Date(
- Date.UTC(
- date.getFullYear(),
- date.getMonth(),
- date.getDate(),
- date.getHours(),
- date.getMinutes(),
- date.getSeconds(),
- date.getMilliseconds(),
- ),
- );
-
- return moment(utc).format(MOMENT_FORMAT);
- },
+ }: any) => ,
Header: t('Last modified'),
accessor: 'changed_on',
size: 'xl',
},
- {
- Cell: ({
- row: {
- original: { created_on: createdOn },
- },
- }: any) => {
- const date = new Date(createdOn);
- const utc = new Date(
- Date.UTC(
- date.getFullYear(),
- date.getMonth(),
- date.getDate(),
- date.getHours(),
- date.getMinutes(),
- date.getSeconds(),
- date.getMilliseconds(),
- ),
- );
-
- return moment(utc).format(MOMENT_FORMAT);
- },
- Header: t('Created on'),
- accessor: 'created_on',
- size: 'xl',
- },
- {
- accessor: 'created_by',
- disableSortBy: true,
- Header: t('Created by'),
- Cell: ({
- row: {
- original: { created_by: createdBy },
- },
- }: any) =>
- createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
- size: 'xl',
- },
{
Cell: ({ row: { original } }: any) => {
const handleEdit = () => handleAnnotationLayerEdit(original);
@@ -249,6 +200,10 @@ function AnnotationLayersList({
hidden: !canEdit && !canDelete,
size: 'xl',
},
+ {
+ accessor: QueryObjectColumns.changed_by,
+ hidden: true,
+ },
],
[canDelete, canCreate],
);
@@ -280,15 +235,22 @@ function AnnotationLayersList({
const filters: Filters = useMemo(
() => [
{
- Header: t('Created by'),
- key: 'created_by',
- id: 'created_by',
+ Header: t('Name'),
+ key: 'search',
+ id: 'name',
+ input: 'search',
+ operator: FilterOperator.contains,
+ },
+ {
+ Header: t('Changed by'),
+ key: 'changed_by',
+ id: 'changed_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'annotation_layer',
- 'created_by',
+ 'changed_by',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching dataset datasource values: %s',
@@ -299,13 +261,6 @@ function AnnotationLayersList({
),
paginate: true,
},
- {
- Header: t('Search'),
- key: 'search',
- id: 'name',
- input: 'search',
- operator: FilterOperator.contains,
- },
],
[],
);
diff --git a/superset-frontend/src/pages/AnnotationList/index.tsx b/superset-frontend/src/pages/AnnotationList/index.tsx
index 980a18ba72e49..e04b48080f32a 100644
--- a/superset-frontend/src/pages/AnnotationList/index.tsx
+++ b/superset-frontend/src/pages/AnnotationList/index.tsx
@@ -154,7 +154,7 @@ function AnnotationList({
() => [
{
accessor: 'short_descr',
- Header: t('Label'),
+ Header: t('Name'),
},
{
accessor: 'long_descr',
diff --git a/superset-frontend/src/pages/ChartList/index.tsx b/superset-frontend/src/pages/ChartList/index.tsx
index d13113158e778..5ed967d7c1023 100644
--- a/superset-frontend/src/pages/ChartList/index.tsx
+++ b/superset-frontend/src/pages/ChartList/index.tsx
@@ -29,7 +29,6 @@ import {
import React, { useState, useMemo, useCallback } from 'react';
import rison from 'rison';
import { uniqBy } from 'lodash';
-import moment from 'moment';
import { useSelector } from 'react-redux';
import {
createErrorHandler,
@@ -69,11 +68,13 @@ import setupPlugins from 'src/setup/setupPlugins';
import InfoTooltip from 'src/components/InfoTooltip';
import CertifiedBadge from 'src/components/CertifiedBadge';
import { GenericLink } from 'src/components/GenericLink/GenericLink';
-import Owner from 'src/types/Owner';
import { loadTags } from 'src/components/Tags/utils';
+import FacePile from 'src/components/FacePile';
import ChartCard from 'src/features/charts/ChartCard';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import { findPermission } from 'src/utils/findPermission';
+import { ModifiedInfo } from 'src/components/AuditInfo';
+import { QueryObjectColumns } from 'src/views/CRUD/types';
const FlexRowContainer = styled.div`
align-items: center;
@@ -245,10 +246,6 @@ function ChartList(props: ChartListProps) {
});
setPreparingExport(true);
};
- const changedByName = (lastSavedBy: Owner) =>
- lastSavedBy?.first_name
- ? `${lastSavedBy?.first_name} ${lastSavedBy?.last_name}`
- : null;
function handleBulkChartDelete(chartsToDelete: Chart[]) {
SupersetClient.delete({
@@ -366,7 +363,7 @@ function ChartList(props: ChartListProps) {
)}
),
- Header: t('Chart'),
+ Header: t('Name'),
accessor: 'slice_name',
},
{
@@ -375,7 +372,7 @@ function ChartList(props: ChartListProps) {
original: { viz_type: vizType },
},
}: any) => registry.get(vizType)?.name || vizType,
- Header: t('Visualization type'),
+ Header: t('Type'),
accessor: 'viz_type',
size: 'xxl',
},
@@ -438,44 +435,27 @@ function ChartList(props: ChartListProps) {
{
Cell: ({
row: {
- original: { last_saved_by: lastSavedBy },
+ original: { owners = [] },
},
- }: any) => <>{changedByName(lastSavedBy)}>,
- Header: t('Modified by'),
- accessor: 'last_saved_by.first_name',
+ }: any) => ,
+ Header: t('Owners'),
+ accessor: 'owners',
+ disableSortBy: true,
size: 'xl',
},
{
Cell: ({
row: {
- original: { last_saved_at: lastSavedAt },
+ original: {
+ changed_on_delta_humanized: changedOn,
+ changed_by: changedBy,
+ },
},
- }: any) => (
-
- {lastSavedAt ? moment.utc(lastSavedAt).fromNow() : null}
-
- ),
+ }: any) => ,
Header: t('Last modified'),
accessor: 'last_saved_at',
size: 'xl',
},
- {
- accessor: 'owners',
- hidden: true,
- disableSortBy: true,
- },
- {
- Cell: ({
- row: {
- original: { created_by: createdBy },
- },
- }: any) =>
- createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
- Header: t('Created by'),
- accessor: 'created_by',
- disableSortBy: true,
- size: 'xl',
- },
{
Cell: ({ row: { original } }: any) => {
const handleDelete = () =>
@@ -563,6 +543,10 @@ function ChartList(props: ChartListProps) {
disableSortBy: true,
hidden: !canEdit && !canDelete,
},
+ {
+ accessor: QueryObjectColumns.changed_by,
+ hidden: true,
+ },
],
[
userId,
@@ -597,58 +581,14 @@ function ChartList(props: ChartListProps) {
const filters: Filters = useMemo(() => {
const filters_list = [
{
- Header: t('Search'),
+ Header: t('Name'),
key: 'search',
id: 'slice_name',
input: 'search',
operator: FilterOperator.chartAllText,
},
{
- Header: t('Owner'),
- key: 'owner',
- id: 'owners',
- input: 'select',
- operator: FilterOperator.relationManyMany,
- unfilteredLabel: t('All'),
- fetchSelects: createFetchRelated(
- 'chart',
- 'owners',
- createErrorHandler(errMsg =>
- addDangerToast(
- t(
- 'An error occurred while fetching chart owners values: %s',
- errMsg,
- ),
- ),
- ),
- props.user,
- ),
- paginate: true,
- },
- {
- Header: t('Created by'),
- key: 'created_by',
- id: 'created_by',
- input: 'select',
- operator: FilterOperator.relationOneMany,
- unfilteredLabel: t('All'),
- fetchSelects: createFetchRelated(
- 'chart',
- 'created_by',
- createErrorHandler(errMsg =>
- addDangerToast(
- t(
- 'An error occurred while fetching chart created by values: %s',
- errMsg,
- ),
- ),
- ),
- props.user,
- ),
- paginate: true,
- },
- {
- Header: t('Chart type'),
+ Header: t('Type'),
key: 'viz_type',
id: 'viz_type',
input: 'select',
@@ -683,8 +623,43 @@ function ChartList(props: ChartListProps) {
fetchSelects: createFetchDatasets,
paginate: true,
},
+ ...(isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) && canReadTag
+ ? [
+ {
+ Header: t('Tag'),
+ key: 'tags',
+ id: 'tags',
+ input: 'select',
+ operator: FilterOperator.chartTags,
+ unfilteredLabel: t('All'),
+ fetchSelects: loadTags,
+ },
+ ]
+ : []),
{
- Header: t('Dashboards'),
+ Header: t('Owner'),
+ key: 'owner',
+ id: 'owners',
+ input: 'select',
+ operator: FilterOperator.relationManyMany,
+ unfilteredLabel: t('All'),
+ fetchSelects: createFetchRelated(
+ 'chart',
+ 'owners',
+ createErrorHandler(errMsg =>
+ addDangerToast(
+ t(
+ 'An error occurred while fetching chart owners values: %s',
+ errMsg,
+ ),
+ ),
+ ),
+ props.user,
+ ),
+ paginate: true,
+ },
+ {
+ Header: t('Dashboard'),
key: 'dashboards',
id: 'dashboards',
input: 'select',
@@ -707,18 +682,27 @@ function ChartList(props: ChartListProps) {
{ label: t('No'), value: false },
],
},
- ] as Filters;
- if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) && canReadTag) {
- filters_list.push({
- Header: t('Tags'),
- key: 'tags',
- id: 'tags',
+ {
+ Header: t('Modified by'),
+ key: 'changed_by',
+ id: 'changed_by',
input: 'select',
- operator: FilterOperator.chartTags,
+ operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
- fetchSelects: loadTags,
- });
- }
+ fetchSelects: createFetchRelated(
+ 'chart',
+ 'changed_by',
+ createErrorHandler(errMsg =>
+ t(
+ 'An error occurred while fetching dataset datasource values: %s',
+ errMsg,
+ ),
+ ),
+ props.user,
+ ),
+ paginate: true,
+ },
+ ] as Filters;
return filters_list;
}, [addDangerToast, favoritesFilter, props.user]);
diff --git a/superset-frontend/src/pages/CssTemplateList/index.tsx b/superset-frontend/src/pages/CssTemplateList/index.tsx
index f777f8e743ee4..b77217b22f7eb 100644
--- a/superset-frontend/src/pages/CssTemplateList/index.tsx
+++ b/superset-frontend/src/pages/CssTemplateList/index.tsx
@@ -21,13 +21,11 @@ import React, { useMemo, useState } from 'react';
import { t, SupersetClient } from '@superset-ui/core';
import rison from 'rison';
-import moment from 'moment';
import { useListViewResource } from 'src/views/CRUD/hooks';
-import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
+import { createErrorHandler, createFetchRelated } from 'src/views/CRUD/utils';
import withToasts from 'src/components/MessageToasts/withToasts';
import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
import DeleteModal from 'src/components/DeleteModal';
-import { Tooltip } from 'src/components/Tooltip';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
import ListView, {
@@ -37,6 +35,8 @@ import ListView, {
} from 'src/components/ListView';
import CssTemplateModal from 'src/features/cssTemplates/CssTemplateModal';
import { TemplateObject } from 'src/features/cssTemplates/types';
+import { ModifiedInfo } from 'src/components/AuditInfo';
+import { QueryObjectColumns } from 'src/views/CRUD/types';
const PAGE_SIZE = 25;
@@ -138,66 +138,12 @@ function CssTemplatesList({
changed_by: changedBy,
},
},
- }: any) => {
- let name = 'null';
-
- if (changedBy) {
- name = `${changedBy.first_name} ${changedBy.last_name}`;
- }
-
- return (
-
- );
- },
+ }: any) => ,
Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
disableSortBy: true,
},
- {
- Cell: ({
- row: {
- original: { created_on: createdOn },
- },
- }: any) => {
- const date = new Date(createdOn);
- const utc = new Date(
- Date.UTC(
- date.getFullYear(),
- date.getMonth(),
- date.getDate(),
- date.getHours(),
- date.getMinutes(),
- date.getSeconds(),
- date.getMilliseconds(),
- ),
- );
-
- return moment(utc).fromNow();
- },
- Header: t('Created on'),
- accessor: 'created_on',
- size: 'xl',
- disableSortBy: true,
- },
- {
- accessor: 'created_by',
- disableSortBy: true,
- Header: t('Created by'),
- Cell: ({
- row: {
- original: { created_by: createdBy },
- },
- }: any) =>
- createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
- size: 'xl',
- },
{
Cell: ({ row: { original } }: any) => {
const handleEdit = () => handleCssTemplateEdit(original);
@@ -232,6 +178,10 @@ function CssTemplatesList({
hidden: !canEdit && !canDelete,
size: 'xl',
},
+ {
+ accessor: QueryObjectColumns.changed_by,
+ hidden: true,
+ },
],
[canDelete, canCreate],
);
@@ -270,15 +220,22 @@ function CssTemplatesList({
const filters: Filters = useMemo(
() => [
{
- Header: t('Created by'),
- key: 'created_by',
- id: 'created_by',
+ Header: t('Name'),
+ key: 'search',
+ id: 'template_name',
+ input: 'search',
+ operator: FilterOperator.contains,
+ },
+ {
+ Header: t('Modified by'),
+ key: 'changed_by',
+ id: 'changed_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'css_template',
- 'created_by',
+ 'changed_by',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching dataset datasource values: %s',
@@ -289,13 +246,6 @@ function CssTemplatesList({
),
paginate: true,
},
- {
- Header: t('Search'),
- key: 'search',
- id: 'template_name',
- input: 'search',
- operator: FilterOperator.contains,
- },
],
[],
);
diff --git a/superset-frontend/src/pages/DashboardList/index.tsx b/superset-frontend/src/pages/DashboardList/index.tsx
index 6542d85129722..e82b70185991e 100644
--- a/superset-frontend/src/pages/DashboardList/index.tsx
+++ b/superset-frontend/src/pages/DashboardList/index.tsx
@@ -57,13 +57,17 @@ import { Tooltip } from 'src/components/Tooltip';
import ImportModelsModal from 'src/components/ImportModal/index';
import Dashboard from 'src/dashboard/containers/Dashboard';
-import { Dashboard as CRUDDashboard } from 'src/views/CRUD/types';
+import {
+ Dashboard as CRUDDashboard,
+ QueryObjectColumns,
+} from 'src/views/CRUD/types';
import CertifiedBadge from 'src/components/CertifiedBadge';
import { loadTags } from 'src/components/Tags/utils';
import DashboardCard from 'src/features/dashboards/DashboardCard';
import { DashboardStatus } from 'src/features/dashboards/types';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import { findPermission } from 'src/utils/findPermission';
+import { ModifiedInfo } from 'src/components/AuditInfo';
const PAGE_SIZE = 25;
const PASSWORDS_NEEDED_MESSAGE = t(
@@ -108,11 +112,7 @@ const Actions = styled.div`
`;
function DashboardList(props: DashboardListProps) {
- const {
- addDangerToast,
- addSuccessToast,
- user: { userId },
- } = props;
+ const { addDangerToast, addSuccessToast, user } = props;
const { roles } = useSelector(
state => state.user,
@@ -178,7 +178,7 @@ function DashboardList(props: DashboardListProps) {
};
// TODO: Fix usage of localStorage keying on the user id
- const userKey = dangerouslyGetItemDoNotUse(userId?.toString(), null);
+ const userKey = dangerouslyGetItemDoNotUse(user?.userId?.toString(), null);
const canCreate = hasPerm('can_write');
const canEdit = hasPerm('can_write');
@@ -274,7 +274,7 @@ function DashboardList(props: DashboardListProps) {
original: { id },
},
}: any) =>
- userId && (
+ user?.userId && (
),
- Header: t('Title'),
+ Header: t('Name'),
accessor: 'dashboard_title',
},
+ {
+ Cell: ({
+ row: {
+ original: { status },
+ },
+ }: any) =>
+ status === DashboardStatus.PUBLISHED ? t('Published') : t('Draft'),
+ Header: t('Status'),
+ accessor: 'published',
+ size: 'xl',
+ },
{
Cell: ({
row: {
@@ -341,55 +352,25 @@ function DashboardList(props: DashboardListProps) {
{
Cell: ({
row: {
- original: { changed_by_name: changedByName },
- },
- }: any) => <>{changedByName}>,
- Header: t('Modified by'),
- accessor: 'changed_by.first_name',
- size: 'xl',
- },
- {
- Cell: ({
- row: {
- original: { status },
- },
- }: any) =>
- status === DashboardStatus.PUBLISHED ? t('Published') : t('Draft'),
- Header: t('Status'),
- accessor: 'published',
- size: 'xl',
- },
- {
- Cell: ({
- row: {
- original: { changed_on_delta_humanized: changedOn },
- },
- }: any) => {changedOn},
- Header: t('Modified'),
- accessor: 'changed_on_delta_humanized',
- size: 'xl',
- },
- {
- Cell: ({
- row: {
- original: { created_by: createdBy },
+ original: { owners = [] },
},
- }: any) =>
- createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
- Header: t('Created by'),
- accessor: 'created_by',
+ }: any) => ,
+ Header: t('Owners'),
+ accessor: 'owners',
disableSortBy: true,
size: 'xl',
},
{
Cell: ({
row: {
- original: { owners = [] },
+ original: {
+ changed_on_delta_humanized: changedOn,
+ changed_by: changedBy,
+ },
},
- }: any) => ,
- Header: t('Owners'),
- accessor: 'owners',
- disableSortBy: true,
+ }: any) => ,
+ Header: t('Last modified'),
+ accessor: 'changed_on_delta_humanized',
size: 'xl',
},
{
@@ -475,9 +456,13 @@ function DashboardList(props: DashboardListProps) {
hidden: !canEdit && !canDelete && !canExport,
disableSortBy: true,
},
+ {
+ accessor: QueryObjectColumns.changed_by,
+ hidden: true,
+ },
],
[
- userId,
+ user?.userId,
canEdit,
canDelete,
canExport,
@@ -509,12 +494,37 @@ function DashboardList(props: DashboardListProps) {
const filters: Filters = useMemo(() => {
const filters_list = [
{
- Header: t('Search'),
+ Header: t('Name'),
key: 'search',
id: 'dashboard_title',
input: 'search',
operator: FilterOperator.titleOrSlug,
},
+ {
+ Header: t('Status'),
+ key: 'published',
+ id: 'published',
+ input: 'select',
+ operator: FilterOperator.equals,
+ unfilteredLabel: t('Any'),
+ selects: [
+ { label: t('Published'), value: true },
+ { label: t('Draft'), value: false },
+ ],
+ },
+ ...(isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) && canReadTag
+ ? [
+ {
+ Header: t('Tag'),
+ key: 'tags',
+ id: 'tags',
+ input: 'select',
+ operator: FilterOperator.dashboardTags,
+ unfilteredLabel: t('All'),
+ fetchSelects: loadTags,
+ },
+ ]
+ : []),
{
Header: t('Owner'),
key: 'owner',
@@ -537,41 +547,7 @@ function DashboardList(props: DashboardListProps) {
),
paginate: true,
},
- {
- Header: t('Created by'),
- key: 'created_by',
- id: 'created_by',
- input: 'select',
- operator: FilterOperator.relationOneMany,
- unfilteredLabel: t('All'),
- fetchSelects: createFetchRelated(
- 'dashboard',
- 'created_by',
- createErrorHandler(errMsg =>
- addDangerToast(
- t(
- 'An error occurred while fetching dashboard created by values: %s',
- errMsg,
- ),
- ),
- ),
- props.user,
- ),
- paginate: true,
- },
- {
- Header: t('Status'),
- key: 'published',
- id: 'published',
- input: 'select',
- operator: FilterOperator.equals,
- unfilteredLabel: t('Any'),
- selects: [
- { label: t('Published'), value: true },
- { label: t('Draft'), value: false },
- ],
- },
- ...(userId ? [favoritesFilter] : []),
+ ...(user?.userId ? [favoritesFilter] : []),
{
Header: t('Certified'),
key: 'certified',
@@ -585,18 +561,27 @@ function DashboardList(props: DashboardListProps) {
{ label: t('No'), value: false },
],
},
- ] as Filters;
- if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) && canReadTag) {
- filters_list.push({
- Header: t('Tags'),
- key: 'tags',
- id: 'tags',
+ {
+ Header: t('Modified by'),
+ key: 'changed_by',
+ id: 'changed_by',
input: 'select',
- operator: FilterOperator.dashboardTags,
+ operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
- fetchSelects: loadTags,
- });
- }
+ fetchSelects: createFetchRelated(
+ 'dashboard',
+ 'changed_by',
+ createErrorHandler(errMsg =>
+ t(
+ 'An error occurred while fetching dataset datasource values: %s',
+ errMsg,
+ ),
+ ),
+ user,
+ ),
+ paginate: true,
+ },
+ ] as Filters;
return filters_list;
}, [addDangerToast, favoritesFilter, props.user]);
@@ -632,7 +617,7 @@ function DashboardList(props: DashboardListProps) {
? userKey.thumbnails
: isFeatureEnabled(FeatureFlag.THUMBNAILS)
}
- userId={userId}
+ userId={user?.userId}
loading={loading}
openDashboardEditModal={openDashboardEditModal}
saveFavoriteStatus={saveFavoriteStatus}
@@ -646,7 +631,7 @@ function DashboardList(props: DashboardListProps) {
favoriteStatus,
hasPerm,
loading,
- userId,
+ user?.userId,
saveFavoriteStatus,
userKey,
],
@@ -743,7 +728,7 @@ function DashboardList(props: DashboardListProps) {
addSuccessToast,
addDangerToast,
undefined,
- userId,
+ user?.userId,
);
setDashboardToDelete(null);
}}
diff --git a/superset-frontend/src/pages/DatabaseList/DatabaseList.test.jsx b/superset-frontend/src/pages/DatabaseList/DatabaseList.test.jsx
index fd989b50d2270..b1bfb245d37d1 100644
--- a/superset-frontend/src/pages/DatabaseList/DatabaseList.test.jsx
+++ b/superset-frontend/src/pages/DatabaseList/DatabaseList.test.jsx
@@ -218,7 +218,7 @@ describe('Admin DatabaseList', () => {
await waitForComponentToPaint(wrapper);
expect(fetchMock.lastCall()[0]).toMatchInlineSnapshot(
- `"http://localhost/api/v1/database/?q=(filters:!((col:expose_in_sqllab,opr:eq,value:!t),(col:allow_run_async,opr:eq,value:!f),(col:database_name,opr:ct,value:fooo)),order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
+ `"http://localhost/api/v1/database/?q=(filters:!((col:database_name,opr:ct,value:fooo),(col:expose_in_sqllab,opr:eq,value:!t),(col:allow_run_async,opr:eq,value:!f)),order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
);
});
diff --git a/superset-frontend/src/pages/DatabaseList/index.tsx b/superset-frontend/src/pages/DatabaseList/index.tsx
index d2308bd117f61..8c98392aca93e 100644
--- a/superset-frontend/src/pages/DatabaseList/index.tsx
+++ b/superset-frontend/src/pages/DatabaseList/index.tsx
@@ -32,7 +32,11 @@ import { LocalStorageKeys, setItem } from 'src/utils/localStorageHelpers';
import Loading from 'src/components/Loading';
import { useListViewResource } from 'src/views/CRUD/hooks';
-import { createErrorHandler, uploadUserPerms } from 'src/views/CRUD/utils';
+import {
+ createErrorHandler,
+ createFetchRelated,
+ uploadUserPerms,
+} from 'src/views/CRUD/utils';
import withToasts from 'src/components/MessageToasts/withToasts';
import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
import DeleteModal from 'src/components/DeleteModal';
@@ -48,6 +52,8 @@ import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import type { MenuObjectProps } from 'src/types/bootstrapTypes';
import DatabaseModal from 'src/features/databases/DatabaseModal';
import { DatabaseObject } from 'src/features/databases/types';
+import { ModifiedInfo } from 'src/components/AuditInfo';
+import { QueryObjectColumns } from 'src/views/CRUD/types';
const extensionsRegistry = getExtensionsRegistry();
const DatabaseDeleteRelatedExtension = extensionsRegistry.get(
@@ -67,6 +73,11 @@ interface DatabaseDeleteObject extends DatabaseObject {
interface DatabaseListProps {
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
+ user: {
+ userId: string | number;
+ firstName: string;
+ lastName: string;
+ };
}
const IconCheck = styled(Icons.Check)`
@@ -90,7 +101,11 @@ function BooleanDisplay({ value }: { value: Boolean }) {
return value ? : ;
}
-function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
+function DatabaseList({
+ addDangerToast,
+ addSuccessToast,
+ user,
+}: DatabaseListProps) {
const {
state: {
loading,
@@ -105,7 +120,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
t('database'),
addDangerToast,
);
- const user = useSelector(
+ const fullUser = useSelector(
state => state.user,
);
const showDatabaseModal = getUrlParam(URL_PARAMS.showDatabaseModal);
@@ -123,11 +138,11 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
null,
);
const [allowUploads, setAllowUploads] = useState(false);
- const isAdmin = isUserAdmin(user);
+ const isAdmin = isUserAdmin(fullUser);
const showUploads = allowUploads || isAdmin;
const [preparingExport, setPreparingExport] = useState(false);
- const { roles } = user;
+ const { roles } = fullUser;
const {
CSV_EXTENSIONS,
COLUMNAR_EXTENSIONS,
@@ -313,7 +328,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
() => [
{
accessor: 'database_name',
- Header: t('Database'),
+ Header: t('Name'),
},
{
accessor: 'backend',
@@ -380,23 +395,14 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
size: 'md',
},
{
- accessor: 'created_by',
- disableSortBy: true,
- Header: t('Created by'),
Cell: ({
row: {
- original: { created_by: createdBy },
+ original: {
+ changed_by: changedBy,
+ changed_on_delta_humanized: changedOn,
+ },
},
- }: any) =>
- createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
- size: 'xl',
- },
- {
- Cell: ({
- row: {
- original: { changed_on_delta_humanized: changedOn },
- },
- }: any) => changedOn,
+ }: any) => ,
Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
@@ -470,12 +476,23 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
hidden: !canEdit && !canDelete,
disableSortBy: true,
},
+ {
+ accessor: QueryObjectColumns.changed_by,
+ hidden: true,
+ },
],
[canDelete, canEdit, canExport],
);
const filters: Filters = useMemo(
() => [
+ {
+ Header: t('Name'),
+ key: 'search',
+ id: 'database_name',
+ input: 'search',
+ operator: FilterOperator.contains,
+ },
{
Header: t('Expose in SQL Lab'),
key: 'expose_in_sql_lab',
@@ -509,11 +526,24 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
],
},
{
- Header: t('Search'),
- key: 'search',
- id: 'database_name',
- input: 'search',
- operator: FilterOperator.contains,
+ Header: t('Modified by'),
+ key: 'changed_by',
+ id: 'changed_by',
+ input: 'select',
+ operator: FilterOperator.relationOneMany,
+ unfilteredLabel: t('All'),
+ fetchSelects: createFetchRelated(
+ 'database',
+ 'changed_by',
+ createErrorHandler(errMsg =>
+ t(
+ 'An error occurred while fetching dataset datasource values: %s',
+ errMsg,
+ ),
+ ),
+ user,
+ ),
+ paginate: true,
},
],
[],
diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx b/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx
index 916dd0615bb8b..c316001bb46e4 100644
--- a/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx
+++ b/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx
@@ -285,56 +285,41 @@ describe('RTL', () => {
});
describe('Prevent unsafe URLs', () => {
+ const columnCount = 8;
+ const exploreUrlIndex = 1;
+ const getTdIndex = (rowNumber: number): number =>
+ rowNumber * columnCount + exploreUrlIndex;
+
const mockedProps = {};
let wrapper: any;
it('Check prevent unsafe is on renders relative links', async () => {
- const tdColumnsNumber = 9;
useSelectorMock.mockReturnValue(true);
wrapper = await mountAndWait(mockedProps);
const tdElements = wrapper.find(ListView).find('td');
- expect(
- tdElements
- .at(0 * tdColumnsNumber + 1)
- .find('a')
- .prop('href'),
- ).toBe('/https://www.google.com?0');
- expect(
- tdElements
- .at(1 * tdColumnsNumber + 1)
- .find('a')
- .prop('href'),
- ).toBe('/https://www.google.com?1');
- expect(
- tdElements
- .at(2 * tdColumnsNumber + 1)
- .find('a')
- .prop('href'),
- ).toBe('/https://www.google.com?2');
+ expect(tdElements.at(getTdIndex(0)).find('a').prop('href')).toBe(
+ '/https://www.google.com?0',
+ );
+ expect(tdElements.at(getTdIndex(1)).find('a').prop('href')).toBe(
+ '/https://www.google.com?1',
+ );
+ expect(tdElements.at(getTdIndex(2)).find('a').prop('href')).toBe(
+ '/https://www.google.com?2',
+ );
});
it('Check prevent unsafe is off renders absolute links', async () => {
- const tdColumnsNumber = 9;
useSelectorMock.mockReturnValue(false);
wrapper = await mountAndWait(mockedProps);
const tdElements = wrapper.find(ListView).find('td');
- expect(
- tdElements
- .at(0 * tdColumnsNumber + 1)
- .find('a')
- .prop('href'),
- ).toBe('https://www.google.com?0');
- expect(
- tdElements
- .at(1 * tdColumnsNumber + 1)
- .find('a')
- .prop('href'),
- ).toBe('https://www.google.com?1');
- expect(
- tdElements
- .at(2 * tdColumnsNumber + 1)
- .find('a')
- .prop('href'),
- ).toBe('https://www.google.com?2');
+ expect(tdElements.at(getTdIndex(0)).find('a').prop('href')).toBe(
+ 'https://www.google.com?0',
+ );
+ expect(tdElements.at(getTdIndex(1)).find('a').prop('href')).toBe(
+ 'https://www.google.com?1',
+ );
+ expect(tdElements.at(getTdIndex(2)).find('a').prop('href')).toBe(
+ 'https://www.google.com?2',
+ );
});
});
diff --git a/superset-frontend/src/pages/DatasetList/index.tsx b/superset-frontend/src/pages/DatasetList/index.tsx
index d86d7a7b0ffd5..8a39cb0463e2b 100644
--- a/superset-frontend/src/pages/DatasetList/index.tsx
+++ b/superset-frontend/src/pages/DatasetList/index.tsx
@@ -70,6 +70,8 @@ import {
} from 'src/features/datasets/constants';
import DuplicateDatasetModal from 'src/features/datasets/DuplicateDatasetModal';
import { useSelector } from 'react-redux';
+import { ModifiedInfo } from 'src/components/AuditInfo';
+import { QueryObjectColumns } from 'src/views/CRUD/types';
const extensionsRegistry = getExtensionsRegistry();
const DatasetDeleteRelatedExtension = extensionsRegistry.get(
@@ -380,26 +382,6 @@ const DatasetList: FunctionComponent = ({
accessor: 'schema',
size: 'lg',
},
- {
- Cell: ({
- row: {
- original: { changed_on_delta_humanized: changedOn },
- },
- }: any) => {changedOn},
- Header: t('Modified'),
- accessor: 'changed_on_delta_humanized',
- size: 'xl',
- },
- {
- Cell: ({
- row: {
- original: { changed_by_name: changedByName },
- },
- }: any) => changedByName,
- Header: t('Modified by'),
- accessor: 'changed_by.first_name',
- size: 'xl',
- },
{
accessor: 'database',
disableSortBy: true,
@@ -416,6 +398,19 @@ const DatasetList: FunctionComponent = ({
disableSortBy: true,
size: 'lg',
},
+ {
+ Cell: ({
+ row: {
+ original: {
+ changed_on_delta_humanized: changedOn,
+ changed_by: changedBy,
+ },
+ },
+ }: any) => ,
+ Header: t('Last modified'),
+ accessor: 'changed_on_delta_humanized',
+ size: 'xl',
+ },
{
accessor: 'sql',
hidden: true,
@@ -515,6 +510,10 @@ const DatasetList: FunctionComponent = ({
hidden: !canEdit && !canDelete && !canDuplicate,
disableSortBy: true,
},
+ {
+ accessor: QueryObjectColumns.changed_by,
+ hidden: true,
+ },
],
[canEdit, canDelete, canExport, openDatasetEditModal, canDuplicate, user],
);
@@ -522,31 +521,23 @@ const DatasetList: FunctionComponent = ({
const filterTypes: Filters = useMemo(
() => [
{
- Header: t('Search'),
+ Header: t('Name'),
key: 'search',
id: 'table_name',
input: 'search',
operator: FilterOperator.contains,
},
{
- Header: t('Owner'),
- key: 'owner',
- id: 'owners',
+ Header: t('Type'),
+ key: 'sql',
+ id: 'sql',
input: 'select',
- operator: FilterOperator.relationManyMany,
+ operator: FilterOperator.datasetIsNullOrEmpty,
unfilteredLabel: 'All',
- fetchSelects: createFetchRelated(
- 'dataset',
- 'owners',
- createErrorHandler(errMsg =>
- t(
- 'An error occurred while fetching dataset owner values: %s',
- errMsg,
- ),
- ),
- user,
- ),
- paginate: true,
+ selects: [
+ { label: t('Virtual'), value: false },
+ { label: t('Physical'), value: true },
+ ],
},
{
Header: t('Database'),
@@ -581,16 +572,24 @@ const DatasetList: FunctionComponent = ({
paginate: true,
},
{
- Header: t('Type'),
- key: 'sql',
- id: 'sql',
+ Header: t('Owner'),
+ key: 'owner',
+ id: 'owners',
input: 'select',
- operator: FilterOperator.datasetIsNullOrEmpty,
+ operator: FilterOperator.relationManyMany,
unfilteredLabel: 'All',
- selects: [
- { label: t('Virtual'), value: false },
- { label: t('Physical'), value: true },
- ],
+ fetchSelects: createFetchRelated(
+ 'dataset',
+ 'owners',
+ createErrorHandler(errMsg =>
+ t(
+ 'An error occurred while fetching dataset owner values: %s',
+ errMsg,
+ ),
+ ),
+ user,
+ ),
+ paginate: true,
},
{
Header: t('Certified'),
@@ -605,6 +604,26 @@ const DatasetList: FunctionComponent = ({
{ label: t('No'), value: false },
],
},
+ {
+ Header: t('Modified by'),
+ key: 'changed_by',
+ id: 'changed_by',
+ input: 'select',
+ operator: FilterOperator.relationOneMany,
+ unfilteredLabel: t('All'),
+ fetchSelects: createFetchRelated(
+ 'dataset',
+ 'changed_by',
+ createErrorHandler(errMsg =>
+ t(
+ 'An error occurred while fetching dataset datasource values: %s',
+ errMsg,
+ ),
+ ),
+ user,
+ ),
+ paginate: true,
+ },
],
[user],
);
diff --git a/superset-frontend/src/pages/QueryHistoryList/index.tsx b/superset-frontend/src/pages/QueryHistoryList/index.tsx
index 77177188e0e47..94b646d9e4350 100644
--- a/superset-frontend/src/pages/QueryHistoryList/index.tsx
+++ b/superset-frontend/src/pages/QueryHistoryList/index.tsx
@@ -53,6 +53,7 @@ import { QueryObject, QueryObjectColumns } from 'src/views/CRUD/types';
import Icons from 'src/components/Icons';
import QueryPreviewModal from 'src/features/queries/QueryPreviewModal';
import { addSuccessToast } from 'src/components/MessageToasts/actions';
+import getOwnerName from 'src/utils/getOwnerName';
const PAGE_SIZE = 25;
const SQL_PREVIEW_MAX_LINES = 4;
@@ -311,7 +312,7 @@ function QueryList({ addDangerToast }: QueryListProps) {
row: {
original: { user },
},
- }: any) => (user ? `${user.first_name} ${user.last_name}` : ''),
+ }: any) => getOwnerName(user),
},
{
accessor: QueryObjectColumns.user,
diff --git a/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx b/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx
index a4621ed10eada..6721f73add1fd 100644
--- a/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx
+++ b/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx
@@ -187,8 +187,8 @@ describe('RuleList RTL', () => {
const searchFilters = screen.queryAllByTestId('filters-search');
expect(searchFilters).toHaveLength(2);
- const typeFilter = await screen.findByTestId('filters-select');
- expect(typeFilter).toBeInTheDocument();
+ const typeFilter = screen.queryAllByTestId('filters-select');
+ expect(typeFilter).toHaveLength(2);
});
it('renders correct list columns', async () => {
@@ -201,7 +201,7 @@ describe('RuleList RTL', () => {
const fitlerTypeColumn = await within(table).findByText('Filter Type');
const groupKeyColumn = await within(table).findByText('Group Key');
const clauseColumn = await within(table).findByText('Clause');
- const modifiedColumn = await within(table).findByText('Modified');
+ const modifiedColumn = await within(table).findByText('Last modified');
const actionsColumn = await within(table).findByText('Actions');
expect(nameColumn).toBeInTheDocument();
diff --git a/superset-frontend/src/pages/RowLevelSecurityList/index.tsx b/superset-frontend/src/pages/RowLevelSecurityList/index.tsx
index 3c1e3b8aae865..bef42284d0b76 100644
--- a/superset-frontend/src/pages/RowLevelSecurityList/index.tsx
+++ b/superset-frontend/src/pages/RowLevelSecurityList/index.tsx
@@ -33,7 +33,9 @@ import rison from 'rison';
import { useListViewResource } from 'src/views/CRUD/hooks';
import RowLevelSecurityModal from 'src/features/rls/RowLevelSecurityModal';
import { RLSObject } from 'src/features/rls/types';
-import { createErrorHandler } from 'src/views/CRUD/utils';
+import { createErrorHandler, createFetchRelated } from 'src/views/CRUD/utils';
+import { ModifiedInfo } from 'src/components/AuditInfo';
+import { QueryObjectColumns } from 'src/views/CRUD/types';
const Actions = styled.div`
color: ${({ theme }) => theme.colors.grayscale.base};
@@ -43,7 +45,7 @@ interface RLSProps {
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
user: {
- userId?: string | number;
+ userId: string | number;
firstName: string;
lastName: string;
};
@@ -146,10 +148,13 @@ function RowLevelSecurityList(props: RLSProps) {
{
Cell: ({
row: {
- original: { changed_on_delta_humanized: changedOn },
+ original: {
+ changed_on_delta_humanized: changedOn,
+ changed_by: changedBy,
+ },
},
- }: any) => {changedOn},
- Header: t('Modified'),
+ }: any) => ,
+ Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
@@ -218,6 +223,10 @@ function RowLevelSecurityList(props: RLSProps) {
hidden: !canEdit && !canWrite && !canExport,
disableSortBy: true,
},
+ {
+ accessor: QueryObjectColumns.changed_by,
+ hidden: true,
+ },
],
[
user.userId,
@@ -270,6 +279,26 @@ function RowLevelSecurityList(props: RLSProps) {
input: 'search',
operator: FilterOperator.startsWith,
},
+ {
+ Header: t('Modified by'),
+ key: 'changed_by',
+ id: 'changed_by',
+ input: 'select',
+ operator: FilterOperator.relationOneMany,
+ unfilteredLabel: t('All'),
+ fetchSelects: createFetchRelated(
+ 'rowlevelsecurity',
+ 'changed_by',
+ createErrorHandler(errMsg =>
+ t(
+ 'An error occurred while fetching dataset datasource values: %s',
+ errMsg,
+ ),
+ ),
+ user,
+ ),
+ paginate: true,
+ },
],
[user],
);
diff --git a/superset-frontend/src/pages/SavedQueryList/index.tsx b/superset-frontend/src/pages/SavedQueryList/index.tsx
index 3ee62c2ce6533..d48ffef8c90c3 100644
--- a/superset-frontend/src/pages/SavedQueryList/index.tsx
+++ b/superset-frontend/src/pages/SavedQueryList/index.tsx
@@ -18,20 +18,19 @@
*/
import {
- isFeatureEnabled,
FeatureFlag,
+ isFeatureEnabled,
styled,
SupersetClient,
t,
} from '@superset-ui/core';
-import React, { useState, useMemo, useCallback } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import rison from 'rison';
-import moment from 'moment';
import {
- createFetchRelated,
- createFetchDistinct,
createErrorHandler,
+ createFetchDistinct,
+ createFetchRelated,
} from 'src/views/CRUD/utils';
import { useSelector } from 'react-redux';
import Popover from 'src/components/Popover';
@@ -39,11 +38,11 @@ import withToasts from 'src/components/MessageToasts/withToasts';
import { useListViewResource } from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import handleResourceExport from 'src/utils/export';
-import SubMenu, { SubMenuProps, ButtonProps } from 'src/features/home/SubMenu';
+import SubMenu, { ButtonProps, SubMenuProps } from 'src/features/home/SubMenu';
import ListView, {
- ListViewProps,
- Filters,
FilterOperator,
+ Filters,
+ ListViewProps,
} from 'src/components/ListView';
import Loading from 'src/components/Loading';
import DeleteModal from 'src/components/DeleteModal';
@@ -51,15 +50,14 @@ import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
import { TagsList } from 'src/components/Tags';
import { Tooltip } from 'src/components/Tooltip';
import { commonMenuData } from 'src/features/home/commonMenuData';
-import { SavedQueryObject } from 'src/views/CRUD/types';
+import { QueryObjectColumns, SavedQueryObject } from 'src/views/CRUD/types';
import copyTextToClipboard from 'src/utils/copy';
import Tag from 'src/types/TagType';
import ImportModelsModal from 'src/components/ImportModal/index';
+import { ModifiedInfo } from 'src/components/AuditInfo';
+import { loadTags } from 'src/components/Tags/utils';
import Icons from 'src/components/Icons';
-import {
- BootstrapUser,
- UserWithPermissionsAndRoles,
-} from 'src/types/bootstrapTypes';
+import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import SavedQueryPreviewModal from 'src/features/queries/SavedQueryPreviewModal';
import { findPermission } from 'src/utils/findPermission';
@@ -80,7 +78,11 @@ const CONFIRM_OVERWRITE_MESSAGE = t(
interface SavedQueryListProps {
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
- user: BootstrapUser;
+ user: {
+ userId: string | number;
+ firstName: string;
+ lastName: string;
+ };
}
const StyledTableLabel = styled.div`
@@ -99,6 +101,7 @@ const StyledPopoverItem = styled.div`
function SavedQueryList({
addDangerToast,
addSuccessToast,
+ user,
}: SavedQueryListProps) {
const {
state: {
@@ -348,41 +351,6 @@ function SavedQueryList({
size: 'xl',
disableSortBy: true,
},
- {
- Cell: ({
- row: {
- original: { created_on: createdOn },
- },
- }: any) => {
- const date = new Date(createdOn);
- const utc = new Date(
- Date.UTC(
- date.getFullYear(),
- date.getMonth(),
- date.getDate(),
- date.getHours(),
- date.getMinutes(),
- date.getSeconds(),
- date.getMilliseconds(),
- ),
- );
-
- return moment(utc).fromNow();
- },
- Header: t('Created on'),
- accessor: 'created_on',
- size: 'xl',
- },
- {
- Cell: ({
- row: {
- original: { changed_on_delta_humanized: changedOn },
- },
- }: any) => changedOn,
- Header: t('Modified'),
- accessor: 'changed_on_delta_humanized',
- size: 'xl',
- },
{
Cell: ({
row: {
@@ -397,6 +365,19 @@ function SavedQueryList({
disableSortBy: true,
hidden: !isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM),
},
+ {
+ Cell: ({
+ row: {
+ original: {
+ changed_by: changedBy,
+ changed_on_delta_humanized: changedOn,
+ },
+ },
+ }: any) => ,
+ Header: t('Last modified'),
+ accessor: 'changed_on_delta_humanized',
+ size: 'xl',
+ },
{
Cell: ({ row: { original } }: any) => {
const handlePreview = () => {
@@ -452,12 +433,23 @@ function SavedQueryList({
id: 'actions',
disableSortBy: true,
},
+ {
+ accessor: QueryObjectColumns.changed_by,
+ hidden: true,
+ },
],
[canDelete, canEdit, canExport, copyQueryLink, handleSavedQueryPreview],
);
const filters: Filters = useMemo(
() => [
+ {
+ Header: t('Name'),
+ id: 'label',
+ key: 'search',
+ input: 'search',
+ operator: FilterOperator.allText,
+ },
{
Header: t('Database'),
key: 'database',
@@ -497,28 +489,42 @@ function SavedQueryList({
),
paginate: true,
},
-
+ ...((isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) && canReadTag
+ ? [
+ {
+ Header: t('Tag'),
+ id: 'tags',
+ key: 'tags',
+ input: 'select',
+ operator: FilterOperator.savedQueryTags,
+ fetchSelects: loadTags,
+ },
+ ]
+ : []) as Filters),
{
- Header: t('Search'),
- id: 'label',
- key: 'search',
- input: 'search',
- operator: FilterOperator.allText,
+ Header: t('Modified by'),
+ key: 'changed_by',
+ id: 'changed_by',
+ input: 'select',
+ operator: FilterOperator.relationOneMany,
+ unfilteredLabel: t('All'),
+ fetchSelects: createFetchRelated(
+ 'saved_query',
+ 'changed_by',
+ createErrorHandler(errMsg =>
+ t(
+ 'An error occurred while fetching dataset datasource values: %s',
+ errMsg,
+ ),
+ ),
+ user,
+ ),
+ paginate: true,
},
],
[addDangerToast],
);
- if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) && canReadTag) {
- filters.push({
- Header: t('Tags'),
- id: 'tags',
- key: 'tags',
- input: 'search',
- operator: FilterOperator.savedQueryTags,
- });
- }
-
return (
<>
diff --git a/superset-frontend/src/pages/Tags/index.tsx b/superset-frontend/src/pages/Tags/index.tsx
index a66d7c7b61b0d..d395ce7cde25a 100644
--- a/superset-frontend/src/pages/Tags/index.tsx
+++ b/superset-frontend/src/pages/Tags/index.tsx
@@ -19,9 +19,9 @@
import React, { useMemo, useState } from 'react';
import { isFeatureEnabled, FeatureFlag, t } from '@superset-ui/core';
import {
- createFetchRelated,
- createErrorHandler,
Actions,
+ createErrorHandler,
+ createFetchRelated,
} from 'src/views/CRUD/utils';
import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
@@ -35,13 +35,13 @@ import { dangerouslyGetItemDoNotUse } from 'src/utils/localStorageHelpers';
import withToasts from 'src/components/MessageToasts/withToasts';
import Icons from 'src/components/Icons';
import { Tooltip } from 'src/components/Tooltip';
-import FacePile from 'src/components/FacePile';
import { Link } from 'react-router-dom';
import { deleteTags } from 'src/features/tags/tags';
import { Tag as AntdTag } from 'antd';
-import { Tag } from 'src/views/CRUD/types';
+import { QueryObjectColumns, Tag } from 'src/views/CRUD/types';
import TagModal from 'src/features/tags/TagModal';
import FaveStar from 'src/components/FaveStar';
+import { ModifiedInfo } from 'src/components/AuditInfo';
const PAGE_SIZE = 25;
@@ -56,11 +56,8 @@ interface TagListProps {
}
function TagList(props: TagListProps) {
- const {
- addDangerToast,
- addSuccessToast,
- user: { userId },
- } = props;
+ const { addDangerToast, addSuccessToast, user } = props;
+ const { userId } = user;
const {
state: {
@@ -162,24 +159,16 @@ function TagList(props: TagListProps) {
{
Cell: ({
row: {
- original: { changed_on_delta_humanized: changedOn },
+ original: {
+ changed_on_delta_humanized: changedOn,
+ changed_by: changedBy,
+ },
},
- }: any) => {changedOn},
- Header: t('Modified'),
+ }: any) => ,
+ Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
- {
- Cell: ({
- row: {
- original: { created_by: createdBy },
- },
- }: any) => (createdBy ? : ''),
- Header: t('Created by'),
- accessor: 'created_by',
- disableSortBy: true,
- size: 'xl',
- },
{
Cell: ({ row: { original } }: any) => {
const handleEdit = () => handleTagEdit(original);
@@ -238,6 +227,10 @@ function TagList(props: TagListProps) {
hidden: !canDelete,
disableSortBy: true,
},
+ {
+ accessor: QueryObjectColumns.changed_by,
+ hidden: true,
+ },
],
[userId, canDelete, refreshData, addSuccessToast, addDangerToast],
);
@@ -245,32 +238,31 @@ function TagList(props: TagListProps) {
const filters: Filters = useMemo(() => {
const filters_list = [
{
- Header: t('Created by'),
- id: 'created_by',
+ Header: t('Name'),
+ id: 'name',
+ input: 'search',
+ operator: FilterOperator.contains,
+ },
+ {
+ Header: t('Modified by'),
+ key: 'changed_by',
+ id: 'changed_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'tag',
- 'created_by',
+ 'changed_by',
createErrorHandler(errMsg =>
- addDangerToast(
- t(
- 'An error occurred while fetching tag created by values: %s',
- errMsg,
- ),
+ t(
+ 'An error occurred while fetching dataset datasource values: %s',
+ errMsg,
),
),
- props.user,
+ user,
),
paginate: true,
},
- {
- Header: t('Search'),
- id: 'name',
- input: 'search',
- operator: FilterOperator.contains,
- },
] as Filters;
return filters_list;
}, [addDangerToast, props.user]);
diff --git a/superset-frontend/src/utils/getOwnerName.test.ts b/superset-frontend/src/utils/getOwnerName.test.ts
new file mode 100644
index 0000000000000..a4a25e57b24ed
--- /dev/null
+++ b/superset-frontend/src/utils/getOwnerName.test.ts
@@ -0,0 +1,29 @@
+/**
+ * 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 getOwnerName from './getOwnerName';
+
+test('render owner name correctly', () => {
+ expect(getOwnerName({ id: 1, first_name: 'Foo', last_name: 'Bar' })).toEqual(
+ 'Foo Bar',
+ );
+});
+
+test('return empty string for undefined owner', () => {
+ expect(getOwnerName(undefined)).toEqual('');
+});
diff --git a/superset-frontend/src/utils/getOwnerName.ts b/superset-frontend/src/utils/getOwnerName.ts
new file mode 100644
index 0000000000000..2534c45f2cbb1
--- /dev/null
+++ b/superset-frontend/src/utils/getOwnerName.ts
@@ -0,0 +1,26 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import Owner from 'src/types/Owner';
+
+export default function getOwnerName(owner?: Owner): string {
+ if (!owner) {
+ return '';
+ }
+ return `${owner.first_name} ${owner.last_name}`;
+}
diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts
index 5a53b57696f53..2fff111b47c9e 100644
--- a/superset-frontend/src/views/CRUD/types.ts
+++ b/superset-frontend/src/views/CRUD/types.ts
@@ -112,6 +112,7 @@ export interface QueryObject {
export enum QueryObjectColumns {
id = 'id',
changed_on = 'changed_on',
+ changed_by = 'changed_by',
database = 'database',
database_name = 'database.database_name',
schema = 'schema',
@@ -138,17 +139,11 @@ export type ImportResourceName =
export interface Tag {
changed_on_delta_humanized: string;
- changed_by: {
- first_name: string;
- last_name: string;
- };
+ changed_by: Owner;
created_on_delta_humanized: string;
name: string;
id: number;
- created_by: {
- first_name: string;
- last_name: string;
- };
+ created_by: Owner;
description: string;
type: string;
}
diff --git a/superset/annotation_layers/api.py b/superset/annotation_layers/api.py
index 886c151a68ba1..5606e944ef2ba 100644
--- a/superset/annotation_layers/api.py
+++ b/superset/annotation_layers/api.py
@@ -99,7 +99,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi):
]
search_filters = {"name": [AnnotationLayerAllTextFilter]}
- allowed_rel_fields = {"created_by"}
+ allowed_rel_fields = {"created_by", "changed_by"}
apispec_parameter_schemas = {
"get_delete_ids_schema": get_delete_ids_schema,
diff --git a/superset/charts/api.py b/superset/charts/api.py
index ea705f0aa9db2..191f09c66e7b4 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -273,7 +273,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"created_by": RelatedFieldFilter("first_name", FilterRelatedOwners),
}
- allowed_rel_fields = {"owners", "created_by"}
+ allowed_rel_fields = {"owners", "created_by", "changed_by"}
@expose("/", methods=("POST",))
@protect()
diff --git a/superset/css_templates/api.py b/superset/css_templates/api.py
index 25f4d50f30585..ac222da66f815 100644
--- a/superset/css_templates/api.py
+++ b/superset/css_templates/api.py
@@ -54,6 +54,10 @@ class CssTemplateRestApi(BaseSupersetModelRestApi):
allow_browser_login = True
show_columns = [
+ "changed_on_delta_humanized",
+ "changed_by.first_name",
+ "changed_by.id",
+ "changed_by.last_name",
"created_by.first_name",
"created_by.id",
"created_by.last_name",
@@ -79,7 +83,7 @@ class CssTemplateRestApi(BaseSupersetModelRestApi):
order_columns = ["template_name"]
search_filters = {"template_name": [CssTemplateAllTextFilter]}
- allowed_rel_fields = {"created_by"}
+ allowed_rel_fields = {"created_by", "changed_by"}
apispec_parameter_schemas = {
"get_delete_ids_schema": get_delete_ids_schema,
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index be773b83c3cdd..cf75a644fbb73 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -261,7 +261,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"roles": RelatedFieldFilter("name", FilterRelatedRoles),
"created_by": RelatedFieldFilter("first_name", FilterRelatedOwners),
}
- allowed_rel_fields = {"owners", "roles", "created_by"}
+ allowed_rel_fields = {"owners", "roles", "created_by", "changed_by"}
openapi_spec_tag = "Dashboards"
""" Override the name set for this collection of endpoints """
diff --git a/superset/databases/api.py b/superset/databases/api.py
index df69d9ccd7956..8de84a16af6dc 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -111,6 +111,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | {
RouteMethod.EXPORT,
RouteMethod.IMPORT,
+ RouteMethod.RELATED,
"tables",
"table_metadata",
"table_extra_metadata",
@@ -162,6 +163,8 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
"backend",
"changed_on",
"changed_on_delta_humanized",
+ "changed_by.first_name",
+ "changed_by.last_name",
"created_by.first_name",
"created_by.last_name",
"database_name",
@@ -194,7 +197,17 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
edit_columns = add_columns
+ search_columns = [
+ "allow_file_upload",
+ "allow_dml",
+ "allow_run_async",
+ "created_by",
+ "changed_by",
+ "database_name",
+ "expose_in_sqllab",
+ ]
search_filters = {"allow_file_upload": [DatabaseUploadEnabledFilter]}
+ allowed_rel_fields = {"changed_by", "created_by"}
list_select_columns = list_columns + ["extra", "sqlalchemy_uri", "password"]
order_columns = [
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index e256ff99d6d06..bc4a42e58ee7e 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -247,8 +247,17 @@ class DatasetRestApi(BaseSupersetModelRestApi):
"sql": [DatasetIsNullOrEmptyFilter],
"id": [DatasetCertifiedFilter],
}
- search_columns = ["id", "database", "owners", "schema", "sql", "table_name"]
- allowed_rel_fields = {"database", "owners"}
+ search_columns = [
+ "id",
+ "database",
+ "owners",
+ "schema",
+ "sql",
+ "table_name",
+ "created_by",
+ "changed_by",
+ ]
+ allowed_rel_fields = {"database", "owners", "created_by", "changed_by"}
allowed_distinct_fields = {"schema"}
apispec_parameter_schemas = {
diff --git a/superset/queries/saved_queries/api.py b/superset/queries/saved_queries/api.py
index 25ac520e45bc3..ce283dd6d6797 100644
--- a/superset/queries/saved_queries/api.py
+++ b/superset/queries/saved_queries/api.py
@@ -82,7 +82,11 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
base_filters = [["id", SavedQueryFilter, lambda: []]]
show_columns = [
+ "changed_on",
"changed_on_delta_humanized",
+ "changed_by.first_name",
+ "changed_by.id",
+ "changed_by.last_name",
"created_by.first_name",
"created_by.id",
"created_by.last_name",
@@ -97,7 +101,11 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
"template_parameters",
]
list_columns = [
+ "changed_on",
"changed_on_delta_humanized",
+ "changed_by.first_name",
+ "changed_by.id",
+ "changed_by.last_name",
"created_on",
"created_by.first_name",
"created_by.id",
@@ -140,7 +148,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
"last_run_delta_humanized",
]
- search_columns = ["id", "database", "label", "schema", "created_by"]
+ search_columns = ["id", "database", "label", "schema", "created_by", "changed_by"]
if is_feature_enabled("TAGGING_SYSTEM"):
search_columns += ["tags"]
search_filters = {
@@ -161,7 +169,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
"database": "database_name",
}
base_related_field_filters = {"database": [["id", DatabaseFilter, lambda: []]]}
- allowed_rel_fields = {"database"}
+ allowed_rel_fields = {"database", "changed_by", "created_by"}
allowed_distinct_fields = {"schema"}
def pre_add(self, item: SavedQuery) -> None:
diff --git a/superset/reports/api.py b/superset/reports/api.py
index ab4f80ae15ed8..8238213fefd72 100644
--- a/superset/reports/api.py
+++ b/superset/reports/api.py
@@ -198,6 +198,7 @@ def ensure_alert_reports_enabled(self) -> Optional[Response]:
search_columns = [
"name",
"active",
+ "changed_by",
"created_by",
"owners",
"type",
@@ -207,7 +208,14 @@ def ensure_alert_reports_enabled(self) -> Optional[Response]:
"chart_id",
]
search_filters = {"name": [ReportScheduleAllTextFilter]}
- allowed_rel_fields = {"owners", "chart", "dashboard", "database", "created_by"}
+ allowed_rel_fields = {
+ "owners",
+ "chart",
+ "dashboard",
+ "database",
+ "created_by",
+ "changed_by",
+ }
base_related_field_filters = {
"chart": [["id", ChartFilter, lambda: []]],
diff --git a/superset/row_level_security/api.py b/superset/row_level_security/api.py
index e7347f5280328..fc505e724ffa3 100644
--- a/superset/row_level_security/api.py
+++ b/superset/row_level_security/api.py
@@ -77,6 +77,9 @@ class RLSRestApi(BaseSupersetModelRestApi):
"roles.name",
"clause",
"changed_on_delta_humanized",
+ "changed_by.first_name",
+ "changed_by.last_name",
+ "changed_by.id",
"group_key",
]
order_columns = [
@@ -115,6 +118,8 @@ class RLSRestApi(BaseSupersetModelRestApi):
"roles",
"group_key",
"clause",
+ "created_by",
+ "changed_by",
)
edit_columns = add_columns
@@ -123,7 +128,7 @@ class RLSRestApi(BaseSupersetModelRestApi):
add_model_schema = RLSPostSchema()
edit_model_schema = RLSPutSchema()
- allowed_rel_fields = {"tables", "roles"}
+ allowed_rel_fields = {"tables", "roles", "created_by", "changed_by"}
base_related_field_filters = {
"tables": [["id", DatasourceFilter, lambda: []]],
"roles": [["id", BaseFilterRelatedRoles, lambda: []]],
diff --git a/superset/row_level_security/schemas.py b/superset/row_level_security/schemas.py
index 6c8249b875a05..f02767ec1334d 100644
--- a/superset/row_level_security/schemas.py
+++ b/superset/row_level_security/schemas.py
@@ -20,6 +20,7 @@
from marshmallow.validate import Length, OneOf
from superset.connectors.sqla.models import RowLevelSecurityFilter
+from superset.dashboards.schemas import UserSchema
from superset.utils.core import RowLevelSecurityFilterType
id_description = "Unique if of rls filter"
@@ -81,6 +82,7 @@ class RLSListSchema(Schema):
)
group_key = fields.String(metadata={"description": "group_key_description"})
description = fields.String(metadata={"description": "description_description"})
+ changed_by = fields.Nested(UserSchema(exclude=["username"]))
class RLSShowSchema(Schema):
diff --git a/superset/tags/api.py b/superset/tags/api.py
index a3c95a5814094..c0df921e3ebf1 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -117,7 +117,7 @@ class TagRestApi(BaseSupersetModelRestApi):
related_field_filters = {
"created_by": RelatedFieldFilter("first_name", FilterRelatedOwners),
}
- allowed_rel_fields = {"created_by"}
+ allowed_rel_fields = {"created_by", "changed_by"}
add_model_schema = TagPostSchema()
edit_model_schema = TagPutSchema()
diff --git a/tests/integration_tests/css_templates/api_tests.py b/tests/integration_tests/css_templates/api_tests.py
index b28cca955ca8d..ceb46f553b1aa 100644
--- a/tests/integration_tests/css_templates/api_tests.py
+++ b/tests/integration_tests/css_templates/api_tests.py
@@ -19,6 +19,8 @@
import json
import pytest
import prison
+from datetime import datetime
+from freezegun import freeze_time
from sqlalchemy.sql import func
import tests.integration_tests.test_app
@@ -189,20 +191,27 @@ def test_get_css_template(self):
"""
CSS Template API: Test get CSS Template
"""
- css_template = (
- db.session.query(CssTemplate)
- .filter(CssTemplate.template_name == "template_name1")
- .one_or_none()
- )
- self.login(username="admin")
- uri = f"api/v1/css_template/{css_template.id}"
- rv = self.get_assert_metric(uri, "get")
+ with freeze_time(datetime.now()):
+ css_template = (
+ db.session.query(CssTemplate)
+ .filter(CssTemplate.template_name == "template_name1")
+ .one_or_none()
+ )
+ self.login(username="admin")
+ uri = f"api/v1/css_template/{css_template.id}"
+ rv = self.get_assert_metric(uri, "get")
assert rv.status_code == 200
expected_result = {
"id": css_template.id,
"template_name": "template_name1",
"css": "css1",
+ "changed_by": {
+ "first_name": css_template.created_by.first_name,
+ "id": css_template.created_by.id,
+ "last_name": css_template.created_by.last_name,
+ },
+ "changed_on_delta_humanized": "now",
"created_by": {
"first_name": css_template.created_by.first_name,
"id": css_template.created_by.id,
diff --git a/tests/integration_tests/databases/api_tests.py b/tests/integration_tests/databases/api_tests.py
index 6e6f6e15b8112..d01013aa46deb 100644
--- a/tests/integration_tests/databases/api_tests.py
+++ b/tests/integration_tests/databases/api_tests.py
@@ -197,6 +197,7 @@ def test_get_items(self):
"allows_subquery",
"allows_virtual_table_explore",
"backend",
+ "changed_by",
"changed_on",
"changed_on_delta_humanized",
"created_by",
diff --git a/tests/integration_tests/queries/saved_queries/api_tests.py b/tests/integration_tests/queries/saved_queries/api_tests.py
index 09929e4d231bd..c51c0dcbf09ca 100644
--- a/tests/integration_tests/queries/saved_queries/api_tests.py
+++ b/tests/integration_tests/queries/saved_queries/api_tests.py
@@ -17,6 +17,7 @@
# isort:skip_file
"""Unit tests for Superset"""
import json
+from datetime import datetime
from io import BytesIO
from typing import Optional
from zipfile import is_zipfile, ZipFile
@@ -24,6 +25,7 @@
import yaml
import pytest
import prison
+from freezegun import freeze_time
from sqlalchemy.sql import func, and_
import tests.integration_tests.test_app
@@ -507,14 +509,17 @@ def test_get_saved_query(self):
db.session.query(SavedQuery).filter(SavedQuery.label == "label1").all()[0]
)
self.login(username="admin")
- uri = f"api/v1/saved_query/{saved_query.id}"
- rv = self.get_assert_metric(uri, "get")
- assert rv.status_code == 200
+ with freeze_time(datetime.now()):
+ uri = f"api/v1/saved_query/{saved_query.id}"
+ rv = self.get_assert_metric(uri, "get")
+ assert rv.status_code == 200
expected_result = {
"id": saved_query.id,
"database": {"id": saved_query.database.id, "database_name": "examples"},
"description": "cool description",
+ "changed_by": None,
+ "changed_on_delta_humanized": "now",
"created_by": {
"first_name": saved_query.created_by.first_name,
"id": saved_query.created_by.id,
@@ -527,9 +532,8 @@ def test_get_saved_query(self):
"template_parameters": None,
}
data = json.loads(rv.data.decode("utf-8"))
- self.assertIn("changed_on_delta_humanized", data["result"])
for key, value in data["result"].items():
- if key not in ("changed_on_delta_humanized",):
+ if key != "changed_on":
assert value == expected_result[key]
def test_get_saved_query_not_found(self):