gitscope
← marzneshin / Diff · d42c6e6 → d42c6e6
Changes d42c6e6..d42c6e6
Patch commit
diff --git a/dashboard/src/features/admins/components/dialogs/mutation/index.tsx b/dashboard/src/features/admins/components/dialogs/mutation/index.tsx
index fba552b..ff7b88d 100644
--- a/dashboard/src/features/admins/components/dialogs/mutation/index.tsx
+++ b/dashboard/src/features/admins/components/dialogs/mutation/index.tsx
@@ -1,4 +1,4 @@
-import { type FC, useState, useEffect, useCallback } from "react";
+import { type FC, useState, useMemo } from "react";
import {
DialogTitle,
DialogContent,
@@ -14,7 +14,6 @@ import {
useAdminsCreationMutation,
useAdminsUpdateMutation,
AdminMutationSchema,
- type AdminMutationType,
AdminType,
} from "@marzneshin/features/admins";
import { ServicesField } from "@marzneshin/features/services";
@@ -27,40 +26,29 @@ import {
SudoPrivilageField,
AllServicesAccessField,
} from "./fields";
-import { useMutationDialog } from "@marzneshin/hooks";
+import { useMutationDialog, MutationDialogProps } from "@marzneshin/hooks";
-interface AdminsMutationDialogProps {
- open: boolean;
- onOpenChange: (state: boolean) => void;
- onClose: () => void;
- entity?: AdminType | null;
-}
-
-export const AdminsMutationDialog: FC<AdminsMutationDialogProps> = ({
- open,
- onOpenChange,
+export const AdminsMutationDialog: FC<MutationDialogProps<AdminType>> = ({
onClose,
entity = null,
}) => {
const { t } = useTranslation();
- const getDefaultValues = useCallback(
- (): AdminMutationType => ({
- service_ids: [],
- username: "",
- password: null,
- is_sudo: false,
- enabled: true,
- }),
- [],
- );
- const { form, handleSubmit } = useMutationDialog({
+ const defaultValue = useMemo(() => ({
+ service_ids: [],
+ username: "",
+ password: null,
+ is_sudo: false,
+ enabled: true,
+ }), [])
+
+ const { onOpenChange, open, form, handleSubmit } = useMutationDialog({
entity,
- onOpenChange,
+ onClose,
createMutation: useAdminsCreationMutation(),
updateMutation: useAdminsUpdateMutation(),
schema: AdminMutationSchema,
- getDefaultValue: getDefaultValues,
+ defaultValue,
});
const [change, setChange] = useState<boolean>(entity === null);
@@ -71,10 +59,6 @@ export const AdminsMutationDialog: FC<AdminsMutationDialogProps> = ({
shouldDirty: true,
});
- useEffect(() => {
- if (!open) onClose();
- }, [open, onClose]);
-
return (
<Dialog open={open} onOpenChange={onOpenChange} defaultOpen={true}>
<DialogContent className="min-w-full h-full md:h-auto md:min-w-[42rem]">
diff --git a/dashboard/src/features/github-repo/github-repo.stories.tsx b/dashboard/src/features/github-repo/github-repo.stories.tsx
index 9b8ecd5..1cdfeff 100644
--- a/dashboard/src/features/github-repo/github-repo.stories.tsx
+++ b/dashboard/src/features/github-repo/github-repo.stories.tsx
@@ -15,15 +15,19 @@ type Story = StoryObj<typeof GithubRepo>;
* Github repo present repo fullname, description, and repo stars.
* */
export const GithubRepoStats: Story = {
- args: {}
+ args: {
+ full_name: "marzneshin/marzneshin",
+ description: "Scalable and Comprehensive Backend Proxy Management",
+ stargazers_count: 1000,
+ }
}
-
/**
* Mini variant of Github repo present repo only the github logo and stars.
* */
export const GithubRepoStatsMini: Story = {
args: {
+ stargazers_count: 1000,
variant: "mini"
}
}
diff --git a/dashboard/src/features/github-repo/index.tsx b/dashboard/src/features/github-repo/github-repo.tsx
similarity index 55%
rename from dashboard/src/features/github-repo/index.tsx
rename to dashboard/src/features/github-repo/github-repo.tsx
index 1942960..34c6401 100644
--- a/dashboard/src/features/github-repo/index.tsx
+++ b/dashboard/src/features/github-repo/github-repo.tsx
@@ -5,9 +5,6 @@ import {
CardDescription,
Button,
} from "@marzneshin/components"
-import {
- useGithubRepoStatsQuery
-} from "./services";
import { type FC } from "react";
import {
GithubIcon,
@@ -17,32 +14,35 @@ import { projectInfo } from "@marzneshin/utils";
interface GithubRepoProps {
variant?: "full" | "mini"
+ stargazers_count: number;
+ full_name?: string;
+ description?: string;
}
-export const GithubRepo: FC<GithubRepoProps> = ({ variant = "full" }) => {
- const { data: stats } = useGithubRepoStatsQuery()
+export const GithubRepo: FC<GithubRepoProps> = ({ variant = "full", stargazers_count, full_name, description }) => {
return (
<Button variant="secondary" className="bg-gray-800 border-0 dark:text-primary text-secondary dark:hover:text-secondary dark:hover:bg-primary hover:bg-secondary hover:text-primary p-2" asChild>
<Card>
<a href={projectInfo.github} target="_blank">
<CardContent className="hstack size-fit p-0 gap-2 items-center">
<GithubIcon className="size-fit" />
- {variant === "full" ? (<div className="vstack items-start">
- <CardTitle className="font-mono text-xs hstack justify-between w-full">
- {stats.full_name}
- <div className="hstack gap-1 font-bold items-center text-xs">
- <StarIcon className="size-3" />
- {stats.stargazers_count}
- </div>
- </CardTitle>
- <CardDescription className="text-xs">
- {stats.description}
- </CardDescription>
- </div>
+ {variant === "full" ? (
+ <div className="vstack items-start">
+ <CardTitle className="font-mono text-xs hstack justify-between w-full">
+ {full_name}
+ <div className="hstack gap-1 font-bold items-center text-xs">
+ <StarIcon className="size-3" />
+ {stargazers_count}
+ </div>
+ </CardTitle>
+ <CardDescription className="text-xs">
+ {description}
+ </CardDescription>
+ </div>
) : (
<CardDescription className="hstack gap-1 font-bold items-center text-xs">
<StarIcon className="size-3" />
- {stats.stargazers_count}
+ {stargazers_count}
</CardDescription>
)}
</CardContent>
diff --git a/dashboard/src/features/github-repo/index.ts b/dashboard/src/features/github-repo/index.ts
new file mode 100644
index 0000000..4e408b2
--- /dev/null
+++ b/dashboard/src/features/github-repo/index.ts
@@ -0,0 +1,2 @@
+export * from "./github-repo"
+export * from "./services"
diff --git a/dashboard/src/features/github-repo/services/github-repo.query.ts b/dashboard/src/features/github-repo/services/github-repo.query.ts
index 5ffa20e..514cf9b 100644
--- a/dashboard/src/features/github-repo/services/github-repo.query.ts
+++ b/dashboard/src/features/github-repo/services/github-repo.query.ts
@@ -19,7 +19,7 @@ export interface GithubRepoStatsResponse {
export const GithubRepoStatsResponseDefault: GithubRepoStatsResponse = {
full_name: projectInfo.repo,
- description: "",
+ description: "A fork of Marzban aiming for scalability",
stargazers_count: 0,
}
@@ -34,7 +34,7 @@ export const useGithubRepoStatsQuery = () => {
return useQuery({
queryKey: ["github", projectInfo.repo, "stats"],
queryFn: fetchGithubRepoStats,
- refetchInterval: 1000 * 60 * 1, // 5min refresh
+ refetchInterval: 1000 * 60 * 5, // 5min refresh
initialData: GithubRepoStatsResponseDefault
})
}
diff --git a/dashboard/src/features/hosts/dialogs/mutation/index.tsx b/dashboard/src/features/hosts/dialogs/mutation/index.tsx
index 5f31d21..5c19826 100644
--- a/dashboard/src/features/hosts/dialogs/mutation/index.tsx
+++ b/dashboard/src/features/hosts/dialogs/mutation/index.tsx
@@ -1,5 +1,4 @@
-import { useEffect } from "react";
-import type { FC } from "react";
+import { type FC, useEffect } from "react";
import {
DialogTitle,
DialogContent,
@@ -22,10 +21,10 @@ import {
getDefaultValues,
useHostsCreationMutation,
useHostsUpdateMutation,
+ type HostType,
} from "@marzneshin/features/hosts";
-import type { HostType } from "@marzneshin/features/hosts";
-import { RemarkField } from "./fields/remark";
import {
+ RemarkField,
AddressField,
PortField,
HostField,
@@ -34,13 +33,10 @@ import {
PathField,
SecurityFields,
} from "./fields";
+import { useDialog, MutationDialogProps } from "@marzneshin/hooks";
-interface MutationDialogProps {
- entity?: HostType;
- open: boolean;
+interface HostMutationDialogProps extends MutationDialogProps<HostType> {
inboundId?: number;
- onOpenChange: (state: boolean) => void;
- onClose: () => void;
}
const transformFormValue = (values: HostType) => {
@@ -50,13 +46,12 @@ const transformFormValue = (values: HostType) => {
return { ...values, alpn, fingerprint, port };
};
-export const HostsMutationDialog: FC<MutationDialogProps> = ({
+export const HostsMutationDialog: FC<HostMutationDialogProps> = ({
inboundId,
entity,
- open,
- onOpenChange,
onClose,
}) => {
+ const [open, onOpenChange] = useDialog(true);
const form = useForm({
defaultValues: entity ? entity : getDefaultValues(),
resolver: zodResolver(HostSchema),
diff --git a/dashboard/src/features/nodes/dialogs/mutation/index.tsx b/dashboard/src/features/nodes/dialogs/mutation/index.tsx
index 4724305..6f2935e 100644
--- a/dashboard/src/features/nodes/dialogs/mutation/index.tsx
+++ b/dashboard/src/features/nodes/dialogs/mutation/index.tsx
@@ -1,4 +1,4 @@
-import { type FC, useEffect } from "react";
+import { type FC, useMemo } from "react";
import {
DialogTitle,
DialogContent,
@@ -27,46 +27,34 @@ import {
useNodesUpdateMutation,
} from "../..";
import type { NodeType } from "../..";
-import { useMutationDialog } from "@marzneshin/hooks";
+import { useMutationDialog, MutationDialogProps } from "@marzneshin/hooks";
-interface MutationDialogProps {
- entity: NodeType | null;
- open: boolean;
- onOpenChange: (state: boolean) => void;
- onClose: () => void;
-}
-
-const getDefaultValue = (): NodeType => ({
- name: "",
- address: "",
- status: "none",
- port: 62050,
- usage_coefficient: 1,
- connection_backend: "grpclib",
-});
-
-export const MutationDialog: FC<MutationDialogProps> = ({
+export const MutationDialog: FC<MutationDialogProps<NodeType>> = ({
entity,
- open,
- onOpenChange,
onClose,
}) => {
const updateMutation = useNodesUpdateMutation();
const createMutation = useNodesCreationMutation();
const { t } = useTranslation();
- const { form, handleSubmit } = useMutationDialog({
- onOpenChange,
+
+ const defaultValue = useMemo(() => ({
+ name: "",
+ address: "",
+ status: "none",
+ port: 62050,
+ usage_coefficient: 1,
+ connection_backend: "grpclib",
+ }), []);
+
+ const { onOpenChange, open, form, handleSubmit } = useMutationDialog({
entity,
+ onClose,
schema: NodeSchema,
createMutation,
updateMutation,
- getDefaultValue,
+ defaultValue,
});
- useEffect(() => {
- if (!open) onClose();
- }, [open, onClose]);
-
return (
<Dialog open={open} onOpenChange={onOpenChange} defaultOpen={true}>
<DialogContent>
diff --git a/dashboard/src/features/services/components/dialogs/mutation/index.tsx b/dashboard/src/features/services/components/dialogs/mutation/index.tsx
index f912154..634d5d1 100644
--- a/dashboard/src/features/services/components/dialogs/mutation/index.tsx
+++ b/dashboard/src/features/services/components/dialogs/mutation/index.tsx
@@ -6,7 +6,7 @@ import {
Form,
Button,
} from "@marzneshin/components";
-import { type FC, useEffect } from "react";
+import { type FC, useMemo } from "react";
import {
type ServiceType,
useServicesCreationMutation,
@@ -14,7 +14,7 @@ import {
} from "@marzneshin/features/services";
import { useTranslation } from "react-i18next";
import { z } from "zod";
-import { useMutationDialog } from "@marzneshin/hooks";
+import { useMutationDialog, MutationDialogProps } from "@marzneshin/hooks";
import { NameField, InboundsField } from "./fields";
export const ServiceSchema = z.object({
@@ -25,49 +25,32 @@ export const ServiceSchema = z.object({
name: z.string().trim().min(1),
});
-type ServiceCreateType = z.infer<typeof ServiceSchema>;
-
-interface MutationDialogProps {
- entity: ServiceType | null;
- open: boolean;
- onOpenChange: (state: boolean) => void;
- onClose: () => void;
-}
-
-const getDefaultValues = (): ServiceCreateType => ({
- name: "",
- inbound_ids: [],
-});
-
-export const MutationDialog: FC<MutationDialogProps> = ({
+export const MutationDialog: FC<MutationDialogProps<ServiceType>> = ({
entity,
- open,
- onOpenChange,
onClose,
}) => {
- const isEditing = entity !== null;
+ const defaultValue = useMemo(() => ({
+ name: "",
+ inbound_ids: [],
+ }), []);
const updateMutation = useServicesUpdateMutation();
const createMutation = useServicesCreationMutation();
- const { form, handleSubmit } = useMutationDialog({
+ const { open, onOpenChange, form, handleSubmit } = useMutationDialog({
+ onClose,
entity,
updateMutation,
createMutation,
- getDefaultValue: getDefaultValues,
+ defaultValue,
schema: ServiceSchema,
- onOpenChange,
});
const { t } = useTranslation();
- useEffect(() => {
- if (!open) onClose();
- }, [open, onClose]);
-
return (
<Dialog open={open} onOpenChange={onOpenChange} defaultOpen={true}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-primary">
- {isEditing
+ {entity
? t("page.services.dialogs.edition.title")
: t("page.services.dialogs.creation.title")}
</DialogTitle>
diff --git a/dashboard/src/features/users/components/dialogs/mutation/index.tsx b/dashboard/src/features/users/components/dialogs/mutation/index.tsx
index 1eaf1c5..d1d0820 100644
--- a/dashboard/src/features/users/components/dialogs/mutation/index.tsx
+++ b/dashboard/src/features/users/components/dialogs/mutation/index.tsx
@@ -1,4 +1,4 @@
-import { type FC, useEffect, useCallback } from "react";
+import { type FC, useMemo } from "react";
import {
Separator,
DialogTitle,
@@ -11,60 +11,43 @@ import {
VStack,
} from "@marzneshin/components";
import { useTranslation } from "react-i18next";
-import { UsernameField, type UserMutationType } from "@marzneshin/features/users";
import {
DATA_LIMIT_METRIC,
useUsersCreationMutation,
useUsersUpdateMutation,
UserSchema,
+ UsernameField, type UserMutationType
} from "@marzneshin/features/users";
import { ServicesField } from "@marzneshin/features/services";
import { NoteField } from "./fields";
-import { useMutationDialog } from "@marzneshin/hooks";
+import { MutationDialogProps, useMutationDialog } from "@marzneshin/hooks";
import { DataLimitFields, ExpirationMethodFields } from "./sections";
-interface UsersMutationDialogProps {
- entity: UserMutationType | null;
- open: boolean;
- onOpenChange: (state: boolean) => void;
- onClose: () => void;
-}
-
-export const UsersMutationDialog: FC<UsersMutationDialogProps> = ({
+export const UsersMutationDialog: FC<MutationDialogProps<UserMutationType>> = ({
entity,
- open,
- onOpenChange,
onClose,
}) => {
const { t } = useTranslation();
- const getDefaultValues = useCallback(
- (): UserMutationType => ({
- service_ids: [],
- expire_strategy: "fixed_date",
- username: "",
- data_limit_reset_strategy: "no_reset",
- note: "",
- }),
- [],
- );
+ const defaultValue = useMemo(() => ({
+ service_ids: [],
+ username: "",
+ data_limit_reset_strategy: "no_reset",
+ note: "",
+ }), [])
- const { form, handleSubmit } = useMutationDialog({
+ const { open, onOpenChange, form, handleSubmit } = useMutationDialog({
entity,
- onOpenChange,
+ onClose,
createMutation: useUsersCreationMutation(),
updateMutation: useUsersUpdateMutation(),
schema: UserSchema,
- getDefaultValue: getDefaultValues,
+ defaultValue,
loadFormtter: (d) => ({
...d,
data_limit: (d.data_limit ? d.data_limit : 0) / DATA_LIMIT_METRIC,
}),
});
- useEffect(() => {
- if (!open) onClose();
- }, [open, onClose]);
-
return (
<Dialog open={open} onOpenChange={onOpenChange} defaultOpen={true}>
<DialogContent className="min-w-full h-full md:h-auto md:min-w-[42rem]">
diff --git a/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/expiration-method.strategy.ts b/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/expiration-method.strategy.ts
index 4598aea..4c459bc 100644
--- a/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/expiration-method.strategy.ts
+++ b/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/expiration-method.strategy.ts
@@ -1,51 +1,52 @@
import { useFormContext } from "react-hook-form";
import { ExpireStrategy } from "@marzneshin/features/users";
-class ExpirationMethodErrorClear {
+abstract class ExpirationMethodStrategy {
+ protected abstract strategy: string;
+ protected abstract applyFieldValues(form: ReturnType<typeof useFormContext>): void;
+
protected clearErrors(form: ReturnType<typeof useFormContext>) {
form.clearErrors("expire_date");
form.clearErrors("usage_duration");
form.clearErrors("activation_deadline");
}
-}
-abstract class ExpirationMethodStrategy {
- abstract apply(form: ReturnType<typeof useFormContext>): void;
+ /**
+ * Applied the rules on the form's fields, called by component
+ *
+ * @param form - form object from the context
+ */
+ public apply(form: ReturnType<typeof useFormContext>) {
+ form.setValue("expire_strategy", this.strategy);
+ this.applyFieldValues(form)
+ this.clearErrors(form)
+ }
}
-export class FirstUseStrategy
- extends ExpirationMethodErrorClear
- implements ExpirationMethodStrategy {
+export class FirstUseStrategy extends ExpirationMethodStrategy {
+ strategy = "start_on_first_use";
- apply(form: ReturnType<typeof useFormContext>) {
- form.setValue("expire_strategy", "start_on_first_use");
+ protected applyFieldValues(form: ReturnType<typeof useFormContext>) {
form.setValue("expire", undefined);
- this.clearErrors(form);
}
}
-export class NeverStrategy
- extends ExpirationMethodErrorClear
- implements ExpirationMethodStrategy {
+export class NeverStrategy extends ExpirationMethodStrategy {
+ strategy = "never";
- apply(form: ReturnType<typeof useFormContext>) {
- form.setValue("expire_strategy", 'never');
+ protected applyFieldValues(form: ReturnType<typeof useFormContext>) {
form.setValue("expire_date", undefined);
form.setValue("activation_deadline", undefined);
form.setValue("usage_duration", undefined);
- this.clearErrors(form);
}
}
-export class FixedStrategy
- extends ExpirationMethodErrorClear
- implements ExpirationMethodStrategy {
+export class FixedStrategy extends ExpirationMethodStrategy {
+ strategy = "fixed_date";
- apply(form: ReturnType<typeof useFormContext>) {
- form.setValue("expire_strategy", 'fixed_date');
- form.setValue("on_hold_expire_duration", 0);
- form.setValue("activation_deadline", null);
- this.clearErrors(form);
+ protected applyFieldValues(form: ReturnType<typeof useFormContext>) {
+ form.setValue("usage_duration", undefined);
+ form.setValue("activation_deadline", undefined);
}
}
diff --git a/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/index.tsx b/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/index.tsx
index f826d60..5b4397a 100644
--- a/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/index.tsx
+++ b/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/index.tsx
@@ -7,7 +7,6 @@ import {
} from '@marzneshin/components';
import type {
UserMutationType,
- ExpireStrategy,
} from "@marzneshin/features/users";
import {
ExpireDateField,
@@ -25,13 +24,9 @@ interface ExpirationMethodProps {
export const ExpirationMethodFields: FC<ExpirationMethodProps> = ({ entity }) => {
const { t } = useTranslation();
const {
- setSelectedExpirationMethodTab,
- defaultExpirationMethodTab
- } = useExpirationMethodTabs({ entity });
-
- const handleTabChange = (value: string) => {
- setSelectedExpirationMethodTab(value as ExpireStrategy);
- };
+ handleTabChange,
+ selectedExpirationMethodTab
+ } = useExpirationMethodTabs(entity);
return (
<>
@@ -39,7 +34,7 @@ export const ExpirationMethodFields: FC<ExpirationMethodProps> = ({ entity }) =>
{t('page.users.expire_method')}
</FormLabel>
<Tabs
- defaultValue={defaultExpirationMethodTab}
+ defaultValue={selectedExpirationMethodTab}
onValueChange={handleTabChange}
className="w-full"
>
diff --git a/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/use-expiration-method-tabs.ts b/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/use-expiration-method-tabs.ts
index 381b819..9e9e5cd 100644
--- a/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/use-expiration-method-tabs.ts
+++ b/dashboard/src/features/users/components/dialogs/mutation/sections/expiration-method/use-expiration-method-tabs.ts
@@ -4,20 +4,25 @@ import { useFormContext } from "react-hook-form";
import { strategies } from "./expiration-method.strategy"
-export const useExpirationMethodTabs = ({ entity }: { entity: UserMutationType | null }) => {
+export const useExpirationMethodTabs = (entity: UserMutationType | null) => {
const form = useFormContext();
- const defaultExpirationMethodTab = entity ? entity.expire_strategy : 'fixed_date';
- const [selectedExpirationMethodTab, setSelectedExpirationMethodTab] = useState<ExpireStrategy>(defaultExpirationMethodTab);
-
+ const [
+ selectedExpirationMethodTab,
+ setSelectedExpirationMethodTab
+ ] = useState<ExpireStrategy>(entity ? entity.expire_strategy : 'fixed_date');
useEffect(() => {
strategies[selectedExpirationMethodTab as ExpireStrategy].apply(form);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedExpirationMethodTab]);
+ const handleTabChange = (value: string) => {
+ setSelectedExpirationMethodTab(value as ExpireStrategy);
+ };
+
return {
- selectedExpirationMethodTab: selectedExpirationMethodTab,
- defaultExpirationMethodTab,
+ selectedExpirationMethodTab,
setSelectedExpirationMethodTab,
+ handleTabChange,
};
};
diff --git a/dashboard/src/features/users/components/index.ts b/dashboard/src/features/users/components/index.ts
index f522142..833b2ff 100644
--- a/dashboard/src/features/users/components/index.ts
+++ b/dashboard/src/features/users/components/index.ts
@@ -8,3 +8,4 @@ export * from "./dialogs"
export * from "./widgets"
export * from "./pills"
export * from "./user-expiration-value"
+export * from "./no-service-alert"
diff --git a/dashboard/src/features/users/components/no-service-alert/index.tsx b/dashboard/src/features/users/components/no-service-alert/index.tsx
new file mode 100644
index 0000000..0f9f43e
--- /dev/null
+++ b/dashboard/src/features/users/components/no-service-alert/index.tsx
@@ -0,0 +1,25 @@
+import { useServicesQuery } from '@marzneshin/features/services';
+import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
+import { Link } from '@tanstack/react-router'
+import { useTranslation } from 'react-i18next';
+import {
+ Alert,
+ AlertDescription,
+ AlertTitle,
+} from '@marzneshin/components';
+
+export const UsersNoServiceAlert = () => {
+ const { t } = useTranslation();
+ const { data } = useServicesQuery({ page: 1, size: 10 });
+ return (data && data.pageCount === 0) && (
+ <Alert>
+ <ExclamationTriangleIcon className="mr-2" />
+ <AlertTitle className="font-semibold text-primary">{t('page.users.services-alert.title')}</AlertTitle>
+ <AlertDescription>
+ {t('page.users.services-alert.desc')}
+ <Link className="m-1 font-semibold text-secondary-foreground" to="/services">{t('page.nodes.certificate-alert.click')}</Link>
+ </AlertDescription>
+ </Alert>
+ )
+}
+
diff --git a/dashboard/src/hooks/use-mutation-dialog/index.ts b/dashboard/src/hooks/use-mutation-dialog/index.ts
index b8d61e0..eda8209 100644
--- a/dashboard/src/hooks/use-mutation-dialog/index.ts
+++ b/dashboard/src/hooks/use-mutation-dialog/index.ts
@@ -1,17 +1,20 @@
-
+import { useDialog } from "@marzneshin/hooks";
import { zodResolver } from "@hookform/resolvers/zod";
import { UseMutationResult } from "@tanstack/react-query";
import { useEffect } from "react";
import { FieldValues, SubmitHandler, useForm } from "react-hook-form";
import { ZodSchema } from "zod";
-interface IMutationDialog<TData, TError = unknown, TVariables = unknown> {
- entity: TData | null
- onOpenChange: (s: boolean) => void
+export interface MutationDialogProps<TData> {
+ entity: TData | null;
+ onClose: () => void;
+}
+
+interface IMutationDialog<TData, TError = unknown, TVariables = unknown> extends MutationDialogProps<TData> {
createMutation: UseMutationResult<TData, TError, TVariables>
updateMutation: UseMutationResult<TData, TError, TVariables>
schema: ZodSchema
- getDefaultValue: () => FieldValues
+ defaultValue: FieldValues
loadFormtter?: (d: FieldValues) => FieldValues
}
@@ -22,11 +25,11 @@ interface IMutationDialog<TData, TError = unknown, TVariables = unknown> {
* @param updateMutation - react-query mutation hook for entity edition
* @param entity - Entity value for update, leave empty for creation
* @param schema - Entity schema validated by zodResolver
+ * @param onClose - Handler for when the dialog is closed
* @param getDefaultValue - Default value function return the default value for function
* - Must be wrapped in useCallback to avoid infinite useEffect loop
- * @param onOpenChange - Dialog state callback to change the dialog openness state
*
- * @returns react-hook-form and submit handler
+ * @returns react-hook-form and submit handler plus the dialog open state
*
* @template TData - Mutation Type of Schema
* @template TError = unknown
@@ -34,17 +37,18 @@ interface IMutationDialog<TData, TError = unknown, TVariables = unknown> {
*/
export const useMutationDialog = <TData, TError = unknown, TVariables = unknown>({
+ onClose,
entity,
- onOpenChange,
createMutation,
updateMutation,
schema,
- getDefaultValue,
+ defaultValue,
loadFormtter = (s) => s,
}: IMutationDialog<TData, TError, TVariables>) => {
+ const [open, onOpenChange] = useDialog(true);
const form = useForm({
- defaultValues: entity ? loadFormtter(entity) : getDefaultValue(),
+ defaultValues: entity ? loadFormtter(entity) : defaultValue,
resolver: zodResolver(schema)
});
@@ -61,15 +65,20 @@ export const useMutationDialog = <TData, TError = unknown, TVariables = unknown>
if (entity) {
form.reset(loadFormtter(entity))
} else {
- form.reset(getDefaultValue());
+ form.reset(defaultValue);
}
// eslint-disable-next-line
- }, [entity, form, getDefaultValue]);
+ }, [entity, defaultValue]);
+
+ useEffect(() => {
+ if (!open) onClose();
+ }, [open, onClose]);
const handleSubmit = form.handleSubmit(submit as SubmitHandler<FieldValues>);
return {
form,
+ open, onOpenChange,
handleSubmit
};
};
diff --git a/dashboard/src/routes/_dashboard.tsx b/dashboard/src/routes/_dashboard.tsx
index 47a19a4..c2d7fda 100644
--- a/dashboard/src/routes/_dashboard.tsx
+++ b/dashboard/src/routes/_dashboard.tsx
@@ -20,7 +20,7 @@ import {
createFileRoute,
redirect
} from "@tanstack/react-router";
-import { GithubRepo } from "@marzneshin/features/github-repo";
+import { useGithubRepoStatsQuery, GithubRepo } from "@marzneshin/features/github-repo";
import { CommandBox } from "@marzneshin/features/search-command";
import { DashboardBottomMenu } from "@marzneshin/features/bottom-menu";
import { VersionIndicator } from "@marzneshin/features/version-indicator";
@@ -34,6 +34,7 @@ export const DashboardLayout = () => {
toggleCollapse,
} = usePanelToggle(isDesktop);
const { isSudo } = useAuth();
+ const { data: stats } = useGithubRepoStatsQuery()
return (
<div className="flex flex-col w-screen h-screen">
@@ -55,7 +56,7 @@ export const DashboardLayout = () => {
center={<CommandBox />}
end={
<>
- <GithubRepo variant={isDesktop ? "full" : "mini"} />
+ <GithubRepo {...stats} variant={isDesktop ? "full" : "mini"} />
<HeaderMenu />
</>
}
diff --git a/dashboard/src/routes/_dashboard/admins/$adminId/edit.tsx b/dashboard/src/routes/_dashboard/admins/$adminId/edit.tsx
index dcc3f53..ff0347a 100644
--- a/dashboard/src/routes/_dashboard/admins/$adminId/edit.tsx
+++ b/dashboard/src/routes/_dashboard/admins/$adminId/edit.tsx
@@ -6,18 +6,13 @@ import {
AdminsMutationDialog,
useRouterAdminContext,
} from "@marzneshin/features/admins";
-import { useDialog } from "@marzneshin/hooks";
-
const AdminEdit = () => {
- const [editDialogOpen, setEditDialogOpen] = useDialog(true);
const value = useRouterAdminContext()
const navigate = useNavigate({ from: "/admins/$adminId/edit" });
return value && (
<AdminsMutationDialog
- open={editDialogOpen}
- onOpenChange={setEditDialogOpen}
entity={value.admin}
onClose={() => navigate({ to: "/admins" })}
/>
diff --git a/dashboard/src/routes/_dashboard/admins/create.tsx b/dashboard/src/routes/_dashboard/admins/create.tsx
index 9bfcb98..63adfb2 100644
--- a/dashboard/src/routes/_dashboard/admins/create.tsx
+++ b/dashboard/src/routes/_dashboard/admins/create.tsx
@@ -1,14 +1,11 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { AdminsMutationDialog } from "@marzneshin/features/admins";
-import { useDialog } from "@marzneshin/hooks";
const AdminCreate = () => {
- const [mutationDialogOpen, setMutationDialogOpen] = useDialog(true);
const navigate = useNavigate({ from: "/admins/create" });
return (
<AdminsMutationDialog
- open={mutationDialogOpen}
- onOpenChange={setMutationDialogOpen}
+ entity={null}
onClose={() => navigate({ to: "/admins" })}
/>
);
diff --git a/dashboard/src/routes/_dashboard/hosts/$hostId/edit.tsx b/dashboard/src/routes/_dashboard/hosts/$hostId/edit.tsx
index 95596f0..6c3ba42 100644
--- a/dashboard/src/routes/_dashboard/hosts/$hostId/edit.tsx
+++ b/dashboard/src/routes/_dashboard/hosts/$hostId/edit.tsx
@@ -6,21 +6,16 @@ import {
HostsMutationDialog,
useRouterHostContext,
} from "@marzneshin/features/hosts";
-import { useDialog } from "@marzneshin/hooks";
-
const HostEdit = () => {
- const [editDialogOpen, setEditDialogOpen] = useDialog(true);
const value = useRouterHostContext()
const navigate = useNavigate({ from: "/hosts/$hostId/edit" });
return value && (
- <HostsMutationDialog
- open={editDialogOpen}
- onOpenChange={setEditDialogOpen}
- entity={value.host}
- onClose={() => navigate({ to: "/hosts" })}
- />
+ <HostsMutationDialog
+ entity={value.host}
+ onClose={() => navigate({ to: "/hosts" })}
+ />
);
}
diff --git a/dashboard/src/routes/_dashboard/hosts/$inboundId/create.tsx b/dashboard/src/routes/_dashboard/hosts/$inboundId/create.tsx
index b711498..d693723 100644
--- a/dashboard/src/routes/_dashboard/hosts/$inboundId/create.tsx
+++ b/dashboard/src/routes/_dashboard/hosts/$inboundId/create.tsx
@@ -1,17 +1,14 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { HostsMutationDialog } from "@marzneshin/features/hosts";
import { AlertDialog, AlertDialogContent } from "@marzneshin/components";
-import { useDialog } from "@marzneshin/hooks";
const HostCreate = () => {
- const [createDialogOpen, setCreateDialogOpen] = useDialog(true);
const { inboundId } = Route.useParams();
const navigate = useNavigate({ from: "/hosts/$inboundId/create" });
return (
<HostsMutationDialog
- open={createDialogOpen}
- onOpenChange={setCreateDialogOpen}
+ entity={null}
inboundId={Number(inboundId)}
onClose={() => navigate({ to: "/hosts" })}
/>
diff --git a/dashboard/src/routes/_dashboard/nodes/$nodeId/edit.tsx b/dashboard/src/routes/_dashboard/nodes/$nodeId/edit.tsx
index d2af223..6f874e2 100644
--- a/dashboard/src/routes/_dashboard/nodes/$nodeId/edit.tsx
+++ b/dashboard/src/routes/_dashboard/nodes/$nodeId/edit.tsx
@@ -6,18 +6,13 @@ import {
MutationDialog,
useRouterNodeContext,
} from "@marzneshin/features/nodes";
-import { useDialog } from "@marzneshin/hooks";
-
const NodeEdit = () => {
- const [editDialogOpen, setEditDialogOpen] = useDialog(true);
const value = useRouterNodeContext()
const navigate = useNavigate({ from: "/nodes/$nodeId/edit" });
return value && (
<MutationDialog
- open={editDialogOpen}
- onOpenChange={setEditDialogOpen}
entity={value.node}
onClose={() => navigate({ to: "/nodes" })}
/>
diff --git a/dashboard/src/routes/_dashboard/nodes/create.tsx b/dashboard/src/routes/_dashboard/nodes/create.tsx
index d5121fd..efc22de 100644
--- a/dashboard/src/routes/_dashboard/nodes/create.tsx
+++ b/dashboard/src/routes/_dashboard/nodes/create.tsx
@@ -1,14 +1,10 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { MutationDialog } from "@marzneshin/features/nodes";
-import { useDialog } from "@marzneshin/hooks";
const NodeCreate = () => {
- const [createDialogOpen, setCreateDialogOpen] = useDialog(true);
const navigate = useNavigate({ from: "/nodes/create" });
return (
<MutationDialog
- open={createDialogOpen}
- onOpenChange={setCreateDialogOpen}
entity={null}
onClose={() => navigate({ to: "/nodes" })}
/>
diff --git a/dashboard/src/routes/_dashboard/services/$serviceId/edit.tsx b/dashboard/src/routes/_dashboard/services/$serviceId/edit.tsx
index e700264..cb3e47b 100644
--- a/dashboard/src/routes/_dashboard/services/$serviceId/edit.tsx
+++ b/dashboard/src/routes/_dashboard/services/$serviceId/edit.tsx
@@ -6,18 +6,13 @@ import {
MutationDialog,
useRouterServiceContext,
} from "@marzneshin/features/services";
-import { useDialog } from "@marzneshin/hooks";
-
const ServiceEdit = () => {
- const [editDialogOpen, setEditDialogOpen] = useDialog(true);
const value = useRouterServiceContext()
const navigate = useNavigate({ from: "/services/$serviceId/edit" });
return value && (
<MutationDialog
- open={editDialogOpen}
- onOpenChange={setEditDialogOpen}
entity={value.service}
onClose={() => navigate({ to: "/services" })}
/>
diff --git a/dashboard/src/routes/_dashboard/services/create.tsx b/dashboard/src/routes/_dashboard/services/create.tsx
index 2d24f45..b74c055 100644
--- a/dashboard/src/routes/_dashboard/services/create.tsx
+++ b/dashboard/src/routes/_dashboard/services/create.tsx
@@ -1,14 +1,10 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { MutationDialog } from "@marzneshin/features/services";
-import { useDialog } from "@marzneshin/hooks";
const ServiceCreate = () => {
- const [createDialogOpen, setCreateDialogOpen] = useDialog(true);
const navigate = useNavigate({ from: "/services/create" });
return (
<MutationDialog
- open={createDialogOpen}
- onOpenChange={setCreateDialogOpen}
entity={null}
onClose={() => navigate({ to: "/services" })}
/>
diff --git a/dashboard/src/routes/_dashboard/users.lazy.tsx b/dashboard/src/routes/_dashboard/users.lazy.tsx
index d447c5c..a1dc410 100644
--- a/dashboard/src/routes/_dashboard/users.lazy.tsx
+++ b/dashboard/src/routes/_dashboard/users.lazy.tsx
@@ -1,7 +1,4 @@
import {
- Alert,
- AlertDescription,
- AlertTitle,
Card,
CardContent,
CardFooter,
@@ -10,15 +7,12 @@ import {
Page,
Loading,
} from '@marzneshin/components';
-import { useServicesQuery } from '@marzneshin/features/services';
-import { UsersTable } from '@marzneshin/features/users';
-import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
-import { Link, createFileRoute, Outlet } from '@tanstack/react-router'
+import { UsersNoServiceAlert, UsersTable } from '@marzneshin/features/users';
+import { createFileRoute, Outlet } from '@tanstack/react-router'
import { type FC, Suspense } from 'react';
import { useTranslation } from 'react-i18next';
export const UsersPage: FC = () => {
- const { data } = useServicesQuery({ page: 1, size: 10 });
const { t } = useTranslation();
return (
<Page>
@@ -35,23 +29,13 @@ export const UsersPage: FC = () => {
</Suspense>
</CardContent>
<CardFooter>
- {(data && data.pageCount === 0) && (
- <Alert>
- <ExclamationTriangleIcon className="mr-2" />
- <AlertTitle className="font-semibold text-primary">{t('page.users.services-alert.title')}</AlertTitle>
- <AlertDescription>
- {t('page.users.services-alert.desc')}
- <Link className="m-1 font-semibold text-secondary-foreground" to="/services">{t('page.nodes.certificate-alert.click')}</Link>
- </AlertDescription>
- </Alert>
- )}
+ <UsersNoServiceAlert />
</CardFooter>
</Card>
</Page>
)
};
-
export const Route = createFileRoute('/_dashboard/users')({
component: () => <UsersPage />,
})
diff --git a/dashboard/src/routes/_dashboard/users/$userId/edit.tsx b/dashboard/src/routes/_dashboard/users/$userId/edit.tsx
index 87f2719..6ab00cd 100644
--- a/dashboard/src/routes/_dashboard/users/$userId/edit.tsx
+++ b/dashboard/src/routes/_dashboard/users/$userId/edit.tsx
@@ -6,18 +6,14 @@ import {
UsersMutationDialog,
useRouterUserContext,
} from "@marzneshin/features/users";
-import { useDialog } from "@marzneshin/hooks";
const UserEdit = () => {
- const [editDialogOpen, setEditDialogOpen] = useDialog(true);
const value = useRouterUserContext()
const navigate = useNavigate({ from: "/users/$userId/edit" });
- return value && (
+ return value && value.user && (
<UsersMutationDialog
- open={editDialogOpen}
- onOpenChange={setEditDialogOpen}
entity={value.user}
onClose={() => navigate({ to: "/users" })}
/>
diff --git a/dashboard/src/routes/_dashboard/users/create.tsx b/dashboard/src/routes/_dashboard/users/create.tsx
index 3fea6ce..5d99ab9 100644
--- a/dashboard/src/routes/_dashboard/users/create.tsx
+++ b/dashboard/src/routes/_dashboard/users/create.tsx
@@ -1,14 +1,10 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { UsersMutationDialog } from "@marzneshin/features/users";
-import { useDialog } from "@marzneshin/hooks";
const UserCreate = () => {
- const [mutationDialogOpen, setMutationDialogOpen] = useDialog(true);
const navigate = useNavigate({ from: "/users/create" });
return (
<UsersMutationDialog
- open={mutationDialogOpen}
- onOpenChange={setMutationDialogOpen}
entity={null}
onClose={() => navigate({ to: "/users" })}
/>