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 { table: Table + disableSelected?: boolean } export function DataTablePagination({ - table, + table, disableSelected }: DataTablePaginationProps) { const { t } = useTranslation(); return ( -
-
- {table.getFilteredSelectedRowModel().rows.length} {"/"} - {table.getFilteredRowModel().rows.length} {t('table.selected')} -
-
+
+ {disableSelected || ( +
+ {table.getFilteredSelectedRowModel().rows.length} {"/"} + {table.getFilteredRowModel().rows.length} {t('table.selected')} +
+ )} +

{t('table.row-per-page')}

- - - - )} - /> -
- ( - - {t("address")} - - - - - - )} - /> - ( - - {t("port")} - - - - - - )} - /> -
- - ( - - {t("page.nodes.connection_backend")} - - - {t("page.nodes.connection_backend_desc")} - - - - )} - /> - ( - - {t("page.nodes.usage_coefficient")} - - - - - - )} - /> - - - - - - - ); -}; 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 = ({ + 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 ( + + + + + {entity + ? t("page.nodes.dialogs.edition.title") + : t("page.nodes.dialogs.creation.title")} + + +
+ + ( + + {t("name")} + + + + + + )} + /> +
+ ( + + {t("address")} + + + + + + )} + /> + ( + + {t("port")} + + + + + + )} + /> +
+ + ( + + {t("page.nodes.connection_backend")} + + + {t("page.nodes.connection_backend_desc")} + + + + )} + /> + ( + + {t("page.nodes.usage_coefficient")} + + + + + + )} + /> + + + + +
+
+ ); +}; 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 = ({ onOpenChange, open, entity, + onClose, }) => { const { t } = useTranslation(); + useEffect(() => { + if (!open) onClose(); + }, [open, onClose]); + return ( 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(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 { + 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 ( 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 = ({ onOpenChange, open, entity }) => { +export const ServicesDeleteConfirmationDialog: FC = ({ onOpenChange, open, entity, onClose }) => { const deleteMutation = useServicesDeletionMutation(); - if (entity !== null) { - return ( - deleteMutation.mutate(entity)} - /> - ) - } else { - return null - } + useEffect(() => { + if (!open) onClose(); + }, [open, onClose]); + return ( + 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 = ({ entity, open, onOpenChange, + onClose, }) => { const isEditing = entity !== null; const updateMutation = useServicesUpdateMutation(); @@ -55,6 +57,11 @@ export const MutationDialog: FC = ({ onOpenChange, }); const { t } = useTranslation(); + + useEffect(() => { + if (!open) onClose(); + }, [open, onClose]); + return ( 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 = ({ onOpenChange, open, entity }) => { - +export const ServiceSettingsDialog: FC = ({ + onOpenChange, open, entity, onClose +}) => { const { t } = useTranslation(); - if (entity) { - return ( - - - - - {t('page.services.settings.title')} - - - - - {t('inbounds')} - {t('users')} - - - - - - - - - - - ) - } + useEffect(() => { + if (!open) onClose(); + }, [open, onClose]); + + return ( + + + + + {t('page.services.settings.title')} + + + + + {t('inbounds')} + {t('users')} + + + + + + + + + + + ) } 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(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 { + 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 ( - - ) -} + const navigate = useNavigate({ from: "/services" }); + return ( + 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 = ({ service }) => const { data } = useUsersServiceQuery({ serviceId: service.id }); return ( -
- -
+ ) } 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 = ({ }) => { const router = useRouterState(); const currentActivePath = router.location.pathname; + const isActive = (path: string) => { + if (path === "/") { + return currentActivePath === path; + } + return currentActivePath.startsWith(path); + }; return (
- + }> + +
@@ -90,7 +100,6 @@ export const DashboardLayout = () => { export const Route = createFileRoute("/_dashboard")({ component: () => , - 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 = () => { + }> + + 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 ( + }> + + {(host) => ( + + + + + + )} + + + ) +} + +export const Route = createFileRoute('/_dashboard/hosts/$hostId')({ + loader: async ({ params }) => { + const hostPromise = fetchHost({ + queryKey: ["hosts", Number.parseInt(params.hostId)] + }); + + return { + host: defer(hostPromise) + } + }, + errorComponent: () => ( + + Host not found + + ), + 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 && ( + 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 && ( + 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 && ( + 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 ( + navigate({ to: "/hosts" })} + /> + ); +} + +export const Route = createFileRoute('/_dashboard/hosts/$inboundId/create')({ + errorComponent: () => ( + + Host not found + + ), + 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 = () => { + }> + + 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 ( + }> + + {(node) => ( + + + + + + )} + + + ); +}; + +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: () => ( + + Node not found + + ), +}) 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 && ( + 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 && ( + 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 && ( + 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 ( + 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 ( + + + + {t("services")} + + + + }> + + + + + + ); +}; + +export const Route = createFileRoute("/_dashboard/services")({ + component: () => , +}); 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 ( - - - - - {t('services')} - - - - - - - - - - ) -}; - -export const Route = createFileRoute('/_dashboard/services')({ - component: () => -}) 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 ( + }> + + {(service) => ( + + + + + + )} + + + ) +} + +export const Route = createFileRoute('/_dashboard/services/$serviceId')({ + loader: async ({ params }) => { + const servicePromise = fetchService({ + queryKey: ["services", Number.parseInt(params.serviceId)] + }); + + return { + service: defer(servicePromise) + } + }, + errorComponent: () => ( + + Service not found + + ), + 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 && ( + 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 && ( + 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 && ( + 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 ( + 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 = () => { + }> + + {(data && data.pageCount === 0) && ( @@ -49,5 +53,5 @@ export const UsersPage: FC = () => { export const Route = createFileRoute('/_dashboard/users')({ - component: () => + component: () => , }) 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 ( + }> + + {(user) => ( + + + + + + )} + + + ) +} + +export const Route = createFileRoute('/_dashboard/users/$userId')({ + loader: async ({ params }) => { + const userPromise = fetchUser({ + queryKey: ["users", params.userId] + }); + + return { + user: defer(userPromise) + } + }, + errorComponent: () => ( + + User not found + + ), + 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 && ( + 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 && ( + 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 && ( + 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 ( + navigate({ to: "/users" })} + /> + ); +} + +export const Route = createFileRoute("/_dashboard/users/create")({ + component: UserCreate, +});