feat(explore): Integrate dataset panel with Folders feature (#33104)

Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>
This commit is contained in:
Elizabeth Thompson
2025-04-14 09:40:31 -07:00
committed by GitHub
parent a5a91d5e48
commit 7b9ebbe735
31 changed files with 1156 additions and 444 deletions

View File

@@ -16,71 +16,31 @@
* specific language governing permissions and limitations
* under the License.
*/
import { CSSProperties, FC } from 'react';
import { CSSProperties, ReactNode, useCallback } from 'react';
import { css, Metric, styled, t, useTheme } from '@superset-ui/core';
import {
css,
styled,
t,
useCSSTextTruncation,
useTheme,
} from '@superset-ui/core';
import { Icons } from 'src/components/Icons';
import { Tooltip } from 'src/components/Tooltip';
import DatasourcePanelDragOption from './DatasourcePanelDragOption';
import { DndItemType } from '../DndItemType';
import { DndItemValue } from './types';
export type DataSourcePanelColumn = {
is_dttm?: boolean | null;
description?: string | null;
expression?: string | null;
is_certified?: number | null;
column_name?: string | null;
name?: string | null;
type?: string;
};
type Props = {
index: number;
style: CSSProperties;
data: {
metricSlice: Metric[];
columnSlice: DataSourcePanelColumn[];
totalMetrics: number;
totalColumns: number;
width: number;
showAllMetrics: boolean;
onShowAllMetricsChange: (showAll: boolean) => void;
showAllColumns: boolean;
onShowAllColumnsChange: (showAll: boolean) => void;
collapseMetrics: boolean;
onCollapseMetricsChange: (collapse: boolean) => void;
collapseColumns: boolean;
onCollapseColumnsChange: (collapse: boolean) => void;
hiddenMetricCount: number;
hiddenColumnCount: number;
};
};
export const DEFAULT_MAX_COLUMNS_LENGTH = 50;
export const DEFAULT_MAX_METRICS_LENGTH = 50;
export const ITEM_HEIGHT = 30;
const Button = styled.button`
background: none;
border: none;
text-decoration: underline;
color: ${({ theme }) => theme.colors.primary.dark1};
`;
const ButtonContainer = styled.div`
text-align: center;
padding-top: 2px;
`;
import { DndItemValue, FlattenedItem, Folder } from './types';
const LabelWrapper = styled.div`
${({ theme }) => css`
color: ${theme.colors.grayscale.dark1};
overflow: hidden;
text-overflow: ellipsis;
font-size: ${theme.typography.sizes.s}px;
background-color: ${theme.colors.grayscale.light4};
margin: ${theme.gridUnit * 2}px 0;
border-radius: 4px;
border-radius: ${theme.borderRadius}px;
padding: 0 ${theme.gridUnit}px;
&:first-of-type {
@@ -117,98 +77,136 @@ const LabelWrapper = styled.div`
`;
const SectionHeaderButton = styled.button`
display: flex;
justify-content: space-between;
align-items: center;
border: none;
background: transparent;
width: 100%;
padding-inline: 0px;
height: 100%;
padding-inline: 0;
`;
const SectionHeaderTextContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
`;
const SectionHeader = styled.span`
${({ theme }) => `
${({ theme }) => css`
color: ${theme.colors.grayscale.dark1};
font-size: ${theme.typography.sizes.m}px;
font-weight: ${theme.typography.weights.medium};
line-height: 1.3;
`}
`;
const Box = styled.div`
${({ theme }) => `
border: 1px ${theme.colors.grayscale.light4} solid;
border-radius: ${theme.gridUnit}px;
font-size: ${theme.typography.sizes.s}px;
padding: ${theme.gridUnit}px;
color: ${theme.colors.grayscale.light1};
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
`}
`;
const DatasourcePanelItem: FC<Props> = ({ index, style, data }) => {
const {
metricSlice: _metricSlice,
columnSlice,
totalMetrics,
totalColumns,
width,
showAllMetrics,
onShowAllMetricsChange,
showAllColumns,
onShowAllColumnsChange,
collapseMetrics,
onCollapseMetricsChange,
collapseColumns,
onCollapseColumnsChange,
hiddenMetricCount,
hiddenColumnCount,
} = data;
const metricSlice = collapseMetrics ? [] : _metricSlice;
const Divider = styled.div`
${({ theme }) => css`
height: 16px;
border-bottom: 1px solid ${theme.colors.grayscale.light3};
`}
`;
const EXTRA_LINES = collapseMetrics ? 1 : 2;
const isColumnSection = collapseMetrics
? index >= 1
: index > metricSlice.length + EXTRA_LINES;
const HEADER_LINE = isColumnSection
? metricSlice.length + EXTRA_LINES + 1
: 0;
const SUBTITLE_LINE = HEADER_LINE + 1;
const BOTTOM_LINE =
(isColumnSection ? columnSlice.length : metricSlice.length) +
(collapseMetrics ? HEADER_LINE : SUBTITLE_LINE) +
1;
const collapsed = isColumnSection ? collapseColumns : collapseMetrics;
const setCollapse = isColumnSection
? onCollapseColumnsChange
: onCollapseMetricsChange;
const showAll = isColumnSection ? showAllColumns : showAllMetrics;
const setShowAll = isColumnSection
? onShowAllColumnsChange
: onShowAllMetricsChange;
export interface DatasourcePanelItemProps {
index: number;
style: CSSProperties;
data: {
flattenedItems: FlattenedItem[];
folderMap: Map<string, Folder>;
width: number;
onToggleCollapse: (folderId: string) => void;
collapsedFolderIds: Set<string>;
};
}
const DatasourcePanelItem = ({
index,
style,
data,
}: DatasourcePanelItemProps) => {
const {
flattenedItems,
folderMap,
width,
onToggleCollapse,
collapsedFolderIds,
} = data;
const item = flattenedItems[index];
const theme = useTheme();
const hiddenCount = isColumnSection ? hiddenColumnCount : hiddenMetricCount;
const [labelRef, labelIsTruncated] = useCSSTextTruncation<HTMLSpanElement>({
isVertical: true,
isHorizontal: false,
});
const getTooltipNode = useCallback(
(folder: Folder) => {
let tooltipNode: ReactNode | null = null;
if (labelIsTruncated) {
tooltipNode = (
<div>
<b>{t('Name')}:</b> {folder.name}
</div>
);
}
if (folder.description) {
tooltipNode = (
<div>
{tooltipNode}
<div
css={
tooltipNode &&
css`
margin-top: ${theme.gridUnit}px;
`
}
>
<b>{t('Description')}:</b> {folder.description}
</div>
</div>
);
}
return tooltipNode;
},
[labelIsTruncated],
);
if (!item) return null;
const folder = folderMap.get(item.folderId);
if (!folder) return null;
const indentation = item.depth * theme.gridUnit * 4;
return (
<div
style={style}
css={css`
padding: 0 ${theme.gridUnit * 4}px;
`}
style={{
...style,
paddingLeft: theme.gridUnit * 4 + indentation,
paddingRight: theme.gridUnit * 4,
}}
>
{index === HEADER_LINE && (
<SectionHeaderButton onClick={() => setCollapse(!collapsed)}>
<SectionHeader>
{isColumnSection ? t('Columns') : t('Metrics')}
</SectionHeader>
{collapsed ? (
<Icons.DownOutlined iconSize="s" />
) : (
<Icons.UpOutlined iconSize="s" />
)}
{item.type === 'header' && (
<SectionHeaderButton onClick={() => onToggleCollapse(folder.id)}>
<Tooltip title={getTooltipNode(folder)}>
<SectionHeaderTextContainer>
<SectionHeader ref={labelRef}>{folder.name}</SectionHeader>
{collapsedFolderIds.has(folder.id) ? (
<Icons.DownOutlined iconSize="s" />
) : (
<Icons.UpOutlined iconSize="s" />
)}
</SectionHeaderTextContainer>
</Tooltip>
</SectionHeaderButton>
)}
{index === SUBTITLE_LINE && !collapsed && (
{item.type === 'subtitle' && (
<div
css={css`
display: flex;
@@ -223,46 +221,34 @@ const DatasourcePanelItem: FC<Props> = ({ index, style, data }) => {
flex-shrink: 0;
`}
>
{isColumnSection
? t(`Showing %s of %s`, columnSlice?.length, totalColumns)
: t(`Showing %s of %s`, metricSlice?.length, totalMetrics)}
{t(`Showing %s of %s items`, item.showingItems, item.totalItems)}
</div>
{hiddenCount > 0 && (
<Box>{t(`%s ineligible item(s) are hidden`, hiddenCount)}</Box>
)}
</div>
)}
{index > SUBTITLE_LINE && index < BOTTOM_LINE && (
{item.type === 'item' && item.item && (
<LabelWrapper
key={
(isColumnSection
? columnSlice[index - SUBTITLE_LINE - 1].column_name
: metricSlice[index - SUBTITLE_LINE - 1].metric_name) +
String(width)
(item.item.type === 'column'
? item.item.column_name
: item.item.metric_name) + String(width)
}
className="column"
>
<DatasourcePanelDragOption
value={
isColumnSection
? (columnSlice[index - SUBTITLE_LINE - 1] as DndItemValue)
: metricSlice[index - SUBTITLE_LINE - 1]
value={item.item as DndItemValue}
type={
item.item.type === 'column'
? DndItemType.Column
: DndItemType.Metric
}
type={isColumnSection ? DndItemType.Column : DndItemType.Metric}
/>
</LabelWrapper>
)}
{index === BOTTOM_LINE &&
!collapsed &&
(isColumnSection
? totalColumns > DEFAULT_MAX_COLUMNS_LENGTH
: totalMetrics > DEFAULT_MAX_METRICS_LENGTH) && (
<ButtonContainer>
<Button onClick={() => setShowAll(!showAll)}>
{showAll ? t('Show less...') : t('Show all...')}
</Button>
</ButtonContainer>
)}
{item.type === 'divider' && (
<Divider data-test="datasource-panel-divider" />
)}
</div>
);
};