mirror of https://github.com/apache/superset.git
feat: smart tooltip in datasourcepanel (#18080)
This commit is contained in:
parent
299635c580
commit
aa21a963a6
|
@ -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}
|
||||
title={tooltipText}
|
||||
trigger={['hover']}
|
||||
placement="top"
|
||||
>
|
||||
<span
|
||||
className="m-r-5 option-label column-option-label"
|
||||
ref={labelRef}
|
||||
>
|
||||
{column.verbose_name || column.column_name}
|
||||
<span className="m-r-5 option-label column-option-label" ref={labelRef}>
|
||||
{getColumnLabelText(column)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className="m-r-5 option-label column-option-label" ref={labelRef}>
|
||||
{column.verbose_name || column.column_name}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{column.description && (
|
||||
<InfoTooltipWithTrigger
|
||||
className="m-r-5 text-muted"
|
||||
|
|
|
@ -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,10 +77,9 @@ export function MetricOption({
|
|||
details={metric.certification_details}
|
||||
/>
|
||||
)}
|
||||
{showTooltip ? (
|
||||
<Tooltip
|
||||
id="metric-name-tooltip"
|
||||
title={verbose}
|
||||
title={tooltipText}
|
||||
trigger={['hover']}
|
||||
placement="top"
|
||||
>
|
||||
|
@ -83,11 +87,6 @@ export function MetricOption({
|
|||
{link}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className="option-label metric-option-label" ref={labelRef}>
|
||||
{link}
|
||||
</span>
|
||||
)}
|
||||
{metric.description && (
|
||||
<InfoTooltipWithTrigger
|
||||
className="text-muted"
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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');
|
||||
});
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -144,7 +144,6 @@ export default function OptionWrapper(
|
|||
<StyledColumnOption
|
||||
column={transformedCol as ColumnMeta}
|
||||
labelRef={labelRef}
|
||||
showTooltip={!!shouldShowTooltip}
|
||||
showType
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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>;
|
||||
|
|
Loading…
Reference in New Issue