feat: smart tooltip in datasourcepanel (#18080)

This commit is contained in:
Yongjie Zhao 2022-02-07 22:48:23 +08:00 committed by GitHub
parent 299635c580
commit aa21a963a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 318 additions and 72 deletions

View File

@ -16,18 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import React, { useEffect, useState, ReactNode } from 'react';
import { styled } from '@superset-ui/core';
import { Tooltip } from './Tooltip';
import { ColumnTypeLabel } from './ColumnTypeLabel';
import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
import CertifiedIconWithTooltip from './CertifiedIconWithTooltip';
import { ColumnMeta } from '../types';
import { getColumnLabelText, getColumnTooltipNode } from './labelUtils';
export type ColumnOptionProps = {
column: ColumnMeta;
showType?: boolean;
showTooltip?: boolean;
labelRef?: React.RefObject<any>;
};
@ -41,11 +41,15 @@ export function ColumnOption({
column,
labelRef,
showType = false,
showTooltip = true,
}: ColumnOptionProps) {
const { expression, column_name, type_generic } = column;
const hasExpression = expression && expression !== column_name;
const type = hasExpression ? 'expression' : type_generic;
const [tooltipText, setTooltipText] = useState<ReactNode>(column.column_name);
useEffect(() => {
setTooltipText(getColumnTooltipNode(column, labelRef));
}, [labelRef, column]);
return (
<StyleOverrides>
@ -57,25 +61,17 @@ export function ColumnOption({
details={column.certification_details}
/>
)}
{showTooltip ? (
<Tooltip
id="metric-name-tooltip"
title={column.verbose_name || column.column_name}
trigger={['hover']}
placement="top"
>
<span
className="m-r-5 option-label column-option-label"
ref={labelRef}
>
{column.verbose_name || column.column_name}
</span>
</Tooltip>
) : (
<Tooltip
id="metric-name-tooltip"
title={tooltipText}
trigger={['hover']}
placement="top"
>
<span className="m-r-5 option-label column-option-label" ref={labelRef}>
{column.verbose_name || column.column_name}
{getColumnLabelText(column)}
</span>
)}
</Tooltip>
{column.description && (
<InfoTooltipWithTrigger
className="m-r-5 text-muted"

View File

@ -16,12 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import React, { useEffect, useState, ReactNode } from 'react';
import { styled, Metric, SafeMarkdown } from '@superset-ui/core';
import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
import { ColumnTypeLabel } from './ColumnTypeLabel';
import CertifiedIconWithTooltip from './CertifiedIconWithTooltip';
import Tooltip from './Tooltip';
import { getMetricTooltipNode } from './labelUtils';
const FlexRowContainer = styled.div`
align-items: center;
@ -37,7 +38,6 @@ export interface MetricOptionProps {
openInNewWindow?: boolean;
showFormula?: boolean;
showType?: boolean;
showTooltip?: boolean;
url?: string;
labelRef?: React.RefObject<any>;
}
@ -48,7 +48,6 @@ export function MetricOption({
openInNewWindow = false,
showFormula = true,
showType = false,
showTooltip = true,
url = '',
}: MetricOptionProps) {
const verbose = metric.verbose_name || metric.metric_name || metric.label;
@ -62,6 +61,12 @@ export function MetricOption({
const warningMarkdown = metric.warning_markdown || metric.warning_text;
const [tooltipText, setTooltipText] = useState<ReactNode>(metric.metric_name);
useEffect(() => {
setTooltipText(getMetricTooltipNode(metric, labelRef));
}, [labelRef, metric]);
return (
<FlexRowContainer className="metric-option">
{showType && <ColumnTypeLabel type="expression" />}
@ -72,22 +77,16 @@ export function MetricOption({
details={metric.certification_details}
/>
)}
{showTooltip ? (
<Tooltip
id="metric-name-tooltip"
title={verbose}
trigger={['hover']}
placement="top"
>
<span className="option-label metric-option-label" ref={labelRef}>
{link}
</span>
</Tooltip>
) : (
<Tooltip
id="metric-name-tooltip"
title={tooltipText}
trigger={['hover']}
placement="top"
>
<span className="option-label metric-option-label" ref={labelRef}>
{link}
</span>
)}
</Tooltip>
{metric.description && (
<InfoTooltipWithTrigger
className="text-muted"

View File

@ -0,0 +1,81 @@
/**
* 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, { ReactNode } from 'react';
import { t } from '@superset-ui/core';
import { ColumnMeta, Metric } from '@superset-ui/chart-controls';
export const isLabelTruncated = (labelRef?: React.RefObject<any>): boolean =>
!!(
labelRef &&
labelRef.current &&
labelRef.current.scrollWidth > labelRef.current.clientWidth
);
export const getColumnLabelText = (column: ColumnMeta): string =>
column.verbose_name || column.column_name;
export const getColumnTooltipNode = (
column: ColumnMeta,
labelRef?: React.RefObject<any>,
): ReactNode => {
// don't show tooltip if it hasn't verbose_name and hasn't truncated
if (!column.verbose_name && !isLabelTruncated(labelRef)) {
return null;
}
if (column.verbose_name) {
return (
<>
<div>{t('column name: %s', column.column_name)}</div>
<div>{t('verbose name: %s', column.verbose_name)}</div>
</>
);
}
// show column name in tooltip when column truncated
return t('column name: %s', column.column_name);
};
type MetricType = Omit<Metric, 'id'> & { label?: string };
export const getMetricTooltipNode = (
metric: MetricType,
labelRef?: React.RefObject<any>,
): ReactNode => {
// don't show tooltip if it hasn't verbose_name, label and hasn't truncated
if (!metric.verbose_name && !metric.label && !isLabelTruncated(labelRef)) {
return null;
}
if (metric.verbose_name) {
return (
<>
<div>{t('metric name: %s', metric.metric_name)}</div>
<div>{t('verbose name: %s', metric.verbose_name)}</div>
</>
);
}
if (isLabelTruncated(labelRef) && metric.label) {
return t('label name: %s', metric.label);
}
return t('metric name: %s', metric.metric_name);
};

View File

@ -0,0 +1,185 @@
/**
* 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 {
getColumnLabelText,
getColumnTooltipNode,
getMetricTooltipNode,
} from '../../src/components/labelUtils';
test("should get column name when column doesn't have verbose_name", () => {
expect(
getColumnLabelText({
id: 123,
column_name: 'column name',
verbose_name: '',
}),
).toBe('column name');
});
test('should get verbose name when column have verbose_name', () => {
expect(
getColumnLabelText({
id: 123,
column_name: 'column name',
verbose_name: 'verbose name',
}),
).toBe('verbose name');
});
test('should get null as tooltip', () => {
const ref = { current: { scrollWidth: 100, clientWidth: 100 } };
expect(
getColumnTooltipNode(
{
id: 123,
column_name: 'column name',
verbose_name: '',
},
ref,
),
).toBe(null);
});
test('should get column name and verbose name when it has a verbose name', () => {
const rvNode = (
<>
<div>column name: column name</div>
<div>verbose name: verbose name</div>
</>
);
const ref = { current: { scrollWidth: 100, clientWidth: 100 } };
expect(
getColumnTooltipNode(
{
id: 123,
column_name: 'column name',
verbose_name: 'verbose name',
},
ref,
),
).toStrictEqual(rvNode);
});
test('should get column name as tooltip if it overflowed', () => {
const ref = { current: { scrollWidth: 200, clientWidth: 100 } };
expect(
getColumnTooltipNode(
{
id: 123,
column_name: 'long long long long column name',
verbose_name: '',
},
ref,
),
).toBe('column name: long long long long column name');
});
test('should get column name and verbose name as tooltip if it overflowed', () => {
const rvNode = (
<>
<div>column name: long long long long column name</div>
<div>verbose name: long long long long verbose name</div>
</>
);
const ref = { current: { scrollWidth: 200, clientWidth: 100 } };
expect(
getColumnTooltipNode(
{
id: 123,
column_name: 'long long long long column name',
verbose_name: 'long long long long verbose name',
},
ref,
),
).toStrictEqual(rvNode);
});
test('should get null as tooltip in metric', () => {
const ref = { current: { scrollWidth: 100, clientWidth: 100 } };
expect(
getMetricTooltipNode(
{
metric_name: 'count',
label: '',
verbose_name: '',
},
ref,
),
).toBe(null);
});
test('should get metric name and verbose name as tooltip in metric', () => {
const rvNode = (
<>
<div>metric name: count</div>
<div>verbose name: count(*)</div>
</>
);
const ref = { current: { scrollWidth: 100, clientWidth: 100 } };
expect(
getMetricTooltipNode(
{
metric_name: 'count',
label: 'count(*)',
verbose_name: 'count(*)',
},
ref,
),
).toStrictEqual(rvNode);
});
test('should get metric name and verbose name in tooltip if it overflowed', () => {
const rvNode = (
<>
<div>metric name: count</div>
<div>verbose name: longlonglonglonglong verbose metric</div>
</>
);
const ref = { current: { scrollWidth: 200, clientWidth: 100 } };
expect(
getMetricTooltipNode(
{
metric_name: 'count',
label: '',
verbose_name: 'longlonglonglonglong verbose metric',
},
ref,
),
).toStrictEqual(rvNode);
});
test('should get label name as tooltip in metric if it overflowed', () => {
const ref = { current: { scrollWidth: 200, clientWidth: 100 } };
expect(
getMetricTooltipNode(
{
metric_name: 'count',
label: 'longlonglonglonglong metric label',
verbose_name: '',
},
ref,
),
).toBe('label name: longlonglonglonglong metric label');
});

View File

@ -17,7 +17,7 @@
* under the License.
*/
import { emitFilterControl } from '../../src/shared-controls/emitFilterControl';
import { emitFilterControl } from '@superset-ui/chart-controls';
describe('isFeatureFlagEnabled', () => {
it('returns empty array for unset feature flag', () => {

View File

@ -45,6 +45,8 @@ export interface Props {
datasource: DatasourceControl;
};
actions: Partial<ExploreActions> & Pick<ExploreActions, 'setControlValue'>;
// we use this props control force update when this panel resize
shouldForceUpdate?: number;
}
const Button = styled.button`
@ -123,32 +125,11 @@ const LabelContainer = (props: {
className: string;
}) => {
const labelRef = useRef<HTMLDivElement>(null);
const [showTooltip, setShowTooltip] = useState(true);
const isLabelTruncated = () =>
!!(
labelRef &&
labelRef.current &&
labelRef.current.scrollWidth > labelRef.current.clientWidth
);
const handleShowTooltip = () => {
const shouldShowTooltip = isLabelTruncated();
if (shouldShowTooltip !== showTooltip) {
setShowTooltip(shouldShowTooltip);
}
};
const handleResetTooltip = () => {
setShowTooltip(true);
};
const extendedProps = {
labelRef,
showTooltip,
};
return (
<LabelWrapper
onMouseEnter={handleShowTooltip}
onMouseLeave={handleResetTooltip}
className={props.className}
>
<LabelWrapper className={props.className}>
{React.cloneElement(props.children, extendedProps)}
</LabelWrapper>
);
@ -162,6 +143,7 @@ export default function DataSourcePanel({
datasource,
controls: { datasource: datasourceControl },
actions,
shouldForceUpdate,
}: Props) {
const { columns: _columns, metrics } = datasource;
@ -309,7 +291,10 @@ export default function DataSourcePanel({
)}
</div>
{metricSlice.map(m => (
<LabelContainer key={m.metric_name} className="column">
<LabelContainer
key={m.metric_name + String(shouldForceUpdate)}
className="column"
>
{enableExploreDnd ? (
<DatasourcePanelDragOption
value={m}
@ -342,7 +327,10 @@ export default function DataSourcePanel({
)}
</div>
{columnSlice.map(col => (
<LabelContainer key={col.column_name} className="column">
<LabelContainer
key={col.column_name + String(shouldForceUpdate)}
className="column"
>
{enableExploreDnd ? (
<DatasourcePanelDragOption
value={col}
@ -376,6 +364,7 @@ export default function DataSourcePanel({
search,
showAllColumns,
showAllMetrics,
shouldForceUpdate,
],
);

View File

@ -210,6 +210,7 @@ function ExploreViewContainer(props) {
const [showingModal, setShowingModal] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(false);
const [shouldForceUpdate, setShouldForceUpdate] = useState(-1);
const theme = useTheme();
const width = `${windowSize.width}px`;
@ -526,9 +527,10 @@ function ExploreViewContainer(props) {
/>
)}
<Resizable
onResizeStop={(evt, direction, ref, d) =>
setSidebarWidths(LocalStorageKeys.datasource_width, d)
}
onResizeStop={(evt, direction, ref, d) => {
setShouldForceUpdate(d?.width);
setSidebarWidths(LocalStorageKeys.datasource_width, d);
}}
defaultSize={{
width: getSidebarWidths(LocalStorageKeys.datasource_width),
height: '100%',
@ -559,6 +561,7 @@ function ExploreViewContainer(props) {
datasource={props.datasource}
controls={props.controls}
actions={props.actions}
shouldForceUpdate={shouldForceUpdate}
/>
</Resizable>
{isCollapsed ? (

View File

@ -144,7 +144,6 @@ export default function OptionWrapper(
<StyledColumnOption
column={transformedCol as ColumnMeta}
labelRef={labelRef}
showTooltip={!!shouldShowTooltip}
showType
/>
);

View File

@ -280,13 +280,7 @@ export const OptionControlLabel = ({
labelRef.current.scrollWidth > labelRef.current.clientWidth);
if (savedMetric && hasMetricName) {
return (
<StyledMetricOption
metric={savedMetric}
labelRef={labelRef}
showTooltip={!!shouldShowTooltip}
/>
);
return <StyledMetricOption metric={savedMetric} labelRef={labelRef} />;
}
if (!shouldShowTooltip) {
return <LabelText ref={labelRef}>{label}</LabelText>;