superset-frontend/src/components/MetadataBar/MetadataBar.tsx
/**
* 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, { useCallback, useEffect, useRef, useState } from 'react';
import { useResizeDetector } from 'react-resize-detector';
import { uniqWith } from 'lodash';
import { styled } from '@superset-ui/core';
import { Tooltip, TooltipPlacement } 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;
border-radius: ${theme.borderRadius}px;
line-height: 1;
`}
`;
const StyledItem = styled.div<{
collapsed: boolean;
last: boolean;
onClick?: () => void;
}>`
${({ theme, collapsed, last, onClick }) => `
display: flex;
align-items: center;
max-width: ${
ICON_WIDTH +
ICON_PADDING +
TEXT_MAX_WIDTH +
(last ? 0 : SPACE_BETWEEN_ITEMS)
}px;
min-width: ${
collapsed
? ICON_WIDTH + (last ? 0 : SPACE_BETWEEN_ITEMS)
: ICON_WIDTH +
ICON_PADDING +
TEXT_MIN_WIDTH +
(last ? 0 : SPACE_BETWEEN_ITEMS)
}px;
padding-right: ${last ? 0 : SPACE_BETWEEN_ITEMS}px;
cursor: ${onClick ? 'pointer' : 'default'};
& .metadata-icon {
color: ${
onClick && collapsed
? theme.colors.primary.base
: theme.colors.grayscale.base
};
padding-right: ${collapsed ? 0 : ICON_PADDING}px;
& .anticon {
line-height: 0;
}
}
& .metadata-text {
min-width: ${TEXT_MIN_WIDTH}px;
overflow: hidden;
text-overflow: ${collapsed ? 'unset' : 'ellipsis'};
white-space: nowrap;
text-decoration: ${onClick ? 'underline' : 'none'};
line-height: 1.4;
}
`}
`;
// 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,
tooltipPlacement,
}: {
barWidth: number | undefined;
contentType: ContentType;
collapsed: boolean;
last?: boolean;
tooltipPlacement: TooltipPlacement;
}) => {
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}
role={onClick ? 'button' : undefined}
>
<Icon iconSize="l" className="metadata-icon" />
{!collapsed && (
<span ref={ref} className="metadata-text">
{title}
</span>
)}
</StyledItem>
);
return isTruncated || collapsed || (tooltip && tooltip !== title) ? (
<Tooltip
placement={tooltipPlacement}
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[];
/**
* Antd tooltip placement. To see available values, check {@link TooltipPlacement}.
* Defaults to "top".
*/
tooltipPlacement?: TooltipPlacement;
}
/**
* 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, tooltipPlacement = 'top' }: MetadataBarProps) => {
const [width, setWidth] = useState<number>();
const [collapsed, setCollapsed] = useState(false);
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.');
}
const onResize = useCallback(
width => {
// 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;
setWidth(width);
setCollapsed(Boolean(width && width < breakpoint));
},
[count],
);
const { ref } = useResizeDetector({ onResize });
return (
<Bar ref={ref} count={count} data-test="metadata-bar">
{sortedItems.map((item, index) => (
<Item
barWidth={width}
key={index}
contentType={item}
collapsed={collapsed}
last={index === count - 1}
tooltipPlacement={tooltipPlacement}
/>
))}
</Bar>
);
};
export default MetadataBar;