gitscope
← marzneshin / Diff · 03b3dbc → 03b3dbc
Changes 03b3dbc..03b3dbc
Patch commit
diff --git a/app/routes/inbounds.py b/app/routes/inbounds.py
index 58018d7..d96221b 100644
--- a/app/routes/inbounds.py
+++ b/app/routes/inbounds.py
@@ -8,6 +8,8 @@ from app.db.models import InboundHost as DBInboundHost, Inbound as DBInbound
from app.dependencies import DBDep, sudo_admin
from app.models.proxy import Inbound, InboundHost, InboundHostResponse
+HOST_NOT_FOUND_ERROR_MSG = "Host not found"
+
router = APIRouter(
prefix="/inbounds", dependencies=[Depends(sudo_admin)], tags=["Inbounds"]
)
@@ -30,6 +32,18 @@ def get_hosts(db: DBDep):
return paginate(db.query(DBInboundHost))
+@router.get("/hosts/{id}", response_model=InboundHostResponse)
+def get_host(id: int, db: DBDep):
+ """
+ Get a host
+ """
+ host = crud.get_host(db, id)
+ if not host:
+ raise HTTPException(status_code=404, detail=HOST_NOT_FOUND_ERROR_MSG)
+
+ return host
+
+
@router.put("/hosts/{id}", response_model=InboundHostResponse)
def update_host(id: int, host: InboundHost, db: DBDep):
"""
@@ -38,7 +52,7 @@ def update_host(id: int, host: InboundHost, db: DBDep):
db_host = crud.get_host(db, id)
if not db_host:
- raise HTTPException(status_code=404, detail="Host not found")
+ raise HTTPException(status_code=404, detail=HOST_NOT_FOUND_ERROR_MSG)
return crud.update_host(db, db_host, host)
@@ -50,7 +64,7 @@ def delete_host(id: int, db: DBDep):
"""
db_host = crud.get_host(db, id)
if not db_host:
- raise HTTPException(status_code=404, detail="Host not found")
+ raise HTTPException(status_code=404, detail=HOST_NOT_FOUND_ERROR_MSG)
db.delete(db_host)
db.commit()
diff --git a/dashboard/chromatic.config.json b/dashboard/chromatic.config.json
new file mode 100644
index 0000000..f946ac5
--- /dev/null
+++ b/dashboard/chromatic.config.json
@@ -0,0 +1,6 @@
+{
+ "onlyChanged": true,
+ "projectId": "Project:665594c0ba1c945234050eca",
+ "storybookBaseDir": "dashboard",
+ "zip": true
+}
diff --git a/dashboard/src/components/data-table/pagination.tsx b/dashboard/src/components/data-table/pagination.tsx
index 050f056..55095e4 100644
--- a/dashboard/src/components/data-table/pagination.tsx
+++ b/dashboard/src/components/data-table/pagination.tsx
@@ -5,7 +5,7 @@ import {
DoubleArrowRightIcon,
} from "@radix-ui/react-icons"
import { Table } from "@tanstack/react-table"
-
+import { cn } from "@marzneshin/utils"
import {
Button,
Select,
@@ -18,19 +18,22 @@ import { useTranslation } from "react-i18next"
interface DataTablePaginationProps<TData> {
table: Table<TData>
+ disableSelected?: boolean
}
export function DataTablePagination<TData>({
- table,
+ table, disableSelected
}: DataTablePaginationProps<TData>) {
const { t } = useTranslation();
return (
- <div className="flex justify-between items-center p-2">
- <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">
+ <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
diff --git a/dashboard/src/components/delete-confirmation-dialog/index.tsx b/dashboard/src/components/delete-confirmation-dialog/index.tsx
index 15b2fb7..b2d00a6 100644
--- a/dashboard/src/components/delete-confirmation-dialog/index.tsx
+++ b/dashboard/src/components/delete-confirmation-dialog/index.tsx
@@ -1,4 +1,3 @@
-
import {
AlertDialog,
AlertDialogAction,
@@ -8,36 +7,42 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
-} from "@marzneshin/components"
-import { ExclamationTriangleIcon } from "@radix-ui/react-icons"
-import { FC } from "react"
-import { useTranslation } from "react-i18next"
+} from "@marzneshin/components";
+import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
+import { FC } from "react";
+import { useTranslation } from "react-i18next";
interface DeleteConfirmationProps {
- action: () => void
- onOpenChange: (open: boolean) => void
- open: boolean
+ action: () => void;
+ onOpenChange: (open: boolean) => void;
+ open: boolean;
}
-export const DeleteConfirmation: FC<DeleteConfirmationProps> = ({ action, open, onOpenChange }) => {
+export const DeleteConfirmation: FC<DeleteConfirmationProps> = ({
+ action,
+ open,
+ onOpenChange,
+}) => {
const { t } = useTranslation();
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
- <AlertDialogHeader >
+ <AlertDialogHeader>
<AlertDialogTitle className="flex flex-row gap-3 items-center text-destructive">
<ExclamationTriangleIcon className="p-2 w-10 h-10 bg-red-200 rounded-md border-2 border-destructive" />
- {t('delete-confirmation.title')}
+ {t("delete-confirmation.title")}
</AlertDialogTitle>
<AlertDialogDescription>
- {t('delete-confirmation.desc')}
+ {t("delete-confirmation.desc")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
- <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
- <AlertDialogAction onClick={action} className="bg-destructive">{t('delete')}</AlertDialogAction>
+ <AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
+ <AlertDialogAction onClick={action} className="bg-destructive">
+ {t("delete")}
+ </AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
- )
-}
+ );
+};
diff --git a/dashboard/src/components/entity-table/index.tsx b/dashboard/src/components/entity-table/index.tsx
index 41c7bed..3d5686a 100644
--- a/dashboard/src/components/entity-table/index.tsx
+++ b/dashboard/src/components/entity-table/index.tsx
@@ -1,4 +1,4 @@
-import { type FC, useMemo, useState } from "react";
+import { useMemo } from "react";
import { Button, DataTableViewOptions } from "@marzneshin/components";
import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query";
@@ -8,7 +8,6 @@ import { DataTablePagination } from "./table-pagination";
import { TableFiltering } from "./table-filtering";
import {
type UseRowSelectionReturn,
- type UseDialogProps,
useFiltering,
usePagination,
type FetchEntityReturn,
@@ -19,56 +18,32 @@ import {
type QueryKey,
type EntityQueryKeyType,
} from "./hooks";
-import { useDialog } from "@marzneshin/hooks";
interface EntityTableProps<T> {
fetchEntity: ({ queryKey }: EntityQueryKeyType) => FetchEntityReturn<T>;
- MutationDialog: FC<UseDialogProps<T>>;
- DeleteConfirmationDialog: FC<UseDialogProps<T>>;
- SettingsDialog: FC<UseDialogProps<T | any>>;
columnsFn: any;
filteredColumn: string;
entityKey: string;
rowSelection?: UseRowSelectionReturn;
manualSorting?: boolean;
+ onCreate: () => void;
+ onEdit: (entity: T) => void;
+ onOpen: (entity: T) => void;
+ onDelete: (entity: T) => void;
}
export function EntityTable<T>({
fetchEntity,
- MutationDialog,
- DeleteConfirmationDialog,
- SettingsDialog,
columnsFn,
filteredColumn,
rowSelection,
entityKey,
manualSorting = false,
+ onCreate,
+ onEdit,
+ onOpen,
+ onDelete,
}: EntityTableProps<T>) {
- const [mutationDialogOpen, setMutationDialogOpen] = useDialog();
- const [deleteDialogOpen, setDeleteDialogOpen] = useDialog();
- const [settingsDialogOpen, setSettingsDialogOpen] = useDialog();
- const [selectedEntity, selectEntity] = useState<T | null>(null);
-
- const onEdit = (entity: T) => {
- selectEntity(entity);
- setMutationDialogOpen(true);
- };
-
- const onDelete = (entity: T) => {
- selectEntity(entity);
- setDeleteDialogOpen(true);
- };
-
- const onCreate = () => {
- selectEntity(null);
- setMutationDialogOpen(true);
- };
-
- const onOpen = (entity: T) => {
- selectEntity(entity);
- setSettingsDialogOpen(true);
- };
-
const { t } = useTranslation();
const filtering = useFiltering({ column: filteredColumn });
const sorting = useSorting();
@@ -107,29 +82,13 @@ export function EntityTable<T>({
onPaginationChange,
});
- const contextValue = useMemo(() => (
- { table, data: data.entity, filtering, isLoading }
- ), [table, data.entity, filtering, isLoading])
+ const contextValue = useMemo(
+ () => ({ table, data: data.entity, filtering, isLoading }),
+ [table, data.entity, filtering, isLoading],
+ );
return (
- <EntityTableContext.Provider
- value={contextValue}
- >
- <SettingsDialog
- open={settingsDialogOpen}
- onOpenChange={setSettingsDialogOpen}
- entity={selectedEntity}
- />
- <DeleteConfirmationDialog
- open={deleteDialogOpen}
- onOpenChange={setDeleteDialogOpen}
- entity={selectedEntity}
- />
- <MutationDialog
- open={mutationDialogOpen}
- onOpenChange={setMutationDialogOpen}
- entity={selectedEntity}
- />
+ <EntityTableContext.Provider value={contextValue}>
<div className="flex flex-col">
<div className="flex items-center py-4">
<TableFiltering />
diff --git a/dashboard/src/components/index.ts b/dashboard/src/components/index.ts
index 89c04dd..4a48089 100644
--- a/dashboard/src/components/index.ts
+++ b/dashboard/src/components/index.ts
@@ -12,4 +12,5 @@ export * from "./stacks";
export * from "./widgets";
export * from "./charts";
export * from "./support-us";
+export * from "./loading";
export * from "./entity-field-card";
diff --git a/dashboard/src/components/loading/index.tsx b/dashboard/src/components/loading/index.tsx
new file mode 100644
index 0000000..694842d
--- /dev/null
+++ b/dashboard/src/components/loading/index.tsx
@@ -0,0 +1,9 @@
+import { Loader } from 'lucide-react';
+
+export const Loading = () => (
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
+ <div className="flex flex-col items-center">
+ <Loader className="animate-spin text-white size-9" />
+ </div>
+ </div>
+);
diff --git a/dashboard/src/features/hosts/dialogs/delete-confirmation/index.tsx b/dashboard/src/features/hosts/dialogs/delete-confirmation/index.tsx
index b0da129..30b1e2f 100644
--- a/dashboard/src/features/hosts/dialogs/delete-confirmation/index.tsx
+++ b/dashboard/src/features/hosts/dialogs/delete-confirmation/index.tsx
@@ -1,22 +1,26 @@
import { DeleteConfirmation } from '@marzneshin/components'
-import { FC } from 'react'
+import { type FC, useEffect } from 'react'
import { HostType, useHostsDeletionMutation } from '@marzneshin/features/hosts'
interface HostsDeleteConfirmationDialogProps {
onOpenChange: (state: boolean) => void
+ onClose: () => void
open: boolean
- entity: HostType | null
+ entity: HostType
}
-export const HostsDeleteConfirmationDialog: FC<HostsDeleteConfirmationDialogProps> = ({ onOpenChange, open, entity }) => {
+export const HostsDeleteConfirmationDialog: FC<HostsDeleteConfirmationDialogProps> = ({ onOpenChange, open, entity, onClose }) => {
const deleteMutation = useHostsDeletionMutation();
- if (entity !== null) {
- return (
- <DeleteConfirmation
- open={open}
- onOpenChange={onOpenChange}
- action={() => deleteMutation.mutate(entity)}
- />
- )
- }
+
+ useEffect(() => {
+ if (!open) onClose();
+ }, [open, onClose]);
+
+ return (
+ <DeleteConfirmation
+ open={open}
+ onOpenChange={onOpenChange}
+ action={() => deleteMutation.mutate(entity)}
+ />
+ )
}
diff --git a/dashboard/src/features/hosts/dialogs/index.ts b/dashboard/src/features/hosts/dialogs/index.ts
index 3612d28..933e0cf 100644
--- a/dashboard/src/features/hosts/dialogs/index.ts
+++ b/dashboard/src/features/hosts/dialogs/index.ts
@@ -1,2 +1,3 @@
export * from './mutation'
export * from './delete-confirmation'
+export * from './settings'
diff --git a/dashboard/src/features/hosts/dialogs/mutation/index.tsx b/dashboard/src/features/hosts/dialogs/mutation/index.tsx
index d6c02bd..5f31d21 100644
--- a/dashboard/src/features/hosts/dialogs/mutation/index.tsx
+++ b/dashboard/src/features/hosts/dialogs/mutation/index.tsx
@@ -36,10 +36,11 @@ import {
} from "./fields";
interface MutationDialogProps {
- entity: HostType | null;
+ entity?: HostType;
open: boolean;
- inboundId: number;
+ inboundId?: number;
onOpenChange: (state: boolean) => void;
+ onClose: () => void;
}
const transformFormValue = (values: HostType) => {
@@ -54,6 +55,7 @@ export const HostsMutationDialog: FC<MutationDialogProps> = ({
entity,
open,
onOpenChange,
+ onClose,
}) => {
const form = useForm({
defaultValues: entity ? entity : getDefaultValues(),
@@ -67,12 +69,17 @@ export const HostsMutationDialog: FC<MutationDialogProps> = ({
const host = transformFormValue(values);
if (entity && entity.id !== undefined) {
updateMutation.mutate({ hostId: entity.id, host });
- } else {
+ onOpenChange(false);
+ } else if (inboundId !== undefined) {
createMutation.mutate({ inboundId, host });
+ onOpenChange(false);
}
- onOpenChange(false);
};
+ useEffect(() => {
+ if (!open) onClose();
+ }, [open, onClose]);
+
useEffect(() => {
if (entity) form.reset(entity);
else form.reset(getDefaultValues());
diff --git a/dashboard/src/features/hosts/dialogs/settings/index.tsx b/dashboard/src/features/hosts/dialogs/settings/index.tsx
index 2b2c26e..469f0bc 100644
--- a/dashboard/src/features/hosts/dialogs/settings/index.tsx
+++ b/dashboard/src/features/hosts/dialogs/settings/index.tsx
@@ -1,4 +1,4 @@
-
+import { type FC, useEffect } from "react";
import {
Sheet,
SheetContent,
@@ -10,91 +10,92 @@ import {
TableHead,
TableRow,
} from "@marzneshin/components"
-import { FC } from "react"
import { useTranslation } from "react-i18next"
import { HostType } from "../.."
interface HostsSettingsDialogProps {
onOpenChange: (state: boolean) => void
+ onClose: () => void
open: boolean
- entity: HostType | null
+ entity: HostType
}
-export const HostSettingsDialog: FC<HostsSettingsDialogProps> = ({ onOpenChange, open, entity }) => {
-
+export const HostSettingsDialog: FC<HostsSettingsDialogProps> = ({ onOpenChange, open, entity, onClose }) => {
const { t } = useTranslation();
- if (entity) {
- return (
- <Sheet open={open} onOpenChange={onOpenChange}>
- <SheetContent className="sm:min-w-full md:min-w-[700px]" >
- <SheetHeader>
- <SheetTitle>
- {t('page.hosts.settings.title')}
- </SheetTitle>
- </SheetHeader>
- <Table>
- <TableBody>
- <TableRow>
- <TableHead>
- {t('remark')}
- </TableHead>
- <TableCell>
- {entity.remark}
- </TableCell>
- </TableRow>
- <TableRow>
- <TableHead>
- {t('address')}
- </TableHead>
- <TableCell>
- {entity.address}
- </TableCell>
- </TableRow>
- <TableRow>
- <TableHead>
- {t('port')}
- </TableHead>
- <TableCell>
- {entity.port}
- </TableCell>
- </TableRow>
- <TableRow>
- <TableHead>
- {t('host')}
- </TableHead>
- <TableCell>
- {entity.host}
- </TableCell>
- </TableRow>
- <TableRow>
- <TableHead>
- {t('path')}
- </TableHead>
- <TableCell>
- {entity.path}
- </TableCell>
- </TableRow>
- <TableRow>
- <TableHead>
- {t('sni')}
- </TableHead>
- <TableCell>
- {entity.sni}
- </TableCell>
- </TableRow>
- <TableRow>
- <TableHead>
- {t('alpn')}
- </TableHead>
- <TableCell>
- {entity.alpn}
- </TableCell>
- </TableRow>
- </TableBody>
- </Table>
- </SheetContent>
- </Sheet>
- )
- }
+ useEffect(() => {
+ if (!open) onClose();
+ }, [open, onClose]);
+
+ return (
+ <Sheet open={open} onOpenChange={onOpenChange}>
+ <SheetContent className="sm:min-w-full md:min-w-[700px]" >
+ <SheetHeader>
+ <SheetTitle>
+ {t('page.hosts.settings.title')}
+ </SheetTitle>
+ </SheetHeader>
+ <Table>
+ <TableBody>
+ <TableRow>
+ <TableHead>
+ {t('remark')}
+ </TableHead>
+ <TableCell>
+ {entity.remark}
+ </TableCell>
+ </TableRow>
+ <TableRow>
+ <TableHead>
+ {t('address')}
+ </TableHead>
+ <TableCell>
+ {entity.address}
+ </TableCell>
+ </TableRow>
+ <TableRow>
+ <TableHead>
+ {t('port')}
+ </TableHead>
+ <TableCell>
+ {entity.port}
+ </TableCell>
+ </TableRow>
+ <TableRow>
+ <TableHead>
+ {t('host')}
+ </TableHead>
+ <TableCell>
+ {entity.host}
+ </TableCell>
+ </TableRow>
+ <TableRow>
+ <TableHead>
+ {t('path')}
+ </TableHead>
+ <TableCell>
+ {entity.path}
+ </TableCell>
+ </TableRow>
+ <TableRow>
+ <TableHead>
+ {t('sni')}
+ </TableHead>
+ <TableCell>
+ {entity.sni}
+ </TableCell>
+ </TableRow>
+ <TableRow>
+ <TableHead>
+ {t('alpn')}
+ </TableHead>
+ <TableCell>
+ {entity.alpn}
+ </TableCell>
+ </TableRow>
+ </TableBody>
+ </Table>
+ </SheetContent>
+ </Sheet>
+ )
}
diff --git a/dashboard/src/features/hosts/index.ts b/dashboard/src/features/hosts/index.ts
index 9207d3f..db31fdb 100644
--- a/dashboard/src/features/hosts/index.ts
+++ b/dashboard/src/features/hosts/index.ts
@@ -3,3 +3,4 @@ export * from './tables'
export * from './schema'
export * from './services'
export * from './dialogs'
+export * from './router-context'
diff --git a/dashboard/src/features/hosts/router-context/index.ts b/dashboard/src/features/hosts/router-context/index.ts
new file mode 100644
index 0000000..53cab01
--- /dev/null
+++ b/dashboard/src/features/hosts/router-context/index.ts
@@ -0,0 +1,13 @@
+import { createContext, useContext } from "react";
+import { HostType } from "@marzneshin/features/hosts";
+
+interface RouterHostContextProps {
+ host: HostType;
+}
+
+export const RouterHostContext = createContext<RouterHostContextProps | null>(null);
+
+export const useRouterHostContext = () => {
+ const ctx = useContext(RouterHostContext);
+ return ctx;
+};
diff --git a/dashboard/src/features/hosts/services/host.query.ts b/dashboard/src/features/hosts/services/host.query.ts
new file mode 100644
index 0000000..27f50ab
--- /dev/null
+++ b/dashboard/src/features/hosts/services/host.query.ts
@@ -0,0 +1,17 @@
+import { HostType } from "@marzneshin/features/hosts";
+import { useQuery } from "@tanstack/react-query";
+import { fetch } from "@marzneshin/utils";
+
+export async function fetchHost({ queryKey }: { queryKey: [string, number] }): Promise<HostType> {
+ return fetch(`/inbounds/hosts/${queryKey[1]}`);
+}
+
+export const HostQueryFetchKey = "hosts";
+
+export const useHostQuery = ({ hostId }: { hostId: number }) => {
+ return useQuery({
+ queryKey: [HostQueryFetchKey, hostId],
+ queryFn: fetchHost,
+ initialData: undefined
+ })
+}
diff --git a/dashboard/src/features/hosts/services/hosts.query.ts b/dashboard/src/features/hosts/services/hosts.query.ts
index 2390e82..0c90a44 100644
--- a/dashboard/src/features/hosts/services/hosts.query.ts
+++ b/dashboard/src/features/hosts/services/hosts.query.ts
@@ -2,7 +2,7 @@ import { HostType } from "@marzneshin/features/hosts";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@marzneshin/utils";
-export async function fetchHosts({ queryKey }: { queryKey: [string, number | undefined] }): Promise<HostType[]> {
+export async function fetchHosts({ queryKey }: { queryKey: [string, number | undefined, string] }): Promise<HostType[]> {
if (queryKey[1]) {
return fetch(`/inbounds/${queryKey[1]}/hosts`).then((result) => {
return result.items;
@@ -18,7 +18,7 @@ export const HostsQueryFetchKey = "hosts";
export const useHostsQuery = (inboundId?: number | undefined) => {
return useQuery({
- queryKey: [HostsQueryFetchKey, inboundId],
+ queryKey: ["inbounds",inboundId,HostsQueryFetchKey ],
queryFn: fetchHosts,
initialData: []
})
diff --git a/dashboard/src/features/hosts/services/inbound.query.ts b/dashboard/src/features/hosts/services/inbound.query.ts
new file mode 100644
index 0000000..cfed55d
--- /dev/null
+++ b/dashboard/src/features/hosts/services/inbound.query.ts
@@ -0,0 +1,17 @@
+import { InboundType } from "@marzneshin/features/inbounds";
+import { useQuery } from "@tanstack/react-query";
+import { fetch } from "@marzneshin/utils";
+
+export async function fetchInbound({ queryKey }: { queryKey: [string, number] }): Promise<InboundType> {
+ return fetch(`/inbounds/${queryKey[1]}`).then((result) => result);
+}
+
+export const InboundsQueryFetchKey = "inbounds";
+
+export const useInboundQuery = ({ inboundId }: { inboundId: number }) => {
+ return useQuery({
+ queryKey: [InboundsQueryFetchKey, inboundId],
+ queryFn: fetchInbound,
+ initialData: undefined
+ })
+}
diff --git a/dashboard/src/features/hosts/services/index.ts b/dashboard/src/features/hosts/services/index.ts
index 4c2442e..b91631e 100644
--- a/dashboard/src/features/hosts/services/index.ts
+++ b/dashboard/src/features/hosts/services/index.ts
@@ -1,4 +1,6 @@
export * from './hosts.query'
+export * from './host.query'
+export * from './inbound.query'
export * from './delete-host.mutate'
export * from './create-host.mutate'
export * from './update-host.mutate'
diff --git a/dashboard/src/features/hosts/tables/inbound-hosts/index.tsx b/dashboard/src/features/hosts/tables/inbound-hosts/index.tsx
index 7cce230..95477d2 100644
--- a/dashboard/src/features/hosts/tables/inbound-hosts/index.tsx
+++ b/dashboard/src/features/hosts/tables/inbound-hosts/index.tsx
@@ -1,15 +1,13 @@
import {
HostType,
- HostsDeleteConfirmationDialog,
- HostsMutationDialog,
useHostsQuery
} from '@marzneshin/features/hosts'
import { InboundHostsDataTable } from './table'
+import { useNavigate } from "@tanstack/react-router"
import { useState } from 'react'
import { useInboundsQuery } from '@marzneshin/features/inbounds'
import { columns } from './columns'
-import { HostSettingsDialog } from '../../dialogs/settings'
import { useDialog } from '@marzneshin/hooks'
import { InboundNotSelectedAlertDialog } from './inbound-not-selected-alert-dialog'
@@ -17,59 +15,32 @@ export const InboundHostsTable = () => {
const { data: inbounds } = useInboundsQuery()
const [selectedInbound, setSelectedInbound] = useState<string | undefined>(inbounds[0]?.id !== undefined ? String(inbounds[0].id) : undefined)
const { data, isLoading } = useHostsQuery(Number(selectedInbound))
-
- const [mutationDialogOpen, setMutationDialogOpen] = useDialog();
- const [deleteDialogOpen, setDeleteDialogOpen] = useDialog();
- const [settingsDialogOpen, setSettingsDialogOpen] = useDialog();
+ const navigate = useNavigate({ from: "/hosts" })
const [inboundSelectionAlert, setInboundSelectionAlert] = useDialog();
- const [selectedEntity, selectEntity] = useState<HostType | null>(null);
- const onEdit = (entity: HostType) => {
- selectEntity(entity);
- setMutationDialogOpen(true);
- };
-
- const onDelete = (entity: HostType) => {
- selectEntity(entity);
- setDeleteDialogOpen(true);
- };
+ const onEdit = (entity: HostType) => navigate({ to: "/hosts/$hostId/edit", params: { hostId: String(entity.id) } });
+ const onDelete = (entity: HostType) => navigate({ to: "/hosts/$hostId/delete", params: { hostId: String(entity.id) } });
+ const onOpen = (entity: HostType) => navigate({ to: "/hosts/$hostId", params: { hostId: String(entity.id) } });
const onCreate = () => {
if (selectedInbound) {
- selectEntity(null);
- setMutationDialogOpen(true);
+ navigate({
+ to: "/hosts/$inboundId/create",
+ params: {
+ inboundId: selectedInbound,
+ }
+ })
} else {
setInboundSelectionAlert(true)
}
};
- const onOpen = (entity: HostType) => {
- selectEntity(entity);
- setSettingsDialogOpen(true);
- };
-
return (
<div>
<InboundNotSelectedAlertDialog
open={inboundSelectionAlert}
onOpenChange={setInboundSelectionAlert}
/>
- <HostSettingsDialog
- open={settingsDialogOpen}
- onOpenChange={setSettingsDialogOpen}
- entity={selectedEntity}
- />
- <HostsDeleteConfirmationDialog
- open={deleteDialogOpen}
- onOpenChange={setDeleteDialogOpen}
- entity={selectedEntity}
- />
- <HostsMutationDialog
- open={mutationDialogOpen}
- onOpenChange={setMutationDialogOpen}
- entity={selectedEntity}
- inboundId={Number(selectedInbound)}
- />
<InboundHostsDataTable
data={data}
inbounds={inbounds}
diff --git a/dashboard/src/features/hosts/tables/inbound-hosts/table.tsx b/dashboard/src/features/hosts/tables/inbound-hosts/table.tsx
index e72f570..b8db56b 100644
--- a/dashboard/src/features/hosts/tables/inbound-hosts/table.tsx
+++ b/dashboard/src/features/hosts/tables/inbound-hosts/table.tsx
@@ -164,7 +164,7 @@ export function InboundHostsDataTable<TData, TValue>({
</Table>
</div>
- <DataTablePagination table={table} />
+ <DataTablePagination table={table} disableSelected={true} />
</div>
</div >
)
diff --git a/dashboard/src/features/nodes/dialogs/delete-confirmation.dialog.tsx b/dashboard/src/features/nodes/dialogs/delete-confirmation.dialog.tsx
deleted file mode 100644
index 19fc101..0000000
--- a/dashboard/src/features/nodes/dialogs/delete-confirmation.dialog.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-
-import { DeleteConfirmation } from '@marzneshin/components'
-import { FC } from 'react'
-import { NodeType, useNodesDeletionMutation } from '@marzneshin/features/nodes'
-
-interface NodesDeleteConfirmationDialogProps {
- onOpenChange: (state: boolean) => void
- open: boolean
- entity: NodeType | null
-}
-
-export const NodesDeleteConfirmationDialog: FC<NodesDeleteConfirmationDialogProps> = ({ onOpenChange, open, entity }) => {
- const deleteMutation = useNodesDeletionMutation();
- if (entity !== null) {
- return (
- <DeleteConfirmation
- open={open}
- onOpenChange={onOpenChange}
- action={() => deleteMutation.mutate(entity)}
- />
- )
- }
-}
diff --git a/dashboard/src/features/nodes/dialogs/delete-confirmation/index.tsx b/dashboard/src/features/nodes/dialogs/delete-confirmation/index.tsx
new file mode 100644
index 0000000..2e76558
--- /dev/null
+++ b/dashboard/src/features/nodes/dialogs/delete-confirmation/index.tsx
@@ -0,0 +1,29 @@
+
+import { DeleteConfirmation } from '@marzneshin/components'
+import { FC, useEffect } from 'react'
+import { NodeType, useNodesDeletionMutation } from '@marzneshin/features/nodes'
+
+interface NodesDeleteConfirmationDialogProps {
+ onOpenChange: (state: boolean) => void
+ open: boolean
+ entity: NodeType
+ onClose: () => void
+}
+
+export const NodesDeleteConfirmationDialog: FC<NodesDeleteConfirmationDialogProps> = (
+ { onOpenChange, open, entity, onClose }
+) => {
+ const deleteMutation = useNodesDeletionMutation();
+
+ useEffect(() => {
+ if (!open) onClose();
+ }, [open, onClose]);
+
+ return (
+ <DeleteConfirmation
+ open={open}
+ onOpenChange={onOpenChange}
+ action={() => deleteMutation.mutate(entity)}
+ />
+ )
+}
diff --git a/dashboard/src/features/nodes/dialogs/index.ts b/dashboard/src/features/nodes/dialogs/index.ts
index a1f7b43..933e0cf 100644
--- a/dashboard/src/features/nodes/dialogs/index.ts
+++ b/dashboard/src/features/nodes/dialogs/index.ts
@@ -1,3 +1,3 @@
-export * from './mutation.dialog'
-export * from './delete-confirmation.dialog'
+export * from './mutation'
+export * from './delete-confirmation'
export * from './settings'
diff --git a/dashboard/src/features/nodes/dialogs/mutation.dialog.tsx b/dashboard/src/features/nodes/dialogs/mutation.dialog.tsx
deleted file mode 100644
index 0cc245f..0000000
--- a/dashboard/src/features/nodes/dialogs/mutation.dialog.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-import type { FC } from "react";
-import {
- DialogTitle,
- DialogContent,
- Dialog,
- DialogHeader,
- Form,
- FormItem,
- FormControl,
- FormMessage,
- FormLabel,
- Input,
- FormField,
- Button,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- FormDescription,
- HStack,
-} from "@marzneshin/components";
-import { useTranslation } from "react-i18next";
-import {
- NodeSchema,
- useNodesCreationMutation,
- useNodesUpdateMutation,
-} from "..";
-import type { NodeType } from "..";
-import { useMutationDialog } from "@marzneshin/hooks";
-
-interface MutationDialogProps {
- entity: NodeType | null;
- open: boolean;
- onOpenChange: (state: boolean) => void;
-}
-
-const getDefaultValue = (): NodeType => ({
- name: "",
- address: "",
- status: "none",
- port: 62050,
- usage_coefficient: 1,
- connection_backend: "grpclib",
-});
-
-export const MutationDialog: FC<MutationDialogProps> = ({
- entity,
- open,
- onOpenChange,
-}) => {
- const updateMutation = useNodesUpdateMutation();
- const createMutation = useNodesCreationMutation();
- const { t } = useTranslation();
- const { form, handleSubmit } = useMutationDialog({
- onOpenChange,
- entity,
- schema: NodeSchema,
- createMutation,
- updateMutation,
- getDefaultValue,
- });
- return (
- <Dialog open={open} onOpenChange={onOpenChange} defaultOpen={true}>
- <DialogContent>
- <DialogHeader>
- <DialogTitle className="text-primary">
- {entity
- ? t("page.nodes.dialogs.edition.title")
- : t("page.nodes.dialogs.creation.title")}
- </DialogTitle>
- </DialogHeader>
- <Form {...form}>
- <form onSubmit={handleSubmit}>
- <FormField
- control={form.control}
- name="name"
- render={({ field }) => (
- <FormItem className="w-full">
- <FormLabel>{t("name")}</FormLabel>
- <FormControl>
- <Input {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <div className="flex flex-row gap-2 items-center">
- <FormField
- control={form.control}
- name="address"
- render={({ field }) => (
- <FormItem className="w-2/3">
- <FormLabel>{t("address")}</FormLabel>
- <FormControl>
- <Input {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="port"
- render={({ field }) => (
- <FormItem className="w-1/3">
- <FormLabel>{t("port")}</FormLabel>
- <FormControl>
- <Input type="number" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
- <HStack>
- <FormField
- control={form.control}
- name="connection_backend"
- render={({ field }) => (
- <FormItem className="w-full">
- <FormLabel>{t("page.nodes.connection_backend")}</FormLabel>
- <Select
- onValueChange={field.onChange}
- defaultValue={field.value}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="Nodes Connection" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="grpclib">grpclib</SelectItem>
- <SelectItem value="grpcio">grpcio</SelectItem>
- </SelectContent>
- </Select>
- <FormDescription>
- {t("page.nodes.connection_backend_desc")}
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="usage_coefficient"
- render={({ field }) => (
- <FormItem className="w-1/2">
- <FormLabel>{t("page.nodes.usage_coefficient")}</FormLabel>
- <FormControl>
- <Input {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </HStack>
- <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/nodes/dialogs/mutation/index.tsx b/dashboard/src/features/nodes/dialogs/mutation/index.tsx
new file mode 100644
index 0000000..4724305
--- /dev/null
+++ b/dashboard/src/features/nodes/dialogs/mutation/index.tsx
@@ -0,0 +1,177 @@
+import { type FC, useEffect } from "react";
+import {
+ DialogTitle,
+ DialogContent,
+ Dialog,
+ DialogHeader,
+ Form,
+ FormItem,
+ FormControl,
+ FormMessage,
+ FormLabel,
+ Input,
+ FormField,
+ Button,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+ FormDescription,
+ HStack,
+} from "@marzneshin/components";
+import { useTranslation } from "react-i18next";
+import {
+ NodeSchema,
+ useNodesCreationMutation,
+ useNodesUpdateMutation,
+} from "../..";
+import type { NodeType } from "../..";
+import { useMutationDialog } 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> = ({
+ entity,
+ open,
+ onOpenChange,
+ onClose,
+}) => {
+ const updateMutation = useNodesUpdateMutation();
+ const createMutation = useNodesCreationMutation();
+ const { t } = useTranslation();
+ const { form, handleSubmit } = useMutationDialog({
+ onOpenChange,
+ entity,
+ schema: NodeSchema,
+ createMutation,
+ updateMutation,
+ getDefaultValue,
+ });
+
+ useEffect(() => {
+ if (!open) onClose();
+ }, [open, onClose]);
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange} defaultOpen={true}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle className="text-primary">
+ {entity
+ ? t("page.nodes.dialogs.edition.title")
+ : t("page.nodes.dialogs.creation.title")}
+ </DialogTitle>
+ </DialogHeader>
+ <Form {...form}>
+ <form onSubmit={handleSubmit}>
+ <FormField
+ control={form.control}
+ name="name"
+ render={({ field }) => (
+ <FormItem className="w-full">
+ <FormLabel>{t("name")}</FormLabel>
+ <FormControl>
+ <Input {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <div className="flex flex-row gap-2 items-center">
+ <FormField
+ control={form.control}
+ name="address"
+ render={({ field }) => (
+ <FormItem className="w-2/3">
+ <FormLabel>{t("address")}</FormLabel>
+ <FormControl>
+ <Input {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="port"
+ render={({ field }) => (
+ <FormItem className="w-1/3">
+ <FormLabel>{t("port")}</FormLabel>
+ <FormControl>
+ <Input type="number" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ <HStack>
+ <FormField
+ control={form.control}
+ name="connection_backend"
+ render={({ field }) => (
+ <FormItem className="w-full">
+ <FormLabel>{t("page.nodes.connection_backend")}</FormLabel>
+ <Select
+ onValueChange={field.onChange}
+ defaultValue={field.value}
+ >
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="Nodes Connection" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="grpclib">grpclib</SelectItem>
+ <SelectItem value="grpcio">grpcio</SelectItem>
+ </SelectContent>
+ </Select>
+ <FormDescription>
+ {t("page.nodes.connection_backend_desc")}
+ </FormDescription>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="usage_coefficient"
+ render={({ field }) => (
+ <FormItem className="w-1/2">
+ <FormLabel>{t("page.nodes.usage_coefficient")}</FormLabel>
+ <FormControl>
+ <Input {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </HStack>
+ <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/nodes/dialogs/settings/index.tsx b/dashboard/src/features/nodes/dialogs/settings/index.tsx
index db94495..992c20b 100644
--- a/dashboard/src/features/nodes/dialogs/settings/index.tsx
+++ b/dashboard/src/features/nodes/dialogs/settings/index.tsx
@@ -11,7 +11,7 @@ import {
TabsList,
TabsTrigger,
} from "@marzneshin/components";
-import type { FC } from "react";
+import { type FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import type { NodeType } from "../..";
import { LogContainer } from "../../log";
@@ -22,15 +22,21 @@ interface NodesSettingsDialogProps {
onOpenChange: (state: boolean) => void;
open: boolean;
entity: NodeType;
+ onClose: () => void;
}
export const NodesSettingsDialog: FC<NodesSettingsDialogProps> = ({
onOpenChange,
open,
entity,
+ onClose,
}) => {
const { t } = useTranslation();
+ useEffect(() => {
+ if (!open) onClose();
+ }, [open, onClose]);
+
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="sm:min-w-full md:min-w-[600px] ">
diff --git a/dashboard/src/features/nodes/index.ts b/dashboard/src/features/nodes/index.ts
index 701b533..bd7acfa 100644
--- a/dashboard/src/features/nodes/index.ts
+++ b/dashboard/src/features/nodes/index.ts
@@ -2,3 +2,4 @@ export * from './types';
export * from './tables';
export * from './services';
export * from './dialogs';
+export * from './router-context';
diff --git a/dashboard/src/features/nodes/router-context/index.ts b/dashboard/src/features/nodes/router-context/index.ts
new file mode 100644
index 0000000..d5a3369
--- /dev/null
+++ b/dashboard/src/features/nodes/router-context/index.ts
@@ -0,0 +1,13 @@
+import { createContext, useContext } from "react";
+import { NodeType } from "@marzneshin/features/nodes";
+
+interface RouterNodeContextProps {
+ node: NodeType;
+}
+
+export const RouterNodeContext = createContext<RouterNodeContextProps | null>(null);
+
+export const useRouterNodeContext = () => {
+ const ctx = useContext(RouterNodeContext);
+ return ctx;
+};
diff --git a/dashboard/src/features/nodes/services/index.ts b/dashboard/src/features/nodes/services/index.ts
index 21a7339..912979a 100644
--- a/dashboard/src/features/nodes/services/index.ts
+++ b/dashboard/src/features/nodes/services/index.ts
@@ -1,4 +1,5 @@
export * from './nodes.query'
+export * from './node.query'
export * from './logs.socket'
export * from './settings.query'
export * from './settings.mutate'
diff --git a/dashboard/src/features/nodes/services/node.query.ts b/dashboard/src/features/nodes/services/node.query.ts
new file mode 100644
index 0000000..b97c3d2
--- /dev/null
+++ b/dashboard/src/features/nodes/services/node.query.ts
@@ -0,0 +1,19 @@
+import { useQuery } from "@tanstack/react-query";
+import { fetch } from "@marzneshin/utils";
+import type { NodeType } from "../types";
+
+export async function fetchNode({
+ queryKey,
+}: { queryKey: [string, number] }): Promise<NodeType> {
+ return await fetch(`/nodes/${queryKey[1]}`).then(result => result);
+}
+
+export const NodeQueryFetchKey = "nodes";
+
+export const useNodeQuery = ({ nodeId }: { nodeId: number }) => {
+ return useQuery({
+ queryKey: [NodeQueryFetchKey, nodeId],
+ queryFn: fetchNode,
+ initialData: null,
+ });
+};
diff --git a/dashboard/src/features/nodes/tables/table.tsx b/dashboard/src/features/nodes/tables/table.tsx
index cd39ad8..f33b2c1 100644
--- a/dashboard/src/features/nodes/tables/table.tsx
+++ b/dashboard/src/features/nodes/tables/table.tsx
@@ -1,23 +1,38 @@
import { FC } from "react";
import {
columns,
- NodesSettingsDialog,
fetchNodes,
} from '@marzneshin/features/nodes';
-import { MutationDialog } from "../dialogs/mutation.dialog";
-import { NodesDeleteConfirmationDialog } from "../dialogs/delete-confirmation.dialog";
import { EntityTable } from "@marzneshin/components";
+import { useNavigate } from "@tanstack/react-router";
export const NodesTable: FC = () => {
+ const navigate = useNavigate({ from: "/nodes" });
return (
<EntityTable
fetchEntity={fetchNodes}
- MutationDialog={MutationDialog}
- DeleteConfirmationDialog={NodesDeleteConfirmationDialog}
- SettingsDialog={NodesSettingsDialog}
columnsFn={columns}
filteredColumn="name"
entityKey="nodes"
+ onCreate={() => navigate({ to: "/nodes/create" })}
+ onOpen={(entity) =>
+ navigate({
+ to: "/nodes/$nodeId",
+ params: { nodeId: String(entity.id) },
+ })
+ }
+ onEdit={(entity) =>
+ navigate({
+ to: "/nodes/$nodeId/edit",
+ params: { nodeId: String(entity.id) },
+ })
+ }
+ onDelete={(entity) =>
+ navigate({
+ to: "/nodes/$nodeId/delete",
+ params: { nodeId: String(entity.id) },
+ })
+ }
/>
)
}
diff --git a/dashboard/src/features/services/dialogs/delete-confirmation.dialog.tsx b/dashboard/src/features/services/dialogs/delete-confirmation.dialog.tsx
index aca3c59..a946c29 100644
--- a/dashboard/src/features/services/dialogs/delete-confirmation.dialog.tsx
+++ b/dashboard/src/features/services/dialogs/delete-confirmation.dialog.tsx
@@ -1,25 +1,25 @@
import { DeleteConfirmation } from '@marzneshin/components'
-import { FC } from 'react'
+import { type FC, useEffect } from 'react'
import { ServiceType, useServicesDeletionMutation } from '@marzneshin/features/services'
interface ServicesDeleteConfirmationDialogProps {
onOpenChange: (state: boolean) => void
open: boolean
- entity: ServiceType | null
+ entity: ServiceType
+ onClose: () => void;
}
-export const ServicesDeleteConfirmationDialog: FC<ServicesDeleteConfirmationDialogProps> = ({ onOpenChange, open, entity }) => {
+export const ServicesDeleteConfirmationDialog: FC<ServicesDeleteConfirmationDialogProps> = ({ onOpenChange, open, entity, onClose }) => {
const deleteMutation = useServicesDeletionMutation();
- if (entity !== null) {
- return (
- <DeleteConfirmation
- open={open}
- onOpenChange={onOpenChange}
- action={() => deleteMutation.mutate(entity)}
- />
- )
- } else {
- return null
- }
+ useEffect(() => {
+ if (!open) onClose();
+ }, [open, onClose]);
+ return (
+ <DeleteConfirmation
+ open={open}
+ onOpenChange={onOpenChange}
+ action={() => deleteMutation.mutate(entity)}
+ />
+ )
}
diff --git a/dashboard/src/features/services/dialogs/index.ts b/dashboard/src/features/services/dialogs/index.ts
index 9e47895..624d2f6 100644
--- a/dashboard/src/features/services/dialogs/index.ts
+++ b/dashboard/src/features/services/dialogs/index.ts
@@ -1,2 +1,3 @@
export * from "./delete-confirmation.dialog";
export * from "./mutation";
+export * from "./settings.dialog"
diff --git a/dashboard/src/features/services/dialogs/mutation/index.tsx b/dashboard/src/features/services/dialogs/mutation/index.tsx
index 910bea4..f912154 100644
--- a/dashboard/src/features/services/dialogs/mutation/index.tsx
+++ b/dashboard/src/features/services/dialogs/mutation/index.tsx
@@ -6,7 +6,7 @@ import {
Form,
Button,
} from "@marzneshin/components";
-import type { FC } from "react";
+import { type FC, useEffect } from "react";
import {
type ServiceType,
useServicesCreationMutation,
@@ -31,6 +31,7 @@ interface MutationDialogProps {
entity: ServiceType | null;
open: boolean;
onOpenChange: (state: boolean) => void;
+ onClose: () => void;
}
const getDefaultValues = (): ServiceCreateType => ({
@@ -42,6 +43,7 @@ export const MutationDialog: FC<MutationDialogProps> = ({
entity,
open,
onOpenChange,
+ onClose,
}) => {
const isEditing = entity !== null;
const updateMutation = useServicesUpdateMutation();
@@ -55,6 +57,11 @@ export const MutationDialog: FC<MutationDialogProps> = ({
onOpenChange,
});
const { t } = useTranslation();
+
+ useEffect(() => {
+ if (!open) onClose();
+ }, [open, onClose]);
+
return (
<Dialog open={open} onOpenChange={onOpenChange} defaultOpen={true}>
<DialogContent>
diff --git a/dashboard/src/features/services/dialogs/settings.dialog.tsx b/dashboard/src/features/services/dialogs/settings.dialog.tsx
index 4c9f26b..e2f575a 100644
--- a/dashboard/src/features/services/dialogs/settings.dialog.tsx
+++ b/dashboard/src/features/services/dialogs/settings.dialog.tsx
@@ -8,7 +8,7 @@ import {
TabsList,
TabsTrigger,
} from "@marzneshin/components"
-import { FC } from "react"
+import { type FC, useEffect } from "react"
import { useTranslation } from "react-i18next"
import { ServiceType } from "..";
import { ServiceInboundsTable } from "../tables/inbounds";
@@ -17,36 +17,40 @@ import { ServicesUsersTable } from "../tables/users";
interface ServicecSettingsDialogProps {
onOpenChange: (state: boolean) => void
open: boolean
- entity: ServiceType | null
+ entity: ServiceType
+ onClose: () => void;
}
-export const ServiceSettingsDialog: FC<ServicecSettingsDialogProps> = ({ onOpenChange, open, entity }) => {
-
+export const ServiceSettingsDialog: FC<ServicecSettingsDialogProps> = ({
+ onOpenChange, open, entity, onClose
+}) => {
const { t } = useTranslation();
- if (entity) {
- return (
- <Sheet open={open} onOpenChange={onOpenChange}>
- <SheetContent className="sm:min-w-full md:min-w-[700px]" >
- <SheetHeader>
- <SheetTitle>
- {t('page.services.settings.title')}
- </SheetTitle>
- </SheetHeader>
- <Tabs className="my-3 w-full h-full" defaultValue="inbounds">
- <TabsList className="w-full">
- <TabsTrigger className="w-full" value="inbounds">{t('inbounds')}</TabsTrigger>
- <TabsTrigger className="w-full" value="users">{t('users')}</TabsTrigger>
- </TabsList>
- <TabsContent value="inbounds" className="h-full">
- <ServiceInboundsTable service={entity} />
- </TabsContent>
- <TabsContent value="users" className="h-full">
- <ServicesUsersTable service={entity} />
- </TabsContent>
- </Tabs>
- </SheetContent>
- </Sheet>
- )
- }
+ useEffect(() => {
+ if (!open) onClose();
+ }, [open, onClose]);
+
+ return (
+ <Sheet open={open} onOpenChange={onOpenChange}>
+ <SheetContent className="sm:min-w-full md:min-w-[700px]" >
+ <SheetHeader>
+ <SheetTitle>
+ {t('page.services.settings.title')}
+ </SheetTitle>
+ </SheetHeader>
+ <Tabs className="my-3 w-full h-full" defaultValue="inbounds">
+ <TabsList className="w-full">
+ <TabsTrigger className="w-full" value="inbounds">{t('inbounds')}</TabsTrigger>
+ <TabsTrigger className="w-full" value="users">{t('users')}</TabsTrigger>
+ </TabsList>
+ <TabsContent value="inbounds" className="h-full">
+ <ServiceInboundsTable service={entity} />
+ </TabsContent>
+ <TabsContent value="users" className="h-full">
+ <ServicesUsersTable service={entity} />
+ </TabsContent>
+ </Tabs>
+ </SheetContent>
+ </Sheet>
+ )
}
diff --git a/dashboard/src/features/services/index.ts b/dashboard/src/features/services/index.ts
index 962370d..95ecaf6 100644
--- a/dashboard/src/features/services/index.ts
+++ b/dashboard/src/features/services/index.ts
@@ -2,3 +2,4 @@ export * from './types'
export * from './tables'
export * from './services'
export * from './dialogs'
+export * from './router-context'
diff --git a/dashboard/src/features/services/router-context/index.ts b/dashboard/src/features/services/router-context/index.ts
new file mode 100644
index 0000000..99b8c16
--- /dev/null
+++ b/dashboard/src/features/services/router-context/index.ts
@@ -0,0 +1,13 @@
+import { createContext, useContext } from "react";
+import { ServiceType } from "@marzneshin/features/services";
+
+interface RouterServiceContextProps {
+ service: ServiceType;
+}
+
+export const RouterServiceContext = createContext<RouterServiceContextProps | null>(null);
+
+export const useRouterServiceContext = () => {
+ const ctx = useContext(RouterServiceContext);
+ return ctx;
+};
diff --git a/dashboard/src/features/services/services/index.ts b/dashboard/src/features/services/services/index.ts
index c0abd61..4739311 100644
--- a/dashboard/src/features/services/services/index.ts
+++ b/dashboard/src/features/services/services/index.ts
@@ -1,4 +1,5 @@
-export * from './services.query.ts'
-export * from './create-service.mutate.ts'
-export * from './delete-service.mutate.ts'
-export * from './update-service.mutate.ts'
+export * from './services.query'
+export * from './service.query'
+export * from './create-service.mutate'
+export * from './delete-service.mutate'
+export * from './update-service.mutate'
diff --git a/dashboard/src/features/services/services/service.query.ts b/dashboard/src/features/services/services/service.query.ts
new file mode 100644
index 0000000..e1e4442
--- /dev/null
+++ b/dashboard/src/features/services/services/service.query.ts
@@ -0,0 +1,25 @@
+import { useQuery } from "@tanstack/react-query";
+import { fetch } from "@marzneshin/utils";
+import type { ServiceType } from "../types";
+
+export async function fetchService({
+ queryKey,
+}: { queryKey: [string, number] }): Promise<ServiceType> {
+ return await fetch(`/services/${queryKey[1]}`).then(result => {
+ return {
+ ...result,
+ users: result.user_ids,
+ inbounds: result.inbound_ids,
+ };
+ });
+}
+
+export const ServiceQueryFetchKey = "services";
+
+export const useServiceQuery = ({ serviceId }: { serviceId: number }) => {
+ return useQuery({
+ queryKey: [ServiceQueryFetchKey, serviceId],
+ queryFn: fetchService,
+ initialData: null,
+ });
+};
diff --git a/dashboard/src/features/services/tables/services/index.tsx b/dashboard/src/features/services/tables/services/index.tsx
index c225d01..15dacfb 100644
--- a/dashboard/src/features/services/tables/services/index.tsx
+++ b/dashboard/src/features/services/tables/services/index.tsx
@@ -1,24 +1,39 @@
-import { FC } from "react";
+import type { FC } from "react";
import {
- MutationDialog,
- ServicesDeleteConfirmationDialog,
- ServicesQueryFetchKey,
- fetchServices
-} from '@marzneshin/features/services';
+ ServicesQueryFetchKey,
+ fetchServices,
+} from "@marzneshin/features/services";
import { columns } from "./columns";
-import { ServiceSettingsDialog } from "../../dialogs/settings.dialog";
import { EntityTable } from "@marzneshin/components";
+import { useNavigate } from "@tanstack/react-router";
export const ServicesTable: FC = () => {
- return (
- <EntityTable
- fetchEntity={fetchServices}
- DeleteConfirmationDialog={ServicesDeleteConfirmationDialog}
- SettingsDialog={ServiceSettingsDialog}
- MutationDialog={MutationDialog}
- columnsFn={columns}
- filteredColumn="name"
- entityKey={ServicesQueryFetchKey}
- />
- )
-}
+ const navigate = useNavigate({ from: "/services" });
+ return (
+ <EntityTable
+ fetchEntity={fetchServices}
+ columnsFn={columns}
+ filteredColumn="name"
+ entityKey={ServicesQueryFetchKey}
+ onCreate={() => navigate({ to: "/services/create" })}
+ onOpen={(entity) =>
+ navigate({
+ to: "/services/$serviceId",
+ params: { serviceId: String(entity.id) },
+ })
+ }
+ onEdit={(entity) =>
+ navigate({
+ to: "/services/$serviceId/edit",
+ params: { serviceId: String(entity.id) },
+ })
+ }
+ onDelete={(entity) =>
+ navigate({
+ to: "/services/$serviceId/delete",
+ params: { serviceId: String(entity.id) },
+ })
+ }
+ />
+ );
+};
diff --git a/dashboard/src/features/services/tables/users/index.tsx b/dashboard/src/features/services/tables/users/index.tsx
index 4fb842f..aa5a219 100644
--- a/dashboard/src/features/services/tables/users/index.tsx
+++ b/dashboard/src/features/services/tables/users/index.tsx
@@ -13,12 +13,10 @@ export const ServicesUsersTable: FC<ServicesUsersTableProps> = ({ service }) =>
const { data } = useUsersServiceQuery({ serviceId: service.id });
return (
- <div>
- <DataTable
- columns={columns}
- data={data}
- filteredColumn='username'
- />
- </div>
+ <DataTable
+ columns={columns}
+ data={data}
+ filteredColumn='username'
+ />
)
}
diff --git a/dashboard/src/features/sidebar/sidebar.tsx b/dashboard/src/features/sidebar/sidebar.tsx
index 81e4066..2650519 100644
--- a/dashboard/src/features/sidebar/sidebar.tsx
+++ b/dashboard/src/features/sidebar/sidebar.tsx
@@ -23,6 +23,12 @@ export const DashboardSidebar: FC<DashboardSidebarProps> = ({
}) => {
const router = useRouterState();
const currentActivePath = router.location.pathname;
+ const isActive = (path: string) => {
+ if (path === "/") {
+ return currentActivePath === path;
+ }
+ return currentActivePath.startsWith(path);
+ };
return (
<aside className="size-full py-4 px-4 ">
@@ -41,9 +47,7 @@ export const DashboardSidebar: FC<DashboardSidebarProps> = ({
<Sidebar.Group>{key}</Sidebar.Group>
{sidebarItems[key].map((item: SidebarItem) => (
<Sidebar.Item
- variant={
- currentActivePath === item.to ? "active" : "default"
- }
+ variant={isActive(item.to) ? "active" : "default"}
className={cn("my-2 border-transparent", {
"w-10 h-10": collapsed,
})}
diff --git a/dashboard/src/features/users/components/services-card.tsx b/dashboard/src/features/users/components/services-card.tsx
index 093b655..8d21b36 100644
--- a/dashboard/src/features/users/components/services-card.tsx
+++ b/dashboard/src/features/users/components/services-card.tsx
@@ -11,15 +11,13 @@ interface ServiceCardProps {
export const ServiceCard: FC<ServiceCardProps> = (
{ service }
) => {
- if (service.user_ids && service.inbound_ids) {
- return (
- <EntityFieldCard
- FirstIcon={UserIcon}
- SecondIcon={ServerIcon}
- firstAmount={service.user_ids.length}
- secondAmount={service.inbound_ids.length}
- name={service.name}
- />
- )
- }
+ return (service.user_ids && service.inbound_ids) && (
+ <EntityFieldCard
+ FirstIcon={UserIcon}
+ SecondIcon={ServerIcon}
+ firstAmount={service.user_ids.length}
+ secondAmount={service.inbound_ids.length}
+ name={service.name}
+ />
+ )
};
diff --git a/dashboard/src/features/users/dialogs/delete-confirmation/index.tsx b/dashboard/src/features/users/dialogs/delete-confirmation/index.tsx
index 949d139..05109c7 100644
--- a/dashboard/src/features/users/dialogs/delete-confirmation/index.tsx
+++ b/dashboard/src/features/users/dialogs/delete-confirmation/index.tsx
@@ -1,23 +1,31 @@
-
-import { DeleteConfirmation } from '@marzneshin/components'
-import { FC } from 'react'
-import { UserMutationType, useUsersDeletionMutation } from '@marzneshin/features/users'
+import { DeleteConfirmation } from "@marzneshin/components";
+import { type FC, useEffect } from "react";
+import {
+ type UserMutationType,
+ useUsersDeletionMutation,
+} from "@marzneshin/features/users";
interface UsersDeleteConfirmationDialogProps {
- onOpenChange: (state: boolean) => void
- open: boolean
- entity: UserMutationType | null
+ onOpenChange: (state: boolean) => void;
+ open: boolean;
+ entity: UserMutationType;
+ onClose: () => void;
}
-export const UsersDeleteConfirmationDialog: FC<UsersDeleteConfirmationDialogProps> = ({ onOpenChange, open, entity }) => {
+export const UsersDeleteConfirmationDialog: FC<
+ UsersDeleteConfirmationDialogProps
+> = ({ onOpenChange, open, entity, onClose }) => {
const deleteMutation = useUsersDeletionMutation();
- if (entity !== null) {
- return (
- <DeleteConfirmation
- open={open}
- onOpenChange={onOpenChange}
- action={() => deleteMutation.mutate(entity)}
- />
- )
- }
-}
+
+ useEffect(() => {
+ if (!open) onClose();
+ }, [open, onClose]);
+
+ return (
+ <DeleteConfirmation
+ open={open}
+ onOpenChange={onOpenChange}
+ action={() => deleteMutation.mutate(entity)}
+ />
+ );
+};
diff --git a/dashboard/src/features/users/dialogs/mutation/index.tsx b/dashboard/src/features/users/dialogs/mutation/index.tsx
index 4fd0669..03e09d1 100644
--- a/dashboard/src/features/users/dialogs/mutation/index.tsx
+++ b/dashboard/src/features/users/dialogs/mutation/index.tsx
@@ -1,5 +1,4 @@
-import type { FC } from "react";
-import { useCallback } from "react";
+import { type FC, useEffect, useCallback } from "react";
import {
DialogTitle,
DialogContent,
@@ -27,12 +26,14 @@ interface UsersMutationDialogProps {
entity: UserMutationType | null;
open: boolean;
onOpenChange: (state: boolean) => void;
+ onClose: () => void;
}
export const UsersMutationDialog: FC<UsersMutationDialogProps> = ({
entity,
open,
onOpenChange,
+ onClose,
}) => {
const { t } = useTranslation();
const getDefaultValues = useCallback(
@@ -63,6 +64,10 @@ export const UsersMutationDialog: FC<UsersMutationDialogProps> = ({
}),
});
+ 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/dialogs/settings/index.tsx b/dashboard/src/features/users/dialogs/settings/index.tsx
index d7d861b..b3c0b6d 100644
--- a/dashboard/src/features/users/dialogs/settings/index.tsx
+++ b/dashboard/src/features/users/dialogs/settings/index.tsx
@@ -11,7 +11,7 @@ import {
} from "@marzneshin/components";
import { UserServicesTable } from "@marzneshin/features/users";
import type { UserType } from "@marzneshin/features/users";
-import type { FC } from "react";
+import { type FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { QRCodeSection, UserInfoTable } from "./user-info";
import { SubscriptionActions } from "./user-info/subscription-actions";
@@ -19,50 +19,54 @@ import { SubscriptionActions } from "./user-info/subscription-actions";
interface UsersSettingsDialogProps {
onOpenChange: (state: boolean) => void;
open: boolean;
- entity: UserType | null;
+ entity: UserType;
+ onClose: () => void;
}
export const UsersSettingsDialog: FC<UsersSettingsDialogProps> = ({
onOpenChange,
open,
entity,
+ onClose,
}) => {
const { t } = useTranslation();
- if (entity) {
- return (
- <Sheet open={open} onOpenChange={onOpenChange}>
- <SheetContent className="sm:min-w-full md:min-w-[700px]">
- <ScrollArea className="flex flex-col gap-4 m-3 h-full">
- <SheetHeader className="my-1">
- <SheetTitle>{t("settings")}</SheetTitle>
- </SheetHeader>
- <Tabs defaultValue="info" className="w-full h-full">
- <TabsList className="w-full bg-accent">
- <TabsTrigger className="w-full" value="info">
- {" "}
- {t("user_info")}{" "}
- </TabsTrigger>
- <TabsTrigger className="w-full" value="services">
- {" "}
- {t("services")}{" "}
- </TabsTrigger>
- </TabsList>
- <TabsContent
- value="info"
- className="flex flex-col gap-2 w-full h-full"
- >
- <UserInfoTable entity={entity} />
- <QRCodeSection entity={entity} />
- <SubscriptionActions entity={entity} />
- </TabsContent>
- <TabsContent value="services">
- <UserServicesTable user={entity} />
- </TabsContent>
- </Tabs>
- </ScrollArea>
- </SheetContent>
- </Sheet>
- );
- }
+ useEffect(() => {
+ if (!open) onClose();
+ }, [open, onClose]);
+
+ return (
+ <Sheet open={open} onOpenChange={onOpenChange}>
+ <SheetContent className="sm:min-w-full md:min-w-[700px]">
+ <ScrollArea className="flex flex-col gap-4 m-3 h-full">
+ <SheetHeader className="my-1">
+ <SheetTitle>{t("settings")}</SheetTitle>
+ </SheetHeader>
+ <Tabs defaultValue="info" className="w-full h-full">
+ <TabsList className="w-full bg-accent">
+ <TabsTrigger className="w-full" value="info">
+ {" "}
+ {t("user_info")}{" "}
+ </TabsTrigger>
+ <TabsTrigger className="w-full" value="services">
+ {" "}
+ {t("services")}{" "}
+ </TabsTrigger>
+ </TabsList>
+ <TabsContent
+ value="info"
+ className="flex flex-col gap-2 w-full h-full"
+ >
+ <UserInfoTable entity={entity} />
+ <QRCodeSection entity={entity} />
+ <SubscriptionActions entity={entity} />
+ </TabsContent>
+ <TabsContent value="services">
+ <UserServicesTable user={entity} />
+ </TabsContent>
+ </Tabs>
+ </ScrollArea>
+ </SheetContent>
+ </Sheet>
+ );
};
diff --git a/dashboard/src/features/users/index.ts b/dashboard/src/features/users/index.ts
index c312335..80e6fe7 100644
--- a/dashboard/src/features/users/index.ts
+++ b/dashboard/src/features/users/index.ts
@@ -5,3 +5,4 @@ export * from './components'
export * from './dialogs'
export * from './constants'
export * from './widgets'
+export * from './router-context'
diff --git a/dashboard/src/features/users/router-context/index.ts b/dashboard/src/features/users/router-context/index.ts
new file mode 100644
index 0000000..9102f6d
--- /dev/null
+++ b/dashboard/src/features/users/router-context/index.ts
@@ -0,0 +1,13 @@
+import { createContext, useContext } from "react";
+import { UserType } from "@marzneshin/features/users";
+
+interface RouterUserContextProps {
+ user: UserType;
+}
+
+export const RouterUserContext = createContext<RouterUserContextProps | null>(null);
+
+export const useRouterUserContext = () => {
+ const ctx = useContext(RouterUserContext);
+ return ctx;
+};
diff --git a/dashboard/src/features/users/services/index.ts b/dashboard/src/features/users/services/index.ts
index 6a9df23..e4e7373 100644
--- a/dashboard/src/features/users/services/index.ts
+++ b/dashboard/src/features/users/services/index.ts
@@ -1,4 +1,5 @@
export * from "./users.query";
+export * from "./user.query";
export * from "./users-stats.query";
export * from "./update-user.mutate";
export * from "./create-user.mutate";
diff --git a/dashboard/src/features/users/services/user.query.ts b/dashboard/src/features/users/services/user.query.ts
new file mode 100644
index 0000000..4fcd087
--- /dev/null
+++ b/dashboard/src/features/users/services/user.query.ts
@@ -0,0 +1,19 @@
+import { useQuery } from "@tanstack/react-query";
+import { fetch } from "@marzneshin/utils";
+import type { UserType } from "../types";
+
+export async function fetchUser({
+ queryKey,
+}: { queryKey: [string, string] }): Promise<UserType> {
+ return fetch(`/users/${queryKey[1]}`);
+}
+
+export const UserQueryFetchKey = "users";
+
+export const useUserQuery = ({ username }: { username: string }) => {
+ return useQuery({
+ queryKey: [UserQueryFetchKey, username],
+ queryFn: fetchUser,
+ initialData: null,
+ });
+};
diff --git a/dashboard/src/features/users/tables/users/index.tsx b/dashboard/src/features/users/tables/users/index.tsx
index ec893e8..7f11d5c 100644
--- a/dashboard/src/features/users/tables/users/index.tsx
+++ b/dashboard/src/features/users/tables/users/index.tsx
@@ -1,26 +1,37 @@
-
-import { FC } from "react";
-import {
- UsersMutationDialog,
- UsersDeleteConfirmationDialog,
- UsersSettingsDialog,
- fetchUsers,
- UsersQueryFetchKey
-} from '@marzneshin/features/users';
-import { columns } from './columns'
+import type { FC } from "react";
+import { fetchUsers, UsersQueryFetchKey } from "@marzneshin/features/users";
+import { columns } from "./columns";
import { EntityTable } from "@marzneshin/components";
+import { useNavigate } from "@tanstack/react-router";
export const UsersTable: FC = () => {
+ const navigate = useNavigate({ from: "/users" });
return (
<EntityTable
fetchEntity={fetchUsers}
manualSorting={true}
- MutationDialog={UsersMutationDialog}
- DeleteConfirmationDialog={UsersDeleteConfirmationDialog}
- SettingsDialog={UsersSettingsDialog}
columnsFn={columns}
filteredColumn="username"
entityKey={UsersQueryFetchKey}
+ onCreate={() => navigate({ to: "/users/create" })}
+ onOpen={(entity) =>
+ navigate({
+ to: "/users/$userId",
+ params: { userId: entity.username },
+ })
+ }
+ onEdit={(entity) =>
+ navigate({
+ to: "/users/$userId/edit",
+ params: { userId: entity.username },
+ })
+ }
+ onDelete={(entity) =>
+ navigate({
+ to: "/users/$userId/delete",
+ params: { userId: entity.username },
+ })
+ }
/>
- )
-}
+ );
+};
diff --git a/dashboard/src/hooks/use-dialog.ts b/dashboard/src/hooks/use-dialog.ts
index abe52ee..54dbd13 100644
--- a/dashboard/src/hooks/use-dialog.ts
+++ b/dashboard/src/hooks/use-dialog.ts
@@ -1,3 +1,2 @@
-
import { useState } from "react";
-export const useDialog = () => useState<boolean>(false)
+export const useDialog = (open = false) => useState<boolean>(open);
diff --git a/dashboard/src/routeTree.gen.ts b/dashboard/src/routeTree.gen.ts
index 50b62ce..2211048 100644
--- a/dashboard/src/routeTree.gen.ts
+++ b/dashboard/src/routeTree.gen.ts
@@ -8,18 +8,43 @@
// This file is auto-generated by TanStack Router
+import { createFileRoute } from '@tanstack/react-router'
+
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as DashboardImport } from './routes/_dashboard'
import { Route as AuthImport } from './routes/_auth'
-import { Route as DashboardIndexImport } from './routes/_dashboard/index'
-import { Route as DashboardUsersImport } from './routes/_dashboard/users'
-import { Route as DashboardSettingsImport } from './routes/_dashboard/settings'
-import { Route as DashboardServicesImport } from './routes/_dashboard/services'
-import { Route as DashboardNodesImport } from './routes/_dashboard/nodes'
-import { Route as DashboardHostsImport } from './routes/_dashboard/hosts'
import { Route as AuthLoginImport } from './routes/_auth/login'
+import { Route as DashboardUsersCreateImport } from './routes/_dashboard/users/create'
+import { Route as DashboardUsersUserIdImport } from './routes/_dashboard/users/$userId'
+import { Route as DashboardServicesCreateImport } from './routes/_dashboard/services/create'
+import { Route as DashboardServicesServiceIdImport } from './routes/_dashboard/services/$serviceId'
+import { Route as DashboardNodesCreateImport } from './routes/_dashboard/nodes/create'
+import { Route as DashboardNodesNodeIdImport } from './routes/_dashboard/nodes/$nodeId'
+import { Route as DashboardHostsHostIdImport } from './routes/_dashboard/hosts/$hostId'
+import { Route as DashboardUsersUserIdIndexImport } from './routes/_dashboard/users/$userId/index'
+import { Route as DashboardServicesServiceIdIndexImport } from './routes/_dashboard/services/$serviceId/index'
+import { Route as DashboardNodesNodeIdIndexImport } from './routes/_dashboard/nodes/$nodeId/index'
+import { Route as DashboardHostsHostIdIndexImport } from './routes/_dashboard/hosts/$hostId/index'
+import { Route as DashboardUsersUserIdEditImport } from './routes/_dashboard/users/$userId/edit'
+import { Route as DashboardUsersUserIdDeleteImport } from './routes/_dashboard/users/$userId/delete'
+import { Route as DashboardServicesServiceIdEditImport } from './routes/_dashboard/services/$serviceId/edit'
+import { Route as DashboardServicesServiceIdDeleteImport } from './routes/_dashboard/services/$serviceId/delete'
+import { Route as DashboardNodesNodeIdEditImport } from './routes/_dashboard/nodes/$nodeId/edit'
+import { Route as DashboardNodesNodeIdDeleteImport } from './routes/_dashboard/nodes/$nodeId/delete'
+import { Route as DashboardHostsInboundIdCreateImport } from './routes/_dashboard/hosts/$inboundId/create'
+import { Route as DashboardHostsHostIdEditImport } from './routes/_dashboard/hosts/$hostId/edit'
+import { Route as DashboardHostsHostIdDeleteImport } from './routes/_dashboard/hosts/$hostId/delete'
+
+// Create Virtual Routes
+
+const DashboardIndexLazyImport = createFileRoute('/_dashboard/')()
+const DashboardUsersLazyImport = createFileRoute('/_dashboard/users')()
+const DashboardSettingsLazyImport = createFileRoute('/_dashboard/settings')()
+const DashboardServicesLazyImport = createFileRoute('/_dashboard/services')()
+const DashboardNodesLazyImport = createFileRoute('/_dashboard/nodes')()
+const DashboardHostsLazyImport = createFileRoute('/_dashboard/hosts')()
// Create/Update Routes
@@ -33,41 +58,165 @@ const AuthRoute = AuthImport.update({
getParentRoute: () => rootRoute,
} as any)
-const DashboardIndexRoute = DashboardIndexImport.update({
+const DashboardIndexLazyRoute = DashboardIndexLazyImport.update({
path: '/',
getParentRoute: () => DashboardRoute,
-} as any)
+} as any).lazy(() =>
+ import('./routes/_dashboard/index.lazy').then((d) => d.Route),
+)
-const DashboardUsersRoute = DashboardUsersImport.update({
+const DashboardUsersLazyRoute = DashboardUsersLazyImport.update({
path: '/users',
getParentRoute: () => DashboardRoute,
-} as any)
+} as any).lazy(() =>
+ import('./routes/_dashboard/users.lazy').then((d) => d.Route),
+)
-const DashboardSettingsRoute = DashboardSettingsImport.update({
+const DashboardSettingsLazyRoute = DashboardSettingsLazyImport.update({
path: '/settings',
getParentRoute: () => DashboardRoute,
-} as any)
+} as any).lazy(() =>
+ import('./routes/_dashboard/settings.lazy').then((d) => d.Route),
+)
-const DashboardServicesRoute = DashboardServicesImport.update({
+const DashboardServicesLazyRoute = DashboardServicesLazyImport.update({
path: '/services',
getParentRoute: () => DashboardRoute,
-} as any)
+} as any).lazy(() =>
+ import('./routes/_dashboard/services.lazy').then((d) => d.Route),
+)
-const DashboardNodesRoute = DashboardNodesImport.update({
+const DashboardNodesLazyRoute = DashboardNodesLazyImport.update({
path: '/nodes',
getParentRoute: () => DashboardRoute,
-} as any)
+} as any).lazy(() =>
+ import('./routes/_dashboard/nodes.lazy').then((d) => d.Route),
+)
-const DashboardHostsRoute = DashboardHostsImport.update({
+const DashboardHostsLazyRoute = DashboardHostsLazyImport.update({
path: '/hosts',
getParentRoute: () => DashboardRoute,
-} as any)
+} as any).lazy(() =>
+ import('./routes/_dashboard/hosts.lazy').then((d) => d.Route),
+)
const AuthLoginRoute = AuthLoginImport.update({
path: '/login',
getParentRoute: () => AuthRoute,
} as any)
+const DashboardUsersCreateRoute = DashboardUsersCreateImport.update({
+ path: '/create',
+ getParentRoute: () => DashboardUsersLazyRoute,
+} as any)
+
+const DashboardUsersUserIdRoute = DashboardUsersUserIdImport.update({
+ path: '/$userId',
+ getParentRoute: () => DashboardUsersLazyRoute,
+} as any)
+
+const DashboardServicesCreateRoute = DashboardServicesCreateImport.update({
+ path: '/create',
+ getParentRoute: () => DashboardServicesLazyRoute,
+} as any)
+
+const DashboardServicesServiceIdRoute = DashboardServicesServiceIdImport.update(
+ {
+ path: '/$serviceId',
+ getParentRoute: () => DashboardServicesLazyRoute,
+ } as any,
+)
+
+const DashboardNodesCreateRoute = DashboardNodesCreateImport.update({
+ path: '/create',
+ getParentRoute: () => DashboardNodesLazyRoute,
+} as any)
+
+const DashboardNodesNodeIdRoute = DashboardNodesNodeIdImport.update({
+ path: '/$nodeId',
+ getParentRoute: () => DashboardNodesLazyRoute,
+} as any)
+
+const DashboardHostsHostIdRoute = DashboardHostsHostIdImport.update({
+ path: '/$hostId',
+ getParentRoute: () => DashboardHostsLazyRoute,
+} as any)
+
+const DashboardUsersUserIdIndexRoute = DashboardUsersUserIdIndexImport.update({
+ path: '/',
+ getParentRoute: () => DashboardUsersUserIdRoute,
+} as any)
+
+const DashboardServicesServiceIdIndexRoute =
+ DashboardServicesServiceIdIndexImport.update({
+ path: '/',
+ getParentRoute: () => DashboardServicesServiceIdRoute,
+ } as any)
+
+const DashboardNodesNodeIdIndexRoute = DashboardNodesNodeIdIndexImport.update({
+ path: '/',
+ getParentRoute: () => DashboardNodesNodeIdRoute,
+} as any)
+
+const DashboardHostsHostIdIndexRoute = DashboardHostsHostIdIndexImport.update({
+ path: '/',
+ getParentRoute: () => DashboardHostsHostIdRoute,
+} as any)
+
+const DashboardUsersUserIdEditRoute = DashboardUsersUserIdEditImport.update({
+ path: '/edit',
+ getParentRoute: () => DashboardUsersUserIdRoute,
+} as any)
+
+const DashboardUsersUserIdDeleteRoute = DashboardUsersUserIdDeleteImport.update(
+ {
+ path: '/delete',
+ getParentRoute: () => DashboardUsersUserIdRoute,
+ } as any,
+)
+
+const DashboardServicesServiceIdEditRoute =
+ DashboardServicesServiceIdEditImport.update({
+ path: '/edit',
+ getParentRoute: () => DashboardServicesServiceIdRoute,
+ } as any)
+
+const DashboardServicesServiceIdDeleteRoute =
+ DashboardServicesServiceIdDeleteImport.update({
+ path: '/delete',
+ getParentRoute: () => DashboardServicesServiceIdRoute,
+ } as any)
+
+const DashboardNodesNodeIdEditRoute = DashboardNodesNodeIdEditImport.update({
+ path: '/edit',
+ getParentRoute: () => DashboardNodesNodeIdRoute,
+} as any)
+
+const DashboardNodesNodeIdDeleteRoute = DashboardNodesNodeIdDeleteImport.update(
+ {
+ path: '/delete',
+ getParentRoute: () => DashboardNodesNodeIdRoute,
+ } as any,
+)
+
+const DashboardHostsInboundIdCreateRoute =
+ DashboardHostsInboundIdCreateImport.update({
+ path: '/$inboundId/create',
+ getParentRoute: () => DashboardHostsLazyRoute,
+ } as any)
+
+const DashboardHostsHostIdEditRoute = DashboardHostsHostIdEditImport.update({
+ path: '/edit',
+ getParentRoute: () => DashboardHostsHostIdRoute,
+} as any)
+
+const DashboardHostsHostIdDeleteRoute = DashboardHostsHostIdDeleteImport.update(
+ {
+ path: '/delete',
+ getParentRoute: () => DashboardHostsHostIdRoute,
+ } as any,
+)
+
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
@@ -97,44 +246,184 @@ declare module '@tanstack/react-router' {
id: '/_dashboard/hosts'
path: '/hosts'
fullPath: '/hosts'
- preLoaderRoute: typeof DashboardHostsImport
+ preLoaderRoute: typeof DashboardHostsLazyImport
parentRoute: typeof DashboardImport
}
'/_dashboard/nodes': {
id: '/_dashboard/nodes'
path: '/nodes'
fullPath: '/nodes'
- preLoaderRoute: typeof DashboardNodesImport
+ preLoaderRoute: typeof DashboardNodesLazyImport
parentRoute: typeof DashboardImport
}
'/_dashboard/services': {
id: '/_dashboard/services'
path: '/services'
fullPath: '/services'
- preLoaderRoute: typeof DashboardServicesImport
+ preLoaderRoute: typeof DashboardServicesLazyImport
parentRoute: typeof DashboardImport
}
'/_dashboard/settings': {
id: '/_dashboard/settings'
path: '/settings'
fullPath: '/settings'
- preLoaderRoute: typeof DashboardSettingsImport
+ preLoaderRoute: typeof DashboardSettingsLazyImport
parentRoute: typeof DashboardImport
}
'/_dashboard/users': {
id: '/_dashboard/users'
path: '/users'
fullPath: '/users'
- preLoaderRoute: typeof DashboardUsersImport
+ preLoaderRoute: typeof DashboardUsersLazyImport
parentRoute: typeof DashboardImport
}
'/_dashboard/': {
id: '/_dashboard/'
path: '/'
fullPath: '/'
- preLoaderRoute: typeof DashboardIndexImport
+ preLoaderRoute: typeof DashboardIndexLazyImport
parentRoute: typeof DashboardImport
}
+ '/_dashboard/hosts/$hostId': {
+ id: '/_dashboard/hosts/$hostId'
+ path: '/$hostId'
+ fullPath: '/hosts/$hostId'
+ preLoaderRoute: typeof DashboardHostsHostIdImport
+ parentRoute: typeof DashboardHostsLazyImport
+ }
+ '/_dashboard/nodes/$nodeId': {
+ id: '/_dashboard/nodes/$nodeId'
+ path: '/$nodeId'
+ fullPath: '/nodes/$nodeId'
+ preLoaderRoute: typeof DashboardNodesNodeIdImport
+ parentRoute: typeof DashboardNodesLazyImport
+ }
+ '/_dashboard/nodes/create': {
+ id: '/_dashboard/nodes/create'
+ path: '/create'
+ fullPath: '/nodes/create'
+ preLoaderRoute: typeof DashboardNodesCreateImport
+ parentRoute: typeof DashboardNodesLazyImport
+ }
+ '/_dashboard/services/$serviceId': {
+ id: '/_dashboard/services/$serviceId'
+ path: '/$serviceId'
+ fullPath: '/services/$serviceId'
+ preLoaderRoute: typeof DashboardServicesServiceIdImport
+ parentRoute: typeof DashboardServicesLazyImport
+ }
+ '/_dashboard/services/create': {
+ id: '/_dashboard/services/create'
+ path: '/create'
+ fullPath: '/services/create'
+ preLoaderRoute: typeof DashboardServicesCreateImport
+ parentRoute: typeof DashboardServicesLazyImport
+ }
+ '/_dashboard/users/$userId': {
+ id: '/_dashboard/users/$userId'
+ path: '/$userId'
+ fullPath: '/users/$userId'
+ preLoaderRoute: typeof DashboardUsersUserIdImport
+ parentRoute: typeof DashboardUsersLazyImport
+ }
+ '/_dashboard/users/create': {
+ id: '/_dashboard/users/create'
+ path: '/create'
+ fullPath: '/users/create'
+ preLoaderRoute: typeof DashboardUsersCreateImport
+ parentRoute: typeof DashboardUsersLazyImport
+ }
+ '/_dashboard/hosts/$hostId/delete': {
+ id: '/_dashboard/hosts/$hostId/delete'
+ path: '/delete'
+ fullPath: '/hosts/$hostId/delete'
+ preLoaderRoute: typeof DashboardHostsHostIdDeleteImport
+ parentRoute: typeof DashboardHostsHostIdImport
+ }
+ '/_dashboard/hosts/$hostId/edit': {
+ id: '/_dashboard/hosts/$hostId/edit'
+ path: '/edit'
+ fullPath: '/hosts/$hostId/edit'
+ preLoaderRoute: typeof DashboardHostsHostIdEditImport
+ parentRoute: typeof DashboardHostsHostIdImport
+ }
+ '/_dashboard/hosts/$inboundId/create': {
+ id: '/_dashboard/hosts/$inboundId/create'
+ path: '/$inboundId/create'
+ fullPath: '/hosts/$inboundId/create'
+ preLoaderRoute: typeof DashboardHostsInboundIdCreateImport
+ parentRoute: typeof DashboardHostsLazyImport
+ }
+ '/_dashboard/nodes/$nodeId/delete': {
+ id: '/_dashboard/nodes/$nodeId/delete'
+ path: '/delete'
+ fullPath: '/nodes/$nodeId/delete'
+ preLoaderRoute: typeof DashboardNodesNodeIdDeleteImport
+ parentRoute: typeof DashboardNodesNodeIdImport
+ }
+ '/_dashboard/nodes/$nodeId/edit': {
+ id: '/_dashboard/nodes/$nodeId/edit'
+ path: '/edit'
+ fullPath: '/nodes/$nodeId/edit'
+ preLoaderRoute: typeof DashboardNodesNodeIdEditImport
+ parentRoute: typeof DashboardNodesNodeIdImport
+ }
+ '/_dashboard/services/$serviceId/delete': {
+ id: '/_dashboard/services/$serviceId/delete'
+ path: '/delete'
+ fullPath: '/services/$serviceId/delete'
+ preLoaderRoute: typeof DashboardServicesServiceIdDeleteImport
+ parentRoute: typeof DashboardServicesServiceIdImport
+ }
+ '/_dashboard/services/$serviceId/edit': {
+ id: '/_dashboard/services/$serviceId/edit'
+ path: '/edit'
+ fullPath: '/services/$serviceId/edit'
+ preLoaderRoute: typeof DashboardServicesServiceIdEditImport
+ parentRoute: typeof DashboardServicesServiceIdImport
+ }
+ '/_dashboard/users/$userId/delete': {
+ id: '/_dashboard/users/$userId/delete'
+ path: '/delete'
+ fullPath: '/users/$userId/delete'
+ preLoaderRoute: typeof DashboardUsersUserIdDeleteImport
+ parentRoute: typeof DashboardUsersUserIdImport
+ }
+ '/_dashboard/users/$userId/edit': {
+ id: '/_dashboard/users/$userId/edit'
+ path: '/edit'
+ fullPath: '/users/$userId/edit'
+ preLoaderRoute: typeof DashboardUsersUserIdEditImport
+ parentRoute: typeof DashboardUsersUserIdImport
+ }
+ '/_dashboard/hosts/$hostId/': {
+ id: '/_dashboard/hosts/$hostId/'
+ path: '/'
+ fullPath: '/hosts/$hostId/'
+ preLoaderRoute: typeof DashboardHostsHostIdIndexImport
+ parentRoute: typeof DashboardHostsHostIdImport
+ }
+ '/_dashboard/nodes/$nodeId/': {
+ id: '/_dashboard/nodes/$nodeId/'
+ path: '/'
+ fullPath: '/nodes/$nodeId/'
+ preLoaderRoute: typeof DashboardNodesNodeIdIndexImport
+ parentRoute: typeof DashboardNodesNodeIdImport
+ }
+ '/_dashboard/services/$serviceId/': {
+ id: '/_dashboard/services/$serviceId/'
+ path: '/'
+ fullPath: '/services/$serviceId/'
+ preLoaderRoute: typeof DashboardServicesServiceIdIndexImport
+ parentRoute: typeof DashboardServicesServiceIdImport
+ }
+ '/_dashboard/users/$userId/': {
+ id: '/_dashboard/users/$userId/'
+ path: '/'
+ fullPath: '/users/$userId/'
+ preLoaderRoute: typeof DashboardUsersUserIdIndexImport
+ parentRoute: typeof DashboardUsersUserIdImport
+ }
}
}
@@ -143,12 +432,41 @@ declare module '@tanstack/react-router' {
export const routeTree = rootRoute.addChildren({
AuthRoute: AuthRoute.addChildren({ AuthLoginRoute }),
DashboardRoute: DashboardRoute.addChildren({
- DashboardHostsRoute,
- DashboardNodesRoute,
- DashboardServicesRoute,
- DashboardSettingsRoute,
- DashboardUsersRoute,
- DashboardIndexRoute,
+ DashboardHostsLazyRoute: DashboardHostsLazyRoute.addChildren({
+ DashboardHostsHostIdRoute: DashboardHostsHostIdRoute.addChildren({
+ DashboardHostsHostIdDeleteRoute,
+ DashboardHostsHostIdEditRoute,
+ DashboardHostsHostIdIndexRoute,
+ }),
+ DashboardHostsInboundIdCreateRoute,
+ }),
+ DashboardNodesLazyRoute: DashboardNodesLazyRoute.addChildren({
+ DashboardNodesNodeIdRoute: DashboardNodesNodeIdRoute.addChildren({
+ DashboardNodesNodeIdDeleteRoute,
+ DashboardNodesNodeIdEditRoute,
+ DashboardNodesNodeIdIndexRoute,
+ }),
+ DashboardNodesCreateRoute,
+ }),
+ DashboardServicesLazyRoute: DashboardServicesLazyRoute.addChildren({
+ DashboardServicesServiceIdRoute:
+ DashboardServicesServiceIdRoute.addChildren({
+ DashboardServicesServiceIdDeleteRoute,
+ DashboardServicesServiceIdEditRoute,
+ DashboardServicesServiceIdIndexRoute,
+ }),
+ DashboardServicesCreateRoute,
+ }),
+ DashboardSettingsLazyRoute,
+ DashboardUsersLazyRoute: DashboardUsersLazyRoute.addChildren({
+ DashboardUsersUserIdRoute: DashboardUsersUserIdRoute.addChildren({
+ DashboardUsersUserIdDeleteRoute,
+ DashboardUsersUserIdEditRoute,
+ DashboardUsersUserIdIndexRoute,
+ }),
+ DashboardUsersCreateRoute,
+ }),
+ DashboardIndexLazyRoute,
}),
})
@@ -186,28 +504,144 @@ export const routeTree = rootRoute.addChildren({
"parent": "/_auth"
},
"/_dashboard/hosts": {
- "filePath": "_dashboard/hosts.tsx",
- "parent": "/_dashboard"
+ "filePath": "_dashboard/hosts.lazy.tsx",
+ "parent": "/_dashboard",
+ "children": [
+ "/_dashboard/hosts/$hostId",
+ "/_dashboard/hosts/$inboundId/create"
+ ]
},
"/_dashboard/nodes": {
- "filePath": "_dashboard/nodes.tsx",
- "parent": "/_dashboard"
+ "filePath": "_dashboard/nodes.lazy.tsx",
+ "parent": "/_dashboard",
+ "children": [
+ "/_dashboard/nodes/$nodeId",
+ "/_dashboard/nodes/create"
+ ]
},
"/_dashboard/services": {
- "filePath": "_dashboard/services.tsx",
- "parent": "/_dashboard"
+ "filePath": "_dashboard/services.lazy.tsx",
+ "parent": "/_dashboard",
+ "children": [
+ "/_dashboard/services/$serviceId",
+ "/_dashboard/services/create"
+ ]
},
"/_dashboard/settings": {
- "filePath": "_dashboard/settings.tsx",
+ "filePath": "_dashboard/settings.lazy.tsx",
"parent": "/_dashboard"
},
"/_dashboard/users": {
- "filePath": "_dashboard/users.tsx",
- "parent": "/_dashboard"
+ "filePath": "_dashboard/users.lazy.tsx",
+ "parent": "/_dashboard",
+ "children": [
+ "/_dashboard/users/$userId",
+ "/_dashboard/users/create"
+ ]
},
"/_dashboard/": {
- "filePath": "_dashboard/index.tsx",
+ "filePath": "_dashboard/index.lazy.tsx",
"parent": "/_dashboard"
+ },
+ "/_dashboard/hosts/$hostId": {
+ "filePath": "_dashboard/hosts/$hostId.tsx",
+ "parent": "/_dashboard/hosts",
+ "children": [
+ "/_dashboard/hosts/$hostId/delete",
+ "/_dashboard/hosts/$hostId/edit",
+ "/_dashboard/hosts/$hostId/"
+ ]
+ },
+ "/_dashboard/nodes/$nodeId": {
+ "filePath": "_dashboard/nodes/$nodeId.tsx",
+ "parent": "/_dashboard/nodes",
+ "children": [
+ "/_dashboard/nodes/$nodeId/delete",
+ "/_dashboard/nodes/$nodeId/edit",
+ "/_dashboard/nodes/$nodeId/"
+ ]
+ },
+ "/_dashboard/nodes/create": {
+ "filePath": "_dashboard/nodes/create.tsx",
+ "parent": "/_dashboard/nodes"
+ },
+ "/_dashboard/services/$serviceId": {
+ "filePath": "_dashboard/services/$serviceId.tsx",
+ "parent": "/_dashboard/services",
+ "children": [
+ "/_dashboard/services/$serviceId/delete",
+ "/_dashboard/services/$serviceId/edit",
+ "/_dashboard/services/$serviceId/"
+ ]
+ },
+ "/_dashboard/services/create": {
+ "filePath": "_dashboard/services/create.tsx",
+ "parent": "/_dashboard/services"
+ },
+ "/_dashboard/users/$userId": {
+ "filePath": "_dashboard/users/$userId.tsx",
+ "parent": "/_dashboard/users",
+ "children": [
+ "/_dashboard/users/$userId/delete",
+ "/_dashboard/users/$userId/edit",
+ "/_dashboard/users/$userId/"
+ ]
+ },
+ "/_dashboard/users/create": {
+ "filePath": "_dashboard/users/create.tsx",
+ "parent": "/_dashboard/users"
+ },
+ "/_dashboard/hosts/$hostId/delete": {
+ "filePath": "_dashboard/hosts/$hostId/delete.tsx",
+ "parent": "/_dashboard/hosts/$hostId"
+ },
+ "/_dashboard/hosts/$hostId/edit": {
+ "filePath": "_dashboard/hosts/$hostId/edit.tsx",
+ "parent": "/_dashboard/hosts/$hostId"
+ },
+ "/_dashboard/hosts/$inboundId/create": {
+ "filePath": "_dashboard/hosts/$inboundId/create.tsx",
+ "parent": "/_dashboard/hosts"
+ },
+ "/_dashboard/nodes/$nodeId/delete": {
+ "filePath": "_dashboard/nodes/$nodeId/delete.tsx",
+ "parent": "/_dashboard/nodes/$nodeId"
+ },
+ "/_dashboard/nodes/$nodeId/edit": {
+ "filePath": "_dashboard/nodes/$nodeId/edit.tsx",
+ "parent": "/_dashboard/nodes/$nodeId"
+ },
+ "/_dashboard/services/$serviceId/delete": {
+ "filePath": "_dashboard/services/$serviceId/delete.tsx",
+ "parent": "/_dashboard/services/$serviceId"
+ },
+ "/_dashboard/services/$serviceId/edit": {
+ "filePath": "_dashboard/services/$serviceId/edit.tsx",
+ "parent": "/_dashboard/services/$serviceId"
+ },
+ "/_dashboard/users/$userId/delete": {
+ "filePath": "_dashboard/users/$userId/delete.tsx",
+ "parent": "/_dashboard/users/$userId"
+ },
+ "/_dashboard/users/$userId/edit": {
+ "filePath": "_dashboard/users/$userId/edit.tsx",
+ "parent": "/_dashboard/users/$userId"
+ },
+ "/_dashboard/hosts/$hostId/": {
+ "filePath": "_dashboard/hosts/$hostId/index.tsx",
+ "parent": "/_dashboard/hosts/$hostId"
+ },
+ "/_dashboard/nodes/$nodeId/": {
+ "filePath": "_dashboard/nodes/$nodeId/index.tsx",
+ "parent": "/_dashboard/nodes/$nodeId"
+ },
+ "/_dashboard/services/$serviceId/": {
+ "filePath": "_dashboard/services/$serviceId/index.tsx",
+ "parent": "/_dashboard/services/$serviceId"
+ },
+ "/_dashboard/users/$userId/": {
+ "filePath": "_dashboard/users/$userId/index.tsx",
+ "parent": "/_dashboard/users/$userId"
}
}
}
diff --git a/dashboard/src/routes/__root.tsx b/dashboard/src/routes/__root.tsx
index 8c60b8e..d3b5c55 100644
--- a/dashboard/src/routes/__root.tsx
+++ b/dashboard/src/routes/__root.tsx
@@ -1,4 +1,4 @@
-import { TooltipProvider } from '@marzneshin/components';
+import { TooltipProvider, Loading } from '@marzneshin/components';
import { ThemeProvider } from '@marzneshin/features/theme-switch'
import { queryClient } from '@marzneshin/utils';
import { QueryClientProvider } from '@tanstack/react-query';
@@ -10,7 +10,7 @@ export const Route = createRootRoute({
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="light" storageKey="ui-theme">
<TooltipProvider>
- <Suspense>
+ <Suspense fallback={<Loading />}>
<Outlet />
</Suspense>
</TooltipProvider>
diff --git a/dashboard/src/routes/_dashboard.tsx b/dashboard/src/routes/_dashboard.tsx
index f90087a..b33a568 100644
--- a/dashboard/src/routes/_dashboard.tsx
+++ b/dashboard/src/routes/_dashboard.tsx
@@ -6,13 +6,19 @@ import {
ResizablePanel,
ResizablePanelGroup,
Toaster,
+ Loading
} from "@marzneshin/components";
import { useAuth } from "@marzneshin/features/auth";
import { DashboardSidebar, ToggleButton } from "@marzneshin/features/sidebar";
import { usePanelToggle } from "@marzneshin/features/sidebar/use-panel-toggle";
import { useScreenBreakpoint } from "@marzneshin/hooks/use-screen-breakpoint";
import { cn } from "@marzneshin/utils";
-import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
+import { Suspense } from "react";
+import {
+ Outlet,
+ createFileRoute,
+ redirect
+} from "@tanstack/react-router";
export const DashboardLayout = () => {
const isDesktop = useScreenBreakpoint("md");
@@ -59,7 +65,9 @@ export const DashboardLayout = () => {
<ResizableHandle withHandle className="w-[2px]" />
<ResizablePanel>
<main>
- <Outlet />
+ <Suspense fallback={<Loading />}>
+ <Outlet />
+ </Suspense>
<Toaster position="top-center" />
</main>
</ResizablePanel>
@@ -79,7 +87,9 @@ export const DashboardLayout = () => {
</Drawer>
</aside>
<main className="sm:block">
- <Outlet />
+ <Suspense fallback={<Loading />}>
+ <Outlet />
+ </Suspense>
<Toaster position="top-center" />
</main>
</div>
@@ -90,7 +100,6 @@ export const DashboardLayout = () => {
export const Route = createFileRoute("/_dashboard")({
component: () => <DashboardLayout />,
-
beforeLoad: async () => {
const loggedIn = await useAuth.getState().isLoggedIn();
if (!loggedIn) {
diff --git a/dashboard/src/routes/_dashboard/hosts.tsx b/dashboard/src/routes/_dashboard/hosts.lazy.tsx
similarity index 67%
rename from dashboard/src/routes/_dashboard/hosts.tsx
rename to dashboard/src/routes/_dashboard/hosts.lazy.tsx
index 50e3774..b0902b0 100644
--- a/dashboard/src/routes/_dashboard/hosts.tsx
+++ b/dashboard/src/routes/_dashboard/hosts.lazy.tsx
@@ -1,7 +1,16 @@
-import { Card, CardContent, CardFooter, CardHeader, CardTitle, Page } from '@marzneshin/components'
+import {
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+ Page,
+ Loading,
+} from '@marzneshin/components'
import { InboundHostsTable } from '@marzneshin/features/hosts'
-import { createFileRoute } from '@tanstack/react-router'
+import { createFileRoute, Outlet } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
+import { Suspense } from 'react';
export const HostsPage = () => {
const { t } = useTranslation()
@@ -15,6 +24,9 @@ export const HostsPage = () => {
</CardHeader>
<CardContent>
<InboundHostsTable />
+ <Suspense fallback={<Loading />}>
+ <Outlet />
+ </Suspense>
</CardContent>
<CardFooter>
</CardFooter>
diff --git a/dashboard/src/routes/_dashboard/hosts/$hostId.tsx b/dashboard/src/routes/_dashboard/hosts/$hostId.tsx
new file mode 100644
index 0000000..a2334e3
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/hosts/$hostId.tsx
@@ -0,0 +1,51 @@
+import {
+ createFileRoute,
+ defer,
+ Await,
+ Outlet,
+} from "@tanstack/react-router";
+import {
+ RouterHostContext,
+ fetchHost,
+} from "@marzneshin/features/hosts";
+import { Suspense } from "react";
+import {
+ AlertDialog,
+ AlertDialogContent,
+ Loading
+} from "@marzneshin/components";
+
+const HostProvider = () => {
+ const { host } = Route.useLoaderData()
+ return (
+ <Suspense fallback={<Loading />}>
+ <Await promise={host}>
+ {(host) => (
+ <RouterHostContext.Provider value={{ host }}>
+ <Suspense>
+ <Outlet />
+ </Suspense>
+ </RouterHostContext.Provider>
+ )}
+ </Await>
+ </Suspense>
+ )
+}
+
+export const Route = createFileRoute('/_dashboard/hosts/$hostId')({
+ loader: async ({ params }) => {
+ const hostPromise = fetchHost({
+ queryKey: ["hosts", Number.parseInt(params.hostId)]
+ });
+
+ return {
+ host: defer(hostPromise)
+ }
+ },
+ errorComponent: () => (
+ <AlertDialog open={true}>
+ <AlertDialogContent>Host not found</AlertDialogContent>
+ </AlertDialog>
+ ),
+ component: HostProvider,
+})
diff --git a/dashboard/src/routes/_dashboard/hosts/$hostId/delete.tsx b/dashboard/src/routes/_dashboard/hosts/$hostId/delete.tsx
new file mode 100644
index 0000000..31a0a5d
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/hosts/$hostId/delete.tsx
@@ -0,0 +1,28 @@
+import {
+ createFileRoute,
+ useNavigate,
+} from "@tanstack/react-router";
+import {
+ HostsDeleteConfirmationDialog,
+ useRouterHostContext,
+} from "@marzneshin/features/hosts";
+import { useDialog } from "@marzneshin/hooks";
+
+const HostDelete = () => {
+ const [deleteDialogOpen, setDeleteDialogOpen] = useDialog(true);
+ const value = useRouterHostContext()
+ const navigate = useNavigate({ from: "/hosts/$hostId/delete" });
+
+ return value && (
+ <HostsDeleteConfirmationDialog
+ open={deleteDialogOpen}
+ onOpenChange={setDeleteDialogOpen}
+ entity={value.host}
+ onClose={() => navigate({ to: "/hosts" })}
+ />
+ );
+}
+
+export const Route = createFileRoute('/_dashboard/hosts/$hostId/delete')({
+ component: HostDelete,
+})
diff --git a/dashboard/src/routes/_dashboard/hosts/$hostId/edit.tsx b/dashboard/src/routes/_dashboard/hosts/$hostId/edit.tsx
new file mode 100644
index 0000000..95596f0
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/hosts/$hostId/edit.tsx
@@ -0,0 +1,29 @@
+import {
+ createFileRoute,
+ useNavigate,
+} from "@tanstack/react-router";
+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" })}
+ />
+ );
+}
+
+export const Route = createFileRoute('/_dashboard/hosts/$hostId/edit')({
+ component: HostEdit
+})
diff --git a/dashboard/src/routes/_dashboard/hosts/$hostId/index.tsx b/dashboard/src/routes/_dashboard/hosts/$hostId/index.tsx
new file mode 100644
index 0000000..17f4ef2
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/hosts/$hostId/index.tsx
@@ -0,0 +1,28 @@
+import {
+ createFileRoute,
+ useNavigate,
+} from "@tanstack/react-router";
+import {
+ useRouterHostContext,
+ HostSettingsDialog,
+} from "@marzneshin/features/hosts";
+import { useDialog } from "@marzneshin/hooks";
+
+const HostOpen = () => {
+ const [settingsDialogOpen, setSettingsDialogOpen] = useDialog(true);
+ const value = useRouterHostContext()
+ const navigate = useNavigate({ from: "/hosts/$hostId" });
+
+ return value && (
+ <HostSettingsDialog
+ open={settingsDialogOpen}
+ onOpenChange={setSettingsDialogOpen}
+ entity={value.host}
+ onClose={() => navigate({ to: "/hosts" })}
+ />
+ );
+}
+
+export const Route = createFileRoute('/_dashboard/hosts/$hostId/')({
+ component: HostOpen
+})
diff --git a/dashboard/src/routes/_dashboard/hosts/$inboundId/create.tsx b/dashboard/src/routes/_dashboard/hosts/$inboundId/create.tsx
new file mode 100644
index 0000000..b711498
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/hosts/$inboundId/create.tsx
@@ -0,0 +1,28 @@
+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}
+ inboundId={Number(inboundId)}
+ onClose={() => navigate({ to: "/hosts" })}
+ />
+ );
+}
+
+export const Route = createFileRoute('/_dashboard/hosts/$inboundId/create')({
+ errorComponent: () => (
+ <AlertDialog open={true}>
+ <AlertDialogContent>Host not found</AlertDialogContent>
+ </AlertDialog>
+ ),
+ component: HostCreate,
+});
diff --git a/dashboard/src/routes/_dashboard/index.tsx b/dashboard/src/routes/_dashboard/index.lazy.tsx
similarity index 100%
rename from dashboard/src/routes/_dashboard/index.tsx
rename to dashboard/src/routes/_dashboard/index.lazy.tsx
diff --git a/dashboard/src/routes/_dashboard/nodes.tsx b/dashboard/src/routes/_dashboard/nodes.lazy.tsx
similarity index 85%
rename from dashboard/src/routes/_dashboard/nodes.tsx
rename to dashboard/src/routes/_dashboard/nodes.lazy.tsx
index fbc0315..5e5195d 100644
--- a/dashboard/src/routes/_dashboard/nodes.tsx
+++ b/dashboard/src/routes/_dashboard/nodes.lazy.tsx
@@ -7,12 +7,13 @@ import {
CardFooter,
CardHeader,
CardTitle,
- Page
+ Page,
+ Loading,
} from '@marzneshin/components';
import { NodesTable } from '@marzneshin/features/nodes';
-import { Link, createFileRoute } from '@tanstack/react-router'
+import { Link, createFileRoute, Outlet } from '@tanstack/react-router'
import { Info } from 'lucide-react';
-import { FC } from 'react';
+import { type FC, Suspense } from 'react';
import { useTranslation } from 'react-i18next';
export const NodesPage: FC = () => {
@@ -27,6 +28,9 @@ export const NodesPage: FC = () => {
</CardHeader>
<CardContent>
<NodesTable />
+ <Suspense fallback={<Loading />}>
+ <Outlet />
+ </Suspense>
</CardContent>
<CardFooter>
<Alert>
diff --git a/dashboard/src/routes/_dashboard/nodes/$nodeId.tsx b/dashboard/src/routes/_dashboard/nodes/$nodeId.tsx
new file mode 100644
index 0000000..2daaec4
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/nodes/$nodeId.tsx
@@ -0,0 +1,52 @@
+import {
+ createFileRoute,
+ defer,
+ Await,
+ Outlet
+} from "@tanstack/react-router";
+import {
+ fetchNode,
+ RouterNodeContext
+} from "@marzneshin/features/nodes";
+import { Suspense } from "react";
+import {
+ AlertDialog,
+ AlertDialogContent,
+ Loading,
+} from "@marzneshin/components";
+
+const NodeProvider = () => {
+ const { node } = Route.useLoaderData()
+
+ return (
+ <Suspense fallback={<Loading />}>
+ <Await promise={node}>
+ {(node) => (
+ <RouterNodeContext.Provider value={{ node }}>
+ <Suspense>
+ <Outlet />
+ </Suspense>
+ </RouterNodeContext.Provider>
+ )}
+ </Await>
+ </Suspense>
+ );
+};
+
+export const Route = createFileRoute('/_dashboard/nodes/$nodeId')({
+ loader: async ({ params }) => {
+ const nodePromise = fetchNode({
+ queryKey: ["nodes", Number.parseInt(params.nodeId)]
+ });
+
+ return {
+ node: defer(nodePromise)
+ }
+ },
+ component: NodeProvider,
+ errorComponent: () => (
+ <AlertDialog open={true}>
+ <AlertDialogContent>Node not found</AlertDialogContent>
+ </AlertDialog>
+ ),
+})
diff --git a/dashboard/src/routes/_dashboard/nodes/$nodeId/delete.tsx b/dashboard/src/routes/_dashboard/nodes/$nodeId/delete.tsx
new file mode 100644
index 0000000..586cee4
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/nodes/$nodeId/delete.tsx
@@ -0,0 +1,28 @@
+import {
+ createFileRoute,
+ useNavigate,
+} from "@tanstack/react-router";
+import {
+ NodesDeleteConfirmationDialog,
+ useRouterNodeContext,
+} from "@marzneshin/features/nodes";
+import { useDialog } from "@marzneshin/hooks";
+
+const NodeDelete = () => {
+ const [deleteDialogOpen, setDeleteDialogOpen] = useDialog(true);
+ const value = useRouterNodeContext()
+ const navigate = useNavigate({ from: "/nodes/$nodeId/delete" });
+
+ return value && (
+ <NodesDeleteConfirmationDialog
+ open={deleteDialogOpen}
+ onOpenChange={setDeleteDialogOpen}
+ entity={value.node}
+ onClose={() => navigate({ to: "/nodes" })}
+ />
+ );
+}
+
+export const Route = createFileRoute('/_dashboard/nodes/$nodeId/delete')({
+ component: NodeDelete,
+})
diff --git a/dashboard/src/routes/_dashboard/nodes/$nodeId/edit.tsx b/dashboard/src/routes/_dashboard/nodes/$nodeId/edit.tsx
new file mode 100644
index 0000000..d2af223
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/nodes/$nodeId/edit.tsx
@@ -0,0 +1,29 @@
+import {
+ createFileRoute,
+ useNavigate,
+} from "@tanstack/react-router";
+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" })}
+ />
+ );
+}
+
+export const Route = createFileRoute('/_dashboard/nodes/$nodeId/edit')({
+ component: NodeEdit
+})
diff --git a/dashboard/src/routes/_dashboard/nodes/$nodeId/index.tsx b/dashboard/src/routes/_dashboard/nodes/$nodeId/index.tsx
new file mode 100644
index 0000000..a0505af
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/nodes/$nodeId/index.tsx
@@ -0,0 +1,28 @@
+import {
+ createFileRoute,
+ useNavigate,
+} from "@tanstack/react-router";
+import {
+ useRouterNodeContext,
+ NodesSettingsDialog,
+} from "@marzneshin/features/nodes";
+import { useDialog } from "@marzneshin/hooks";
+
+const NodeOpen = () => {
+ const [settingsDialogOpen, setSettingsDialogOpen] = useDialog(true);
+ const value = useRouterNodeContext()
+ const navigate = useNavigate({ from: "/nodes/$nodeId" });
+
+ return value && (
+ <NodesSettingsDialog
+ open={settingsDialogOpen}
+ onOpenChange={setSettingsDialogOpen}
+ entity={value.node}
+ onClose={() => navigate({ to: "/nodes" })}
+ />
+ );
+}
+
+export const Route = createFileRoute('/_dashboard/nodes/$nodeId/')({
+ component: NodeOpen
+})
diff --git a/dashboard/src/routes/_dashboard/nodes/create.tsx b/dashboard/src/routes/_dashboard/nodes/create.tsx
new file mode 100644
index 0000000..d5121fd
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/nodes/create.tsx
@@ -0,0 +1,20 @@
+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" })}
+ />
+ );
+}
+
+export const Route = createFileRoute("/_dashboard/nodes/create")({
+ component: NodeCreate,
+});
diff --git a/dashboard/src/routes/_dashboard/services.lazy.tsx b/dashboard/src/routes/_dashboard/services.lazy.tsx
new file mode 100644
index 0000000..1c28541
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/services.lazy.tsx
@@ -0,0 +1,35 @@
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ Page,
+ Loading,
+} from "@marzneshin/components";
+import { ServicesTable } from "@marzneshin/features/services";
+import { createFileRoute, Outlet } from "@tanstack/react-router";
+import { type FC, Suspense } from "react";
+import { useTranslation } from "react-i18next";
+
+export const ServicesPage: FC = () => {
+ const { t } = useTranslation();
+ return (
+ <Page>
+ <Card className="border-0 sm:w-screen md:w-full">
+ <CardHeader>
+ <CardTitle>{t("services")}</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <ServicesTable />
+ <Suspense fallback={<Loading />}>
+ <Outlet />
+ </Suspense>
+ </CardContent>
+ </Card>
+ </Page>
+ );
+};
+
+export const Route = createFileRoute("/_dashboard/services")({
+ component: () => <ServicesPage />,
+});
diff --git a/dashboard/src/routes/_dashboard/services.tsx b/dashboard/src/routes/_dashboard/services.tsx
deleted file mode 100644
index ccdbce9..0000000
--- a/dashboard/src/routes/_dashboard/services.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import {
- Card,
- CardContent,
- CardFooter,
- CardHeader,
- CardTitle,
- Page
-} from '@marzneshin/components'
-import { ServicesTable } from '@marzneshin/features/services';
-import { createFileRoute } from '@tanstack/react-router'
-import { FC } from 'react';
-import { useTranslation } from 'react-i18next';
-
-export const ServicesPage: FC = () => {
- const { t } = useTranslation();
- return (
- <Page>
- <Card className="border-0 sm:w-screen md:w-full">
- <CardHeader>
- <CardTitle>
- {t('services')}
- </CardTitle>
- </CardHeader>
- <CardContent>
- <ServicesTable />
- </CardContent>
- <CardFooter>
- </CardFooter>
- </Card >
- </Page>
- )
-};
-
-export const Route = createFileRoute('/_dashboard/services')({
- component: () => <ServicesPage />
-})
diff --git a/dashboard/src/routes/_dashboard/services/$serviceId.tsx b/dashboard/src/routes/_dashboard/services/$serviceId.tsx
new file mode 100644
index 0000000..a9129b5
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/services/$serviceId.tsx
@@ -0,0 +1,51 @@
+import {
+ createFileRoute,
+ defer,
+ Await,
+ Outlet,
+} from "@tanstack/react-router";
+import {
+ RouterServiceContext,
+ fetchService,
+} from "@marzneshin/features/services";
+import { Suspense } from "react";
+import {
+ AlertDialog,
+ AlertDialogContent,
+ Loading
+} from "@marzneshin/components";
+
+const ServiceProvider = () => {
+ const { service } = Route.useLoaderData()
+ return (
+ <Suspense fallback={<Loading />}>
+ <Await promise={service}>
+ {(service) => (
+ <RouterServiceContext.Provider value={{ service }}>
+ <Suspense>
+ <Outlet />
+ </Suspense>
+ </RouterServiceContext.Provider>
+ )}
+ </Await>
+ </Suspense>
+ )
+}
+
+export const Route = createFileRoute('/_dashboard/services/$serviceId')({
+ loader: async ({ params }) => {
+ const servicePromise = fetchService({
+ queryKey: ["services", Number.parseInt(params.serviceId)]
+ });
+
+ return {
+ service: defer(servicePromise)
+ }
+ },
+ errorComponent: () => (
+ <AlertDialog open={true}>
+ <AlertDialogContent>Service not found</AlertDialogContent>
+ </AlertDialog>
+ ),
+ component: ServiceProvider,
+})
diff --git a/dashboard/src/routes/_dashboard/services/$serviceId/delete.tsx b/dashboard/src/routes/_dashboard/services/$serviceId/delete.tsx
new file mode 100644
index 0000000..e5c3d54
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/services/$serviceId/delete.tsx
@@ -0,0 +1,28 @@
+import {
+ createFileRoute,
+ useNavigate,
+} from "@tanstack/react-router";
+import {
+ ServicesDeleteConfirmationDialog,
+ useRouterServiceContext,
+} from "@marzneshin/features/services";
+import { useDialog } from "@marzneshin/hooks";
+
+const ServiceDelete = () => {
+ const [deleteDialogOpen, setDeleteDialogOpen] = useDialog(true);
+ const value = useRouterServiceContext()
+ const navigate = useNavigate({ from: "/services/$serviceId/delete" });
+
+ return value && (
+ <ServicesDeleteConfirmationDialog
+ open={deleteDialogOpen}
+ onOpenChange={setDeleteDialogOpen}
+ entity={value.service}
+ onClose={() => navigate({ to: "/services" })}
+ />
+ );
+}
+
+export const Route = createFileRoute('/_dashboard/services/$serviceId/delete')({
+ component: ServiceDelete,
+})
diff --git a/dashboard/src/routes/_dashboard/services/$serviceId/edit.tsx b/dashboard/src/routes/_dashboard/services/$serviceId/edit.tsx
new file mode 100644
index 0000000..e700264
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/services/$serviceId/edit.tsx
@@ -0,0 +1,29 @@
+import {
+ createFileRoute,
+ useNavigate,
+} from "@tanstack/react-router";
+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" })}
+ />
+ );
+}
+
+export const Route = createFileRoute('/_dashboard/services/$serviceId/edit')({
+ component: ServiceEdit
+})
diff --git a/dashboard/src/routes/_dashboard/services/$serviceId/index.tsx b/dashboard/src/routes/_dashboard/services/$serviceId/index.tsx
new file mode 100644
index 0000000..8c618c4
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/services/$serviceId/index.tsx
@@ -0,0 +1,28 @@
+import {
+ createFileRoute,
+ useNavigate,
+} from "@tanstack/react-router";
+import {
+ useRouterServiceContext,
+ ServiceSettingsDialog,
+} from "@marzneshin/features/services";
+import { useDialog } from "@marzneshin/hooks";
+
+const ServiceOpen = () => {
+ const [settingsDialogOpen, setSettingsDialogOpen] = useDialog(true);
+ const value = useRouterServiceContext()
+ const navigate = useNavigate({ from: "/services/$serviceId" });
+
+ return value && (
+ <ServiceSettingsDialog
+ open={settingsDialogOpen}
+ onOpenChange={setSettingsDialogOpen}
+ entity={value.service}
+ onClose={() => navigate({ to: "/services" })}
+ />
+ );
+}
+
+export const Route = createFileRoute('/_dashboard/services/$serviceId/')({
+ component: ServiceOpen
+})
diff --git a/dashboard/src/routes/_dashboard/services/create.tsx b/dashboard/src/routes/_dashboard/services/create.tsx
new file mode 100644
index 0000000..2d24f45
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/services/create.tsx
@@ -0,0 +1,20 @@
+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" })}
+ />
+ );
+}
+
+export const Route = createFileRoute("/_dashboard/services/create")({
+ component: ServiceCreate,
+});
diff --git a/dashboard/src/routes/_dashboard/settings.tsx b/dashboard/src/routes/_dashboard/settings.lazy.tsx
similarity index 100%
rename from dashboard/src/routes/_dashboard/settings.tsx
rename to dashboard/src/routes/_dashboard/settings.lazy.tsx
diff --git a/dashboard/src/routes/_dashboard/users.tsx b/dashboard/src/routes/_dashboard/users.lazy.tsx
similarity index 85%
rename from dashboard/src/routes/_dashboard/users.tsx
rename to dashboard/src/routes/_dashboard/users.lazy.tsx
index 087f737..d447c5c 100644
--- a/dashboard/src/routes/_dashboard/users.tsx
+++ b/dashboard/src/routes/_dashboard/users.lazy.tsx
@@ -8,12 +8,13 @@ import {
CardHeader,
CardTitle,
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 } from '@tanstack/react-router'
-import { FC } from 'react';
+import { Link, createFileRoute, Outlet } from '@tanstack/react-router'
+import { type FC, Suspense } from 'react';
import { useTranslation } from 'react-i18next';
export const UsersPage: FC = () => {
@@ -29,6 +30,9 @@ export const UsersPage: FC = () => {
</CardHeader>
<CardContent>
<UsersTable />
+ <Suspense fallback={<Loading />}>
+ <Outlet />
+ </Suspense>
</CardContent>
<CardFooter>
{(data && data.pageCount === 0) && (
@@ -49,5 +53,5 @@ export const UsersPage: FC = () => {
export const Route = createFileRoute('/_dashboard/users')({
- component: () => <UsersPage />
+ component: () => <UsersPage />,
})
diff --git a/dashboard/src/routes/_dashboard/users/$userId.tsx b/dashboard/src/routes/_dashboard/users/$userId.tsx
new file mode 100644
index 0000000..57049b6
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/users/$userId.tsx
@@ -0,0 +1,51 @@
+import {
+ createFileRoute,
+ defer,
+ Await,
+ Outlet,
+} from "@tanstack/react-router";
+import {
+ RouterUserContext,
+ fetchUser,
+} from "@marzneshin/features/users";
+import { Suspense } from "react";
+import {
+ AlertDialog,
+ AlertDialogContent,
+ Loading
+} from "@marzneshin/components";
+
+const UserProvider = () => {
+ const { user } = Route.useLoaderData()
+ return (
+ <Suspense fallback={<Loading />}>
+ <Await promise={user}>
+ {(user) => (
+ <RouterUserContext.Provider value={{ user }}>
+ <Suspense>
+ <Outlet />
+ </Suspense>
+ </RouterUserContext.Provider>
+ )}
+ </Await>
+ </Suspense>
+ )
+}
+
+export const Route = createFileRoute('/_dashboard/users/$userId')({
+ loader: async ({ params }) => {
+ const userPromise = fetchUser({
+ queryKey: ["users", params.userId]
+ });
+
+ return {
+ user: defer(userPromise)
+ }
+ },
+ errorComponent: () => (
+ <AlertDialog open={true}>
+ <AlertDialogContent>User not found</AlertDialogContent>
+ </AlertDialog>
+ ),
+ component: UserProvider,
+})
diff --git a/dashboard/src/routes/_dashboard/users/$userId/delete.tsx b/dashboard/src/routes/_dashboard/users/$userId/delete.tsx
new file mode 100644
index 0000000..43f230d
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/users/$userId/delete.tsx
@@ -0,0 +1,28 @@
+import {
+ createFileRoute,
+ useNavigate,
+} from "@tanstack/react-router";
+import {
+ UsersDeleteConfirmationDialog,
+ useRouterUserContext,
+} from "@marzneshin/features/users";
+import { useDialog } from "@marzneshin/hooks";
+
+const UserDelete = () => {
+ const [deleteDialogOpen, setDeleteDialogOpen] = useDialog(true);
+ const value = useRouterUserContext()
+ const navigate = useNavigate({ from: "/users/$userId/delete" });
+
+ return value && (
+ <UsersDeleteConfirmationDialog
+ open={deleteDialogOpen}
+ onOpenChange={setDeleteDialogOpen}
+ entity={value.user}
+ onClose={() => navigate({ to: "/users" })}
+ />
+ );
+}
+
+export const Route = createFileRoute('/_dashboard/users/$userId/delete')({
+ component: UserDelete,
+})
diff --git a/dashboard/src/routes/_dashboard/users/$userId/edit.tsx b/dashboard/src/routes/_dashboard/users/$userId/edit.tsx
new file mode 100644
index 0000000..87f2719
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/users/$userId/edit.tsx
@@ -0,0 +1,29 @@
+import {
+ createFileRoute,
+ useNavigate,
+} from "@tanstack/react-router";
+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 && (
+ <UsersMutationDialog
+ open={editDialogOpen}
+ onOpenChange={setEditDialogOpen}
+ entity={value.user}
+ onClose={() => navigate({ to: "/users" })}
+ />
+ );
+}
+
+export const Route = createFileRoute('/_dashboard/users/$userId/edit')({
+ component: UserEdit
+})
diff --git a/dashboard/src/routes/_dashboard/users/$userId/index.tsx b/dashboard/src/routes/_dashboard/users/$userId/index.tsx
new file mode 100644
index 0000000..bb45b5a
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/users/$userId/index.tsx
@@ -0,0 +1,28 @@
+import {
+ createFileRoute,
+ useNavigate,
+} from "@tanstack/react-router";
+import {
+ useRouterUserContext,
+ UsersSettingsDialog,
+} from "@marzneshin/features/users";
+import { useDialog } from "@marzneshin/hooks";
+
+const UserOpen = () => {
+ const [settingsDialogOpen, setSettingsDialogOpen] = useDialog(true);
+ const value = useRouterUserContext()
+ const navigate = useNavigate({ from: "/users/$userId" });
+
+ return value && (
+ <UsersSettingsDialog
+ open={settingsDialogOpen}
+ onOpenChange={setSettingsDialogOpen}
+ entity={value.user}
+ onClose={() => navigate({ to: "/users" })}
+ />
+ );
+}
+
+export const Route = createFileRoute('/_dashboard/users/$userId/')({
+ component: UserOpen
+})
diff --git a/dashboard/src/routes/_dashboard/users/create.tsx b/dashboard/src/routes/_dashboard/users/create.tsx
new file mode 100644
index 0000000..3fea6ce
--- /dev/null
+++ b/dashboard/src/routes/_dashboard/users/create.tsx
@@ -0,0 +1,20 @@
+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" })}
+ />
+ );
+}
+
+export const Route = createFileRoute("/_dashboard/users/create")({
+ component: UserCreate,
+});