popover lineage breadcrumbs

This commit is contained in:
Nate Kelley 2025-03-03 21:16:41 -07:00
parent f7bd0abdff
commit 3840d64360
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
5 changed files with 136 additions and 72 deletions

View File

@ -100,8 +100,6 @@ export const PolicyCheck: React.FC<{
}
}, [placement]);
console.log(placement);
const alignMemo: PopoverProps['align'] = useMemo(() => {
switch (placement) {
case 'top':

View File

@ -0,0 +1,72 @@
import type { Meta, StoryObj } from '@storybook/react';
import { PermissionLineageBreadcrumb } from './PermissionLineageBreadcrumb';
import type { DatasetPermissionOverviewUser } from '@/api/asset_interfaces';
const meta: Meta<typeof PermissionLineageBreadcrumb> = {
title: 'Features/Permissions/PermissionLineageBreadcrumb',
component: PermissionLineageBreadcrumb,
tags: ['autodocs'],
parameters: {
layout: 'centered'
}
};
export default meta;
type Story = StoryObj<typeof PermissionLineageBreadcrumb>;
// Sample data for different scenarios
const singleLineage: DatasetPermissionOverviewUser['lineage'] = [
[
{ type: 'user', name: 'John Doe', id: 'user1' },
{ type: 'datasets', name: 'Sales Dataset', id: 'dataset1' },
{ type: 'permissionGroups', name: 'Analysts', id: 'group1' }
]
];
const multipleLineage: DatasetPermissionOverviewUser['lineage'] = [
[
{ type: 'user', name: 'John Doe', id: 'user1' },
{ type: 'datasets', name: 'Sales Dataset', id: 'dataset1' }
],
[
{ type: 'user', name: 'John Doe', id: 'user1' },
{ type: 'datasetGroups', name: 'Marketing Data', id: 'datasetGroup1' }
]
];
const emptyLineage: DatasetPermissionOverviewUser['lineage'] = [];
export const SingleLineageCanQuery: Story = {
args: {
lineage: singleLineage,
canQuery: true
}
};
export const SingleLineageCannotQuery: Story = {
args: {
lineage: singleLineage,
canQuery: false
}
};
export const MultipleLineageCanQuery: Story = {
args: {
lineage: multipleLineage,
canQuery: true
}
};
export const MultipleLineageCannotQuery: Story = {
args: {
lineage: multipleLineage,
canQuery: false
}
};
export const NoLineage: Story = {
args: {
lineage: emptyLineage,
canQuery: false
}
};

View File

@ -1,10 +1,12 @@
import type { DatasetPermissionOverviewUser } from '@/api/asset_interfaces';
import { AppMaterialIcons, AppPopover } from '@/components/ui';
import { ChevronRight } from '@/components/ui/icons';
import { Popover } from '@/components/ui/tooltip/Popover';
import { BusterRoutes, createBusterRoute } from '@/routes';
import { useMemoizedFn } from 'ahooks';
import { createStyles } from 'antd-style';
import Link from 'next/link';
import React, { useMemo } from 'react';
import { cn } from '@/lib/utils';
import { cva } from 'class-variance-authority';
export const PermissionLineageBreadcrumb: React.FC<{
lineage: DatasetPermissionOverviewUser['lineage'];
@ -56,10 +58,10 @@ const MultipleLineage: React.FC<{
lineage: DatasetPermissionOverviewUser['lineage'];
canQuery: DatasetPermissionOverviewUser['can_query'];
}> = ({ lineage, canQuery }) => {
const { styles, cx } = useStyles();
// const { styles, cx } = useStyles();
const Content = useMemo(() => {
return (
<div className="flex min-w-[200px] flex-col space-y-2 p-2">
<div className="flex min-w-[200px] flex-col space-y-2">
{lineage.map((item, lineageindex) => {
const items = item.map((v, index) => {
return <SelectedComponent key={index} item={v} />;
@ -71,17 +73,10 @@ const MultipleLineage: React.FC<{
);
}, [lineage]);
const onClickPreflight = useMemoizedFn((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
e.preventDefault();
});
return (
<AppPopover placement="topRight" destroyTooltipOnHide trigger="click" content={Content}>
<div className={cx(styles.linearItem, 'clickable')} onClick={onClickPreflight}>
Multiple access sources
</div>
</AppPopover>
<Popover side="top" align="start" trigger="click" content={Content} size="sm">
<div className={linearItem({ clickable: true })}>Multiple access sources</div>
</Popover>
);
};
@ -91,39 +86,30 @@ interface LineageItemProps {
}
const UserLineageItem: React.FC<LineageItemProps> = ({ name, id }) => {
const { styles, cx } = useStyles();
return <div className={cx(styles.linearItem)}>{name}</div>;
return <div className={linearItem({ clickable: false })}>{name}</div>;
};
const DatasetLineageItem: React.FC<LineageItemProps> = ({ name, id }) => {
const { styles, cx } = useStyles();
return (
<Link href={createBusterRoute({ route: BusterRoutes.APP_DATASETS_ID, datasetId: id })}>
<div className={cx(styles.linearItem, 'clickable')}>{name}</div>
<div className={linearItem({ clickable: true })}>{name}</div>
</Link>
);
};
const PermissionGroupLineageItem: React.FC<LineageItemProps> = ({ name, id }) => {
const { styles, cx } = useStyles();
return <div className={cx(styles.linearItem)}>{name}</div>;
return <div className={linearItem({ clickable: false })}>{name}</div>;
};
const DatasetGroupLineageItem: React.FC<LineageItemProps> = ({ name, id }) => {
const { styles, cx } = useStyles();
return <div className={cx(styles.linearItem)}>{name}</div>;
return <div className={linearItem({ clickable: false })}>{name}</div>;
};
const CanQueryTag: React.FC<{
canQuery: boolean;
}> = ({ canQuery }) => {
const { styles, cx } = useStyles();
return (
<div
className={cx(
styles.canQueryTag,
canQuery ? styles.canQueryTagSuccess : styles.canQueryTagError
)}>
<div className={canQueryTag({ status: canQuery ? 'success' : 'error' })}>
{canQuery ? 'Can query' : 'Cannot query'}
</div>
);
@ -133,13 +119,12 @@ const LineageBreadcrumb: React.FC<{
items: React.ReactNode[];
canQuery: boolean;
}> = ({ items, canQuery }) => {
const { styles, cx } = useStyles();
const BreadcrumbIcon = <AppMaterialIcons icon="chevron_right" />;
const BreadcrumbIcon = <ChevronRight />;
const allItems = [...items, <CanQueryTag key="can-query" canQuery={canQuery} />];
return (
<div className={cx(styles.linearContainer, 'flex justify-end space-x-0')}>
<div className={cn('text-text-secondary', 'flex justify-end space-x-0')}>
{allItems.map((item, index) => {
return (
<div key={index} className="flex items-center space-x-0">
@ -152,33 +137,36 @@ const LineageBreadcrumb: React.FC<{
);
};
const useStyles = createStyles(({ token, css }) => ({
linearContainer: css`
color: ${token.colorTextSecondary};
`,
linearItem: css`
color: ${token.colorTextSecondary};
padding: 4px 6px;
border-radius: 4px;
&.clickable {
cursor: pointer;
&:hover {
color: ${token.colorText};
background-color: ${token.colorFillSecondary};
}
const linearItem = cva('text-text-secondary text-base px-1 py-1.5 rounded-sm', {
variants: {
clickable: {
true: 'cursor-pointer hover:text-text hover:bg-item-hover-active',
false: ''
}
`,
canQueryTag: css`
border-radius: 4px;
padding: 4px 6px;
`,
canQueryTagSuccess: css`
color: #34a32d;
background-color: #edfff0;
`,
canQueryTagError: css`
color: #ff9e00;
background-color: #fff7ed;
`
}));
}
});
const canQueryTag = cva('rounded-sm px-1 py-1.5 text-base', {
variants: {
status: {
success: 'bg-success-background text-success-foreground',
error: 'bg-danger-background text-danger-foreground'
}
}
});
// const useStyles = createStyles(({ token, css }) => ({
// canQueryTag: css`
// border-radius: 4px;
// padding: 4px 6px;
// `,
// canQueryTagSuccess: css`
// color: #34a32d;
// background-color: #edfff0;
// `,
// canQueryTagError: css`
// color: #ff9e00;
// background-color: #fff7ed;
// `
// }));

View File

@ -1,6 +1,7 @@
import {
PopoverRoot as PopoverBase,
PopoverContent,
PopoverContentVariant,
PopoverTrigger,
PopoverTriggerType
} from './PopoverBase';
@ -14,7 +15,8 @@ export interface PopoverProps
content: React.ReactNode;
className?: string;
headerContent?: string | React.ReactNode;
triggerType?: PopoverTriggerType;
trigger?: PopoverTriggerType;
size?: PopoverContentVariant['size'];
}
export const Popover = React.memo<PopoverProps>(
@ -25,16 +27,18 @@ export const Popover = React.memo<PopoverProps>(
side,
className = '',
headerContent,
triggerType = 'click',
trigger = 'click',
size = 'default',
...props
}) => {
return (
<PopoverBase triggerType={triggerType} {...props}>
<PopoverBase trigger={trigger} {...props}>
<PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverContent
align={align}
side={side}
className={className}
size={size}
headerContent={headerContent && <PopoverHeaderContent title={headerContent} />}>
{content}
</PopoverContent>

View File

@ -12,26 +12,26 @@ export type PopoverTriggerType = 'click' | 'hover';
const Popover = PopoverPrimitive.Root;
interface PopoverProps extends React.ComponentPropsWithoutRef<typeof Popover> {
triggerType?: PopoverTriggerType;
trigger?: PopoverTriggerType;
}
const PopoverRoot: React.FC<PopoverProps> = ({ children, triggerType = 'click', ...props }) => {
const PopoverRoot: React.FC<PopoverProps> = ({ children, trigger = 'click', ...props }) => {
const [isOpen, setIsOpen] = React.useState(false);
const handleMouseEnter = useMemoizedFn(() => {
if (triggerType === 'hover') {
if (trigger === 'hover') {
setIsOpen(true);
}
});
const handleMouseLeave = useMemoizedFn(() => {
if (triggerType === 'hover') {
if (trigger === 'hover') {
setIsOpen(false);
}
});
const content =
triggerType === 'hover' ? (
trigger === 'hover' ? (
<div className="relative" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<div className="absolute -inset-[4px]" />
<div className="relative z-10">{children}</div>
@ -41,7 +41,7 @@ const PopoverRoot: React.FC<PopoverProps> = ({ children, triggerType = 'click',
);
return (
<Popover {...props} open={triggerType === 'hover' ? isOpen : undefined}>
<Popover {...props} open={trigger === 'hover' ? isOpen : undefined}>
{content}
</Popover>
);
@ -64,11 +64,13 @@ const popoverContentVariant = cva('', {
}
});
export type PopoverContentVariant = VariantProps<typeof popoverContentVariant>;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
headerContent?: React.ReactNode;
} & VariantProps<typeof popoverContentVariant>
} & PopoverContentVariant
>(
(
{ className, align = 'center', children, sideOffset = 4, headerContent, size, ...props },