- {/* Desktop Sidebar */}
- {showSidebar && (
-
- )}
-
- {/* Main content area */}
-
- {/* Mobile header */}
- {showHeader && (
-
+
+ {/* Desktop Sidebar */}
+ {showSidebar && (
+
)}
- {/* Desktop header */}
- {showHeader &&
}
+ {/* Main content area */}
+
+ {/* Mobile header */}
+ {showHeader && (
+
+ )}
- {/* Main content */}
-
-
- {children}
-
-
+ {/* Desktop header */}
+ {showHeader &&
}
+
+ {/* Main content */}
+
+
+ {children}
+
+
+
-
+
);
}
diff --git a/src/components/command-palette/CommandPalette.tsx b/src/components/command-palette/CommandPalette.tsx
new file mode 100644
index 000000000..02556b7dd
--- /dev/null
+++ b/src/components/command-palette/CommandPalette.tsx
@@ -0,0 +1,368 @@
+"use client";
+
+import {
+ CommandDialog,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator
+} from "@app/components/ui/command";
+import type { SidebarNavSection } from "@app/app/navigation";
+import { Badge } from "@app/components/ui/badge";
+import { ListUserOrgsResponse } from "@server/routers/org";
+import { Loader2 } from "lucide-react";
+import { useRouter } from "next/navigation";
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState
+} from "react";
+import { useTranslations } from "next-intl";
+import { useCommandPaletteActions } from "./useCommandPaletteActions";
+import { useCommandPaletteNavigation } from "./useCommandPaletteNavigation";
+import { useCommandPaletteOrganizations } from "./useCommandPaletteOrganizations";
+import { useCommandPaletteSearch } from "./useCommandPaletteSearch";
+
+type CommandPaletteProps = {
+ orgId?: string;
+ orgs?: ListUserOrgsResponse["orgs"];
+ navItems: SidebarNavSection[];
+};
+
+export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
+ const t = useTranslations();
+ const router = useRouter();
+ const { open, setOpen } = useCommandPalette();
+ const [search, setSearch] = useState("");
+
+ const navigationGroups = useCommandPaletteNavigation(navItems);
+ const organizations = useCommandPaletteOrganizations(orgs);
+ const actions = useCommandPaletteActions(orgId, orgs);
+ const { shouldSearch, sites, resources, users, machineClients, isLoading } =
+ useCommandPaletteSearch({
+ orgId,
+ query: search,
+ enabled: open
+ });
+
+ const handleOpenChange = useCallback(
+ (nextOpen: boolean) => {
+ setOpen(nextOpen);
+ if (!nextOpen) {
+ setSearch("");
+ }
+ },
+ [setOpen]
+ );
+
+ const runCommand = useCallback(
+ (command: () => void) => {
+ setOpen(false);
+ setSearch("");
+ command();
+ },
+ [setOpen]
+ );
+
+ const hasEntityResults =
+ sites.length > 0 ||
+ resources.length > 0 ||
+ users.length > 0 ||
+ machineClients.length > 0;
+
+ return (
+
+
+
+ {t("commandPaletteNoResults")}
+
+ {navigationGroups.map((group) => (
+
+ {group.items.map((item) => (
+
+ runCommand(() => router.push(item.href))
+ }
+ >
+ {item.icon}
+ {item.title}
+
+ ))}
+
+ ))}
+
+ {organizations.length > 1 && (
+ <>
+
+
+ {organizations.map((org) => (
+
+ runCommand(() => router.push(org.href))
+ }
+ >
+ {org.name}
+
+ {org.orgId}
+
+ {org.isPrimaryOrg && (
+
+ {t("primary")}
+
+ )}
+
+ ))}
+
+ >
+ )}
+
+ {shouldSearch && orgId && (
+ <>
+
+ {isLoading && !hasEntityResults ? (
+
+
+ {t("commandPaletteSearching")}
+
+ ) : (
+ <>
+ {sites.length > 0 && (
+
+ {sites.map((site) => (
+
+ runCommand(() =>
+ router.push(site.href)
+ )
+ }
+ >
+
+ {site.name}
+
+
+ ))}
+
+ )}
+ {resources.length > 0 && (
+
+ {resources.map((resource) => (
+
+ runCommand(() =>
+ router.push(
+ resource.href
+ )
+ )
+ }
+ >
+
+ {resource.name}
+
+
+ ))}
+
+ )}
+ {users.length > 0 && (
+
+ {users.map((user) => (
+
+ runCommand(() =>
+ router.push(user.href)
+ )
+ }
+ >
+
+
+ {user.name}
+
+
+ {user.email}
+
+
+
+ ))}
+
+ )}
+ {machineClients.length > 0 && (
+
+ {machineClients.map((client) => (
+
+ runCommand(() =>
+ router.push(client.href)
+ )
+ }
+ >
+
+ {client.name}
+
+
+ ))}
+
+ )}
+ >
+ )}
+ >
+ )}
+
+ {actions.length > 0 && (
+ <>
+
+
+ {actions.map((action) => (
+
+ runCommand(() => {
+ if (action.onSelect) {
+ action.onSelect();
+ } else if (action.href) {
+ router.push(action.href);
+ }
+ })
+ }
+ >
+ {action.icon}
+ {action.label}
+
+ ))}
+
+ >
+ )}
+
+
+ );
+}
+
+/*******************************/
+/* COMMAND PALETTE CONTEXT */
+/*******************************/
+export type CommandPaletteContextValue = {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ toggle: () => void;
+};
+
+const CommandPaletteContext = createContext
(
+ null
+);
+
+export function useCommandPalette() {
+ const context = useContext(CommandPaletteContext);
+ if (!context) {
+ throw new Error(
+ "useCommandPalette must be used within CommandPaletteProvider"
+ );
+ }
+ return context;
+}
+
+//*******************************/
+/* COMMAND PALETTE PROVIDER */
+/*******************************/
+type CommandPaletteProviderProps = {
+ children: React.ReactNode;
+ orgId?: string;
+ orgs?: ListUserOrgsResponse["orgs"];
+ navItems: SidebarNavSection[];
+};
+
+function isEditableTarget(target: EventTarget | null) {
+ if (!(target instanceof HTMLElement)) return false;
+ if (target.isContentEditable) return true;
+ const tagName = target.tagName;
+ return (
+ tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT"
+ );
+}
+
+export function CommandPaletteProvider({
+ children,
+ orgId,
+ orgs,
+ navItems
+}: CommandPaletteProviderProps) {
+ const [open, setOpen] = useState(false);
+
+ const toggle = useCallback(() => {
+ setOpen((current) => !current);
+ }, []);
+
+ const contextValue = useMemo(
+ () => ({
+ open,
+ setOpen,
+ toggle
+ }),
+ [open, toggle]
+ );
+
+ useEffect(() => {
+ function onKeyDown(event: KeyboardEvent) {
+ if (
+ event.key.toLowerCase() !== "k" ||
+ !(event.metaKey || event.ctrlKey)
+ ) {
+ return;
+ }
+
+ if (!open && isEditableTarget(event.target)) {
+ return;
+ }
+
+ event.preventDefault();
+ toggle();
+ }
+
+ document.addEventListener("keydown", onKeyDown);
+ return () => document.removeEventListener("keydown", onKeyDown);
+ }, [open, toggle]);
+
+ return (
+
+ {children}
+
+
+ );
+}
diff --git a/src/components/command-palette/CommandPaletteTrigger.tsx b/src/components/command-palette/CommandPaletteTrigger.tsx
new file mode 100644
index 000000000..785013ae2
--- /dev/null
+++ b/src/components/command-palette/CommandPaletteTrigger.tsx
@@ -0,0 +1,69 @@
+"use client";
+
+import { Button } from "@app/components/ui/button";
+import { CommandShortcut } from "@app/components/ui/command";
+import { cn } from "@app/lib/cn";
+import { Search } from "lucide-react";
+import { useEffect, useState } from "react";
+import { useTranslations } from "next-intl";
+import { useCommandPalette } from "./CommandPalette";
+
+type CommandPaletteTriggerProps = {
+ variant?: "header" | "mobile";
+ className?: string;
+};
+
+function useIsMac() {
+ const [isMac, setIsMac] = useState(false);
+
+ useEffect(() => {
+ setIsMac(/Mac|iPhone|iPod|iPad/.test(navigator.platform));
+ }, []);
+
+ return isMac;
+}
+
+export function CommandPaletteTrigger({
+ variant = "header",
+ className
+}: CommandPaletteTriggerProps) {
+ const t = useTranslations();
+ const { setOpen } = useCommandPalette();
+ const isMac = useIsMac();
+
+ if (variant === "mobile") {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/command-palette/useCommandPaletteActions.tsx b/src/components/command-palette/useCommandPaletteActions.tsx
new file mode 100644
index 000000000..998fb97b8
--- /dev/null
+++ b/src/components/command-palette/useCommandPaletteActions.tsx
@@ -0,0 +1,161 @@
+"use client";
+
+import { useEnvContext } from "@app/hooks/useEnvContext";
+import { useUserContext } from "@app/hooks/useUserContext";
+import { build } from "@server/build";
+import {
+ BellRing,
+ Building2,
+ Globe,
+ KeyRound,
+ MonitorUp,
+ Plus,
+ SunMoon,
+ UserPlus
+} from "lucide-react";
+import { useTheme } from "next-themes";
+import { usePathname } from "next/navigation";
+import type { ReactNode } from "react";
+import { useMemo } from "react";
+import { useTranslations } from "next-intl";
+import { ListUserOrgsResponse } from "@server/routers/org";
+
+export type CommandPaletteAction = {
+ id: string;
+ label: string;
+ icon: ReactNode;
+ href?: string;
+ onSelect?: () => void;
+};
+
+export function useCommandPaletteActions(
+ orgId?: string,
+ orgs?: ListUserOrgsResponse["orgs"]
+): CommandPaletteAction[] {
+ const t = useTranslations();
+ const pathname = usePathname();
+ const { env } = useEnvContext();
+ const { user } = useUserContext();
+ const { setTheme, theme } = useTheme();
+ const isAdminPage = pathname?.startsWith("/admin");
+
+ return useMemo(() => {
+ const actions: CommandPaletteAction[] = [];
+
+ function cycleTheme() {
+ const currentTheme = theme || "system";
+ if (currentTheme === "light") {
+ setTheme("dark");
+ } else if (currentTheme === "dark") {
+ setTheme("system");
+ } else {
+ setTheme("light");
+ }
+ }
+
+ if (isAdminPage) {
+ actions.push({
+ id: "create-admin-api-key",
+ label: t("commandPaletteCreateApiKey"),
+ icon: ,
+ href: "/admin/api-keys/create"
+ });
+
+ if (
+ build === "oss" ||
+ env?.app.identityProviderMode === "global" ||
+ env?.app.identityProviderMode === undefined
+ ) {
+ actions.push({
+ id: "create-admin-idp",
+ label: t("commandPaletteCreateIdentityProvider"),
+ icon: ,
+ href: "/admin/idp/create"
+ });
+ }
+ } else if (orgId) {
+ actions.push({
+ id: "create-site",
+ label: t("commandPaletteCreateSite"),
+ icon: ,
+ href: `/${orgId}/settings/sites/create`
+ });
+ actions.push({
+ id: "create-proxy-resource",
+ label: t("commandPaletteCreateProxyResource"),
+ icon: ,
+ href: `/${orgId}/settings/resources/proxy/create`
+ });
+ actions.push({
+ id: "create-user",
+ label: t("commandPaletteCreateUser"),
+ icon: ,
+ href: `/${orgId}/settings/access/users/create`
+ });
+ actions.push({
+ id: "create-api-key",
+ label: t("commandPaletteCreateApiKey"),
+ icon: ,
+ href: `/${orgId}/settings/api-keys/create`
+ });
+ actions.push({
+ id: "create-machine-client",
+ label: t("commandPaletteCreateMachineClient"),
+ icon: ,
+ href: `/${orgId}/settings/clients/machine/create`
+ });
+
+ if (!env?.flags.disableEnterpriseFeatures) {
+ actions.push({
+ id: "create-alert-rule",
+ label: t("commandPaletteCreateAlertRule"),
+ icon: ,
+ href: `/${orgId}/settings/alerting/create`
+ });
+ }
+
+ if (
+ (build === "oss" && !env?.flags.disableEnterpriseFeatures) ||
+ build === "saas" ||
+ env?.app.identityProviderMode === "org" ||
+ (env?.app.identityProviderMode === undefined && build !== "oss")
+ ) {
+ actions.push({
+ id: "create-idp",
+ label: t("commandPaletteCreateIdentityProvider"),
+ icon: ,
+ href: `/${orgId}/settings/idp/create`
+ });
+ }
+ }
+
+ const canChooseOrganization = !isAdminPage && (orgs?.length ?? 0) > 1;
+
+ if (canChooseOrganization) {
+ actions.push({
+ id: "choose-org",
+ label: t("commandPaletteChooseOrganization"),
+ icon: ,
+ href: "/?orgs=1"
+ });
+ }
+
+ actions.push({
+ id: "toggle-theme",
+ label: t("commandPaletteToggleTheme"),
+ icon: ,
+ onSelect: cycleTheme
+ });
+
+ if (user.serverAdmin && !isAdminPage) {
+ actions.push({
+ id: "go-admin",
+ label: t("serverAdmin"),
+ icon: ,
+ href: "/admin/users"
+ });
+ }
+
+ return actions;
+ }, [isAdminPage, orgId, orgs, env, user.serverAdmin, theme, setTheme, t]);
+}
\ No newline at end of file
diff --git a/src/components/command-palette/useCommandPaletteNavigation.ts b/src/components/command-palette/useCommandPaletteNavigation.ts
new file mode 100644
index 000000000..1968d1a89
--- /dev/null
+++ b/src/components/command-palette/useCommandPaletteNavigation.ts
@@ -0,0 +1,57 @@
+"use client";
+
+import type { SidebarNavSection } from "@app/components/SidebarNav";
+import { flattenNavSections } from "@app/lib/flattenNavItems";
+import {
+ hydrateNavHref,
+ navHrefParamsFromRoute
+} from "@app/lib/hydrateNavHref";
+import { useParams } from "next/navigation";
+import { useMemo } from "react";
+import { useTranslations } from "next-intl";
+
+export type NavigationCommand = {
+ id: string;
+ title: string;
+ href: string;
+ icon?: React.ReactNode;
+ sectionHeading: string;
+};
+
+export type NavigationCommandGroup = {
+ heading: string;
+ items: NavigationCommand[];
+};
+
+export function useCommandPaletteNavigation(
+ navItems: SidebarNavSection[]
+): NavigationCommandGroup[] {
+ const params = useParams();
+ const t = useTranslations();
+
+ return useMemo(() => {
+ const hrefParams = navHrefParamsFromRoute(params);
+ const flat = flattenNavSections(navItems);
+ const groups = new Map();
+
+ for (const item of flat) {
+ const href = hydrateNavHref(item.href, hrefParams);
+ if (!href) continue;
+
+ const groupItems = groups.get(item.sectionHeading) ?? [];
+ groupItems.push({
+ id: `nav-${item.sectionHeading}-${item.title}-${href}`,
+ title: t(item.title),
+ href,
+ icon: item.icon,
+ sectionHeading: item.sectionHeading
+ });
+ groups.set(item.sectionHeading, groupItems);
+ }
+
+ return Array.from(groups.entries()).map(([heading, items]) => ({
+ heading: t(heading),
+ items
+ }));
+ }, [navItems, params, t]);
+}
diff --git a/src/components/command-palette/useCommandPaletteOrganizations.ts b/src/components/command-palette/useCommandPaletteOrganizations.ts
new file mode 100644
index 000000000..1e5343ccd
--- /dev/null
+++ b/src/components/command-palette/useCommandPaletteOrganizations.ts
@@ -0,0 +1,45 @@
+"use client";
+
+import { ListUserOrgsResponse } from "@server/routers/org";
+import { usePathname } from "next/navigation";
+import { useMemo } from "react";
+
+export type OrganizationCommand = {
+ id: string;
+ orgId: string;
+ name: string;
+ isPrimaryOrg?: boolean;
+ href: string;
+};
+
+export function useCommandPaletteOrganizations(
+ orgs: ListUserOrgsResponse["orgs"] | undefined
+): OrganizationCommand[] {
+ const pathname = usePathname();
+
+ return useMemo(() => {
+ if (!orgs?.length) return [];
+
+ const sortedOrgs = [...orgs].sort((a, b) => {
+ const aPrimary = Boolean(a.isPrimaryOrg);
+ const bPrimary = Boolean(b.isPrimaryOrg);
+ if (aPrimary && !bPrimary) return -1;
+ if (!aPrimary && bPrimary) return 1;
+ return 0;
+ });
+
+ return sortedOrgs.map((org) => {
+ const newPath = pathname.includes("/settings/")
+ ? pathname.replace(/^\/[^/]+/, `/${org.orgId}`)
+ : `/${org.orgId}`;
+
+ return {
+ id: `org-${org.orgId}`,
+ orgId: org.orgId,
+ name: org.name,
+ isPrimaryOrg: org.isPrimaryOrg,
+ href: newPath
+ };
+ });
+ }, [orgs, pathname]);
+}
diff --git a/src/components/command-palette/useCommandPaletteSearch.ts b/src/components/command-palette/useCommandPaletteSearch.ts
new file mode 100644
index 000000000..984892354
--- /dev/null
+++ b/src/components/command-palette/useCommandPaletteSearch.ts
@@ -0,0 +1,142 @@
+"use client";
+
+import { orgQueries } from "@app/lib/queries";
+import { useQueries } from "@tanstack/react-query";
+import { useMemo } from "react";
+import { useDebounce } from "use-debounce";
+
+const SEARCH_PER_PAGE = 5;
+const MIN_QUERY_LENGTH = 2;
+
+export type SiteSearchResult = {
+ id: string;
+ name: string;
+ href: string;
+};
+
+export type ResourceSearchResult = {
+ id: string;
+ name: string;
+ href: string;
+};
+
+export type UserSearchResult = {
+ id: string;
+ name: string;
+ email: string;
+ href: string;
+};
+
+export type ClientSearchResult = {
+ id: string;
+ name: string;
+ href: string;
+};
+
+export function useCommandPaletteSearch({
+ orgId,
+ query,
+ enabled
+}: {
+ orgId?: string;
+ query: string;
+ enabled: boolean;
+}) {
+ const [debouncedQuery] = useDebounce(query, 150);
+ const trimmedQuery = debouncedQuery.trim();
+ const shouldSearch =
+ enabled && !!orgId && trimmedQuery.length >= MIN_QUERY_LENGTH;
+
+ const [sitesQuery, resourcesQuery, usersQuery, clientsQuery] = useQueries({
+ queries: [
+ {
+ ...orgQueries.sites({
+ orgId: orgId ?? "",
+ query: trimmedQuery,
+ perPage: SEARCH_PER_PAGE
+ }),
+ enabled: shouldSearch
+ },
+ {
+ ...orgQueries.resources({
+ orgId: orgId ?? "",
+ query: trimmedQuery,
+ perPage: SEARCH_PER_PAGE
+ }),
+ enabled: shouldSearch
+ },
+ {
+ ...orgQueries.users({
+ orgId: orgId ?? "",
+ query: trimmedQuery,
+ perPage: SEARCH_PER_PAGE
+ }),
+ enabled: shouldSearch
+ },
+ {
+ ...orgQueries.machineClients({
+ orgId: orgId ?? "",
+ query: trimmedQuery,
+ perPage: SEARCH_PER_PAGE
+ }),
+ enabled: shouldSearch
+ }
+ ]
+ });
+
+ const sites = useMemo((): SiteSearchResult[] => {
+ if (!orgId || !sitesQuery.data) return [];
+ return sitesQuery.data.map((site) => ({
+ id: `site-${site.siteId}`,
+ name: site.name,
+ href: `/${orgId}/settings/sites/${site.niceId}`
+ }));
+ }, [orgId, sitesQuery.data]);
+
+ const resources = useMemo((): ResourceSearchResult[] => {
+ if (!orgId || !resourcesQuery.data) return [];
+ return resourcesQuery.data.map((resource) => ({
+ id: `resource-${resource.resourceId}`,
+ name: resource.name,
+ href: `/${orgId}/settings/resources/proxy/${resource.niceId}`
+ }));
+ }, [orgId, resourcesQuery.data]);
+
+ const users = useMemo((): UserSearchResult[] => {
+ if (!orgId || !usersQuery.data) return [];
+ return usersQuery.data.map((user) => ({
+ id: `user-${user.id}`,
+ name: user.name ?? user.email ?? user.username ?? "",
+ email: user.email ?? user.username ?? "",
+ href: `/${orgId}/settings/access/users/${user.id}`
+ }));
+ }, [orgId, usersQuery.data]);
+
+ const machineClients = useMemo((): ClientSearchResult[] => {
+ if (!orgId || !clientsQuery.data) return [];
+ return clientsQuery.data
+ .filter((client) => !client.userId)
+ .map((client) => ({
+ id: `client-${client.clientId}`,
+ name: client.name,
+ href: `/${orgId}/settings/clients/machine/${client.niceId}`
+ }));
+ }, [orgId, clientsQuery.data]);
+
+ const isLoading =
+ shouldSearch &&
+ (sitesQuery.isFetching ||
+ resourcesQuery.isFetching ||
+ usersQuery.isFetching ||
+ clientsQuery.isFetching);
+
+ return {
+ debouncedQuery: trimmedQuery,
+ shouldSearch,
+ sites,
+ resources,
+ users,
+ machineClients,
+ isLoading
+ };
+}
diff --git a/src/lib/flattenNavItems.ts b/src/lib/flattenNavItems.ts
new file mode 100644
index 000000000..b64268322
--- /dev/null
+++ b/src/lib/flattenNavItems.ts
@@ -0,0 +1,42 @@
+import type { ReactNode } from "react";
+import type {
+ SidebarNavItem,
+ SidebarNavSection
+} from "@app/components/SidebarNav";
+
+export type FlatNavItem = {
+ title: string;
+ href: string;
+ icon?: ReactNode;
+ sectionHeading: string;
+};
+
+function flattenItems(
+ items: SidebarNavItem[],
+ sectionHeading: string,
+ result: FlatNavItem[]
+) {
+ for (const item of items) {
+ if (item.href) {
+ result.push({
+ title: item.title,
+ href: item.href,
+ icon: item.icon,
+ sectionHeading
+ });
+ }
+ if (item.items?.length) {
+ flattenItems(item.items, sectionHeading, result);
+ }
+ }
+}
+
+export function flattenNavSections(
+ sections: SidebarNavSection[]
+): FlatNavItem[] {
+ const result: FlatNavItem[] = [];
+ for (const section of sections) {
+ flattenItems(section.items, section.heading, result);
+ }
+ return result;
+}
diff --git a/src/lib/hydrateNavHref.ts b/src/lib/hydrateNavHref.ts
new file mode 100644
index 000000000..45fa0f5ee
--- /dev/null
+++ b/src/lib/hydrateNavHref.ts
@@ -0,0 +1,35 @@
+export type NavHrefParams = {
+ orgId?: string;
+ niceId?: string;
+ resourceId?: string;
+ userId?: string;
+ apiKeyId?: string;
+ clientId?: string;
+};
+
+export function hydrateNavHref(
+ val: string | undefined,
+ params: NavHrefParams
+): string | undefined {
+ if (!val) return undefined;
+ return val
+ .replace("{orgId}", params.orgId ?? "")
+ .replace("{niceId}", params.niceId ?? "")
+ .replace("{resourceId}", params.resourceId ?? "")
+ .replace("{userId}", params.userId ?? "")
+ .replace("{apiKeyId}", params.apiKeyId ?? "")
+ .replace("{clientId}", params.clientId ?? "");
+}
+
+export function navHrefParamsFromRoute(
+ params: Record
+): NavHrefParams {
+ return {
+ orgId: params.orgId as string | undefined,
+ niceId: params.niceId as string | undefined,
+ resourceId: params.resourceId as string | undefined,
+ userId: params.userId as string | undefined,
+ apiKeyId: params.apiKeyId as string | undefined,
+ clientId: params.clientId as string | undefined
+ };
+}
From a68b57067c5b93662e18c1da0f9f48d3d41ebff9 Mon Sep 17 00:00:00 2001
From: Fred KISSIE
Date: Fri, 12 Jun 2026 23:51:05 +0200
Subject: [PATCH 03/53] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20Use=20command=20bar?=
=?UTF-8?q?=20nav=20items?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/[orgId]/settings/layout.tsx | 5 +-
src/app/navigation.tsx | 250 ++++++++++++++++++
src/components/Layout.tsx | 8 +-
src/components/SitesTable.tsx | 7 -
.../command-palette/CommandPalette.tsx | 85 +++---
5 files changed, 315 insertions(+), 40 deletions(-)
diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx
index 6a3d648a4..cac644be3 100644
--- a/src/app/[orgId]/settings/layout.tsx
+++ b/src/app/[orgId]/settings/layout.tsx
@@ -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}
diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx
index 43323ba2f..e0ada4c1c 100644
--- a/src/app/navigation.tsx
+++ b/src/app/navigation.tsx
@@ -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:
+ },
+ {
+ title: "sidebarResources",
+ icon: ,
+ items: [
+ {
+ title: "sidebarProxyResources",
+ href: "/{orgId}/settings/resources/public",
+ icon:
+ },
+ {
+ title: "sidebarClientResources",
+ href: "/{orgId}/settings/resources/private",
+ icon:
+ }
+ ]
+ },
+ {
+ title: "sidebarClients",
+ icon: ,
+ items: [
+ {
+ href: "/{orgId}/settings/clients/user",
+ title: "sidebarUserDevices",
+ icon:
+ },
+ {
+ href: "/{orgId}/settings/clients/machine",
+ title: "sidebarMachineClients",
+ icon:
+ }
+ ]
+ },
+ {
+ title: "sidebarDomains",
+ href: "/{orgId}/settings/domains",
+ icon:
+ },
+ ...(build === "saas"
+ ? [
+ {
+ title: "sidebarRemoteExitNodes",
+ href: "/{orgId}/settings/remote-exit-nodes",
+ icon:
+ }
+ ]
+ : [])
+ ]
+ },
+ {
+ heading: "accessControl",
+ items: [
+ {
+ title: "sidebarTeam",
+ icon: ,
+ items: [
+ {
+ title: "sidebarUsers",
+ href: "/{orgId}/settings/access/users",
+ icon:
+ },
+ {
+ title: "sidebarRoles",
+ href: "/{orgId}/settings/access/roles",
+ icon:
+ },
+ {
+ title: "sidebarInvitations",
+ href: "/{orgId}/settings/access/invitations",
+ icon:
+ }
+ ]
+ },
+ ...(!env?.flags.disableEnterpriseFeatures
+ ? [
+ {
+ title: "sidebarPolicies",
+
+ icon: ,
+ items: [
+ {
+ title: "sidebarResourcePolicies",
+ href: "/{orgId}/settings/policies/resources/public",
+ icon: (
+
+ )
+ }
+ ]
+ }
+ ]
+ : []),
+ // 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:
+ }
+ ]
+ : []),
+ ...(!env?.flags.disableEnterpriseFeatures
+ ? [
+ {
+ title: "sidebarApprovals",
+ href: "/{orgId}/settings/access/approvals",
+ icon:
+ }
+ ]
+ : []),
+ {
+ title: "sidebarShareableLinks",
+ href: "/{orgId}/settings/share-links",
+ icon:
+ }
+ ]
+ },
+ {
+ heading: "sidebarOrganization",
+ items: [
+ {
+ title: "sidebarLogsAndAnalytics",
+ icon: ,
+ items: [
+ {
+ title: "sidebarLogsAnalytics",
+ href: "/{orgId}/settings/logs/analytics",
+ icon:
+ },
+ {
+ title: "sidebarLogsRequest",
+ href: "/{orgId}/settings/logs/request",
+ icon: (
+
+ )
+ },
+ ...(!env?.flags.disableEnterpriseFeatures
+ ? [
+ {
+ title: "sidebarLogsAccess",
+ href: "/{orgId}/settings/logs/access",
+ icon:
+ },
+ {
+ title: "sidebarLogsAction",
+ href: "/{orgId}/settings/logs/action",
+ icon:
+ },
+ {
+ title: "sidebarLogsConnection",
+ href: "/{orgId}/settings/logs/connection",
+ icon:
+ },
+ {
+ title: "sidebarLogsStreaming",
+ href: "/{orgId}/settings/logs/streaming",
+ icon:
+ }
+ ]
+ : [])
+ ]
+ },
+ {
+ title: "sidebarManagement",
+ icon: ,
+ items: [
+ ...(!env?.flags.disableEnterpriseFeatures
+ ? [
+ {
+ title: "sidebarAlerting",
+ href: "/{orgId}/settings/alerting",
+ icon: (
+
+ )
+ },
+ {
+ title: "sidebarProvisioning",
+ href: "/{orgId}/settings/provisioning",
+ icon:
+ }
+ ]
+ : []),
+ {
+ title: "sidebarBluePrints",
+ href: "/{orgId}/settings/blueprints",
+ icon:
+ },
+ {
+ title: "sidebarApiKeys",
+ href: "/{orgId}/settings/api-keys",
+ icon:
+ },
+ ...(!env?.flags.disableEnterpriseFeatures
+ ? [
+ {
+ title: "labels",
+ href: "/{orgId}/settings/labels",
+ icon:
+ }
+ ]
+ : [])
+ ]
+ },
+ ...(build === "saas" && options?.isPrimaryOrg
+ ? [
+ {
+ title: "sidebarBillingAndLicenses",
+ icon: ,
+ items: [
+ {
+ title: "sidebarBilling",
+ href: "/{orgId}/settings/billing",
+ icon: (
+
+ )
+ },
+ {
+ title: "sidebarEnterpriseLicenses",
+ href: "/{orgId}/settings/license",
+ icon: (
+
+ )
+ }
+ ]
+ }
+ ]
+ : []),
+ {
+ title: "sidebarSettings",
+ href: "/{orgId}/settings/general",
+ icon:
+ }
+ ]
+ }
+];
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index c0f814453..8dc95968f 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -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 (
-
+
{/* Desktop Sidebar */}
{showSidebar && (
diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx
index 8c3036c4a..98b69fa3e 100644
--- a/src/components/SitesTable.tsx
+++ b/src/components/SitesTable.tsx
@@ -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()
diff --git a/src/components/command-palette/CommandPalette.tsx b/src/components/command-palette/CommandPalette.tsx
index 02556b7dd..b0536d8a9 100644
--- a/src/components/command-palette/CommandPalette.tsx
+++ b/src/components/command-palette/CommandPalette.tsx
@@ -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 (
-
+
{t("commandPaletteNoResults")}
- {navigationGroups.map((group) => (
-
- {group.items.map((item) => (
-
- runCommand(() => router.push(item.href))
- }
- >
- {item.icon}
- {item.title}
-
- ))}
-
+
+
+ {navigationGroups.map((group, idx) => (
+
+ {idx > 0 && }
+
+ {group.items.map((item) => (
+
+ runCommand(() => router.push(item.href))
+ }
+ className="h-9"
+ >
+ {item.icon}
+ {item.title}
+
+ ))}
+
+
))}
- {organizations.length > 1 && (
+ {/* {organizations.length > 1 && (
<>
>
- )}
+ )} */}
- {shouldSearch && orgId && (
+ {/* {shouldSearch && orgId && (
<>
{isLoading && !hasEntityResults ? (
@@ -243,12 +263,15 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
>
)}
>
- )}
+ )} */}
- {actions.length > 0 && (
+ {/* {actions.length > 0 && (
<>
-
+
{actions.map((action) => (
>
- )}
+ )} */}
);
From abc0a41d9ecf9a27a023202a729311d748c63310 Mon Sep 17 00:00:00 2001
From: Fred KISSIE
Date: Sat, 13 Jun 2026 00:27:39 +0200
Subject: [PATCH 04/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20reorganize=20command?=
=?UTF-8?q?=20bar=20items?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
messages/en-US.json | 36 ++
src/app/navigation.tsx | 328 +++++++++---------
src/components/Layout.tsx | 7 +-
.../command-palette/CommandPalette.tsx | 27 +-
.../useCommandPaletteNavigation.ts | 6 +-
src/components/ui/command.tsx | 6 +-
6 files changed, 223 insertions(+), 187 deletions(-)
diff --git a/messages/en-US.json b/messages/en-US.json
index e18385159..e6567cc08 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -1590,6 +1590,42 @@
"sidebarManagement": "Management",
"sidebarBillingAndLicenses": "Billing & Licenses",
"sidebarLogsAnalytics": "Analytics",
+ "commandSites": "Sites",
+ "commandActionModeInfo": "Type \">\" to open action mode",
+ "commandResources": "Resources",
+ "commandProxyResources": "Public Resources",
+ "commandClientResources": "Private Resources",
+ "commandClients": "Clients",
+ "commandUserDevices": "User Devices",
+ "commandMachineClients": "Machine Clients",
+ "commandDomains": "Domains",
+ "commandRemoteExitNodes": "Remote Nodes",
+ "commandTeam": "Team",
+ "commandUsers": "Users",
+ "commandRoles": "Roles",
+ "commandInvitations": "Invitations",
+ "commandPolicies": "Shared Policies",
+ "commandResourcePolicies": "Public Resources Policies",
+ "commandIdentityProviders": "Identity Providers",
+ "commandApprovals": "Approval Requests",
+ "commandShareableLinks": "Shareable Links",
+ "commandOrganization": "Organization",
+ "commandLogsAndAnalytics": "Logs & Analytics",
+ "commandLogsAnalytics": "Analytics",
+ "commandLogsRequest": "HTTP Request Logs",
+ "commandLogsAccess": "Authentication Logs",
+ "commandLogsAction": "Admin Action Logs",
+ "commandLogsConnection": "Network Logs",
+ "commandLogsStreaming": "Event Streaming",
+ "commandManagement": "Management",
+ "commandAlerting": "Alerting",
+ "commandProvisioning": "Provisioning",
+ "commandBluePrints": "Blueprints",
+ "commandApiKeys": "API Keys",
+ "commandBillingAndLicenses": "Billing & Licenses",
+ "commandBilling": "Billing",
+ "commandEnterpriseLicenses": "Licenses",
+ "commandSettings": "Settings",
"alertingTitle": "Alerting",
"alertingDescription": "Define sources, triggers, and actions for notifications",
"alertingRules": "Alert rules",
diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx
index e0ada4c1c..b3eab82ed 100644
--- a/src/app/navigation.tsx
+++ b/src/app/navigation.tsx
@@ -341,59 +341,56 @@ export const adminNavSections = (env?: Env): SidebarNavSection[] => [
}
];
+export type CommandBarNavSection = {
+ // Added from 'dev' branch
+ heading: string;
+ items: CommandBarNavItem[];
+};
+
+export type CommandBarNavItem = {
+ href?: string;
+ title: string;
+ icon?: React.ReactNode;
+ showEE?: boolean;
+ isBeta?: boolean;
+};
+
export const commandBarNavSections = (
env?: Env,
options?: OrgNavSectionsOptions
-): SidebarNavSection[] => [
+): CommandBarNavSection[] => [
{
heading: "network",
items: [
{
- title: "sidebarSites",
+ title: "commandSites",
href: "/{orgId}/settings/sites",
icon:
},
{
- title: "sidebarResources",
- icon: ,
- items: [
- {
- title: "sidebarProxyResources",
- href: "/{orgId}/settings/resources/public",
- icon:
- },
- {
- title: "sidebarClientResources",
- href: "/{orgId}/settings/resources/private",
- icon:
- }
- ]
- },
- {
- title: "sidebarClients",
- icon: ,
- items: [
- {
- href: "/{orgId}/settings/clients/user",
- title: "sidebarUserDevices",
- icon:
- },
- {
- href: "/{orgId}/settings/clients/machine",
- title: "sidebarMachineClients",
- icon:
- }
- ]
- },
- {
- title: "sidebarDomains",
- href: "/{orgId}/settings/domains",
+ title: "commandProxyResources",
+ href: "/{orgId}/settings/resources/public",
icon:
},
+ {
+ title: "commandClientResources",
+ href: "/{orgId}/settings/resources/private",
+ icon:
+ },
+ {
+ href: "/{orgId}/settings/clients/user",
+ title: "commandUserDevices",
+ icon:
+ },
+ {
+ href: "/{orgId}/settings/clients/machine",
+ title: "commandMachineClients",
+ icon:
+ },
...(build === "saas"
? [
{
- title: "sidebarRemoteExitNodes",
+ title: "commandRemoteExitNodes",
href: "/{orgId}/settings/remote-exit-nodes",
icon:
}
@@ -402,44 +399,34 @@ export const commandBarNavSections = (
]
},
{
- heading: "accessControl",
+ heading: "commandTeam",
items: [
{
- title: "sidebarTeam",
- icon: ,
- items: [
- {
- title: "sidebarUsers",
- href: "/{orgId}/settings/access/users",
- icon:
- },
- {
- title: "sidebarRoles",
- href: "/{orgId}/settings/access/roles",
- icon:
- },
- {
- title: "sidebarInvitations",
- href: "/{orgId}/settings/access/invitations",
- icon:
- }
- ]
+ title: "commandUsers",
+ href: "/{orgId}/settings/access/users",
+ icon:
},
+ {
+ title: "commandRoles",
+ href: "/{orgId}/settings/access/roles",
+ icon:
+ },
+ {
+ title: "commandInvitations",
+ href: "/{orgId}/settings/access/invitations",
+ icon:
+ }
+ ]
+ },
+ {
+ heading: "accessControl",
+ items: [
...(!env?.flags.disableEnterpriseFeatures
? [
{
- title: "sidebarPolicies",
-
- icon: ,
- items: [
- {
- title: "sidebarResourcePolicies",
- href: "/{orgId}/settings/policies/resources/public",
- icon: (
-
- )
- }
- ]
+ title: "commandResourcePolicies",
+ href: "/{orgId}/settings/policies/resources/public",
+ icon:
}
]
: []),
@@ -450,7 +437,7 @@ export const commandBarNavSections = (
(env?.app.identityProviderMode === undefined && build !== "oss")
? [
{
- title: "sidebarIdentityProviders",
+ title: "commandIdentityProviders",
href: "/{orgId}/settings/idp",
icon:
}
@@ -459,134 +446,129 @@ export const commandBarNavSections = (
...(!env?.flags.disableEnterpriseFeatures
? [
{
- title: "sidebarApprovals",
+ title: "commandApprovals",
href: "/{orgId}/settings/access/approvals",
icon:
}
]
: []),
{
- title: "sidebarShareableLinks",
+ title: "commandShareableLinks",
href: "/{orgId}/settings/share-links",
icon:
}
]
},
{
- heading: "sidebarOrganization",
+ heading: "commandLogsAndAnalytics",
items: [
{
- title: "sidebarLogsAndAnalytics",
- icon: ,
- items: [
- {
- title: "sidebarLogsAnalytics",
- href: "/{orgId}/settings/logs/analytics",
- icon:
- },
- {
- title: "sidebarLogsRequest",
- href: "/{orgId}/settings/logs/request",
- icon: (
-
- )
- },
- ...(!env?.flags.disableEnterpriseFeatures
- ? [
- {
- title: "sidebarLogsAccess",
- href: "/{orgId}/settings/logs/access",
- icon:
- },
- {
- title: "sidebarLogsAction",
- href: "/{orgId}/settings/logs/action",
- icon:
- },
- {
- title: "sidebarLogsConnection",
- href: "/{orgId}/settings/logs/connection",
- icon:
- },
- {
- title: "sidebarLogsStreaming",
- href: "/{orgId}/settings/logs/streaming",
- icon:
- }
- ]
- : [])
- ]
+ title: "commandLogsAnalytics",
+ href: "/{orgId}/settings/logs/analytics",
+ icon:
},
{
- title: "sidebarManagement",
- icon: ,
- items: [
- ...(!env?.flags.disableEnterpriseFeatures
- ? [
- {
- title: "sidebarAlerting",
- href: "/{orgId}/settings/alerting",
- icon: (
-
- )
- },
- {
- title: "sidebarProvisioning",
- href: "/{orgId}/settings/provisioning",
- icon:
- }
- ]
- : []),
- {
- title: "sidebarBluePrints",
- href: "/{orgId}/settings/blueprints",
- icon:
- },
- {
- title: "sidebarApiKeys",
- href: "/{orgId}/settings/api-keys",
- icon:
- },
- ...(!env?.flags.disableEnterpriseFeatures
- ? [
- {
- title: "labels",
- href: "/{orgId}/settings/labels",
- icon:
- }
- ]
- : [])
- ]
+ title: "commandLogsRequest",
+ href: "/{orgId}/settings/logs/request",
+ icon:
},
- ...(build === "saas" && options?.isPrimaryOrg
+ ...(!env?.flags.disableEnterpriseFeatures
? [
{
- title: "sidebarBillingAndLicenses",
- icon: ,
- items: [
- {
- title: "sidebarBilling",
- href: "/{orgId}/settings/billing",
- icon: (
-
- )
- },
- {
- title: "sidebarEnterpriseLicenses",
- href: "/{orgId}/settings/license",
- icon: (
-
- )
- }
- ]
+ title: "commandLogsAccess",
+ href: "/{orgId}/settings/logs/access",
+ icon:
+ },
+ {
+ title: "commandLogsAction",
+ href: "/{orgId}/settings/logs/action",
+ icon:
+ },
+ {
+ title: "commandLogsConnection",
+ href: "/{orgId}/settings/logs/connection",
+ icon:
+ },
+ {
+ title: "commandLogsStreaming",
+ href: "/{orgId}/settings/logs/streaming",
+ icon:
+ }
+ ]
+ : [])
+ ]
+ },
+ {
+ heading: "commandManagement",
+ items: [
+ ...(!env?.flags.disableEnterpriseFeatures
+ ? [
+ {
+ title: "commandAlerting",
+ href: "/{orgId}/settings/alerting",
+ icon:
+ },
+ {
+ title: "commandProvisioning",
+ href: "/{orgId}/settings/provisioning",
+ icon:
}
]
: []),
{
- title: "sidebarSettings",
+ title: "commandBluePrints",
+ href: "/{orgId}/settings/blueprints",
+ icon:
+ },
+ {
+ title: "commandApiKeys",
+ href: "/{orgId}/settings/api-keys",
+ icon:
+ },
+ ...(!env?.flags.disableEnterpriseFeatures
+ ? [
+ {
+ title: "labels",
+ href: "/{orgId}/settings/labels",
+ icon:
+ }
+ ]
+ : [])
+ ]
+ },
+
+ {
+ heading: "commandOrganization",
+ items: [
+ {
+ title: "commandSettings",
href: "/{orgId}/settings/general",
icon:
+ },
+ {
+ title: "commandDomains",
+ href: "/{orgId}/settings/domains",
+ icon:
}
]
- }
+ },
+ ...(build === "saas" && options?.isPrimaryOrg
+ ? [
+ {
+ heading: "commandBillingAndLicenses",
+ items: [
+ {
+ title: "commandBilling",
+ href: "/{orgId}/settings/billing",
+ icon:
+ },
+ {
+ title: "commandEnterpriseLicenses",
+ href: "/{orgId}/settings/license",
+ icon:
+ }
+ ]
+ }
+ ]
+ : [])
];
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index 8dc95968f..991c489a7 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -1,7 +1,10 @@
import React from "react";
import { cn } from "@app/lib/cn";
import { ListUserOrgsResponse } from "@server/routers/org";
-import type { SidebarNavSection } from "@app/app/navigation";
+import type {
+ CommandBarNavSection,
+ SidebarNavSection
+} from "@app/app/navigation";
import { LayoutSidebar } from "@app/components/LayoutSidebar";
import { LayoutHeader } from "@app/components/LayoutHeader";
import { LayoutMobileMenu } from "@app/components/LayoutMobileMenu";
@@ -13,7 +16,7 @@ interface LayoutProps {
orgId?: string;
orgs?: ListUserOrgsResponse["orgs"];
navItems?: SidebarNavSection[];
- commandNavItems?: SidebarNavSection[];
+ commandNavItems?: CommandBarNavSection[];
showSidebar?: boolean;
showHeader?: boolean;
showTopBar?: boolean;
diff --git a/src/components/command-palette/CommandPalette.tsx b/src/components/command-palette/CommandPalette.tsx
index b0536d8a9..bbf5b9187 100644
--- a/src/components/command-palette/CommandPalette.tsx
+++ b/src/components/command-palette/CommandPalette.tsx
@@ -9,7 +9,10 @@ import {
CommandList,
CommandSeparator
} from "@app/components/ui/command";
-import type { SidebarNavSection } from "@app/app/navigation";
+import type {
+ CommandBarNavSection,
+ SidebarNavSection
+} from "@app/app/navigation";
import { Badge } from "@app/components/ui/badge";
import { ListUserOrgsResponse } from "@server/routers/org";
import { Loader2 } from "lucide-react";
@@ -28,11 +31,12 @@ import { useCommandPaletteNavigation } from "./useCommandPaletteNavigation";
import { useCommandPaletteOrganizations } from "./useCommandPaletteOrganizations";
import { useCommandPaletteSearch } from "./useCommandPaletteSearch";
import { useUserContext } from "@app/hooks/useUserContext";
+import { cn } from "@app/lib/cn";
type CommandPaletteProps = {
orgId?: string;
orgs?: ListUserOrgsResponse["orgs"];
- navItems: SidebarNavSection[];
+ navItems: CommandBarNavSection[];
};
/**
@@ -89,28 +93,35 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
title={t("commandPaletteTitle")}
description={t("commandPaletteDescription")}
className="max-w-2xl **:data-[slot=command-input-wrapper]:h-15"
+ commandProps={{
+ loop: true
+ }}
>
-
+
{t("commandPaletteNoResults")}
- {navigationGroups.map((group, idx) => (
+ {navigationGroups.map((group, groupIndex) => (
- {idx > 0 && }
+ {groupIndex > 0 && }
0 &&
+ "[&_[cmdk-group-heading]]:pt-3"
+ )}
>
- {group.items.map((item) => (
+ {group.items.map((item, itemIndex) => (
;
}) {
return (
+
+ );
+ })}
{/* {organizations.length > 1 && (
<>
@@ -282,7 +306,7 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
>
)} */}
- {/* {actions.length > 0 && (
+ {isActionMode && actions.length > 0 && (
<>
>
- )} */}
+ )}
);
diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx
index f4b9e1bfe..efd1c35f5 100644
--- a/src/components/ui/command.tsx
+++ b/src/components/ui/command.tsx
@@ -2,7 +2,7 @@
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk";
-import { SearchIcon } from "lucide-react";
+import { LoaderIcon, SearchIcon } from "lucide-react";
import {
Dialog,
@@ -68,14 +68,21 @@ function CommandDialog({
function CommandInput({
className,
+ isLoading,
...props
-}: React.ComponentProps) {
+}: React.ComponentProps & {
+ isLoading?: boolean;
+}) {
return (
-
+ {isLoading ? (
+
+ ) : (
+
+ )}
Date: Tue, 16 Jun 2026 22:28:04 +0200
Subject: [PATCH 08/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20rename=20query,=20?=
=?UTF-8?q?=20add=20query?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/CreateShareLinkForm.tsx | 2 +-
.../alert-rule-editor/AlertRuleFields.tsx | 2 +-
src/components/resource-selector.tsx | 2 +-
src/lib/queries.ts | 82 ++++++++++++++++++-
4 files changed, 81 insertions(+), 7 deletions(-)
diff --git a/src/components/CreateShareLinkForm.tsx b/src/components/CreateShareLinkForm.tsx
index 78f705445..95884b486 100644
--- a/src/components/CreateShareLinkForm.tsx
+++ b/src/components/CreateShareLinkForm.tsx
@@ -90,7 +90,7 @@ export default function CreateShareLinkForm({
const t = useTranslations();
const { data: allResources = [] } = useQuery(
- orgQueries.resources({ orgId: org?.org.orgId ?? "" })
+ orgQueries.proxyResources({ orgId: org?.org.orgId ?? "" })
);
const [selectedResource, setSelectedResource] =
diff --git a/src/components/alert-rule-editor/AlertRuleFields.tsx b/src/components/alert-rule-editor/AlertRuleFields.tsx
index d787595ed..7ae6d0a04 100644
--- a/src/components/alert-rule-editor/AlertRuleFields.tsx
+++ b/src/components/alert-rule-editor/AlertRuleFields.tsx
@@ -348,7 +348,7 @@ function ResourceMultiSelect({
const [debounced] = useDebounce(q, 150);
const { data: resources = [] } = useQuery(
- orgQueries.resources({ orgId, query: debounced, perPage: 10 })
+ orgQueries.proxyResources({ orgId, query: debounced, perPage: 10 })
);
const shown = useMemo(() => {
diff --git a/src/components/resource-selector.tsx b/src/components/resource-selector.tsx
index b01263333..625bb2d64 100644
--- a/src/components/resource-selector.tsx
+++ b/src/components/resource-selector.tsx
@@ -39,7 +39,7 @@ export function ResourceSelector({
const [debouncedSearchQuery] = useDebounce(resourceSearchQuery, 150);
const { data: resources = [] } = useQuery(
- orgQueries.resources({
+ orgQueries.proxyResources({
orgId: orgId,
query: debouncedSearchQuery,
perPage: 10
diff --git a/src/lib/queries.ts b/src/lib/queries.ts
index 7d224c7b1..18e937ceb 100644
--- a/src/lib/queries.ts
+++ b/src/lib/queries.ts
@@ -7,7 +7,10 @@ import type {
QueryConnectionAuditLogResponse,
QueryRequestAuditLogResponse
} from "@server/routers/auditLogs/types";
-import type { ListClientsResponse } from "@server/routers/client";
+import type {
+ ListClientsResponse,
+ ListUserDevicesResponse
+} from "@server/routers/client";
import type {
GetDNSRecordsResponse,
ListDomainsResponse
@@ -25,6 +28,7 @@ import type {
import type { ListRolesResponse } from "@server/routers/role";
import type { ListSitesResponse } from "@server/routers/site";
import type {
+ ListAllSiteResourcesByOrgResponse,
ListSiteResourceClientsResponse,
ListSiteResourceRolesResponse,
ListSiteResourceUsersResponse
@@ -38,7 +42,7 @@ import {
queryOptions
} from "@tanstack/react-query";
import type { AxiosResponse } from "axios";
-import z from "zod";
+import z, { meta } from "zod";
import { remote } from "./api";
import { durationToMs } from "./durationToMs";
import type { ListOrgLabelsResponse } from "@server/routers/labels/types";
@@ -138,6 +142,38 @@ export const orgQueries = {
return res.data.data.clients;
}
}),
+ userDevices: ({
+ orgId,
+ query,
+ perPage = 10_000
+ }: {
+ orgId: string;
+ query?: string;
+ perPage?: number;
+ }) =>
+ queryOptions({
+ queryKey: [
+ "ORG",
+ orgId,
+ "USER_DEVICES",
+ { query, perPage }
+ ] as const,
+ queryFn: async ({ signal, meta }) => {
+ const sp = new URLSearchParams({
+ pageSize: perPage.toString()
+ });
+
+ if (query?.trim()) {
+ sp.set("query", query);
+ }
+
+ const res = await meta!.api.get<
+ AxiosResponse
+ >(`/org/${orgId}/user-devices?${sp.toString()}`, { signal });
+
+ return res.data.data.devices;
+ }
+ }),
users: ({
orgId,
query,
@@ -282,7 +318,7 @@ export const orgQueries = {
}
}),
- resources: ({
+ proxyResources: ({
orgId,
query,
perPage = 10_000
@@ -292,7 +328,12 @@ export const orgQueries = {
perPage?: number;
}) =>
queryOptions({
- queryKey: ["ORG", orgId, "RESOURCES", { query, perPage }] as const,
+ queryKey: [
+ "ORG",
+ orgId,
+ "PROXY_RESOURCES",
+ { query, perPage }
+ ] as const,
queryFn: async ({ signal, meta }) => {
const sp = new URLSearchParams({
pageSize: perPage.toString()
@@ -310,6 +351,39 @@ export const orgQueries = {
}
}),
+ privateResources: ({
+ orgId,
+ query,
+ perPage = 10_000
+ }: {
+ orgId: string;
+ query?: string;
+ perPage?: number;
+ }) =>
+ queryOptions({
+ queryKey: [
+ "ORG",
+ orgId,
+ "PRIVATE_RESOURCES",
+ { query, perPage }
+ ] as const,
+ queryFn: async ({ signal, meta }) => {
+ const sp = new URLSearchParams({
+ pageSize: perPage.toString()
+ });
+
+ if (query?.trim()) {
+ sp.set("query", query);
+ }
+
+ const res = await meta!.api.get<
+ AxiosResponse
+ >(`/org/${orgId}/site-resources?${sp.toString()}`, { signal });
+
+ return res.data.data.siteResources;
+ }
+ }),
+
healthChecks: ({
orgId,
perPage = 10_000
From 6380079239a599996255a0883348ed2fd9106b35 Mon Sep 17 00:00:00 2001
From: Fred KISSIE
Date: Tue, 16 Jun 2026 22:28:31 +0200
Subject: [PATCH 09/53] =?UTF-8?q?=E2=9C=A8=20Command=20palette=20with=20se?=
=?UTF-8?q?arch=20&=20actions?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
messages/en-US.json | 1 +
.../command-palette/CommandPalette.tsx | 362 ++++++++++--------
.../useCommandPaletteSearch.ts | 74 +++-
3 files changed, 270 insertions(+), 167 deletions(-)
diff --git a/messages/en-US.json b/messages/en-US.json
index e6567cc08..cf32d15fd 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -1626,6 +1626,7 @@
"commandBilling": "Billing",
"commandEnterpriseLicenses": "Licenses",
"commandSettings": "Settings",
+ "commandSearchResults": "Search Results",
"alertingTitle": "Alerting",
"alertingDescription": "Define sources, triggers, and actions for notifications",
"alertingRules": "Alert rules",
diff --git a/src/components/command-palette/CommandPalette.tsx b/src/components/command-palette/CommandPalette.tsx
index 726d1c999..816c83fb3 100644
--- a/src/components/command-palette/CommandPalette.tsx
+++ b/src/components/command-palette/CommandPalette.tsx
@@ -15,6 +15,15 @@ import {
} from "@app/components/ui/command";
import { cn } from "@app/lib/cn";
import { ListUserOrgsResponse } from "@server/routers/org";
+import {
+ ChevronRightIcon,
+ GlobeIcon,
+ GlobeLockIcon,
+ LaptopIcon,
+ PlugIcon,
+ ServerIcon,
+ UserIcon
+} from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import React, {
@@ -28,7 +37,7 @@ import React, {
import { useCommandPaletteActions } from "./useCommandPaletteActions";
import { useCommandPaletteNavigation } from "./useCommandPaletteNavigation";
import { useCommandPaletteSearch } from "./useCommandPaletteSearch";
-import { search } from "jmespath";
+import { resources } from "@server/db";
type CommandPaletteProps = {
orgId?: string;
@@ -54,12 +63,21 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
const navigationGroups = useCommandPaletteNavigation(navItems);
// const organizations = useCommandPaletteOrganizations(orgs);
const actions = useCommandPaletteActions(orgId, orgs);
- const { shouldSearch, sites, resources, users, machineClients, isLoading } =
- useCommandPaletteSearch({
- orgId,
- query: search.substring(1),
- enabled: open && isActionMode
- });
+ const {
+ shouldSearch,
+ sites,
+ publicResources,
+ privateResources,
+ users,
+ machineClients,
+ userDevices,
+ isLoading,
+ hasResults: hasEntityResults
+ } = useCommandPaletteSearch({
+ orgId,
+ query: search,
+ enabled: !isActionMode && open
+ });
const handleOpenChange = useCallback(
(nextOpen: boolean) => {
@@ -80,15 +98,9 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
[setOpen]
);
- // const hasEntityResults =
- // sites.length > 0 ||
- // resources.length > 0 ||
- // users.length > 0 ||
- // machineClients.length > 0;
-
return (
-
+
{t("commandPaletteNoResults")}
{!isActionMode &&
- navigationGroups.map((group, groupIndex) => {
- console.log({
- groupIndex,
- groupHeading: group.heading
- });
- return (
-
- {groupIndex > 0 && }
- 0 &&
- "[&_[cmdk-group-heading]]:pt-3"
- )}
- >
- {group.items.map((item) => (
-
- runCommand(() =>
- router.push(item.href)
- )
- }
- className="h-9"
- >
- {item.icon}
- {item.title}
-
- ))}
-
-
- );
- })}
+ navigationGroups.map((group, groupIndex) => (
+
+ {groupIndex > 0 && }
+ 0 &&
+ "[&_[cmdk-group-heading]]:pt-3"
+ )}
+ >
+ {group.items.map((item) => (
+
+ runCommand(() =>
+ router.push(item.href)
+ )
+ }
+ className="h-9"
+ >
+ {item.icon}
+ {item.title}
+
+ ))}
+
+
+ ))}
{/* {organizations.length > 1 && (
<>
@@ -200,118 +206,157 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
>
)} */}
- {/* {shouldSearch && orgId && (
- <>
-
- {isLoading && !hasEntityResults ? (
-
-
- {t("commandPaletteSearching")}
-
- ) : (
- <>
- {sites.length > 0 && (
-
- {sites.map((site) => (
-
- runCommand(() =>
- router.push(site.href)
- )
- }
- >
-
- {site.name}
-
-
- ))}
-
- )}
- {resources.length > 0 && (
-
- {resources.map((resource) => (
-
- runCommand(() =>
- router.push(
- resource.href
- )
- )
- }
- >
-
- {resource.name}
-
-
- ))}
-
- )}
- {users.length > 0 && (
-
- {users.map((user) => (
-
- runCommand(() =>
- router.push(user.href)
- )
- }
- >
-
-
- {user.name}
-
-
- {user.email}
-
-
-
- ))}
-
- )}
- {machineClients.length > 0 && (
-
- {machineClients.map((client) => (
-
- runCommand(() =>
- router.push(client.href)
- )
- }
- >
-
- {client.name}
-
-
- ))}
-
- )}
- >
+ {!isActionMode && shouldSearch && orgId && hasEntityResults && (
+
- )} */}
+ >
+ {sites.map((site) => (
+
+ runCommand(() => router.push(site.href))
+ }
+ className="h-9"
+ >
+
+
+
+ {t("commandSites")}
+
+
+
+ {site.name}
+
+
+
+ ))}
+ {publicResources.map((resource) => (
+
+ runCommand(() => router.push(resource.href))
+ }
+ className="h-9"
+ >
+
+
+
+ {t("commandProxyResources")}
+
+
+
+ {resource.name}
+
+
+
+ ))}
+ {privateResources.map((resource) => (
+
+ runCommand(() => router.push(resource.href))
+ }
+ className="h-9"
+ >
+
+
+
+ {t("commandClientResources")}
+
+
+
+ {resource.name}
+
+
+
+ ))}
+ {users.map((user) => (
+
+ runCommand(() => router.push(user.href))
+ }
+ className="h-9"
+ >
+
+
+
+ {t("commandUsers")}
+
+
+
+
+ {user.name}
+
+
+ ·
+
+
+ {user.email}
+
+
+
+
+ ))}
+ {machineClients.map((client) => (
+
+ runCommand(() => router.push(client.href))
+ }
+ className="h-9"
+ >
+
+
+
+ {t("commandMachineClients")}
+
+
+
+ {client.name}
+
+
+
+ ))}
+ {userDevices.map((device) => (
+
+ runCommand(() => router.push(device.href))
+ }
+ className="h-9"
+ >
+
+
+
+ {t("commandUserDevices")}
+
+
+
+ {device.name}
+
+
+
+ ))}
+
+ )}
{isActionMode && actions.length > 0 && (
<>
-
{actions.map((action) => (
{action.icon}
{action.label}
@@ -387,7 +433,7 @@ export function CommandPaletteProvider({
orgs,
navItems
}: CommandPaletteProviderProps) {
- const [open, setOpen] = useState(false);
+ const [open, setOpen] = useState(true); // FIXME: should be set to `false` by default, this is temporary
const toggle = useCallback(() => {
setOpen((current) => !current);
diff --git a/src/components/command-palette/useCommandPaletteSearch.ts b/src/components/command-palette/useCommandPaletteSearch.ts
index 984892354..7471438d5 100644
--- a/src/components/command-palette/useCommandPaletteSearch.ts
+++ b/src/components/command-palette/useCommandPaletteSearch.ts
@@ -47,7 +47,14 @@ export function useCommandPaletteSearch({
const shouldSearch =
enabled && !!orgId && trimmedQuery.length >= MIN_QUERY_LENGTH;
- const [sitesQuery, resourcesQuery, usersQuery, clientsQuery] = useQueries({
+ const [
+ sitesQuery,
+ proxyResourcesQuery,
+ privateResourcesQuery,
+ usersQuery,
+ clientsQuery,
+ userDevicesQuery
+ ] = useQueries({
queries: [
{
...orgQueries.sites({
@@ -58,7 +65,15 @@ export function useCommandPaletteSearch({
enabled: shouldSearch
},
{
- ...orgQueries.resources({
+ ...orgQueries.proxyResources({
+ orgId: orgId ?? "",
+ query: trimmedQuery,
+ perPage: SEARCH_PER_PAGE
+ }),
+ enabled: shouldSearch
+ },
+ {
+ ...orgQueries.privateResources({
orgId: orgId ?? "",
query: trimmedQuery,
perPage: SEARCH_PER_PAGE
@@ -80,6 +95,14 @@ export function useCommandPaletteSearch({
perPage: SEARCH_PER_PAGE
}),
enabled: shouldSearch
+ },
+ {
+ ...orgQueries.userDevices({
+ orgId: orgId ?? "",
+ query: trimmedQuery,
+ perPage: SEARCH_PER_PAGE
+ }),
+ enabled: shouldSearch
}
]
});
@@ -93,14 +116,23 @@ export function useCommandPaletteSearch({
}));
}, [orgId, sitesQuery.data]);
- const resources = useMemo((): ResourceSearchResult[] => {
- if (!orgId || !resourcesQuery.data) return [];
- return resourcesQuery.data.map((resource) => ({
+ const publicResources = useMemo((): ResourceSearchResult[] => {
+ if (!orgId || !proxyResourcesQuery.data) return [];
+ return proxyResourcesQuery.data.map((resource) => ({
id: `resource-${resource.resourceId}`,
name: resource.name,
href: `/${orgId}/settings/resources/proxy/${resource.niceId}`
}));
- }, [orgId, resourcesQuery.data]);
+ }, [orgId, proxyResourcesQuery.data]);
+
+ const privateResources = useMemo((): ResourceSearchResult[] => {
+ if (!orgId || !privateResourcesQuery.data) return [];
+ return privateResourcesQuery.data.map((resource) => ({
+ id: `site-resource-${resource.siteResourceId}`,
+ name: resource.name,
+ href: `/${orgId}/settings/resources/private?query=${resource.name}`
+ }));
+ }, [orgId, privateResourcesQuery.data]);
const users = useMemo((): UserSearchResult[] => {
if (!orgId || !usersQuery.data) return [];
@@ -123,20 +155,44 @@ export function useCommandPaletteSearch({
}));
}, [orgId, clientsQuery.data]);
+ const userDevices = useMemo((): ClientSearchResult[] => {
+ if (!orgId || !userDevicesQuery.data) return [];
+ return userDevicesQuery.data
+ .filter((client) => !client.userId)
+ .map((client) => ({
+ id: `client-${client.clientId}`,
+ name: client.name,
+ href: `/${orgId}/settings/clients/user/${client.niceId}`
+ }));
+ }, [orgId, userDevicesQuery.data]);
+
const isLoading =
shouldSearch &&
(sitesQuery.isFetching ||
- resourcesQuery.isFetching ||
+ proxyResourcesQuery.isFetching ||
+ privateResourcesQuery.isFetching ||
usersQuery.isFetching ||
+ userDevicesQuery.isFetching ||
clientsQuery.isFetching);
+ const hasResults =
+ sites.length > 0 ||
+ publicResources.length > 0 ||
+ users.length > 0 ||
+ privateResources.length > 0 ||
+ userDevices.length > 0 ||
+ machineClients.length > 0;
+
return {
debouncedQuery: trimmedQuery,
shouldSearch,
sites,
- resources,
+ publicResources,
+ privateResources,
users,
machineClients,
- isLoading
+ userDevices,
+ isLoading,
+ hasResults
};
}
From 4e7328a1cc2c37b4fa5f17e648395323dd1659ea Mon Sep 17 00:00:00 2001
From: Fred KISSIE
Date: Tue, 16 Jun 2026 22:29:03 +0200
Subject: [PATCH 10/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20private?=
=?UTF-8?q?=20resource=20page?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../settings/resources/private/page.tsx | 45 ++-----------------
src/components/PrivateResourcesTable.tsx | 6 +--
2 files changed, 5 insertions(+), 46 deletions(-)
diff --git a/src/app/[orgId]/settings/resources/private/page.tsx b/src/app/[orgId]/settings/resources/private/page.tsx
index 23ff86296..3cd2507fc 100644
--- a/src/app/[orgId]/settings/resources/private/page.tsx
+++ b/src/app/[orgId]/settings/resources/private/page.tsx
@@ -1,18 +1,15 @@
+import PrivateResourcesBanner from "@app/components/PrivateResourcesBanner";
import type { InternalResourceRow } from "@app/components/PrivateResourcesTable";
import PrivateResourcesTable from "@app/components/PrivateResourcesTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
-import PrivateResourcesBanner from "@app/components/PrivateResourcesBanner";
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
import OrgProvider from "@app/providers/OrgProvider";
-import type { ListResourcesResponse } from "@server/routers/resource";
-import { GetSiteResponse } from "@server/routers/site/getSite";
import type { ListAllSiteResourcesByOrgResponse } from "@server/routers/siteResource";
-import type ResponseT from "@server/types/Response";
import type { AxiosResponse } from "axios";
-import { getTranslations } from "next-intl/server";
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
export const metadata: Metadata = {
@@ -24,13 +21,6 @@ export interface ClientResourcesPageProps {
searchParams: Promise>;
}
-function parsePositiveInt(s: string | undefined): number | undefined {
- if (!s) return undefined;
- const n = Number(s);
- if (!Number.isInteger(n) || n <= 0) return undefined;
- return n;
-}
-
export default async function ClientResourcesPage(
props: ClientResourcesPageProps
) {
@@ -39,7 +29,7 @@ export default async function ClientResourcesPage(
const searchParams = new URLSearchParams(await props.searchParams);
let siteResources: ListAllSiteResourcesByOrgResponse["siteResources"] = [];
- let pagination: ListResourcesResponse["pagination"] = {
+ let pagination: ListAllSiteResourcesByOrgResponse["pagination"] = {
total: 0,
page: 1,
pageSize: 20
@@ -56,34 +46,6 @@ export default async function ClientResourcesPage(
pagination = responseData.pagination;
} catch (e) {}
- const siteIdParam = parsePositiveInt(
- searchParams.get("siteId") ?? undefined
- );
-
- let initialFilterSite: {
- siteId: number;
- name: string;
- type: string;
- } | null = null;
- if (siteIdParam) {
- try {
- const siteRes = await internal.get(
- `/site/${siteIdParam}`,
- await authCookieHeader()
- );
- const s = (siteRes.data as ResponseT).data;
- if (s && s.orgId === params.orgId) {
- initialFilterSite = {
- siteId: s.siteId,
- name: s.name,
- type: s.type
- };
- }
- } catch {
- // leave null
- }
- }
-
let org = null;
try {
const res = await getCachedOrg(params.orgId);
@@ -153,7 +115,6 @@ export default async function ClientResourcesPage(
pageIndex: pagination.page - 1,
pageSize: pagination.pageSize
}}
- initialFilterSite={initialFilterSite}
/>
>
diff --git a/src/components/PrivateResourcesTable.tsx b/src/components/PrivateResourcesTable.tsx
index 2a4d63e85..c7b1fdf9f 100644
--- a/src/components/PrivateResourcesTable.tsx
+++ b/src/components/PrivateResourcesTable.tsx
@@ -117,15 +117,13 @@ type ClientResourcesTableProps = {
orgId: string;
pagination: PaginationState;
rowCount: number;
- initialFilterSite?: Selectedsite | null;
};
export default function PrivateResourcesTable({
internalResources,
orgId,
pagination,
- rowCount,
- initialFilterSite = null
+ rowCount
}: ClientResourcesTableProps) {
const router = useRouter();
const {
@@ -142,7 +140,7 @@ export default function PrivateResourcesTable({
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedInternalResource, setSelectedInternalResource] =
- useState();
+ useState(null);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [editingResource, setEditingResource] =
useState();
From 2ab5540085bfb9415638455848bddcbb4ef903b6 Mon Sep 17 00:00:00 2001
From: Fred KISSIE
Date: Fri, 19 Jun 2026 18:08:36 +0200
Subject: [PATCH 11/53] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20add=20command=20pale?=
=?UTF-8?q?tte=20trigger=20on=20the=20header?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/LayoutHeader.tsx | 2 ++
src/components/LayoutMobileMenu.tsx | 33 ++++++++++---------
.../command-palette/CommandPalette.tsx | 4 +--
3 files changed, 21 insertions(+), 18 deletions(-)
diff --git a/src/components/LayoutHeader.tsx b/src/components/LayoutHeader.tsx
index 29850f115..5e825a8e2 100644
--- a/src/components/LayoutHeader.tsx
+++ b/src/components/LayoutHeader.tsx
@@ -8,6 +8,7 @@ import { useTheme } from "next-themes";
import BrandingLogo from "./BrandingLogo";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
+import { CommandPaletteTrigger } from "@app/components/command-palette/CommandPaletteTrigger";
interface LayoutHeaderProps {
showTopBar: boolean;
@@ -67,6 +68,7 @@ export function LayoutHeader({ showTopBar }: LayoutHeaderProps) {
{showTopBar && (
diff --git a/src/components/LayoutMobileMenu.tsx b/src/components/LayoutMobileMenu.tsx
index 13efdd564..8e7d0dfc0 100644
--- a/src/components/LayoutMobileMenu.tsx
+++ b/src/components/LayoutMobileMenu.tsx
@@ -1,27 +1,27 @@
"use client";
-import React, { useState } from "react";
-import { SidebarNav } from "@app/components/SidebarNav";
-import { OrgSelector } from "@app/components/OrgSelector";
-import { cn } from "@app/lib/cn";
-import { ListUserOrgsResponse } from "@server/routers/org";
-import { Button } from "@app/components/ui/button";
-import { ArrowRight, Menu, Server } from "lucide-react";
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-import { useUserContext } from "@app/hooks/useUserContext";
-import { useTranslations } from "next-intl";
-import ProfileIcon from "@app/components/ProfileIcon";
-import ThemeSwitcher from "@app/components/ThemeSwitcher";
import type { SidebarNavSection } from "@app/app/navigation";
+import { CommandPaletteTrigger } from "@app/components/command-palette/CommandPaletteTrigger";
+import { OrgSelector } from "@app/components/OrgSelector";
+import ProfileIcon from "@app/components/ProfileIcon";
+import { SidebarNav } from "@app/components/SidebarNav";
+import ThemeSwitcher from "@app/components/ThemeSwitcher";
+import { Button } from "@app/components/ui/button";
import {
Sheet,
SheetContent,
- SheetTrigger,
+ SheetDescription,
SheetTitle,
- SheetDescription
+ SheetTrigger
} from "@app/components/ui/sheet";
-import { Abel } from "next/font/google";
+import { useUserContext } from "@app/hooks/useUserContext";
+import { cn } from "@app/lib/cn";
+import { ListUserOrgsResponse } from "@server/routers/org";
+import { Menu, Server } from "lucide-react";
+import { useTranslations } from "next-intl";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useState } from "react";
interface LayoutMobileMenuProps {
orgId?: string;
@@ -121,6 +121,7 @@ export function LayoutMobileMenu({
{showTopBar && (
diff --git a/src/components/command-palette/CommandPalette.tsx b/src/components/command-palette/CommandPalette.tsx
index 816c83fb3..514ef6e1a 100644
--- a/src/components/command-palette/CommandPalette.tsx
+++ b/src/components/command-palette/CommandPalette.tsx
@@ -100,7 +100,7 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
return (
{
setOpen((current) => !current);
From 8f0e17774f07292d9187f22924e3f9a85d7d8e63 Mon Sep 17 00:00:00 2001
From: Fred KISSIE
Date: Mon, 22 Jun 2026 21:22:55 +0200
Subject: [PATCH 12/53] =?UTF-8?q?=F0=9F=92=84=20Update=20command=20bar?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../useCommandPaletteActions.tsx | 25 ++++++-------------
src/components/ui/command.tsx | 3 ++-
2 files changed, 9 insertions(+), 19 deletions(-)
diff --git a/src/components/command-palette/useCommandPaletteActions.tsx b/src/components/command-palette/useCommandPaletteActions.tsx
index 998fb97b8..4b288c3bc 100644
--- a/src/components/command-palette/useCommandPaletteActions.tsx
+++ b/src/components/command-palette/useCommandPaletteActions.tsx
@@ -86,6 +86,12 @@ export function useCommandPaletteActions(
icon: ,
href: `/${orgId}/settings/resources/proxy/create`
});
+ actions.push({
+ id: "create-machine-client",
+ label: t("commandPaletteCreateMachineClient"),
+ icon: ,
+ href: `/${orgId}/settings/clients/machine/create`
+ });
actions.push({
id: "create-user",
label: t("commandPaletteCreateUser"),
@@ -98,12 +104,6 @@ export function useCommandPaletteActions(
icon: ,
href: `/${orgId}/settings/api-keys/create`
});
- actions.push({
- id: "create-machine-client",
- label: t("commandPaletteCreateMachineClient"),
- icon: ,
- href: `/${orgId}/settings/clients/machine/create`
- });
if (!env?.flags.disableEnterpriseFeatures) {
actions.push({
@@ -129,17 +129,6 @@ export function useCommandPaletteActions(
}
}
- const canChooseOrganization = !isAdminPage && (orgs?.length ?? 0) > 1;
-
- if (canChooseOrganization) {
- actions.push({
- id: "choose-org",
- label: t("commandPaletteChooseOrganization"),
- icon: ,
- href: "/?orgs=1"
- });
- }
-
actions.push({
id: "toggle-theme",
label: t("commandPaletteToggleTheme"),
@@ -158,4 +147,4 @@ export function useCommandPaletteActions(
return actions;
}, [isAdminPage, orgId, orgs, env, user.serverAdmin, theme, setTheme, t]);
-}
\ No newline at end of file
+}
diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx
index efd1c35f5..3d577c275 100644
--- a/src/components/ui/command.tsx
+++ b/src/components/ui/command.tsx
@@ -174,10 +174,11 @@ function CommandShortcut({
...props
}: React.ComponentProps<"span">) {
return (
-
Date: Thu, 25 Jun 2026 20:28:57 +0200
Subject: [PATCH 13/53] =?UTF-8?q?=F0=9F=9A=A7=20add=20country=20is=20not?=
=?UTF-8?q?=20rule?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
messages/en-US.json | 1 +
server/db/pg/schema/schema.ts | 11 ++++++++++-
server/db/sqlite/schema/schema.ts | 10 +++++++++-
server/lib/blueprints/publicResources.ts | 2 +-
server/lib/blueprints/resourcePolicies.ts | 3 ++-
server/lib/validators.ts | 2 ++
.../PolicyAccessRulesTable.tsx | 19 ++++++++++++++-----
.../policy-access-rule-validation.ts | 1 +
8 files changed, 40 insertions(+), 9 deletions(-)
diff --git a/messages/en-US.json b/messages/en-US.json
index 83e8a488d..3bb70a34f 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -2431,6 +2431,7 @@
"noRemoteExitNodesAvailableDescription": "No nodes are available for this organization. Create a node first to use local sites.",
"exitNode": "Exit Node",
"country": "Country",
+ "countryIsNot": "Country Is Not",
"rulesMatchCountry": "Currently based on source IP",
"region": "Region",
"selectRegion": "Select region",
diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts
index 1b48aa520..934eba1f6 100644
--- a/server/db/pg/schema/schema.ts
+++ b/server/db/pg/schema/schema.ts
@@ -981,7 +981,15 @@ export const resourcePolicyRules = pgTable("resourcePolicyRules", {
priority: integer("priority").notNull(),
action: varchar("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(),
match: varchar("match")
- .$type<"CIDR" | "PATH" | "IP" | "COUNTRY" | "ASN" | "REGION">()
+ .$type<
+ | "CIDR"
+ | "PATH"
+ | "IP"
+ | "COUNTRY"
+ | "COUNTRY_IS_NOT"
+ | "ASN"
+ | "REGION"
+ >()
.notNull(),
value: varchar("value").notNull()
});
@@ -1553,3 +1561,4 @@ export type Label = InferSelectModel;
export type ResourcePolicy = InferSelectModel;
export type RolePolicy = InferSelectModel;
export type UserPolicy = InferSelectModel;
+export type ResourcePolicyRule = InferSelectModel;
diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts
index 0c4a143f5..76e6d22cf 100644
--- a/server/db/sqlite/schema/schema.ts
+++ b/server/db/sqlite/schema/schema.ts
@@ -1252,7 +1252,15 @@ export const resourcePolicyRules = sqliteTable("resourcePolicyRules", {
priority: integer("priority").notNull(),
action: text("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(),
match: text("match")
- .$type<"CIDR" | "PATH" | "IP" | "COUNTRY" | "ASN" | "REGION">()
+ .$type<
+ | "CIDR"
+ | "PATH"
+ | "IP"
+ | "COUNTRY"
+ | "COUNTRY_IS_NOT"
+ | "ASN"
+ | "REGION"
+ >()
.notNull(),
value: text("value").notNull()
});
diff --git a/server/lib/blueprints/publicResources.ts b/server/lib/blueprints/publicResources.ts
index 1bbe0a4f1..a6147ea86 100644
--- a/server/lib/blueprints/publicResources.ts
+++ b/server/lib/blueprints/publicResources.ts
@@ -1321,7 +1321,7 @@ function getRuleAction(input: string) {
function getRuleValue(match: string, value: string) {
// if the match is a country, uppercase the value
- if (match == "COUNTRY") {
+ if (match === "COUNTRY" || match === "COUNTRY_IS_NOT") {
return value.toUpperCase();
}
return value;
diff --git a/server/lib/blueprints/resourcePolicies.ts b/server/lib/blueprints/resourcePolicies.ts
index f8d8d1269..aa5f4bc9b 100644
--- a/server/lib/blueprints/resourcePolicies.ts
+++ b/server/lib/blueprints/resourcePolicies.ts
@@ -347,12 +347,13 @@ function getRuleAction(input: string): "ACCEPT" | "DROP" | "PASS" {
function getRuleMatch(
input: string
-): "CIDR" | "IP" | "PATH" | "COUNTRY" | "ASN" | "REGION" {
+): "CIDR" | "IP" | "PATH" | "COUNTRY" | "COUNTRY_IS_NOT" | "ASN" | "REGION" {
return input.toUpperCase() as
| "CIDR"
| "IP"
| "PATH"
| "COUNTRY"
+ | "COUNTRY_IS_NOT"
| "ASN"
| "REGION";
}
diff --git a/server/lib/validators.ts b/server/lib/validators.ts
index ec19bd852..ff0c5fb3d 100644
--- a/server/lib/validators.ts
+++ b/server/lib/validators.ts
@@ -74,6 +74,7 @@ export const RESOURCE_RULE_MATCH_TYPES = [
"IP",
"PATH",
"COUNTRY",
+ "COUNTRY_IS_NOT",
"ASN",
"REGION"
] as const;
@@ -96,6 +97,7 @@ export function getResourceRuleValueValidationError(
case "REGION":
return isValidRegionId(value) ? null : "Invalid region ID provided";
case "COUNTRY":
+ case "COUNTRY_IS_NOT":
return COUNTRIES.some((country) => country.code === value)
? null
: "Invalid country code provided";
diff --git a/src/components/resource-policy/PolicyAccessRulesTable.tsx b/src/components/resource-policy/PolicyAccessRulesTable.tsx
index a701b92ff..2818f3652 100644
--- a/src/components/resource-policy/PolicyAccessRulesTable.tsx
+++ b/src/components/resource-policy/PolicyAccessRulesTable.tsx
@@ -229,6 +229,7 @@ export function PolicyAccessRulesTable({
IP: "IP",
CIDR: t("ipAddressRange"),
COUNTRY: t("country"),
+ COUNTRY_IS_NOT: t("countryIsNot"),
ASN: "ASN",
REGION: t("region")
}),
@@ -441,13 +442,15 @@ export function PolicyAccessRulesTable({
| "IP"
| "PATH"
| "COUNTRY"
+ | "COUNTRY_IS_NOT"
| "ASN"
| "REGION"
) =>
updateRule(row.original.ruleId, {
match: value,
value:
- value === "COUNTRY"
+ value === "COUNTRY" ||
+ value === "COUNTRY_IS_NOT"
? "US"
: value === "ASN"
? "AS15169"
@@ -469,9 +472,14 @@ export function PolicyAccessRulesTable({
{RuleMatch.CIDR}
{isMaxmindAvailable && (
-
- {RuleMatch.COUNTRY}
-
+ <>
+
+ {RuleMatch.COUNTRY}
+
+
+ {RuleMatch.COUNTRY_IS_NOT}
+
+ >
)}
{includeRegionMatch && isMaxmindAvailable && (
@@ -491,7 +499,8 @@ export function PolicyAccessRulesTable({
accessorKey: "value",
header: () => {t("value")},
cell: ({ row }) =>
- row.original.match === "COUNTRY" ? (
+ row.original.match === "COUNTRY" ||
+ row.original.match === "COUNTRY_IS_NOT" ? (
)}
Date: Mon, 6 Jul 2026 22:00:35 -0400
Subject: [PATCH 21/53] includen on sso resources in launcher
---
.../launcher/launcherResourceAccess.ts | 55 +++++++++++++++++--
1 file changed, 51 insertions(+), 4 deletions(-)
diff --git a/server/routers/launcher/launcherResourceAccess.ts b/server/routers/launcher/launcherResourceAccess.ts
index 246f86688..0784dff70 100644
--- a/server/routers/launcher/launcherResourceAccess.ts
+++ b/server/routers/launcher/launcherResourceAccess.ts
@@ -1,10 +1,11 @@
import { createHash } from "node:crypto";
-import { db } from "@server/db";
+import { alias, db } from "@server/db";
import {
exitNodes,
labels,
launcherViews,
resourceLabels,
+ resourcePolicies,
resources,
rolePolicies,
roleResources,
@@ -31,6 +32,7 @@ import {
inArray,
isNull,
like,
+ not,
or,
sql,
type SQL
@@ -134,6 +136,48 @@ function launcherAccessibleIdsCacheKey(
return `launcherAccessibleIds:${orgId}:${userId}:${rolesKey}`;
}
+async function fetchOrgNonSsoPublicResourceIds(
+ orgId: string
+): Promise {
+ const sharedPolicy = alias(resourcePolicies, "sharedPolicy");
+ const defaultPolicy = alias(resourcePolicies, "defaultPolicy");
+
+ const effectiveSso = sql`
+ COALESCE(
+ CASE
+ WHEN ${sharedPolicy.resourcePolicyId} IS NOT NULL THEN ${sharedPolicy.sso}
+ ELSE ${defaultPolicy.sso}
+ END,
+ false
+ )
+ `;
+
+ const rows = await db
+ .select({ resourceId: resources.resourceId })
+ .from(resources)
+ .leftJoin(
+ sharedPolicy,
+ eq(sharedPolicy.resourcePolicyId, resources.resourcePolicyId)
+ )
+ .leftJoin(
+ defaultPolicy,
+ eq(
+ defaultPolicy.resourcePolicyId,
+ resources.defaultResourcePolicyId
+ )
+ )
+ .where(
+ and(
+ eq(resources.orgId, orgId),
+ eq(resources.enabled, true),
+ eq(resources.blockAccess, false),
+ not(effectiveSso)
+ )
+ );
+
+ return rows.map((row) => row.resourceId);
+}
+
async function resolveAccessibleIdsUncached(
orgId: string,
userId: string,
@@ -145,7 +189,8 @@ async function resolveAccessibleIdsUncached(
directPolicyResourceResults,
rolePolicyResourceResults,
directSiteResourceResults,
- roleSiteResourceResults
+ roleSiteResourceResults,
+ orgNonSsoPublicResources
] = await Promise.all([
db
.select({ resourceId: userResources.resourceId })
@@ -214,7 +259,8 @@ async function resolveAccessibleIdsUncached(
})
.from(roleSiteResources)
.where(inArray(roleSiteResources.roleId, userRoleIds))
- : Promise.resolve([])
+ : Promise.resolve([]),
+ fetchOrgNonSsoPublicResourceIds(orgId)
]);
return {
@@ -223,7 +269,8 @@ async function resolveAccessibleIdsUncached(
...directResources.map((r) => r.resourceId),
...roleResourceResults.map((r) => r.resourceId),
...directPolicyResourceResults.map((r) => r.resourceId),
- ...rolePolicyResourceResults.map((r) => r.resourceId)
+ ...rolePolicyResourceResults.map((r) => r.resourceId),
+ ...orgNonSsoPublicResources
])
),
siteResourceIds: Array.from(
From beaa5735ce0e9f3d878e914afdac2b438b7bf4b0 Mon Sep 17 00:00:00 2001
From: miloschwartz
Date: Tue, 7 Jul 2026 14:51:15 -0400
Subject: [PATCH 22/53] move cert to info box and add network access to create
wizard
---
messages/en-US.json | 1 +
.../private/PrivateResourceHttpFields.tsx | 30 ---------------
.../resources/private/[niceId]/http/page.tsx | 6 ---
.../resources/private/create/page.tsx | 23 ++++++++++++
src/components/LayoutMobileMenu.tsx | 2 +-
src/components/LayoutSidebar.tsx | 4 +-
src/components/SiteResourceInfoBox.tsx | 37 ++++++++++++++++++-
src/lib/privateResourceForm.ts | 2 +
8 files changed, 65 insertions(+), 40 deletions(-)
diff --git a/messages/en-US.json b/messages/en-US.json
index 003fd74a4..196fdf4b9 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -3585,6 +3585,7 @@
"memberPortalResourceDisabled": "Resource Disabled",
"memberPortalShowingResources": "Showing {start}-{end} of {total} resources",
"resourceLauncherTitle": "Resource Launcher",
+ "resourceSidebarLauncherTitle": "Launcher",
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
"resourceLauncherSearchPlaceholder": "Search your resources...",
"resourceLauncherDefaultView": "Default",
diff --git a/src/app/[orgId]/settings/resources/private/PrivateResourceHttpFields.tsx b/src/app/[orgId]/settings/resources/private/PrivateResourceHttpFields.tsx
index 814adecb1..43fb49c08 100644
--- a/src/app/[orgId]/settings/resources/private/PrivateResourceHttpFields.tsx
+++ b/src/app/[orgId]/settings/resources/private/PrivateResourceHttpFields.tsx
@@ -1,6 +1,5 @@
"use client";
-import CertificateStatus from "@app/components/CertificateStatus";
import DomainPicker from "@app/components/DomainPicker";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import {
@@ -27,7 +26,6 @@ import {
SelectValue
} from "@app/components/ui/select";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
-import { build } from "@server/build";
import { useTranslations } from "next-intl";
import type { Control, UseFormSetValue, UseFormWatch } from "react-hook-form";
@@ -38,8 +36,6 @@ type PrivateResourceHttpFieldsProps = {
watch: UseFormWatch;
disabled?: boolean;
siteResourceId?: number;
- resourceDomainId?: string | null;
- resourceFullDomain?: string | null;
labelPrefix?: "create" | "edit";
hideDomainPicker?: boolean;
hidePaidFeaturesAlert?: boolean;
@@ -52,8 +48,6 @@ export function PrivateResourceHttpFields({
watch,
disabled = false,
siteResourceId,
- resourceDomainId,
- resourceFullDomain,
labelPrefix = "edit",
hideDomainPicker = false,
hidePaidFeaturesAlert = false
@@ -91,7 +85,6 @@ export function PrivateResourceHttpFields({
const httpConfigSubdomain = watch("httpConfigSubdomain");
const httpConfigDomainId = watch("httpConfigDomainId");
const httpConfigFullDomain = watch("httpConfigFullDomain");
- const ssl = watch("ssl");
return (
@@ -274,29 +267,6 @@ export function PrivateResourceHttpFields({
)}
/>
- {siteResourceId &&
- resourceDomainId &&
- httpConfigFullDomain &&
- httpConfigDomainId === resourceDomainId &&
- httpConfigFullDomain === resourceFullDomain &&
- build != "oss" &&
- ssl && (
-
-
-
- {t("certificateStatus")}:
-
-
-
-
- )}
>
)}
diff --git a/src/app/[orgId]/settings/resources/private/[niceId]/http/page.tsx b/src/app/[orgId]/settings/resources/private/[niceId]/http/page.tsx
index d69210ef5..2bbd776e5 100644
--- a/src/app/[orgId]/settings/resources/private/[niceId]/http/page.tsx
+++ b/src/app/[orgId]/settings/resources/private/[niceId]/http/page.tsx
@@ -122,12 +122,6 @@ export default function PrivateResourceHttpPage() {
watch={asAnyWatch(form.watch)}
disabled={httpSectionDisabled}
siteResourceId={siteResource.id}
- resourceDomainId={
- siteResource.domainId
- }
- resourceFullDomain={
- siteResource.fullDomain
- }
/>
diff --git a/src/app/[orgId]/settings/resources/private/create/page.tsx b/src/app/[orgId]/settings/resources/private/create/page.tsx
index f8109fd66..49b40e3c9 100644
--- a/src/app/[orgId]/settings/resources/private/create/page.tsx
+++ b/src/app/[orgId]/settings/resources/private/create/page.tsx
@@ -53,6 +53,7 @@ import { z } from "zod";
import { PrivateResourceSitesField } from "../PrivateResourceSitesField";
import { PrivateResourceHttpFields } from "../PrivateResourceHttpFields";
import { PrivateResourceSshFields } from "../PrivateResourceSshFields";
+import { PrivateResourcePortRanges } from "../PrivateResourcePortRanges";
import {
PrivateResourceAliasField,
PrivateResourceCidrDestinationField,
@@ -105,6 +106,8 @@ export default function CreatePrivateResourcePage() {
standardDaemonLocation: "site",
authDaemonPort: null,
pamMode: "passthrough",
+ tcpPortRangeString: "*",
+ udpPortRangeString: "*",
disableIcmp: false
}
});
@@ -426,6 +429,16 @@ export default function CreatePrivateResourcePage() {
hideAlias
/>
+
+
+
@@ -466,6 +479,16 @@ export default function CreatePrivateResourcePage() {
labelPrefix="create"
/>
+
+
+
diff --git a/src/components/LayoutMobileMenu.tsx b/src/components/LayoutMobileMenu.tsx
index b549d1f2e..7d7ebb902 100644
--- a/src/components/LayoutMobileMenu.tsx
+++ b/src/components/LayoutMobileMenu.tsx
@@ -155,7 +155,7 @@ export function LayoutMobileMenu({
{t(
- "resourceLauncherTitle"
+ "resourceSidebarLauncherTitle"
)}
diff --git a/src/components/LayoutSidebar.tsx b/src/components/LayoutSidebar.tsx
index 9175cedc4..c42e2c1fa 100644
--- a/src/components/LayoutSidebar.tsx
+++ b/src/components/LayoutSidebar.tsx
@@ -195,7 +195,7 @@ export function LayoutSidebar({
sideOffset={8}
>
- {t("resourceLauncherTitle")}
+ {t("resourceSidebarLauncherTitle")}
@@ -211,7 +211,7 @@ export function LayoutSidebar({
- {t("resourceLauncherTitle")}
+ {t("resourceSidebarLauncherTitle")}
)}
diff --git a/src/components/SiteResourceInfoBox.tsx b/src/components/SiteResourceInfoBox.tsx
index 36b7f29ad..f57acbe99 100644
--- a/src/components/SiteResourceInfoBox.tsx
+++ b/src/components/SiteResourceInfoBox.tsx
@@ -1,5 +1,6 @@
"use client";
+import CertificateStatus from "@app/components/CertificateStatus";
import CopyToClipboard from "@app/components/CopyToClipboard";
import {
InfoSection,
@@ -17,14 +18,17 @@ import {
type LauncherAccessFields
} from "@app/lib/launcherResourceAccess";
import type { PrivateResourceMode } from "@app/lib/privateResourceForm";
+import { build } from "@server/build";
import { useTranslations } from "next-intl";
type SiteResourceInfoInput = {
+ orgId: string;
mode: PrivateResourceMode;
destination: string | null;
destinationPort: number | null;
scheme: "http" | "https" | null;
ssl: boolean;
+ domainId?: string | null;
fullDomain?: string | null;
alias?: string | null;
aliasAddress?: string | null;
@@ -108,9 +112,20 @@ export function SiteResourceInfoSections({
const showDestination = !(
siteResource.mode === "ssh" && siteResource.authDaemonMode === "native"
);
+ const showCertificate = !!(
+ siteResource.mode === "http" &&
+ siteResource.ssl &&
+ siteResource.domainId &&
+ siteResource.fullDomain &&
+ build != "oss"
+ );
const numSections =
- 2 + (showDestination ? 1 : 0) + (showAlias ? 1 : 0) + (isPanel ? 1 : 0);
+ 2 +
+ (showDestination ? 1 : 0) +
+ (showAlias ? 1 : 0) +
+ (showCertificate ? 1 : 0) +
+ (isPanel ? 1 : 0);
const sections = (
@@ -155,6 +170,26 @@ export function SiteResourceInfoSections({
) : null}
+ {showCertificate ? (
+
+
+ {t("certificateStatus", {
+ defaultValue: "Certificate"
+ })}
+
+
+
+
+
+ ) : null}
+
{isPanel ? (
{t("portRestrictions")}
diff --git a/src/lib/privateResourceForm.ts b/src/lib/privateResourceForm.ts
index ecac87e88..d9f6fd691 100644
--- a/src/lib/privateResourceForm.ts
+++ b/src/lib/privateResourceForm.ts
@@ -400,6 +400,8 @@ export function createCreateFormSchema(t: TranslateFn) {
.nullable(),
authDaemonPort: z.number().int().positive().optional().nullable(),
pamMode: z.enum(["passthrough", "push"]).optional().nullable(),
+ tcpPortRangeString: createPortRangeStringSchema(t),
+ udpPortRangeString: createPortRangeStringSchema(t),
disableIcmp: z.boolean().optional()
})
.superRefine((data, ctx) => {
From 417438c209aad5769d41a5cfbae1b8e0a1f37141 Mon Sep 17 00:00:00 2001
From: Owen
Date: Tue, 7 Jul 2026 14:04:34 -0400
Subject: [PATCH 23/53] Remove feature flags from labels
---
server/lib/billing/tierMatrix.ts | 2 -
server/private/routers/external.ts | 54 --------
server/private/routers/labels/index.ts | 19 ---
server/routers/client/listClients.ts | 38 +++---
server/routers/external.ts | 43 +++++++
.../routers/labels/attachLabelToItem.ts | 13 --
.../routers/labels/createOrgLabel.ts | 12 --
.../routers/labels/deleteOrgLabel.ts | 12 --
.../routers/labels/detachLabelFromItem.ts | 13 --
server/routers/labels/index.ts | 6 +
.../routers/labels/listOrgLabels.ts | 13 --
.../routers/labels/updateOrgLabel.ts | 13 --
.../launcher/launcherResourceAccess.ts | 115 +++++-------------
server/routers/resource/getUserResources.ts | 85 ++++++-------
server/routers/resource/listResources.ts | 74 +++++------
.../resource/listUserResourceAliases.ts | 9 +-
server/routers/site/listSites.ts | 71 +++++------
.../siteResource/listAllSiteResourcesByOrg.ts | 38 +++---
.../settings/{(private) => }/labels/page.tsx | 4 -
src/components/MachineClientsTable.tsx | 14 +--
src/components/OrgLabelsTable.tsx | 3 -
src/components/PrivateResourcesTable.tsx | 14 +--
src/components/PublicResourcesTable.tsx | 14 +--
src/components/SitesTable.tsx | 20 ++-
24 files changed, 233 insertions(+), 466 deletions(-)
delete mode 100644 server/private/routers/labels/index.ts
rename server/{private => }/routers/labels/attachLabelToItem.ts (94%)
rename server/{private => }/routers/labels/createOrgLabel.ts (91%)
rename server/{private => }/routers/labels/deleteOrgLabel.ts (82%)
rename server/{private => }/routers/labels/detachLabelFromItem.ts (94%)
create mode 100644 server/routers/labels/index.ts
rename server/{private => }/routers/labels/listOrgLabels.ts (91%)
rename server/{private => }/routers/labels/updateOrgLabel.ts (89%)
rename src/app/[orgId]/settings/{(private) => }/labels/page.tsx (90%)
diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts
index f0e6dc95a..7c0b591ca 100644
--- a/server/lib/billing/tierMatrix.ts
+++ b/server/lib/billing/tierMatrix.ts
@@ -23,7 +23,6 @@ export enum TierFeature {
StandaloneHealthChecks = "standaloneHealthChecks",
AlertingRules = "alertingRules",
WildcardSubdomain = "wildcardSubdomain",
- Labels = "labels",
NewtAutoUpdate = "newtAutoUpdate",
ResourcePolicies = "resourcePolicies",
AdvancedPublicResources = "advancedPublicResources",
@@ -31,7 +30,6 @@ export enum TierFeature {
}
export const tierMatrix: Record = {
- [TierFeature.Labels]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"],
diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts
index c6ee3de55..75a5b8d48 100644
--- a/server/private/routers/external.ts
+++ b/server/private/routers/external.ts
@@ -31,7 +31,6 @@ import * as siteProvisioning from "#private/routers/siteProvisioning";
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
import * as alertRule from "#private/routers/alertRule";
import * as healthChecks from "#private/routers/healthChecks";
-import * as labels from "#private/routers/labels";
import * as client from "@server/routers/client";
import * as resource from "#private/routers/resource";
import * as policy from "#private/routers/policy";
@@ -810,59 +809,6 @@ authenticated.get(
alertRule.getAlertRule
);
-authenticated.get(
- "/org/:orgId/labels",
- verifyValidLicense,
- verifyOrgAccess,
- verifyValidSubscription(tierMatrix.labels),
- verifyUserHasAction(ActionsEnum.listOrgLabels),
- labels.listOrgLabels
-);
-
-authenticated.post(
- "/org/:orgId/labels",
- verifyValidLicense,
- verifyOrgAccess,
- verifyValidSubscription(tierMatrix.labels),
- verifyUserHasAction(ActionsEnum.createOrgLabel),
- labels.createOrgLabel
-);
-
-authenticated.patch(
- "/org/:orgId/label/:labelId",
- verifyValidLicense,
- verifyOrgAccess,
- verifyValidSubscription(tierMatrix.labels),
- verifyUserHasAction(ActionsEnum.updateOrgLabel),
- labels.updateOrgLabel
-);
-
-authenticated.delete(
- "/org/:orgId/label/:labelId",
- verifyValidLicense,
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.deleteOrgLabel),
- labels.deleteOrgLabel
-);
-
-authenticated.put(
- "/org/:orgId/label/:labelId/attach",
- verifyValidLicense,
- verifyOrgAccess,
- verifyValidSubscription(tierMatrix.labels),
- verifyUserHasAction(ActionsEnum.attachLabelToItem),
- labels.attachLabelToItem
-);
-
-authenticated.put(
- "/org/:orgId/label/:labelId/detach",
- verifyValidLicense,
- verifyOrgAccess,
- verifyValidSubscription(tierMatrix.labels),
- verifyUserHasAction(ActionsEnum.detachLabelFromItem),
- labels.detachLabelFromItem
-);
-
authenticated.get(
"/org/:orgId/health-checks",
verifyValidLicense,
diff --git a/server/private/routers/labels/index.ts b/server/private/routers/labels/index.ts
deleted file mode 100644
index d988d8e38..000000000
--- a/server/private/routers/labels/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 Fossorial, Inc.
- * All rights reserved.
- *
- * This file is licensed under the Fossorial Commercial License.
- * You may not use this file except in compliance with the License.
- * Unauthorized use, copying, modification, or distribution is strictly prohibited.
- *
- * This file is not licensed under the AGPLv3.
- */
-
-export * from "./listOrgLabels";
-export * from "./createOrgLabel";
-export * from "./updateOrgLabel";
-export * from "./attachLabelToItem";
-export * from "./detachLabelFromItem";
-export * from "./deleteOrgLabel";
diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts
index 98c0fc550..3a05c41c5 100644
--- a/server/routers/client/listClients.ts
+++ b/server/routers/client/listClients.ts
@@ -304,11 +304,6 @@ export async function listClients(
(client) => client.clientId
);
- const isLabelFeatureEnabled = await isLicensedOrSubscribed(
- orgId,
- tierMatrix.labels
- );
-
// Get client count with filter
const conditions = [
and(
@@ -341,7 +336,7 @@ export async function listClients(
conditions.push(or(...filterAggregates));
}
- if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) {
+ if (labelFilter && labelFilter.length > 0) {
conditions.push(
inArray(
clients.clientId,
@@ -361,25 +356,20 @@ export async function listClients(
const q = "%" + query.toLowerCase() + "%";
const queryList = [
like(sql`LOWER(${clients.name})`, q),
- like(sql`LOWER(${clients.niceId})`, q)
+ like(sql`LOWER(${clients.niceId})`, q),
+ inArray(
+ clients.clientId,
+ db
+ .select({ id: clientLabels.clientId })
+ .from(clientLabels)
+ .innerJoin(
+ labels,
+ eq(labels.labelId, clientLabels.labelId)
+ )
+ .where(like(sql`LOWER(${labels.name})`, q))
+ )
];
- if (isLabelFeatureEnabled) {
- queryList.push(
- inArray(
- clients.clientId,
- db
- .select({ id: clientLabels.clientId })
- .from(clientLabels)
- .innerJoin(
- labels,
- eq(labels.labelId, clientLabels.labelId)
- )
- .where(like(sql`LOWER(${labels.name})`, q))
- )
- );
- }
-
conditions.push(or(...queryList));
}
@@ -414,7 +404,7 @@ export async function listClients(
clientId: number;
}> = [];
- if (isLabelFeatureEnabled && clientIds.length > 0) {
+ if (clientIds.length > 0) {
labelsForClients = await db
.select({
labelId: labels.labelId,
diff --git a/server/routers/external.ts b/server/routers/external.ts
index 8a22393ca..9e4c5b3f1 100644
--- a/server/routers/external.ts
+++ b/server/routers/external.ts
@@ -54,6 +54,7 @@ import { build } from "@server/build";
import { createStore } from "#dynamic/lib/rateLimitStore";
import { logActionAudit } from "#dynamic/middlewares";
import { checkRoundTripMessage } from "./ws";
+import * as labels from "@server/routers/labels";
// Root routes
export const unauthenticated = Router();
@@ -1335,6 +1336,48 @@ authenticated.get(
authenticated.get("/ws/round-trip-message/:messageId", checkRoundTripMessage);
+authenticated.get(
+ "/org/:orgId/labels",
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.listOrgLabels),
+ labels.listOrgLabels
+);
+
+authenticated.post(
+ "/org/:orgId/labels",
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.createOrgLabel),
+ labels.createOrgLabel
+);
+
+authenticated.patch(
+ "/org/:orgId/label/:labelId",
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.updateOrgLabel),
+ labels.updateOrgLabel
+);
+
+authenticated.delete(
+ "/org/:orgId/label/:labelId",
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.deleteOrgLabel),
+ labels.deleteOrgLabel
+);
+
+authenticated.put(
+ "/org/:orgId/label/:labelId/attach",
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.attachLabelToItem),
+ labels.attachLabelToItem
+);
+
+authenticated.put(
+ "/org/:orgId/label/:labelId/detach",
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.detachLabelFromItem),
+ labels.detachLabelFromItem
+);
+
// Auth routes
export const authRouter = Router();
unauthenticated.use("/auth", authRouter);
diff --git a/server/private/routers/labels/attachLabelToItem.ts b/server/routers/labels/attachLabelToItem.ts
similarity index 94%
rename from server/private/routers/labels/attachLabelToItem.ts
rename to server/routers/labels/attachLabelToItem.ts
index d011a606d..0d9e49b35 100644
--- a/server/private/routers/labels/attachLabelToItem.ts
+++ b/server/routers/labels/attachLabelToItem.ts
@@ -1,16 +1,3 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 Fossorial, Inc.
- * All rights reserved.
- *
- * This file is licensed under the Fossorial Commercial License.
- * You may not use this file except in compliance with the License.
- * Unauthorized use, copying, modification, or distribution is strictly prohibited.
- *
- * This file is not licensed under the AGPLv3.
- */
-
import {
clients,
clientLabels,
diff --git a/server/private/routers/labels/createOrgLabel.ts b/server/routers/labels/createOrgLabel.ts
similarity index 91%
rename from server/private/routers/labels/createOrgLabel.ts
rename to server/routers/labels/createOrgLabel.ts
index c856eecf4..6fa20816e 100644
--- a/server/private/routers/labels/createOrgLabel.ts
+++ b/server/routers/labels/createOrgLabel.ts
@@ -1,15 +1,3 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 Fossorial, Inc.
- * All rights reserved.
- *
- * This file is licensed under the Fossorial Commercial License.
- * You may not use this file except in compliance with the License.
- * Unauthorized use, copying, modification, or distribution is strictly prohibited.
- *
- * This file is not licensed under the AGPLv3.
- */
import {
db,
labels,
diff --git a/server/private/routers/labels/deleteOrgLabel.ts b/server/routers/labels/deleteOrgLabel.ts
similarity index 82%
rename from server/private/routers/labels/deleteOrgLabel.ts
rename to server/routers/labels/deleteOrgLabel.ts
index f091c910a..c46e67437 100644
--- a/server/private/routers/labels/deleteOrgLabel.ts
+++ b/server/routers/labels/deleteOrgLabel.ts
@@ -1,15 +1,3 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 Fossorial, Inc.
- * All rights reserved.
- *
- * This file is licensed under the Fossorial Commercial License.
- * You may not use this file except in compliance with the License.
- * Unauthorized use, copying, modification, or distribution is strictly prohibited.
- *
- * This file is not licensed under the AGPLv3.
- */
import { db, labels } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
diff --git a/server/private/routers/labels/detachLabelFromItem.ts b/server/routers/labels/detachLabelFromItem.ts
similarity index 94%
rename from server/private/routers/labels/detachLabelFromItem.ts
rename to server/routers/labels/detachLabelFromItem.ts
index 9a5545312..52c752ca8 100644
--- a/server/private/routers/labels/detachLabelFromItem.ts
+++ b/server/routers/labels/detachLabelFromItem.ts
@@ -1,16 +1,3 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 Fossorial, Inc.
- * All rights reserved.
- *
- * This file is licensed under the Fossorial Commercial License.
- * You may not use this file except in compliance with the License.
- * Unauthorized use, copying, modification, or distribution is strictly prohibited.
- *
- * This file is not licensed under the AGPLv3.
- */
-
import {
clients,
clientLabels,
diff --git a/server/routers/labels/index.ts b/server/routers/labels/index.ts
new file mode 100644
index 000000000..aa8c34222
--- /dev/null
+++ b/server/routers/labels/index.ts
@@ -0,0 +1,6 @@
+export * from "./listOrgLabels";
+export * from "./createOrgLabel";
+export * from "./updateOrgLabel";
+export * from "./attachLabelToItem";
+export * from "./detachLabelFromItem";
+export * from "./deleteOrgLabel";
diff --git a/server/private/routers/labels/listOrgLabels.ts b/server/routers/labels/listOrgLabels.ts
similarity index 91%
rename from server/private/routers/labels/listOrgLabels.ts
rename to server/routers/labels/listOrgLabels.ts
index dc2b50017..6d6b853cf 100644
--- a/server/private/routers/labels/listOrgLabels.ts
+++ b/server/routers/labels/listOrgLabels.ts
@@ -1,16 +1,3 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 Fossorial, Inc.
- * All rights reserved.
- *
- * This file is licensed under the Fossorial Commercial License.
- * You may not use this file except in compliance with the License.
- * Unauthorized use, copying, modification, or distribution is strictly prohibited.
- *
- * This file is not licensed under the AGPLv3.
- */
-
import { db, labels } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
diff --git a/server/private/routers/labels/updateOrgLabel.ts b/server/routers/labels/updateOrgLabel.ts
similarity index 89%
rename from server/private/routers/labels/updateOrgLabel.ts
rename to server/routers/labels/updateOrgLabel.ts
index 134a01f02..fe8a6f573 100644
--- a/server/private/routers/labels/updateOrgLabel.ts
+++ b/server/routers/labels/updateOrgLabel.ts
@@ -1,16 +1,3 @@
-/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025-2026 Fossorial, Inc.
- * All rights reserved.
- *
- * This file is licensed under the Fossorial Commercial License.
- * You may not use this file except in compliance with the License.
- * Unauthorized use, copying, modification, or distribution is strictly prohibited.
- *
- * This file is not licensed under the AGPLv3.
- */
-
import { db, labels } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
diff --git a/server/routers/launcher/launcherResourceAccess.ts b/server/routers/launcher/launcherResourceAccess.ts
index 246f86688..ec4500262 100644
--- a/server/routers/launcher/launcherResourceAccess.ts
+++ b/server/routers/launcher/launcherResourceAccess.ts
@@ -274,10 +274,7 @@ function combineOrConditions(
return or(...parts);
}
-function buildSearchConditionForPublic(
- query: string,
- labelsFeatureEnabled: boolean
-) {
+function buildSearchConditionForPublic(query: string) {
if (!query.trim()) {
return undefined;
}
@@ -295,32 +292,21 @@ function buildSearchConditionForPublic(
.leftJoin(sites, eq(targets.siteId, sites.siteId))
.leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId))
.where(like(sql`LOWER(${exitNodes.endpoint})`, pattern))
+ ),
+ inArray(
+ resources.resourceId,
+ db
+ .select({ id: resourceLabels.resourceId })
+ .from(resourceLabels)
+ .innerJoin(labels, eq(labels.labelId, resourceLabels.labelId))
+ .where(like(sql`LOWER(${labels.name})`, pattern))
)
];
- if (labelsFeatureEnabled) {
- queryList.push(
- inArray(
- resources.resourceId,
- db
- .select({ id: resourceLabels.resourceId })
- .from(resourceLabels)
- .innerJoin(
- labels,
- eq(labels.labelId, resourceLabels.labelId)
- )
- .where(like(sql`LOWER(${labels.name})`, pattern))
- )
- );
- }
-
return or(...queryList);
}
-function buildSearchConditionForSiteResource(
- query: string,
- labelsFeatureEnabled: boolean
-) {
+function buildSearchConditionForSiteResource(query: string) {
if (!query.trim()) {
return undefined;
}
@@ -335,40 +321,34 @@ function buildSearchConditionForSiteResource(
like(sql`LOWER(${siteResources.scheme})`, pattern),
like(sql`LOWER(${siteResources.alias})`, pattern),
like(sql`LOWER(${siteResources.fullDomain})`, pattern),
- like(sql`LOWER(${siteResources.aliasAddress})`, pattern)
+ like(sql`LOWER(${siteResources.aliasAddress})`, pattern),
+ inArray(
+ siteResources.siteResourceId,
+ db
+ .select({ id: siteResourceLabels.siteResourceId })
+ .from(siteResourceLabels)
+ .innerJoin(
+ labels,
+ eq(labels.labelId, siteResourceLabels.labelId)
+ )
+ .where(like(sql`LOWER(${labels.name})`, pattern))
+ )
];
- if (labelsFeatureEnabled) {
- queryList.push(
- inArray(
- siteResources.siteResourceId,
- db
- .select({ id: siteResourceLabels.siteResourceId })
- .from(siteResourceLabels)
- .innerJoin(
- labels,
- eq(labels.labelId, siteResourceLabels.labelId)
- )
- .where(like(sql`LOWER(${labels.name})`, pattern))
- )
- );
- }
-
return or(...queryList);
}
async function filterPublicResourceIdsByTextSearch(
orgId: string,
resourceIds: number[],
- query: string,
- labelsFeatureEnabled: boolean
+ query: string
): Promise {
if (!query.trim() || resourceIds.length === 0) {
return resourceIds;
}
const textMatch = combineOrConditions(
- buildSearchConditionForPublic(query, labelsFeatureEnabled),
+ buildSearchConditionForPublic(query),
buildSiteNameSearchCondition(query)
);
if (!textMatch) {
@@ -395,15 +375,14 @@ async function filterPublicResourceIdsByTextSearch(
async function filterSiteResourceIdsByTextSearch(
orgId: string,
siteResourceIds: number[],
- query: string,
- labelsFeatureEnabled: boolean
+ query: string
): Promise {
if (!query.trim() || siteResourceIds.length === 0) {
return siteResourceIds;
}
const textMatch = combineOrConditions(
- buildSearchConditionForSiteResource(query, labelsFeatureEnabled),
+ buildSearchConditionForSiteResource(query),
buildSiteNameSearchCondition(query)
);
if (!textMatch) {
@@ -430,10 +409,6 @@ async function filterSiteResourceIdsByTextSearch(
return rows.map((row) => row.siteResourceId);
}
-async function labelsEnabled(orgId: string): Promise {
- return isLicensedOrSubscribed(orgId, tierMatrix.labels);
-}
-
async function fetchLabelsForResources(
orgId: string,
resourceIds: number[],
@@ -445,10 +420,6 @@ async function fetchLabelsForResources(
const byResourceId = new Map();
const bySiteResourceId = new Map();
- if (!(await labelsEnabled(orgId))) {
- return { byResourceId, bySiteResourceId };
- }
-
const [resourceLabelRows, siteResourceLabelRows] = await Promise.all([
resourceIds.length === 0
? Promise.resolve([])
@@ -524,15 +495,8 @@ async function listSiteGroups(
): Promise<{ groups: LauncherGroup[]; total: number }> {
const siteFilterIds = parseIdListParam(query.siteIds);
const labelFilterIds = parseIdListParam(query.labelIds);
- const labelsFeatureEnabled = await labelsEnabled(orgId);
- const searchPublic = buildSearchConditionForPublic(
- query.query,
- labelsFeatureEnabled
- );
- const searchSite = buildSearchConditionForSiteResource(
- query.query,
- labelsFeatureEnabled
- );
+ const searchPublic = buildSearchConditionForPublic(query.query);
+ const searchSite = buildSearchConditionForSiteResource(query.query);
const siteCountMap = new Map();
if (accessible.resourceIds.length > 0) {
@@ -775,10 +739,6 @@ async function listLabelGroups(
>();
let unlabeledCount = 0;
- if (!(await labelsEnabled(orgId))) {
- return { groups: [], total: 0 };
- }
-
const matchesLabelFilters = (labelId: number) =>
labelFilterIds.length === 0 || labelFilterIds.includes(labelId);
@@ -788,7 +748,7 @@ async function listLabelGroups(
eq(resources.orgId, orgId),
eq(resources.enabled, true)
];
- const searchPublic = buildSearchConditionForPublic(query.query, true);
+ const searchPublic = buildSearchConditionForPublic(query.query);
if (searchPublic) {
publicConditions.push(searchPublic);
}
@@ -852,10 +812,7 @@ async function listLabelGroups(
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true)
];
- const searchSite = buildSearchConditionForSiteResource(
- query.query,
- true
- );
+ const searchSite = buildSearchConditionForSiteResource(query.query);
if (searchSite) {
siteConditions.push(searchSite);
}
@@ -1302,23 +1259,19 @@ async function listLauncherResourcesForUserUncached(
}
}
- const labelsFeatureEnabled = await labelsEnabled(orgId);
-
if (query.query.trim()) {
if (filteredResourceIds.length > 0) {
filteredResourceIds = await filterPublicResourceIdsByTextSearch(
orgId,
filteredResourceIds,
- query.query,
- labelsFeatureEnabled
+ query.query
);
}
if (filteredSiteResourceIds.length > 0) {
filteredSiteResourceIds = await filterSiteResourceIdsByTextSearch(
orgId,
filteredSiteResourceIds,
- query.query,
- labelsFeatureEnabled
+ query.query
);
}
}
@@ -1518,10 +1471,6 @@ async function collectAccessibleLabels(
): Promise
);
}
- }
- ];
-
- if (isLabelFeatureEnabled) {
- cols.splice(cols.length - 1, 0, {
+ },
+ {
id: "labels",
accessorKey: "labels",
header: () => (
@@ -500,11 +496,11 @@ export default function PrivateResourcesTable({
orgId={orgId}
/>
)
- });
- }
+ }
+ ];
return cols;
- }, [isLabelFeatureEnabled, orgId, t, searchParams]);
+ }, [orgId, t, searchParams]);
function handleFilterChange(
column: string,
diff --git a/src/components/PublicResourcesTable.tsx b/src/components/PublicResourcesTable.tsx
index 1cc13ad80..2ac60013a 100644
--- a/src/components/PublicResourcesTable.tsx
+++ b/src/components/PublicResourcesTable.tsx
@@ -151,7 +151,6 @@ export default function PublicResourcesTable({
useState();
const { isPaidUser } = usePaidStatus();
- const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels);
const [isRefreshing, startTransition] = useTransition();
const [isNavigatingToAddPage, startNavigation] = useTransition();
@@ -604,11 +603,8 @@ export default function PublicResourcesTable({
);
}
- }
- ];
-
- if (isLabelFeatureEnabled) {
- cols.splice(cols.length - 1, 0, {
+ },
+ {
id: "labels",
accessorKey: "labels",
header: () => (
@@ -625,11 +621,11 @@ export default function PublicResourcesTable({
cell: ({ row }: { row: { original: ResourceRow } }) => (
)
- });
- }
+ }
+ ];
return cols;
- }, [isLabelFeatureEnabled, orgId, t, searchParams]);
+ }, [orgId, t, searchParams]);
function handleFilterChange(
column: string,
diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx
index efcf83e72..2592a2143 100644
--- a/src/components/SitesTable.tsx
+++ b/src/components/SitesTable.tsx
@@ -53,7 +53,6 @@ import {
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
-import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
import { LabelsTableCell } from "./LabelsTableCell";
import { useQuery } from "@tanstack/react-query";
@@ -114,7 +113,6 @@ export default function SitesTable({
const [isNavigatingToAddPage, startNavigation] = useTransition();
const { isPaidUser } = usePaidStatus();
- const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels);
const api = createApiClient(useEnvContext());
const t = useTranslations();
@@ -171,7 +169,10 @@ export default function SitesTable({
toast({
variant: "destructive",
title: t("siteErrorRestart"),
- description: formatAxiosError(e, t("siteErrorRestartDescription"))
+ description: formatAxiosError(
+ e,
+ t("siteErrorRestartDescription")
+ )
});
} finally {
setRestartingSite(null);
@@ -598,11 +599,8 @@ export default function SitesTable({
);
}
- }
- ];
-
- if (isLabelFeatureEnabled) {
- cols.splice(cols.length - 1, 0, {
+ },
+ {
accessorKey: "labels",
header: () => (