Skip to content

Commit 28fd4c3

Browse files
authored
[UI v2] Refactor parts of variables components (#16084)
1 parent f2221ba commit 28fd4c3

26 files changed

+957
-651
lines changed

ui-v2/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ node_modules
1111
dist
1212
dist-ssr
1313
*.local
14+
.vite
1415

1516
# Editor directories and files
1617
.vscode/*

ui-v2/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"build": "tsc -b && vite build",
99
"test": "vitest",
1010
"lint": "eslint .",
11+
"lint:fix": "eslint . --fix",
1112
"format:check": "biome format",
1213
"format": "biome format --write",
1314
"preview": "vite preview",

ui-v2/src/components/ui/badge/badge.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@ import type { VariantProps } from "class-variance-authority";
22

33
import { cn } from "@/lib/utils";
44
import { badgeVariants } from "./styles";
5+
import React from "react";
56

67
export interface BadgeProps
78
extends React.HTMLAttributes<HTMLDivElement>,
89
VariantProps<typeof badgeVariants> {}
910

10-
export function Badge({ className, variant, ...props }: BadgeProps) {
11-
return (
12-
<div className={cn(badgeVariants({ variant }), className)} {...props} />
13-
);
14-
}
11+
export const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
12+
({ className, variant, ...props }, ref) => (
13+
<div
14+
ref={ref}
15+
className={cn(badgeVariants({ variant }), className)}
16+
{...props}
17+
/>
18+
),
19+
);
20+
21+
Badge.displayName = "Badge";

ui-v2/src/components/ui/data-table.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@ export function DataTable<TData>({
5959
data-state={row.getIsSelected() && "selected"}
6060
>
6161
{row.getVisibleCells().map((cell) => (
62-
<TableCell key={cell.id}>
62+
<TableCell
63+
key={cell.id}
64+
style={{
65+
maxWidth: `${cell.column.columnDef.maxSize}px`,
66+
}}
67+
>
6368
{flexRender(
6469
cell.column.columnDef.cell,
6570
cell.getContext(),

ui-v2/src/components/ui/json-input.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import React, { useRef, useEffect } from "react";
22

33
import { json } from "@codemirror/lang-json";
44
import { cn } from "@/lib/utils";
5-
import { useCodeMirror } from "@uiw/react-codemirror";
5+
import { useCodeMirror, EditorView } from "@uiw/react-codemirror";
66

7-
const extensions = [json()];
7+
const extensions = [json(), EditorView.lineWrapping];
88

99
type JsonInputProps = React.ComponentProps<"div"> & {
1010
value?: string;
@@ -28,6 +28,11 @@ export const JsonInput = React.forwardRef<HTMLDivElement, JsonInputProps>(
2828
onBlur,
2929
indentWithTab: false,
3030
editable: !disabled,
31+
basicSetup: {
32+
highlightActiveLine: !disabled,
33+
foldGutter: !disabled,
34+
highlightActiveLineGutter: !disabled,
35+
},
3136
});
3237

3338
useEffect(() => {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { TagBadgeGroup } from "@/components/ui/tag-badge-group.tsx";
2+
import type { Meta, StoryObj } from "@storybook/react";
3+
import type { ComponentProps } from "react";
4+
5+
export default {
6+
title: "UI/TagBadgeGroup",
7+
component: TagBadgeGroup,
8+
args: {
9+
tags: [],
10+
},
11+
// To control input value in Stories via useState()
12+
render: function Render(args: ComponentProps<typeof TagBadgeGroup>) {
13+
return <TagBadgeGroup {...args} />;
14+
},
15+
} satisfies Meta<typeof TagBadgeGroup>;
16+
17+
type Story = StoryObj<typeof TagBadgeGroup>;
18+
19+
export const TwoTags: Story = {
20+
args: { tags: ["testTag", "testTag2"] },
21+
};
22+
23+
export const FourTags: Story = {
24+
args: { tags: ["testTag", "testTag2", "testTag3", "testTag4"] },
25+
};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Badge, type BadgeProps } from "./badge";
2+
import { HoverCard, HoverCardContent, HoverCardTrigger } from "./hover-card";
3+
import { TagBadge } from "./tag-badge";
4+
5+
type TagBadgeGroupProps = {
6+
tags: string[];
7+
variant?: BadgeProps["variant"];
8+
maxTagsDisplayed?: number;
9+
onTagsChange?: (tags: string[]) => void;
10+
};
11+
12+
export const TagBadgeGroup = ({
13+
tags,
14+
variant,
15+
maxTagsDisplayed = 2,
16+
onTagsChange,
17+
}: TagBadgeGroupProps) => {
18+
const removeTag = (tag: string) => {
19+
onTagsChange?.(tags.filter((t) => t !== tag));
20+
};
21+
22+
const numTags = tags.length;
23+
24+
if (numTags > maxTagsDisplayed) {
25+
return (
26+
<HoverCard>
27+
<HoverCardTrigger asChild>
28+
<Badge variant={variant} className="ml-1 whitespace-nowrap">
29+
{numTags} tags
30+
</Badge>
31+
</HoverCardTrigger>
32+
<HoverCardContent className="flex flex-wrap gap-1">
33+
{tags.map((tag) => (
34+
<TagBadge
35+
key={tag}
36+
tag={tag}
37+
onRemove={onTagsChange ? () => removeTag(tag) : undefined}
38+
/>
39+
))}
40+
</HoverCardContent>
41+
</HoverCard>
42+
);
43+
}
44+
45+
return (
46+
<>
47+
{tags.map((tag) => (
48+
<TagBadge
49+
key={tag}
50+
tag={tag}
51+
onRemove={onTagsChange ? () => removeTag(tag) : undefined}
52+
variant={variant}
53+
/>
54+
))}
55+
</>
56+
);
57+
};

ui-v2/src/components/ui/tag-badge.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { X } from "lucide-react";
2+
import { Badge, type BadgeProps } from "./badge";
3+
4+
type TagBadgeProps = {
5+
tag: string;
6+
variant?: BadgeProps["variant"];
7+
onRemove?: () => void;
8+
};
9+
10+
export const TagBadge = ({ tag, variant, onRemove }: TagBadgeProps) => {
11+
return (
12+
<Badge variant={variant} className="ml-1 max-w-20" title={tag}>
13+
<span className="truncate">{tag}</span>
14+
{onRemove && (
15+
<button
16+
type="button"
17+
onClick={onRemove}
18+
className="text-muted-foreground hover:text-foreground"
19+
aria-label={`Remove ${tag} tag`}
20+
>
21+
<X size={14} />
22+
</button>
23+
)}
24+
</Badge>
25+
);
26+
};

ui-v2/src/components/ui/tags-input.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { TagsInput } from "@/components/ui/tags-input.tsx";
2-
import { Meta, StoryObj } from "@storybook/react";
2+
import type { Meta, StoryObj } from "@storybook/react";
33
import { expect, fn, userEvent, within } from "@storybook/test";
4-
import { ComponentProps, useState } from "react";
4+
import { type ComponentProps, useState } from "react";
55

66
export default {
77
title: "UI/TagsInput",

ui-v2/src/components/ui/tags-input.tsx

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import React from "react";
22
import { useState } from "react";
33
import type { KeyboardEvent, ChangeEvent, FocusEvent } from "react";
44
import { Input, type InputProps } from "@/components/ui/input";
5-
import { Badge } from "@/components/ui/badge";
6-
import { X } from "lucide-react";
5+
import { TagBadgeGroup } from "./tag-badge-group";
76

87
type TagsInputProps = InputProps & {
98
value?: string[];
@@ -62,21 +61,11 @@ const TagsInput = React.forwardRef<HTMLInputElement, TagsInputProps>(
6261

6362
return (
6463
<div className="flex items-center border rounded-md focus-within:ring-1 focus-within:ring-ring ">
65-
<div className="flex items-center">
66-
{value.map((tag, index) => (
67-
<Badge key={tag} variant="secondary" className="ml-1">
68-
{tag}
69-
<button
70-
type="button"
71-
onClick={() => removeTag(index)}
72-
className="text-muted-foreground hover:text-foreground"
73-
aria-label={`Remove ${tag} tag`}
74-
>
75-
<X size={14} />
76-
</button>
77-
</Badge>
78-
))}
79-
</div>
64+
<TagBadgeGroup
65+
tags={value}
66+
onTagsChange={onChange}
67+
variant="secondary"
68+
/>
8069
<Input
8170
type="text"
8271
value={inputValue}

ui-v2/src/components/ui/toast.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ const ToastClose = React.forwardRef<
9393
<ToastPrimitives.Close
9494
ref={ref}
9595
className={cn(
96-
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
96+
"absolute right-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
9797
className,
9898
)}
9999
toast-close=""

ui-v2/src/components/variables/data-table/cells.tsx

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,22 @@ import {
44
DropdownMenuItem,
55
DropdownMenuLabel,
66
DropdownMenuTrigger,
7-
} from "../../ui/dropdown-menu";
8-
import { Button } from "../../ui/button";
7+
} from "@/components/ui/dropdown-menu";
8+
import { Button } from "@/components/ui/button";
99
import { MoreVerticalIcon } from "lucide-react";
1010
import { useMutation, useQueryClient } from "@tanstack/react-query";
1111
import { getQueryService } from "@/api/service";
1212
import type { CellContext } from "@tanstack/react-table";
1313
import type { components } from "@/api/prefect";
1414
import { useToast } from "@/hooks/use-toast";
15+
import { JsonInput } from "@/components/ui/json-input";
16+
import {
17+
HoverCard,
18+
HoverCardContent,
19+
HoverCardTrigger,
20+
} from "@/components/ui/hover-card";
21+
import { useRef } from "react";
22+
import { useIsOverflowing } from "@/hooks/use-is-overflowing";
1523

1624
type ActionsCellProps = CellContext<
1725
components["schemas"]["Variable"],
@@ -62,14 +70,22 @@ export const ActionsCell = ({ row, onVariableEdit }: ActionsCellProps) => {
6270
<DropdownMenuContent align="end">
6371
<DropdownMenuLabel>Actions</DropdownMenuLabel>
6472
<DropdownMenuItem
65-
onClick={() => void navigator.clipboard.writeText(id)}
73+
onClick={() => {
74+
void navigator.clipboard.writeText(id);
75+
toast({
76+
title: "ID copied",
77+
});
78+
}}
6679
>
6780
Copy ID
6881
</DropdownMenuItem>
6982
<DropdownMenuItem
70-
onClick={() =>
71-
void navigator.clipboard.writeText(row.original.name)
72-
}
83+
onClick={() => {
84+
void navigator.clipboard.writeText(row.original.name);
85+
toast({
86+
title: "Name copied",
87+
});
88+
}}
7389
>
7490
Copy Name
7591
</DropdownMenuItem>
@@ -81,6 +97,9 @@ export const ActionsCell = ({ row, onVariableEdit }: ActionsCellProps) => {
8197
: row.original.value;
8298
if (copyValue) {
8399
void navigator.clipboard.writeText(copyValue);
100+
toast({
101+
title: "Value copied",
102+
});
84103
}
85104
}}
86105
>
@@ -95,3 +114,29 @@ export const ActionsCell = ({ row, onVariableEdit }: ActionsCellProps) => {
95114
</div>
96115
);
97116
};
117+
118+
export const ValueCell = (
119+
props: CellContext<components["schemas"]["Variable"], unknown>,
120+
) => {
121+
const value = props.getValue();
122+
const codeRef = useRef<HTMLDivElement>(null);
123+
const isOverflowing = useIsOverflowing(codeRef);
124+
125+
if (!value) return null;
126+
return (
127+
// Disable the hover card if the value is overflowing
128+
<HoverCard open={isOverflowing ? undefined : false}>
129+
<HoverCardTrigger asChild>
130+
<code
131+
ref={codeRef}
132+
className="px-2 py-1 font-mono text-sm text-ellipsis overflow-hidden whitespace-nowrap block"
133+
>
134+
{JSON.stringify(value, null, 2)}
135+
</code>
136+
</HoverCardTrigger>
137+
<HoverCardContent className="p-0">
138+
<JsonInput value={JSON.stringify(value, null, 2)} disabled />
139+
</HoverCardContent>
140+
</HoverCard>
141+
);
142+
};

0 commit comments

Comments
 (0)