mirror of
https://github.com/apache/superset.git
synced 2024-09-06 22:07:34 -04:00
feat: add certification to metrics (#10630)
This commit is contained in:
parent
5136c5c16e
commit
38da552a57
19
superset-frontend/images/icons/certified.svg
Normal file
19
superset-frontend/images/icons/certified.svg
Normal 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 |
@ -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 {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
@ -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,
|
||||
|
@ -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} />
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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"),
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user