You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/08/30 19:25:44 UTC
(superset) branch master updated: feat(dashboard): add a tooltip displaying chart descriptions (#43271)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new b310f469c20 feat(dashboard): add a tooltip displaying chart descriptions (#43271)
b310f469c20 is described below
commit b310f469c208cb7f5885189f21558935b4b66687
Author: SBIN2010 <Sb...@mail.ru>
AuthorDate: Sun Aug 30 22:25:37 2026 +0300
feat(dashboard): add a tooltip displaying chart descriptions (#43271)
---
.../components/SliceHeader/SliceHeader.test.tsx | 180 ++++++++++++++++++++-
.../components/SliceHeader/SliceInfo.test.tsx | 64 ++++++++
.../dashboard/components/SliceHeader/SliceInfo.tsx | 45 ++++++
.../src/dashboard/components/SliceHeader/index.tsx | 32 +++-
4 files changed, 319 insertions(+), 2 deletions(-)
diff --git a/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx b/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx
index 5ccb7fc546d..73bce191e29 100644
--- a/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx
@@ -19,7 +19,12 @@
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { getExtensionsRegistry, VizType } from '@superset-ui/core';
-import { render, screen, userEvent } from 'spec/helpers/testing-library';
+import {
+ fireEvent,
+ render,
+ screen,
+ userEvent,
+} from 'spec/helpers/testing-library';
import {
enableMobileConsumptionFlag,
mockMobileMatchMedia,
@@ -106,6 +111,13 @@ jest.mock('src/dashboard/components/FiltersBadge', () => ({
),
}));
+jest.mock('./SliceInfo', () => ({
+ __esModule: true,
+ default: ({ slice }: { slice: { description: string } }) => (
+ <div data-test="slice-info">{slice.description}</div>
+ ),
+}));
+
jest.mock('src/dashboard/util/isEmbedded', () => ({
isEmbedded: jest.fn().mockReturnValue(false),
}));
@@ -571,6 +583,172 @@ test('Correct actions to "SliceHeaderControls"', () => {
expect(props.handleToggleFullSize).toHaveBeenCalledTimes(1);
});
+test('Should show chart description info icon when description exists and is collapsed', () => {
+ const props = createProps({
+ slice: {
+ ...createProps().slice,
+ description: 'Test chart description',
+ },
+ isExpanded: false,
+ });
+ render(<SliceHeader {...props} />, {
+ useRedux: true,
+ useRouter: true,
+ initialState,
+ });
+ expect(screen.getByTestId('chart-description-info-icon')).toBeInTheDocument();
+});
+
+test('Should hide chart description info icon when description is expanded', () => {
+ const props = createProps({
+ slice: {
+ ...createProps().slice,
+ description: 'Test chart description',
+ },
+ isExpanded: true,
+ });
+ render(<SliceHeader {...props} />, {
+ useRedux: true,
+ useRouter: true,
+ initialState,
+ });
+ expect(
+ screen.queryByTestId('chart-description-info-icon'),
+ ).not.toBeInTheDocument();
+});
+
+test('Should hide chart description info icon when chart has no description', () => {
+ const props = createProps({
+ slice: {
+ ...createProps().slice,
+ description: '',
+ },
+ isExpanded: false,
+ });
+ render(<SliceHeader {...props} />, {
+ useRedux: true,
+ useRouter: true,
+ initialState,
+ });
+ expect(
+ screen.queryByTestId('chart-description-info-icon'),
+ ).not.toBeInTheDocument();
+});
+
+test('Chart description icon is a keyboard-focusable button', () => {
+ const props = createProps({
+ slice: {
+ ...createProps().slice,
+ description: 'Test chart description',
+ },
+ isExpanded: false,
+ });
+ render(<SliceHeader {...props} />, {
+ useRedux: true,
+ useRouter: true,
+ initialState,
+ });
+ const icon = screen.getByRole('button', { name: 'Chart description' });
+ icon.focus();
+ expect(icon).toHaveFocus();
+});
+
+test('Should show chart description in popover on hover', async () => {
+ const props = createProps({
+ slice: {
+ ...createProps().slice,
+ description: 'Test chart description',
+ },
+ isExpanded: false,
+ });
+ render(<SliceHeader {...props} />, {
+ useRedux: true,
+ useRouter: true,
+ initialState,
+ });
+
+ expect(screen.queryByTestId('slice-info')).not.toBeInTheDocument();
+
+ await userEvent.hover(screen.getByTestId('chart-description-info-icon'));
+
+ expect(await screen.findByTestId('slice-info')).toHaveTextContent(
+ 'Test chart description',
+ );
+});
+
+test('Should show chart description in popover on click', async () => {
+ const props = createProps({
+ slice: {
+ ...createProps().slice,
+ description: 'Test chart description',
+ },
+ isExpanded: false,
+ });
+ render(<SliceHeader {...props} />, {
+ useRedux: true,
+ useRouter: true,
+ initialState,
+ });
+
+ expect(screen.queryByTestId('slice-info')).not.toBeInTheDocument();
+
+ await userEvent.click(screen.getByTestId('chart-description-info-icon'));
+
+ expect(await screen.findByTestId('slice-info')).toHaveTextContent(
+ 'Test chart description',
+ );
+});
+
+test('Should open chart description popover with Enter', async () => {
+ const props = createProps({
+ slice: {
+ ...createProps().slice,
+ description: 'Test chart description',
+ },
+ isExpanded: false,
+ });
+ render(<SliceHeader {...props} />, {
+ useRedux: true,
+ useRouter: true,
+ initialState,
+ });
+
+ const icon = screen.getByRole('button', { name: 'Chart description' });
+ expect(screen.queryByTestId('slice-info')).not.toBeInTheDocument();
+
+ // user-event v12 (pinned in this repo) doesn't expose .keyboard(); use
+ // fireEvent to dispatch keydown directly to the focused icon.
+ icon.focus();
+ fireEvent.keyDown(icon, { key: 'Enter' });
+ expect(await screen.findByTestId('slice-info')).toHaveTextContent(
+ 'Test chart description',
+ );
+});
+
+test('Should open chart description popover with Space', async () => {
+ const props = createProps({
+ slice: {
+ ...createProps().slice,
+ description: 'Test chart description',
+ },
+ isExpanded: false,
+ });
+ render(<SliceHeader {...props} />, {
+ useRedux: true,
+ useRouter: true,
+ initialState,
+ });
+
+ const icon = screen.getByRole('button', { name: 'Chart description' });
+ expect(screen.queryByTestId('slice-info')).not.toBeInTheDocument();
+
+ icon.focus();
+ fireEvent.keyDown(icon, { key: ' ' });
+ expect(await screen.findByTestId('slice-info')).toHaveTextContent(
+ 'Test chart description',
+ );
+});
+
test('Add extension to SliceHeader', () => {
const extensionsRegistry = getExtensionsRegistry();
extensionsRegistry.set('dashboard.slice.header', () => (
diff --git a/superset-frontend/src/dashboard/components/SliceHeader/SliceInfo.test.tsx b/superset-frontend/src/dashboard/components/SliceHeader/SliceInfo.test.tsx
new file mode 100644
index 00000000000..9306174a37a
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/SliceHeader/SliceInfo.test.tsx
@@ -0,0 +1,64 @@
+/**
+ * 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 { render, screen } from 'spec/helpers/testing-library';
+import SliceInfo from './SliceInfo';
+
+jest.mock('@superset-ui/core/components/SafeMarkdown/SafeMarkdown', () => ({
+ SafeMarkdown: ({ source }: { source: string }) => (
+ <div data-test="safe-markdown">{source}</div>
+ ),
+}));
+
+const setup = (description = 'Default description') =>
+ render(<SliceInfo slice={{ description }} />);
+
+test('Should render chart description', () => {
+ setup('Hello world');
+ expect(screen.getByTestId('safe-markdown')).toHaveTextContent('Hello world');
+});
+
+test('Should pass markdown source to SafeMarkdown', () => {
+ const markdown = [
+ '# Chart overview',
+ '',
+ 'This chart shows **revenue** by region.',
+ '',
+ '- North',
+ '- South',
+ '',
+ '[Learn more](https://superset.apache.org)',
+ ].join('\n');
+
+ setup(markdown);
+ expect(screen.getByTestId('safe-markdown').textContent).toBe(markdown);
+});
+
+test('Should render long markdown description without crashing', () => {
+ const longMarkdown = `# Summary\n\n${'Long description paragraph. '.repeat(100)}`;
+
+ setup(longMarkdown);
+ const content = screen.getByTestId('safe-markdown').textContent ?? '';
+ expect(content).toContain('# Summary');
+ expect(content.match(/Long description paragraph\./g)).toHaveLength(100);
+});
+
+test('Should render empty description without crashing', () => {
+ setup('');
+ expect(screen.getByTestId('safe-markdown')).toBeEmptyDOMElement();
+});
diff --git a/superset-frontend/src/dashboard/components/SliceHeader/SliceInfo.tsx b/superset-frontend/src/dashboard/components/SliceHeader/SliceInfo.tsx
new file mode 100644
index 00000000000..cd452280d0c
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/SliceHeader/SliceInfo.tsx
@@ -0,0 +1,45 @@
+/**
+ * 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 { FC } from 'react';
+import { css, styled } from '@apache-superset/core/theme';
+import { SafeMarkdown } from '@superset-ui/core/components';
+
+const SliceInfoContainer = styled.div`
+ ${({ theme }) => css`
+ max-width: 350px;
+ max-height: 400px;
+ overflow-y: auto;
+ overflow-x: auto;
+ font-size: ${theme.fontSize}px;
+ `}
+`;
+
+interface SliceInfoProps {
+ slice: {
+ description: string;
+ };
+}
+
+const SliceInfo: FC<SliceInfoProps> = ({ slice }) => (
+ <SliceInfoContainer>
+ <SafeMarkdown source={slice.description} />
+ </SliceInfoContainer>
+);
+
+export default SliceInfo;
diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
index ba8e7b27617..f674d91ee79 100644
--- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
@@ -28,6 +28,7 @@ import {
import { t } from '@apache-superset/core/translation';
import {
getExtensionsRegistry,
+ handleKeyboardActivation,
JsonObject,
QueryData,
VizType,
@@ -40,7 +41,12 @@ import {
} from '@apache-superset/core/theme';
import { useUiConfig } from 'src/components/UiConfigContext';
import { isEmbedded } from 'src/dashboard/util/isEmbedded';
-import { Tooltip, EditableTitle, Icons } from '@superset-ui/core/components';
+import {
+ Tooltip,
+ EditableTitle,
+ Icons,
+ Popover,
+} from '@superset-ui/core/components';
import { useSelector } from 'react-redux';
import SliceHeaderControls from 'src/dashboard/components/SliceHeaderControls';
import { useIsMobile } from 'src/hooks/useIsMobile';
@@ -52,6 +58,7 @@ import { getSliceHeaderTooltip } from 'src/dashboard/util/getSliceHeaderTooltip'
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
import RowCountLabel from 'src/components/RowCountLabel';
import { Link } from 'react-router-dom';
+import SliceInfo from './SliceInfo';
const extensionsRegistry = getExtensionsRegistry();
@@ -210,6 +217,8 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
state => state.charts[slice.slice_id].queriesResponse?.[1],
);
+ const [isDescriptionOpen, setIsDescriptionOpen] = useState(false);
+
const theme = useTheme();
const rowLimit = Number(formData.row_limit ?? 0);
@@ -339,6 +348,27 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
<CrossFilterIcon iconSize="m" />
</Tooltip>
)}
+ {slice.description && !isExpanded && (
+ <Popover
+ trigger={['hover', 'click']}
+ content={<SliceInfo slice={slice} />}
+ placement="leftBottom"
+ open={isDescriptionOpen}
+ onOpenChange={setIsDescriptionOpen}
+ >
+ <Icons.InfoCircleOutlined
+ iconSize="m"
+ // eslint-disable-next-line jsx-a11y/prefer-tag-over-role
+ role="button"
+ tabIndex={0}
+ aria-label={t('Chart description')}
+ data-test="chart-description-info-icon"
+ onKeyDown={handleKeyboardActivation(() =>
+ setIsDescriptionOpen(open => !open),
+ )}
+ />
+ </Popover>
+ )}
{!uiConfig.hideChartControls && (
<MemoizedCustomizationsBadge chartId={slice.slice_id} />
)}