mirror of
https://github.com/apache/superset.git
synced 2026-06-04 07:09:22 +00:00
feat: Certify Charts and Dashboards (#17335)
* Certify charts * Format * Certify dashboards * Format * Refactor card certification * Clear details when certified by empty * Show certification in detail page * Add RTL tests * Test charts api * Enhance integration tests * Lint * Fix dashboards count * Format * Handle empty value * Handle empty slice * Downgrade migration * Indent * Use alter * Fix revision * Fix revision
This commit is contained in:
@@ -27,6 +27,8 @@ import PropertiesModal from '.';
|
||||
const createProps = () => ({
|
||||
slice: {
|
||||
cache_timeout: null,
|
||||
certified_by: 'John Doe',
|
||||
certification_details: 'Sample certification',
|
||||
changed_on: '2021-03-19T16:30:56.750230',
|
||||
changed_on_humanized: '7 days ago',
|
||||
datasource: 'FCC 2018 Survey',
|
||||
@@ -87,6 +89,8 @@ fetchMock.get('http://localhost/api/v1/chart/318', {
|
||||
},
|
||||
result: {
|
||||
cache_timeout: null,
|
||||
certified_by: 'John Doe',
|
||||
certification_details: 'Sample certification',
|
||||
dashboards: [
|
||||
{
|
||||
dashboard_title: 'FCC New Coder Survey 2018',
|
||||
@@ -145,6 +149,8 @@ fetchMock.put('http://localhost/api/v1/chart/318', {
|
||||
id: 318,
|
||||
result: {
|
||||
cache_timeout: null,
|
||||
certified_by: 'John Doe',
|
||||
certification_details: 'Sample certification',
|
||||
description: null,
|
||||
owners: [],
|
||||
slice_name: 'Age distribution of respondents',
|
||||
@@ -211,7 +217,7 @@ test('Should render all elements inside modal', async () => {
|
||||
const props = createProps();
|
||||
render(<PropertiesModal {...props} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(3);
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(5);
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'Basic information' }),
|
||||
@@ -226,6 +232,12 @@ test('Should render all elements inside modal', async () => {
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Access' })).toBeVisible();
|
||||
expect(screen.getByText('Owners')).toBeVisible();
|
||||
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'Configuration' }),
|
||||
).toBeVisible();
|
||||
expect(screen.getByText('Certified by')).toBeVisible();
|
||||
expect(screen.getByText('Certification details')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -275,3 +287,19 @@ test('"Save" button should call only "onSave"', async () => {
|
||||
expect(props.onHide).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Empty "Certified by" should clear "Certification details"', async () => {
|
||||
const props = createProps();
|
||||
const noCertifiedByProps = {
|
||||
...props,
|
||||
slice: {
|
||||
...props.slice,
|
||||
certified_by: '',
|
||||
},
|
||||
};
|
||||
render(<PropertiesModal {...noCertifiedByProps} />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('textbox', { name: 'Certification details' }),
|
||||
).toHaveValue('');
|
||||
});
|
||||
|
||||
@@ -18,14 +18,13 @@
|
||||
*/
|
||||
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
||||
import Modal from 'src/components/Modal';
|
||||
import { Row, Col, Input, TextArea } from 'src/common/components';
|
||||
import { Form, Row, Col, Input, TextArea } from 'src/common/components';
|
||||
import Button from 'src/components/Button';
|
||||
import { Select } from 'src/components';
|
||||
import { SelectValue } from 'antd/lib/select';
|
||||
import rison from 'rison';
|
||||
import { t, SupersetClient } from '@superset-ui/core';
|
||||
import { t, SupersetClient, styled } from '@superset-ui/core';
|
||||
import Chart, { Slice } from 'src/types/Chart';
|
||||
import { Form, FormItem } from 'src/components/Form';
|
||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||
|
||||
type PropertiesModalProps = {
|
||||
@@ -37,6 +36,16 @@ type PropertiesModalProps = {
|
||||
existingOwners?: SelectValue;
|
||||
};
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
const StyledFormItem = styled(Form.Item)`
|
||||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
const StyledHelpBlock = styled.span`
|
||||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
export default function PropertiesModal({
|
||||
slice,
|
||||
onHide,
|
||||
@@ -44,14 +53,11 @@ export default function PropertiesModal({
|
||||
show,
|
||||
}: PropertiesModalProps) {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [selectedOwners, setSelectedOwners] = useState<SelectValue | null>(
|
||||
null,
|
||||
);
|
||||
const [form] = Form.useForm();
|
||||
// values of form inputs
|
||||
const [name, setName] = useState(slice.slice_name || '');
|
||||
const [description, setDescription] = useState(slice.description || '');
|
||||
const [cacheTimeout, setCacheTimeout] = useState(
|
||||
slice.cache_timeout != null ? slice.cache_timeout : '',
|
||||
const [selectedOwners, setSelectedOwners] = useState<SelectValue | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
function showError({ error, statusText, message }: any) {
|
||||
@@ -110,14 +116,26 @@ export default function PropertiesModal({
|
||||
[],
|
||||
);
|
||||
|
||||
const onSubmit = async (event: React.FormEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const onSubmit = async (values: {
|
||||
certified_by?: string;
|
||||
certification_details?: string;
|
||||
description?: string;
|
||||
cache_timeout?: number;
|
||||
}) => {
|
||||
setSubmitting(true);
|
||||
const {
|
||||
certified_by: certifiedBy,
|
||||
certification_details: certificationDetails,
|
||||
description,
|
||||
cache_timeout: cacheTimeout,
|
||||
} = values;
|
||||
const payload: { [key: string]: any } = {
|
||||
slice_name: name || null,
|
||||
description: description || null,
|
||||
cache_timeout: cacheTimeout || null,
|
||||
certified_by: certifiedBy || null,
|
||||
certification_details:
|
||||
certifiedBy && certificationDetails ? certificationDetails : null,
|
||||
};
|
||||
if (selectedOwners) {
|
||||
payload.owners = (
|
||||
@@ -177,11 +195,10 @@ export default function PropertiesModal({
|
||||
</Button>
|
||||
<Button
|
||||
data-test="properties-modal-save-button"
|
||||
htmlType="button"
|
||||
htmlType="submit"
|
||||
buttonSize="small"
|
||||
buttonStyle="primary"
|
||||
// @ts-ignore
|
||||
onClick={onSubmit}
|
||||
onClick={form.submit}
|
||||
disabled={submitting || !name}
|
||||
cta
|
||||
>
|
||||
@@ -192,7 +209,21 @@ export default function PropertiesModal({
|
||||
responsive
|
||||
wrapProps={{ 'data-test': 'properties-edit-modal' }}
|
||||
>
|
||||
<Form onFinish={onSubmit} layout="vertical">
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onSubmit}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
name: slice.slice_name || '',
|
||||
description: slice.description || '',
|
||||
cache_timeout: slice.cache_timeout != null ? slice.cache_timeout : '',
|
||||
certified_by: slice.certified_by || '',
|
||||
certification_details:
|
||||
slice.certified_by && slice.certification_details
|
||||
? slice.certification_details
|
||||
: '',
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<h3>{t('Basic information')}</h3>
|
||||
@@ -208,40 +239,50 @@ export default function PropertiesModal({
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label={t('Description')}>
|
||||
<TextArea
|
||||
rows={3}
|
||||
name="description"
|
||||
value={description}
|
||||
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setDescription(event.target.value ?? '')
|
||||
}
|
||||
style={{ maxWidth: '100%' }}
|
||||
/>
|
||||
<p className="help-block">
|
||||
<FormItem>
|
||||
<StyledFormItem label={t('Description')} name="description">
|
||||
<TextArea rows={3} style={{ maxWidth: '100%' }} />
|
||||
</StyledFormItem>
|
||||
<StyledHelpBlock className="help-block">
|
||||
{t(
|
||||
'The description can be displayed as widget headers in the dashboard view. Supports markdown.',
|
||||
)}
|
||||
</p>
|
||||
</StyledHelpBlock>
|
||||
</FormItem>
|
||||
<h3>{t('Certification')}</h3>
|
||||
<FormItem>
|
||||
<StyledFormItem label={t('Certified by')} name="certified_by">
|
||||
<Input />
|
||||
</StyledFormItem>
|
||||
<StyledHelpBlock className="help-block">
|
||||
{t('Person or group that has certified this chart.')}
|
||||
</StyledHelpBlock>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<StyledFormItem
|
||||
label={t('Certification details')}
|
||||
name="certification_details"
|
||||
>
|
||||
<Input />
|
||||
</StyledFormItem>
|
||||
<StyledHelpBlock className="help-block">
|
||||
{t(
|
||||
'Any additional detail to show in the certification tooltip.',
|
||||
)}
|
||||
</StyledHelpBlock>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<h3>{t('Configuration')}</h3>
|
||||
<FormItem label={t('Cache timeout')}>
|
||||
<Input
|
||||
name="cacheTimeout"
|
||||
type="text"
|
||||
value={cacheTimeout}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const targetValue = event.target.value ?? '';
|
||||
setCacheTimeout(targetValue.replace(/[^0-9]/, ''));
|
||||
}}
|
||||
/>
|
||||
<p className="help-block">
|
||||
<FormItem>
|
||||
<StyledFormItem label={t('Cache timeout')} name="cacheTimeout">
|
||||
<Input />
|
||||
</StyledFormItem>
|
||||
<StyledHelpBlock className="help-block">
|
||||
{t(
|
||||
"Duration (in seconds) of the caching timeout for this chart. Note this defaults to the dataset's timeout if undefined.",
|
||||
)}
|
||||
</p>
|
||||
</StyledHelpBlock>
|
||||
</FormItem>
|
||||
<h3 style={{ marginTop: '1em' }}>{t('Access')}</h3>
|
||||
<FormItem label={ownersLabel}>
|
||||
@@ -255,11 +296,11 @@ export default function PropertiesModal({
|
||||
disabled={!selectedOwners}
|
||||
allowClear
|
||||
/>
|
||||
<p className="help-block">
|
||||
<StyledHelpBlock className="help-block">
|
||||
{t(
|
||||
'A list of users who can alter the chart. Searchable by name or username.',
|
||||
)}
|
||||
</p>
|
||||
</StyledHelpBlock>
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
Reference in New Issue
Block a user