Skip to content

Commit

Permalink
[UI v2] Variables table - part 2 (#16025)
Browse files Browse the repository at this point in the history
  • Loading branch information
desertaxle authored Nov 15, 2024
1 parent 2faeb9f commit c87003f
Show file tree
Hide file tree
Showing 12 changed files with 748 additions and 68 deletions.
2 changes: 1 addition & 1 deletion ui-v2/src/components/ui/button/button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import * as React from "react";

import { cn } from "@/lib/utils";
Expand Down
52 changes: 47 additions & 5 deletions ui-v2/src/components/ui/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { type Table as TanstackTable, flexRender } from "@tanstack/react-table";

import {
Expand All @@ -25,7 +32,7 @@ export function DataTable<TData>({
table: TanstackTable<TData>;
}) {
return (
<>
<div className="flex flex-col gap-4">
<div className="rounded-md border">
<Table>
<TableHeader>
Expand Down Expand Up @@ -74,8 +81,39 @@ export function DataTable<TData>({
</TableBody>
</Table>
</div>
<DataTablePagination table={table} />
</>
<div className="flex flex-row justify-between items-center">
<DataTablePageSize table={table} />
<DataTablePagination table={table} />
</div>
</div>
);
}

interface DataTablePageSizeProps<TData> {
table: TanstackTable<TData>;
}

function DataTablePageSize<TData>({ table }: DataTablePageSizeProps<TData>) {
return (
<div className="flex flex-row items-center gap-2 text-xs text-muted-foreground">
<span className="whitespace-nowrap">Items per page</span>
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger aria-label="Items per page">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="25">25</SelectItem>
<SelectItem value="50">50</SelectItem>
</SelectContent>
</Select>
</div>
);
}

Expand All @@ -88,6 +126,11 @@ export function DataTablePagination<TData>({
table,
className,
}: DataTablePaginationProps<TData>) {
const totalPages = table.getPageCount();
const currentPage = Math.min(
Math.ceil(table.getState().pagination.pageIndex + 1),
totalPages,
);
return (
<Pagination className={cn("justify-end", className)}>
<PaginationContent>
Expand All @@ -102,8 +145,7 @@ export function DataTablePagination<TData>({
/>
</PaginationItem>
<PaginationItem className="text-sm">
Page {Math.ceil(table.getState().pagination.pageIndex + 1)} of{" "}
{table.getPageCount()}
Page {currentPage} of {totalPages}
</PaginationItem>
<PaginationItem>
<PaginationNextButton
Expand Down
58 changes: 56 additions & 2 deletions ui-v2/src/components/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as React from "react";

import { useEffect, useState } from "react";
import useDebounce from "@/hooks/use-debounce";
import { cn } from "@/lib/utils";
import { SearchIcon } from "lucide-react";

type InputProps = React.ComponentProps<"input"> & {
className?: string;
Expand All @@ -24,4 +26,56 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
);
Input.displayName = "Input";

export { Input, type InputProps };
type IconInputProps = InputProps & {
Icon: React.ElementType;
};

const IconInput = React.forwardRef<HTMLInputElement, IconInputProps>(
({ className, Icon, ...props }, ref) => {
return (
<div className="relative w-full">
<Icon className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input className={cn("pl-8", className)} ref={ref} {...props} />
</div>
);
},
);
IconInput.displayName = "IconInput";

type SearchInputProps = Omit<IconInputProps, "Icon"> & {
debounceMs?: number;
};

const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
({ className, debounceMs = 200, onChange, value, ...props }, ref) => {
const [state, setState] = useState<{
value: typeof value;
event?: React.ChangeEvent<HTMLInputElement>;
}>({ value });
const debouncedValue = useDebounce(state.value, debounceMs);

useEffect(() => {
if (debouncedValue && state.event) {
onChange?.(state.event);
}
}, [debouncedValue, onChange, state.event]);

useEffect(() => {
setState({ value });
}, [value]);

return (
<IconInput
Icon={SearchIcon}
className={className}
ref={ref}
value={state.value}
onChange={(e) => setState({ value: e.target.value, event: e })}
{...props}
/>
);
},
);
SearchInput.displayName = "SearchInput";

export { Input, type InputProps, IconInput, SearchInput };
19 changes: 13 additions & 6 deletions ui-v2/src/components/ui/tags-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@ import { X } from "lucide-react";
type TagsInputProps = InputProps & {
value?: string[];
onChange?: (tags: string[]) => void;
placeholder?: string;
};

const TagsInput = React.forwardRef<HTMLInputElement, TagsInputProps>(
({ onChange, value = [], onBlur, ...props }: TagsInputProps = {}) => {
({
onChange,
value = [],
onBlur,
placeholder = "Enter tags",
...props
}: TagsInputProps = {}) => {
const [inputValue, setInputValue] = useState("");

const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -54,10 +61,10 @@ const TagsInput = React.forwardRef<HTMLInputElement, TagsInputProps>(
};

return (
<div className="flex flex-wrap items-center border rounded-md focus-within:ring-1 focus-within:ring-ring ">
<div className="flex flex-wrap items-center gap-2 px-2">
<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="gap-1 px-2 mt-2">
<Badge key={tag} variant="secondary" className="ml-1">
{tag}
<button
type="button"
Expand All @@ -77,8 +84,8 @@ const TagsInput = React.forwardRef<HTMLInputElement, TagsInputProps>(
onKeyDown={handleInputKeyDown}
onBlur={handleInputBlur(onBlur)}
className="flex-grow border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
placeholder="Enter tags"
aria-label="Enter tags"
placeholder={placeholder}
aria-label={placeholder}
{...props}
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion ui-v2/src/components/variables/data-table/cells.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const ActionsCell = ({
<div className="flex flex-row justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<Button variant="outline" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreVerticalIcon className="h-4 w-4" />
</Button>
Expand Down
106 changes: 93 additions & 13 deletions ui-v2/src/components/variables/data-table/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@ import {
createColumnHelper,
type PaginationState,
type OnChangeFn,
type ColumnFiltersState,
} from "@tanstack/react-table";
import { DataTable } from "@/components/ui/data-table";
import { Badge } from "@/components/ui/badge";
import { ActionsCell } from "./cells";
import { useCallback } from "react";
import { SearchInput } from "@/components/ui/input";
import { TagsInput } from "@/components/ui/tags-input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type React from "react";

const columnHelper = createColumnHelper<components["schemas"]["Variable"]>();

Expand Down Expand Up @@ -66,35 +78,103 @@ const columns = [
}),
];

type VariablesDataTableProps = {
variables: components["schemas"]["Variable"][];
currentVariableCount: number;
pagination: PaginationState;
onPaginationChange: OnChangeFn<PaginationState>;
columnFilters: ColumnFiltersState;
onColumnFiltersChange: OnChangeFn<ColumnFiltersState>;
sorting: components["schemas"]["VariableSort"];
onSortingChange: (sortKey: components["schemas"]["VariableSort"]) => void;
};

export const VariablesDataTable = ({
variables,
totalVariableCount,
currentVariableCount,
pagination,
onPaginationChange,
}: {
variables: components["schemas"]["Variable"][];
totalVariableCount: number;
pagination: PaginationState;
onPaginationChange: OnChangeFn<PaginationState>;
}) => {
columnFilters,
onColumnFiltersChange,
sorting,
onSortingChange,
}: VariablesDataTableProps) => {
const nameSearchValue = columnFilters.find((filter) => filter.id === "name")
?.value as string;
const tagsSearchValue = columnFilters.find((filter) => filter.id === "tags")
?.value as string[];
const handleNameSearchChange = useCallback(
(value?: string) => {
onColumnFiltersChange((prev) => [
...prev.filter((filter) => filter.id !== "name"),
{ id: "name", value },
]);
},
[onColumnFiltersChange],
);

const handleTagsSearchChange: React.ChangeEventHandler<HTMLInputElement> &
((tags: string[]) => void) = useCallback(
(e: string[] | React.ChangeEvent<HTMLInputElement>) => {
const tags = Array.isArray(e) ? e : e.target.value;

onColumnFiltersChange((prev) => [
...prev.filter((filter) => filter.id !== "tags"),
{ id: "tags", value: tags },
]);
},
[onColumnFiltersChange],
);

const table = useReactTable({
data: variables,
columns: columns,
state: {
pagination,
columnFilters,
},
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
onPaginationChange: onPaginationChange,
rowCount: totalVariableCount,
onColumnFiltersChange: onColumnFiltersChange,
rowCount: currentVariableCount,
});

return (
<div className="flex flex-col gap-6 mt-2">
<div className="flex flex-row justify-between items-center">
<p className="text-sm text-muted-foreground">
{totalVariableCount} Variables
</p>
<div>
<div className="grid sm:grid-cols-2 md:grid-cols-6 lg:grid-cols-12 gap-2 pb-4 items-center">
<div className="sm:col-span-2 md:col-span-6 lg:col-span-4 order-last lg:order-first">
<p className="text-sm text-muted-foreground">
{currentVariableCount} Variables
</p>
</div>
<div className="sm:col-span-2 md:col-span-2 lg:col-span-3">
<SearchInput
placeholder="Search variables"
value={nameSearchValue}
onChange={(e) => handleNameSearchChange(e.target.value)}
/>
</div>
<div className="xs:col-span-1 md:col-span-2 lg:col-span-3">
<TagsInput
placeholder="Filter by tags"
onChange={handleTagsSearchChange}
value={tagsSearchValue}
/>
</div>
<div className="xs:col-span-1 md:col-span-2 lg:col-span-2">
<Select value={sorting} onValueChange={onSortingChange}>
<SelectTrigger aria-label="Variable sort order">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="CREATED_DESC">Created</SelectItem>
<SelectItem value="UPDATED_DESC">Updated</SelectItem>
<SelectItem value="NAME_ASC">A to Z</SelectItem>
<SelectItem value="NAME_DESC">Z to A</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DataTable table={table} />
</div>
Expand Down
Loading

0 comments on commit c87003f

Please sign in to comment.