chore: harmonize and clean up list views (#25961)

This commit is contained in:
Ville Brofeldt 2023-12-04 11:51:18 -08:00 committed by GitHub
parent 3ab27c6ec9
commit 0b477e3f7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 785 additions and 667 deletions

View File

@ -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');
});

View File

@ -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');
});

View File

@ -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');
});
});

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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(<ModifiedInfo user={USER} date={TEST_DATE} />);
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(<ModifiedInfo date={TEST_DATE} />);
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 },
);
});

View File

@ -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 = (
<span className="no-wrap" data-test="audit-info-date">
{date}
</span>
);
if (user) {
const userName = getOwnerName(user);
const title = t('Modified by: %s', userName);
return (
<Tooltip title={title} placement="bottom">
{dateSpan}
</Tooltip>
);
}
return dateSpan;
};

View File

@ -1114,7 +1114,7 @@ class DatasourceEditor extends React.PureComponent {
<div css={{ width: 'calc(100% - 34px)', marginTop: -16 }}>
<Field
fieldKey="table_name"
label={t('Dataset name')}
label={t('Name')}
control={
<TextControl
controlId="table_name"

View File

@ -681,7 +681,7 @@ const PropertiesModal = ({
</Row>
<Row gutter={16}>
<Col xs={24} md={12}>
<FormItem label={t('Title')} name="title">
<FormItem label={t('Name')} name="title">
<Input
data-test="dashboard-title-input"
type="text"

View File

@ -287,7 +287,7 @@ const AnnotationModal: FunctionComponent<AnnotationModalProps> = ({
</StyledAnnotationTitle>
<AnnotationContainer>
<div className="control-label">
{t('Annotation name')}
{t('Name')}
<span className="required">*</span>
</div>
<input

View File

@ -105,6 +105,9 @@ const CssTemplateModal: FunctionComponent<CssTemplateModalProps> = ({
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<CssTemplateModalProps> = ({
</StyledCssTemplateTitle>
<TemplateContainer>
<div className="control-label">
{t('CSS template name')}
{t('Name')}
<span className="required">*</span>
</div>
<input

View File

@ -1,3 +1,5 @@
import Owner from 'src/types/Owner';
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@ -16,17 +18,12 @@
* specific language governing permissions and limitations
* under the License.
*/
type CreatedByUser = {
id: number;
first_name: string;
last_name: string;
};
export type TemplateObject = {
id?: number;
changed_on_delta_humanized?: string;
created_on?: string;
created_by?: CreatedByUser;
changed_by?: Owner;
created_by?: Owner;
css?: string;
template_name: string;
};

View File

@ -56,10 +56,12 @@ test('renders correctly in edit mode', () => {
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',
},

View File

@ -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) => <span className="no-wrap">{changedOn}</span>,
Header: t('Modified'),
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
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,
},
],
[],

View File

@ -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);

View File

@ -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) => <ModifiedInfo date={changedOn} user={changedBy} />,
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,
},
],
[],
);

View File

@ -154,7 +154,7 @@ function AnnotationList({
() => [
{
accessor: 'short_descr',
Header: t('Label'),
Header: t('Name'),
},
{
accessor: 'long_descr',

View File

@ -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) {
)}
</FlexRowContainer>
),
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) => <FacePile users={owners} />,
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) => (
<span className="no-wrap">
{lastSavedAt ? moment.utc(lastSavedAt).fromNow() : null}
</span>
),
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
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]);

View File

@ -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 (
<Tooltip
id="allow-run-async-header-tooltip"
title={t('Last modified by %s', name)}
placement="right"
>
<span>{changedOn}</span>
</Tooltip>
);
},
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
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,
},
],
[],
);

View File

