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]); }, [placement]);
console.log(placement);
const alignMemo: PopoverProps['align'] = useMemo(() => { const alignMemo: PopoverProps['align'] = useMemo(() => {
switch (placement) { switch (placement) {
case 'top': 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 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 { BusterRoutes, createBusterRoute } from '@/routes';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { createStyles } from 'antd-style';
import Link from 'next/link'; import Link from 'next/link';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { cn } from '@/lib/utils';
import { cva } from 'class-variance-authority';
export const PermissionLineageBreadcrumb: React.FC<{ export const PermissionLineageBreadcrumb: React.FC<{
lineage: DatasetPermissionOverviewUser['lineage']; lineage: DatasetPermissionOverviewUser['lineage'];
@ -56,10 +58,10 @@ const MultipleLineage: React.FC<{
lineage: DatasetPermissionOverviewUser['lineage']; lineage: DatasetPermissionOverviewUser['lineage'];
canQuery: DatasetPermissionOverviewUser['can_query']; canQuery: DatasetPermissionOverviewUser['can_query'];
}> = ({ lineage, canQuery }) => { }> = ({ lineage, canQuery }) => {
const { styles, cx } = useStyles(); // const { styles, cx } = useStyles();
const Content = useMemo(() => { const Content = useMemo(() => {
return ( 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) => { {lineage.map((item, lineageindex) => {
const items = item.map((v, index) => { const items = item.map((v, index) => {
return <SelectedComponent key={index} item={v} />; return <SelectedComponent key={index} item={v} />;
@ -71,17 +73,10 @@ const MultipleLineage: React.FC<{
); );
}, [lineage]); }, [lineage]);
const onClickPreflight = useMemoizedFn((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
e.preventDefault();
});
return ( return (
<AppPopover placement="topRight" destroyTooltipOnHide trigger="click" content={Content}> <Popover side="top" align="start" trigger="click" content={Content} size="sm">
<div className={cx(styles.linearItem, 'clickable')} onClick={onClickPreflight}> <div className={linearItem({ clickable: true })}>Multiple access sources</div>
Multiple access sources </Popover>
</div>
</AppPopover>
); );
}; };
@ -91,39 +86,30 @@ interface LineageItemProps {
} }
const UserLineageItem: React.FC<LineageItemProps> = ({ name, id }) => { const UserLineageItem: React.FC<LineageItemProps> = ({ name, id }) => {
const { styles, cx } = useStyles(); return <div className={linearItem({ clickable: false })}>{name}</div>;
return <div className={cx(styles.linearItem)}>{name}</div>;
}; };
const DatasetLineageItem: React.FC<LineageItemProps> = ({ name, id }) => { const DatasetLineageItem: React.FC<LineageItemProps> = ({ name, id }) => {
const { styles, cx } = useStyles();
return ( return (
<Link href={createBusterRoute({ route: BusterRoutes.APP_DATASETS_ID, datasetId: id })}> <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> </Link>
); );
}; };
const PermissionGroupLineageItem: React.FC<LineageItemProps> = ({ name, id }) => { const PermissionGroupLineageItem: React.FC<LineageItemProps> = ({ name, id }) => {
const { styles, cx } = useStyles(); return <div className={linearItem({ clickable: false })}>{name}</div>;
return <div className={cx(styles.linearItem)}>{name}</div>;
}; };
const DatasetGroupLineageItem: React.FC<LineageItemProps> = ({ name, id }) => { const DatasetGroupLineageItem: React.FC<LineageItemProps> = ({ name, id }) => {
const { styles, cx } = useStyles(); return <div className={linearItem({ clickable: false })}>{name}</div>;
return <div className={cx(styles.linearItem)}>{name}</div>;
}; };
const CanQueryTag: React.FC<{ const CanQueryTag: React.FC<{
canQuery: boolean; canQuery: boolean;
}> = ({ canQuery }) => { }> = ({ canQuery }) => {
const { styles, cx } = useStyles();
return ( return (
<div <div className={canQueryTag({ status: canQuery ? 'success' : 'error' })}>
className={cx(
styles.canQueryTag,
canQuery ? styles.canQueryTagSuccess : styles.canQueryTagError
)}>
{canQuery ? 'Can query' : 'Cannot query'} {canQuery ? 'Can query' : 'Cannot query'}
</div> </div>
); );
@ -133,13 +119,12 @@ const LineageBreadcrumb: React.FC<{
items: React.ReactNode[]; items: React.ReactNode[];
canQuery: boolean; canQuery: boolean;
}> = ({ items, canQuery }) => { }> = ({ items, canQuery }) => {
const { styles, cx } = useStyles(); const BreadcrumbIcon = <ChevronRight />;
const BreadcrumbIcon = <AppMaterialIcons icon="chevron_right" />;
const allItems = [...items, <CanQueryTag key="can-query" canQuery={canQuery} />]; const allItems = [...items, <CanQueryTag key="can-query" canQuery={canQuery} />];
return ( 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) => { {allItems.map((item, index) => {
return ( return (
<div key={index} className="flex items-center space-x-0"> <div key={index} className="flex items-center space-x-0">
@ -152,33 +137,36 @@ const LineageBreadcrumb: React.FC<{
); );
}; };
const useStyles = createStyles(({ token, css }) => ({ const linearItem = cva('text-text-secondary text-base px-1 py-1.5 rounded-sm', {
linearContainer: css` variants: {
color: ${token.colorTextSecondary}; clickable: {
`, true: 'cursor-pointer hover:text-text hover:bg-item-hover-active',
linearItem: css` false: ''
color: ${token.colorTextSecondary};
padding: 4px 6px;
border-radius: 4px;
&.clickable {
cursor: pointer;
&:hover {
color: ${token.colorText};
background-color: ${token.colorFillSecondary};
}
} }
`, }
canQueryTag: css` });
border-radius: 4px;
padding: 4px 6px; const canQueryTag = cva('rounded-sm px-1 py-1.5 text-base', {
`, variants: {
canQueryTagSuccess: css` status: {
color: #34a32d; success: 'bg-success-background text-success-foreground',
background-color: #edfff0; error: 'bg-danger-background text-danger-foreground'
`, }
canQueryTagError: css` }
color: #ff9e00; });
background-color: #fff7ed;
` // 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 { import {
PopoverRoot as PopoverBase, PopoverRoot as PopoverBase,
PopoverContent, PopoverContent,
PopoverContentVariant,
PopoverTrigger, PopoverTrigger,
PopoverTriggerType PopoverTriggerType
} from './PopoverBase'; } from './PopoverBase';
@ -14,7 +15,8 @@ export interface PopoverProps
content: React.ReactNode; content: React.ReactNode;
className?: string; className?: string;
headerContent?: string | React.ReactNode; headerContent?: string | React.ReactNode;
triggerType?: PopoverTriggerType; trigger?: PopoverTriggerType;
size?: PopoverContentVariant['size'];
} }
export const Popover = React.memo<PopoverProps>( export const Popover = React.memo<PopoverProps>(
@ -25,16 +27,18 @@ export const Popover = React.memo<PopoverProps>(
side, side,
className = '', className = '',
headerContent, headerContent,
triggerType = 'click', trigger = 'click',
size = 'default',
...props ...props
}) => { }) => {
return ( return (
<PopoverBase triggerType={triggerType} {...props}> <PopoverBase trigger={trigger} {...props}>
<PopoverTrigger asChild>{children}</PopoverTrigger> <PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverContent <PopoverContent
align={align} align={align}
side={side} side={side}
className={className} className={className}
size={size}
headerContent={headerContent && <PopoverHeaderContent title={headerContent} />}> headerContent={headerContent && <PopoverHeaderContent title={headerContent} />}>
{content} {content}
</PopoverContent> </PopoverContent>

View File

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