🚧 wip: Use command bar nav items

This commit is contained in:
Fred KISSIE
2026-06-12 23:51:05 +02:00
parent 444d293a29
commit a68b57067c
5 changed files with 315 additions and 40 deletions
+4 -1
View File
@@ -12,7 +12,7 @@ import UserProvider from "@app/providers/UserProvider";
import { Layout } from "@app/components/Layout"; import { Layout } from "@app/components/Layout";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { pullEnv } from "@app/lib/pullEnv"; import { pullEnv } from "@app/lib/pullEnv";
import { orgNavSections } from "@app/app/navigation"; import { commandBarNavSections, orgNavSections } from "@app/app/navigation";
import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -82,6 +82,9 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
navItems={orgNavSections(env, { navItems={orgNavSections(env, {
isPrimaryOrg: primaryOrg isPrimaryOrg: primaryOrg
})} })}
commandNavItems={commandBarNavSections(env, {
isPrimaryOrg: primaryOrg
})}
> >
{children} {children}
</Layout> </Layout>
+250
View File
@@ -340,3 +340,253 @@ export const adminNavSections = (env?: Env): SidebarNavSection[] => [
] ]
} }
]; ];
export const commandBarNavSections = (
env?: Env,
options?: OrgNavSectionsOptions
): SidebarNavSection[] => [
{
heading: "network",
items: [
{
title: "sidebarSites",
href: "/{orgId}/settings/sites",
icon: <Plug className="size-4 flex-none" />
},
{
title: "sidebarResources",
icon: <Waypoints className="size-4 flex-none" />,
items: [
{
title: "sidebarProxyResources",
href: "/{orgId}/settings/resources/public",
icon: <Globe className="size-4 flex-none" />
},
{
title: "sidebarClientResources",
href: "/{orgId}/settings/resources/private",
icon: <GlobeLock className="size-4 flex-none" />
}
]
},
{
title: "sidebarClients",
icon: <MonitorUp className="size-4 flex-none" />,
items: [
{
href: "/{orgId}/settings/clients/user",
title: "sidebarUserDevices",
icon: <Laptop className="size-4 flex-none" />
},
{
href: "/{orgId}/settings/clients/machine",
title: "sidebarMachineClients",
icon: <Server className="size-4 flex-none" />
}
]
},
{
title: "sidebarDomains",
href: "/{orgId}/settings/domains",
icon: <Globe className="size-4 flex-none" />
},
...(build === "saas"
? [
{
title: "sidebarRemoteExitNodes",
href: "/{orgId}/settings/remote-exit-nodes",
icon: <Server className="size-4 flex-none" />
}
]
: [])
]
},
{
heading: "accessControl",
items: [
{
title: "sidebarTeam",
icon: <Users className="size-4 flex-none" />,
items: [
{
title: "sidebarUsers",
href: "/{orgId}/settings/access/users",
icon: <User className="size-4 flex-none" />
},
{
title: "sidebarRoles",
href: "/{orgId}/settings/access/roles",
icon: <Users className="size-4 flex-none" />
},
{
title: "sidebarInvitations",
href: "/{orgId}/settings/access/invitations",
icon: <TicketCheck className="size-4 flex-none" />
}
]
},
...(!env?.flags.disableEnterpriseFeatures
? [
{
title: "sidebarPolicies",
icon: <ShieldIcon className="size-4 flex-none" />,
items: [
{
title: "sidebarResourcePolicies",
href: "/{orgId}/settings/policies/resources/public",
icon: (
<GlobeIcon className="size-4 flex-none" />
)
}
]
}
]
: []),
// PaidFeaturesAlert
...((build === "oss" && !env?.flags.disableEnterpriseFeatures) ||
build === "saas" ||
env?.app.identityProviderMode === "org" ||
(env?.app.identityProviderMode === undefined && build !== "oss")
? [
{
title: "sidebarIdentityProviders",
href: "/{orgId}/settings/idp",
icon: <Fingerprint className="size-4 flex-none" />
}
]
: []),
...(!env?.flags.disableEnterpriseFeatures
? [
{
title: "sidebarApprovals",
href: "/{orgId}/settings/access/approvals",
icon: <UserCog className="size-4 flex-none" />
}
]
: []),
{
title: "sidebarShareableLinks",
href: "/{orgId}/settings/share-links",
icon: <LinkIcon className="size-4 flex-none" />
}
]
},
{
heading: "sidebarOrganization",
items: [
{
title: "sidebarLogsAndAnalytics",
icon: <ChartLine className="size-4 flex-none" />,
items: [
{
title: "sidebarLogsAnalytics",
href: "/{orgId}/settings/logs/analytics",
icon: <ChartLine className="size-4 flex-none" />
},
{
title: "sidebarLogsRequest",
href: "/{orgId}/settings/logs/request",
icon: (
<SquareMousePointer className="size-4 flex-none" />
)
},
...(!env?.flags.disableEnterpriseFeatures
? [
{
title: "sidebarLogsAccess",
href: "/{orgId}/settings/logs/access",
icon: <ScanEye className="size-4 flex-none" />
},
{
title: "sidebarLogsAction",
href: "/{orgId}/settings/logs/action",
icon: <Logs className="size-4 flex-none" />
},
{
title: "sidebarLogsConnection",
href: "/{orgId}/settings/logs/connection",
icon: <Cable className="size-4 flex-none" />
},
{
title: "sidebarLogsStreaming",
href: "/{orgId}/settings/logs/streaming",
icon: <Unplug className="size-4 flex-none" />
}
]
: [])
]
},
{
title: "sidebarManagement",
icon: <Building2 className="size-4 flex-none" />,
items: [
...(!env?.flags.disableEnterpriseFeatures
? [
{
title: "sidebarAlerting",
href: "/{orgId}/settings/alerting",
icon: (
<BellRing className="size-4 flex-none" />
)
},
{
title: "sidebarProvisioning",
href: "/{orgId}/settings/provisioning",
icon: <Boxes className="size-4 flex-none" />
}
]
: []),
{
title: "sidebarBluePrints",
href: "/{orgId}/settings/blueprints",
icon: <ReceiptText className="size-4 flex-none" />
},
{
title: "sidebarApiKeys",
href: "/{orgId}/settings/api-keys",
icon: <KeyRound className="size-4 flex-none" />
},
...(!env?.flags.disableEnterpriseFeatures
? [
{
title: "labels",
href: "/{orgId}/settings/labels",
icon: <TagIcon className="size-4 flex-none" />
}
]
: [])
]
},
...(build === "saas" && options?.isPrimaryOrg
? [
{
title: "sidebarBillingAndLicenses",
icon: <CreditCard className="size-4 flex-none" />,
items: [
{
title: "sidebarBilling",
href: "/{orgId}/settings/billing",
icon: (
<CreditCard className="size-4 flex-none" />
)
},
{
title: "sidebarEnterpriseLicenses",
href: "/{orgId}/settings/license",
icon: (
<TicketCheck className="size-4 flex-none" />
)
}
]
}
]
: []),
{
title: "sidebarSettings",
href: "/{orgId}/settings/general",
icon: <Settings className="size-4 flex-none" />
}
]
}
];
+7 -1
View File
@@ -13,6 +13,7 @@ interface LayoutProps {
orgId?: string; orgId?: string;
orgs?: ListUserOrgsResponse["orgs"]; orgs?: ListUserOrgsResponse["orgs"];
navItems?: SidebarNavSection[]; navItems?: SidebarNavSection[];
commandNavItems?: SidebarNavSection[];
showSidebar?: boolean; showSidebar?: boolean;
showHeader?: boolean; showHeader?: boolean;
showTopBar?: boolean; showTopBar?: boolean;
@@ -24,6 +25,7 @@ export async function Layout({
orgId, orgId,
orgs, orgs,
navItems = [], navItems = [],
commandNavItems = [],
showSidebar = true, showSidebar = true,
showHeader = true, showHeader = true,
showTopBar = true, showTopBar = true,
@@ -38,7 +40,11 @@ export async function Layout({
(sidebarStateCookie !== "expanded" && defaultSidebarCollapsed); (sidebarStateCookie !== "expanded" && defaultSidebarCollapsed);
return ( return (
<CommandPaletteProvider orgId={orgId} orgs={orgs} navItems={navItems}> <CommandPaletteProvider
orgId={orgId}
orgs={orgs}
navItems={commandNavItems}
>
<div className="flex h-screen-safe overflow-hidden"> <div className="flex h-screen-safe overflow-hidden">
{/* Desktop Sidebar */} {/* Desktop Sidebar */}
{showSidebar && ( {showSidebar && (
-7
View File
@@ -113,13 +113,6 @@ export default function SitesTable({
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const t = useTranslations(); const t = useTranslations();
// useEffect(() => {
// const interval = setInterval(() => {
// router.refresh();
// }, 30_000);
// return () => clearInterval(interval);
// }, []);
const booleanSearchFilterSchema = z const booleanSearchFilterSchema = z
.enum(["true", "false"]) .enum(["true", "false"])
.optional() .optional()
@@ -14,7 +14,7 @@ import { Badge } from "@app/components/ui/badge";
import { ListUserOrgsResponse } from "@server/routers/org"; import { ListUserOrgsResponse } from "@server/routers/org";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { import React, {
createContext, createContext,
useCallback, useCallback,
useContext, useContext,
@@ -27,6 +27,7 @@ import { useCommandPaletteActions } from "./useCommandPaletteActions";
import { useCommandPaletteNavigation } from "./useCommandPaletteNavigation"; import { useCommandPaletteNavigation } from "./useCommandPaletteNavigation";
import { useCommandPaletteOrganizations } from "./useCommandPaletteOrganizations"; import { useCommandPaletteOrganizations } from "./useCommandPaletteOrganizations";
import { useCommandPaletteSearch } from "./useCommandPaletteSearch"; import { useCommandPaletteSearch } from "./useCommandPaletteSearch";
import { useUserContext } from "@app/hooks/useUserContext";
type CommandPaletteProps = { type CommandPaletteProps = {
orgId?: string; orgId?: string;
@@ -34,6 +35,12 @@ type CommandPaletteProps = {
navItems: SidebarNavSection[]; navItems: SidebarNavSection[];
}; };
/**
* Plan for command bar:
* - the nav items should be custom items instead of all of the ones in the sidebar
* - actions should be triggered by using `>` (like in Github)
*/
export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) { export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
const t = useTranslations(); const t = useTranslations();
const router = useRouter(); const router = useRouter();
@@ -41,7 +48,7 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const navigationGroups = useCommandPaletteNavigation(navItems); const navigationGroups = useCommandPaletteNavigation(navItems);
const organizations = useCommandPaletteOrganizations(orgs); // const organizations = useCommandPaletteOrganizations(orgs);
const actions = useCommandPaletteActions(orgId, orgs); const actions = useCommandPaletteActions(orgId, orgs);
const { shouldSearch, sites, resources, users, machineClients, isLoading } = const { shouldSearch, sites, resources, users, machineClients, isLoading } =
useCommandPaletteSearch({ useCommandPaletteSearch({
@@ -69,45 +76,58 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
[setOpen] [setOpen]
); );
const hasEntityResults = // const hasEntityResults =
sites.length > 0 || // sites.length > 0 ||
resources.length > 0 || // resources.length > 0 ||
users.length > 0 || // users.length > 0 ||
machineClients.length > 0; // machineClients.length > 0;
return ( return (
<CommandDialog <CommandDialog
open={open} open //={open}
onOpenChange={handleOpenChange} onOpenChange={handleOpenChange}
title={t("commandPaletteTitle")} title={t("commandPaletteTitle")}
description={t("commandPaletteDescription")} description={t("commandPaletteDescription")}
className="max-w-2xl **:data-[slot=command-input-wrapper]:h-15"
> >
<CommandInput <CommandInput
placeholder={t("commandPaletteSearchPlaceholder")} placeholder={t("commandPaletteSearchPlaceholder")}
value={search} value={search}
onValueChange={setSearch} onValueChange={setSearch}
/> />
<CommandList> <CommandList className="max-h-125 min-h-0 h-auto">
<CommandEmpty>{t("commandPaletteNoResults")}</CommandEmpty> <CommandEmpty>{t("commandPaletteNoResults")}</CommandEmpty>
{navigationGroups.map((group) => ( <CommandGroup
<CommandGroup key={group.heading} heading={group.heading}> heading='Type ">" to open action mode'
{group.items.map((item) => ( className="[&_[cmdk-group-heading]]:text-sm"
<CommandItem />
key={item.id}
value={`${item.title} ${group.heading}`} {navigationGroups.map((group, idx) => (
onSelect={() => <React.Fragment key={group.heading}>
runCommand(() => router.push(item.href)) {idx > 0 && <CommandSeparator />}
} <CommandGroup
> heading={group.heading}
{item.icon} className="[&_[cmdk-group-heading]]:py-2 [&_[cmdk-group-heading]]:text-sm pb-2.5"
<span>{item.title}</span> >
</CommandItem> {group.items.map((item) => (
))} <CommandItem
</CommandGroup> key={item.id}
value={`${item.title} ${group.heading}`}
onSelect={() =>
runCommand(() => router.push(item.href))
}
className="h-9"
>
{item.icon}
<span>{item.title}</span>
</CommandItem>
))}
</CommandGroup>
</React.Fragment>
))} ))}
{organizations.length > 1 && ( {/* {organizations.length > 1 && (
<> <>
<CommandSeparator /> <CommandSeparator />
<CommandGroup <CommandGroup
@@ -137,9 +157,9 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
))} ))}
</CommandGroup> </CommandGroup>
</> </>
)} )} */}
{shouldSearch && orgId && ( {/* {shouldSearch && orgId && (
<> <>
<CommandSeparator /> <CommandSeparator />
{isLoading && !hasEntityResults ? ( {isLoading && !hasEntityResults ? (
@@ -243,12 +263,15 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
</> </>
)} )}
</> </>
)} )} */}
{actions.length > 0 && ( {/* {actions.length > 0 && (
<> <>
<CommandSeparator /> <CommandSeparator />
<CommandGroup heading={t("commandPaletteActions")}> <CommandGroup
heading={t("commandPaletteActions")}
className="pb-2.5"
>
{actions.map((action) => ( {actions.map((action) => (
<CommandItem <CommandItem
key={action.id} key={action.id}
@@ -269,7 +292,7 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
))} ))}
</CommandGroup> </CommandGroup>
</> </>
)} )} */}
</CommandList> </CommandList>
</CommandDialog> </CommandDialog>
); );