implement remaining features of the server page (#9)

* implement remaining features of the server page

* fix fm init

* ?
This commit is contained in:
UUBulb authored and GitHub committed 2024-11-21 22:20:14 +08:00
1 parent be2f5a519f
commit 8248d6c910
18 files changed
+994 -32

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
"use client"
import * as React from "react"
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { formatPath } from "@/lib/utils"
const ITEMS_TO_DISPLAY = 3
interface FilepathProps {
path: string;
setPath: React.Dispatch<React.SetStateAction<string>>;
}
function pathToItems(path: string) {
const segments = path.split('/').filter(Boolean);
const result: { href: string; label: string; }[] = [];
let currentPath = '';
segments.forEach(segment => {
currentPath += `/${segment}`;
result.push({ href: currentPath, label: segment });
});
return result;
}
export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => {
const [open, setOpen] = React.useState(false)
const items = pathToItems(formatPath(path));
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<p className="cursor-pointer hover:text-white transition" onClick={() => { setPath('/') }}>{'/'}</p>
</BreadcrumbItem>
<BreadcrumbSeparator />
{items.length > ITEMS_TO_DISPLAY ? (
<>
<BreadcrumbItem>
{
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
className="flex items-center gap-1"
aria-label="Toggle menu"
>
<BreadcrumbEllipsis className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{items.slice(0, -ITEMS_TO_DISPLAY).map((item, index) => (
<DropdownMenuItem key={index}>
<p onClick={() => { setPath(item.href) }}>
{item.label}
</p>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
}
</BreadcrumbItem>
<BreadcrumbSeparator />
</>
) : null}
{items.slice(-ITEMS_TO_DISPLAY).map((item, index, slicedItems) => (
<React.Fragment key={index}>
<BreadcrumbItem className="overflow-auto">
{item.href ? (
<>
<p
className="max-w-20 truncate md:max-w-none cursor-pointer hover:text-white transition"
onClick={() => { setPath(item.href) }}
>
{item.label}
</p>
</>
) : (
<BreadcrumbPage className="max-w-20 truncate md:max-w-none">
{item.label}
</BreadcrumbPage>
)}
</BreadcrumbItem>
{index !== slicedItems.length - 1 ? <BreadcrumbSeparator /> : null}
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
)
}
+36 -2
View File
@@ -1,9 +1,34 @@
import { Plus, Edit2, Trash2, Terminal, CircleArrowUp, Clipboard, Check, FolderClosed, Play } from "lucide-react"
import {
Plus,
Edit2,
Trash2,
Terminal,
CircleArrowUp,
Clipboard,
Check,
FolderClosed,
Play,
Download,
Upload,
Menu,
} from "lucide-react"
import { Button, ButtonProps } from "@/components/ui/button"
import { forwardRef } from "react";
export interface IconButtonProps extends ButtonProps {
icon: "clipboard" | "check" | "edit" | "trash" | "plus" | "terminal" | "update" | "folder-closed" | "play";
icon:
"clipboard" |
"check" |
"edit" |
"trash" |
"plus" |
"terminal" |
"update" |
"folder-closed" |
"play" |
"download" |
"upload" |
"menu";
}
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
@@ -38,6 +63,15 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props,
case "play": {
return <Play />;
}
case "download": {
return <Download />;
}
case "upload": {
return <Upload />;
}
case "menu": {
return <Menu />;
}
}
})()}
</Button>
@@ -0,0 +1,180 @@
"use client";
import {
ColumnDef,
Row,
SortDirection,
SortingState,
flexRender,
getCoreRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { TableCell, TableHead, TableRow } from "@/components/ui/table";
import { HTMLAttributes, forwardRef, useState, useRef, useEffect } from "react";
import { TableVirtuoso } from "react-virtuoso";
import { cn } from "@/lib/utils"
import { ScrollArea } from "@/components/ui/scroll-area";
// Original Table is wrapped with a <div> (see https://ui.shadcn.com/docs/components/table#radix-:r24:-content-manual),
// but here we don't want it, so let's use a new component with only <table> tag
const TableComponent = forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
));
TableComponent.displayName = "TableComponent";
const TableRowComponent = <TData,>(rows: Row<TData>[]) =>
function getTableRow(props: HTMLAttributes<HTMLTableRowElement>) {
// @ts-expect-error data-index is a valid attribute
const index = props["data-index"];
const row = rows[index];
if (!row) return null;
return (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="translate-y-[50%]"
{...props}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
);
};
function SortingIndicator({ isSorted }: { isSorted: SortDirection | false }) {
if (!isSorted) return null;
return (
<div>
{
{
asc: "↑",
desc: "↓",
}[isSorted]
}
</div>
);
}
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
rowComponent?: (rows: Row<TData>[]) => (props: HTMLAttributes<HTMLTableRowElement>) => JSX.Element | null,
}
export function DataTable<TData, TValue>({
columns,
data,
rowComponent,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([{
id: 'type',
desc: true,
}]);
const table = useReactTable({
data,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
const { rows } = table.getRowModel();
const [heightState, setHeight] = useState(0)
const ref = useRef(null);
useEffect(() => {
const calculateHeight = () => {
if (ref.current) {
const virtuosoElement = ref.current;
let topOffset = 0;
let currentElement = virtuosoElement as any;
// Calculate the total offset from the top of the document
while (currentElement) {
topOffset += currentElement.offsetTop || 0;
currentElement = currentElement.offsetParent as HTMLElement;
}
const totalHeight = window.innerHeight;
const calculatedHeight = totalHeight - topOffset;
setHeight(calculatedHeight);
}
};
window.addEventListener('resize', calculateHeight);
calculateHeight(); // Initial calculation
return () => window.removeEventListener('resize', calculateHeight);
}, []);
return (
<div className="rounded-md border" ref={ref} style={{ height: heightState }}>
<TableVirtuoso
totalCount={rows.length}
components={{
Table: TableComponent,
TableRow: rowComponent ? rowComponent(rows) : TableRowComponent(rows),
Scroller: ScrollArea,
}}
fixedHeaderContent={() =>
table.getHeaderGroups().map((headerGroup) => (
// Change header background color to non-transparent
<TableRow className="bg-card hover:bg-muted" key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead
key={header.id}
colSpan={header.colSpan}
style={{
width: header.getSize(),
}}
>
{header.isPlaceholder ? null : (
<div
className="flex items-center"
{...{
style: header.column.getCanSort()
? {
cursor: "pointer",
userSelect: "none",
}
: {},
onClick: header.column.getToggleSortingHandler(),
}}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
<SortingIndicator
isSorted={header.column.getIsSorted()}
/>
</div>
)}
</TableHead>
);
})}
</TableRow>
))
}
/>
</div>
);
}