@ -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<any, UserWithPermissionsAndRoles>(
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 && (
<FaveStar
itemId={id}
saveFaveStar={saveFavoriteStatus}
@ -285,7 +285,7 @@ function DashboardList(props: DashboardListProps) {
id: 'id',
disableSortBy: true,
size: 'xs',
hidden: !userId,
hidden: !user?.userId,
},
{
Cell: ({
@ -310,9 +310,20 @@ function DashboardList(props: DashboardListProps) {
{dashboardTitle}
</Link>
),
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: {
@ -338,49 +349,6 @@ function DashboardList(props: DashboardListProps) {
disableSortBy: true,
hidden: !isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM),
},
{
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) => <span className="no-wrap">{changedOn}</span>,
Header: t('Modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
{
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: {
@ -392,6 +360,19 @@ function DashboardList(props: DashboardListProps) {
disableSortBy: true,
size: 'xl',
},
{
Cell: ({
row: {
original: {
changed_on_delta_humanized: changedOn,
changed_by: changedBy,
},
},
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
{
Cell: ({ row: { original } }: any) => {
const handleDelete = () =>
@ -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);
}}

View File

@ -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)"`,
);
});

View File

@ -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 ? <IconCheck /> : <IconCancelX />;
}
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<any, UserWithPermissionsAndRoles>(
const fullUser = useSelector<any, UserWithPermissionsAndRoles>(
state => state.user,
);
const showDatabaseModal = getUrlParam(URL_PARAMS.showDatabaseModal);
@ -123,11 +138,11 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
null,
);
const [allowUploads, setAllowUploads] = useState<boolean>(false);
const isAdmin = isUserAdmin(user);
const isAdmin = isUserAdmin(fullUser);
const showUploads = allowUploads || isAdmin;
const [preparingExport, setPreparingExport] = useState<boolean>(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) => <ModifiedInfo date={changedOn} user={changedBy} />,
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,
},
],
[],

View File

@ -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',
);
});
});

View File

@ -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<DatasetListProps> = ({
accessor: 'schema',
size: 'lg',
},
{
Cell: ({
row: {
original: { changed_on_delta_humanized: changedOn },
},
}: any) => <span className="no-wrap">{changedOn}</span>,
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<DatasetListProps> = ({
disableSortBy: true,
size: 'lg',
},
{
Cell: ({
row: {
original: {
changed_on_delta_humanized: changedOn,
changed_by: changedBy,
},
},
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
{
accessor: 'sql',
hidden: true,
@ -515,6 +510,10 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
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<DatasetListProps> = ({
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<DatasetListProps> = ({
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<DatasetListProps> = ({
{ 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],
);

View File

@ -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,

View File

@ -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();

View File

@ -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) => <span className="no-wrap">{changedOn}</span>,
Header: t('Modified'),
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
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],
);

View File

@ -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) => <ModifiedInfo user={changedBy} date={changedOn} />,
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 (
<>
<SubMenu {...menuData} />

View File

@ -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) => <span className="no-wrap">{changedOn}</span>,
Header: t('Modified'),
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
{
Cell: ({
row: {
original: { created_by: createdBy },
},
}: any) => (createdBy ? <FacePile users={[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]);

View File

@ -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('');
});

View File

@ -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}`;
}

View File

@ -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;
}

View File

@ -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,

View File

@ -273,7 +273,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
"created_by": RelatedFieldFilter("first_name", FilterRelatedOwners),
}
allowed_rel_fields = {"owners", "created_by"}
allowed_rel_fields = {"owners", "created_by", "changed_by"}
@expose("/", methods=("POST",))
@protect()

View File

@ -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,

View File

@ -261,7 +261,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"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 """

View File

@ -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 = [

View File

@ -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 = {

View File

@ -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:

View File

@ -198,6 +198,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
search_columns = [
"name",
"active",
"changed_by",
"created_by",
"owners",
"type",
@ -207,7 +208,14 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
"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: []]],

View File

@ -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: []]],

View File

@ -20,6 +20,7 @@ from marshmallow import fields, Schema
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):

View File

@ -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()

View File

@ -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 @@ class TestCssTemplateApi(SupersetTestCase):
"""
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,

View File

@ -197,6 +197,7 @@ class TestDatabaseApi(SupersetTestCase):
"allows_subquery",
"allows_virtual_table_explore",
"backend",
"changed_by",
"changed_on",
"changed_on_delta_humanized",
"created_by",

View File

@ -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 @@ from zipfile import is_zipfile, ZipFile
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 @@ class TestSavedQueryApi(SupersetTestCase):
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 @@ class TestSavedQueryApi(SupersetTestCase):
"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):