diff --git a/dashboard/src/components/stacks/vertical.tsx b/dashboard/src/components/stacks/vertical.tsx
@@ -1,5 +1,14 @@
-import { FC, PropsWithChildren } from "react";
+import type { FC, PropsWithChildren, HTMLAttributes } from "react";
+import { cn } from "@marzneshin/utils";
-export const VStack: FC<PropsWithChildren> = ({ children }) => {
- return <div className="flex flex-col gap-2">{children}</div>;
+export const VStack: FC<PropsWithChildren & HTMLAttributes<HTMLDivElement>> = ({
+ children,
+ className,
+ ...props
+}) => {
+ return (
+ <div className={cn("flex flex-col gap-2", className)} {...props}>
+ {children}
+ </div>
+ );
};
diff --git a/dashboard/src/features/services/dialogs/mutation.dialog.tsx b/dashboard/src/features/services/dialogs/mutation.dialog.tsx
@@ -1,147 +1,170 @@
-
-
import {
- DialogTitle,
- DialogContent,
- Dialog,
- DialogHeader,
- Form,
- FormItem,
- FormControl,
- FormMessage,
- FormLabel,
- Input,
- FormField,
- Button,
- Checkbox,
- Badge
-} from '@marzneshin/components'
-import { FC } from 'react'
+ 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'
+ 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<typeof ServiceSchema>
+ 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<typeof ServiceSchema>;
interface MutationDialogProps {
- entity: ServiceType | null
- open: boolean
- onOpenChange: (state: boolean) => void
+ entity: ServiceType | null;
+ open: boolean;
+ onOpenChange: (state: boolean) => void;
}
const getDefaultValues = (): ServiceCreateType => ({
- name: '',
- inbounds: [],
-})
+ name: "",
+ inbounds: [],
+});
-export const MutationDialog: FC<MutationDialogProps> = ({ 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 (
- <Dialog open={open} onOpenChange={onOpenChange} defaultOpen={true}>
- <DialogContent>
- <DialogHeader>
- <DialogTitle className="text-primary">
- {isEditing ?
- t('page.services.dialogs.edition.title') :
- t('page.services.dialogs.creation.title')}
- </DialogTitle>
- </DialogHeader>
- <Form {...form} >
- <form onSubmit={handleSubmit} >
- <FormField
- control={form.control}
- name="name"
- render={({ field }) => (
- <FormItem>
- <FormLabel>{t('name')}</FormLabel>
- <FormControl>
- <Input {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="inbounds"
- render={() => (
- <FormItem>
- <FormLabel>{t('inbounds')}</FormLabel>
- {inbounds.map((inbound) => (
- <FormField
- key={inbound.id}
- control={form.control}
- name="inbounds"
- render={({ field }) => (
- <FormItem key={inbound.id} >
- <FormControl>
- <div
- className={cn("flex flex-row items-center p-3 space-y-0 space-x-3 rounded-md border", { "bg-secondary": field.value?.includes(inbound.id) })}>
- <Checkbox
- checked={field.value?.includes(inbound.id)}
- onCheckedChange={(checked) => {
- return checked
- ? field.onChange([...field.value, inbound.id])
- : field.onChange(
- field.value?.filter(
- (value: number) => value !== inbound.id
- )
- )
- }}
- />
- <FormLabel className="flex flex-row justify-between items-center w-full">
- {inbound.tag}
- <div className="flex gap-2">
- <Badge className="py-1 px-2"> <GlobeLock className="mr-1 w-4" /> {inbound.protocol} </Badge>
- <Badge className="py-1 px-2"> <Box className="mr-1 w-4" /> {inbound.node.name} </Badge>
- </div>
- </FormLabel>
- </div>
- </FormControl>
- </FormItem>
- )}
- />
- ))}
- <FormMessage />
- </FormItem>
- )}
- />
- <Button
- className="mt-3 w-full font-semibold"
- type="submit"
- disabled={form.formState.isSubmitting}>
- {t('submit')}
- </Button>
- </form>
- </Form>
- </DialogContent>
- </Dialog >
- )
-}
+export const MutationDialog: FC<MutationDialogProps> = ({
+ 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 (
+ <Dialog open={open} onOpenChange={onOpenChange} defaultOpen={true}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle className="text-primary">
+ {isEditing
+ ? t("page.services.dialogs.edition.title")
+ : t("page.services.dialogs.creation.title")}
+ </DialogTitle>
+ </DialogHeader>
+ <Form {...form}>
+ <form onSubmit={handleSubmit} className="h-full">
+ <FormField
+ control={form.control}
+ name="name"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>{t("name")}</FormLabel>
+ <FormControl>
+ <Input {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="inbounds"
+ render={() => (
+ <FormItem>
+ <FormLabel>{t("inbounds")}</FormLabel>
+ {inbounds.map((inbound) => (
+ <FormField
+ key={inbound.id}
+ control={form.control}
+ name="inbounds"
+ render={({ field }) => (
+ <FormItem key={inbound.id}>
+ <FormControl>
+ <div
+ className={cn(
+ "flex flex-row items-center p-3 space-y-0 space-x-3 rounded-md border",
+ {
+ "bg-secondary": field.value?.includes(
+ inbound.id,
+ ),
+ },
+ )}
+ >
+ <Checkbox
+ checked={field.value?.includes(inbound.id)}
+ onCheckedChange={(checked) => {
+ return checked
+ ? field.onChange([
+ ...field.value,
+ inbound.id,
+ ])
+ : field.onChange(
+ field.value?.filter(
+ (value: number) =>
+ value !== inbound.id,
+ ),
+ );
+ }}
+ />
+ <FormLabel className="flex flex-row justify-between items-center w-full">
+ {inbound.tag}
+ <div className="flex gap-2">
+ <Badge className="py-1 px-2">
+ {" "}
+ <GlobeLock className="mr-1 w-4" />{" "}
+ {inbound.protocol}{" "}
+ </Badge>
+ <Badge className="py-1 px-2">
+ {" "}
+ <Box className="mr-1 w-4" />{" "}
+ {inbound.node.name}{" "}
+ </Badge>
+ </div>
+ </FormLabel>
+ </div>
+ </FormControl>
+ </FormItem>
+ )}
+ />
+ ))}
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <Button
+ className="mt-3 w-full font-semibold"
+ type="submit"
+ disabled={form.formState.isSubmitting}
+ >
+ {t("submit")}
+ </Button>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ );
+};
diff --git a/dashboard/src/features/users/dialogs/mutation/fields/services.tsx b/dashboard/src/features/users/dialogs/mutation/fields/services.tsx
@@ -1,68 +1,71 @@
import {
- Checkbox,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
- ScrollArea,
-} from '@marzneshin/components';
-import { useServicesQuery } from '@marzneshin/features/services';
-import { ServiceCard } from '@marzneshin/features/users';
-import { cn } from '@marzneshin/utils';
-import { FC } from 'react'
-import { useFormContext } from 'react-hook-form';
-import { useTranslation } from 'react-i18next';
+ Checkbox,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ ScrollArea,
+} from "@marzneshin/components";
+import { useServicesQuery } 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 { useTranslation } from "react-i18next";
-interface ServicesFieldProps { }
+export const ServicesField: FC = () => {
+ const form = useFormContext();
+ const { data } = useServicesQuery({ page: 1, size: 100 });
+ const { t } = useTranslation();
-export const ServicesField: FC<ServicesFieldProps> = () => {
- const form = useFormContext();
- const { data } = useServicesQuery({ page: 1, size: 100 });
- const { t } = useTranslation();
-
- return (
- <FormField
- control={form.control}
- name="services"
- render={() => (
- <FormItem>
- <FormLabel>{t('services')}</FormLabel>
- <ScrollArea className="flex flex-col justify-start px-1 h-[22rem]">
- {data.entity.map(service => (
- <FormField
- control={form.control}
- name='services'
- render={({ field }) => (
- <FormItem key={service.id}>
- <FormControl>
- <div
- className={cn("flex mb-2 flex-row gap-2 items-center p-3 rounded-md border", { "bg-secondary": field.value?.includes(service.id) })}>
- <Checkbox
- checked={field.value?.includes(service.id)}
- onCheckedChange={(checked) => {
- return checked
- ? field.onChange([...field.value, service.id])
- : field.onChange(
- field.value?.filter(
- (value: number) => value !== service.id
- )
- )
- }}
- />
- <FormLabel className="flex flex-row justify-between items-center w-full">
- <ServiceCard service={service} />
- </FormLabel>
- </div>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- ))}
- </ScrollArea>
- </FormItem>
- )}
- />
- );
+ return (
+ <FormField
+ control={form.control}
+ name="services"
+ render={() => (
+ <FormItem>
+ <FormLabel>{t("services")}</FormLabel>
+ <ScrollArea className="flex flex-col justify-start px-1 h-full max-h-[20rem]">
+ {data.entity.map((service) => (
+ <FormField
+ key={service.id}
+ control={form.control}
+ name="services"
+ render={({ field }) => (
+ <FormItem key={service.id}>
+ <FormControl>
+ <div
+ className={cn(
+ "flex mb-2 flex-row gap-2 items-center p-3 rounded-md border",
+ { "bg-secondary": field.value?.includes(service.id) },
+ )}
+ >
+ <Checkbox
+ checked={field.value?.includes(service.id)}
+ onCheckedChange={(checked) => {
+ return checked
+ ? field.onChange([...field.value, service.id])
+ : field.onChange(
+ field.value?.filter(
+ (value: number) => value !== service.id,
+ ),
+ );
+ }}
+ />
+ <FormLabel className="flex flex-row justify-between items-center w-full">
+ <ServiceCard service={service} />
+ </FormLabel>
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ ))}
+ </ScrollArea>
+ </FormItem>
+ )}
+ />
+ );
};
diff --git a/dashboard/src/features/users/dialogs/mutation/index.tsx b/dashboard/src/features/users/dialogs/mutation/index.tsx
@@ -1,4 +1,5 @@
-import { FC, useCallback } from "react";
+import type { FC } from "react";
+import { useCallback } from "react";
import {
DialogTitle,
DialogContent,
@@ -6,20 +7,19 @@ import {
DialogHeader,
Form,
Button,
-} from '@marzneshin/components';
-import { useTranslation } from 'react-i18next';
+ ScrollArea,
+ VStack,
+} from "@marzneshin/components";
+import { useTranslation } from "react-i18next";
+import type { UserMutationType } from "@marzneshin/features/users";
import {
DATA_LIMIT_METRIC,
- UserMutationType,
useUsersCreationMutation,
- useUsersUpdateMutation
+ useUsersUpdateMutation,
} from "@marzneshin/features/users";
+
import { UserSchema } from "./schema";
-import {
- UsernameField,
- NoteField,
- ServicesField,
-} from "./fields";
+import { UsernameField, NoteField, ServicesField } from "./fields";
import { useMutationDialog } from "@marzneshin/hooks";
import { DataLimitFields, ExpirationMethodFields } from "./sections";
@@ -35,17 +35,20 @@ export const UsersMutationDialog: FC<UsersMutationDialogProps> = ({
onOpenChange,
}) => {
const { t } = useTranslation();
- const getDefaultValues = useCallback((): UserMutationType => ({
- services: [],
- data_limit: 1,
- expire: null,
- username: '',
- data_limit_reset_strategy: 'no_reset',
- status: 'active',
- note: '',
- on_hold_expire_duration: 0,
- on_hold_timeout: undefined,
- }), []);
+ const getDefaultValues = useCallback(
+ (): UserMutationType => ({
+ services: [],
+ data_limit: 1,
+ expire: null,
+ username: "",
+ data_limit_reset_strategy: "no_reset",
+ status: "active",
+ note: "",
+ on_hold_expire_duration: 0,
+ on_hold_timeout: undefined,
+ }),
+ [],
+ );
const { form, handleSubmit } = useMutationDialog({
entity,
@@ -54,41 +57,46 @@ export const UsersMutationDialog: FC<UsersMutationDialogProps> = ({
updateMutation: useUsersUpdateMutation(),
schema: UserSchema,
getDefaultValue: getDefaultValues,
- loadFormtter: (d) => ({ ...d, data_limit: (d.data_limit ? d.data_limit : 0) / DATA_LIMIT_METRIC })
- })
+ loadFormtter: (d) => ({
+ ...d,
+ data_limit: (d.data_limit ? d.data_limit : 0) / DATA_LIMIT_METRIC,
+ }),
+ });
return (
<Dialog open={open} onOpenChange={onOpenChange} defaultOpen={true}>
- <DialogContent className="sm:max-w-full md:max-w-[600px]">
- <DialogHeader>
- <DialogTitle className="text-primary">
- {entity
- ? t('page.users.dialogs.edition.title')
- : t('page.users.dialogs.creation.title')}
- </DialogTitle>
- </DialogHeader>
- <Form {...form}>
- <form onSubmit={handleSubmit}>
- <div className="flex-col grid-cols-2 gap-2 sm:flex md:grid">
- <div>
- <UsernameField disabled={(entity && entity.username) ? true : false} />
- <DataLimitFields />
- <ExpirationMethodFields entity={entity} />
- <NoteField />
- </div>
- <div>
- <ServicesField />
+ <DialogContent className="min-w-full h-full md:h-[42rem] md:min-w-[42rem]">
+ <ScrollArea className="flex flex-col justify-between h-full p-0">
+ <DialogHeader>
+ <DialogTitle className="text-primary">
+ {entity
+ ? t("page.users.dialogs.edition.title")
+ : t("page.users.dialogs.creation.title")}
+ </DialogTitle>
+ </DialogHeader>
+ <Form {...form}>
+ <form onSubmit={handleSubmit} className="">
+ <div className="flex-col grid-cols-2 gap-2 sm:flex md:grid h-full">
+ <div>
+ <UsernameField disabled={!!entity?.username} />
+ <DataLimitFields />
+ <ExpirationMethodFields entity={entity} />
+ <NoteField />
+ </div>
+ <VStack>
+ <ServicesField />
+ </VStack>
</div>
- </div>
- <Button
- className="mt-3 w-full font-semibold"
- type="submit"
- disabled={form.formState.isSubmitting}
- >
- {t('submit')}
- </Button>
- </form>
- </Form>
+ <Button
+ className="mt-3 w-full font-semibold"
+ type="submit"
+ disabled={form.formState.isSubmitting}
+ >
+ {t("submit")}
+ </Button>
+ </form>
+ </Form>
+ </ScrollArea>
</DialogContent>
</Dialog>
);