Skip to content

Commit

Permalink
[UI v2] Refactor parts of variables components (PrefectHQ#16084)
Browse files Browse the repository at this point in the history
  • Loading branch information
desertaxle authored Nov 25, 2024
1 parent f2221ba commit 28fd4c3
Show file tree
Hide file tree
Showing 26 changed files with 957 additions and 651 deletions.
1 change: 1 addition & 0 deletions ui-v2/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
.vite

# Editor directories and files
.vscode/*
Expand Down
1 change: 1 addition & 0 deletions ui-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build": "tsc -b && vite build",
"test": "vitest",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format:check": "biome format",
"format": "biome format --write",
"preview": "vite preview",
Expand Down
17 changes: 12 additions & 5 deletions ui-v2/src/components/ui/badge/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ import type { VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";
import { badgeVariants } from "./styles";
import React from "react";

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

export function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
({ className, variant, ...props }, ref) => (
<div
ref={ref}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
),
);

Badge.displayName = "Badge";
7 changes: 6 additions & 1 deletion ui-v2/src/components/ui/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ export function DataTable<TData>({
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
<TableCell
key={cell.id}
style={{
maxWidth: `${cell.column.columnDef.maxSize}px`,
}}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
Expand Down
9 changes: 7 additions & 2 deletions ui-v2/src/components/ui/json-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import React, { useRef, useEffect } from "react";

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

const extensions = [json()];
const extensions = [json(), EditorView.lineWrapping];

type JsonInputProps = React.ComponentProps<"div"> & {
value?: string;
Expand All @@ -28,6 +28,11 @@ export const JsonInput = React.forwardRef<HTMLDivElement, JsonInputProps>(
onBlur,
indentWithTab: false,
editable: !disabled,
basicSetup: {
highlightActiveLine: !disabled,
foldGutter: !disabled,
highlightActiveLineGutter: !disabled,
},
});

useEffect(() => {
Expand Down
25 changes: 25 additions & 0 deletions ui-v2/src/components/ui/tag-badge-group.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { TagBadgeGroup } from "@/components/ui/tag-badge-group.tsx";
import type { Meta, StoryObj } from "@storybook/react";
import type { ComponentProps } from "react";

export default {
title: "UI/TagBadgeGroup",
component: TagBadgeGroup,
args: {
tags: [],
},
// To control input value in Stories via useState()
render: function Render(args: ComponentProps<typeof TagBadgeGroup>) {
return <TagBadgeGroup {...args} />;
},
} satisfies Meta<typeof TagBadgeGroup>;

type Story = StoryObj<typeof TagBadgeGroup>;

export const TwoTags: Story = {
args: { tags: ["testTag", "testTag2"] },
};

export const FourTags: Story = {
args: { tags: ["testTag", "testTag2", "testTag3", "testTag4"] },
};
57 changes: 57 additions & 0 deletions ui-v2/src/components/ui/tag-badge-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Badge, type BadgeProps } from "./badge";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "./hover-card";
import { TagBadge } from "./tag-badge";

type TagBadgeGroupProps = {
tags: string[];
variant?: BadgeProps["variant"];
maxTagsDisplayed?: number;
onTagsChange?: (tags: string[]) => void;
};

export const TagBadgeGroup = ({
tags,
variant,
maxTagsDisplayed = 2,
onTagsChange,
}: TagBadgeGroupProps) => {
const removeTag = (tag: string) => {
onTagsChange?.(tags.filter((t) => t !== tag));
};

const numTags = tags.length;

if (numTags > maxTagsDisplayed) {
return (
<HoverCard>
<HoverCardTrigger asChild>
<Badge variant={variant} className="ml-1 whitespace-nowrap">
{numTags} tags
</Badge>
</HoverCardTrigger>
<HoverCardContent className="flex flex-wrap gap-1">
{tags.map((tag) => (
<TagBadge
key={tag}
tag={tag}
onRemove={onTagsChange ? () => removeTag(tag) : undefined}
/>
))}
</HoverCardContent>
</HoverCard>
);
}

return (
<>
{tags.map((tag) => (
<TagBadge
key={tag}
tag={tag}
onRemove={onTagsChange ? () => removeTag(tag) : undefined}
variant={variant}
/>
))}
</>
);
};
26 changes: 26 additions & 0 deletions ui-v2/src/components/ui/tag-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { X } from "lucide-react";
import { Badge, type BadgeProps } from "./badge";

type TagBadgeProps = {
tag: string;
variant?: BadgeProps["variant"];
onRemove?: () => void;
};

export const TagBadge = ({ tag, variant, onRemove }: TagBadgeProps) => {
return (
<Badge variant={variant} className="ml-1 max-w-20" title={tag}>
<span className="truncate">{tag}</span>
{onRemove && (
<button
type="button"
onClick={onRemove}
className="text-muted-foreground hover:text-foreground"
aria-label={`Remove ${tag} tag`}
>
<X size={14} />
</button>
)}
</Badge>
);
};
4 changes: 2 additions & 2 deletions ui-v2/src/components/ui/tags-input.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TagsInput } from "@/components/ui/tags-input.tsx";
import { Meta, StoryObj } from "@storybook/react";
import type { Meta, StoryObj } from "@storybook/react";
import { expect, fn, userEvent, within } from "@storybook/test";
import { ComponentProps, useState } from "react";
import { type ComponentProps, useState } from "react";

export default {
title: "UI/TagsInput",
Expand Down
23 changes: 6 additions & 17 deletions ui-v2/src/components/ui/tags-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import React from "react";
import { useState } from "react";
import type { KeyboardEvent, ChangeEvent, FocusEvent } from "react";
import { Input, type InputProps } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { X } from "lucide-react";
import { TagBadgeGroup } from "./tag-badge-group";

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

return (
<div className="flex items-center border rounded-md focus-within:ring-1 focus-within:ring-ring ">
<div className="flex items-center">
{value.map((tag, index) => (
<Badge key={tag} variant="secondary" className="ml-1">
{tag}
<button
type="button"
onClick={() => removeTag(index)}
className="text-muted-foreground hover:text-foreground"
aria-label={`Remove ${tag} tag`}
>
<X size={14} />
</button>
</Badge>
))}
</div>
<TagBadgeGroup
tags={value}
onTagsChange={onChange}
variant="secondary"
/>
<Input
type="text"
value={inputValue}
Expand Down
2 changes: 1 addition & 1 deletion ui-v2/src/components/ui/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close
ref={ref}
className={cn(
"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",
"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",
className,
)}
toast-close=""
Expand Down
57 changes: 51 additions & 6 deletions ui-v2/src/components/variables/data-table/cells.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@ import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "../../ui/dropdown-menu";
import { Button } from "../../ui/button";
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { MoreVerticalIcon } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { getQueryService } from "@/api/service";
import type { CellContext } from "@tanstack/react-table";
import type { components } from "@/api/prefect";
import { useToast } from "@/hooks/use-toast";
import { JsonInput } from "@/components/ui/json-input";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { useRef } from "react";
import { useIsOverflowing } from "@/hooks/use-is-overflowing";

type ActionsCellProps = CellContext<
components["schemas"]["Variable"],
Expand Down Expand Up @@ -62,14 +70,22 @@ export const ActionsCell = ({ row, onVariableEdit }: ActionsCellProps) => {
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => void navigator.clipboard.writeText(id)}
onClick={() => {
void navigator.clipboard.writeText(id);
toast({
title: "ID copied",
});
}}
>
Copy ID
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
void navigator.clipboard.writeText(row.original.name)
}
onClick={() => {
void navigator.clipboard.writeText(row.original.name);
toast({
title: "Name copied",
});
}}
>
Copy Name
</DropdownMenuItem>
Expand All @@ -81,6 +97,9 @@ export const ActionsCell = ({ row, onVariableEdit }: ActionsCellProps) => {
: row.original.value;
if (copyValue) {
void navigator.clipboard.writeText(copyValue);
toast({
title: "Value copied",
});
}
}}
>
Expand All @@ -95,3 +114,29 @@ export const ActionsCell = ({ row, onVariableEdit }: ActionsCellProps) => {
</div>
);
};

export const ValueCell = (
props: CellContext<components["schemas"]["Variable"], unknown>,
) => {
const value = props.getValue();
const codeRef = useRef<HTMLDivElement>(null);
const isOverflowing = useIsOverflowing(codeRef);

if (!value) return null;
return (
// Disable the hover card if the value is overflowing
<HoverCard open={isOverflowing ? undefined : false}>
<HoverCardTrigger asChild>
<code
ref={codeRef}
className="px-2 py-1 font-mono text-sm text-ellipsis overflow-hidden whitespace-nowrap block"
>
{JSON.stringify(value, null, 2)}
</code>
</HoverCardTrigger>
<HoverCardContent className="p-0">
<JsonInput value={JSON.stringify(value, null, 2)} disabled />
</HoverCardContent>
</HoverCard>
);
};
Loading

0 comments on commit 28fd4c3

Please sign in to comment.