🚧 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 { getTranslations } from "next-intl/server";
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";
export const dynamic = "force-dynamic";
@@ -82,6 +82,9 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
navItems={orgNavSections(env, {
isPrimaryOrg: primaryOrg
})}
commandNavItems={commandBarNavSections(env, {
isPrimaryOrg: primaryOrg
})}
>
{children}
</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;
orgs?: ListUserOrgsResponse["orgs"];
navItems?: SidebarNavSection[];
commandNavItems?: SidebarNavSection[];
showSidebar?: boolean;
showHeader?: boolean;
showTopBar?: boolean;
@@ -24,6 +25,7 @@ export async function Layout({
orgId,
orgs,
navItems = [],
commandNavItems = [],
showSidebar = true,
showHeader = true,
showTopBar = true,
@@ -38,7 +40,11 @@ export async function Layout({
(sidebarStateCookie !== "expanded" && defaultSidebarCollapsed);
return (
<CommandPaletteProvider orgId={orgId} orgs={orgs} navItems={navItems}>
<CommandPaletteProvider
orgId={orgId}
orgs={orgs}
navItems={commandNavItems}
>
<div className="flex h-screen-safe overflow-hidden">
{/* Desktop Sidebar */}
{showSidebar && (
-7
View File
@@ -113,13 +113,6 @@ export default function SitesTable({
const api = createApiClient(useEnvContext());
const t = useTranslations();
// useEffect(() => {
// const interval = setInterval(() => {
// router.refresh();
// }, 30_000);
// return () => clearInterval(interval);
// }, []);
const booleanSearchFilterSchema = z
.enum(["true", "false"])
.optional()
@@ -14,7 +14,7 @@ import { Badge } from "@app/components/ui/badge";
import { ListUserOrgsResponse } from "@server/routers/org";
import { Loader2 } from "lucide-react";
import { useRouter } from "next/navigation";
import {
import React, {
createContext,
useCallback,
useContext,
@@ -27,6 +27,7 @@ import { useCommandPaletteActions } from "./useCommandPaletteActions";
import { useCommandPaletteNavigation } from "./useCommandPaletteNavigation";
import { useCommandPaletteOrganizations } from "./useCommandPaletteOrganizations";
import { useCommandPaletteSearch } from "./useCommandPaletteSearch";
import { useUserContext } from "@app/hooks/useUserContext";
type CommandPaletteProps = {
orgId?: string;
@@ -34,6 +35,12 @@ type CommandPaletteProps = {
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) {
const t = useTranslations();
const router = useRouter();
@@ -41,7 +48,7 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
const [search, setSearch] = useState("");
const navigationGroups = useCommandPaletteNavigation(navItems);
const organizations = useCommandPaletteOrganizations(orgs);
// const organizations = useCommandPaletteOrganizations(orgs);
const actions = useCommandPaletteActions(orgId, orgs);
const { shouldSearch, sites, resources, users, machineClients, isLoading } =
useCommandPaletteSearch({
@@ -69,45 +76,58 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
[setOpen]
);
const hasEntityResults =
sites.length > 0 ||
resources.length > 0 ||
users.length > 0 ||
machineClients.length > 0;
// const hasEntityResults =
// sites.length > 0 ||
// resources.length > 0 ||
// users.length > 0 ||
// machineClients.length > 0;
return (
<CommandDialog
open={open}
open //={open}
onOpenChange={handleOpenChange}
title={t("commandPaletteTitle")}
description={t("commandPaletteDescription")}
className="max-w-2xl **:data-[slot=command-input-wrapper]:h-15"
>
<CommandInput
placeholder={t("commandPaletteSearchPlaceholder")}
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandList className="max-h-125 min-h-0 h-auto">
<CommandEmpty>{t("commandPaletteNoResults")}</CommandEmpty>
{navigationGroups.map((group) => (
<CommandGroup key={group.heading} heading={group.heading}>
{group.items.map((item) => (
<CommandItem
key={item.id}
value={`${item.title} ${group.heading}`}
onSelect={() =>
runCommand(() => router.push(item.href))
}
>
{item.icon}
<span>{item.title}</span>
</CommandItem>
))}
</CommandGroup>
<CommandGroup
heading='Type ">" to open action mode'
className="[&_[cmdk-group-heading]]:text-sm"
/>
{navigationGroups.map((group, idx) => (
<React.Fragment key={group.heading}>
{idx > 0 && <CommandSeparator />}
<CommandGroup
heading={group.heading}
className="[&_[cmdk-group-heading]]:py-2 [&_[cmdk-group-heading]]:text-sm pb-2.5"
>
{group.items.map((item) => (
<CommandItem
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 />
<CommandGroup
@@ -137,9 +157,9 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
))}
</CommandGroup>
</>
)}
)} */}
{shouldSearch && orgId && (
{/* {shouldSearch && orgId && (
<>
<CommandSeparator />
{isLoading && !hasEntityResults ? (
@@ -243,12 +263,15 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
</>
)}
</>
)}
)} */}
{actions.length > 0 && (
{/* {actions.length > 0 && (
<>
<CommandSeparator />
<CommandGroup heading={t("commandPaletteActions")}>
<CommandGroup
heading={t("commandPaletteActions")}
className="pb-2.5"
>
{actions.map((action) => (
<CommandItem
key={action.id}
@@ -269,7 +292,7 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
))}
</CommandGroup>
</>
)}
)} */}
</CommandList>
</CommandDialog>
);