Skip to content

Commit

Permalink
feat: Adds MetadataBar to Drill to Detail modal (#21343)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-s-molina committed Sep 8, 2022
1 parent 0601b2d commit 8ebf4ed
Show file tree
Hide file tree
Showing 9 changed files with 426 additions and 201 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import React from 'react';
import moment from 'moment';
import { ensureIsArray, styled, t } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { ContentType, MetadataType } from './ContentType';
import { ContentType, MetadataType } from '.';

const Header = styled.div`
font-weight: ${({ theme }) => theme.typography.weights.bold};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import React from 'react';
import { css } from '@superset-ui/core';
import { useResizeDetector } from 'react-resize-detector';
import MetadataBar, { MetadataBarProps } from './index';
import MetadataBar, { MetadataBarProps, MetadataType } from '.';

export default {
title: 'MetadataBar',
Expand Down Expand Up @@ -72,26 +72,26 @@ Component.story = {
Component.args = {
items: [
{
type: 'sql',
type: MetadataType.SQL,
title: 'Click to view query',
},
{
type: 'owner',
type: MetadataType.OWNER,
createdBy: 'Jane Smith',
owners: ['John Doe', 'Mary Wilson'],
createdOn: A_WEEK_AGO,
},
{
type: 'lastModified',
type: MetadataType.LAST_MODIFIED,
value: A_WEEK_AGO,
modifiedBy: 'Jane Smith',
},
{
type: 'tags',
type: MetadataType.TAGS,
values: ['management', 'research', 'poc'],
},
{
type: 'dashboards',
type: MetadataType.DASHBOARDS,
title: 'Added to 452 dashboards',
description:
'To preview the list of dashboards go to "More" settings on the right.',
Expand Down
16 changes: 10 additions & 6 deletions superset-frontend/src/components/MetadataBar/MetadataBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ import * as resizeDetector from 'react-resize-detector';
import moment from 'moment';
import { supersetTheme } from '@superset-ui/core';
import { hexToRgb } from 'src/utils/colorUtils';
import MetadataBar, { MIN_NUMBER_ITEMS, MAX_NUMBER_ITEMS } from '.';
import { ContentType, MetadataType } from './ContentType';
import MetadataBar, {
MIN_NUMBER_ITEMS,
MAX_NUMBER_ITEMS,
ContentType,
MetadataType,
} from '.';

const DASHBOARD_TITLE = 'Added to 452 dashboards';
const DASHBOARD_DESCRIPTION =
Expand Down Expand Up @@ -166,8 +170,8 @@ test('renders the items sorted', () => {
const { container } = render(<MetadataBar items={ITEMS.slice(0, 6)} />);
const nodes = container.firstChild?.childNodes as NodeListOf<HTMLElement>;
expect(within(nodes[0]).getByText(DASHBOARD_TITLE)).toBeInTheDocument();
expect(within(nodes[1]).getByText(ROWS_TITLE)).toBeInTheDocument();
expect(within(nodes[2]).getByText(SQL_TITLE)).toBeInTheDocument();
expect(within(nodes[1]).getByText(SQL_TITLE)).toBeInTheDocument();
expect(within(nodes[2]).getByText(ROWS_TITLE)).toBeInTheDocument();
expect(within(nodes[3]).getByText(DESCRIPTION_VALUE)).toBeInTheDocument();
expect(within(nodes[4]).getByText(CREATED_BY)).toBeInTheDocument();
});
Expand Down Expand Up @@ -217,7 +221,7 @@ test('correctly renders the owner tooltip', async () => {
test('correctly renders the rows tooltip', async () => {
await runWithBarCollapsed(async () => {
render(<MetadataBar items={ITEMS.slice(4, 8)} />);
userEvent.hover(screen.getAllByRole('img')[0]);
userEvent.hover(screen.getAllByRole('img')[2]);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(within(tooltip).getByText(ROWS_TITLE)).toBeInTheDocument();
Expand All @@ -237,7 +241,7 @@ test('correctly renders the sql tooltip', async () => {
test('correctly renders the table tooltip', async () => {
await runWithBarCollapsed(async () => {
render(<MetadataBar items={ITEMS.slice(4, 8)} />);
userEvent.hover(screen.getAllByRole('img')[2]);
userEvent.hover(screen.getAllByRole('img')[0]);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(within(tooltip).getByText(TABLE_TITLE)).toBeInTheDocument();
Expand Down
190 changes: 190 additions & 0 deletions superset-frontend/src/components/MetadataBar/MetadataBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/**
* 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, { useEffect, useRef, useState } from 'react';
import { useResizeDetector } from 'react-resize-detector';
import { uniqWith } from 'lodash';
import { styled } from '@superset-ui/core';
import { Tooltip } from 'src/components/Tooltip';
import { ContentType } from './ContentType';
import { config } from './ContentConfig';

export const MIN_NUMBER_ITEMS = 2;
export const MAX_NUMBER_ITEMS = 6;

const HORIZONTAL_PADDING = 12;
const VERTICAL_PADDING = 8;
const ICON_PADDING = 8;
const SPACE_BETWEEN_ITEMS = 16;
const ICON_WIDTH = 16;
const TEXT_MIN_WIDTH = 70;
const TEXT_MAX_WIDTH = 150;
const ORDER = {
dashboards: 0,
table: 1,
sql: 2,
rows: 3,
tags: 4,
description: 5,
owner: 6,
lastModified: 7,
};

const Bar = styled.div<{ count: number }>`
${({ theme, count }) => `
display: flex;
align-items: center;
padding: ${VERTICAL_PADDING}px ${HORIZONTAL_PADDING}px;
background-color: ${theme.colors.grayscale.light4};
color: ${theme.colors.grayscale.base};
font-size: ${theme.typography.sizes.s}px;
min-width: ${
HORIZONTAL_PADDING * 2 +
(ICON_WIDTH + SPACE_BETWEEN_ITEMS) * count -
SPACE_BETWEEN_ITEMS
}px;
`}
`;

const StyledItem = styled.div<{
collapsed: boolean;
last: boolean;
onClick?: () => void;
}>`
${({ theme, collapsed, last, onClick }) => `
max-width: ${
ICON_WIDTH +
ICON_PADDING +
TEXT_MAX_WIDTH +
(last ? 0 : SPACE_BETWEEN_ITEMS)
}px;
min-width: ${ICON_WIDTH + (last ? 0 : SPACE_BETWEEN_ITEMS)}px;
overflow: hidden;
text-overflow: ${collapsed ? 'unset' : 'ellipsis'};
white-space: nowrap;
padding-right: ${last ? 0 : SPACE_BETWEEN_ITEMS}px;
text-decoration: ${onClick ? 'underline' : 'none'};
cursor: ${onClick ? 'pointer' : 'default'};
& > span {
color: ${onClick && collapsed ? theme.colors.primary.base : 'undefined'};
padding-right: ${collapsed ? 0 : ICON_PADDING}px;
}
`}
`;

// Make sure big tootips are truncated
const TootipContent = styled.div`
display: -webkit-box;
-webkit-line-clamp: 20;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
`;

const Item = ({
barWidth,
contentType,
collapsed,
last = false,
}: {
barWidth: number | undefined;
contentType: ContentType;
collapsed: boolean;
last?: boolean;
}) => {
const { icon, title, tooltip = title } = config(contentType);
const [isTruncated, setIsTruncated] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const Icon = icon;
const { type, onClick } = contentType;

useEffect(() => {
setIsTruncated(
ref.current ? ref.current.scrollWidth > ref.current.clientWidth : false,
);
}, [barWidth, setIsTruncated, contentType]);

const content = (
<StyledItem
collapsed={collapsed}
last={last}
onClick={onClick ? () => onClick(type) : undefined}
ref={ref}
>
<Icon iconSize="l" />
{!collapsed && title}
</StyledItem>
);
return isTruncated || collapsed || (tooltip && tooltip !== title) ? (
<Tooltip title={<TootipContent>{tooltip}</TootipContent>}>
{content}
</Tooltip>
) : (
content
);
};

export interface MetadataBarProps {
/**
* Array of content type configurations. To see the available properties
* for each content type, check {@link ContentType}
*/
items: ContentType[];
}

/**
* The metadata bar component is used to display additional information about an entity.
* Content types are predefined and consistent across the whole app. This means that
* they will be displayed and behave in a consistent manner, keeping the same ordering,
* information formatting, and interactions.
* To extend the list of content types, a developer needs to request the inclusion of the new type in the design system.
* This process is important to make sure the new type is reviewed by the design team, improving Superset consistency.
*/
const MetadataBar = ({ items }: MetadataBarProps) => {
const { width, ref } = useResizeDetector();
const uniqueItems = uniqWith(items, (a, b) => a.type === b.type);
const sortedItems = uniqueItems.sort((a, b) => ORDER[a.type] - ORDER[b.type]);
const count = sortedItems.length;
if (count < MIN_NUMBER_ITEMS) {
throw Error('The minimum number of items for the metadata bar is 2.');
}
if (count > MAX_NUMBER_ITEMS) {
throw Error('The maximum number of items for the metadata bar is 6.');
}
// Calculates the breakpoint width to collapse the bar.
// The last item does not have a space, so we subtract SPACE_BETWEEN_ITEMS from the total.
const breakpoint =
(ICON_WIDTH + ICON_PADDING + TEXT_MIN_WIDTH + SPACE_BETWEEN_ITEMS) * count -
SPACE_BETWEEN_ITEMS;
const collapsed = Boolean(width && width < breakpoint);
return (
<Bar ref={ref} count={count}>
{sortedItems.map((item, index) => (
<Item
barWidth={width}
key={index}
contentType={item}
collapsed={collapsed}
last={index === count - 1}
/>
))}
</Bar>
);
};

export default MetadataBar;

0 comments on commit 8ebf4ed

Please sign in to comment.