diff --git a/dashboard/public/locales/en.json b/dashboard/public/locales/en.json
@@ -365,6 +365,7 @@
"desc": "This action cannot be undone."
},
"table": {
+ "no-result-found": "No results found.",
"row-per-page": "Rows per page",
"no-result": "No result",
"selected": "Selected",
diff --git a/dashboard/src/components/data-table/index.ts b/dashboard/src/components/data-table/index.ts
@@ -1,5 +0,0 @@
-export * from './table'
-export * from './pagination'
-export * from './column-header'
-export * from './table-view-option'
-export * from './actions-cell'
diff --git a/dashboard/src/components/data-table/pagination.tsx b/dashboard/src/components/data-table/pagination.tsx
@@ -1,102 +0,0 @@
-import {
- ChevronLeftIcon,
- ChevronRightIcon,
- DoubleArrowLeftIcon,
- DoubleArrowRightIcon,
-} from "@radix-ui/react-icons"
-import { Table } from "@tanstack/react-table"
-import { cn } from "@marzneshin/utils"
-import {
- Button,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@marzneshin/components"
-import { useTranslation } from "react-i18next"
-
-interface DataTablePaginationProps<TData> {
- table: Table<TData>
- disableSelected?: boolean
-}
-
-export function DataTablePagination<TData>({
- table, disableSelected
-}: DataTablePaginationProps<TData>) {
- const { t } = useTranslation();
- return (
- <div className={cn("flex items-center p-2 w-full", disableSelected && "justify-between")}>
- {disableSelected || (
- <div className="flex-1 text-sm text-muted-foreground">
- {table.getFilteredSelectedRowModel().rows.length} {"/"}
- {table.getFilteredRowModel().rows.length} {t('table.selected')}
- </div>
- )}
- <div className="flex items-center space-x-6 lg:space-x-8 w-full justify-between">
- <div className="flex items-center space-x-2">
- <p className="text-sm font-medium">{t('table.row-per-page')}</p>
- <Select
- value={`${table.getState().pagination.pageSize}`}
- onValueChange={(value) => {
- table.setPageSize(Number(value))
- }}
- >
- <SelectTrigger className="h-8 w-[70px]">
- <SelectValue placeholder={table.getState().pagination.pageSize} />
- </SelectTrigger>
- <SelectContent side="top">
- {[10, 20, 30, 40, 50].map((pageSize) => (
- <SelectItem key={pageSize} value={`${pageSize}`}>
- {pageSize}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
- <div className="flex justify-center items-center text-sm font-medium w-[100px]">
- Page {table.getState().pagination.pageIndex + 1} of{" "}
- {table.getPageCount()}
- </div>
- <div className="flex items-center space-x-2">
- <Button
- variant="outline"
- className="hidden p-0 w-8 h-8 lg:flex"
- onClick={() => table.setPageIndex(0)}
- disabled={!table.getCanPreviousPage()}
- >
- <span className="sr-only">Go to first page</span>
- <DoubleArrowLeftIcon className="w-4 h-4" />
- </Button>
- <Button
- variant="outline"
- className="p-0 w-8 h-8"
- onClick={() => table.previousPage()}
- disabled={!table.getCanPreviousPage()}
- >
- <span className="sr-only">Go to previous page</span>
- <ChevronLeftIcon className="w-4 h-4" />
- </Button>
- <Button
- variant="outline"
- className="p-0 w-8 h-8"
- onClick={() => table.nextPage()}
- disabled={!table.getCanNextPage()}
- >
- <span className="sr-only">Go to next page</span>
- <ChevronRightIcon className="w-4 h-4" />
- </Button>
- <Button
- variant="outline"
- className="hidden p-0 w-8 h-8 lg:flex"
- onClick={() => table.setPageIndex(table.getPageCount() - 1)}
- disabled={!table.getCanNextPage()}
- >
- <span className="sr-only">Go to last page</span>
- <DoubleArrowRightIcon className="w-4 h-4" />
- </Button>
- </div>
- </div>
- </div>
- )
-}
diff --git a/dashboard/src/components/index.ts b/dashboard/src/components/index.ts
@@ -2,7 +2,6 @@ export * from "./ui";
export * from "./settings-dialog";
export * from "./header";
export * from "./sidebar";
-export * from "./data-table";
export * from "./delete-confirmation-dialog";
export * from "./nav-dir";
export * from "./header";
diff --git a/dashboard/src/features/admins/components/tables/admins/columns.tsx b/dashboard/src/features/admins/components/tables/admins/columns.tsx
@@ -4,17 +4,19 @@ import {
AdminEnabledPill,
AdminPermissionPill,
} from "@marzneshin/features/admins";
-import { DataTableColumnHeader } from "@marzneshin/components/data-table/column-header";
-import i18n from "@marzneshin/features/i18n";
import {
DataTableActionsCell,
+ DataTableColumnHeader
+} from "@marzneshin/features/entity-table"
+import i18n from "@marzneshin/features/i18n";
+import {
NoPropogationButton,
} from "@marzneshin/components";
import {
type ColumnActions
} from "@marzneshin/features/entity-table";
-export const columns = (actions: ColumnActions<AdminType>): ColumnDef<AdminType>[] => [
+export const columns = (actions: ColumnActions<AdminType>): ColumnDef<AdminType, any>[] => [
{
accessorKey: "username",
header: ({ column }) => (
@@ -44,12 +46,10 @@ export const columns = (actions: ColumnActions<AdminType>): ColumnDef<AdminType>
},
{
id: "actions",
- cell: ({ row }) => {
- return (
- <NoPropogationButton row={row} actions={actions}>
- <DataTableActionsCell {...actions} row={row} />
- </NoPropogationButton>
- );
- },
+ cell: ({ row }) => (
+ <NoPropogationButton row={row} actions={actions}>
+ <DataTableActionsCell {...actions} row={row} />
+ </NoPropogationButton>
+ ),
}
];
diff --git a/dashboard/src/features/admins/components/tables/admins/index.tsx b/dashboard/src/features/admins/components/tables/admins/index.tsx
@@ -1,4 +1,5 @@
import type { FC } from "react";
+import { ColumnDef } from "@tanstack/react-table";
import { fetchAdmins, AdminType, AdminsQueryFetchKey } from "@marzneshin/features/admins";
import { columns as columnsFn } from "./columns";
import { EntityTable } from "@marzneshin/features/entity-table";
@@ -32,9 +33,8 @@ export const AdminsTable: FC = () => {
return (
<EntityTable
fetchEntity={fetchAdmins}
- manualSorting={false}
- columns={columns}
- filteredColumn="username"
+ columns={columns as ColumnDef<unknown>[]}
+ primaryFilter="username"
entityKey={AdminsQueryFetchKey}
onCreate={() => navigate({ to: "/admins/create" })}
onOpen={onOpen}
diff --git a/dashboard/src/features/admins/services/admins.query.ts b/dashboard/src/features/admins/services/admins.query.ts
@@ -8,11 +8,16 @@ import {
} from "@marzneshin/features/entity-table";
export async function fetchAdmins({ queryKey }: EntityQueryKeyType): FetchEntityReturn<AdminType> {
+ const pagination = queryKey[1];
+ const primaryFilter = queryKey[2];
+ const filters = queryKey[4].filters;
return fetch(`/admins`, {
query: {
- page: queryKey[1],
- size: queryKey[2],
- username: queryKey[3],
+ ...pagination,
+ ...filters,
+ username: primaryFilter,
+ descending: queryKey[3].desc,
+ order_by: queryKey[3].sortBy,
}
}).then((result) => {
return {
@@ -22,13 +27,13 @@ export async function fetchAdmins({ queryKey }: EntityQueryKeyType): FetchEntity
});
}
-export const AdminsQueryFetchKey = "users";
+export const AdminsQueryFetchKey = "admins";
export const useAdminsQuery = ({
- page, size, search = ""
+ page, size, sortBy = "created_at", desc = false, filters = {}
}: UseEntityQueryProps) => {
return useQuery({
- queryKey: [AdminsQueryFetchKey, page, size, search],
+ queryKey: [AdminsQueryFetchKey, { page, size }, filters?.username ?? "", { sortBy, desc }, { filters }],
queryFn: fetchAdmins,
initialData: { entity: [], pageCount: 0 }
})
diff --git a/dashboard/src/components/data-table/actions-cell.tsx b/dashboard/src/features/entity-table/components/actions-cell.tsx
rename from dashboard/src/components/data-table/actions-cell.tsx
rename to dashboard/src/features/entity-table/components/actions-cell.tsx
diff --git a/dashboard/src/features/entity-table/components/column-header-combo-filter.tsx b/dashboard/src/features/entity-table/components/column-header-combo-filter.tsx
@@ -0,0 +1,124 @@
+import * as React from "react"
+import { type Column } from "@tanstack/react-table";
+import { useEntityTableContext } from "../contexts";
+import { useScreenBreakpoint } from "@marzneshin/hooks";
+import { useTranslation } from "react-i18next";
+import {
+ Button,
+ Drawer,
+ Command,
+ Popover,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ DrawerContent,
+ DrawerTrigger,
+ PopoverContent,
+ PopoverTrigger,
+} from "@marzneshin/components"
+import { FilterX } from "lucide-react";
+
+interface DataTableColumnHeaderFilterOptionProps<TData, TValue>
+ extends React.HTMLAttributes<HTMLDivElement> {
+ title: string
+ options: string[]
+ column: Column<TData, TValue>
+}
+
+export function DataTableColumnHeaderFilterOption<TData, TValue>(
+ { title, column, options }: DataTableColumnHeaderFilterOptionProps<TData, TValue>
+) {
+ const [open, setOpen] = React.useState(false)
+ const isDesktop = useScreenBreakpoint("md");
+ const [selectedOption, setSelectedOption] = React.useState<string | null>(
+ null
+ )
+ const { filters } = useEntityTableContext();
+
+ function handleClearingFilter() {
+ filters.setColumnsFilter({ ...filters.columnsFilter, [column.id]: undefined });
+ setSelectedOption(null);
+ }
+
+ if (isDesktop) {
+ return (
+ <Popover open={open} onOpenChange={setOpen}>
+ <PopoverTrigger asChild>
+ <Button variant="ghost" size="sm" className="data-[state=open]:bg-accent justify-start">
+ {selectedOption ? selectedOption : title}
+ {selectedOption &&
+ <Button
+ variant="ghost"
+ onMouseDown={handleClearingFilter}
+ size="sm"
+ className="hover:text-destructive/90 p-2 ml-1"
+ >
+ <FilterX className="size-4" />
+ </Button>
+ }
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-[200px] p-0" align="start">
+ <ComboFilterOptionList setOpen={setOpen} columnName={column.id} setSelectedOption={setSelectedOption} options={options} />
+ </PopoverContent>
+ </Popover>
+ )
+ }
+
+ return (
+ <Drawer open={open} onOpenChange={setOpen}>
+ <DrawerTrigger asChild>
+ <Button variant="ghost" size="sm" className="data-[state=open]:bg-accent w-fit justify-start">
+ {selectedOption ? selectedOption : title}
+ </Button>
+ </DrawerTrigger>
+ <DrawerContent>
+ <div className="mt-4 border-t">
+ <ComboFilterOptionList setOpen={setOpen} columnName={column.id} setSelectedOption={setSelectedOption} options={options} />
+ </div>
+ </DrawerContent>
+ </Drawer>
+ )
+}
+
+interface ComboFilterOptionListProps {
+ setOpen: (open: boolean) => void
+ setSelectedOption: (option: string | null) => void
+ columnName: string
+ options: string[]
+}
+
+function ComboFilterOptionList({
+ setOpen,
+ setSelectedOption,
+ options,
+ columnName,
+}: ComboFilterOptionListProps) {
+ const { filters } = useEntityTableContext();
+ const { t } = useTranslation();
+ return (
+ <Command>
+ <CommandInput className="border-0 focus:ring-0" placeholder="Filters..." />
+ <CommandList>
+ <CommandEmpty>{t("table.no-result-found")}</CommandEmpty>
+ <CommandGroup>
+ {options.map((option) => (
+ <CommandItem
+ key={option}
+ value={option}
+ onSelect={(value) => {
+ setSelectedOption(value)
+ setOpen(false)
+ filters.setColumnsFilter({ ...filters.columnsFilter, [columnName]: value })
+ }}
+ >
+ {option}
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ )
+}
diff --git a/dashboard/src/components/data-table/column-header.tsx b/dashboard/src/features/entity-table/components/column-header.tsx
rename from dashboard/src/components/data-table/column-header.tsx
rename to dashboard/src/features/entity-table/components/column-header.tsx
diff --git a/dashboard/src/components/data-table/table.tsx b/dashboard/src/features/entity-table/components/data-table.tsx
rename from dashboard/src/components/data-table/table.tsx
rename to dashboard/src/features/entity-table/components/data-table.tsx
@@ -25,7 +25,7 @@ import {
} from "@marzneshin/components"
import { useTranslation } from "react-i18next"
-import { DataTablePagination } from "./pagination"
+import { DataTablePagination } from "./table-pagination"
import { useState } from "react"
import { DataTableViewOptions } from "./table-view-option"
diff --git a/dashboard/src/features/entity-table/components/index.ts b/dashboard/src/features/entity-table/components/index.ts
@@ -1,6 +1,11 @@
-export * from "./table";
+export * from './table'
+export * from './data-table'
+export * from './table-pagination'
+export * from './column-header'
+export * from "./column-header-combo-filter";
+export * from './table-view-option'
+export * from './actions-cell'
export * from "./table-filtering";
-export * from "./table-pagination";
export * from "./sidebar-entity-card";
export * from "./sidebar-entity-popover";
export * from "./sidebar-entity-selection";
diff --git a/dashboard/src/features/entity-table/components/sidebar-entity-card.tsx b/dashboard/src/features/entity-table/components/sidebar-entity-card.tsx
@@ -20,7 +20,7 @@ export const SidebarEntityCard: FC<SidebarEntityCardSectionProps<any> & SidebarE
header: Header, content: Content, entity, checked = false
}) => {
return (
- <Card className="w-full h-30 m-[2px]">
+ <Card className="w-full h-30 m-[4px]">
<CardContent className={cn("p-2 flex-col flex gap-2 justify-start", checked && "dark:bg-primary-foreground bg-secondary")}>
<Header entity={entity} />
<Content entity={entity} />
diff --git a/dashboard/src/features/entity-table/components/sidebar-entity-popover.tsx b/dashboard/src/features/entity-table/components/sidebar-entity-popover.tsx
@@ -19,6 +19,7 @@ export const SidebarEntityTablePopover: FC<SidebarEntityTablePopoverProps> = ({
<Popover>
<PopoverTrigger asChild>
<Button
+ size="sm"
variant="outline"
className="mr-1 lg:flex"
>
diff --git a/dashboard/src/features/entity-table/components/sidebar-entity-selection.tsx b/dashboard/src/features/entity-table/components/sidebar-entity-selection.tsx
@@ -50,7 +50,7 @@ export const SidebarEntitySelection = () => {
onValueChange={(value) => setSidebarEntityId(value === "" ? undefined : value)}
defaultValue={String(sidebarEntityId)}
>
- <ScrollArea className={cn("flex flex-col justify-start p-1 gap-3 h-full", scrollBarHeight)}>
+ <ScrollArea className={cn("flex flex-col justify-start p-1 gap-3 h-full w-full", scrollBarHeight)}>
{sidebarEntities.map((entity: any) => (
<ToggleGroupItem
className="px-0 w-full h-full"
diff --git a/dashboard/src/features/entity-table/components/table-filtering.tsx b/dashboard/src/features/entity-table/components/table-filtering.tsx
@@ -6,16 +6,16 @@ import {
} from "@marzneshin/features/entity-table/contexts";
import { useTranslation } from "react-i18next";
-interface TableFilteringProps { }
+interface TableSearchProps { }
-export const TableFiltering: FC<TableFilteringProps> = () => {
- const { filtering } = useEntityTableContext()
+export const TableSearch: FC<TableSearchProps> = () => {
+ const { primaryFilter } = useEntityTableContext()
const { t } = useTranslation()
return (
<Input
- placeholder={t('table.filter-placeholder', { name: filtering.column })}
- value={filtering.columnFilters}
- onChange={(e) => filtering.setColumnFilters(e.target.value)}
+ placeholder={t('table.filter-placeholder', { name: primaryFilter.column })}
+ value={primaryFilter.columnFilters}
+ onChange={(e) => primaryFilter.setColumnFilters(e.target.value)}
className=" w-full md:w-[20rem]"
/>
);
diff --git a/dashboard/src/features/entity-table/components/table-pagination.tsx b/dashboard/src/features/entity-table/components/table-pagination.tsx
@@ -13,14 +13,11 @@ import {
SelectValue,
} from "@marzneshin/components";
import { useTranslation } from "react-i18next";
-import {
- useEntityTableContext
-} from "@marzneshin/features/entity-table/contexts";
import { useEffect } from "react";
+import { Table } from "@tanstack/react-table"
-export function DataTablePagination() {
+export function DataTablePagination<TData>({ table }: { table: Table<TData> }) {
const { t } = useTranslation();
- const { table } = useEntityTableContext();
useEffect(() => {
if (table.getState().pagination.pageIndex === 1) table.setPageIndex(2);
diff --git a/dashboard/src/components/data-table/table-view-option.tsx b/dashboard/src/features/entity-table/components/table-view-option.tsx
rename from dashboard/src/components/data-table/table-view-option.tsx
rename to dashboard/src/features/entity-table/components/table-view-option.tsx
diff --git a/dashboard/src/features/entity-table/components/table.tsx b/dashboard/src/features/entity-table/components/table.tsx
@@ -76,11 +76,21 @@ export function EntityDataTable<TData, TValue>({
<>
{Array.from({ length: 5 }).map((_, rowIndex) => (
<TableRow key={`skeleton-row-${rowIndex}`} className="w-full">
- {columns.map((column, cellIndex) => (
- <TableCell key={`${column.cell}-skeleton-cell-${rowIndex}-${cellIndex}`} className="h-12">
- <Skeleton className="w-full h-full" />
- </TableCell>
- ))}
+ <TableCell className="h-12">
+ <Skeleton className="w-full h-full" />
+ </TableCell>
+ <TableCell className="h-12">
+ <Skeleton className="w-full h-full" />
+ </TableCell>
+ <TableCell className="h-12">
+ <Skeleton className="w-full h-full" />
+ </TableCell>
+ <TableCell className="h-12">
+ <Skeleton className="w-full h-full" />
+ </TableCell>
+ <TableCell className="h-12">
+ <Skeleton className="w-full h-full" />
+ </TableCell>
</TableRow>
))}
</>
diff --git a/dashboard/src/features/entity-table/contexts/entity-table-provider.ts b/dashboard/src/features/entity-table/contexts/entity-table-provider.tsx
rename from dashboard/src/features/entity-table/contexts/entity-table-provider.ts
rename to dashboard/src/features/entity-table/contexts/entity-table-provider.tsx
@@ -1,11 +1,12 @@
import { createContext, useContext } from "react";
-import { UseFilteringReturn } from "../hooks";
+import { UseFiltersReturn, UsePrimaryFilterReturn } from "../hooks";
import { Table } from "@tanstack/react-table";
interface EntityTableContextProps<TData> {
table: Table<TData>
data: TData[]
- filtering: UseFilteringReturn
+ primaryFilter: UsePrimaryFilterReturn
+ filters: UseFiltersReturn
isLoading: boolean
}
diff --git a/dashboard/src/features/entity-table/entity-table.tsx b/dashboard/src/features/entity-table/entity-table.tsx
@@ -1,30 +1,30 @@
import { useMemo } from "react";
-import { Button, DataTableViewOptions } from "@marzneshin/components";
+import { Button } from "@marzneshin/components";
+import { DataTableViewOptions } from "./components";
import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query";
import { ColumnDef } from "@tanstack/react-table";
import { EntityTableContext } from "./contexts";
-import { TableFiltering, DataTablePagination, EntityDataTable } from "./components";
+import { TableSearch, DataTablePagination, EntityDataTable } from "./components";
import {
type UseRowSelectionReturn,
- useFiltering,
+ usePrimaryFiltering,
usePagination,
type FetchEntityReturn,
useEntityTable,
useVisibility,
useSorting,
- type SortableQueryKey,
type QueryKey,
type EntityQueryKeyType,
+ useFilters,
} from "./hooks";
export interface EntityTableProps<T> {
fetchEntity: ({ queryKey }: EntityQueryKeyType) => FetchEntityReturn<T>;
columns: ColumnDef<T>[];
- filteredColumn: string;
+ primaryFilter: string;
entityKey: string;
rowSelection?: UseRowSelectionReturn;
- manualSorting?: boolean;
onCreate: () => void;
onOpen: (entity: any) => void;
}
@@ -32,36 +32,35 @@ export interface EntityTableProps<T> {
export function EntityTable<T>({
fetchEntity,
columns,
- filteredColumn,
+ primaryFilter,
rowSelection,
entityKey,
- manualSorting = false,
onCreate,
onOpen,
}: EntityTableProps<T>) {
const { t } = useTranslation();
- const filtering = useFiltering({ column: filteredColumn });
+ const columnPrimaryFilter = usePrimaryFiltering({ column: primaryFilter });
+ const filters = useFilters();
const sorting = useSorting();
const visibility = useVisibility();
const { onPaginationChange, pageIndex, pageSize } = usePagination();
- const sortedQuery: SortableQueryKey = [
- entityKey,
- pageIndex,
- pageSize,
- filtering.columnFilters,
- sorting.sorting[0]?.id ? sorting.sorting[0].id : "created_at",
- sorting.sorting[0]?.desc,
- ];
const query: QueryKey = [
entityKey,
- pageIndex,
- pageSize,
- filtering.columnFilters,
+ {
+ page: pageIndex,
+ size: pageSize,
+ },
+ columnPrimaryFilter.columnFilters,
+ {
+ sortBy: sorting.sorting[0]?.id ? sorting.sorting[0].id : "created_at",
+ desc: sorting.sorting[0]?.desc
+ },
+ { filters: filters.columnsFilter }
];
const { data, isFetching } = useQuery({
queryFn: fetchEntity,
- queryKey: manualSorting ? sortedQuery : query,
+ queryKey: query,
initialData: { entity: [], pageCount: 1 },
});
@@ -77,8 +76,8 @@ export function EntityTable<T>({
});
const contextValue = useMemo(
- () => ({ table, data: data.entity, filtering, isLoading: isFetching }),
- [table, data.entity, filtering, isFetching],
+ () => ({ table, data: data.entity, primaryFilter: columnPrimaryFilter, filters, isLoading: isFetching }),
+ [table, data.entity, filters, columnPrimaryFilter, isFetching],
);
return (
@@ -93,11 +92,11 @@ export function EntityTable<T>({
</Button>
)}
</div>
- <TableFiltering />
+ <TableSearch />
</div>
<div className="w-full rounded-md border">
<EntityDataTable columns={columns} onRowClick={onOpen} />
- <DataTablePagination />
+ <DataTablePagination table={table} />
</div>
</div>
</EntityTableContext.Provider>
diff --git a/dashboard/src/features/entity-table/hooks/index.ts b/dashboard/src/features/entity-table/hooks/index.ts
@@ -1,3 +1,4 @@
+export * from './use-primary-filtering'
export * from './use-filtering'
export * from './use-pagination'
export * from './use-dialog'
diff --git a/dashboard/src/features/entity-table/hooks/use-entity-query.ts b/dashboard/src/features/entity-table/hooks/use-entity-query.ts
@@ -1,33 +1,40 @@
-import { z } from "zod";
-const UseEntityQueryPropsSchema = z.object({
- page: z.number().gte(1),
- size: z.number().gte(1).lte(100),
- search: z.string().optional()
-})
+interface PaginatedEntityQueryProps {
+ page: number;
+ size: number;
+}
+
+export type FilteredEntityType = {
+ [key: string]: string | undefined;
+}
-export interface SortableEntitySortQueryProps {
+export interface SortedEntityQueryProps {
sortBy: string
desc: boolean
}
-export type UseEntityQueryProps = z.infer<typeof UseEntityQueryPropsSchema & SortableEntitySortQueryProps>
+interface FiltersEntityQueryProps {
+ filters: FilteredEntityType;
+}
+
+export type UseEntityQueryProps = Partial<FiltersEntityQueryProps> & PaginatedEntityQueryProps & Partial<SortedEntityQueryProps>
+
+export type EntityName = string;
+export type EntityId = number | string | undefined;
+export type PrimaryFilter = string;
export type QueryKey =
- [string, number, number, string]
-export type SortableQueryKey =
- [string, number, number, string, string, boolean]
+ [EntityName, PaginatedEntityQueryProps, PrimaryFilter, SortedEntityQueryProps, FiltersEntityQueryProps]
+
export type SidebarQueryKey =
- [string, number | string | undefined, string, number, number, string]
-export type SortableSidebarQueryKey =
- [string, number | string | undefined, string, number, number, string, string, boolean]
+ [EntityName, EntityId, EntityName, PaginatedEntityQueryProps, PrimaryFilter, SortedEntityQueryProps, FiltersEntityQueryProps]
export interface QueryKeyType<QT> {
queryKey: QT
}
-export type EntityQueryKeyType = QueryKeyType<SortableQueryKey | QueryKey>
-export type EntitySidebarQueryKeyType = QueryKeyType<SortableSidebarQueryKey | SidebarQueryKey>
+export type EntityQueryKeyType = QueryKeyType<QueryKey>
+export type EntitySidebarQueryKeyType = QueryKeyType<SidebarQueryKey>
interface FetchEntityResult<T> {
pageCount: number
diff --git a/dashboard/src/features/entity-table/hooks/use-filtering.ts b/dashboard/src/features/entity-table/hooks/use-filtering.ts
@@ -1,16 +1,12 @@
import { OnChangeFn } from "@tanstack/react-table"
import { useState } from "react"
-export interface UseFilteringType {
- column: string
+export interface UseFiltersReturn {
+ columnsFilter: Record<string, string | undefined>
+ setColumnsFilter: OnChangeFn<Record<string, string | undefined>>
}
-export interface UseFilteringReturn extends UseFilteringType {
- columnFilters: string
- setColumnFilters: OnChangeFn<string>
-}
-
-export const useFiltering = ({ column }: UseFilteringType): UseFilteringReturn => {
- const [columnFilters, setColumnFilters] = useState<string>("")
- return { setColumnFilters, columnFilters, column }
+export const useFilters = (): UseFiltersReturn => {
+ const [columnsFilter, setColumnsFilter] = useState<Record<string, string | undefined>>({})
+ return { columnsFilter, setColumnsFilter }
}
diff --git a/dashboard/src/features/entity-table/hooks/use-primary-filtering.ts b/dashboard/src/features/entity-table/hooks/use-primary-filtering.ts
@@ -0,0 +1,16 @@
+import { OnChangeFn } from "@tanstack/react-table"
+import { useState } from "react"
+
+export interface UsePrimaryFilterType {
+ column: string
+}
+
+export interface UsePrimaryFilterReturn extends UsePrimaryFilterType {
+ columnFilters: string
+ setColumnFilters: OnChangeFn<string>
+}
+
+export const usePrimaryFiltering = ({ column }: UsePrimaryFilterType): UsePrimaryFilterReturn => {
+ const [columnFilters, setColumnFilters] = useState<string>("")
+ return { setColumnFilters, columnFilters, column }
+}
diff --git a/dashboard/src/features/entity-table/hooks/use-sidebar-entity-table.ts b/dashboard/src/features/entity-table/hooks/use-sidebar-entity-table.ts
@@ -2,12 +2,12 @@ import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query";
import {
- useFiltering,
+ usePrimaryFiltering,
usePagination,
useEntityTable,
useVisibility,
useSorting,
- type SortableSidebarQueryKey,
+ useFilters,
type EntitySidebarQueryKeyType,
type FetchEntityReturn,
type SidebarQueryKey,
@@ -42,7 +42,6 @@ export const useSidebarEntityTable = <T, S>({
filteredColumn,
rowSelection,
entityKey,
- manualSorting = false,
onCreate,
onEdit,
onOpen,
@@ -54,35 +53,32 @@ export const useSidebarEntityTable = <T, S>({
secondaryEntityKey,
}: UseSidebarEntityTableParams<T, S>) => {
const { t } = useTranslation();
- const filtering = useFiltering({ column: filteredColumn });
+ const primaryFilter = usePrimaryFiltering({ column: filteredColumn });
+ const filters = useFilters();
const sorting = useSorting();
const visibility = useVisibility();
const desktop = useScreenBreakpoint("md");
const { onPaginationChange, pageIndex, pageSize } = usePagination();
- const sortedQuery: SortableSidebarQueryKey = [
- entityKey,
- sidebarEntityId,
- secondaryEntityKey,
- pageIndex,
- pageSize,
- filtering.columnFilters,
- sorting.sorting[0]?.id ? sorting.sorting[0].id : "created_at",
- sorting.sorting[0]?.desc,
- ];
-
const query: SidebarQueryKey = [
entityKey,
sidebarEntityId,
secondaryEntityKey,
- pageIndex,
- pageSize,
- filtering.columnFilters,
+ {
+ page: pageIndex,
+ size: pageSize,
+ },
+ primaryFilter.columnFilters,
+ {
+ sortBy: sorting.sorting[0]?.id ? sorting.sorting[0].id : "created_at",
+ desc: sorting.sorting[0]?.desc
+ },
+ { filters: filters.columnsFilter }
];
const { data, isFetching } = useQuery({
queryFn: fetchEntity,
- queryKey: manualSorting ? sortedQuery : query,
+ queryKey: query,
initialData: { entity: [], pageCount: 1 },
});
@@ -99,13 +95,8 @@ export const useSidebarEntityTable = <T, S>({
});
const entityTableContextValue = useMemo(
- () => ({
- table,
- data: data.entity,
- filtering,
- isLoading: isFetching,
- }),
- [table, data.entity, filtering, isFetching],
+ () => ({ table, data: data.entity, primaryFilter, filters, isLoading: isFetching }),
+ [table, data.entity, filters, primaryFilter, isFetching],
);
const sidebarEntityTableContextValue = useMemo(
diff --git a/dashboard/src/features/entity-table/index.ts b/dashboard/src/features/entity-table/index.ts
@@ -1,4 +1,5 @@
export * from "./entity-table";
+export * from "./components";
export * from "./sidebar-entity-table";
export * from "./hooks";
export * from "./types";
diff --git a/dashboard/src/features/entity-table/sidebar-entity-table.tsx b/dashboard/src/features/entity-table/sidebar-entity-table.tsx
@@ -1,9 +1,11 @@
-import { Button, DataTableViewOptions, ResizableHandle, ResizablePanel, ResizablePanelGroup, HStack } from "@marzneshin/components";
+import { Button, ResizableHandle, ResizablePanel, ResizablePanelGroup, HStack } from "@marzneshin/components";
+import { Table } from "@tanstack/react-table"
+import { DataTableViewOptions } from "./components";
import { Server } from "lucide-react";
import { EntityTableContext, SidebarEntityTableContext } from "./contexts";
import {
SidebarEntityCardSectionsProps,
- TableFiltering,
+ TableSearch,
DataTablePagination,
EntityDataTable,
SidebarEntityTablePopover,
@@ -36,6 +38,13 @@ interface SidebarEntityTableProps<T, S> {
onDelete: (entity: T) => void;
}
+function MainTable<T, S>({ table, columns, props }: { table: Table<T>, columns: any, props: SidebarEntityTableProps<T, S> }) {
+ return <div className="flex flex-col justify-between size-full">
+ <EntityDataTable columns={columns} onRowClick={props.onOpen} />
+ <DataTablePagination table={table} />
+ </div>;
+}
+
export function SidebarEntityTable<T, S>(props: SidebarEntityTableProps<T, S>) {
const { t } = useTranslation();
const {
@@ -69,24 +78,26 @@ export function SidebarEntityTable<T, S>(props: SidebarEntityTableProps<T, S>) {
</Button>
)}
</div>
- <TableFiltering />
+ <TableSearch />
</div>
<div className="w-full rounded-md border">
- <ResizablePanelGroup direction="horizontal">
- <ResizablePanel minSize={20} maxSize={desktop ? 40 : 0}>
- <SidebarEntitySelection />
- </ResizablePanel>
- <ResizableHandle withHandle={desktop} />
- <ResizablePanel >
- <div className="flex flex-col justify-between size-full">
- <EntityDataTable columns={columns} onRowClick={props.onOpen} />
- <DataTablePagination />
- </div>
- </ResizablePanel>
- </ResizablePanelGroup>
+ {desktop ? (
+ <ResizablePanelGroup direction="horizontal">
+ <ResizablePanel minSize={20} maxSize={40}>
+ <SidebarEntitySelection />
+ </ResizablePanel>
+ <ResizableHandle withHandle={desktop} />
+ <ResizablePanel >
+ <MainTable table={table} columns={columns} props={props} />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ ) : (
+ <MainTable table={table} columns={columns} props={props} />
+ )}
</div>
</div>
</SidebarEntityTableContext.Provider>
</EntityTableContext.Provider>
);
}
+
diff --git a/dashboard/src/features/entity-table/stories/entity-table.stories.tsx b/dashboard/src/features/entity-table/stories/entity-table.stories.tsx
@@ -1,7 +1,7 @@
import {
EntityDataTable,
DataTablePagination,
- TableFiltering,
+ TableSearch,
} from "../components";
import { EntityTable } from "../entity-table";
import React from "react";
@@ -11,7 +11,7 @@ import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { columnsFnBasicEntityTable } from "./columns-fns";
import { fetchEntity, fetchEntityLoading } from "./fetch-entities";
-const TableFilteringComponent = TableFiltering as React.FC<unknown>;
+const TableSearchComponent = TableSearch as React.FC<unknown>;
const DataTablePaginationComponent = DataTablePagination as React.FC<unknown>;
const EntityDataTableComponent = EntityDataTable as React.FC<unknown>;
@@ -27,7 +27,7 @@ const meta: Meta<typeof EntityTable> = {
)],
tags: ["autodocs"],
subcomponents: {
- TableFilteringComponent,
+ TableSearchComponent,
DataTablePaginationComponent,
EntityDataTableComponent
}
@@ -46,7 +46,7 @@ export const BasicEntityTable: Story = {
args: {
fetchEntity: fetchEntity,
columns: columnsFnBasicEntityTable({ onEdit, onDelete, onOpen }),
- filteredColumn: "name",
+ primaryFilter: "name",
entityKey: "entities",
onCreate: () => alert("Create entity"),
onOpen: onOpen,
@@ -57,7 +57,7 @@ export const LoadingEntityTable: Story = {
args: {
fetchEntity: fetchEntityLoading,
columns: columnsFnBasicEntityTable({ onEdit, onDelete, onOpen }),
- filteredColumn: "name",
+ primaryFilter: "name",
entityKey: "entities",
onCreate: () => alert("Create entity"),
onOpen: onOpen,
diff --git a/dashboard/src/features/hosts/services/hosts.query.ts b/dashboard/src/features/hosts/services/hosts.query.ts
@@ -8,10 +8,16 @@ import {
} from "@marzneshin/features/entity-table/hooks";
export async function fetchHosts({ queryKey }: EntitySidebarQueryKeyType): FetchEntityReturn<HostType> {
+ const pagination = queryKey[3];
+ const primaryFilter = queryKey[4];
+ const filters = queryKey[6].filters;
return fetch(queryKey[1] !== undefined ? `/inbounds/${queryKey[1]}/hosts` : `/inbounds/hosts`, {
query: {
- page: queryKey[3],
- size: queryKey[4],
+ ...pagination,
+ ...filters,
+ remark: primaryFilter,
+ descending: queryKey[5].desc,
+ order_by: queryKey[5].sortBy,
}
}).then((result) => {
return {
@@ -24,10 +30,10 @@ export async function fetchHosts({ queryKey }: EntitySidebarQueryKeyType): Fetch
export const HostsQueryFetchKey = "hosts";
export const useHostsQuery = ({
- inboundId, page, size, search = ""
+ page, size, sortBy = "created_at", desc = false, filters = {}, inboundId
}: UseEntityQueryProps & { inboundId?: number }) => {
return useQuery({
- queryKey: ["inbounds", inboundId, HostsQueryFetchKey, page, size, search],
+ queryKey: ["inbounds", inboundId, HostsQueryFetchKey, { page, size }, filters?.username ?? "", { sortBy, desc }, { filters }],
queryFn: fetchHosts,
initialData: { entity: [], pageCount: 0 }
})
diff --git a/dashboard/src/features/hosts/tables/inbound-hosts/columns.tsx b/dashboard/src/features/hosts/tables/inbound-hosts/columns.tsx
@@ -1,12 +1,14 @@
import { ColumnDef } from "@tanstack/react-table"
import { HostType } from "@marzneshin/features/hosts"
-import { DataTableColumnHeader } from "@marzneshin/components/data-table/column-header"
+import {
+ DataTableActionsCell,
+ DataTableColumnHeader
+} from "@marzneshin/features/entity-table"
import i18n from "@marzneshin/features/i18n"
import {
type ColumnActions
} from "@marzneshin/features/entity-table";
import {
- DataTableActionsCell,
NoPropogationButton,
} from "@marzneshin/components"
diff --git a/dashboard/src/features/nodes/dialogs/settings/node-backend-setting.tsx b/dashboard/src/features/nodes/dialogs/settings/node-backend-setting.tsx
@@ -31,7 +31,7 @@ export const NodeBackendSetting: FC<{ node: NodeType; backend: string }> = ({
)}
{backendStats === null ? (
<Badge variant="disabled" className="size-fit">
- {t("unkown")}
+ {t("unknown")}
</Badge>
) : backendStats.running ? (
<Badge variant="positive" className="size-fit">
diff --git a/dashboard/src/features/nodes/services/nodes.query.ts b/dashboard/src/features/nodes/services/nodes.query.ts
@@ -9,9 +9,18 @@ import type {
export async function fetchNodes({
queryKey,
}: EntityQueryKeyType): Promise<{ entity: NodeType[]; pageCount: number }> {
- return fetch(
- `/nodes?page=${queryKey[1]}&size=${queryKey[2]}&name=${queryKey[3]}`,
- ).then((result) => {
+ const pagination = queryKey[1];
+ const primaryFilter = queryKey[2];
+ const filters = queryKey[4].filters;
+ return fetch(`/nodes`, {
+ query: {
+ ...pagination,
+ ...filters,
+ name: primaryFilter,
+ descending: queryKey[3].desc,
+ order_by: queryKey[3].sortBy,
+ }
+ }).then((result) => {
return {
entity: result.items,
pageCount: result.pages,
@@ -21,9 +30,11 @@ export async function fetchNodes({
export const NodesQueryFetchKey = "nodes";
-export const useNodesQuery = ({ page, size, search }: UseEntityQueryProps) => {
+export const useNodesQuery = ({
+ page, size, sortBy = "created_at", desc = false, filters = {}
+}: UseEntityQueryProps) => {
return useQuery({
- queryKey: [NodesQueryFetchKey, page, size, search ? search : ""],
+ queryKey: [NodesQueryFetchKey, { page, size }, filters?.username ?? "", { sortBy, desc }, { filters }],
queryFn: fetchNodes,
initialData: { entity: [], pageCount: 0 },
});
diff --git a/dashboard/src/features/nodes/tables/columns.tsx b/dashboard/src/features/nodes/tables/columns.tsx
@@ -1,12 +1,14 @@
import { ColumnDef } from "@tanstack/react-table"
import { NodesStatusBadge, NodeType, NodesStatus } from "@marzneshin/features/nodes"
-import { DataTableColumnHeader } from "@marzneshin/components/data-table/column-header"
+import {
+ DataTableActionsCell,
+ DataTableColumnHeader
+} from "@marzneshin/features/entity-table"
import i18n from "@marzneshin/features/i18n"
import {
type ColumnActions
} from "@marzneshin/features/entity-table";
import {
- DataTableActionsCell,
NoPropogationButton,
} from "@marzneshin/components"
diff --git a/dashboard/src/features/nodes/tables/table.tsx b/dashboard/src/features/nodes/tables/table.tsx
@@ -37,7 +37,7 @@ export const NodesTable: FC = () => {
<EntityTable
fetchEntity={fetchNodes}
columns={columns}
- filteredColumn="name"
+ primaryFilter="name"
entityKey="nodes"
onCreate={() => navigate({ to: "/nodes/create" })}
onOpen={onOpen}
diff --git a/dashboard/src/features/services/components/tables/inbounds/columns.tsx b/dashboard/src/features/services/components/tables/inbounds/columns.tsx
@@ -1,7 +1,9 @@
import { ColumnDef } from "@tanstack/react-table"
import { InboundType } from "@marzneshin/features/inbounds"
-import { DataTableColumnHeader } from "@marzneshin/components/data-table/column-header"
+import {
+ DataTableColumnHeader
+} from "@marzneshin/features/entity-table"
import i18n from "@marzneshin/features/i18n"
import { Badge, Checkbox } from "@marzneshin/components"
diff --git a/dashboard/src/features/services/components/tables/inbounds/index.tsx b/dashboard/src/features/services/components/tables/inbounds/index.tsx
@@ -1,6 +1,7 @@
import { type FC, useCallback, useEffect, useState } from "react";
import { useInboundsQuery } from "@marzneshin/features/inbounds";
-import { Button, DataTable } from "@marzneshin/components";
+import { Button } from "@marzneshin/components";
+import { DataTable } from "@marzneshin/features/entity-table";
import { columns } from "./columns";
import { type ServiceType, useServicesUpdateMutation } from "@marzneshin/features/services";
import { useTranslation } from "react-i18next";
diff --git a/dashboard/src/features/services/components/tables/services/columns.tsx b/dashboard/src/features/services/components/tables/services/columns.tsx
@@ -1,9 +1,11 @@
import { ColumnDef } from "@tanstack/react-table";
import { ServiceType } from "@marzneshin/features/services";
-import { DataTableColumnHeader } from "@marzneshin/components/data-table/column-header";
-import i18n from "@marzneshin/features/i18n";
import {
DataTableActionsCell,
+ DataTableColumnHeader
+} from "@marzneshin/features/entity-table"
+import i18n from "@marzneshin/features/i18n";
+import {
NoPropogationButton,
} from "@marzneshin/components";
import { ColumnActions } from "@marzneshin/features/entity-table";
diff --git a/dashboard/src/features/services/components/tables/services/index.tsx b/dashboard/src/features/services/components/tables/services/index.tsx
@@ -1,46 +1,46 @@
import type { FC } from "react";
import {
- ServicesQueryFetchKey,
- fetchServices,
- ServiceType
+ ServicesQueryFetchKey,
+ fetchServices,
+ ServiceType
} from "@marzneshin/features/services";
import { columns as columnsFn } from "./columns";
import { EntityTable } from "@marzneshin/features/entity-table";
import { useNavigate } from "@tanstack/react-router";
export const ServicesTable: FC = () => {
- const navigate = useNavigate({ from: "/services" });
- const onEdit = (entity: ServiceType) => {
- navigate({
- to: "/services/$serviceId/edit",
- params: { serviceId: String(entity.id) },
- })
- }
+ const navigate = useNavigate({ from: "/services" });
+ const onEdit = (entity: ServiceType) => {
+ navigate({
+ to: "/services/$serviceId/edit",
+ params: { serviceId: String(entity.id) },
+ })
+ }
- const onDelete = (entity: ServiceType) => {
- navigate({
- to: "/services/$serviceId/delete",
- params: { serviceId: String(entity.id) },
- })
- }
+ const onDelete = (entity: ServiceType) => {
+ navigate({
+ to: "/services/$serviceId/delete",
+ params: { serviceId: String(entity.id) },
+ })
+ }
- const onOpen = (entity: ServiceType) => {
- navigate({
- to: "/services/$serviceId",
- params: { serviceId: String(entity.id) },
- })
- }
+ const onOpen = (entity: ServiceType) => {
+ navigate({
+ to: "/services/$serviceId",
+ params: { serviceId: String(entity.id) },
+ })
+ }
- const columns = columnsFn({ onEdit, onDelete, onOpen });
+ const columns = columnsFn({ onEdit, onDelete, onOpen });
- return (
- <EntityTable
- fetchEntity={fetchServices}
- columns={columns}
- filteredColumn="name"
- entityKey={ServicesQueryFetchKey}
- onCreate={() => navigate({ to: "/services/create" })}
- onOpen={onOpen}
- />
- );
+ return (
+ <EntityTable
+ fetchEntity={fetchServices}
+ columns={columns}
+ primaryFilter="name"
+ entityKey={ServicesQueryFetchKey}
+ onCreate={() => navigate({ to: "/services/create" })}
+ onOpen={onOpen}
+ />
+ );
};
diff --git a/dashboard/src/features/services/components/tables/users/columns.tsx b/dashboard/src/features/services/components/tables/users/columns.tsx
@@ -1,5 +1,5 @@
import { ColumnDef } from "@tanstack/react-table"
-import { DataTableColumnHeader } from "@marzneshin/components/data-table/column-header"
+import { DataTableColumnHeader } from "@marzneshin/features/entity-table"
import i18n from "@marzneshin/features/i18n"
import {
UserType,
diff --git a/dashboard/src/features/services/components/tables/users/index.tsx b/dashboard/src/features/services/components/tables/users/index.tsx
@@ -1,6 +1,6 @@
import { FC } from "react";
-import { DataTable } from "@marzneshin/components";
+import { DataTable } from "@marzneshin/features/entity-table";
import { columns } from "./columns";
import { useUsersServiceQuery, type ServiceType } from "@marzneshin/features/services";
diff --git a/dashboard/src/features/services/services/services.query.ts b/dashboard/src/features/services/services/services.query.ts
@@ -8,10 +8,16 @@ import {
import { useQuery } from "@tanstack/react-query";
export async function fetchServices({ queryKey }: EntityQueryKeyType): FetchEntityReturn<ServiceType> {
+ const pagination = queryKey[1];
+ const primaryFilter = queryKey[2];
+ const filters = queryKey[4].filters;
return fetch(`/services`, {
query: {
- page: queryKey[1],
- size: queryKey[2]
+ ...pagination,
+ ...filters,
+ name: primaryFilter,
+ descending: queryKey[3].desc,
+ order_by: queryKey[3].sortBy,
}
}).then((result) => {
const services: ServiceType[] = result.items;
@@ -24,9 +30,11 @@ export async function fetchServices({ queryKey }: EntityQueryKeyType): FetchEnti
export const ServicesQueryFetchKey = "services";
-export const useServicesQuery = ({ page, size, search = "" }: UseEntityQueryProps) => {
+export const useServicesQuery = ({
+ page, size, sortBy = "created_at", desc = false, filters = {}
+}: UseEntityQueryProps) => {
return useQuery({
- queryKey: [ServicesQueryFetchKey, page, size, search],
+ queryKey: [ServicesQueryFetchKey, { page, size }, filters?.username ?? "", { sortBy, desc }, { filters }],
queryFn: fetchServices,
initialData: { entity: [], pageCount: 0 }
})
diff --git a/dashboard/src/features/users/components/tables/services/columns.tsx b/dashboard/src/features/users/components/tables/services/columns.tsx
@@ -1,5 +1,7 @@
import { ColumnDef } from "@tanstack/react-table"
-import { DataTableColumnHeader } from "@marzneshin/components/data-table/column-header"
+import {
+ DataTableColumnHeader
+} from "@marzneshin/features/entity-table"
import i18n from "@marzneshin/features/i18n"
import { Checkbox } from "@marzneshin/components"
import { ServiceType } from "@marzneshin/features/services"
diff --git a/dashboard/src/features/users/components/tables/services/index.tsx b/dashboard/src/features/users/components/tables/services/index.tsx
@@ -1,6 +1,6 @@
import { type FC, useCallback, useEffect, useState } from "react";
-import { } from "@marzneshin/features/inbounds";
-import { Button, DataTable } from "@marzneshin/components";
+import { Button } from "@marzneshin/components";
+import { DataTable } from "@marzneshin/features/entity-table";
import { columns } from "./columns";
import {
type UserType,
diff --git a/dashboard/src/features/users/components/tables/users/columns.tsx b/dashboard/src/features/users/components/tables/users/columns.tsx
@@ -6,17 +6,22 @@ import {
UserExpireStrategyPill,
UserExpirationValue
} from "@marzneshin/features/users";
-import { DataTableColumnHeader } from "@marzneshin/components/data-table/column-header";
+import { useAdminsQuery } from "@marzneshin/features/admins";
import i18n from "@marzneshin/features/i18n";
import {
CopyToClipboardButton,
- DataTableActionsCell,
buttonVariants,
NoPropogationButton,
} from "@marzneshin/components";
import { LinkIcon } from "lucide-react";
import { getSubscriptionLink } from "@marzneshin/utils";
-import type { ColumnActions, ColumnDefWithSudoRole } from "@marzneshin/features/entity-table";
+import {
+ DataTableColumnHeader,
+ DataTableColumnHeaderFilterOption,
+ DataTableActionsCell,
+ type ColumnActions, type ColumnDefWithSudoRole
+} from "@marzneshin/features/entity-table";
+import { type Column } from "@tanstack/react-table";
export const columns = (actions: ColumnActions<UserType>): ColumnDefWithSudoRole<UserType>[] => [
{
@@ -45,12 +50,7 @@ export const columns = (actions: ColumnActions<UserType>): ColumnDefWithSudoRole
accessorKey: "owner_username",
enableSorting: false,
sudoVisibleOnly: true,
- header: ({ column }) => (
- <DataTableColumnHeader
- title={i18n.t("owner")}
- column={column}
- />
- ),
+ header: ({ column }) => <AdminsColumnsHeaderOptionFilter column={column} />,
},
{
accessorKey: "used_traffic",
@@ -104,3 +104,15 @@ export const columns = (actions: ColumnActions<UserType>): ColumnDefWithSudoRole
)
}
];
+
+function AdminsColumnsHeaderOptionFilter<TData, TValue>({ column }: { column: Column<TData, TValue> }) {
+ const { data } = useAdminsQuery({ page: 1, size: 100, filters: {} });
+ return (
+ <DataTableColumnHeaderFilterOption
+ title={i18n.t("owner")}
+ column={column}
+ options={data.entity.map((admin) => admin.username)}
+ />
+ );
+}
+
diff --git a/dashboard/src/features/users/components/tables/users/index.tsx b/dashboard/src/features/users/components/tables/users/index.tsx
@@ -36,9 +36,8 @@ export const UsersTable: FC = () => {
return (
<EntityTable
fetchEntity={fetchUsers}
- manualSorting={true}
columns={isSudo() ? columns : noneSudoColumns}
- filteredColumn="username"
+ primaryFilter="username"
entityKey={UsersQueryFetchKey}
onCreate={() => navigate({ to: "/users/create" })}
onOpen={onOpen}
diff --git a/dashboard/src/features/users/services/users.query.ts b/dashboard/src/features/users/services/users.query.ts
@@ -4,20 +4,22 @@ import { UserType } from "../types";
import {
FetchEntityReturn,
UseEntityQueryProps,
- SortableEntitySortQueryProps,
EntityQueryKeyType
} from "@marzneshin/features/entity-table";
export type SortUserBy = "username" | "used_traffic" | "data_limit" | "expire_date" | "created_at"
export async function fetchUsers({ queryKey }: EntityQueryKeyType): FetchEntityReturn<UserType> {
+ const pagination = queryKey[1];
+ const primaryFilter = queryKey[2];
+ const filters = queryKey[4].filters;
return fetch(`/users`, {
query: {
- page: queryKey[1],
- size: queryKey[2],
- username: queryKey[3],
- descending: queryKey[5],
- order_by: queryKey[4]
+ ...pagination,
+ ...filters,
+ username: primaryFilter,
+ descending: queryKey[3].desc,
+ order_by: queryKey[3].sortBy,
}
}).then((result) => {
return {
@@ -30,10 +32,10 @@ export async function fetchUsers({ queryKey }: EntityQueryKeyType): FetchEntityR
export const UsersQueryFetchKey = "users";
export const useUsersQuery = ({
- page, size, search = "", sortBy = "created_at", desc = false
-}: UseEntityQueryProps & SortableEntitySortQueryProps) => {
+ page, size, sortBy = "created_at", desc = false, filters = {}
+}: UseEntityQueryProps) => {
return useQuery({
- queryKey: [UsersQueryFetchKey, page, size, search, sortBy, desc],
+ queryKey: [UsersQueryFetchKey, { page, size }, filters?.username ?? "", { sortBy, desc }, { filters }],
queryFn: fetchUsers,
initialData: { entity: [], pageCount: 0 }
})