mirror of
https://github.com/apache/superset.git
synced 2024-09-18 19:49:37 -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;
|
expandFieldset: ReactNode;
|
||||||
extraButtons: ReactNode;
|
extraButtons: ReactNode;
|
||||||
itemGenerator?: () => any;
|
itemGenerator?: () => any;
|
||||||
itemRenderers?: any;
|
itemRenderers?: ((
|
||||||
|
val: unknown,
|
||||||
|
onChange: () => void,
|
||||||
|
label: string,
|
||||||
|
record: any,
|
||||||
|
) => ReactNode)[];
|
||||||
onChange?: (arg0: any) => void;
|
onChange?: (arg0: any) => void;
|
||||||
tableColumns: Array<any>;
|
tableColumns: Array<any>;
|
||||||
}
|
}
|
||||||
@ -183,7 +188,7 @@ export default class CRUDCollection extends React.PureComponent<
|
|||||||
const renderer = this.props.itemRenderers && this.props.itemRenderers[col];
|
const renderer = this.props.itemRenderers && this.props.itemRenderers[col];
|
||||||
const val = record[col];
|
const val = record[col];
|
||||||
const onChange = this.onCellChange.bind(this, record.id, 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) {
|
renderItem(record: any) {
|
||||||
const {
|
const {
|
||||||
|
@ -32,7 +32,7 @@ import './crud.less';
|
|||||||
const propTypes = {
|
const propTypes = {
|
||||||
value: PropTypes.any.isRequired,
|
value: PropTypes.any.isRequired,
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
descr: PropTypes.node,
|
description: PropTypes.node,
|
||||||
fieldKey: PropTypes.string.isRequired,
|
fieldKey: PropTypes.string.isRequired,
|
||||||
control: PropTypes.node.isRequired,
|
control: PropTypes.node.isRequired,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
@ -54,7 +54,14 @@ export default class Field extends React.PureComponent {
|
|||||||
this.props.onChange(this.props.fieldKey, newValue);
|
this.props.onChange(this.props.fieldKey, newValue);
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const { compact, value, label, control, descr, fieldKey } = this.props;
|
const {
|
||||||
|
compact,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
control,
|
||||||
|
description,
|
||||||
|
fieldKey,
|
||||||
|
} = this.props;
|
||||||
const hookedControl = React.cloneElement(control, {
|
const hookedControl = React.cloneElement(control, {
|
||||||
value,
|
value,
|
||||||
onChange: this.onChange,
|
onChange: this.onChange,
|
||||||
@ -63,12 +70,12 @@ export default class Field extends React.PureComponent {
|
|||||||
<FormGroup controlId={fieldKey}>
|
<FormGroup controlId={fieldKey}>
|
||||||
<FormLabel className="m-r-5">
|
<FormLabel className="m-r-5">
|
||||||
{label || fieldKey}
|
{label || fieldKey}
|
||||||
{compact && descr && (
|
{compact && description && (
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
placement="right"
|
placement="right"
|
||||||
overlay={
|
overlay={
|
||||||
<Tooltip id="field-descr" bsSize="lg">
|
<Tooltip id="field-descr" bsSize="lg">
|
||||||
{descr}
|
{description}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -78,7 +85,7 @@ export default class Field extends React.PureComponent {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
{hookedControl}
|
{hookedControl}
|
||||||
<FormControl.Feedback />
|
<FormControl.Feedback />
|
||||||
{!compact && descr && <HelpBlock>{descr}</HelpBlock>}
|
{!compact && description && <HelpBlock>{description}</HelpBlock>}
|
||||||
</FormGroup>
|
</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 React, { SVGProps } from 'react';
|
||||||
import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg';
|
import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg';
|
||||||
import { ReactComponent as CardViewIcon } from 'images/icons/card-view.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 CheckboxHalfIcon } from 'images/icons/checkbox-half.svg';
|
||||||
import { ReactComponent as CheckboxOffIcon } from 'images/icons/checkbox-off.svg';
|
import { ReactComponent as CheckboxOffIcon } from 'images/icons/checkbox-off.svg';
|
||||||
import { ReactComponent as CheckboxOnIcon } from 'images/icons/checkbox-on.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 =
|
type IconName =
|
||||||
| 'cancel-x'
|
| 'cancel-x'
|
||||||
| 'card-view'
|
| 'card-view'
|
||||||
|
| 'certified'
|
||||||
| 'check'
|
| 'check'
|
||||||
| 'checkbox-half'
|
| 'checkbox-half'
|
||||||
| 'checkbox-off'
|
| 'checkbox-off'
|
||||||
@ -88,6 +90,7 @@ export const iconsRegistry: Record<
|
|||||||
'list-view': ListViewIcon,
|
'list-view': ListViewIcon,
|
||||||
'sort-asc': SortAscIcon,
|
'sort-asc': SortAscIcon,
|
||||||
'sort-desc': SortDescIcon,
|
'sort-desc': SortDescIcon,
|
||||||
|
certified: CertifiedIcon,
|
||||||
check: CheckIcon,
|
check: CheckIcon,
|
||||||
close: CloseIcon,
|
close: CloseIcon,
|
||||||
compass: CompassIcon,
|
compass: CompassIcon,
|
||||||
|
@ -28,6 +28,7 @@ import Label from 'src/components/Label';
|
|||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import Loading from 'src/components/Loading';
|
import Loading from 'src/components/Loading';
|
||||||
import TableSelector from 'src/components/TableSelector';
|
import TableSelector from 'src/components/TableSelector';
|
||||||
|
import CertifiedIconWithTooltip from 'src/components/CertifiedIconWithTooltip';
|
||||||
|
|
||||||
import getClientErrorObject from '../utils/getClientErrorObject';
|
import getClientErrorObject from '../utils/getClientErrorObject';
|
||||||
import CheckboxControl from '../explore/components/controls/CheckboxControl';
|
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) => (
|
const checkboxGenerator = (d, onChange) => (
|
||||||
<CheckboxControl value={d} onChange={onChange} />
|
<CheckboxControl value={d} onChange={onChange} />
|
||||||
);
|
);
|
||||||
@ -130,7 +140,7 @@ function ColumnCollectionTable({
|
|||||||
<Field
|
<Field
|
||||||
fieldKey="python_date_format"
|
fieldKey="python_date_format"
|
||||||
label={t('Datetime Format')}
|
label={t('Datetime Format')}
|
||||||
descr={
|
description={
|
||||||
/* Note the fragmented translations may not work. */
|
/* Note the fragmented translations may not work. */
|
||||||
<div>
|
<div>
|
||||||
{t('The pattern of timestamp format. For strings use ')}
|
{t('The pattern of timestamp format. For strings use ')}
|
||||||
@ -460,7 +470,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||||||
handleError={this.props.addDangerToast}
|
handleError={this.props.addDangerToast}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
descr={t(
|
description={t(
|
||||||
'The pointer to a physical table. Keep in mind that the chart is ' +
|
'The pointer to a physical table. Keep in mind that the chart is ' +
|
||||||
'associated to this Superset logical table, and this logical table points ' +
|
'associated to this Superset logical table, and this logical table points ' +
|
||||||
'the physical table referenced here.',
|
'the physical table referenced here.',
|
||||||
@ -477,7 +487,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||||||
<Field
|
<Field
|
||||||
fieldKey="default_endpoint"
|
fieldKey="default_endpoint"
|
||||||
label={t('Default URL')}
|
label={t('Default URL')}
|
||||||
descr={t(
|
description={t(
|
||||||
'Default URL to redirect to when accessing from the datasource list page',
|
'Default URL to redirect to when accessing from the datasource list page',
|
||||||
)}
|
)}
|
||||||
control={<TextControl />}
|
control={<TextControl />}
|
||||||
@ -485,14 +495,14 @@ export class DatasourceEditor extends React.PureComponent {
|
|||||||
<Field
|
<Field
|
||||||
fieldKey="filter_select_enabled"
|
fieldKey="filter_select_enabled"
|
||||||
label={t('Autocomplete filters')}
|
label={t('Autocomplete filters')}
|
||||||
descr={t('Whether to populate autocomplete filters options')}
|
description={t('Whether to populate autocomplete filters options')}
|
||||||
control={<CheckboxControl />}
|
control={<CheckboxControl />}
|
||||||
/>
|
/>
|
||||||
{this.state.isSqla && (
|
{this.state.isSqla && (
|
||||||
<Field
|
<Field
|
||||||
fieldKey="fetch_values_predicate"
|
fieldKey="fetch_values_predicate"
|
||||||
label={t('Autocomplete Query Predicate')}
|
label={t('Autocomplete Query Predicate')}
|
||||||
descr={t(
|
description={t(
|
||||||
'When using "Autocomplete filters", this can be used to improve performance ' +
|
'When using "Autocomplete filters", this can be used to improve performance ' +
|
||||||
'of the query fetching the values. Use this option to apply a ' +
|
'of the query fetching the values. Use this option to apply a ' +
|
||||||
'predicate (WHERE clause) to the query selecting the distinct ' +
|
'predicate (WHERE clause) to the query selecting the distinct ' +
|
||||||
@ -505,7 +515,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||||||
<Field
|
<Field
|
||||||
fieldKey="owners"
|
fieldKey="owners"
|
||||||
label={t('Owners')}
|
label={t('Owners')}
|
||||||
descr={t('Owners of the datasource')}
|
description={t('Owners of the datasource')}
|
||||||
control={
|
control={
|
||||||
<SelectAsyncControl
|
<SelectAsyncControl
|
||||||
dataEndpoint="/users/api/read"
|
dataEndpoint="/users/api/read"
|
||||||
@ -536,7 +546,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||||||
<Field
|
<Field
|
||||||
fieldKey="sql"
|
fieldKey="sql"
|
||||||
label={t('SQL')}
|
label={t('SQL')}
|
||||||
descr={t(
|
description={t(
|
||||||
'When specifying SQL, the datasource acts as a view. ' +
|
'When specifying SQL, the datasource acts as a view. ' +
|
||||||
'Superset will use this statement as a subquery while grouping and filtering ' +
|
'Superset will use this statement as a subquery while grouping and filtering ' +
|
||||||
'on the generated parent queries.',
|
'on the generated parent queries.',
|
||||||
@ -550,7 +560,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||||||
<Field
|
<Field
|
||||||
fieldKey="json"
|
fieldKey="json"
|
||||||
label={t('JSON')}
|
label={t('JSON')}
|
||||||
descr={
|
description={
|
||||||
<div>{t('The JSON metric or post aggregation definition.')}</div>
|
<div>{t('The JSON metric or post aggregation definition.')}</div>
|
||||||
}
|
}
|
||||||
control={
|
control={
|
||||||
@ -561,7 +571,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||||||
<Field
|
<Field
|
||||||
fieldKey="cache_timeout"
|
fieldKey="cache_timeout"
|
||||||
label={t('Cache Timeout')}
|
label={t('Cache Timeout')}
|
||||||
descr={t(
|
description={t(
|
||||||
'The duration of time in seconds before the cache is invalidated',
|
'The duration of time in seconds before the cache is invalidated',
|
||||||
)}
|
)}
|
||||||
control={<TextControl />}
|
control={<TextControl />}
|
||||||
@ -575,7 +585,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||||||
<Field
|
<Field
|
||||||
fieldKey="template_params"
|
fieldKey="template_params"
|
||||||
label={t('Template parameters')}
|
label={t('Template parameters')}
|
||||||
descr={t(
|
description={t(
|
||||||
'A set of parameters that become available in the query using Jinja templating syntax',
|
'A set of parameters that become available in the query using Jinja templating syntax',
|
||||||
)}
|
)}
|
||||||
control={<TextControl />}
|
control={<TextControl />}
|
||||||
@ -642,7 +652,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||||||
}}
|
}}
|
||||||
expandFieldset={
|
expandFieldset={
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<Fieldset>
|
<Fieldset compact>
|
||||||
<Field
|
<Field
|
||||||
fieldKey="verbose_name"
|
fieldKey="verbose_name"
|
||||||
label={t('Label')}
|
label={t('Label')}
|
||||||
@ -666,6 +676,22 @@ export class DatasourceEditor extends React.PureComponent {
|
|||||||
)}
|
)}
|
||||||
control={<TextControl placeholder={t('Warning Message')} />}
|
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>
|
</Fieldset>
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
}
|
}
|
||||||
@ -678,8 +704,16 @@ export class DatasourceEditor extends React.PureComponent {
|
|||||||
expression: '',
|
expression: '',
|
||||||
})}
|
})}
|
||||||
itemRenderers={{
|
itemRenderers={{
|
||||||
metric_name: (v, onChange) => (
|
metric_name: (v, onChange, _, record) => (
|
||||||
<EditableTitle canEdit title={v} onSaveTitle={onChange} />
|
<FlexRowContainer>
|
||||||
|
{record.is_certified && (
|
||||||
|
<CertifiedIconWithTooltip
|
||||||
|
certifiedBy={record.certified_by}
|
||||||
|
details={record.certification_details}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<EditableTitle canEdit title={v} onSaveTitle={onChange} />
|
||||||
|
</FlexRowContainer>
|
||||||
),
|
),
|
||||||
verbose_name: (v, onChange) => (
|
verbose_name: (v, onChange) => (
|
||||||
<EditableTitle canEdit title={v} onSaveTitle={onChange} />
|
<EditableTitle canEdit title={v} onSaveTitle={onChange} />
|
||||||
|
@ -36,6 +36,18 @@ interface DatasourceModalProps {
|
|||||||
show: boolean;
|
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> = ({
|
const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
||||||
addSuccessToast,
|
addSuccessToast,
|
||||||
datasource,
|
datasource,
|
||||||
@ -48,11 +60,19 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
|||||||
const dialog = useRef<any>(null);
|
const dialog = useRef<any>(null);
|
||||||
|
|
||||||
const onConfirmSave = () => {
|
const onConfirmSave = () => {
|
||||||
|
// Pull out extra fields into the extra object
|
||||||
|
|
||||||
SupersetClient.post({
|
SupersetClient.post({
|
||||||
endpoint: '/datasource/save/',
|
endpoint: '/datasource/save/',
|
||||||
postPayload: {
|
postPayload: {
|
||||||
data: {
|
data: {
|
||||||
...currentDatasource,
|
...currentDatasource,
|
||||||
|
metrics: currentDatasource?.metrics?.map(
|
||||||
|
(metric: Record<string, unknown>) => ({
|
||||||
|
...metric,
|
||||||
|
extra: buildMetricExtraJsonObject(metric),
|
||||||
|
}),
|
||||||
|
),
|
||||||
type: currentDatasource.type || currentDatasource.datasource_type,
|
type: currentDatasource.type || currentDatasource.datasource_type,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -75,8 +95,14 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDatasourceChange = (data: object, err: Array<any>) => {
|
const onDatasourceChange = (data: Record<string, any>, err: Array<any>) => {
|
||||||
setCurrentDatasource(data);
|
setCurrentDatasource({
|
||||||
|
...data,
|
||||||
|
metrics: data?.metrics.map((metric: Record<string, unknown>) => ({
|
||||||
|
...metric,
|
||||||
|
is_certified: metric?.certified_by || metric?.certification_details,
|
||||||
|
})),
|
||||||
|
});
|
||||||
setErrors(err);
|
setErrors(err);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
@ -351,6 +352,7 @@ class SqlMetric(Model, BaseMetric):
|
|||||||
foreign_keys=[table_id],
|
foreign_keys=[table_id],
|
||||||
)
|
)
|
||||||
expression = Column(Text, nullable=False)
|
expression = Column(Text, nullable=False)
|
||||||
|
extra = Column(Text)
|
||||||
|
|
||||||
export_fields = [
|
export_fields = [
|
||||||
"metric_name",
|
"metric_name",
|
||||||
@ -360,6 +362,7 @@ class SqlMetric(Model, BaseMetric):
|
|||||||
"expression",
|
"expression",
|
||||||
"description",
|
"description",
|
||||||
"d3format",
|
"d3format",
|
||||||
|
"extra",
|
||||||
"warning_text",
|
"warning_text",
|
||||||
]
|
]
|
||||||
update_from_object_fields = list(
|
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)
|
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 = Table(
|
||||||
"sqlatable_user",
|
"sqlatable_user",
|
||||||
|
@ -189,6 +189,7 @@ class SqlMetricInlineView( # pylint: disable=too-many-ancestors
|
|||||||
"expression",
|
"expression",
|
||||||
"table",
|
"table",
|
||||||
"d3format",
|
"d3format",
|
||||||
|
"extra",
|
||||||
"warning_text",
|
"warning_text",
|
||||||
]
|
]
|
||||||
description_columns = {
|
description_columns = {
|
||||||
@ -205,6 +206,14 @@ class SqlMetricInlineView( # pylint: disable=too-many-ancestors
|
|||||||
"formats",
|
"formats",
|
||||||
True,
|
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
|
add_columns = edit_columns
|
||||||
page_size = 500
|
page_size = 500
|
||||||
@ -216,6 +225,7 @@ class SqlMetricInlineView( # pylint: disable=too-many-ancestors
|
|||||||
"expression": _("SQL Expression"),
|
"expression": _("SQL Expression"),
|
||||||
"table": _("Table"),
|
"table": _("Table"),
|
||||||
"d3format": _("D3 Format"),
|
"d3format": _("D3 Format"),
|
||||||
|
"extra": _("Extra"),
|
||||||
"warning_text": _("Warning Message"),
|
"warning_text": _("Warning Message"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user