mirror of
https://github.com/apache/superset.git
synced 2026-05-07 00:44:26 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ac189963c |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -19,8 +19,6 @@ under the License.
|
||||
## Change Log
|
||||
### 1.4
|
||||
**Database Migrations**
|
||||
- [#17335](https://github.com/apache/superset/pull/17335) feat: Certify Charts and Dashboards (@geido)
|
||||
- [#17078](https://github.com/apache/superset/pull/17078) chore(engine): Translate fractional time grains—requires @superset-ui bump (@john-bodley)
|
||||
- [#16849](https://github.com/apache/superset/pull/16849) chore: db migrate timeseries_limit_metric to legacy_order_by (@zhaoyongjie)
|
||||
- [#14015](https://github.com/apache/superset/pull/14015) feat(filter-set): Add filterset resource (@ofekisr)
|
||||
- [#16454](https://github.com/apache/superset/pull/16454) feat: add certifiedby & certification details fields to the edit dataset columns fields (@pkdotson)
|
||||
@@ -67,13 +65,6 @@ under the License.
|
||||
- [#16136](https://github.com/apache/superset/pull/16136) feat: add profiling to Superset pages (@betodealmeida)
|
||||
|
||||
**Fixes**
|
||||
- [#17945](https://github.com/apache/superset/pull/17945) fix(dashboard): scope status of filter not update in dashboard metadata (@stephenLYZ)
|
||||
- [#17349](https://github.com/apache/superset/pull/17349) fix(Dashboard): Check validity of control item (@geido)
|
||||
- [#17842](https://github.com/apache/superset/pull/17842) fix(dashboard): update native filter info in metadata is not updated (@stephenLYZ)
|
||||
- [#17835](https://github.com/apache/superset/pull/17835) fix: resolve tests for 1.4 (@eschutho)
|
||||
- [#17781](https://github.com/apache/superset/pull/17781) fix(dashboard): commit update once (@serenajiang)
|
||||
- [#17766](https://github.com/apache/superset/pull/17766) fix: Remove positions from json_metadata (@geido)
|
||||
- [#17330](https://github.com/apache/superset/pull/17330) fix: import should accept old keys (@eschutho)
|
||||
- [#17570](https://github.com/apache/superset/pull/17570) fix: Save properties after applying changes in Dashboard (@geido)
|
||||
- [#17707](https://github.com/apache/superset/pull/17707) fix(Dashboard): Copy dashboard with duplicating charts 500 error (@geido)
|
||||
- [#16041](https://github.com/apache/superset/pull/16041) fix: set correct schema on config import (@betodealmeida)
|
||||
@@ -233,11 +224,6 @@ under the License.
|
||||
- [#15762](https://github.com/apache/superset/pull/15762) fix: Align alert solid small svg center (@duynguyenhoang)
|
||||
|
||||
**Others**
|
||||
- [#17964](https://github.com/apache/superset/pull/17964) chore: bump FAB to 3.4.3 (@dpgaspar)
|
||||
- [#17894](https://github.com/apache/superset/pull/17894) chore: bump gunicorn to 20.1.0 (@mporracindie)
|
||||
- [#17420](https://github.com/apache/superset/pull/17420) chore: Bump FAB to 3.4.0 (@kamalkeshavani-aiinside)
|
||||
- [#17752](https://github.com/apache/superset/pull/17752) chore: add release to pip requirements (@eschutho)
|
||||
- [#17724](https://github.com/apache/superset/pull/17724) ci: temp fix for mysqlclient on an OS regression bug (@dpgaspar)
|
||||
- [#17702](https://github.com/apache/superset/pull/17702) chore(sql): clean up invalid filter clause exception types (@villebro)
|
||||
- [#17579](https://github.com/apache/superset/pull/17579) chore(datasets): Sanitizing /save response (@craig-rueda)
|
||||
- [#17005](https://github.com/apache/superset/pull/17005) ci: skip unnecessary test steps (@villebro)
|
||||
|
||||
@@ -85,7 +85,7 @@ flask==1.1.4
|
||||
# flask-openid
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==3.4.3
|
||||
flask-appbuilder==3.3.4
|
||||
# via apache-superset
|
||||
flask-babel==1.0.0
|
||||
# via flask-appbuilder
|
||||
@@ -117,7 +117,7 @@ geopy==2.2.0
|
||||
# via apache-superset
|
||||
graphlib-backport==1.0.3
|
||||
# via apache-superset
|
||||
gunicorn==20.1.0
|
||||
gunicorn==20.0.4
|
||||
# via apache-superset
|
||||
holidays==0.10.3
|
||||
# via apache-superset
|
||||
|
||||
4
setup.py
4
setup.py
@@ -75,7 +75,7 @@ setup(
|
||||
"cryptography>=3.3.2",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=1.1.0, <2.0.0",
|
||||
"flask-appbuilder>=3.4.3, <4.0.0",
|
||||
"flask-appbuilder>=3.3.4, <4.0.0",
|
||||
"flask-caching>=1.10.0",
|
||||
"flask-compress",
|
||||
"flask-talisman",
|
||||
@@ -83,7 +83,7 @@ setup(
|
||||
"flask-wtf",
|
||||
"geopy",
|
||||
"graphlib-backport",
|
||||
"gunicorn>=20.1.0",
|
||||
"gunicorn>=20.0.2, <20.1",
|
||||
"holidays==0.10.3", # PINNED! https://github.com/dr-prodigy/python-holidays/issues/406
|
||||
"humanize",
|
||||
"itsdangerous>=1.0.0, <2.0.0", # https://github.com/apache/superset/pull/14627
|
||||
|
||||
@@ -111,6 +111,4 @@ export enum FilterOperator {
|
||||
between = 'between',
|
||||
dashboardIsFav = 'dashboard_is_favorite',
|
||||
chartIsFav = 'chart_is_favorite',
|
||||
chartIsCertified = 'chart_is_certified',
|
||||
dashboardIsCertified = 'dashboard_is_certified',
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { styled, useTheme } from '@superset-ui/core';
|
||||
import { AntdCard, Skeleton, ThinSkeleton } from 'src/common/components';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import ImageLoader, { BackgroundPosition } from './ImageLoader';
|
||||
import CertifiedIcon from '../CertifiedIcon';
|
||||
|
||||
const ActionsWrapper = styled.div`
|
||||
width: 64px;
|
||||
@@ -162,8 +161,6 @@ interface CardProps {
|
||||
rows?: number | string;
|
||||
avatar?: React.ReactElement | null;
|
||||
cover?: React.ReactNode | null;
|
||||
certifiedBy?: string;
|
||||
certificationDetails?: string;
|
||||
}
|
||||
|
||||
function ListViewCard({
|
||||
@@ -181,8 +178,6 @@ function ListViewCard({
|
||||
loading,
|
||||
imgPosition = 'top',
|
||||
cover,
|
||||
certifiedBy,
|
||||
certificationDetails,
|
||||
}: CardProps) {
|
||||
const Link = url && linkComponent ? linkComponent : AnchorLink;
|
||||
const theme = useTheme();
|
||||
@@ -254,17 +249,7 @@ function ListViewCard({
|
||||
<TitleContainer>
|
||||
<Tooltip title={title}>
|
||||
<TitleLink>
|
||||
<Link to={url!}>
|
||||
{certifiedBy && (
|
||||
<>
|
||||
<CertifiedIcon
|
||||
certifiedBy={certifiedBy}
|
||||
details={certificationDetails}
|
||||
/>{' '}
|
||||
</>
|
||||
)}
|
||||
{title}
|
||||
</Link>
|
||||
<Link to={url!}>{title}</Link>
|
||||
</TitleLink>
|
||||
</Tooltip>
|
||||
{titleRight && <TitleRight>{titleRight}</TitleRight>}
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('TimezoneSelector', () => {
|
||||
expect(select).toBeInTheDocument();
|
||||
userEvent.click(select);
|
||||
const selection = await screen.findByTitle(
|
||||
'GMT -10:00 (Hawaii Standard Time)',
|
||||
'GMT -06:00 (Mountain Daylight Time)',
|
||||
);
|
||||
expect(selection).toBeInTheDocument();
|
||||
userEvent.click(selection);
|
||||
|
||||
@@ -46,7 +46,6 @@ import {
|
||||
removeFilter,
|
||||
updateDirectPathToFilter,
|
||||
} from './dashboardFilters';
|
||||
import { SET_FILTER_CONFIG_COMPLETE } from './nativeFilters';
|
||||
|
||||
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
|
||||
export function setUnsavedChanges(hasUnsavedChanges) {
|
||||
@@ -282,12 +281,6 @@ export function saveDashboardRequest(data, id, saveType) {
|
||||
chartConfiguration: metadata.chart_configuration,
|
||||
});
|
||||
}
|
||||
if (metadata.native_filter_configuration) {
|
||||
dispatch({
|
||||
type: SET_FILTER_CONFIG_COMPLETE,
|
||||
filterConfig: metadata.native_filter_configuration,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (lastModifiedTime) {
|
||||
dispatch(saveDashboardRequestSuccess(lastModifiedTime));
|
||||
|
||||
@@ -146,25 +146,6 @@ export const setInScopeStatusOfFilters = (
|
||||
type: SET_IN_SCOPE_STATUS_OF_FILTERS,
|
||||
filterConfig: filtersWithScopes,
|
||||
});
|
||||
// need to update native_filter_configuration in the dashboard metadata
|
||||
const { metadata } = getState().dashboardInfo;
|
||||
const filterConfig: FilterConfiguration =
|
||||
metadata.native_filter_configuration;
|
||||
const mergedFilterConfig = filterConfig.map(filter => {
|
||||
const filterWithScope = filtersWithScopes.find(
|
||||
scope => scope.id === filter.id,
|
||||
);
|
||||
if (!filterWithScope) {
|
||||
return filter;
|
||||
}
|
||||
return { ...filterWithScope, ...filter };
|
||||
});
|
||||
metadata.native_filter_configuration = mergedFilterConfig;
|
||||
dispatch(
|
||||
dashboardInfoChanged({
|
||||
metadata,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
type BootstrapData = {
|
||||
|
||||
@@ -22,7 +22,6 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import ButtonGroup from 'src/components/ButtonGroup';
|
||||
import CertifiedIcon from 'src/components/CertifiedIcon';
|
||||
|
||||
import {
|
||||
LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD,
|
||||
@@ -494,14 +493,6 @@ class Header extends React.PureComponent {
|
||||
data-test-id={`${dashboardInfo.id}`}
|
||||
>
|
||||
<div className="dashboard-component-header header-large">
|
||||
{dashboardInfo.certified_by && (
|
||||
<>
|
||||
<CertifiedIcon
|
||||
certifiedBy={dashboardInfo.certified_by}
|
||||
details={dashboardInfo.certification_details}
|
||||
/>{' '}
|
||||
</>
|
||||
)}
|
||||
<EditableTitle
|
||||
title={dashboardTitle}
|
||||
canEdit={userCanEdit && editMode}
|
||||
|
||||
@@ -90,8 +90,6 @@ fetchMock.get(
|
||||
fetchMock.get('http://localhost/api/v1/dashboard/26', {
|
||||
body: {
|
||||
result: {
|
||||
certified_by: 'John Doe',
|
||||
certification_details: 'Sample certification',
|
||||
changed_by: null,
|
||||
changed_by_name: '',
|
||||
changed_by_url: '',
|
||||
@@ -124,8 +122,6 @@ fetchMock.get('http://localhost/api/v1/dashboard/26', {
|
||||
});
|
||||
|
||||
const createProps = () => ({
|
||||
certified_by: 'John Doe',
|
||||
certification_details: 'Sample certification',
|
||||
dashboardId: 26,
|
||||
show: true,
|
||||
colorScheme: 'supersetColors',
|
||||
@@ -160,10 +156,7 @@ test('should render - FeatureFlag disabled', async () => {
|
||||
expect(screen.getByRole('heading', { name: 'Access' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Colors' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Advanced' })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'Certification' }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('heading')).toHaveLength(5);
|
||||
expect(screen.getAllByRole('heading')).toHaveLength(4);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Advanced' })).toBeInTheDocument();
|
||||
@@ -171,7 +164,7 @@ test('should render - FeatureFlag disabled', async () => {
|
||||
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button')).toHaveLength(4);
|
||||
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(4);
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(2);
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
|
||||
expect(spyColorSchemeControlWrapper).toBeCalledWith(
|
||||
@@ -199,10 +192,7 @@ test('should render - FeatureFlag enabled', async () => {
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Access' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Advanced' })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'Certification' }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('heading')).toHaveLength(4);
|
||||
expect(screen.getAllByRole('heading')).toHaveLength(3);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Advanced' })).toBeInTheDocument();
|
||||
@@ -210,7 +200,7 @@ test('should render - FeatureFlag enabled', async () => {
|
||||
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button')).toHaveLength(4);
|
||||
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(4);
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(2);
|
||||
expect(screen.getAllByRole('combobox')).toHaveLength(2);
|
||||
|
||||
expect(spyColorSchemeControlWrapper).toBeCalledWith(
|
||||
@@ -229,10 +219,10 @@ test('should open advance', async () => {
|
||||
await screen.findByTestId('dashboard-edit-properties-form'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(4);
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(2);
|
||||
expect(screen.getAllByRole('combobox')).toHaveLength(2);
|
||||
userEvent.click(screen.getByRole('button', { name: 'Advanced' }));
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(5);
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(3);
|
||||
expect(screen.getAllByRole('combobox')).toHaveLength(2);
|
||||
});
|
||||
|
||||
@@ -331,18 +321,3 @@ test('submitting with onlyApply:true', async () => {
|
||||
expect(props.onSubmit).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Empty "Certified by" should clear "Certification details"', async () => {
|
||||
const props = createProps();
|
||||
const noCertifiedByProps = {
|
||||
...props,
|
||||
certified_by: '',
|
||||
};
|
||||
render(<PropertiesModal {...noCertifiedByProps} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByRole('textbox', { name: 'Certification details' }),
|
||||
).toHaveValue('');
|
||||
});
|
||||
|
||||
@@ -161,15 +161,10 @@ const PropertiesModal = ({
|
||||
|
||||
form.setFieldsValue(dashboardInfo);
|
||||
setDashboardInfo(dashboardInfo);
|
||||
setJsonMetadata(metadata ? jsonStringify(metadata) : '');
|
||||
setOwners(owners);
|
||||
setRoles(roles);
|
||||
setColorScheme(metadata.color_scheme);
|
||||
|
||||
// temporary fix to remove positions from dashboards' metadata
|
||||
if (metadata?.positions) {
|
||||
delete metadata.positions;
|
||||
}
|
||||
setJsonMetadata(metadata ? jsonStringify(metadata) : '');
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
@@ -36,17 +36,16 @@ import {
|
||||
} from 'src/reports/actions/reports';
|
||||
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||
import HeaderReportActionsDropdown from 'src/components/ReportModal/HeaderReportActionsDropdown';
|
||||
import { chartPropShape } from 'src/dashboard/util/propShapes';
|
||||
import EditableTitle from 'src/components/EditableTitle';
|
||||
import AlteredSliceTag from 'src/components/AlteredSliceTag';
|
||||
import FaveStar from 'src/components/FaveStar';
|
||||
import Timer from 'src/components/Timer';
|
||||
import CachedLabel from 'src/components/CachedLabel';
|
||||
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||
import { sliceUpdated } from 'src/explore/actions/exploreActions';
|
||||
import CertifiedIcon from 'src/components/CertifiedIcon';
|
||||
import { chartPropShape } from '../../dashboard/util/propShapes';
|
||||
import ExploreActionButtons from './ExploreActionButtons';
|
||||
import RowCountLabel from './RowCountLabel';
|
||||
import EditableTitle from '../../components/EditableTitle';
|
||||
import AlteredSliceTag from '../../components/AlteredSliceTag';
|
||||
import FaveStar from '../../components/FaveStar';
|
||||
import Timer from '../../components/Timer';
|
||||
import CachedLabel from '../../components/CachedLabel';
|
||||
import PropertiesModal from './PropertiesModal';
|
||||
import { sliceUpdated } from '../actions/exploreActions';
|
||||
|
||||
const CHART_STATUS_MAP = {
|
||||
failed: 'danger',
|
||||
@@ -166,10 +165,7 @@ export class ExploreChartHeader extends React.PureComponent {
|
||||
}
|
||||
|
||||
getSliceName() {
|
||||
const { sliceName, table_name: tableName } = this.props;
|
||||
const title = sliceName || t('%s - untitled', tableName);
|
||||
|
||||
return title;
|
||||
return this.props.sliceName || t('%s - untitled', this.props.table_name);
|
||||
}
|
||||
|
||||
postChartFormData() {
|
||||
@@ -245,7 +241,7 @@ export class ExploreChartHeader extends React.PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user, form_data: formData, slice } = this.props;
|
||||
const { user, form_data: formData } = this.props;
|
||||
const {
|
||||
chartStatus,
|
||||
chartUpdateEndTime,
|
||||
@@ -261,14 +257,6 @@ export class ExploreChartHeader extends React.PureComponent {
|
||||
return (
|
||||
<StyledHeader id="slice-header" className="panel-title-large">
|
||||
<div className="title-panel">
|
||||
{slice?.certified_by && (
|
||||
<>
|
||||
<CertifiedIcon
|
||||
certifiedBy={slice.certified_by}
|
||||
details={slice.certification_details}
|
||||
/>{' '}
|
||||
</>
|
||||
)}
|
||||
<EditableTitle
|
||||
title={this.getSliceName()}
|
||||
canEdit={!this.props.slice || this.props.can_overwrite}
|
||||
|
||||
@@ -27,8 +27,6 @@ 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',
|
||||
@@ -89,8 +87,6 @@ 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',
|
||||
@@ -149,8 +145,6 @@ 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',
|
||||
@@ -217,7 +211,7 @@ test('Should render all elements inside modal', async () => {
|
||||
const props = createProps();
|
||||
render(<PropertiesModal {...props} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(5);
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(3);
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'Basic information' }),
|
||||
@@ -232,12 +226,6 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -287,19 +275,3 @@ 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,13 +18,14 @@
|
||||
*/
|
||||
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
||||
import Modal from 'src/components/Modal';
|
||||
import { Form, Row, Col, Input, TextArea } from 'src/common/components';
|
||||
import { 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, styled } from '@superset-ui/core';
|
||||
import { t, SupersetClient } 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 = {
|
||||
@@ -36,16 +37,6 @@ 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,
|
||||
@@ -53,12 +44,15 @@ export default function PropertiesModal({
|
||||
show,
|
||||
}: PropertiesModalProps) {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
// values of form inputs
|
||||
const [name, setName] = useState(slice.slice_name || '');
|
||||
const [selectedOwners, setSelectedOwners] = useState<SelectValue | null>(
|
||||
null,
|
||||
);
|
||||
// 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 : '',
|
||||
);
|
||||
|
||||
function showError({ error, statusText, message }: any) {
|
||||
let errorText = error || statusText || t('An error has occurred');
|
||||
@@ -115,26 +109,14 @@ export default function PropertiesModal({
|
||||
[],
|
||||
);
|
||||
|
||||
const onSubmit = async (values: {
|
||||
certified_by?: string;
|
||||
certification_details?: string;
|
||||
description?: string;
|
||||
cache_timeout?: number;
|
||||
}) => {
|
||||
const onSubmit = async (event: React.FormEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
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 = (selectedOwners as {
|
||||
@@ -192,10 +174,11 @@ export default function PropertiesModal({
|
||||
</Button>
|
||||
<Button
|
||||
data-test="properties-modal-save-button"
|
||||
htmlType="submit"
|
||||
htmlType="button"
|
||||
buttonSize="small"
|
||||
buttonStyle="primary"
|
||||
onClick={form.submit}
|
||||
// @ts-ignore
|
||||
onClick={onSubmit}
|
||||
disabled={submitting || !name}
|
||||
cta
|
||||
>
|
||||
@@ -206,21 +189,7 @@ export default function PropertiesModal({
|
||||
responsive
|
||||
wrapProps={{ 'data-test': 'properties-edit-modal' }}
|
||||
>
|
||||
<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
|
||||
: '',
|
||||
}}
|
||||
>
|
||||
<Form onFinish={onSubmit} layout="vertical">
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<h3>{t('Basic information')}</h3>
|
||||
@@ -235,50 +204,40 @@ export default function PropertiesModal({
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<StyledFormItem label={t('Description')} name="description">
|
||||
<TextArea rows={3} style={{ maxWidth: '100%' }} />
|
||||
</StyledFormItem>
|
||||
<StyledHelpBlock className="help-block">
|
||||
<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">
|
||||
{t(
|
||||
'The description can be displayed as widget headers in the dashboard view. Supports markdown.',
|
||||
)}
|
||||
</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>
|
||||
</p>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<h3>{t('Configuration')}</h3>
|
||||
<FormItem>
|
||||
<StyledFormItem label={t('Cache timeout')} name="cacheTimeout">
|
||||
<Input />
|
||||
</StyledFormItem>
|
||||
<StyledHelpBlock className="help-block">
|
||||
<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">
|
||||
{t(
|
||||
"Duration (in seconds) of the caching timeout for this chart. Note this defaults to the dataset's timeout if undefined.",
|
||||
)}
|
||||
</StyledHelpBlock>
|
||||
</p>
|
||||
</FormItem>
|
||||
<h3 style={{ marginTop: '1em' }}>{t('Access')}</h3>
|
||||
<FormItem label={ownersLabel}>
|
||||
@@ -292,11 +251,11 @@ export default function PropertiesModal({
|
||||
disabled={!selectedOwners}
|
||||
allowClear
|
||||
/>
|
||||
<StyledHelpBlock className="help-block">
|
||||
<p className="help-block">
|
||||
{t(
|
||||
'A list of users who can alter the chart. Searchable by name or username.',
|
||||
)}
|
||||
</StyledHelpBlock>
|
||||
</p>
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
ControlState,
|
||||
ControlType,
|
||||
ControlValueValidator,
|
||||
CustomControlItem,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { getSectionsToRender } from './getSectionsToRender';
|
||||
import { getControlConfig } from './getControlConfig';
|
||||
@@ -161,8 +160,8 @@ export function getAllControlsState(
|
||||
const controlsState = {};
|
||||
getSectionsToRender(vizType, datasourceType).forEach(section =>
|
||||
section.controlSetRows.forEach(fieldsetRow =>
|
||||
fieldsetRow.forEach((field: CustomControlItem) => {
|
||||
if (field && field.config && field.name) {
|
||||
fieldsetRow.forEach(field => {
|
||||
if (field && 'config' in field && field.config && field.name) {
|
||||
const { config, name } = field;
|
||||
controlsState[name] = getControlStateFromControlConfig(
|
||||
config,
|
||||
|
||||
@@ -33,8 +33,6 @@ export interface Chart {
|
||||
changed_on: string;
|
||||
changed_on_delta_humanized?: string;
|
||||
changed_on_utc?: string;
|
||||
certified_by?: string;
|
||||
certification_details?: string;
|
||||
description: string | null;
|
||||
cache_timeout: number | null;
|
||||
thumbnail_url?: string;
|
||||
@@ -48,8 +46,6 @@ export type Slice = {
|
||||
slice_name: string;
|
||||
description: string | null;
|
||||
cache_timeout: number | null;
|
||||
certified_by?: string;
|
||||
certification_details?: string;
|
||||
form_data?: QueryFormData;
|
||||
query_context?: object;
|
||||
};
|
||||
|
||||
@@ -142,8 +142,6 @@ export default function ChartCard({
|
||||
<ListViewCard
|
||||
loading={loading}
|
||||
title={chart.slice_name}
|
||||
certifiedBy={chart.certified_by}
|
||||
certificationDetails={chart.certification_details}
|
||||
cover={
|
||||
!isFeatureEnabled(FeatureFlag.THUMBNAILS) || !showThumbnails ? (
|
||||
<></>
|
||||
|
||||
@@ -58,7 +58,6 @@ import { Tooltip } from 'src/components/Tooltip';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { nativeFilterGate } from 'src/dashboard/components/nativeFilters/utils';
|
||||
import setupPlugins from 'src/setup/setupPlugins';
|
||||
import CertifiedIcon from 'src/components/CertifiedIcon';
|
||||
import ChartCard from './ChartCard';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
@@ -238,23 +237,10 @@ function ChartList(props: ChartListProps) {
|
||||
{
|
||||
Cell: ({
|
||||
row: {
|
||||
original: {
|
||||
url,
|
||||
slice_name: sliceName,
|
||||
certified_by: certifiedBy,
|
||||
certification_details: certificationDetails,
|
||||
},
|
||||
original: { url, slice_name: sliceName },
|
||||
},
|
||||
}: any) => (
|
||||
<a href={url} data-test={`${sliceName}-list-chart-title`}>
|
||||
{certifiedBy && (
|
||||
<>
|
||||
<CertifiedIcon
|
||||
certifiedBy={certifiedBy}
|
||||
details={certificationDetails}
|
||||
/>{' '}
|
||||
</>
|
||||
)}
|
||||
{sliceName}
|
||||
</a>
|
||||
),
|
||||
@@ -526,18 +512,6 @@ function ChartList(props: ChartListProps) {
|
||||
paginate: true,
|
||||
},
|
||||
...(props.user.userId ? [favoritesFilter] : []),
|
||||
{
|
||||
Header: t('Certified'),
|
||||
id: 'id',
|
||||
urlDisplay: 'certified',
|
||||
input: 'select',
|
||||
operator: FilterOperator.chartIsCertified,
|
||||
unfilteredLabel: t('Any'),
|
||||
selects: [
|
||||
{ label: t('Yes'), value: true },
|
||||
{ label: t('No'), value: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
Header: t('Search'),
|
||||
id: 'slice_name',
|
||||
|
||||
@@ -147,8 +147,6 @@ function DashboardCard({
|
||||
<ListViewCard
|
||||
loading={dashboard.loading || false}
|
||||
title={dashboard.dashboard_title}
|
||||
certifiedBy={dashboard.certified_by}
|
||||
certificationDetails={dashboard.certification_details}
|
||||
titleRight={
|
||||
<Label>{dashboard.published ? t('published') : t('draft')}</Label>
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ import ImportModelsModal from 'src/components/ImportModal/index';
|
||||
import OmniContainer from 'src/components/OmniContainer';
|
||||
|
||||
import Dashboard from 'src/dashboard/containers/Dashboard';
|
||||
import CertifiedIcon from 'src/components/CertifiedIcon';
|
||||
import DashboardCard from './DashboardCard';
|
||||
import { DashboardStatus } from './types';
|
||||
|
||||
@@ -174,8 +173,6 @@ function DashboardList(props: DashboardListProps) {
|
||||
json_metadata = '',
|
||||
changed_on_delta_humanized,
|
||||
url = '',
|
||||
certified_by = '',
|
||||
certification_details = '',
|
||||
} = json.result;
|
||||
return {
|
||||
...dashboard,
|
||||
@@ -187,8 +184,6 @@ function DashboardList(props: DashboardListProps) {
|
||||
json_metadata,
|
||||
changed_on_delta_humanized,
|
||||
url,
|
||||
certified_by,
|
||||
certification_details,
|
||||
};
|
||||
}
|
||||
return dashboard;
|
||||
@@ -255,26 +250,9 @@ function DashboardList(props: DashboardListProps) {
|
||||
{
|
||||
Cell: ({
|
||||
row: {
|
||||
original: {
|
||||
url,
|
||||
dashboard_title: dashboardTitle,
|
||||
certified_by: certifiedBy,
|
||||
certification_details: certificationDetails,
|
||||
},
|
||||
original: { url, dashboard_title: dashboardTitle },
|
||||
},
|
||||
}: any) => (
|
||||
<Link to={url}>
|
||||
{certifiedBy && (
|
||||
<>
|
||||
<CertifiedIcon
|
||||
certifiedBy={certifiedBy}
|
||||
details={certificationDetails}
|
||||
/>{' '}
|
||||
</>
|
||||
)}
|
||||
{dashboardTitle}
|
||||
</Link>
|
||||
),
|
||||
}: any) => <Link to={url}>{dashboardTitle}</Link>,
|
||||
Header: t('Title'),
|
||||
accessor: 'dashboard_title',
|
||||
},
|
||||
@@ -500,18 +478,6 @@ function DashboardList(props: DashboardListProps) {
|
||||
],
|
||||
},
|
||||
...(props.user.userId ? [favoritesFilter] : []),
|
||||
{
|
||||
Header: t('Certified'),
|
||||
id: 'id',
|
||||
urlDisplay: 'certified',
|
||||
input: 'select',
|
||||
operator: FilterOperator.dashboardIsCertified,
|
||||
unfilteredLabel: t('Any'),
|
||||
selects: [
|
||||
{ label: t('Yes'), value: true },
|
||||
{ label: t('No'), value: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
Header: t('Search'),
|
||||
id: 'dashboard_title',
|
||||
|
||||
@@ -566,8 +566,6 @@ export const useChartEditModal = (
|
||||
slice_name: chart.slice_name,
|
||||
description: chart.description,
|
||||
cache_timeout: chart.cache_timeout,
|
||||
certified_by: chart.certified_by,
|
||||
certification_details: chart.certification_details,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,6 @@ export interface DashboardTableProps {
|
||||
}
|
||||
|
||||
export interface Dashboard {
|
||||
certified_by?: string;
|
||||
certification_details?: string;
|
||||
changed_by_name: string;
|
||||
changed_by_url: string;
|
||||
changed_on_delta_humanized?: string;
|
||||
|
||||
@@ -52,12 +52,7 @@ from superset.charts.commands.export import ExportChartsCommand
|
||||
from superset.charts.commands.importers.dispatcher import ImportChartsCommand
|
||||
from superset.charts.commands.update import UpdateChartCommand
|
||||
from superset.charts.dao import ChartDAO
|
||||
from superset.charts.filters import (
|
||||
ChartAllTextFilter,
|
||||
ChartCertifiedFilter,
|
||||
ChartFavoriteFilter,
|
||||
ChartFilter,
|
||||
)
|
||||
from superset.charts.filters import ChartAllTextFilter, ChartFavoriteFilter, ChartFilter
|
||||
from superset.charts.post_processing import apply_post_process
|
||||
from superset.charts.schemas import (
|
||||
CHART_SCHEMAS,
|
||||
@@ -126,8 +121,6 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
|
||||
show_columns = [
|
||||
"cache_timeout",
|
||||
"certified_by",
|
||||
"certification_details",
|
||||
"dashboards.dashboard_title",
|
||||
"dashboards.id",
|
||||
"dashboards.json_metadata",
|
||||
@@ -143,8 +136,6 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
]
|
||||
show_select_columns = show_columns + ["table.id"]
|
||||
list_columns = [
|
||||
"certified_by",
|
||||
"certification_details",
|
||||
"cache_timeout",
|
||||
"changed_by.first_name",
|
||||
"changed_by.last_name",
|
||||
@@ -211,7 +202,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
base_order = ("changed_on", "desc")
|
||||
base_filters = [["id", ChartFilter, lambda: []]]
|
||||
search_filters = {
|
||||
"id": [ChartFavoriteFilter, ChartCertifiedFilter],
|
||||
"id": [ChartFavoriteFilter],
|
||||
"slice_name": [ChartAllTextFilter],
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
from typing import Any
|
||||
|
||||
from flask_babel import lazy_gettext as _
|
||||
from sqlalchemy import and_, or_
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm.query import Query
|
||||
|
||||
from superset import security_manager
|
||||
@@ -55,22 +55,6 @@ class ChartFavoriteFilter(BaseFavoriteFilter): # pylint: disable=too-few-public
|
||||
model = Slice
|
||||
|
||||
|
||||
class ChartCertifiedFilter(BaseFilter): # pylint: disable=too-few-public-methods
|
||||
"""
|
||||
Custom filter for the GET list that filters all certified charts
|
||||
"""
|
||||
|
||||
name = _("Is certified")
|
||||
arg_name = "chart_is_certified"
|
||||
|
||||
def apply(self, query: Query, value: Any) -> Query:
|
||||
if value is True:
|
||||
return query.filter(and_(Slice.certified_by.isnot(None)))
|
||||
if value is False:
|
||||
return query.filter(and_(Slice.certified_by.is_(None)))
|
||||
return query
|
||||
|
||||
|
||||
class ChartFilter(BaseFilter): # pylint: disable=too-few-public-methods
|
||||
def apply(self, query: Query, value: Any) -> Query:
|
||||
if security_manager.can_access_all_datasources():
|
||||
|
||||
@@ -114,8 +114,6 @@ form_data_description = (
|
||||
)
|
||||
description_markeddown_description = "Sanitized HTML version of the chart description."
|
||||
owners_name_description = "Name of an owner of the chart."
|
||||
certified_by_description = "Person or group that has certified this chart"
|
||||
certification_details_description = "Details of the certification"
|
||||
|
||||
#
|
||||
# OpenAPI method specification overrides
|
||||
@@ -159,8 +157,6 @@ class ChartEntityResponseSchema(Schema):
|
||||
)
|
||||
form_data = fields.Dict(description=form_data_description)
|
||||
slice_url = fields.String(description=slice_url_description)
|
||||
certified_by = fields.String(description=certified_by_description)
|
||||
certification_details = fields.String(description=certification_details_description)
|
||||
|
||||
|
||||
class ChartPostSchema(Schema):
|
||||
@@ -202,10 +198,6 @@ class ChartPostSchema(Schema):
|
||||
description=datasource_name_description, allow_none=True
|
||||
)
|
||||
dashboards = fields.List(fields.Integer(description=dashboards_description))
|
||||
certified_by = fields.String(description=certified_by_description, allow_none=True)
|
||||
certification_details = fields.String(
|
||||
description=certification_details_description, allow_none=True
|
||||
)
|
||||
|
||||
|
||||
class ChartPutSchema(Schema):
|
||||
@@ -243,10 +235,6 @@ class ChartPutSchema(Schema):
|
||||
allow_none=True,
|
||||
)
|
||||
dashboards = fields.List(fields.Integer(description=dashboards_description))
|
||||
certified_by = fields.String(description=certified_by_description, allow_none=True)
|
||||
certification_details = fields.String(
|
||||
description=certification_details_description, allow_none=True
|
||||
)
|
||||
|
||||
|
||||
class ChartGetDatasourceObjectDataResponseSchema(Schema):
|
||||
|
||||
@@ -53,7 +53,6 @@ from superset.dashboards.commands.update import UpdateDashboardCommand
|
||||
from superset.dashboards.dao import DashboardDAO
|
||||
from superset.dashboards.filters import (
|
||||
DashboardAccessFilter,
|
||||
DashboardCertifiedFilter,
|
||||
DashboardFavoriteFilter,
|
||||
DashboardTitleOrSlugFilter,
|
||||
FilterRelatedRoles,
|
||||
@@ -122,8 +121,6 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||
"position_json",
|
||||
"json_metadata",
|
||||
"thumbnail_url",
|
||||
"certified_by",
|
||||
"certification_details",
|
||||
"changed_by.first_name",
|
||||
"changed_by.last_name",
|
||||
"changed_by.username",
|
||||
@@ -153,8 +150,6 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||
]
|
||||
|
||||
add_columns = [
|
||||
"certified_by",
|
||||
"certification_details",
|
||||
"dashboard_title",
|
||||
"slug",
|
||||
"owners",
|
||||
@@ -178,7 +173,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
||||
)
|
||||
search_filters = {
|
||||
"dashboard_title": [DashboardTitleOrSlugFilter],
|
||||
"id": [DashboardFavoriteFilter, DashboardCertifiedFilter],
|
||||
"id": [DashboardFavoriteFilter],
|
||||
}
|
||||
base_order = ("changed_on", "desc")
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ from superset.dashboards.commands.exceptions import (
|
||||
)
|
||||
from superset.dashboards.dao import DashboardDAO
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
from superset.extensions import db
|
||||
from superset.models.dashboard import Dashboard
|
||||
from superset.views.base import check_ownership
|
||||
|
||||
@@ -52,14 +51,13 @@ class UpdateDashboardCommand(UpdateMixin, BaseCommand):
|
||||
self.validate()
|
||||
try:
|
||||
dashboard = DashboardDAO.update(self._model, self._properties, commit=False)
|
||||
dashboard = DashboardDAO.update_charts_owners(dashboard, commit=False)
|
||||
dashboard = DashboardDAO.update_charts_owners(dashboard, commit=True)
|
||||
if self._properties.get("json_metadata"):
|
||||
dashboard = DashboardDAO.set_dash_metadata(
|
||||
dashboard,
|
||||
data=json.loads(self._properties.get("json_metadata", "{}")),
|
||||
commit=False,
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
except DAOUpdateFailedError as ex:
|
||||
logger.exception(ex.exception)
|
||||
raise DashboardUpdateFailedError() from ex
|
||||
|
||||
@@ -239,9 +239,6 @@ class DashboardDAO(BaseDAO):
|
||||
}
|
||||
md["default_filters"] = json.dumps(applicable_filters)
|
||||
|
||||
# positions have its own column, no need to store it in metadata
|
||||
md.pop("positions", None)
|
||||
|
||||
# The css and dashboard_title properties are not part of the metadata
|
||||
# TODO (geido): remove by refactoring/deprecating save_dash endpoint
|
||||
if data.get("css") is not None:
|
||||
|
||||
@@ -158,19 +158,3 @@ class FilterRelatedRoles(BaseFilter): # pylint: disable=too-few-public-methods
|
||||
if value:
|
||||
return query.filter(role_model.name.ilike(f"%{value}%"),)
|
||||
return query
|
||||
|
||||
|
||||
class DashboardCertifiedFilter(BaseFilter): # pylint: disable=too-few-public-methods
|
||||
"""
|
||||
Custom filter for the GET list that filters all certified dashboards
|
||||
"""
|
||||
|
||||
name = _("Is certified")
|
||||
arg_name = "dashboard_is_certified"
|
||||
|
||||
def apply(self, query: Query, value: Any) -> Query:
|
||||
if value is True:
|
||||
return query.filter(and_(Dashboard.certified_by.isnot(None),))
|
||||
if value is False:
|
||||
return query.filter(and_(Dashboard.certified_by.is_(None),))
|
||||
return query
|
||||
|
||||
@@ -64,8 +64,6 @@ published_description = (
|
||||
charts_description = (
|
||||
"The names of the dashboard's charts. Names are used for legacy reasons."
|
||||
)
|
||||
certified_by_description = "Person or group that has certified this dashboard"
|
||||
certification_details_description = "Details of the certification"
|
||||
|
||||
openapi_spec_methods_override = {
|
||||
"get": {"get": {"description": "Get a dashboard detail information."}},
|
||||
@@ -155,8 +153,6 @@ class DashboardGetResponseSchema(Schema):
|
||||
css = fields.String(description=css_description)
|
||||
json_metadata = fields.String(description=json_metadata_description)
|
||||
position_json = fields.String(description=position_json_description)
|
||||
certified_by = fields.String(description=certified_by_description)
|
||||
certification_details = fields.String(description=certification_details_description)
|
||||
changed_by_name = fields.String()
|
||||
changed_by_url = fields.String()
|
||||
changed_by = fields.Nested(UserSchema)
|
||||
@@ -243,10 +239,6 @@ class DashboardPostSchema(BaseDashboardSchema):
|
||||
description=json_metadata_description, validate=validate_json_metadata,
|
||||
)
|
||||
published = fields.Boolean(description=published_description)
|
||||
certified_by = fields.String(description=certified_by_description, allow_none=True)
|
||||
certification_details = fields.String(
|
||||
description=certification_details_description, allow_none=True
|
||||
)
|
||||
|
||||
|
||||
class DashboardPutSchema(BaseDashboardSchema):
|
||||
@@ -272,10 +264,6 @@ class DashboardPutSchema(BaseDashboardSchema):
|
||||
validate=validate_json_metadata,
|
||||
)
|
||||
published = fields.Boolean(description=published_description, allow_none=True)
|
||||
certified_by = fields.String(description=certified_by_description, allow_none=True)
|
||||
certification_details = fields.String(
|
||||
description=certification_details_description, allow_none=True
|
||||
)
|
||||
|
||||
|
||||
class ChartFavStarResponseResult(Schema):
|
||||
|
||||
@@ -1,44 +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.
|
||||
"""add_certifications_columns_to_dashboard
|
||||
|
||||
Revision ID: aea15018d53b
|
||||
Revises: f9847149153d
|
||||
Create Date: 2021-11-05 11:11:55.496618
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "aea15018d53b"
|
||||
down_revision = "f9847149153d"
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table("dashboards") as batch_op:
|
||||
batch_op.add_column(sa.Column("certified_by", sa.Text(), nullable=True))
|
||||
batch_op.add_column(
|
||||
sa.Column("certification_details", sa.Text(), nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("dashboards") as batch_op:
|
||||
batch_op.drop_column("certified_by")
|
||||
batch_op.drop_column("certification_details")
|
||||
@@ -1,44 +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.
|
||||
"""add_certifications_columns_to_slice
|
||||
|
||||
Revision ID: f9847149153d
|
||||
Revises: 32646df09c64
|
||||
Create Date: 2021-11-03 14:07:09.905194
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "f9847149153d"
|
||||
down_revision = "32646df09c64"
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table("slices") as batch_op:
|
||||
batch_op.add_column(sa.Column("certified_by", sa.Text(), nullable=True))
|
||||
batch_op.add_column(
|
||||
sa.Column("certification_details", sa.Text(), nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("slices") as batch_op:
|
||||
batch_op.drop_column("certified_by")
|
||||
batch_op.drop_column("certification_details")
|
||||
@@ -142,8 +142,6 @@ class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
|
||||
position_json = Column(utils.MediumText())
|
||||
description = Column(Text)
|
||||
css = Column(Text)
|
||||
certified_by = Column(Text)
|
||||
certification_details = Column(Text)
|
||||
json_metadata = Column(Text)
|
||||
slug = Column(String(255), unique=True)
|
||||
slices = relationship(Slice, secondary=dashboard_slices, backref="dashboards")
|
||||
@@ -267,8 +265,6 @@ class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
|
||||
return {
|
||||
"id": self.id,
|
||||
"metadata": self.params_dict,
|
||||
"certified_by": self.certified_by,
|
||||
"certification_details": self.certification_details,
|
||||
"css": self.css,
|
||||
"dashboard_title": self.dashboard_title,
|
||||
"published": self.published,
|
||||
|
||||
@@ -75,8 +75,6 @@ class Slice( # pylint: disable=too-many-public-methods
|
||||
# when the database row was last written
|
||||
last_saved_at = Column(DateTime, nullable=True)
|
||||
last_saved_by_fk = Column(Integer, ForeignKey("ab_user.id"), nullable=True)
|
||||
certified_by = Column(Text)
|
||||
certification_details = Column(Text)
|
||||
last_saved_by = relationship(
|
||||
security_manager.user_model, foreign_keys=[last_saved_by_fk]
|
||||
)
|
||||
@@ -207,8 +205,6 @@ class Slice( # pylint: disable=too-many-public-methods
|
||||
"slice_id": self.id,
|
||||
"slice_name": self.slice_name,
|
||||
"slice_url": self.slice_url,
|
||||
"certified_by": self.certified_by,
|
||||
"certification_details": self.certification_details,
|
||||
}
|
||||
|
||||
@property
|
||||
|
||||
@@ -109,7 +109,7 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixin):
|
||||
charts = []
|
||||
admin = self.get_user("admin")
|
||||
for cx in range(CHARTS_FIXTURE_COUNT - 1):
|
||||
charts.append(self.insert_chart(f"name{cx}", [admin.id], 1,))
|
||||
charts.append(self.insert_chart(f"name{cx}", [admin.id], 1))
|
||||
fav_charts = []
|
||||
for cx in range(round(CHARTS_FIXTURE_COUNT / 2)):
|
||||
fav_star = FavStar(
|
||||
@@ -127,29 +127,6 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixin):
|
||||
db.session.delete(fav_chart)
|
||||
db.session.commit()
|
||||
|
||||
@pytest.fixture()
|
||||
def create_certified_charts(self):
|
||||
with self.create_app().app_context():
|
||||
certified_charts = []
|
||||
admin = self.get_user("admin")
|
||||
for cx in range(CHARTS_FIXTURE_COUNT):
|
||||
certified_charts.append(
|
||||
self.insert_chart(
|
||||
f"certified{cx}",
|
||||
[admin.id],
|
||||
1,
|
||||
certified_by="John Doe",
|
||||
certification_details="Sample certification",
|
||||
)
|
||||
)
|
||||
|
||||
yield certified_charts
|
||||
|
||||
# rollback changes
|
||||
for chart in certified_charts:
|
||||
db.session.delete(chart)
|
||||
db.session.commit()
|
||||
|
||||
@pytest.fixture()
|
||||
def create_chart_with_report(self):
|
||||
with self.create_app().app_context():
|
||||
@@ -472,8 +449,6 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixin):
|
||||
"datasource_id": 1,
|
||||
"datasource_type": "table",
|
||||
"dashboards": dashboards_ids,
|
||||
"certified_by": "John Doe",
|
||||
"certification_details": "Sample certification",
|
||||
}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/chart/"
|
||||
@@ -589,8 +564,6 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixin):
|
||||
"datasource_id": birth_names_table_id,
|
||||
"datasource_type": "table",
|
||||
"dashboards": [dash_id],
|
||||
"certified_by": "Mario Rossi",
|
||||
"certification_details": "Edited certification",
|
||||
}
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/chart/{chart_id}"
|
||||
@@ -609,8 +582,6 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixin):
|
||||
self.assertEqual(model.datasource_id, birth_names_table_id)
|
||||
self.assertEqual(model.datasource_type, "table")
|
||||
self.assertEqual(model.datasource_name, full_table_name)
|
||||
self.assertEqual(model.certified_by, "Mario Rossi")
|
||||
self.assertEqual(model.certification_details, "Edited certification")
|
||||
self.assertIn(model.id, [slice.id for slice in related_dashboard.slices])
|
||||
db.session.delete(model)
|
||||
db.session.commit()
|
||||
@@ -761,8 +732,6 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixin):
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
expected_result = {
|
||||
"cache_timeout": None,
|
||||
"certified_by": None,
|
||||
"certification_details": None,
|
||||
"dashboards": [],
|
||||
"description": None,
|
||||
"owners": [
|
||||
@@ -957,36 +926,6 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixin):
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
self.assertEqual(data["count"], 8)
|
||||
|
||||
@pytest.mark.usefixtures("create_certified_charts")
|
||||
def test_gets_certified_charts_filter(self):
|
||||
arguments = {
|
||||
"filters": [{"col": "id", "opr": "chart_is_certified", "value": True,}],
|
||||
"keys": ["none"],
|
||||
"columns": ["slice_name"],
|
||||
}
|
||||
self.login(username="admin")
|
||||
|
||||
uri = f"api/v1/chart/?q={prison.dumps(arguments)}"
|
||||
rv = self.get_assert_metric(uri, "get_list")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
self.assertEqual(data["count"], CHARTS_FIXTURE_COUNT)
|
||||
|
||||
@pytest.mark.usefixtures("create_charts")
|
||||
def test_gets_not_certified_charts_filter(self):
|
||||
arguments = {
|
||||
"filters": [{"col": "id", "opr": "chart_is_certified", "value": False,}],
|
||||
"keys": ["none"],
|
||||
"columns": ["slice_name"],
|
||||
}
|
||||
self.login(username="admin")
|
||||
|
||||
uri = f"api/v1/chart/?q={prison.dumps(arguments)}"
|
||||
rv = self.get_assert_metric(uri, "get_list")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
self.assertEqual(data["count"], 17)
|
||||
|
||||
@pytest.mark.usefixtures("load_energy_charts")
|
||||
def test_user_gets_none_filtered_energy_slices(self):
|
||||
# test filtering on datasource_name
|
||||
|
||||
@@ -86,8 +86,6 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
|
||||
css: str = "",
|
||||
json_metadata: str = "",
|
||||
published: bool = False,
|
||||
certified_by: Optional[str] = None,
|
||||
certification_details: Optional[str] = None,
|
||||
) -> Dashboard:
|
||||
obj_owners = list()
|
||||
obj_roles = list()
|
||||
@@ -109,8 +107,6 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
|
||||
slices=slices,
|
||||
published=published,
|
||||
created_by=created_by,
|
||||
certified_by=certified_by,
|
||||
certification_details=certification_details,
|
||||
)
|
||||
db.session.add(dashboard)
|
||||
db.session.commit()
|
||||
@@ -129,8 +125,6 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
|
||||
f"slug{cx}",
|
||||
[admin.id],
|
||||
slices=charts if cx < half_dash_count else [],
|
||||
certified_by="John Doe",
|
||||
certification_details="Sample certification",
|
||||
)
|
||||
if cx < half_dash_count:
|
||||
chart = self.insert_chart(f"slice{cx}", [admin.id], 1, params="{}")
|
||||
@@ -321,8 +315,6 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
|
||||
rv = self.get_assert_metric(uri, "get")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
expected_result = {
|
||||
"certified_by": None,
|
||||
"certification_details": None,
|
||||
"changed_by": None,
|
||||
"changed_by_name": "",
|
||||
"changed_by_url": "",
|
||||
@@ -619,38 +611,6 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
|
||||
expected_model.dashboard_title == data["result"][i]["dashboard_title"]
|
||||
)
|
||||
|
||||
@pytest.mark.usefixtures("create_dashboards")
|
||||
def test_gets_certified_dashboards_filter(self):
|
||||
arguments = {
|
||||
"filters": [{"col": "id", "opr": "dashboard_is_certified", "value": True,}],
|
||||
"keys": ["none"],
|
||||
"columns": ["dashboard_title"],
|
||||
}
|
||||
self.login(username="admin")
|
||||
|
||||
uri = f"api/v1/dashboard/?q={prison.dumps(arguments)}"
|
||||
rv = self.get_assert_metric(uri, "get_list")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
self.assertEqual(data["count"], DASHBOARDS_FIXTURE_COUNT)
|
||||
|
||||
@pytest.mark.usefixtures("create_dashboards")
|
||||
def test_gets_not_certified_dashboards_filter(self):
|
||||
arguments = {
|
||||
"filters": [
|
||||
{"col": "id", "opr": "dashboard_is_certified", "value": False,}
|
||||
],
|
||||
"keys": ["none"],
|
||||
"columns": ["dashboard_title"],
|
||||
}
|
||||
self.login(username="admin")
|
||||
|
||||
uri = f"api/v1/dashboard/?q={prison.dumps(arguments)}"
|
||||
rv = self.get_assert_metric(uri, "get_list")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
self.assertEqual(data["count"], 6)
|
||||
|
||||
def create_dashboard_import(self):
|
||||
buf = BytesIO()
|
||||
with ZipFile(buf, "w") as bundle:
|
||||
|
||||
@@ -36,8 +36,6 @@ class InsertChartMixin:
|
||||
viz_type: Optional[str] = None,
|
||||
params: Optional[str] = None,
|
||||
cache_timeout: Optional[int] = None,
|
||||
certified_by: Optional[str] = None,
|
||||
certification_details: Optional[str] = None,
|
||||
) -> Slice:
|
||||
obj_owners = list()
|
||||
for owner in owners:
|
||||
@@ -48,8 +46,6 @@ class InsertChartMixin:
|
||||
)
|
||||
slice = Slice(
|
||||
cache_timeout=cache_timeout,
|
||||
certified_by=certified_by,
|
||||
certification_details=certification_details,
|
||||
created_by=created_by,
|
||||
datasource_id=datasource.id,
|
||||
datasource_name=datasource.name,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user