Skip to content

feat: add bulk certify functionality to charts and dashboards #33002

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,11 @@ describe('Charts list', () => {
cy.get('[aria-label="checkbox-on"]').should('have.length', 26);
cy.getBySel('bulk-select-copy').contains('25 Selected');
cy.getBySel('bulk-select-action')
.should('have.length', 2)
.should('have.length', 3)
.then($btns => {
expect($btns).to.contain('Delete');
expect($btns).to.contain('Export');
expect($btns).to.contain('Certify');
});
cy.getBySel('bulk-select-deselect-all').click();
cy.get('[aria-label="checkbox-on"]').should('have.length', 0);
Expand All @@ -154,10 +155,11 @@ describe('Charts list', () => {
cy.getBySel('styled-card').click({ multiple: true });
cy.getBySel('bulk-select-copy').contains('25 Selected');
cy.getBySel('bulk-select-action')
.should('have.length', 2)
.should('have.length', 3)
.then($btns => {
expect($btns).to.contain('Delete');
expect($btns).to.contain('Export');
expect($btns).to.contain('Certify');
});
cy.getBySel('bulk-select-deselect-all').click();
cy.getBySel('bulk-select-copy').contains('0 Selected');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,11 @@ describe('Dashboards list', () => {
cy.get('[aria-label="checkbox-on"]').should('have.length', 6);
cy.getBySel('bulk-select-copy').contains('5 Selected');
cy.getBySel('bulk-select-action')
.should('have.length', 2)
.should('have.length', 3)
.then($btns => {
expect($btns).to.contain('Delete');
expect($btns).to.contain('Export');
expect($btns).to.contain('Certify');
});
cy.getBySel('bulk-select-deselect-all').click();
cy.get('[aria-label="checkbox-on"]').should('have.length', 0);
Expand All @@ -119,10 +120,11 @@ describe('Dashboards list', () => {
cy.getBySel('styled-card').click({ multiple: true });
cy.getBySel('bulk-select-copy').contains('5 Selected');
cy.getBySel('bulk-select-action')
.should('have.length', 2)
.should('have.length', 3)
.then($btns => {
expect($btns).to.contain('Delete');
expect($btns).to.contain('Export');
expect($btns).to.contain('Certify');
});
cy.getBySel('bulk-select-deselect-all').click();
cy.getBySel('bulk-select-copy').contains('0 Selected');
Expand Down
171 changes: 171 additions & 0 deletions superset-frontend/src/features/bulkUpdate/BulkCertifyModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* 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. 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 { render, screen, fireEvent } from 'spec/helpers/testing-library';
import fetchMock from 'fetch-mock';
import { Chart } from 'src/types/Chart';
import { Dashboard } from 'src/pages/DashboardList';
import BulkCertifyModal from './BulkCertifyModal';

const mockedChartProps = {
onHide: jest.fn(),
refreshData: jest.fn(),
addSuccessToast: jest.fn(),
addDangerToast: jest.fn(),
show: true,
selected: [
{
id: 1,
slice_name: 'Chart 1',
url: '/chart/1',
viz_type: 'table',
creator: 'user',
changed_on: 'now',
},
{
id: 2,
slice_name: 'Chart 2',
url: '/chart/2',
viz_type: 'line',
creator: 'another_user',
changed_on: 'then',
},
] as Chart[],
resourceName: 'chart' as 'chart' | 'dashboard',
resourceLabel: 'chart',
};

const mockedDashboardProps = {
onHide: jest.fn(),
refreshData: jest.fn(),
addSuccessToast: jest.fn(),
addDangerToast: jest.fn(),
show: true,
selected: [
{
id: 1,
dashboard_title: 'Dashboard 1',
url: '/dashboard/1',
published: true,
changed_by_name: 'user',
changed_on_delta_humanized: 'a while ago',
changed_by: 'user',
},
{
id: 2,
dashboard_title: 'Dashboard 2',
url: '/dashboard/2',
published: false,
changed_by_name: 'admin',
changed_on_delta_humanized: 'recently',
changed_by: 'admin',
},
] as Dashboard[],
resourceName: 'dashboard' as 'chart' | 'dashboard',
resourceLabel: 'dashboard',
};

describe('BulkCertifyModal', () => {
afterEach(() => {
fetchMock.reset();
jest.clearAllMocks();
});

describe('when resourceName is chart', () => {
test('should render', () => {
const { container } = render(<BulkCertifyModal {...mockedChartProps} />);
expect(container).toBeInTheDocument();
});

test('renders the correct title and message for charts', () => {
render(<BulkCertifyModal {...mockedChartProps} />);
expect(
screen.getByText(content =>
content.startsWith('You are certifying 2 charts'),
),
).toBeInTheDocument();
expect(screen.getByText(/bulk certify charts/i)).toBeInTheDocument();
});
});

describe('when resourceName is dashboard', () => {
test('should render', () => {
const { container } = render(
<BulkCertifyModal {...mockedDashboardProps} />,
);
expect(container).toBeInTheDocument();
});

test('renders the correct title and message for dashboards', () => {
render(<BulkCertifyModal {...mockedDashboardProps} />);
expect(
screen.getByText(content =>
content.startsWith('You are certifying 2 dashboards'),
),
).toBeInTheDocument();
expect(screen.getByText(/bulk certify dashboards/i)).toBeInTheDocument();
});
});

test('calls onHide when the Cancel button is clicked', () => {
render(<BulkCertifyModal {...mockedChartProps} />);
const cancelButton = screen.getByText('Cancel');
fireEvent.click(cancelButton);
expect(mockedChartProps.onHide).toHaveBeenCalled();
});

describe('rendering', () => {
describe('when resourceName is chart', () => {
test('should render', () => {
const { container } = render(
<BulkCertifyModal {...mockedChartProps} />,
);
expect(container).toBeInTheDocument();
});

test('renders the correct title and message for charts', () => {
render(<BulkCertifyModal {...mockedChartProps} />);
expect(
screen.getByText(content =>
content.startsWith('You are certifying 2 charts'),
),
).toBeInTheDocument();
expect(screen.getByText(/bulk certify charts/i)).toBeInTheDocument();
});
});

describe('when resourceName is dashboard', () => {
test('should render', () => {
const { container } = render(
<BulkCertifyModal {...mockedDashboardProps} />,
);
expect(container).toBeInTheDocument();
});

test('renders the correct title and message for dashboards', () => {
render(<BulkCertifyModal {...mockedDashboardProps} />);
expect(
screen.getByText(content =>
content.startsWith('You are certifying 2 dashboards'),
),
).toBeInTheDocument();
expect(
screen.getByText(/bulk certify dashboards/i),
).toBeInTheDocument();
});
});
});
});
158 changes: 158 additions & 0 deletions superset-frontend/src/features/bulkUpdate/BulkCertifyModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* 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. 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 { useState, FC } from 'react';

import { t, SupersetClient } from '@superset-ui/core';
import { FormLabel } from 'src/components/Form';
import Modal from 'src/components/Modal';
import { Input } from 'src/components/Input';
import Button from 'src/components/Button';

import Chart from 'src/types/Chart';
import { Dashboard } from 'src/pages/DashboardList';
import { Row, Col } from 'src/components';

interface BulkCertifyModalProps {
onHide: () => void;
refreshData: () => void;
addSuccessToast: (msg: string) => void;
addDangerToast: (msg: string) => void;
show: boolean;
resourceName: 'chart' | 'dashboard';
resourceLabel: string;
selected: Chart[] | Dashboard[];
}

const BulkCertifyModal: FC<BulkCertifyModalProps> = ({
show,
selected = [],
resourceName,
resourceLabel,
onHide,
refreshData,
addSuccessToast,
addDangerToast,
}) => {
const [certifiedBy, setCertifiedBy] = useState<string>('');
const [certificationDetails, setCertificationDetails] = useState<string>('');

const resourceLabelPlural = t(
'%s',
selected.length > 1 ? `${resourceLabel}s` : resourceLabel,
);

const onSave = async () => {
if (!certifiedBy) {
addDangerToast(t('Please enter who certified these items'));
return;
}

Promise.all(
selected.map(item => {
const url = `/api/v1/${resourceName}/${item.id}`;
const payload = {
certified_by: certifiedBy,
certification_details: certificationDetails,
};

return SupersetClient.put({
url,
headers: { 'Content-Type': 'application/json' },
jsonPayload: payload,
});
}),
)
.then(() => {
addSuccessToast(t('Successfully certified %s', resourceLabelPlural));
})
.catch(() => {
addDangerToast(t('Failed to certify %s', resourceLabelPlural));
});

refreshData();
onHide();
setCertifiedBy('');
setCertificationDetails('');
};

return (
<Modal
title={<h4>{t('Bulk certify %s', resourceLabelPlural)}</h4>}
show={show}
onHide={() => {
setCertifiedBy('');
setCertificationDetails('');
onHide();
}}
footer={
<div>
<Button
data-test="modal-cancel-certify-button"
buttonStyle="secondary"
onClick={onHide}
>
{t('Cancel')}
</Button>
<Button
data-test="modal-save-certify-button"
buttonStyle="primary"
onClick={onSave}
disabled={!certifiedBy}
>
{t('Certify')}
</Button>
</div>
}
>
<Row gutter={16}>
<Col xs={24} md={12}>
<div className="bulk-certify-text">
{t(
'You are certifying %s %s',
selected.length,
resourceLabelPlural,
)}
</div>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} md={12}>
<FormLabel>{t('Certified by')}</FormLabel>
<Input
value={certifiedBy}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setCertifiedBy(event.target.value)
}
placeholder={t('e.g., Data Governance Team')}
/>
</Col>
<Col xs={24} md={12}>
<FormLabel>{t('Certification details')} (optional)</FormLabel>
<Input.TextArea
rows={1}
value={certificationDetails}
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
setCertificationDetails(event.target.value)
}
placeholder={t('Optional details about the certification')}
/>
</Col>
</Row>
</Modal>
);
};

export default BulkCertifyModal;
Loading
Loading