diff --git a/dashboard/src/components/entity-table/index.tsx b/dashboard/src/components/entity-table/index.tsx index 2b92685..41c7bed 100644 --- a/dashboard/src/components/entity-table/index.tsx +++ b/dashboard/src/components/entity-table/index.tsx @@ -1,25 +1,25 @@ -import { FC, useEffect, useState } from "react"; +import { type FC, useMemo, useState } from "react"; import { Button, DataTableViewOptions } from "@marzneshin/components"; import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; import { EntityTableContext } from "./entity-table-provider"; -import { EntityDataTable } from "./table" -import { DataTablePagination } from "./table-pagination" +import { EntityDataTable } from "./table"; +import { DataTablePagination } from "./table-pagination"; import { TableFiltering } from "./table-filtering"; import { - UseRowSelectionReturn, - UseDialogProps, + type UseRowSelectionReturn, + type UseDialogProps, useFiltering, usePagination, - FetchEntityReturn, + type FetchEntityReturn, useEntityTable, useVisibility, useSorting, - SortableQueryKey, - QueryKey, - EntityQueryKeyType, + type SortableQueryKey, + type QueryKey, + type EntityQueryKeyType, } from "./hooks"; -import { useDialog } from "@marzneshin/hooks" +import { useDialog } from "@marzneshin/hooks"; interface EntityTableProps { fetchEntity: ({ queryKey }: EntityQueryKeyType) => FetchEntityReturn; @@ -28,9 +28,9 @@ interface EntityTableProps { SettingsDialog: FC>; columnsFn: any; filteredColumn: string; - entityKey: string - rowSelection?: UseRowSelectionReturn - manualSorting?: boolean + entityKey: string; + rowSelection?: UseRowSelectionReturn; + manualSorting?: boolean; } export function EntityTable({ @@ -42,7 +42,7 @@ export function EntityTable({ filteredColumn, rowSelection, entityKey, - manualSorting = false + manualSorting = false, }: EntityTableProps) { const [mutationDialogOpen, setMutationDialogOpen] = useDialog(); const [deleteDialogOpen, setDeleteDialogOpen] = useDialog(); @@ -69,10 +69,10 @@ export function EntityTable({ setSettingsDialogOpen(true); }; - const { t } = useTranslation() - const filtering = useFiltering({ column: filteredColumn }) - const sorting = useSorting() - const visibility = useVisibility() + const { t } = useTranslation(); + const filtering = useFiltering({ column: filteredColumn }); + const sorting = useSorting(); + const visibility = useVisibility(); const { onPaginationChange, pageIndex, pageSize } = usePagination(); const sortedQuery: SortableQueryKey = [ entityKey, @@ -80,16 +80,22 @@ export function EntityTable({ 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] + sorting.sorting[0]?.desc, + ]; + const query: QueryKey = [ + entityKey, + pageIndex, + pageSize, + filtering.columnFilters, + ]; const { data, isLoading } = useQuery({ queryFn: fetchEntity, queryKey: manualSorting ? sortedQuery : query, - initialData: { entity: [], pageCount: 1 } + initialData: { entity: [], pageCount: 1 }, }); - const columns = columnsFn({ onEdit, onDelete, onOpen }) + const columns = columnsFn({ onEdit, onDelete, onOpen }); const table = useEntityTable({ data, columns, @@ -98,14 +104,17 @@ export function EntityTable({ rowSelection, visibility, sorting, - onPaginationChange - }) + onPaginationChange, + }); - useEffect(() => table.setPageIndex(1), [filtering.columnFilters, table]) + const contextValue = useMemo(() => ( + { table, data: data.entity, filtering, isLoading } + ), [table, data.entity, filtering, isLoading]) - // TODO: Move the selectedEntity to context return ( - + ({ {onCreate && ( - )}
- +
@@ -145,4 +149,4 @@ export function EntityTable({ ); } -export * from './hooks' +export * from "./hooks"; diff --git a/dashboard/src/features/services/dialogs/index.ts b/dashboard/src/features/services/dialogs/index.ts index 4af80da..9e47895 100644 --- a/dashboard/src/features/services/dialogs/index.ts +++ b/dashboard/src/features/services/dialogs/index.ts @@ -1,2 +1,2 @@ -export * from './delete-confirmation.dialog' -export * from './mutation.dialog' +export * from "./delete-confirmation.dialog"; +export * from "./mutation"; diff --git a/dashboard/src/features/services/dialogs/mutation.dialog.tsx b/dashboard/src/features/services/dialogs/mutation.dialog.tsx deleted file mode 100644 index 3b376e0..0000000 --- a/dashboard/src/features/services/dialogs/mutation.dialog.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { - DialogTitle, - DialogContent, - Dialog, - DialogHeader, - Form, - FormItem, - FormControl, - FormMessage, - FormLabel, - Input, - FormField, - Button, - Checkbox, - Badge, -} from "@marzneshin/components"; -import { FC } from "react"; -import { - ServiceType, - useServicesCreationMutation, - useServicesUpdateMutation, -} from "@marzneshin/features/services"; -import { useInboundsQuery } from "@marzneshin/features/inbounds"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { cn } from "@marzneshin/utils"; -import { Box, GlobeLock } from "lucide-react"; -import { useMutationDialog } from "@marzneshin/hooks"; - -export const ServiceSchema = z.object({ - id: z.number().optional(), - inbounds: z.array(z.number()).refine((value) => value.some((item) => item), { - message: "You have to select at least one inbound.", - }), - name: z.string().trim().min(1), -}); -type ServiceCreateType = z.infer; - -interface MutationDialogProps { - entity: ServiceType | null; - open: boolean; - onOpenChange: (state: boolean) => void; -} - -const getDefaultValues = (): ServiceCreateType => ({ - name: "", - inbounds: [], -}); - -export const MutationDialog: FC = ({ - entity, - open, - onOpenChange, -}) => { - const isEditing = entity !== null; - const updateMutation = useServicesUpdateMutation(); - const createMutation = useServicesCreationMutation(); - const { data: inbounds } = useInboundsQuery(); - const { form, handleSubmit } = useMutationDialog({ - entity, - updateMutation, - createMutation, - getDefaultValue: getDefaultValues, - schema: ServiceSchema, - onOpenChange, - }); - const { t } = useTranslation(); - return ( - - - - - {isEditing - ? t("page.services.dialogs.edition.title") - : t("page.services.dialogs.creation.title")} - - -
- - ( - - {t("name")} - - - - - - )} - /> - ( - - {t("inbounds")} - {inbounds.map((inbound) => ( - ( - - -
- { - return checked - ? field.onChange([ - ...field.value, - inbound.id, - ]) - : field.onChange( - field.value?.filter( - (value: number) => - value !== inbound.id, - ), - ); - }} - /> - - {inbound.tag} -
- - {" "} - {" "} - {inbound.protocol}{" "} - - - {" "} - {" "} - {inbound.node.name}{" "} - -
-
-
-
-
- )} - /> - ))} - -
- )} - /> - - - -
-
- ); -}; diff --git a/dashboard/src/features/services/dialogs/mutation/fields/inbounds.tsx b/dashboard/src/features/services/dialogs/mutation/fields/inbounds.tsx new file mode 100644 index 0000000..510b489 --- /dev/null +++ b/dashboard/src/features/services/dialogs/mutation/fields/inbounds.tsx @@ -0,0 +1,97 @@ +import { + FormItem, + FormControl, + FormMessage, + FormLabel, + FormField, + Checkbox, + Badge, +} from "@marzneshin/components"; +import { + useInboundsQuery, + type InboundType, +} from "@marzneshin/features/inbounds"; +import { + useFormContext, + type FieldValues, + type ControllerRenderProps, +} from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { cn } from "@marzneshin/utils"; +import { Box, GlobeLock } from "lucide-react"; + +export const InboundCard = ({ + inbound, + field, +}: { + inbound: InboundType; + field: ControllerRenderProps; +}) => { + return ( +
+ { + return checked + ? field.onChange([...field.value, inbound.id]) + : field.onChange( + field.value?.filter((value: number) => value !== inbound.id), + ); + }} + /> + + {inbound.tag} +
+ + {" "} + {inbound.protocol}{" "} + + + {" "} + {inbound.node.name}{" "} + +
+
+
+ ); +}; + +export const InboundsField = () => { + const { data: inbounds } = useInboundsQuery(); + const form = useFormContext(); + const { t } = useTranslation(); + + return ( + ( + + {t("inbounds")} + {inbounds.map((inbound) => ( + ( + + + + + + )} + /> + ))} + + + )} + /> + ); +}; diff --git a/dashboard/src/features/services/dialogs/mutation/fields/index.ts b/dashboard/src/features/services/dialogs/mutation/fields/index.ts new file mode 100644 index 0000000..01eb333 --- /dev/null +++ b/dashboard/src/features/services/dialogs/mutation/fields/index.ts @@ -0,0 +1,2 @@ +export * from "./name"; +export * from "./inbounds"; diff --git a/dashboard/src/features/services/dialogs/mutation/fields/name.tsx b/dashboard/src/features/services/dialogs/mutation/fields/name.tsx new file mode 100644 index 0000000..05a68b1 --- /dev/null +++ b/dashboard/src/features/services/dialogs/mutation/fields/name.tsx @@ -0,0 +1,30 @@ +import { + FormItem, + FormControl, + FormMessage, + FormLabel, + Input, + FormField, +} from "@marzneshin/components"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; + +export const NameField = () => { + const form = useFormContext(); + const { t } = useTranslation(); + return ( + ( + + {t("name")} + + + + + + )} + /> + ); +}; diff --git a/dashboard/src/features/services/dialogs/mutation/index.tsx b/dashboard/src/features/services/dialogs/mutation/index.tsx new file mode 100644 index 0000000..cfdadd3 --- /dev/null +++ b/dashboard/src/features/services/dialogs/mutation/index.tsx @@ -0,0 +1,83 @@ +import { + DialogTitle, + DialogContent, + Dialog, + DialogHeader, + Form, + Button, +} from "@marzneshin/components"; +import type { FC } from "react"; +import { + type ServiceType, + useServicesCreationMutation, + useServicesUpdateMutation, +} from "@marzneshin/features/services"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { useMutationDialog } from "@marzneshin/hooks"; +import { NameField, InboundsField } from "./fields"; + +export const ServiceSchema = z.object({ + id: z.number().optional(), + inbounds: z.array(z.number()).refine((value) => value.some((item) => item), { + message: "You have to select at least one inbound.", + }), + name: z.string().trim().min(1), +}); +type ServiceCreateType = z.infer; + +interface MutationDialogProps { + entity: ServiceType | null; + open: boolean; + onOpenChange: (state: boolean) => void; +} + +const getDefaultValues = (): ServiceCreateType => ({ + name: "", + inbounds: [], +}); + +export const MutationDialog: FC = ({ + entity, + open, + onOpenChange, +}) => { + const isEditing = entity !== null; + const updateMutation = useServicesUpdateMutation(); + const createMutation = useServicesCreationMutation(); + const { form, handleSubmit } = useMutationDialog({ + entity, + updateMutation, + createMutation, + getDefaultValue: getDefaultValues, + schema: ServiceSchema, + onOpenChange, + }); + const { t } = useTranslation(); + return ( + + + + + {isEditing + ? t("page.services.dialogs.edition.title") + : t("page.services.dialogs.creation.title")} + + +
+ + + + + + +
+
+ ); +}; diff --git a/dashboard/src/features/services/tables/inbounds/index.tsx b/dashboard/src/features/services/tables/inbounds/index.tsx index cff8d45..0605c8e 100644 --- a/dashboard/src/features/services/tables/inbounds/index.tsx +++ b/dashboard/src/features/services/tables/inbounds/index.tsx @@ -1,58 +1,61 @@ - -import { FC, useCallback, useEffect, useState } from "react"; -import { - useInboundsQuery, -} from '@marzneshin/features/inbounds'; +import { type FC, useCallback, useEffect, useState } from "react"; +import { useInboundsQuery } from "@marzneshin/features/inbounds"; import { Button, DataTable } from "@marzneshin/components"; import { columns } from "./columns"; -import { ServiceType, useServicesUpdateMutation } from "../.."; +import { type ServiceType, useServicesUpdateMutation } from "../.."; import { useTranslation } from "react-i18next"; -import { RowSelectionState } from "@tanstack/react-table"; +import type { RowSelectionState } from "@tanstack/react-table"; interface ServiceInboundsTableProps { - service: ServiceType + service: ServiceType; } -export const ServiceInboundsTable: FC = ({ service }) => { +export const ServiceInboundsTable: FC = ({ + service, +}) => { const { mutate: updateService } = useServicesUpdateMutation(); const { data } = useInboundsQuery(); - const [selectedInbound, setSelectedInbound] = useState<{ [key: number]: boolean }>({}) - const { t } = useTranslation() + const [selectedInbound, setSelectedInbound] = useState<{ + [key: number]: boolean; + }>({}); + const { t } = useTranslation(); useEffect(() => { setSelectedInbound((prevSelected) => { const updatedSelected: RowSelectionState = { ...prevSelected }; - service.inbounds.forEach((inboundId) => { - data.forEach((fetchedInbound, i) => { + for (const inboundId of service.inbounds) { + for (const [i, fetchedInbound] of data.entries()) { if (fetchedInbound.id === inboundId) { updatedSelected[i] = true; } - }); - }); + } + } return updatedSelected; }); - }, [data, service.inbounds]) + }, [data, service.inbounds]); const handleApply = useCallback(() => { const selectedInboundIds = Object.keys(selectedInbound) - .filter(key => selectedInbound[parseInt(key)]) - .map(key => data[parseInt(key)].id); + .filter((key) => selectedInbound[Number.parseInt(key)]) + .map((key) => data[Number.parseInt(key)].id); updateService({ ...service, inbounds: selectedInboundIds }); }, [data, selectedInbound, service, updateService]); - const disabled = (Object.keys(selectedInbound).length < 1) ? true : false + const disabled = Object.keys(selectedInbound).length < 1; return (
- +
- ) -} + ); +}; diff --git a/dashboard/src/features/users/dialogs/mutation/fields/services.tsx b/dashboard/src/features/users/dialogs/mutation/fields/services.tsx index 9f444b9..0fcd47b 100644 --- a/dashboard/src/features/users/dialogs/mutation/fields/services.tsx +++ b/dashboard/src/features/users/dialogs/mutation/fields/services.tsx @@ -1,71 +1,90 @@ import { - Checkbox, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, - ScrollArea, + Checkbox, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + ScrollArea, } from "@marzneshin/components"; -import { useServicesQuery } from "@marzneshin/features/services"; +import { + useServicesQuery, + type ServiceType, +} from "@marzneshin/features/services"; import { ServiceCard } from "@marzneshin/features/users"; import { cn } from "@marzneshin/utils"; import type { FC } from "react"; -import { useFormContext } from "react-hook-form"; +import { + useFormContext, + type FieldValues, + type ControllerRenderProps, +} from "react-hook-form"; import { useTranslation } from "react-i18next"; +interface ServiceCheckboxCardProps { + service: ServiceType; + field: ControllerRenderProps; +} + +const ServiceCheckboxCard: FC = ({ + service, + field, +}) => { + return ( +
+ { + return checked + ? field.onChange([...field.value, service.id]) + : field.onChange( + field.value?.filter((value: number) => value !== service.id), + ); + }} + /> + + + +
+ ); +}; + export const ServicesField: FC = () => { - const form = useFormContext(); - const { data } = useServicesQuery({ page: 1, size: 100 }); - const { t } = useTranslation(); + const form = useFormContext(); + const { data } = useServicesQuery({ page: 1, size: 100 }); + const { t } = useTranslation(); - return ( - ( - - {t("services")} - - {data.entity.map((service) => ( - ( - - -
- { - return checked - ? field.onChange([...field.value, service.id]) - : field.onChange( - field.value?.filter( - (value: number) => value !== service.id, - ), - ); - }} - /> - - - -
-
- -
- )} - /> - ))} -
-
- )} - /> - ); + return ( + ( + + {t("services")} + + {data.entity.map((service) => ( + ( + + + + + + + )} + /> + ))} + + + )} + /> + ); }; diff --git a/dashboard/src/features/users/dialogs/mutation/fields/username.tsx b/dashboard/src/features/users/dialogs/mutation/fields/username.tsx index 3c8912e..651d475 100644 --- a/dashboard/src/features/users/dialogs/mutation/fields/username.tsx +++ b/dashboard/src/features/users/dialogs/mutation/fields/username.tsx @@ -4,24 +4,25 @@ import { FormItem, Input, FormLabel, - FormMessage -} from '@marzneshin/components'; -import { FC, InputHTMLAttributes } from 'react' -import { useFormContext } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; + FormMessage, +} from "@marzneshin/components"; +import type { FC, InputHTMLAttributes } from "react"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; -interface UsernameFieldProps { } +export const UsernameField: FC> = ({ + disabled, +}) => { + const form = useFormContext(); + const { t } = useTranslation(); -export const UsernameField: FC> = ({ disabled }) => { - const form = useFormContext() - const { t } = useTranslation() return ( ( - {t('username')} + {t("username")} @@ -30,4 +31,4 @@ export const UsernameField: FC ); -} +}; diff --git a/dashboard/src/features/users/tables/services/index.tsx b/dashboard/src/features/users/tables/services/index.tsx index 1161ddb..edf614d 100644 --- a/dashboard/src/features/users/tables/services/index.tsx +++ b/dashboard/src/features/users/tables/services/index.tsx @@ -1,63 +1,64 @@ - - -import { FC, useCallback, useEffect, useState } from "react"; -import { -} from '@marzneshin/features/inbounds'; +import { type FC, useCallback, useEffect, useState } from "react"; +import { } from "@marzneshin/features/inbounds"; import { Button, DataTable } from "@marzneshin/components"; import { columns } from "./columns"; -import { UserType, useUsersUpdateMutation } from "@marzneshin/features/users"; -import { useTranslation } from "react-i18next"; -import { RowSelectionState } from "@tanstack/react-table"; import { - ServiceType, - useServicesQuery, -} from "@marzneshin/features/services"; + type UserType, + useUsersUpdateMutation, +} from "@marzneshin/features/users"; +import { useTranslation } from "react-i18next"; +import type { RowSelectionState } from "@tanstack/react-table"; +import { useServicesQuery } from "@marzneshin/features/services"; interface UserServicesTableProps { - user: UserType + user: UserType; } -// TODO: Implement pagination for row selectable with appliedable action table +// TODO: #322 Implement pagination for row selectable with appliedable action table export const UserServicesTable: FC = ({ user }) => { const { mutate: updateUser } = useUsersUpdateMutation(); const { data } = useServicesQuery({ page: 1, size: 100 }); - const [selectedService, setSelectedService] = useState<{ [key: number]: boolean }>({}) - const { t } = useTranslation() + const [selectedService, setSelectedService] = useState<{ + [key: number]: boolean; + }>({}); + const { t } = useTranslation(); useEffect(() => { setSelectedService((prevSelected) => { const updatedSelected: RowSelectionState = { ...prevSelected }; - user.services.forEach((serviceId) => { - data.entity.forEach((fetchedService: ServiceType, i) => { + for (const serviceId of user.services) { + for (const [i, fetchedService] of data.entity.entries()) { if (fetchedService.id === serviceId) { updatedSelected[i] = true; } - }); - }); + } + } return updatedSelected; }); - }, [data, user.services]) + }, [data, user.services]); const handleApply = useCallback(() => { const selectedServiceIds = Object.keys(selectedService) - .filter(key => selectedService[parseInt(key)]) - .map(key => data.entity[parseInt(key)].id); + .filter((key) => selectedService[Number.parseInt(key)]) + .map((key) => data.entity[Number.parseInt(key)].id); updateUser({ ...user, services: selectedServiceIds }); }, [data, selectedService, user, updateUser]); - const disabled = (Object.keys(selectedService).length < 1) ? true : false + const disabled = Object.keys(selectedService).length < 1; return (
- +
- ) -} + ); +};