refactor: Refactor reports for Charts and Dashboards (#19130)

* bumping shillelagh

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor: Arash/new state report (#16987)

* code dry (#16358)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor(reports): Arash/refactor reports (#16855)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor: Reports - ReportModal (#16622)

* refactoring progress

* removed consoles

* Working, but with 2 fetches

* report pickup

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* refactor(reports):  Arash/again refactor reports (#16872)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor: Reports - ReportModal (#16622)

* refactoring progress

* removed consoles

* Working, but with 2 fetches

* it is still not working

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* next changes

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* refactor: Reports code clean 10-29 (#17424)

* Add delete functionality

* Report schema restructure progress

* Fix lint

* Removed console.log

* fix(Explore): Remove changes to the properties on cancel (#17184)

* Remove on close

* Fix lint

* Add tests

* fix(dashboard): don't show report modal for anonymous user (#17106)

* Added sunburst echart

* fix(dashboard):Hide reports modal for anonymous users

* Address comments

* Make prettier happy

Co-authored-by: Mayur <mayurp@kpmg.com>

* fix(explore): Metric control breaks when saved metric deleted from dataset (#17503)

* Add functionality is now working (#17578)

* Preset io ch28954 refactor reports (#19129)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor: Arash/new state report (#16987)

* code dry (#16358)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor(reports): Arash/refactor reports (#16855)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor: Reports - ReportModal (#16622)

* refactoring progress

* removed consoles

* Working, but with 2 fetches

* report pickup

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* refactor(reports):  Arash/again refactor reports (#16872)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor: Reports - ReportModal (#16622)

* refactoring progress

* removed consoles

* Working, but with 2 fetches

* it is still not working

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* next changes

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* refactor: Reports code clean 10-29 (#17424)

* Add delete functionality

* Report schema restructure progress

* Fix lint

* Removed console.log

* fix(Explore): Remove changes to the properties on cancel (#17184)

* Remove on close

* Fix lint

* Add tests

* fix(dashboard): don't show report modal for anonymous user (#17106)

* Added sunburst echart

* fix(dashboard):Hide reports modal for anonymous users

* Address comments

* Make prettier happy

Co-authored-by: Mayur <mayurp@kpmg.com>

* fix(explore): Metric control breaks when saved metric deleted from dataset (#17503)

* Add functionality is now working (#17578)

* refactoring reports

* ready for review

* added testing

* removed user reducer

* elizabeth suggestions

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>
Co-authored-by: Geido <60598000+geido@users.noreply.github.com>
Co-authored-by: Mayur <mayurnewase111@gmail.com>
Co-authored-by: Mayur <mayurp@kpmg.com>
Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>

* deleted additional folder

* fixing tests

* all but styling

* bumping shillelagh

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor: Arash/new state report (#16987)

* code dry (#16358)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor(reports): Arash/refactor reports (#16855)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor: Reports - ReportModal (#16622)

* refactoring progress

* removed consoles

* Working, but with 2 fetches

* report pickup

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* refactor(reports):  Arash/again refactor reports (#16872)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor: Reports - ReportModal (#16622)

* refactoring progress

* removed consoles

* Working, but with 2 fetches

* it is still not working

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* next changes

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* refactor: Reports code clean 10-29 (#17424)

* Add delete functionality

* Report schema restructure progress

* Fix lint

* Removed console.log

* fix(Explore): Remove changes to the properties on cancel (#17184)

* Remove on close

* Fix lint

* Add tests

* fix(dashboard): don't show report modal for anonymous user (#17106)

* Added sunburst echart

* fix(dashboard):Hide reports modal for anonymous users

* Address comments

* Make prettier happy

Co-authored-by: Mayur <mayurp@kpmg.com>

* fix(explore): Metric control breaks when saved metric deleted from dataset (#17503)

* Add functionality is now working (#17578)

* Preset io ch28954 refactor reports (#19129)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor: Arash/new state report (#16987)

* code dry (#16358)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor(reports): Arash/refactor reports (#16855)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor: Reports - ReportModal (#16622)

* refactoring progress

* removed consoles

* Working, but with 2 fetches

* report pickup

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* refactor(reports):  Arash/again refactor reports (#16872)

* pexdax refactor (#16333)

* refactor progress (#16339)

* fix: Header Actions test refactor (#16336)

* fixed tests

* Update index.tsx

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* code dry (#16358)

* Fetch bug fixed (#16376)

* continued refactoring (#16377)

* refactor: Reports - ReportModal (#16622)

* refactoring progress

* removed consoles

* Working, but with 2 fetches

* it is still not working

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* next changes

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>

* refactor: Reports code clean 10-29 (#17424)

* Add delete functionality

* Report schema restructure progress

* Fix lint

* Removed console.log

* fix(Explore): Remove changes to the properties on cancel (#17184)

* Remove on close

* Fix lint

* Add tests

* fix(dashboard): don't show report modal for anonymous user (#17106)

* Added sunburst echart

* fix(dashboard):Hide reports modal for anonymous users

* Address comments

* Make prettier happy

Co-authored-by: Mayur <mayurp@kpmg.com>

* fix(explore): Metric control breaks when saved metric deleted from dataset (#17503)

* Add functionality is now working (#17578)

* refactoring reports

* ready for review

* added testing

* removed user reducer

* elizabeth suggestions

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>
Co-authored-by: Geido <60598000+geido@users.noreply.github.com>
Co-authored-by: Mayur <mayurnewase111@gmail.com>
Co-authored-by: Mayur <mayurp@kpmg.com>
Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>

* deleted additional folder

* fixing tests

* all but styling

* fixed tests

Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com>
Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>
Co-authored-by: Geido <60598000+geido@users.noreply.github.com>
Co-authored-by: Mayur <mayurnewase111@gmail.com>
Co-authored-by: Mayur <mayurp@kpmg.com>
Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>
This commit is contained in:
AAfghahi 2022-05-11 14:19:51 -04:00 committed by GitHub
parent 8bb8b7f612
commit fd611d7653
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 816 additions and 751 deletions

View File

@ -103,7 +103,7 @@ describe('Dashboard edit action', () => {
.click()
.then(() => {
// assert that modal edit window has closed
cy.get('.ant-modal-body').should('not.be.visible');
cy.get('.ant-modal-body').should('not.exist');
// assert title has been updated
cy.get('.editable-title input').should('have.value', dashboardTitle);

View File

@ -1,116 +0,0 @@
/**
* 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 React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { t, SupersetTheme, css, useTheme } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { Switch } from 'src/components/Switch';
import { AlertObject } from 'src/views/CRUD/alert/types';
import { Menu } from 'src/components/Menu';
import { NoAnimationDropdown } from 'src/components/Dropdown';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import DeleteModal from 'src/components/DeleteModal';
const deleteColor = (theme: SupersetTheme) => css`
color: ${theme.colors.error.base};
`;
export default function HeaderReportActionsDropDown({
showReportModal,
toggleActive,
deleteActiveReport,
}: {
showReportModal: () => void;
toggleActive: (data: AlertObject, checked: boolean) => void;
deleteActiveReport: (data: AlertObject) => void;
}) {
const reports = useSelector<any, AlertObject>(state => state.reports);
const reportsIds = Object.keys(reports);
const report = reports[reportsIds[0]];
const [currentReportDeleting, setCurrentReportDeleting] =
useState<AlertObject | null>(null);
const theme = useTheme();
const toggleActiveKey = async (data: AlertObject, checked: boolean) => {
if (data?.id) {
toggleActive(data, checked);
}
};
const handleReportDelete = (report: AlertObject) => {
deleteActiveReport(report);
setCurrentReportDeleting(null);
};
const menu = () => (
<Menu selectable={false} css={{ width: '200px' }}>
<Menu.Item>
{t('Email reports active')}
<Switch
data-test="toggle-active"
checked={report?.active}
onClick={(checked: boolean) => toggleActiveKey(report, checked)}
size="small"
css={{ marginLeft: theme.gridUnit * 2 }}
/>
</Menu.Item>
<Menu.Item onClick={showReportModal}>{t('Edit email report')}</Menu.Item>
<Menu.Item
onClick={() => setCurrentReportDeleting(report)}
css={deleteColor}
>
{t('Delete email report')}
</Menu.Item>
</Menu>
);
return isFeatureEnabled(FeatureFlag.ALERT_REPORTS) ? (
<>
<NoAnimationDropdown
// ref={ref}
overlay={menu()}
trigger={['click']}
getPopupContainer={(triggerNode: any) =>
triggerNode.closest('.action-button')
}
>
<span role="button" className="action-button" tabIndex={0}>
<Icons.Calendar />
</span>
</NoAnimationDropdown>
{currentReportDeleting && (
<DeleteModal
description={t(
'This action will permanently delete %s.',
currentReportDeleting.name,
)}
onConfirm={() => {
if (currentReportDeleting) {
handleReportDelete(currentReportDeleting);
}
}}
onHide={() => setCurrentReportDeleting(null)}
open
title={t('Delete Report?')}
/>
)}
</>
) : null;
}

View File

@ -0,0 +1,198 @@
/**
* 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 * as React from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen, act } from 'spec/helpers/testing-library';
import * as featureFlags from '@superset-ui/core';
import HeaderReportDropdown, { HeaderReportProps } from '.';
let isFeatureEnabledMock: jest.MockInstance<boolean, [string]>;
const createProps = () => ({
dashboardId: 1,
useTextMenu: false,
isDropdownVisible: false,
setIsDropdownVisible: jest.fn,
setShowReportSubMenu: jest.fn,
});
const stateWithOnlyUser = {
explore: {
user: {
email: 'admin@test.com',
firstName: 'admin',
isActive: true,
lastName: 'admin',
permissions: {},
createdOn: '2022-01-12T10:17:37.801361',
roles: { Admin: [['menu_access', 'Manage']] },
userId: 1,
username: 'admin',
},
},
reports: {},
};
const stateWithUserAndReport = {
explore: {
user: {
email: 'admin@test.com',
firstName: 'admin',
isActive: true,
lastName: 'admin',
permissions: {},
createdOn: '2022-01-12T10:17:37.801361',
roles: { Admin: [['menu_access', 'Manage']] },
userId: 1,
username: 'admin',
},
},
reports: {
dashboards: {
1: {
id: 1,
result: {
active: true,
creation_method: 'dashboards',
crontab: '0 12 * * 1',
dashboard: 1,
name: 'Weekly Report',
owners: [1],
recipients: [
{
recipient_config_json: {
target: 'admin@test.com',
},
type: 'Email',
},
],
type: 'Report',
},
},
},
},
};
function setup(props: HeaderReportProps, initialState = {}) {
render(
<div>
<HeaderReportDropdown {...props} />
</div>,
{ useRedux: true, initialState },
);
}
describe('Header Report Dropdown', () => {
beforeAll(() => {
isFeatureEnabledMock = jest
.spyOn(featureFlags, 'isFeatureEnabled')
.mockImplementation(
(featureFlag: featureFlags.FeatureFlag) =>
featureFlag === featureFlags.FeatureFlag.ALERT_REPORTS,
);
});
afterAll(() => {
// @ts-ignore
isFeatureEnabledMock.restore();
});
it('renders correctly', () => {
const mockedProps = createProps();
act(() => {
setup(mockedProps, stateWithUserAndReport);
});
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('renders the dropdown correctly', () => {
const mockedProps = createProps();
act(() => {
setup(mockedProps, stateWithUserAndReport);
});
const emailReportModalButton = screen.getByRole('button');
userEvent.click(emailReportModalButton);
expect(screen.getByText('Email reports active')).toBeInTheDocument();
expect(screen.getByText('Edit email report')).toBeInTheDocument();
expect(screen.getByText('Delete email report')).toBeInTheDocument();
});
it('opens an edit modal', async () => {
const mockedProps = createProps();
act(() => {
setup(mockedProps, stateWithUserAndReport);
});
const emailReportModalButton = screen.getByRole('button');
userEvent.click(emailReportModalButton);
const editModal = screen.getByText('Edit email report');
userEvent.click(editModal);
const textBoxes = await screen.findAllByText('Edit email report');
expect(textBoxes).toHaveLength(2);
});
it('opens a delete modal', () => {
const mockedProps = createProps();
act(() => {
setup(mockedProps, stateWithUserAndReport);
});
const emailReportModalButton = screen.getByRole('button');
userEvent.click(emailReportModalButton);
const deleteModal = screen.getByText('Delete email report');
userEvent.click(deleteModal);
expect(screen.getByText('Delete Report?')).toBeInTheDocument();
});
it('renders a new report modal if there is no report', () => {
const mockedProps = createProps();
act(() => {
setup(mockedProps, stateWithOnlyUser);
});
const emailReportModalButton = screen.getByRole('button');
userEvent.click(emailReportModalButton);
expect(screen.getByText('Schedule a new email report')).toBeInTheDocument();
});
it('renders Manage Email Reports Menu if textMenu is set to true and there is a report', () => {
let mockedProps = createProps();
mockedProps = {
...mockedProps,
useTextMenu: true,
isDropdownVisible: true,
};
act(() => {
setup(mockedProps, stateWithUserAndReport);
});
expect(screen.getByText('Email reports active')).toBeInTheDocument();
expect(screen.getByText('Edit email report')).toBeInTheDocument();
expect(screen.getByText('Delete email report')).toBeInTheDocument();
});
it('renders Schedule Email Reports if textMenu is set to true and there is a report', () => {
let mockedProps = createProps();
mockedProps = {
...mockedProps,
useTextMenu: true,
isDropdownVisible: true,
};
act(() => {
setup(mockedProps, stateWithOnlyUser);
});
expect(screen.getByText('Set up an email report')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,299 @@
/**
* 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 React, { useState, useEffect } from 'react';
import { usePrevious } from 'src/hooks/usePrevious';
import { useSelector, useDispatch } from 'react-redux';
import {
t,
SupersetTheme,
css,
useTheme,
FeatureFlag,
isFeatureEnabled,
} from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { Switch } from 'src/components/Switch';
import { AlertObject } from 'src/views/CRUD/alert/types';
import { Menu } from 'src/components/Menu';
import Checkbox from 'src/components/Checkbox';
import { noOp } from 'src/utils/common';
import { NoAnimationDropdown } from 'src/components/Dropdown';
import DeleteModal from 'src/components/DeleteModal';
import ReportModal from 'src/components/ReportModal';
import { ChartState } from 'src/explore/types';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import {
fetchUISpecificReport,
toggleActive,
deleteActiveReport,
} from 'src/reports/actions/reports';
import { reportSelector } from 'src/views/CRUD/hooks';
import { MenuItemWithCheckboxContainer } from 'src/explore/components/ExploreAdditionalActionsMenu/index';
const deleteColor = (theme: SupersetTheme) => css`
color: ${theme.colors.error.base};
`;
const onMenuHover = (theme: SupersetTheme) => css`
& .ant-menu-item {
padding: 5px 12px;
margin-top: 0px;
margin-bottom: 4px;
:hover {
color: ${theme.colors.grayscale.dark1};
}
}
:hover {
background-color: ${theme.colors.secondary.light5};
}
`;
const onMenuItemHover = (theme: SupersetTheme) => css`
&:hover {
color: ${theme.colors.grayscale.dark1};
background-color: ${theme.colors.secondary.light5};
}
`;
export enum CreationMethod {
CHARTS = 'charts',
DASHBOARDS = 'dashboards',
}
export interface HeaderReportProps {
dashboardId?: number;
chart?: ChartState;
useTextMenu?: boolean;
setShowReportSubMenu?: (show: boolean) => void;
setIsDropdownVisible?: (visible: boolean) => void;
isDropdownVisible?: boolean;
showReportSubMenu?: boolean;
}
export default function HeaderReportDropDown({
dashboardId,
chart,
useTextMenu = false,
setShowReportSubMenu,
setIsDropdownVisible,
isDropdownVisible,
}: HeaderReportProps) {
const dispatch = useDispatch();
const report = useSelector<any, AlertObject>(state => {
const resourceType = dashboardId
? CreationMethod.DASHBOARDS
: CreationMethod.CHARTS;
return reportSelector(state, resourceType, dashboardId || chart?.id);
});
const isReportActive: boolean = report?.active || false;
const user: UserWithPermissionsAndRoles = useSelector<
any,
UserWithPermissionsAndRoles
>(state => state.user || state.explore?.user);
const canAddReports = () => {
if (!isFeatureEnabled(FeatureFlag.ALERT_REPORTS)) {
return false;
}
if (!user?.userId) {
// this is in the case that there is an anonymous user.
return false;
}
const roles = Object.keys(user.roles || []);
const permissions = roles.map(key =>
user.roles[key].filter(
perms => perms[0] === 'menu_access' && perms[1] === 'Manage',
),
);
return permissions[0].length > 0;
};
const [currentReportDeleting, setCurrentReportDeleting] =
useState<AlertObject | null>(null);
const theme = useTheme();
const prevDashboard = usePrevious(dashboardId);
const [showModal, setShowModal] = useState<boolean>(false);
const toggleActiveKey = async (data: AlertObject, checked: boolean) => {
if (data?.id) {
dispatch(toggleActive(data, checked));
}
};
const handleReportDelete = async (report: AlertObject) => {
await dispatch(deleteActiveReport(report));
setCurrentReportDeleting(null);
};
const shouldFetch =
canAddReports() &&
!!((dashboardId && prevDashboard !== dashboardId) || chart?.id);
useEffect(() => {
if (shouldFetch) {
dispatch(
fetchUISpecificReport({
userId: user.userId,
filterField: dashboardId ? 'dashboard_id' : 'chart_id',
creationMethod: dashboardId ? 'dashboards' : 'charts',
resourceId: dashboardId || chart?.id,
}),
);
}
}, []);
const showReportSubMenu = report && setShowReportSubMenu && canAddReports();
useEffect(() => {
if (showReportSubMenu) {
setShowReportSubMenu(true);
} else if (!report && setShowReportSubMenu) {
setShowReportSubMenu(false);
}
}, [report]);
const handleShowMenu = () => {
if (setIsDropdownVisible) {
setIsDropdownVisible(false);
setShowModal(true);
}
};
const handleDeleteMenuClick = () => {
if (setIsDropdownVisible) {
setIsDropdownVisible(false);
setCurrentReportDeleting(report);
}
};
const textMenu = () =>
report ? (
isDropdownVisible && (
<Menu selectable={false} css={{ border: 'none' }}>
<Menu.Item
css={onMenuItemHover}
onClick={() => toggleActiveKey(report, !isReportActive)}
>
<MenuItemWithCheckboxContainer>
<Checkbox checked={isReportActive} onChange={noOp} />
{t('Email reports active')}
</MenuItemWithCheckboxContainer>
</Menu.Item>
<Menu.Item css={onMenuItemHover} onClick={handleShowMenu}>
{t('Edit email report')}
</Menu.Item>
<Menu.Item css={onMenuItemHover} onClick={handleDeleteMenuClick}>
{t('Delete email report')}
</Menu.Item>
</Menu>
)
) : (
<Menu selectable={false} css={onMenuHover}>
<Menu.Item onClick={handleShowMenu}>
{t('Set up an email report')}
</Menu.Item>
<Menu.Divider />
</Menu>
);
const menu = () => (
<Menu selectable={false} css={{ width: '200px' }}>
<Menu.Item>
{t('Email reports active')}
<Switch
data-test="toggle-active"
checked={isReportActive}
onClick={(checked: boolean) => toggleActiveKey(report, checked)}
size="small"
css={{ marginLeft: theme.gridUnit * 2 }}
/>
</Menu.Item>
<Menu.Item onClick={() => setShowModal(true)}>
{t('Edit email report')}
</Menu.Item>
<Menu.Item
onClick={() => setCurrentReportDeleting(report)}
css={deleteColor}
>
{t('Delete email report')}
</Menu.Item>
</Menu>
);
const iconMenu = () =>
report ? (
<>
<NoAnimationDropdown
overlay={menu()}
trigger={['click']}
getPopupContainer={(triggerNode: any) =>
triggerNode.closest('.action-button')
}
>
<span role="button" className="action-button" tabIndex={0}>
<Icons.Calendar />
</span>
</NoAnimationDropdown>
</>
) : (
<span
role="button"
title={t('Schedule email report')}
tabIndex={0}
className="action-button"
onClick={() => setShowModal(true)}
>
<Icons.Calendar />
</span>
);
return (
<>
{canAddReports() && (
<>
<ReportModal
userId={user.userId}
show={showModal}
onHide={() => setShowModal(false)}
userEmail={user.email}
dashboardId={dashboardId}
chart={chart}
creationMethod={
dashboardId ? CreationMethod.DASHBOARDS : CreationMethod.CHARTS
}
/>
{useTextMenu ? textMenu() : iconMenu()}
{currentReportDeleting && (
<DeleteModal
description={t(
'This action will permanently delete %s.',
currentReportDeleting?.name,
)}
onConfirm={() => {
if (currentReportDeleting) {
handleReportDelete(currentReportDeleting);
}
}}
onHide={() => setCurrentReportDeleting(null)}
open
title={t('Delete Report?')}
/>
)}
</>
)}
</>
);
}

View File

@ -16,15 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import * as React from 'react';
import userEvent from '@testing-library/user-event';
import sinon from 'sinon';
import fetchMock from 'fetch-mock';
import { render, screen } from 'spec/helpers/testing-library';
import * as featureFlags from 'src/featureFlags';
import * as actions from 'src/reports/actions/reports';
import { FeatureFlag } from '@superset-ui/core';
import ReportModal from '.';
let isFeatureEnabledMock: jest.MockInstance<boolean, [string]>;
const REPORT_ENDPOINT = 'glob:*/api/v1/report*';
fetchMock.get(REPORT_ENDPOINT, {});
const NOOP = () => {};
const defaultProps = {
@ -37,12 +43,10 @@ const defaultProps = {
userId: 1,
userEmail: 'test@test.com',
dashboardId: 1,
creationMethod: 'charts_dashboards',
props: {
chart: {
sliceFormData: {
viz_type: 'table',
},
creationMethod: 'dashboards',
chart: {
sliceFormData: {
viz_type: 'table',
},
},
};
@ -107,4 +111,69 @@ describe('Email Report Modal', () => {
expect(reportNameTextbox).toHaveDisplayValue('');
expect(addButton).toBeDisabled();
});
describe('Email Report Modal', () => {
let isFeatureEnabledMock: any;
let dispatch: any;
beforeEach(async () => {
isFeatureEnabledMock = jest
.spyOn(featureFlags, 'isFeatureEnabled')
.mockImplementation(() => true);
dispatch = sinon.spy();
});
afterAll(() => {
isFeatureEnabledMock.mockRestore();
});
it('creates a new email report', async () => {
// ---------- Render/value setup ----------
const reportValues = {
id: 1,
result: {
active: true,
creation_method: 'dashboards',
crontab: '0 12 * * 1',
dashboard: 1,
name: 'Weekly Report',
owners: [1],
recipients: [
{
recipient_config_json: {
target: 'test@test.com',
},
type: 'Email',
},
],
type: 'Report',
},
};
// This is needed to structure the reportValues to match the fetchMock return
const stringyReportValues = `{"id":1,"result":{"active":true,"creation_method":"dashboards","crontab":"0 12 * * 1","dashboard":${1},"name":"Weekly Report","owners":[${1}],"recipients":[{"recipient_config_json":{"target":"test@test.com"},"type":"Email"}],"type":"Report"}}`;
// Watch for report POST
fetchMock.post(REPORT_ENDPOINT, reportValues);
// Click "Add" button to create a new email report
const addButton = screen.getByRole('button', { name: /add/i });
userEvent.click(addButton);
// Mock addReport from Redux
const makeRequest = () => {
const request = actions.addReport(reportValues);
return request(dispatch);
};
return makeRequest().then(() => {
// 🐞 ----- There are 2 POST calls at this point ----- 🐞
// addReport's mocked POST return should match the mocked values
expect(fetchMock.lastOptions()?.body).toEqual(stringyReportValues);
// Dispatch should be called once for addReport
expect(dispatch.callCount).toBe(2);
const reportCalls = fetchMock.calls(REPORT_ENDPOINT);
expect(reportCalls).toHaveLength(2);
});
});
});
});

View File

@ -20,28 +20,28 @@ import React, {
useState,
useEffect,
useReducer,
FunctionComponent,
useCallback,
useMemo,
} from 'react';
import { t, SupersetTheme } from '@superset-ui/core';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { useDispatch, useSelector } from 'react-redux';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { addReport, editReport } from 'src/reports/actions/reports';
import Alert from 'src/components/Alert';
import TimezoneSelector from 'src/components/TimezoneSelector';
import LabeledErrorBoundInput from 'src/components/Form/LabeledErrorBoundInput';
import Icons from 'src/components/Icons';
import withToasts from 'src/components/MessageToasts/withToasts';
import { CronError } from 'src/components/CronPicker';
import { RadioChangeEvent } from 'src/components';
import withToasts from 'src/components/MessageToasts/withToasts';
import { ChartState } from 'src/explore/types';
import {
ReportCreationMethod,
ReportRecipientType,
ReportScheduleType,
ReportObject,
NOTIFICATION_FORMATS,
} from 'src/reports/types';
import { reportSelector } from 'src/views/CRUD/hooks';
import { CreationMethod } from './HeaderReportDropdown';
import {
antDErrorAlertStyles,
StyledModal,
@ -60,32 +60,8 @@ import {
StyledRadioGroup,
} from './styles';
export interface ReportObject {
id?: number;
active: boolean;
crontab: string;
dashboard?: number;
chart?: number;
description?: string;
log_retention: number;
name: string;
owners: number[];
recipients: [
{ recipient_config_json: { target: string }; type: ReportRecipientType },
];
report_format: string;
timezone: string;
type: ReportScheduleType;
validator_config_json: {} | null;
validator_type: string;
working_timeout: number;
creation_method: string;
force_screenshot: boolean;
}
interface ReportProps {
onHide: () => {};
onReportAdd: (report?: ReportObject) => {};
addDangerToast: (msg: string) => void;
show: boolean;
userId: number;
@ -95,6 +71,7 @@ interface ReportProps {
dashboardId?: number;
dashboardName?: string;
creationMethod: ReportCreationMethod;
props: any;
}
const TEXT_BASED_VISUALIZATION_TYPES = [
@ -104,12 +81,6 @@ const TEXT_BASED_VISUALIZATION_TYPES = [
'paired_ttest',
];
const NOTIFICATION_FORMATS = {
TEXT: 'TEXT',
PNG: 'PNG',
CSV: 'CSV',
};
const INITIAL_STATE = {
crontab: '0 12 * * 1',
};
@ -122,20 +93,25 @@ type ReportObjectState = Partial<ReportObject> & {
isSubmitting?: boolean;
};
const ReportModal: FunctionComponent<ReportProps> = ({
onReportAdd,
function ReportModal({
onHide,
show = false,
...props
}) => {
const vizType = props.chart?.sliceFormData?.viz_type;
const isChart = !!props.chart;
dashboardId,
chart,
userId,
userEmail,
creationMethod,
dashboardName,
chartName,
}: ReportProps) {
const vizType = chart?.sliceFormData?.viz_type;
const isChart = !!chart;
const isTextBasedChart =
isChart && vizType && TEXT_BASED_VISUALIZATION_TYPES.includes(vizType);
const defaultNotificationFormat = isTextBasedChart
? NOTIFICATION_FORMATS.TEXT
: NOTIFICATION_FORMATS.PNG;
const entityName = props.dashboardName || props.chartName;
const entityName = dashboardName || chartName;
const initialState: ReportObjectState = useMemo(
() => ({
...INITIAL_STATE,
@ -166,18 +142,22 @@ const ReportModal: FunctionComponent<ReportProps> = ({
const [cronError, setCronError] = useState<CronError>();
const dispatch = useDispatch();
const reports = useSelector<any, ReportObject>(state => state.reports);
const isEditMode = reports && Object.keys(reports).length;
// Report fetch logic
const report = useSelector<any, ReportObject>(state => {
const resourceType = dashboardId
? CreationMethod.DASHBOARDS
: CreationMethod.CHARTS;
return reportSelector(state, resourceType, dashboardId || chart?.id);
});
const isEditMode = report && Object.keys(report).length;
useEffect(() => {
if (isEditMode) {
const reportsIds = Object.keys(reports);
const report = reports[reportsIds[0]];
setCurrentReport(report);
} else {
setCurrentReport('reset');
}
}, [isEditMode, reports]);
}, [isEditMode, report]);
const onSave = async () => {
// Create new Report
@ -185,13 +165,13 @@ const ReportModal: FunctionComponent<ReportProps> = ({
type: 'Report',
active: true,
force_screenshot: false,
creation_method: props.creationMethod,
dashboard: props.dashboardId,
chart: props.chart?.id,
owners: [props.userId],
creation_method: creationMethod,
dashboard: dashboardId,
chart: chart?.id,
owners: [userId],
recipients: [
{
recipient_config_json: { target: props.userEmail },
recipient_config_json: { target: userEmail },
type: 'Email',
},
],
@ -217,8 +197,6 @@ const ReportModal: FunctionComponent<ReportProps> = ({
setCurrentReport({ error });
}
setCurrentReport({ isSubmitting: false });
if (onReportAdd) onReportAdd();
};
const wrappedTitle = (
@ -363,6 +341,6 @@ const ReportModal: FunctionComponent<ReportProps> = ({
)}
</StyledModal>
);
};
}
export default withToasts(ReportModal);

View File

@ -19,11 +19,7 @@
import React from 'react';
import { render, screen, fireEvent } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import sinon from 'sinon';
import fetchMock from 'fetch-mock';
import * as actions from 'src/reports/actions/reports';
import * as featureFlags from 'src/featureFlags';
import mockState from 'spec/fixtures/mockStateWithoutUser';
import { HeaderProps } from './types';
import Header from '.';
@ -112,10 +108,7 @@ const redoProps = {
redoLength: 1,
};
const REPORT_ENDPOINT = 'glob:*/api/v1/report*';
fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
fetchMock.get(REPORT_ENDPOINT, {});
function setup(props: HeaderProps, initialState = {}) {
return render(
@ -323,171 +316,3 @@ test('should refresh the charts', async () => {
userEvent.click(screen.getByText('Refresh dashboard'));
expect(mockedProps.onRefresh).toHaveBeenCalledTimes(1);
});
describe('Email Report Modal', () => {
let isFeatureEnabledMock: any;
let dispatch: any;
beforeEach(async () => {
isFeatureEnabledMock = jest
.spyOn(featureFlags, 'isFeatureEnabled')
.mockImplementation(() => true);
dispatch = sinon.spy();
});
afterAll(() => {
isFeatureEnabledMock.mockRestore();
});
it('creates a new email report', async () => {
// ---------- Render/value setup ----------
const mockedProps = createProps();
setup(mockedProps);
const reportValues = {
id: 1,
result: {
active: true,
creation_method: 'dashboards',
crontab: '0 12 * * 1',
dashboard: mockedProps.dashboardInfo.id,
name: 'Weekly Report',
owners: [mockedProps.user.userId],
recipients: [
{
recipient_config_json: {
target: mockedProps.user.email,
},
type: 'Email',
},
],
type: 'Report',
},
};
// This is needed to structure the reportValues to match the fetchMock return
const stringyReportValues = `{"id":1,"result":{"active":true,"creation_method":"dashboards","crontab":"0 12 * * 1","dashboard":${mockedProps.dashboardInfo.id},"name":"Weekly Report","owners":[${mockedProps.user.userId}],"recipients":[{"recipient_config_json":{"target":"${mockedProps.user.email}"},"type":"Email"}],"type":"Report"}}`;
// Watch for report POST
fetchMock.post(REPORT_ENDPOINT, reportValues);
screen.logTestingPlaygroundURL();
// ---------- Begin tests ----------
// Click calendar icon to open email report modal
const emailReportModalButton = screen.getByRole('button', {
name: /schedule email report/i,
});
userEvent.click(emailReportModalButton);
// Click "Add" button to create a new email report
const addButton = screen.getByRole('button', { name: /add/i });
userEvent.click(addButton);
// Mock addReport from Redux
const makeRequest = () => {
const request = actions.addReport(reportValues);
return request(dispatch);
};
return makeRequest().then(() => {
// 🐞 ----- There are 2 POST calls at this point ----- 🐞
// addReport's mocked POST return should match the mocked values
expect(fetchMock.lastOptions()?.body).toEqual(stringyReportValues);
// Dispatch should be called once for addReport
expect(dispatch.callCount).toBe(2);
const reportCalls = fetchMock.calls(REPORT_ENDPOINT);
expect(reportCalls).toHaveLength(2);
});
});
it('edits an existing email report', async () => {
// TODO (lyndsiWilliams): This currently does not work, see TODOs below
// The modal does appear with the edit title, but the PUT call is not registering
// ---------- Render/value setup ----------
const mockedProps = createProps();
const editedReportValues = {
active: true,
creation_method: 'dashboards',
crontab: '0 12 * * 1',
dashboard: mockedProps.dashboardInfo.id,
name: 'Weekly Report edit',
owners: [mockedProps.user.userId],
recipients: [
{
recipient_config_json: {
target: mockedProps.user.email,
},
type: 'Email',
},
],
type: 'Report',
};
// getMockStore({ reports: reportValues });
setup(mockedProps, mockState);
// TODO (lyndsiWilliams): currently fetchMock detects this PUT
// address as 'glob:*/api/v1/report/undefined', is not detected
// on fetchMock.calls()
fetchMock.put(`glob:*/api/v1/report*`, editedReportValues);
// Mock fetchUISpecificReport from Redux
// const makeFetchRequest = () => {
// const request = actions.fetchUISpecificReport(
// mockedProps.user.userId,
// 'dashboard_id',
// 'dashboards',
// mockedProps.dashboardInfo.id,
// );
// return request(dispatch);
// };
// makeFetchRequest();
dispatch(actions.setReport(editedReportValues));
// ---------- Begin tests ----------
// Click calendar icon to open email report modal
const emailReportModalButton = screen.getByRole('button', {
name: /schedule email report/i,
});
userEvent.click(emailReportModalButton);
const nameTextbox = screen.getByTestId('report-name-test');
userEvent.type(nameTextbox, ' edit');
const saveButton = screen.getByRole('button', { name: /save/i });
userEvent.click(saveButton);
// TODO (lyndsiWilliams): There should be a report in state at this porint,
// which would render the HeaderReportActionsDropDown under the calendar icon
// BLOCKER: I cannot get report to populate, as its data is handled through redux
expect.anything();
});
it('Should render report header', async () => {
const mockedProps = createProps();
setup(mockedProps);
expect(
screen.getByRole('button', { name: 'Schedule email report' }),
).toBeInTheDocument();
});
it('Should not render report header even with menu access for anonymous user', async () => {
const mockedProps = createProps();
const anonymousUserProps = {
...mockedProps,
user: {
roles: {
Public: [['menu_access', 'Manage']],
},
permissions: {
datasource_access: ['[examples].[birth_names](id:2)'],
},
},
};
setup(anonymousUserProps);
expect(
screen.queryByRole('button', { name: 'Schedule email report' }),
).not.toBeInTheDocument();
});
});

View File

@ -22,25 +22,22 @@ import React from 'react';
import PropTypes from 'prop-types';
import { styled, t, getSharedLabelColor } from '@superset-ui/core';
import ButtonGroup from 'src/components/ButtonGroup';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import {
LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD,
LOG_ACTIONS_FORCE_REFRESH_DASHBOARD,
LOG_ACTIONS_TOGGLE_EDIT_DASHBOARD,
} from 'src/logger/LogUtils';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import Icons from 'src/components/Icons';
import Button from 'src/components/Button';
import EditableTitle from 'src/components/EditableTitle';
import FaveStar from 'src/components/FaveStar';
import { safeStringify } from 'src/utils/safeStringify';
import HeaderActionsDropdown from 'src/dashboard/components/Header/HeaderActionsDropdown';
import HeaderReportActionsDropdown from 'src/components/ReportModal/HeaderReportActionsDropdown';
import HeaderReportDropdown from 'src/components/ReportModal/HeaderReportDropdown';
import PublishedStatus from 'src/dashboard/components/PublishedStatus';
import UndoRedoKeyListeners from 'src/dashboard/components/UndoRedoKeyListeners';
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
import ReportModal from 'src/components/ReportModal';
import { chartPropShape } from 'src/dashboard/util/propShapes';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import {
@ -78,7 +75,6 @@ const propTypes = {
onChange: PropTypes.func.isRequired,
fetchFaveStar: PropTypes.func.isRequired,
fetchCharts: PropTypes.func.isRequired,
fetchUISpecificReport: PropTypes.func.isRequired,
saveFaveStar: PropTypes.func.isRequired,
savePublished: PropTypes.func.isRequired,
updateDashboardTitle: PropTypes.func.isRequired,
@ -153,7 +149,6 @@ class Header extends React.PureComponent {
didNotifyMaxUndoHistoryToast: false,
emphasizeUndo: false,
showingPropertiesModal: false,
showingReportModal: false,
};
this.handleChangeText = this.handleChangeText.bind(this);
@ -165,26 +160,11 @@ class Header extends React.PureComponent {
this.overwriteDashboard = this.overwriteDashboard.bind(this);
this.showPropertiesModal = this.showPropertiesModal.bind(this);
this.hidePropertiesModal = this.hidePropertiesModal.bind(this);
this.showReportModal = this.showReportModal.bind(this);
this.hideReportModal = this.hideReportModal.bind(this);
this.renderReportModal = this.renderReportModal.bind(this);
}
componentDidMount() {
const { refreshFrequency, user, dashboardInfo } = this.props;
const { refreshFrequency } = this.props;
this.startPeriodicRender(refreshFrequency * 1000);
if (this.canAddReports()) {
// this is in case there is an anonymous user.
if (Object.entries(dashboardInfo).length) {
this.props.fetchUISpecificReport(
user.userId,
'dashboard_id',
'dashboards',
dashboardInfo.id,
user.email,
);
}
}
}
componentDidUpdate(prevProps) {
@ -195,7 +175,6 @@ class Header extends React.PureComponent {
}
UNSAFE_componentWillReceiveProps(nextProps) {
const { user } = this.props;
if (
UNDO_LIMIT - nextProps.undoLength <= 0 &&
!this.state.didNotifyMaxUndoHistoryToast
@ -209,19 +188,6 @@ class Header extends React.PureComponent {
) {
this.props.setMaxUndoHistoryExceeded();
}
if (
this.canAddReports() &&
nextProps.dashboardInfo.id !== this.props.dashboardInfo.id
) {
// this is in case there is an anonymous user.
this.props.fetchUISpecificReport(
user?.userId,
'dashboard_id',
'dashboards',
nextProps?.dashboardInfo?.id,
user?.email,
);
}
}
componentWillUnmount() {
@ -414,14 +380,6 @@ class Header extends React.PureComponent {
this.setState({ showingPropertiesModal: false });
}
showReportModal() {
this.setState({ showingReportModal: true });
}
hideReportModal() {
this.setState({ showingReportModal: false });
}
showEmbedModal = () => {
this.setState({ showingEmbedModal: true });
};
@ -430,47 +388,6 @@ class Header extends React.PureComponent {
this.setState({ showingEmbedModal: false });
};
renderReportModal() {
const attachedReportExists = !!Object.keys(this.props.reports).length;
return attachedReportExists ? (
<HeaderReportActionsDropdown
showReportModal={this.showReportModal}
toggleActive={this.props.toggleActive}
deleteActiveReport={this.props.deleteActiveReport}
/>
) : (
<>
<span
role="button"
title={t('Schedule email report')}
tabIndex={0}
className="action-button"
onClick={this.showReportModal}
>
<Icons.Calendar />
</span>
</>
);
}
canAddReports() {
if (!isFeatureEnabled(FeatureFlag.ALERT_REPORTS)) {
return false;
}
const { user } = this.props;
if (!user?.userId) {
// this is in the case that there is an anonymous user.
return false;
}
const roles = Object.keys(user.roles || []);
const permissions = roles.map(key =>
user.roles[key].filter(
perms => perms[0] === 'menu_access' && perms[1] === 'Manage',
),
);
return permissions[0].length > 0;
}
render() {
const {
dashboardTitle,
@ -500,6 +417,7 @@ class Header extends React.PureComponent {
lastModifiedTime,
filterboxMigrationState,
} = this.props;
const userCanEdit =
dashboardInfo.dash_edit_perm &&
filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.REVIEWING &&
@ -511,7 +429,6 @@ class Header extends React.PureComponent {
const userCanCurate =
isFeatureEnabled(FeatureFlag.EMBEDDED_SUPERSET) &&
findPermission('can_set_embedded', 'Dashboard', user.roles);
const shouldShowReport = !editMode && this.canAddReports();
const refreshLimit =
dashboardInfo.common?.conf?.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT;
const refreshWarning =
@ -625,48 +542,41 @@ class Header extends React.PureComponent {
)}
</div>
)}
{editMode && (
{editMode ? (
<UndoRedoKeyListeners
onUndo={this.handleCtrlZ}
onRedo={this.handleCtrlY}
/>
)}
{!editMode && userCanEdit && (
) : (
<>
<span
role="button"
title={t('Edit dashboard')}
tabIndex={0}
className="action-button"
onClick={this.toggleEditMode}
>
<Icons.EditAlt />
</span>
{userCanEdit && (
<span
role="button"
title={t('Edit dashboard')}
tabIndex={0}
className="action-button"
onClick={this.toggleEditMode}
>
<Icons.EditAlt />
</span>
)}
<HeaderReportDropdown
key={dashboardInfo.id}
dashboardId={dashboardInfo.id}
/>
</>
)}
{shouldShowReport && this.renderReportModal()}
<PropertiesModal
dashboardId={dashboardInfo.id}
dashboardInfo={dashboardInfo}
dashboardTitle={dashboardTitle}
show={this.state.showingPropertiesModal}
onHide={this.hidePropertiesModal}
colorScheme={this.props.colorScheme}
onSubmit={handleOnPropertiesChange}
onlyApply
/>
{this.state.showingReportModal && (
<ReportModal
show={this.state.showingReportModal}
onHide={this.hideReportModal}
userId={user.userId}
userEmail={user.email}
{this.state.showingPropertiesModal && (
<PropertiesModal
dashboardId={dashboardInfo.id}
dashboardName={dashboardInfo.name}
creationMethod="dashboards"
dashboardInfo={dashboardInfo}
dashboardTitle={dashboardTitle}
show={this.state.showingPropertiesModal}
onHide={this.hidePropertiesModal}
colorScheme={this.props.colorScheme}
onSubmit={handleOnPropertiesChange}
onlyApply
/>
)}

View File

@ -56,11 +56,7 @@ import {
import { logEvent } from 'src/logger/actions';
import { DASHBOARD_HEADER_ID } from 'src/dashboard/util/constants';
import {
fetchUISpecificReport,
toggleActive,
deleteActiveReport,
} from 'src/reports/actions/reports';
import { fetchUISpecificReport } from 'src/reports/actions/reports';
function mapStateToProps({
dashboardLayout: undoableLayout,
@ -134,8 +130,6 @@ function mapDispatchToProps(dispatch) {
dashboardTitleChanged,
updateDataMask,
fetchUISpecificReport,
toggleActive,
deleteActiveReport,
},
dispatch,
);

View File

@ -34,6 +34,16 @@ export type ChartConfiguration = {
};
};
export type User = {
email: string;
firstName: string;
isActive: boolean;
lastName: string;
permissions: Record<string, any>;
roles: Record<string, any>;
userId: number;
username: string;
};
export interface DashboardInfo {
id: number;
json_metadata: string;

View File

@ -336,7 +336,6 @@ export const DataTablesPane = ({
},
[queryFormData, columnNames],
);
useEffect(() => {
setItem(LocalStorageKeys.is_datapanel_open, panelOpen);
}, [panelOpen]);

View File

@ -80,8 +80,8 @@ const createProps = () => ({
chartStatus: 'rendered',
onOpenPropertiesModal: jest.fn(),
onOpenInEditor: jest.fn(),
canAddReports: false,
canDownloadCSV: false,
showReportSubMenu: false,
});
fetchMock.post(
@ -120,18 +120,6 @@ test('Should open a menu', () => {
expect(screen.queryByText('Manage email report')).not.toBeInTheDocument();
});
test('Menu has email report item if user can add report', () => {
const props = createProps();
props.canAddReports = true;
render(<ExploreAdditionalActionsMenu {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByRole('button'));
expect(screen.queryByText('Manage email report')).not.toBeInTheDocument();
expect(screen.getByText('Set up an email report')).toBeInTheDocument();
});
test('Should open download submenu', async () => {
const props = createProps();
render(<ExploreAdditionalActionsMenu {...props} />, {
@ -174,31 +162,6 @@ test('Should open share submenu', async () => {
expect(await screen.findByText('Share chart by email')).toBeInTheDocument();
});
test('Should open report submenu if report exists', async () => {
const props = createProps();
props.canAddReports = true;
render(<ExploreAdditionalActionsMenu {...props} />, {
useRedux: true,
initialState: {
reports: {
'1': { name: 'Test report' },
},
},
});
userEvent.click(screen.getByRole('button'));
expect(screen.queryByText('Email reports active')).not.toBeInTheDocument();
expect(screen.queryByText('Edit email report')).not.toBeInTheDocument();
expect(screen.queryByText('Download as image')).not.toBeInTheDocument();
expect(screen.getByText('Manage email report')).toBeInTheDocument();
userEvent.hover(screen.getByText('Manage email report'));
expect(await screen.findByText('Email reports active')).toBeInTheDocument();
expect(await screen.findByText('Edit email report')).toBeInTheDocument();
expect(await screen.findByText('Delete email report')).toBeInTheDocument();
});
test('Should call onOpenPropertiesModal when click on "Edit chart properties"', () => {
const props = createProps();
render(<ExploreAdditionalActionsMenu {...props} />, {

View File

@ -1,87 +0,0 @@
/**
* 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 React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import pick from 'lodash/pick';
import { t } from '@superset-ui/core';
import ReportModal from 'src/components/ReportModal';
import { ExplorePageState } from 'src/explore/reducers/getInitialState';
import DeleteModal from 'src/components/DeleteModal';
import { deleteActiveReport } from 'src/reports/actions/reports';
type ReportMenuItemsProps = {
report: Record<string, any>;
isVisible: boolean;
onHide: () => void;
isDeleting: boolean;
setIsDeleting: (isDeleting: boolean) => void;
};
export const ExploreReport = ({
report,
isVisible,
onHide,
isDeleting,
setIsDeleting,
}: ReportMenuItemsProps) => {
const dispatch = useDispatch();
const { chart, chartName } = useSelector((state: ExplorePageState) => ({
chart: Object.values(state.charts || {})[0],
chartName: state.explore.sliceName,
}));
const { userId, email } = useSelector<
ExplorePageState,
{ userId?: number; email?: string }
>(state => pick(state.explore.user, ['userId', 'email']));
const handleReportDelete = useCallback(() => {
dispatch(deleteActiveReport(report));
setIsDeleting(false);
}, [dispatch, report, setIsDeleting]);
return (
<>
<ReportModal
show={isVisible}
onHide={onHide}
userId={userId}
userEmail={email}
chart={chart}
chartName={chartName}
creationMethod="charts"
/>
{isDeleting && (
<DeleteModal
description={t(
'This action will permanently delete %s.',
report.name,
)}
onConfirm={() => {
if (report) {
handleReportDelete();
}
}}
onHide={() => setIsDeleting(false)}
open
title={t('Delete Report?')}
/>
)}
</>
);
};

View File

@ -17,7 +17,7 @@
* under the License.
*/
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { FileOutlined, FileImageOutlined } from '@ant-design/icons';
import { css, styled, t, useTheme } from '@superset-ui/core';
@ -27,15 +27,12 @@ import Icons from 'src/components/Icons';
import ModalTrigger from 'src/components/ModalTrigger';
import Button from 'src/components/Button';
import withToasts from 'src/components/MessageToasts/withToasts';
import Checkbox from 'src/components/Checkbox';
import { exportChart } from 'src/explore/exploreUtils';
import downloadAsImage from 'src/utils/downloadAsImage';
import { noOp } from 'src/utils/common';
import { getChartPermalink } from 'src/utils/urlUtils';
import { toggleActive } from 'src/reports/actions/reports';
import HeaderReportDropDown from 'src/components/ReportModal/HeaderReportDropdown';
import ViewQueryModal from '../controls/ViewQueryModal';
import EmbedCodeContent from '../EmbedCodeContent';
import { ExploreReport } from './ExploreReport';
import copyTextToClipboard from '../../../utils/copy';
const propTypes = {
@ -69,7 +66,7 @@ const MENU_KEYS = {
const VIZ_TYPES_PIVOTABLE = ['pivot_table', 'pivot_table_v2'];
const MenuItemWithCheckboxContainer = styled.div`
export const MenuItemWithCheckboxContainer = styled.div`
${({ theme }) => css`
display: flex;
align-items: center;
@ -86,7 +83,7 @@ const MenuItemWithCheckboxContainer = styled.div`
`}
`;
const MenuTrigger = styled(Button)`
export const MenuTrigger = styled(Button)`
${({ theme }) => css`
width: ${theme.gridUnit * 8}px;
height: ${theme.gridUnit * 8}px;
@ -112,25 +109,22 @@ const ExploreAdditionalActionsMenu = ({
slice,
onOpenInEditor,
onOpenPropertiesModal,
canAddReports,
}) => {
const theme = useTheme();
const [showReportSubMenu, setShowReportSubMenu] = useState(null);
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [openSubmenus, setOpenSubmenus] = useState([]);
const [showReportModal, setShowReportModal] = useState(false);
const [showDeleteReportModal, setShowDeleteReportModal] = useState(false);
const dispatch = useDispatch();
const report = useSelector(state => {
if (!state.reports) {
const chart = useSelector(state => {
if (!state.charts) {
return undefined;
}
const reports = Object.values(state?.reports);
if (reports.length > 0) {
return reports[0];
const charts = Object.values(state.charts);
if (charts.length > 0) {
return charts[0];
}
return undefined;
});
const isReportActive = report?.active;
const { datasource } = latestQueryFormData;
const sqlSupported = datasource && datasource.split('__')[1] === 'table';
@ -240,23 +234,6 @@ const ExploreAdditionalActionsMenu = ({
setIsDropdownVisible(false);
setOpenSubmenus([]);
break;
case MENU_KEYS.SET_UP_REPORT:
setShowReportModal(true);
setIsDropdownVisible(false);
break;
case MENU_KEYS.SET_REPORT_ACTIVE:
dispatch(toggleActive(report, !isReportActive));
break;
case MENU_KEYS.EDIT_REPORT:
setShowReportModal(true);
setIsDropdownVisible(false);
setOpenSubmenus([]);
break;
case MENU_KEYS.DELETE_REPORT:
setShowDeleteReportModal(true);
setIsDropdownVisible(false);
setOpenSubmenus([]);
break;
case MENU_KEYS.VIEW_QUERY:
setIsDropdownVisible(false);
break;
@ -270,15 +247,12 @@ const ExploreAdditionalActionsMenu = ({
},
[
copyLink,
dispatch,
exportCSV,
exportCSVPivoted,
exportJson,
isReportActive,
latestQueryFormData,
onOpenInEditor,
onOpenPropertiesModal,
report,
shareByEmail,
slice?.slice_name,
],
@ -372,35 +346,31 @@ const ExploreAdditionalActionsMenu = ({
</Menu.Item>
</Menu.SubMenu>
<Menu.Divider />
{canAddReports && (
{showReportSubMenu ? (
<>
{report ? (
<Menu.SubMenu
title={t('Manage email report')}
key={MENU_KEYS.REPORT_SUBMENU}
>
<Menu.Item key={MENU_KEYS.SET_REPORT_ACTIVE}>
<MenuItemWithCheckboxContainer>
<Checkbox checked={isReportActive} onChange={noOp} />
{t('Email reports active')}
</MenuItemWithCheckboxContainer>
</Menu.Item>
<Menu.Item key={MENU_KEYS.EDIT_REPORT}>
{t('Edit email report')}
</Menu.Item>
<Menu.Item key={MENU_KEYS.DELETE_REPORT}>
{t('Delete email report')}
</Menu.Item>
</Menu.SubMenu>
) : (
<Menu.Item key={MENU_KEYS.SET_UP_REPORT}>
{t('Set up an email report')}
</Menu.Item>
)}
<Menu.SubMenu title={t('Manage email report')}>
<HeaderReportDropDown
chart={chart}
setShowReportSubMenu={setShowReportSubMenu}
showReportSubMenu={showReportSubMenu}
setIsDropdownVisible={setIsDropdownVisible}
isDropdownVisible={isDropdownVisible}
useTextMenu
/>
</Menu.SubMenu>
<Menu.Divider />
</>
) : (
<Menu>
<HeaderReportDropDown
chart={chart}
setShowReportSubMenu={setShowReportSubMenu}
setIsDropdownVisible={setIsDropdownVisible}
isDropdownVisible={isDropdownVisible}
useTextMenu
/>
</Menu>
)}
<Menu.Item key={MENU_KEYS.VIEW_QUERY}>
<ModalTrigger
triggerNode={
@ -435,13 +405,6 @@ const ExploreAdditionalActionsMenu = ({
/>
</MenuTrigger>
</AntdDropdown>
<ExploreReport
report={report}
isVisible={showReportModal}
onHide={() => setShowReportModal(false)}
isDeleting={showDeleteReportModal}
setIsDeleting={setShowDeleteReportModal}
/>
</>
);
};

View File

@ -20,8 +20,14 @@
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import ExploreHeader from '.';
const chartEndpoint = 'glob:*api/v1/chart/*';
fetchMock.get(chartEndpoint, { json: 'foo' });
const createProps = () => ({
chart: {
latestQueryFormData: {

View File

@ -20,18 +20,14 @@ import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import PropTypes from 'prop-types';
import { Tooltip } from 'src/components/Tooltip';
import {
CategoricalColorNamespace,
css,
SupersetClient,
t,
} from '@superset-ui/core';
import {
fetchUISpecificReport,
toggleActive,
deleteActiveReport,
} from 'src/reports/actions/reports';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import { toggleActive, deleteActiveReport } from 'src/reports/actions/reports';
import { chartPropShape } from 'src/dashboard/util/propShapes';
import AlteredSliceTag from 'src/components/AlteredSliceTag';
import FaveStar from 'src/components/FaveStar';
@ -40,7 +36,6 @@ import Icons from 'src/components/Icons';
import PropertiesModal from 'src/explore/components/PropertiesModal';
import { sliceUpdated } from 'src/explore/actions/exploreActions';
import CertifiedBadge from 'src/components/CertifiedBadge';
import { Tooltip } from 'src/components/Tooltip';
import ExploreAdditionalActionsMenu from '../ExploreAdditionalActionsMenu';
import { ChartEditableTitle } from './ChartEditableTitle';
@ -124,16 +119,6 @@ export class ExploreChartHeader extends React.PureComponent {
componentDidMount() {
const { dashboardId } = this.props;
if (this.canAddReports()) {
const { user, chart } = this.props;
// this is in the case that there is an anonymous user.
this.props.fetchUISpecificReport(
user.userId,
'chart_id',
'charts',
chart.id,
);
}
if (dashboardId) {
this.fetchChartDashboardData();
}
@ -201,24 +186,6 @@ export class ExploreChartHeader extends React.PureComponent {
});
}
canAddReports() {
if (!isFeatureEnabled(FeatureFlag.ALERT_REPORTS)) {
return false;
}
const { user } = this.props;
if (!user?.userId) {
// this is in the case that there is an anonymous user.
return false;
}
const roles = Object.keys(user.roles || []);
const permissions = roles.map(key =>
user.roles[key].filter(
perms => perms[0] === 'menu_access' && perms[1] === 'Manage',
),
);
return permissions[0].length > 0;
}
render() {
const {
actions,
@ -312,7 +279,6 @@ export class ExploreChartHeader extends React.PureComponent {
slice={slice}
canDownloadCSV={canDownload}
latestQueryFormData={latestQueryFormData}
canAddReports={this.canAddReports()}
/>
</div>
</div>
@ -324,7 +290,7 @@ ExploreChartHeader.propTypes = propTypes;
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{ sliceUpdated, fetchUISpecificReport, toggleActive, deleteActiveReport },
{ sliceUpdated, toggleActive, deleteActiveReport },
dispatch,
);
}

View File

@ -83,7 +83,7 @@ function PropertiesModal({
});
const chart = response.json.result;
setSelectedOwners(
chart.owners.map((owner: any) => ({
chart?.owners?.map((owner: any) => ({
value: owner.id,
label: `${owner.first_name} ${owner.last_name}`,
})),

View File

@ -25,27 +25,27 @@ import {
} from 'src/components/MessageToasts/actions';
export const SET_REPORT = 'SET_REPORT';
export function setReport(report) {
return { type: SET_REPORT, report };
export function setReport(report, resourceId, creationMethod, filterField) {
return { type: SET_REPORT, report, resourceId, creationMethod, filterField };
}
export function fetchUISpecificReport(
export function fetchUISpecificReport({
userId,
filter_field,
creation_method,
dashboardId,
) {
filterField,
creationMethod,
resourceId,
}) {
const queryParams = rison.encode({
filters: [
{
col: filter_field,
col: filterField,
opr: 'eq',
value: dashboardId,
value: resourceId,
},
{
col: 'creation_method',
opr: 'eq',
value: creation_method,
value: creationMethod,
},
{
col: 'created_by',
@ -59,7 +59,7 @@ export function fetchUISpecificReport(
endpoint: `/api/v1/report/?q=${queryParams}`,
})
.then(({ json }) => {
dispatch(setReport(json));
dispatch(setReport(json, resourceId, creationMethod, filterField));
})
.catch(() =>
dispatch(
@ -78,22 +78,22 @@ const structureFetchAction = (dispatch, getState) => {
const { user, dashboardInfo, charts, explore } = state;
if (dashboardInfo) {
dispatch(
fetchUISpecificReport(
user.userId,
'dashboard_id',
'dashboards',
dashboardInfo.id,
),
fetchUISpecificReport({
userId: user.userId,
filterField: 'dashboard_id',
creationMethod: 'dashboards',
resourceId: dashboardInfo.id,
}),
);
} else {
const [chartArr] = Object.keys(charts);
dispatch(
fetchUISpecificReport(
explore.user.userId,
'chart_id',
'charts',
charts[chartArr].id,
),
fetchUISpecificReport({
userId: explore.user.userId,
filterField: 'chart_id',
creationMethod: 'charts',
resourceId: charts[chartArr].id,
}),
);
}
};

View File

@ -17,32 +17,65 @@
* under the License.
*/
/* eslint-disable camelcase */
// eslint-disable-next-line import/no-extraneous-dependencies
import { SET_REPORT, ADD_REPORT, EDIT_REPORT } from '../actions/reports';
export default function reportsReducer(state = {}, action) {
const actionHandlers = {
[SET_REPORT]() {
return {
...action.report.result.reduce(
(obj, report) => ({ ...obj, [report.id]: report }),
{},
),
};
const { report, resourceId, creationMethod, filterField } = action;
// For now report count should only be one, but we are checking in case
// functionality changes.
const reportObject = report.result?.find(
report => report[filterField] === resourceId,
);
if (reportObject) {
return {
...state,
[creationMethod]: {
...state[creationMethod],
[resourceId]: reportObject,
},
};
}
if (state?.[creationMethod]?.[resourceId]) {
// remove the empty report from state
const newState = { ...state };
delete newState[creationMethod][resourceId];
return newState;
}
return { ...state };
},
[ADD_REPORT]() {
const report = action.json.result;
report.id = action.json.id;
const { result, id } = action.json;
const report = { ...result, id };
const reportTypeId = report.dashboard || report.chart;
// this is the id of either the chart or the dashboard associated with the report.
return {
...state,
[action.json.id]: report,
[report.creation_method]: {
...state[report.creation_method],
[reportTypeId]: report,
},
};
},
[EDIT_REPORT]() {
const report = action.json.result;
report.id = action.json.id;
const report = {
...action.json.result,
id: action.json.id,
};
const reportTypeId = report.dashboard || report.chart;
return {
...state,
[action.json.id]: report,
[report.creation_method]: {
...state[report.creation_method],
[reportTypeId]: report,
},
};
},
};

View File

@ -24,3 +24,37 @@ export type ReportScheduleType = 'Alert' | 'Report';
export type ReportCreationMethod = 'charts' | 'dashboards' | 'alerts_reports';
export type ReportRecipientType = 'Email' | 'Slack';
export enum ReportType {
DASHBOARDS = 'dashboards',
CHARTS = 'charts',
}
export enum NOTIFICATION_FORMATS {
TEXT = 'TEXT',
PNG = 'PNG',
CSV = 'CSV',
}
export interface ReportObject {
id?: number;
active: boolean;
crontab: string;
dashboard?: number;
chart?: number;
description?: string;
log_retention: number;
name: string;
owners: number[];
recipients: [
{ recipient_config_json: { target: string }; type: ReportRecipientType },
];
report_format: string;
timezone: string;
type: ReportScheduleType;
validator_config_json: {} | null;
validator_type: string;
working_timeout: number;
creation_method: string;
force_screenshot: boolean;
error?: string;
}

View File

@ -26,4 +26,5 @@ export default interface Owner {
id: number;
last_name: string;
username: string;
email?: string;
}

View File

@ -18,6 +18,7 @@
*/
import Owner from 'src/types/Owner';
import { NOTIFICATION_FORMATS } from 'src/reports/types';
type user = {
id: number;
@ -59,13 +60,16 @@ export type Operator = '<' | '>' | '<=' | '>=' | '==' | '!=' | 'not null';
export type AlertObject = {
active?: boolean;
creation_method?: string;
chart?: MetaObject;
changed_by?: user;
changed_on_delta_humanized?: string;
chart_id: number;
created_by?: user;
created_on?: string;
crontab?: string;
dashboard?: MetaObject;
dashboard_id?: number;
database?: MetaObject;
description?: string;
force_screenshot: boolean;
@ -79,7 +83,7 @@ export type AlertObject = {
sql?: string;
timezone?: string;
recipients?: Array<Recipient>;
report_format?: 'PNG' | 'CSV' | 'TEXT';
report_format?: NOTIFICATION_FORMATS;
type?: string;
validator_config_json?: {
op?: Operator;
@ -87,6 +91,7 @@ export type AlertObject = {
};
validator_type?: string;
working_timeout?: number;
error?: string;
};
export type LogObject = {

View File

@ -788,3 +788,14 @@ export function useDatabaseValidation() {
return [validationErrors, getValidation, setValidationErrors] as const;
}
export const reportSelector = (
state: Record<string, any>,
resourceType: string,
resourceId?: number,
) => {
if (resourceId) {
return state.reports[resourceType]?.[resourceId];
}
return {};
};

View File

@ -125,12 +125,14 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
"changed_by.last_name",
"changed_on",
"changed_on_delta_humanized",
"chart_id",
"created_by.first_name",
"created_by.last_name",
"created_on",
"creation_method",
"crontab",
"crontab_humanized",
"dashboard_id",
"description",
"id",
"last_eval_dttm",

View File

@ -273,11 +273,13 @@ class TestReportSchedulesApi(SupersetTestCase):
"changed_by",
"changed_on",
"changed_on_delta_humanized",
"chart_id",
"created_by",
"created_on",
"creation_method",
"crontab",
"crontab_humanized",
"dashboard_id",
"description",
"id",
"last_eval_dttm",