Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
171
apps/web/core/components/analytics/insight-table/data-table.tsx
Normal file
171
apps/web/core/components/analytics/insight-table/data-table.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import type {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
Table as TanstackTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { Search } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// plane package imports
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@plane/propel/table";
|
||||
import { cn } from "@plane/utils";
|
||||
// plane web components
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
searchPlaceholder: string;
|
||||
actions?: (table: TanstackTable<TData>) => React.ReactNode;
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, actions }: DataTableProps<TData, TValue>) {
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const { t } = useTranslation();
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
columnFilters,
|
||||
},
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="relative flex max-w-[300px] items-center gap-4 ">
|
||||
{table.getHeaderGroups()?.[0]?.headers?.[0]?.id && (
|
||||
<div className="flex items-center gap-2 whitespace-nowrap text-sm text-custom-text-400">
|
||||
{searchPlaceholder}
|
||||
</div>
|
||||
)}
|
||||
{!isSearchOpen && (
|
||||
<button
|
||||
type="button"
|
||||
className="-mr-5 grid place-items-center rounded p-2 text-custom-text-400 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
setIsSearchOpen(true);
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<Search className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"mr-auto flex w-0 items-center justify-start gap-1 overflow-hidden rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 opacity-0 transition-[width] ease-linear",
|
||||
{
|
||||
"w-64 border-custom-border-200 px-2.5 py-1.5 opacity-100": isSearchOpen,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Search className="h-3.5 w-3.5" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
|
||||
placeholder="Search"
|
||||
value={table.getColumn(table.getHeaderGroups()?.[0]?.headers?.[0]?.id)?.getFilterValue() as string}
|
||||
onChange={(e) => {
|
||||
const columnId = table.getHeaderGroups()?.[0]?.headers?.[0]?.id;
|
||||
if (columnId) table.getColumn(columnId)?.setFilterValue(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
setIsSearchOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isSearchOpen && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center"
|
||||
onClick={() => {
|
||||
const columnId = table.getHeaderGroups()?.[0]?.headers?.[0]?.id;
|
||||
if (columnId) {
|
||||
table.getColumn(columnId)?.setFilterValue("");
|
||||
}
|
||||
setIsSearchOpen(false);
|
||||
}}
|
||||
>
|
||||
<CloseIcon className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{actions && <div>{actions(table)}</div>}
|
||||
</div>
|
||||
|
||||
<div className="rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id} colSpan={header.colSpan} className="whitespace-nowrap">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: (flexRender(header.column.columnDef.header, header.getContext()) as any)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length > 0 ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext()) as any}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="p-0">
|
||||
<EmptyStateCompact
|
||||
assetKey="unknown"
|
||||
assetClassName="size-20"
|
||||
rootClassName="border border-custom-border-100 px-5 py-10 md:py-20 md:px-20"
|
||||
title={t("workspace_empty_state.analytics_work_items.title")}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
34
apps/web/core/components/analytics/insight-table/loader.tsx
Normal file
34
apps/web/core/components/analytics/insight-table/loader.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as React from "react";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@plane/propel/table";
|
||||
import { Loader } from "@plane/ui";
|
||||
|
||||
interface TableSkeletonProps {
|
||||
columns: ColumnDef<any>[];
|
||||
rows: number;
|
||||
}
|
||||
|
||||
export const TableLoader: React.FC<TableSkeletonProps> = ({ columns, rows }) => (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column, index) => (
|
||||
<TableHead key={column.header?.toString() ?? index}>
|
||||
{typeof column.header === "string" ? column.header : ""}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{Array.from({ length: rows }).map((_, rowIndex) => (
|
||||
<TableRow key={rowIndex}>
|
||||
{columns.map((_, colIndex) => (
|
||||
<TableCell key={colIndex}>
|
||||
<Loader.Item height="20px" width="100%" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
45
apps/web/core/components/analytics/insight-table/root.tsx
Normal file
45
apps/web/core/components/analytics/insight-table/root.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { ColumnDef, Row, Table } from "@tanstack/react-table";
|
||||
import { Download } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import type { AnalyticsTableDataMap, TAnalyticsTabsBase } from "@plane/types";
|
||||
import { DataTable } from "./data-table";
|
||||
import { TableLoader } from "./loader";
|
||||
interface InsightTableProps<T extends Exclude<TAnalyticsTabsBase, "overview">> {
|
||||
analyticsType: T;
|
||||
data?: AnalyticsTableDataMap[T][];
|
||||
isLoading?: boolean;
|
||||
columns: ColumnDef<AnalyticsTableDataMap[T]>[];
|
||||
columnsLabels?: Record<string, string>;
|
||||
headerText: string;
|
||||
onExport?: (rows: Row<AnalyticsTableDataMap[T]>[]) => void;
|
||||
}
|
||||
|
||||
export const InsightTable = <T extends Exclude<TAnalyticsTabsBase, "overview">>(
|
||||
props: InsightTableProps<T>
|
||||
): React.ReactElement => {
|
||||
const { data, isLoading, columns, headerText, onExport } = props;
|
||||
const { t } = useTranslation();
|
||||
if (isLoading) {
|
||||
return <TableLoader columns={columns} rows={5} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data || []}
|
||||
searchPlaceholder={`${data?.length || 0} ${headerText}`}
|
||||
actions={(table: Table<AnalyticsTableDataMap[T]>) => (
|
||||
<Button
|
||||
variant="accent-primary"
|
||||
prependIcon={<Download className="h-3.5 w-3.5" />}
|
||||
onClick={() => onExport?.(table.getFilteredRowModel().rows)}
|
||||
>
|
||||
<div>{t("exporter.csv.short_description")}</div>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user