feat: add certification to metrics (#10630)

This commit is contained in:
Erik Ritter 2020-08-19 20:45:33 -07:00 committed by GitHub
parent 5136c5c16e
commit 38da552a57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 204 additions and 22 deletions

View File

@ -0,0 +1,19 @@
<!--
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.
-->
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><path fill="currentColor" d="M23,12l-2.44-2.79l0.34-3.69l-3.61-0.82L15.4,1.5L12,2.96L8.6,1.5L6.71,4.69L3.1,5.5L3.44,9.2L1,12l2.44,2.79l-0.34,3.7 l3.61,0.82L8.6,22.5l3.4-1.47l3.4,1.46l1.89-3.19l3.61-0.82l-0.34-3.69L23,12z M9.38,16.01L7,13.61c-0.39-0.39-0.39-1.02,0-1.41 l0.07-0.07c0.39-0.39,1.03-0.39,1.42,0l1.61,1.62l5.15-5.16c0.39-0.39,1.03-0.39,1.42,0l0.07,0.07c0.39,0.39,0.39,1.02,0,1.41 l-5.92,5.94C10.41,16.4,9.78,16.4,9.38,16.01z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -33,7 +33,12 @@ interface CRUDCollectionProps {
expandFieldset: ReactNode;
extraButtons: ReactNode;
itemGenerator?: () => any;
itemRenderers?: any;
itemRenderers?: ((
val: unknown,
onChange: () => void,
label: string,
record: any,
) => ReactNode)[];
onChange?: (arg0: any) => void;
tableColumns: Array<any>;
}
@ -183,7 +188,7 @@ export default class CRUDCollection extends React.PureComponent<
const renderer = this.props.itemRenderers && this.props.itemRenderers[col];
const val = record[col];
const onChange = this.onCellChange.bind(this, record.id, col);
return renderer ? renderer(val, onChange, this.getLabel(col)) : val;
return renderer ? renderer(val, onChange, this.getLabel(col), record) : val;
}
renderItem(record: any) {
const {

View File

@ -32,7 +32,7 @@ import './crud.less';
const propTypes = {
value: PropTypes.any.isRequired,
label: PropTypes.string.isRequired,
descr: PropTypes.node,
description: PropTypes.node,
fieldKey: PropTypes.string.isRequired,
control: PropTypes.node.isRequired,
onChange: PropTypes.func,
@ -54,7 +54,14 @@ export default class Field extends React.PureComponent {
this.props.onChange(this.props.fieldKey, newValue);
}
render() {
const { compact, value, label, control, descr, fieldKey } = this.props;
const {
compact,
value,
label,
control,
description,
fieldKey,
} = this.props;
const hookedControl = React.cloneElement(control, {
value,
onChange: this.onChange,
@ -63,12 +70,12 @@ export default class Field extends React.PureComponent {
<FormGroup controlId={fieldKey}>
<FormLabel className="m-r-5">
{label || fieldKey}
{compact && descr && (
{compact && description && (
<OverlayTrigger
placement="right"
overlay={
<Tooltip id="field-descr" bsSize="lg">
{descr}
{description}
</Tooltip>
}
>
@ -78,7 +85,7 @@ export default class Field extends React.PureComponent {
</FormLabel>
{hookedControl}
<FormControl.Feedback />
{!compact && descr && <HelpBlock>{descr}</HelpBlock>}
{!compact && description && <HelpBlock>{description}</HelpBlock>}
</FormGroup>
);
}

View File

@ -0,0 +1,49 @@
/**
* 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 from 'react';
import { t } from '@superset-ui/translation';
import { supersetTheme } from '@superset-ui/style';
import Icon from 'src/components/Icon';
import TooltipWrapper from 'src/components/TooltipWrapper';
interface CertifiedIconWithTooltipProps {
certifiedBy?: string;
details?: string;
}
function CertifiedIconWithTooltip({
certifiedBy,
details,
}: CertifiedIconWithTooltipProps) {
return (
<TooltipWrapper
label="certified-details"
tooltip={
<>
{certifiedBy && <div>{t('Certified by %s', certifiedBy)}</div>}
<div>{details}</div>
</>
}
>
<Icon color={supersetTheme.colors.primary.base} name="certified" />
</TooltipWrapper>
);
}
export default CertifiedIconWithTooltip;

View File

@ -19,6 +19,7 @@
import React, { SVGProps } from 'react';
import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg';
import { ReactComponent as CardViewIcon } from 'images/icons/card-view.svg';
import { ReactComponent as CertifiedIcon } from 'images/icons/certified.svg';
import { ReactComponent as CheckboxHalfIcon } from 'images/icons/checkbox-half.svg';
import { ReactComponent as CheckboxOffIcon } from 'images/icons/checkbox-off.svg';
import { ReactComponent as CheckboxOnIcon } from 'images/icons/checkbox-on.svg';
@ -46,6 +47,7 @@ import { ReactComponent as WarningIcon } from 'images/icons/warning.svg';
type IconName =
| 'cancel-x'
| 'card-view'
| 'certified'
| 'check'
| 'checkbox-half'
| 'checkbox-off'
@ -88,6 +90,7 @@ export const iconsRegistry: Record<
'list-view': ListViewIcon,
'sort-asc': SortAscIcon,
'sort-desc': SortDescIcon,
certified: CertifiedIcon,
check: CheckIcon,
close: CloseIcon,
compass: CompassIcon,

View File

@ -28,6 +28,7 @@ import Label from 'src/components/Label';
import Button from 'src/components/Button';
import Loading from 'src/components/Loading';
import TableSelector from 'src/components/TableSelector';
import CertifiedIconWithTooltip from 'src/components/CertifiedIconWithTooltip';
import getClientErrorObject from '../utils/getClientErrorObject';
import CheckboxControl from '../explore/components/controls/CheckboxControl';
@ -59,6 +60,15 @@ const DatasourceContainer = styled.div`
}
`;
const FlexRowContainer = styled.div`
align-items: center;
display: flex;
> svg {
margin-right: ${({ theme }) => theme.gridUnit}px;
}
`;
const checkboxGenerator = (d, onChange) => (
<CheckboxControl value={d} onChange={onChange} />
);
@ -130,7 +140,7 @@ function ColumnCollectionTable({
<Field
fieldKey="python_date_format"
label={t('Datetime Format')}
descr={
description={
/* Note the fragmented translations may not work. */
<div>
{t('The pattern of timestamp format. For strings use ')}
@ -460,7 +470,7 @@ export class DatasourceEditor extends React.PureComponent {
handleError={this.props.addDangerToast}
/>
}
descr={t(
description={t(
'The pointer to a physical table. Keep in mind that the chart is ' +
'associated to this Superset logical table, and this logical table points ' +
'the physical table referenced here.',
@ -477,7 +487,7 @@ export class DatasourceEditor extends React.PureComponent {
<Field
fieldKey="default_endpoint"
label={t('Default URL')}
descr={t(
description={t(
'Default URL to redirect to when accessing from the datasource list page',
)}
control={<TextControl />}
@ -485,14 +495,14 @@ export class DatasourceEditor extends React.PureComponent {
<Field
fieldKey="filter_select_enabled"
label={t('Autocomplete filters')}
descr={t('Whether to populate autocomplete filters options')}
description={t('Whether to populate autocomplete filters options')}
control={<CheckboxControl />}
/>
{this.state.isSqla && (
<Field
fieldKey="fetch_values_predicate"
label={t('Autocomplete Query Predicate')}
descr={t(
description={t(
'When using "Autocomplete filters", this can be used to improve performance ' +
'of the query fetching the values. Use this option to apply a ' +
'predicate (WHERE clause) to the query selecting the distinct ' +
@ -505,7 +515,7 @@ export class DatasourceEditor extends React.PureComponent {
<Field
fieldKey="owners"
label={t('Owners')}
descr={t('Owners of the datasource')}
description={t('Owners of the datasource')}
control={
<SelectAsyncControl
dataEndpoint="/users/api/read"
@ -536,7 +546,7 @@ export class DatasourceEditor extends React.PureComponent {
<Field
fieldKey="sql"
label={t('SQL')}
descr={t(
description={t(
'When specifying SQL, the datasource acts as a view. ' +
'Superset will use this statement as a subquery while grouping and filtering ' +
'on the generated parent queries.',
@ -550,7 +560,7 @@ export class DatasourceEditor extends React.PureComponent {
<Field
fieldKey="json"
label={t('JSON')}
descr={
description={
<div>{t('The JSON metric or post aggregation definition.')}</div>
}
control={
@ -561,7 +571,7 @@ export class DatasourceEditor extends React.PureComponent {
<Field
fieldKey="cache_timeout"
label={t('Cache Timeout')}
descr={t(
description={t(
'The duration of time in seconds before the cache is invalidated',
)}
control={<TextControl />}
@ -575,7 +585,7 @@ export class DatasourceEditor extends React.PureComponent {
<Field
fieldKey="template_params"
label={t('Template parameters')}
descr={t(
description={t(
'A set of parameters that become available in the query using Jinja templating syntax',
)}
control={<TextControl />}
@ -642,7 +652,7 @@ export class DatasourceEditor extends React.PureComponent {
}}
expandFieldset={
<FormContainer>
<Fieldset>
<Fieldset compact>
<Field
fieldKey="verbose_name"
label={t('Label')}
@ -666,6 +676,22 @@ export class DatasourceEditor extends React.PureComponent {
)}
control={<TextControl placeholder={t('Warning Message')} />}
/>
<Field
label={t('Certified By')}
fieldKey="certified_by"
description={t(
'Person or group that has certified this metric',
)}
control={<TextControl placeholder={t('Certified By')} />}
/>
<Field
label={t('Certification Details')}
fieldKey="certification_details"
description={t('Details of the certification')}
control={
<TextControl placeholder={t('Certification Details')} />
}
/>
</Fieldset>
</FormContainer>
}
@ -678,8 +704,16 @@ export class DatasourceEditor extends React.PureComponent {
expression: '',
})}
itemRenderers={{
metric_name: (v, onChange) => (
<EditableTitle canEdit title={v} onSaveTitle={onChange} />
metric_name: (v, onChange, _, record) => (
<FlexRowContainer>
{record.is_certified && (
<CertifiedIconWithTooltip
certifiedBy={record.certified_by}
details={record.certification_details}
/>
)}
<EditableTitle canEdit title={v} onSaveTitle={onChange} />
</FlexRowContainer>
),
verbose_name: (v, onChange) => (
<EditableTitle canEdit title={v} onSaveTitle={onChange} />

View File

@ -36,6 +36,18 @@ interface DatasourceModalProps {
show: boolean;
}
function buildMetricExtraJsonObject(metric: Record<string, unknown>) {
if (metric?.certified_by || metric?.certification_details) {
return JSON.stringify({
certification: {
certified_by: metric?.certified_by ?? null,
details: metric?.certification_details ?? null,
},
});
}
return null;
}
const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
addSuccessToast,
datasource,
@ -48,11 +60,19 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
const dialog = useRef<any>(null);
const onConfirmSave = () => {
// Pull out extra fields into the extra object
SupersetClient.post({
endpoint: '/datasource/save/',
postPayload: {
data: {
...currentDatasource,
metrics: currentDatasource?.metrics?.map(
(metric: Record<string, unknown>) => ({
...metric,
extra: buildMetricExtraJsonObject(metric),
}),
),
type: currentDatasource.type || currentDatasource.datasource_type,
},
},
@ -75,8 +95,14 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
);
};
const onDatasourceChange = (data: object, err: Array<any>) => {
setCurrentDatasource(data);
const onDatasourceChange = (data: Record<string, any>, err: Array<any>) => {
setCurrentDatasource({
...data,
metrics: data?.metrics.map((metric: Record<string, unknown>) => ({
...metric,
is_certified: metric?.certified_by || metric?.certification_details,
})),
});
setErrors(err);
};

View File

@ -14,6 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import json
import logging
from collections import OrderedDict
from dataclasses import dataclass, field
@ -351,6 +352,7 @@ class SqlMetric(Model, BaseMetric):
foreign_keys=[table_id],
)
expression = Column(Text, nullable=False)
extra = Column(Text)
export_fields = [
"metric_name",
@ -360,6 +362,7 @@ class SqlMetric(Model, BaseMetric):
"expression",
"description",
"d3format",
"extra",
"warning_text",
]
update_from_object_fields = list(
@ -399,6 +402,32 @@ class SqlMetric(Model, BaseMetric):
return import_datasource.import_simple_obj(db.session, i_metric, lookup_obj)
def get_extra_dict(self) -> Dict[str, Any]:
try:
return json.loads(self.extra)
except (TypeError, json.JSONDecodeError):
return {}
@property
def is_certified(self) -> bool:
return bool(self.get_extra_dict().get("certification"))
@property
def certified_by(self) -> Optional[str]:
return self.get_extra_dict().get("certification", {}).get("certified_by")
@property
def certification_details(self) -> Optional[str]:
return self.get_extra_dict().get("certification", {}).get("details")
@property
def data(self) -> Dict[str, Any]:
attrs = ("is_certified", "certified_by", "certification_details")
attr_dict = {s: getattr(self, s) for s in attrs}
attr_dict.update(super().data)
return attr_dict
sqlatable_user = Table(
"sqlatable_user",

View File

@ -189,6 +189,7 @@ class SqlMetricInlineView( # pylint: disable=too-many-ancestors
"expression",
"table",
"d3format",
"extra",
"warning_text",
]
description_columns = {
@ -205,6 +206,14 @@ class SqlMetricInlineView( # pylint: disable=too-many-ancestors
"formats",
True,
),
"extra": utils.markdown(
"Extra data to specify metric metadata. Currently supports "
'certification data of the format: `{ "certification": "certified_by": '
'"Taylor Swift", "details": "This metric is the source of truth." '
"} }`. This should be modified from the edit datasource model in "
"Explore to ensure correct formatting.",
True,
),
}
add_columns = edit_columns
page_size = 500
@ -216,6 +225,7 @@ class SqlMetricInlineView( # pylint: disable=too-many-ancestors
"expression": _("SQL Expression"),
"table": _("Table"),
"d3format": _("D3 Format"),
"extra": _("Extra"),
"warning_text": _("Warning Message"),
}