From 3e97d834dc83effaff040a3afe98f3516d89ed52 Mon Sep 17 00:00:00 2001 From: dogukanoksuz Date: Mon, 9 Sep 2024 18:29:49 +0300 Subject: [PATCH] feat: Code optimization and left menu extension support --- src/components/_layout/app_layout.tsx | 12 +- src/components/dashboard/cards.tsx | 21 +-- src/components/dashboard/favorite-servers.tsx | 9 +- .../navigation/sidebar-selected.tsx | 89 ++++----- src/components/navigation/sidebar.tsx | 170 +++++++++++++---- src/components/navigation/site-header.tsx | 9 +- src/components/settings/server-actions.tsx | 9 +- src/components/ui/button.tsx | 5 +- src/hooks/auth/useCurrentUser.ts | 3 + .../servers/[server_id]/extensions/index.tsx | 17 +- src/pages/servers/[server_id]/index.tsx | 141 ++++++++------- .../servers/[server_id]/users/groups.tsx | 9 +- src/pages/servers/[server_id]/users/local.tsx | 9 +- src/pages/servers/create.tsx | 7 +- src/providers/sidebar-provider.tsx | 171 ++++++++++++------ 15 files changed, 401 insertions(+), 280 deletions(-) diff --git a/src/components/_layout/app_layout.tsx b/src/components/_layout/app_layout.tsx index b028400..dd90d33 100644 --- a/src/components/_layout/app_layout.tsx +++ b/src/components/_layout/app_layout.tsx @@ -1,10 +1,7 @@ import { ReactNode, useCallback, useEffect, useRef, useState } from "react" import Link from "next/link" import { Router, useRouter } from "next/router" -import { - SIDEBARCTX_STATES, - useSidebarContext, -} from "@/providers/sidebar-provider" +import { useSidebarContext } from "@/providers/sidebar-provider" import { useAutoAnimate } from "@formkit/auto-animate/react" import Cookies from "js-cookie" import nProgress from "nprogress" @@ -51,7 +48,7 @@ const Layout = ({ Component, pageProps }: any) => { useEffect(() => { const handleRouteChangeStart = () => nProgress.start() const handleRouteChangeComplete = () => { - sidebarCtx[SIDEBARCTX_STATES.setCollapsed](true) + sidebarCtx.setCollapsed(true) nProgress.done() } const handleRouteChangeError = () => nProgress.done() @@ -106,10 +103,7 @@ const Layout = ({ Component, pageProps }: any) => { defaultSize={18} minSize={15} collapsible={true} - className={cn( - "md:block", - sidebarCtx[SIDEBARCTX_STATES.collapsed] && "hidden" - )} + className={cn("md:block", sidebarCtx.collapsed && "hidden")} ref={panel} > diff --git a/src/components/dashboard/cards.tsx b/src/components/dashboard/cards.tsx index dea526e..741043b 100644 --- a/src/components/dashboard/cards.tsx +++ b/src/components/dashboard/cards.tsx @@ -1,13 +1,13 @@ +import { useEffect, useMemo, useState } from "react" +import Link from "next/link" import { apiService } from "@/services" import { ArrowRight, Cog, Server, ToyBrick, Users } from "lucide-react" -import Link from "next/link" -import { useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" -import { CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { useCurrentUser } from "@/hooks/auth/useCurrentUser" -import { cn } from "@/lib/utils" import { DashboardEnum } from "@/types/user" +import { cn } from "@/lib/utils" +import { useCurrentUser } from "@/hooks/auth/useCurrentUser" +import { CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Skeleton } from "../ui/skeleton" @@ -51,10 +51,7 @@ export default function DashboardCards() { const cardGridItemGridColumnClassName = useMemo(() => { const cardGridItemsLength = cardGridItems.length - return ( - (cardGridItemsLength > 3 ? `md:grid-cols-2` : `md:grid-cols-1`) + - ` lg:grid-cols-${cardGridItemsLength}` - ) + return `md:grid-cols-${cardGridItemsLength} lg:grid-cols-${cardGridItemsLength}` }, [cardGridItems]) if (cardGridItems.length === 0) return null @@ -67,9 +64,9 @@ export default function DashboardCards() { cardGridItemGridColumnClassName )} > - {cardGridItems.map((item) => { + {cardGridItems.map((item, idx) => { return ( - <> +
{item === "servers" && (
@@ -179,7 +176,7 @@ export default function DashboardCards() {
)} - +
) })} diff --git a/src/components/dashboard/favorite-servers.tsx b/src/components/dashboard/favorite-servers.tsx index fbb610c..e771808 100644 --- a/src/components/dashboard/favorite-servers.tsx +++ b/src/components/dashboard/favorite-servers.tsx @@ -1,8 +1,5 @@ import { useEffect, useState } from "react" -import { - SIDEBARCTX_STATES, - useSidebarContext, -} from "@/providers/sidebar-provider" +import { useSidebarContext } from "@/providers/sidebar-provider" import { apiService } from "@/services" import { FolderX } from "lucide-react" import { useTranslation } from "react-i18next" @@ -46,9 +43,7 @@ export default function FavoriteServers() { return (
- sidebarCtx[SIDEBARCTX_STATES.setSelected](item.id) - } + onClick={() => sidebarCtx.setSelected(item.id)} >
diff --git a/src/components/navigation/sidebar-selected.tsx b/src/components/navigation/sidebar-selected.tsx index 404a3c9..ba1bd60 100644 --- a/src/components/navigation/sidebar-selected.tsx +++ b/src/components/navigation/sidebar-selected.tsx @@ -1,9 +1,6 @@ -import { useEffect, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import Link from "next/link" -import { - SIDEBARCTX_STATES, - useSidebarContext, -} from "@/providers/sidebar-provider" +import { useSidebarContext } from "@/providers/sidebar-provider" import { apiService } from "@/services" import axios, { CancelTokenSource } from "axios" import { @@ -44,34 +41,33 @@ import ExtensionItem from "./extension-item" import ServerItem from "./server-item" export default function SidebarSelected() { - const [ + const { selected, - setSelected, selectedData, setSelectedData, selectedLoading, setSelectedLoading, - ] = useSidebarContext() - const sidebarCtx = useSidebarContext() + } = useSidebarContext() const user = useCurrentUser() const { t } = useTranslation("common") - let cancelToken: CancelTokenSource | undefined + const [isCollapsed, setIsCollapsed] = useState(true) + const [isUserCollapsed, setIsUserCollapsed] = useState(true) + + const cancelToken = useRef(undefined) useEffect(() => { - // Cancel the previous request before making a new request - if (cancelToken) { - cancelToken.cancel() + if (cancelToken.current) { + cancelToken.current.cancel() } - // Create a new CancelToken - cancelToken = axios.CancelToken.source() + cancelToken.current = axios.CancelToken.source() setSelectedLoading(true) apiService .getInstance() .get(`/menu/servers/${selected}`, { - cancelToken: cancelToken.token, + cancelToken: cancelToken.current.token, }) .then((res) => { setSelectedData(res.data) @@ -84,41 +80,37 @@ export default function SidebarSelected() { }) return () => { - if (cancelToken) { - cancelToken.cancel() + if (cancelToken.current) { + cancelToken.current.cancel() } } - }, [selected]) + }, [selected, setSelectedData, setSelectedLoading]) - const toggleFavorite = (id: string) => { - apiService - .getInstance() - .post(`/servers/${id}/favorites`) - .then(() => { - sidebarCtx[SIDEBARCTX_STATES.refreshServers]() - setSelectedData((prev: IServer) => { - return { + const toggleFavorite = useCallback( + (id: string) => { + apiService + .getInstance() + .post(`/servers/${id}/favorites`) + .then(() => { + setSelectedData((prev: IServer) => ({ ...prev, is_favorite: !prev.is_favorite, - } + })) }) - }) - } + }, + [setSelectedData] + ) - const elementIsActive = (server: IServer): boolean => { - return !server.is_online || !server.can_run_command - } + const elementIsActive = useCallback((): boolean => { + return !(selectedData.is_online && selectedData.can_run_command) + }, [selectedData]) - // Toggle server opts - // store it on the localstorage if it's collapsed or not - const [isCollapsed, setIsCollapsed] = useState(true) const toggleCollapsed = () => { setIsCollapsed(!isCollapsed) localStorage.setItem("serverSettingsCollapsed", (!isCollapsed).toString()) } // Toggle user operations - const [isUserCollapsed, setIsUserCollapsed] = useState(true) const toggleUserCollapsed = () => { setIsUserCollapsed(!isUserCollapsed) } @@ -221,7 +213,7 @@ export default function SidebarSelected() { {t("sidebar.system_status")} @@ -243,7 +235,7 @@ export default function SidebarSelected() { toggleCollapsed()} + onClick={toggleCollapsed} >

@@ -262,7 +254,7 @@ export default function SidebarSelected() { {user.permissions.server_services && ( {t("sidebar.services")} @@ -273,8 +265,7 @@ export default function SidebarSelected() { @@ -283,8 +274,7 @@ export default function SidebarSelected() { @@ -294,13 +284,16 @@ export default function SidebarSelected() { + + + + )} + + )} +

+ + +
+ +
+ + ) + } return (
@@ -56,8 +155,7 @@ export function Sidebar({ className }: { className?: string }) { >
-
- {sidebarCtx[SIDEBARCTX_STATES.settingsActive] ? ( + {settingsActive ? ( ) : ( <> @@ -67,7 +165,7 @@ export function Sidebar({ className }: { className?: string }) { {t("sidebar.servers")}
- {sidebarCtx[SIDEBARCTX_STATES.serversLoading] ? ( + {isLoading ? (
{[...Array(12)].map((_, i) => ( ) : ( <> - {sidebarCtx[SIDEBARCTX_STATES.servers].map( - (server: IServer) => ( - - ) - )} - {sidebarCtx[SIDEBARCTX_STATES.servers].length > 0 ? ( + +
+ + ))} + {servers.length > 0 ? ( @@ -38,7 +35,7 @@ export function SiteHeader() { sidebarCtx[SIDEBARCTX_STATES.setSelected]("")} + onClick={() => sidebarCtx.setSelected("")} > diff --git a/src/components/settings/server-actions.tsx b/src/components/settings/server-actions.tsx index f77d5d3..b9230b2 100644 --- a/src/components/settings/server-actions.tsx +++ b/src/components/settings/server-actions.tsx @@ -1,8 +1,5 @@ import { useState } from "react" -import { - SIDEBARCTX_STATES, - useSidebarContext, -} from "@/providers/sidebar-provider" +import { useSidebarContext } from "@/providers/sidebar-provider" import { apiService } from "@/services" import { Row } from "@tanstack/react-table" import { Edit2, Key, MoreHorizontal, Trash } from "lucide-react" @@ -120,7 +117,7 @@ function Edit({ description: t("servers.actions.edit.success_msg"), }) emitter.emit("REFETCH_SERVERS") - sidebarCtx[SIDEBARCTX_STATES.refreshServers]() + sidebarCtx.refreshServers() setOpen(false) }) .catch(() => { @@ -233,7 +230,7 @@ function DeleteServer({ description: t("servers.actions.delete_dialog.success_msg"), }) emitter.emit("REFETCH_SERVERS") - sidebarCtx[SIDEBARCTX_STATES.refreshServers]() + sidebarCtx.refreshServers() setOpen(false) }) .catch(() => { diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index ac8e0c9..ca083f9 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -37,11 +37,12 @@ export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean + as?: React.ElementType } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" + ({ className, variant, size, asChild = false, as, ...props }, ref) => { + const Comp = as || (asChild ? Slot : "button") return ( ( <> {row.original.display_name} @@ -151,7 +144,7 @@ export default function ServerExtensionPage() { title: t("success"), description: t("extensions.toasts.assign"), }) - sidebarCtx[SIDEBARCTX_STATES.refreshSelected]() + sidebarCtx.refreshSelected() fetchData() }) } @@ -172,7 +165,7 @@ export default function ServerExtensionPage() { }) setSelected([]) tableRef.current?.resetRowSelection() - sidebarCtx[SIDEBARCTX_STATES.refreshSelected]() + sidebarCtx.refreshSelected() fetchData() }) } diff --git a/src/pages/servers/[server_id]/index.tsx b/src/pages/servers/[server_id]/index.tsx index 854fdb6..b8fb166 100644 --- a/src/pages/servers/[server_id]/index.tsx +++ b/src/pages/servers/[server_id]/index.tsx @@ -35,77 +35,84 @@ export default function ServerStatus() { }) }, [router.query.server_id]) - return ( - <> + if (!router.query.server_id || loading) { + return (
- {data && data.server && data.server.os === "linux" ? ( -
- - {data.server && data.server.os === "linux" && } -
-
-

- {t("system_status.cpu_usage")} -

- -
-
-

- {t("system_status.ram_usage")} -

- -
-
-

- {t("system_status.disk_status")} -

- -
+
+ + +
+
+

+ {t("system_status.cpu_usage")} +

+ +
+
+

+ {t("system_status.ram_usage")} +

+
+
+

+ {t("system_status.disk_status")} +

+ +
+
+
+
+ ) + } + + if (data.server?.os !== "linux") { + return ( +
+ +
+
+

+ {t("warning")} +

+

+ {t("not_supported")} +

+
+
+
+ ) + } + + return ( +
+ +
+
{JSON.stringify(loading)}
+ + +
+
+

+ {t("system_status.cpu_usage")} +

+ +
+
+

+ {t("system_status.ram_usage")} +

+ +
+
+

+ {t("system_status.disk_status")} +

+
- ) : ( - <> - {loading ? ( -
- - -
-
-

- {t("system_status.cpu_usage")} -

- -
-
-

- {t("system_status.ram_usage")} -

- -
-
-

- {t("system_status.disk_status")} -

- -
-
-
- ) : ( -
-
-

- {t("warning")} -

-

- {t("not_supported")} -

-
-
- )} - - )} +
- +
) } diff --git a/src/pages/servers/[server_id]/users/groups.tsx b/src/pages/servers/[server_id]/users/groups.tsx index 6296516..e65a74b 100644 --- a/src/pages/servers/[server_id]/users/groups.tsx +++ b/src/pages/servers/[server_id]/users/groups.tsx @@ -1,9 +1,6 @@ import { useEffect, useState } from "react" import { useRouter } from "next/router" -import { - SIDEBARCTX_STATES, - useSidebarContext, -} from "@/providers/sidebar-provider" +import { useSidebarContext } from "@/providers/sidebar-provider" import { apiService } from "@/services" import { zodResolver } from "@hookform/resolvers/zod" import { PlusCircle } from "lucide-react" @@ -92,8 +89,8 @@ export default function LocalGroups() { selectable={false} > {!( - sidebarCtx[SIDEBARCTX_STATES.selectedData].type && - sidebarCtx[SIDEBARCTX_STATES.selectedData].type.includes("winrm") + sidebarCtx.selectedData.type && + sidebarCtx.selectedData.type.includes("winrm") ) && } diff --git a/src/pages/servers/[server_id]/users/local.tsx b/src/pages/servers/[server_id]/users/local.tsx index eeabe6f..2b054db 100644 --- a/src/pages/servers/[server_id]/users/local.tsx +++ b/src/pages/servers/[server_id]/users/local.tsx @@ -1,9 +1,6 @@ import { useEffect, useState } from "react" import { useRouter } from "next/router" -import { - SIDEBARCTX_STATES, - useSidebarContext, -} from "@/providers/sidebar-provider" +import { useSidebarContext } from "@/providers/sidebar-provider" import { apiService } from "@/services" import { zodResolver } from "@hookform/resolvers/zod" import { PlusCircle } from "lucide-react" @@ -92,8 +89,8 @@ export default function LocalUsersPage() { selectable={false} > {!( - sidebarCtx[SIDEBARCTX_STATES.selectedData].type && - sidebarCtx[SIDEBARCTX_STATES.selectedData].type.includes("winrm") + sidebarCtx.selectedData.type && + sidebarCtx.selectedData.type.includes("winrm") ) && } diff --git a/src/pages/servers/create.tsx b/src/pages/servers/create.tsx index b18a51c..5a85316 100644 --- a/src/pages/servers/create.tsx +++ b/src/pages/servers/create.tsx @@ -1,10 +1,7 @@ import { useRef, useState } from "react" import Link from "next/link" import { useRouter } from "next/router" -import { - SIDEBARCTX_STATES, - useSidebarContext, -} from "@/providers/sidebar-provider" +import { useSidebarContext } from "@/providers/sidebar-provider" import { apiService } from "@/services" import { useAutoAnimate } from "@formkit/auto-animate/react" import { ChevronLeft, ChevronRight, PlusCircle, Server } from "lucide-react" @@ -194,7 +191,7 @@ export default function ServerCreatePage() { .getInstance() .post("/servers", data) .then(() => { - sidebarCtx[SIDEBARCTX_STATES.refreshServers]() + sidebarCtx.refreshServers() toast({ title: t("success"), description: t("create.errors.success"), diff --git a/src/providers/sidebar-provider.tsx b/src/providers/sidebar-provider.tsx index 1869484..9b138cb 100644 --- a/src/providers/sidebar-provider.tsx +++ b/src/providers/sidebar-provider.tsx @@ -2,22 +2,61 @@ import * as React from "react" import { useRouter } from "next/router" import { apiService } from "@/services" +import { IExtension } from "@/types/extension" import { IServer } from "@/types/server" +import { useCurrentUser } from "@/hooks/auth/useCurrentUser" -const Context = React.createContext([] as any) +interface SidebarContextType { + selected: string + setSelected: React.Dispatch> + selectedData: IServer + setSelectedData: React.Dispatch> + selectedLoading: boolean + setSelectedLoading: React.Dispatch> + refreshSelected: () => void + settingsActive: boolean + setSettingsActive: React.Dispatch> + serversLoading: boolean + setServersLoading: React.Dispatch> + servers: IServer[] + setServers: React.Dispatch> + refreshServers: () => void + // Add extension menu support + extensionsLoading: boolean + setExtensionsLoading: React.Dispatch> + extensions: IExtension[] + setExtensions: React.Dispatch> + refreshExtensions: () => void + collapsed: boolean + setCollapsed: React.Dispatch> + toggleSidebar: () => void +} + +const SidebarContext = React.createContext( + undefined +) -export function SidebarProvider({ +export const SidebarProvider = ({ children, }: { children: React.ReactNode -}): React.JSX.Element { +}) => { const router = useRouter() + const user = useCurrentUser() const [selected, setSelected] = React.useState("") const [selectedLoading, setSelectedLoading] = React.useState(true) const [selectedData, setSelectedData] = React.useState({} as IServer) const [settingsActive, setSettingsActive] = React.useState(false) + + // Server menu state const [serversLoading, setServersLoading] = React.useState(true) const [servers, setServers] = React.useState([]) + + // Extension menu state + const [extensionsLoading, setExtensionsLoading] = + React.useState(true) + const [extensions, setExtensions] = React.useState([]) + const [collapsed, setCollapsed] = React.useState(true) React.useEffect(() => { @@ -30,23 +69,28 @@ export function SidebarProvider({ }, [router.asPath]) React.useEffect(() => { + if (user.permissions.view.sidebar === "extensions") { + setSelected("") + return + } + if (router.query.server_id) { setSelected(router.query.server_id as string) } else { setSelected("") } - }, [router.query.server_id]) + }, [router.query.server_id, user.permissions.view.sidebar]) - const refreshSelected = () => { + const refreshSelected = React.useCallback(() => { apiService .getInstance() .get(`/menu/servers/${selected}`) .then((res) => { setSelectedData(res.data) }) - } + }, [selected]) - const refreshServers = () => { + const refreshServers = React.useCallback(() => { apiService .getInstance() .get("/menu/servers") @@ -54,59 +98,76 @@ export function SidebarProvider({ setServers(res.data) setServersLoading(false) }) - } + }, []) - const toggleSidebar = () => { - setCollapsed(!collapsed) - } + // Add extension menu support + const refreshExtensions = React.useCallback(() => { + apiService + .getInstance() + .get("/menu/extensions") + .then((res) => { + setExtensions(res.data) + setExtensionsLoading(false) + }) + }, []) + + const toggleSidebar = React.useCallback(() => { + setCollapsed((prev) => !prev) + }, []) + + const contextValue = React.useMemo( + () => ({ + selected, + setSelected, + selectedData, + setSelectedData, + selectedLoading, + setSelectedLoading, + refreshSelected, + settingsActive, + setSettingsActive, + serversLoading, + setServersLoading, + servers, + setServers, + refreshServers, + extensionsLoading, + setExtensionsLoading, + extensions, + setExtensions, + refreshExtensions, + collapsed, + setCollapsed, + toggleSidebar, + }), + [ + selected, + selectedData, + selectedLoading, + settingsActive, + serversLoading, + servers, + collapsed, + refreshSelected, + refreshServers, + extensionsLoading, + extensions, + refreshExtensions, + toggleSidebar, + ] + ) return ( - + {children} - + ) } -export function useSidebarContext() { - return React.useContext(Context) -} - -export const SIDEBARCTX_STATES = { - selected: 0, - setSelected: 1, - selectedData: 2, - setSelectedData: 3, - selectedLoading: 4, - setSelectedLoading: 5, - refreshSelected: 6, - settingsActive: 7, - setSettingsActive: 8, - serversLoading: 9, - setServersLoading: 10, - servers: 11, - setServers: 12, - refreshServers: 13, - collapsed: 14, - setCollapsed: 15, - toggleSidebar: 16, +export const useSidebarContext = () => { + const context = React.useContext(SidebarContext) + if (context === undefined) { + throw new Error("useSidebarContext must be used within a SidebarProvider") + } + return context }