mirror of https://github.com/apache/superset.git
chore: harmonize and clean up list views (#25961)
This commit is contained in:
parent
3ab27c6ec9
commit
0b477e3f7c
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 },
|
||||
);
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
|
|
@ -154,7 +154,7 @@ function AnnotationList({
|
|||
() => [
|
||||
{
|
||||
accessor: 'short_descr',
|
||||
Header: t('Label'),
|
||||
Header: t('Name'),
|
||||
},
|
||||
{
|
||||
accessor: 'long_descr',
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
|
|
|
@ -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)"`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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('');
|
||||
});
|
|
@ -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}`;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 """
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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: []]],
|
||||
|
|
|
@ -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: []]],
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -197,6 +197,7 @@ class TestDatabaseApi(SupersetTestCase):
|
|||
"allows_subquery",
|
||||
"allows_virtual_table_explore",
|
||||
"backend",
|
||||
"changed_by",
|
||||
"changed_on",
|
||||
"changed_on_delta_humanized",
|
||||
"created_by",
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue