Merge branch 'dev' into alerting-rules

This commit is contained in:
Owen
2026-04-20 21:21:03 -07:00
16 changed files with 255 additions and 236 deletions

View File

@@ -167,7 +167,7 @@
"proxyResourceTitle": "Manage Public Resources", "proxyResourceTitle": "Manage Public Resources",
"proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser", "proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser",
"proxyResourcesBannerTitle": "Web-based Public Access", "proxyResourcesBannerTitle": "Web-based Public Access",
"proxyResourcesBannerDescription": "Public resources are HTTPS or TCP/UDP proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.", "proxyResourcesBannerDescription": "Public resources are HTTPS proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.",
"clientResourceTitle": "Manage Private Resources", "clientResourceTitle": "Manage Private Resources",
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client", "clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
"privateResourcesBannerTitle": "Zero-Trust Private Access", "privateResourcesBannerTitle": "Zero-Trust Private Access",
@@ -384,7 +384,7 @@
"userTitle": "Manage All Users", "userTitle": "Manage All Users",
"userDescription": "View and manage all users in the system", "userDescription": "View and manage all users in the system",
"userAbount": "About User Management", "userAbount": "About User Management",
"userAbountDescription": "This table displays all root user objects in the system. Each user may belong to multiple organizations. Removing a user from an organization does not delete their root user object - they will remain in the system. To completely remove a user from the system, you must delete their root user object using the delete action in this table.", "userAbountDescription": "This table displays all base user objects in the system. Each user may belong to multiple organizations. Removing a user from an organization does not delete their base user object. They will remain in the system. To completely remove a user from the system, you must delete their base user object using the delete action in this table.",
"userServer": "Server Users", "userServer": "Server Users",
"userSearch": "Search server users...", "userSearch": "Search server users...",
"userErrorDelete": "Error deleting user", "userErrorDelete": "Error deleting user",
@@ -527,7 +527,7 @@
"userSettings": "User Information", "userSettings": "User Information",
"userSettingsDescription": "Enter the details for the new user", "userSettingsDescription": "Enter the details for the new user",
"inviteEmailSent": "Send invite email to user", "inviteEmailSent": "Send invite email to user",
"inviteValid": "Valid For", "inviteValid": "Invite Valid For (days)",
"selectDuration": "Select duration", "selectDuration": "Select duration",
"selectResource": "Select Resource", "selectResource": "Select Resource",
"filterByResource": "Filter By Resource", "filterByResource": "Filter By Resource",
@@ -2565,7 +2565,7 @@
"action": "Action", "action": "Action",
"actor": "Actor", "actor": "Actor",
"timestamp": "Timestamp", "timestamp": "Timestamp",
"accessLogs": "Access Logs", "accessLogs": "Authentication Logs",
"exportCsv": "Export CSV", "exportCsv": "Export CSV",
"exportError": "Unknown error when exporting CSV", "exportError": "Unknown error when exporting CSV",
"exportCsvTooltip": "Within Time Range", "exportCsvTooltip": "Within Time Range",
@@ -2586,25 +2586,25 @@
"noMoreAuthMethods": "No Valid Auth", "noMoreAuthMethods": "No Valid Auth",
"ip": "IP", "ip": "IP",
"reason": "Reason", "reason": "Reason",
"requestLogs": "Request Logs", "requestLogs": "HTTPS Request Logs",
"requestAnalytics": "Request Analytics", "requestAnalytics": "Request Analytics",
"host": "Host", "host": "Host",
"location": "Location", "location": "Location",
"actionLogs": "Action Logs", "actionLogs": "Admin Action Logs",
"sidebarLogsRequest": "Request Logs", "sidebarLogsRequest": "HTTPS Request Logs",
"sidebarLogsAccess": "Access Logs", "sidebarLogsAccess": "Authentication Logs",
"sidebarLogsAction": "Action Logs", "sidebarLogsAction": "Admin Action Logs",
"logRetention": "Log Retention", "logRetention": "Log Retention",
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them", "logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
"requestLogsDescription": "View detailed request logs for resources in this organization", "requestLogsDescription": "View detailed request logs for HTTPS resources in this organization",
"requestAnalyticsDescription": "View detailed request analytics for resources in this organization", "requestAnalyticsDescription": "View detailed request analytics for resources in this organization",
"logRetentionRequestLabel": "Request Log Retention", "logRetentionRequestLabel": "HTTPS Request Log Retention",
"logRetentionRequestDescription": "How long to retain request logs", "logRetentionRequestDescription": "How long to retain request logs",
"logRetentionAccessLabel": "Access Log Retention", "logRetentionAccessLabel": "Authentication Log Retention",
"logRetentionAccessDescription": "How long to retain access logs", "logRetentionAccessDescription": "How long to retain access logs",
"logRetentionActionLabel": "Action Log Retention", "logRetentionActionLabel": "Admin Action Log Retention",
"logRetentionActionDescription": "How long to retain action logs", "logRetentionActionDescription": "How long to retain action logs",
"logRetentionConnectionLabel": "Connection Log Retention", "logRetentionConnectionLabel": "Network Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs", "logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Disabled", "logRetentionDisabled": "Disabled",
"logRetention3Days": "3 days", "logRetention3Days": "3 days",
@@ -2616,10 +2616,10 @@
"logRetentionEndOfFollowingYear": "End of following year", "logRetentionEndOfFollowingYear": "End of following year",
"actionLogsDescription": "View a history of actions performed in this organization", "actionLogsDescription": "View a history of actions performed in this organization",
"accessLogsDescription": "View access auth requests for resources in this organization", "accessLogsDescription": "View access auth requests for resources in this organization",
"connectionLogs": "Connection Logs", "connectionLogs": "Network Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization", "connectionLogsDescription": "View network session logs handled by sites in this organization",
"sidebarLogsConnection": "Connection Logs", "sidebarLogsConnection": "Network Logs",
"sidebarLogsStreaming": "Streaming", "sidebarLogsStreaming": "Event Streaming",
"sourceAddress": "Source Address", "sourceAddress": "Source Address",
"destinationAddress": "Destination Address", "destinationAddress": "Destination Address",
"duration": "Duration", "duration": "Duration",
@@ -3051,13 +3051,13 @@
"httpDestFormatSingleDescription": "Sends a separate HTTP POST for each individual event. Use only for endpoints that cannot handle batches.", "httpDestFormatSingleDescription": "Sends a separate HTTP POST for each individual event. Use only for endpoints that cannot handle batches.",
"httpDestLogTypesTitle": "Log Types", "httpDestLogTypesTitle": "Log Types",
"httpDestLogTypesDescription": "Choose which log types are forwarded to this destination. Only enabled log types will be streamed.", "httpDestLogTypesDescription": "Choose which log types are forwarded to this destination. Only enabled log types will be streamed.",
"httpDestAccessLogsTitle": "Access Logs", "httpDestAccessLogsTitle": "Authentication Logs",
"httpDestAccessLogsDescription": "Resource access attempts, including authenticated and denied requests.", "httpDestAccessLogsDescription": "Resource access attempts, including authenticated and denied requests.",
"httpDestActionLogsTitle": "Action Logs", "httpDestActionLogsTitle": "Admin Action Logs",
"httpDestActionLogsDescription": "Administrative actions performed by users within the organization.", "httpDestActionLogsDescription": "Administrative actions performed by users within the organization.",
"httpDestConnectionLogsTitle": "Connection Logs", "httpDestConnectionLogsTitle": "Network Logs",
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.", "httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
"httpDestRequestLogsTitle": "Request Logs", "httpDestRequestLogsTitle": "HTTPS Request Logs",
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.", "httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
"httpDestSaveChanges": "Save Changes", "httpDestSaveChanges": "Save Changes",
"httpDestCreateDestination": "Create Destination", "httpDestCreateDestination": "Create Destination",

View File

@@ -103,7 +103,8 @@ export async function listDomains(
const [{ count }] = await db const [{ count }] = await db
.select({ count: sql<number>`count(*)` }) .select({ count: sql<number>`count(*)` })
.from(domains); .from(orgDomains)
.where(eq(orgDomains.orgId, orgId));
return response<ListDomainsResponse>(res, { return response<ListDomainsResponse>(res, {
data: { data: {

View File

@@ -467,7 +467,7 @@ export default function Page() {
<div> <div>
<SettingsContainer> <SettingsContainer>
{!inviteLink ? ( {!inviteLink && userOptions.length > 1 ? (
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
@@ -490,7 +490,7 @@ export default function Page() {
genericOidcForm.reset(); genericOidcForm.reset();
} }
}} }}
cols={2} cols={3}
/> />
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>

View File

@@ -776,9 +776,15 @@ export default function Page() {
pathMatchType: row.original.pathMatchType pathMatchType: row.original.pathMatchType
}} }}
onChange={(config) => onChange={(config) =>
updateTarget(row.original.targetId, updateTarget(
config.path === null && config.pathMatchType === null row.original.targetId,
? { ...config, rewritePath: null, rewritePathType: null } config.path === null &&
config.pathMatchType === null
? {
...config,
rewritePath: null,
rewritePathType: null
}
: config : config
) )
} }
@@ -804,9 +810,15 @@ export default function Page() {
pathMatchType: row.original.pathMatchType pathMatchType: row.original.pathMatchType
}} }}
onChange={(config) => onChange={(config) =>
updateTarget(row.original.targetId, updateTarget(
config.path === null && config.pathMatchType === null row.original.targetId,
? { ...config, rewritePath: null, rewritePathType: null } config.path === null &&
config.pathMatchType === null
? {
...config,
rewritePath: null,
rewritePathType: null
}
: config : config
) )
} }
@@ -1061,7 +1073,7 @@ export default function Page() {
: null : null
); );
}} }}
cols={2} cols={3}
/> />
</> </>
)} )}
@@ -1118,28 +1130,30 @@ export default function Page() {
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<DomainPicker <SettingsSectionForm>
orgId={orgId as string} <DomainPicker
warnOnProvidedDomain={ orgId={orgId as string}
remoteExitNodes.length >= 1 warnOnProvidedDomain={
} remoteExitNodes.length >= 1
onDomainChange={(res) => { }
if (!res) return; onDomainChange={(res) => {
if (!res) return;
httpForm.setValue( httpForm.setValue(
"subdomain", "subdomain",
res.subdomain res.subdomain
); );
httpForm.setValue( httpForm.setValue(
"domainId", "domainId",
res.domainId res.domainId
); );
console.log( console.log(
"Domain changed:", "Domain changed:",
res res
); );
}} }}
/> />
</SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
) : ( ) : (
@@ -1155,98 +1169,101 @@ export default function Page() {
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<Form {...tcpUdpForm}> <SettingsSectionForm>
<form <Form {...tcpUdpForm}>
onKeyDown={(e) => { <form
if (e.key === "Enter") { onKeyDown={(e) => {
e.preventDefault(); // block default enter refresh if (e.key === "Enter") {
} e.preventDefault(); // block default enter refresh
}} }
className="space-y-4 grid gap-4 grid-cols-1 md:grid-cols-2 items-start" }}
id="tcp-udp-settings-form" className="space-y-4 grid gap-4 grid-cols-1 md:grid-cols-2 items-start"
> id="tcp-udp-settings-form"
<Controller >
control={tcpUdpForm.control} <Controller
name="protocol" control={
render={({ field }) => ( tcpUdpForm.control
<FormItem> }
<FormLabel> name="protocol"
{t("protocol")} render={({ field }) => (
</FormLabel> <FormItem>
<Select <FormLabel>
onValueChange={ {t(
field.onChange "protocol"
} )}
{...field} </FormLabel>
> <Select
<FormControl> onValueChange={
<SelectTrigger> field.onChange
<SelectValue }
placeholder={t( {...field}
"protocolSelect" >
)} <FormControl>
/> <SelectTrigger>
</SelectTrigger> <SelectValue
</FormControl> placeholder={t(
<SelectContent> "protocolSelect"
<SelectItem value="tcp"> )}
TCP />
</SelectItem> </SelectTrigger>
<SelectItem value="udp"> </FormControl>
UDP <SelectContent>
</SelectItem> <SelectItem value="tcp">
</SelectContent> TCP
</Select> </SelectItem>
<FormMessage /> <SelectItem value="udp">
</FormItem> UDP
)} </SelectItem>
/> </SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={tcpUdpForm.control} control={
name="proxyPort" tcpUdpForm.control
render={({ field }) => ( }
<FormItem> name="proxyPort"
<FormLabel> render={({ field }) => (
{t( <FormItem>
"resourcePortNumber" <FormLabel>
)} {t(
</FormLabel> "resourcePortNumber"
<FormControl> )}
<Input </FormLabel>
type="number" <FormControl>
value={ <Input
field.value ?? type="number"
"" value={
} field.value ??
onChange={( ""
e }
) => onChange={(
field.onChange(
e e
.target ) =>
.value field.onChange(
? parseInt( e
e .target
.target .value
.value ? parseInt(
) e
: undefined .target
) .value
} )
/> : undefined
</FormControl> )
<FormMessage /> }
<FormDescription> />
{t( </FormControl>
"resourcePortNumberDescription" <FormMessage />
)} </FormItem>
</FormDescription> )}
</FormItem> />
)} </form>
/> </Form>
</form> </SettingsSectionForm>
</Form>
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
)} )}

View File

@@ -6,7 +6,7 @@
:root { :root {
--radius: 0.75rem; --radius: 0.75rem;
--background: oklch(0.985 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823); --foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823); --card-foreground: oklch(0.141 0.005 285.823);
@@ -22,30 +22,30 @@
--accent-foreground: oklch(0.21 0.006 285.885); --accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0); --destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.91 0.004 286.32); --border: oklch(0.88 0.004 286.32);
--input: oklch(0.92 0.004 286.32); --input: oklch(0.88 0.004 286.32);
--ring: oklch(0.705 0.213 47.604); --ring: oklch(0.705 0.213 47.604);
--chart-1: oklch(0.646 0.222 41.116); --chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704); --chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392); --chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429); --chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08); --chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0); --sidebar: #fafafa;
--sidebar-foreground: oklch(0.141 0.005 285.823); --sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.705 0.213 47.604); --sidebar-primary: oklch(0.705 0.213 47.604);
--sidebar-primary-foreground: oklch(0.98 0.016 73.684); --sidebar-primary-foreground: oklch(0.98 0.016 73.684);
--sidebar-accent: oklch(0.967 0.001 286.375); --sidebar-accent: #eaeaea;
--sidebar-accent-foreground: oklch(0.21 0.006 285.885); --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32); --sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.213 47.604); --sidebar-ring: oklch(0.705 0.213 47.604);
} }
.dark { .dark {
--background: oklch(0.19 0.006 285.885); --background: #0d0d0f;
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885); --card: #0d0d0f;
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885); --popover: #0d0d0f;
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.6717 0.1946 41.93); --primary: oklch(0.6717 0.1946 41.93);
--primary-foreground: oklch(0.98 0.016 73.684); --primary-foreground: oklch(0.98 0.016 73.684);
@@ -57,7 +57,7 @@
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.5382 0.1949 22.216); --destructive: oklch(0.5382 0.1949 22.216);
--destructive-foreground: oklch(0.985 0 0); --destructive-foreground: oklch(0.985 0 0);
--border: oklch(1 0 0 / 13%); --border: oklch(1 0 0 / 15%);
--input: oklch(1 0 0 / 18%); --input: oklch(1 0 0 / 18%);
--ring: oklch(0.646 0.222 41.116); --ring: oklch(0.646 0.222 41.116);
--chart-1: oklch(0.488 0.243 264.376); --chart-1: oklch(0.488 0.243 264.376);
@@ -65,11 +65,11 @@
--chart-3: oklch(0.769 0.188 70.08); --chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9); --chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439); --chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885); --sidebar: #040404;
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.646 0.222 41.116); --sidebar-primary: oklch(0.646 0.222 41.116);
--sidebar-primary-foreground: oklch(0.98 0.016 73.684); --sidebar-primary-foreground: oklch(0.98 0.016 73.684);
--sidebar-accent: oklch(0.274 0.006 286.033); --sidebar-accent: #131317;
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.646 0.222 41.116); --sidebar-ring: oklch(0.646 0.222 41.116);
@@ -110,6 +110,15 @@
--color-chart-4: var(--chart-4); --color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5); --color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-lg: var(--radius); --radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
@@ -166,7 +175,9 @@ p {
} }
@keyframes dot-pulse { @keyframes dot-pulse {
0%, 80%, 100% { 0%,
80%,
100% {
opacity: 0.3; opacity: 0.3;
transform: scale(0.8); transform: scale(0.8);
} }
@@ -189,7 +200,10 @@ p {
/* Only apply custom viewport height on mobile */ /* Only apply custom viewport height on mobile */
@media (max-width: 767px) { @media (max-width: 767px) {
.h-screen-safe { .h-screen-safe {
height: var(--vh, 100vh); /* Use CSS variable set by ViewportHeightFix on mobile */ height: var(
--vh,
100vh
); /* Use CSS variable set by ViewportHeightFix on mobile */
} }
} }
} }

View File

@@ -23,7 +23,7 @@ import { TanstackQueryProvider } from "@app/components/TanstackQueryProvider";
import { TailwindIndicator } from "@app/components/TailwindIndicator"; import { TailwindIndicator } from "@app/components/TailwindIndicator";
import { ViewportHeightFix } from "@app/components/ViewportHeightFix"; import { ViewportHeightFix } from "@app/components/ViewportHeightFix";
import StoreInternalRedirect from "@app/components/StoreInternalRedirect"; import StoreInternalRedirect from "@app/components/StoreInternalRedirect";
import { Inter } from "next/font/google"; import { Inter, Mona_Sans } from "next/font/google";
export const metadata: Metadata = { export const metadata: Metadata = {
title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`,
@@ -36,7 +36,11 @@ const inter = Inter({
subsets: ["latin"] subsets: ["latin"]
}); });
const fontClassName = inter.className; const monaSans = Mona_Sans({
subsets: ["latin"]
});
const fontClassName = monaSans.className;
export default async function RootLayout({ export default async function RootLayout({
children children

View File

@@ -8,23 +8,25 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { build } from "@server/build"; import { build } from "@server/build";
import type { Env } from "@app/lib/types/env";
export function isIdpGlobalModeBannerVisible(env: Env): boolean {
if (build === "saas") {
return false;
}
return env.app.identityProviderMode === undefined;
}
export function IdpGlobalModeBanner() { export function IdpGlobalModeBanner() {
const t = useTranslations(); const t = useTranslations();
const { env } = useEnvContext(); const { env } = useEnvContext();
const { isPaidUser, hasEnterpriseLicense } = usePaidStatus(); const { isPaidUser, hasEnterpriseLicense } = usePaidStatus();
const identityProviderModeUndefined =
env.app.identityProviderMode === undefined;
const paidUserForOrgOidc = isPaidUser(tierMatrix.orgOidc); const paidUserForOrgOidc = isPaidUser(tierMatrix.orgOidc);
const enterpriseUnlicensed = const enterpriseUnlicensed =
build === "enterprise" && !hasEnterpriseLicense; build === "enterprise" && !hasEnterpriseLicense;
if (build === "saas") { if (!isIdpGlobalModeBannerVisible(env)) {
return null;
}
if (!identityProviderModeUndefined) {
return null; return null;
} }

View File

@@ -1542,7 +1542,7 @@ export function InternalResourceForm({
</span> </span>
) )
)} )}
<span className="pl-1"> <span className="pl-1 font-normal">
{t( {t(
"accessClientSelect" "accessClientSelect"
)} )}

View File

@@ -49,7 +49,7 @@ export function LayoutHeader({ showTopBar }: LayoutHeaderProps) {
return ( return (
<div className="absolute top-0 left-0 right-0 z-50 hidden md:block"> <div className="absolute top-0 left-0 right-0 z-50 hidden md:block">
<div className="absolute inset-0 bg-background/83 backdrop-blur-sm" /> <div className="absolute inset-0 bg-background backdrop-blur-sm" />
<div className="relative z-10 px-6 py-2"> <div className="relative z-10 px-6 py-2">
<div className="container mx-auto max-w-12xl"> <div className="container mx-auto max-w-12xl">
<div className="h-16 flex items-center justify-between"> <div className="h-16 flex items-center justify-between">

View File

@@ -125,7 +125,7 @@ export function LayoutSidebar({
return ( return (
<div <div
className={cn( className={cn(
"hidden md:flex border-r bg-card flex-col h-full shrink-0 relative", "hidden md:flex border-r bg-sidebar flex-col h-full shrink-0 relative",
isSidebarCollapsed ? "w-16" : "w-64" isSidebarCollapsed ? "w-16" : "w-64"
)} )}
> >
@@ -154,7 +154,7 @@ export function LayoutSidebar({
<Link <Link
href="/admin" href="/admin"
className={cn( className={cn(
"flex items-center transition-colors text-muted-foreground hover:text-foreground text-sm w-full hover:bg-secondary/80 dark:hover:bg-secondary/50 rounded-md", "flex items-center transition-colors text-muted-foreground hover:text-foreground text-sm w-full hover:bg-sidebar-accent/80 dark:hover:bg-sidebar-accent/50 rounded-md",
isSidebarCollapsed isSidebarCollapsed
? "px-2 py-2 justify-center" ? "px-2 py-2 justify-center"
: "px-3 py-1.5" : "px-3 py-1.5"
@@ -191,7 +191,7 @@ export function LayoutSidebar({
/> />
</div> </div>
{/* Fade gradient at bottom to indicate scrollable content */} {/* Fade gradient at bottom to indicate scrollable content */}
<div className="sticky bottom-0 left-0 right-0 h-8 pointer-events-none bg-gradient-to-t from-card to-transparent" /> <div className="sticky bottom-0 left-0 right-0 h-8 pointer-events-none bg-gradient-to-t from-sidebar to-transparent" />
</div> </div>
{isSidebarCollapsed && ( {isSidebarCollapsed && (
@@ -206,7 +206,7 @@ export function LayoutSidebar({
setHasManualToggle(true); setHasManualToggle(true);
setSidebarStateCookie(false); setSidebarStateCookie(false);
}} }}
className="rounded-md p-2 text-muted-foreground hover:text-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 transition-colors" className="rounded-md p-2 text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/80 dark:hover:bg-sidebar-accent/50 transition-colors"
aria-label={t("sidebarExpand")} aria-label={t("sidebarExpand")}
> >
<PanelRightOpen className="h-4 w-4" /> <PanelRightOpen className="h-4 w-4" />
@@ -220,7 +220,12 @@ export function LayoutSidebar({
</div> </div>
)} )}
<div className="pt-1 flex flex-col shrink-0 gap-2 w-full border-t border-border"> <div
className={cn(
"pt-1 flex flex-col shrink-0 gap-2 w-full border-t border-border",
isSidebarCollapsed && "pb-2"
)}
>
{canShowProductUpdates ? ( {canShowProductUpdates ? (
<div className="px-4"> <div className="px-4">
<ProductUpdates isCollapsed={isSidebarCollapsed} /> <ProductUpdates isCollapsed={isSidebarCollapsed} />

View File

@@ -53,7 +53,7 @@ export default function LocaleSwitcherSelect({
)} )}
aria-label={label} aria-label={label}
> >
<Languages className="h-4 w-4" /> <Languages className="text-muted-foreground h-4 w-4" />
<span className="text-left flex-1"> <span className="text-left flex-1">
{selected?.label ?? label} {selected?.label ?? label}
</span> </span>

View File

@@ -12,13 +12,15 @@ interface DataTableProps<TData, TValue> {
data: TData[]; data: TData[];
onAdd?: () => void; onAdd?: () => void;
addActions?: DataTableAddAction[]; addActions?: DataTableAddAction[];
addButtonDisabled?: boolean;
} }
export function IdpDataTable<TData, TValue>({ export function IdpDataTable<TData, TValue>({
columns, columns,
data, data,
onAdd, onAdd,
addActions addActions,
addButtonDisabled
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const t = useTranslations(); const t = useTranslations();
@@ -33,6 +35,7 @@ export function IdpDataTable<TData, TValue>({
addButtonText={t("idpAdd")} addButtonText={t("idpAdd")}
onAdd={onAdd} onAdd={onAdd}
addActions={addActions} addActions={addActions}
addButtonDisabled={addButtonDisabled}
enableColumnVisibility={true} enableColumnVisibility={true}
stickyRightColumn="actions" stickyRightColumn="actions"
/> />

View File

@@ -52,6 +52,7 @@ import type { ListUserAdminOrgIdpsResponse } from "@server/routers/orgIdp/types"
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { isIdpGlobalModeBannerVisible } from "@app/components/IdpGlobalModeBanner";
export type IdpRow = { export type IdpRow = {
idpId: number; idpId: number;
@@ -85,13 +86,15 @@ export default function IdpTable({ idps, orgId }: Props) {
const [importSubmitting, setImportSubmitting] = useState(false); const [importSubmitting, setImportSubmitting] = useState(false);
const [debouncedImportSearch] = useDebounce(importSearchQuery, 150); const [debouncedImportSearch] = useDebounce(importSearchQuery, 150);
const api = createApiClient(useEnvContext()); const envContext = useEnvContext();
const api = createApiClient(envContext);
const { user } = useUserContext(); const { user } = useUserContext();
const { isPaidUser } = usePaidStatus(); const { isPaidUser } = usePaidStatus();
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const t = useTranslations();
const canImportOrgOidcIdp = isPaidUser(tierMatrix.orgOidc); const canImportOrgOidcIdp = isPaidUser(tierMatrix.orgOidc);
const addIdpDisabled = isIdpGlobalModeBannerVisible(envContext.env);
const { data: adminIdpsRaw = [] } = useQuery({ const { data: adminIdpsRaw = [] } = useQuery({
queryKey: ["admin-org-idps", user.userId], queryKey: ["admin-org-idps", user.userId],
@@ -184,23 +187,6 @@ export default function IdpTable({ idps, orgId }: Props) {
}; };
const columns: ExtendedColumnDef<IdpRow>[] = [ const columns: ExtendedColumnDef<IdpRow>[] = [
{
accessorKey: "idpId",
friendlyName: "ID",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
ID
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{ {
accessorKey: "name", accessorKey: "name",
friendlyName: t("name"), friendlyName: t("name"),
@@ -427,6 +413,7 @@ export default function IdpTable({ idps, orgId }: Props) {
<IdpDataTable <IdpDataTable
columns={columns} columns={columns}
data={idps} data={idps}
addButtonDisabled={addIdpDisabled}
addActions={[ addActions={[
{ {
label: t("idpAddActionCreateNew"), label: t("idpAddActionCreateNew"),

View File

@@ -76,8 +76,8 @@ export function OrgSelector({
className={cn( className={cn(
"cursor-pointer transition-colors", "cursor-pointer transition-colors",
isCollapsed isCollapsed
? "w-full h-16 flex items-center justify-center hover:bg-muted" ? "w-full h-16 flex items-center justify-center hover:bg-sidebar-accent/80 dark:hover:bg-sidebar-accent/50"
: "w-full px-5 py-4 hover:bg-muted" : "w-full px-5 py-4 hover:bg-sidebar-accent/80 dark:hover:bg-sidebar-accent/50"
)} )}
> >
{isCollapsed ? ( {isCollapsed ? (
@@ -172,7 +172,7 @@ export function OrgSelector({
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="w-full justify-start h-8 font-normal text-muted-foreground hover:text-foreground" className="w-full justify-start h-8 font-normal text-muted-foreground"
onClick={() => { onClick={() => {
setOpen(false); setOpen(false);
router.push("/setup"); router.push("/setup");

View File

@@ -121,8 +121,8 @@ function CollapsibleNavItem({
"flex items-center w-full rounded-md transition-colors", "flex items-center w-full rounded-md transition-colors",
"px-3 py-1.5", "px-3 py-1.5",
isActive isActive
? "bg-secondary font-medium" ? "bg-sidebar-accent font-medium"
: "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground", : "text-muted-foreground hover:bg-sidebar-accent/80 dark:hover:bg-sidebar-accent/50 hover:text-foreground",
isDisabled && "cursor-not-allowed opacity-60" isDisabled && "cursor-not-allowed opacity-60"
)} )}
disabled={isDisabled} disabled={isDisabled}
@@ -256,8 +256,8 @@ function CollapsedNavItemWithPopover({
className={cn( className={cn(
"flex items-center rounded-md transition-colors px-2 py-2 justify-center w-full", "flex items-center rounded-md transition-colors px-2 py-2 justify-center w-full",
isActive || isChildActive isActive || isChildActive
? "bg-secondary font-medium" ? "bg-sidebar-accent font-medium"
: "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground", : "text-muted-foreground hover:bg-sidebar-accent/80 dark:hover:bg-sidebar-accent/50 hover:text-foreground",
isDisabled && isDisabled &&
"cursor-not-allowed opacity-60" "cursor-not-allowed opacity-60"
)} )}
@@ -308,8 +308,8 @@ function CollapsedNavItemWithPopover({
className={cn( className={cn(
"flex items-center rounded-md transition-colors px-3 py-1.5 text-sm", "flex items-center rounded-md transition-colors px-3 py-1.5 text-sm",
childIsActive childIsActive
? "bg-secondary font-medium" ? "bg-sidebar-accent font-medium"
: "text-muted-foreground hover:bg-secondary/50 hover:text-foreground", : "text-muted-foreground hover:bg-sidebar-accent/50 hover:text-foreground",
childIsDisabled && childIsDisabled &&
"cursor-not-allowed opacity-60" "cursor-not-allowed opacity-60"
)} )}
@@ -450,8 +450,8 @@ export function SidebarNav({
"flex items-center rounded-md transition-colors relative", "flex items-center rounded-md transition-colors relative",
isCollapsed ? "px-2 py-2 justify-center" : "px-3 py-1.5", isCollapsed ? "px-2 py-2 justify-center" : "px-3 py-1.5",
isActive isActive
? "bg-secondary font-medium" ? "bg-sidebar-accent font-medium"
: "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground", : "text-muted-foreground hover:bg-sidebar-accent/80 dark:hover:bg-sidebar-accent/50 hover:text-foreground",
isDisabled && "cursor-not-allowed opacity-60" isDisabled && "cursor-not-allowed opacity-60"
)} )}
onClick={(e) => { onClick={(e) => {

View File

@@ -27,7 +27,7 @@ import {
TableHeader, TableHeader,
TableRow TableRow
} from "@app/components/ui/table"; } from "@app/components/ui/table";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@app/components/ui/tabs"; import { HorizontalTabs } from "@app/components/HorizontalTabs";
import { Loader2, RefreshCw } from "lucide-react"; import { Loader2, RefreshCw } from "lucide-react";
import moment from "moment"; import moment from "moment";
import { useUserContext } from "@app/hooks/useUserContext"; import { useUserContext } from "@app/hooks/useUserContext";
@@ -58,7 +58,6 @@ export default function ViewDevicesDialog({
const [devices, setDevices] = useState<Device[]>([]); const [devices, setDevices] = useState<Device[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState<"available" | "archived">("available");
const fetchDevices = async () => { const fetchDevices = async () => {
setLoading(true); setLoading(true);
@@ -177,34 +176,21 @@ export default function ViewDevicesDialog({
<Loader2 className="h-6 w-6 animate-spin" /> <Loader2 className="h-6 w-6 animate-spin" />
</div> </div>
) : ( ) : (
<Tabs <HorizontalTabs
value={activeTab} clientSide
onValueChange={(value) => defaultTab={0}
setActiveTab(value as "available" | "archived") items={[
} {
className="w-full" title: `${t("available") || "Available"} (${devices.filter((d) => !d.archived).length})`,
href: "#available"
},
{
title: `${t("archived") || "Archived"} (${devices.filter((d) => d.archived).length})`,
href: "#archived"
}
]}
> >
<TabsList className="grid w-full grid-cols-2"> <div>
<TabsTrigger value="available">
{t("available") || "Available"} (
{
devices.filter(
(d) => !d.archived
).length
}
)
</TabsTrigger>
<TabsTrigger value="archived">
{t("archived") || "Archived"} (
{
devices.filter(
(d) => d.archived
).length
}
)
</TabsTrigger>
</TabsList>
<TabsContent value="available" className="mt-4">
{devices.filter((d) => !d.archived) {devices.filter((d) => !d.archived)
.length === 0 ? ( .length === 0 ? (
<div className="text-center py-8 text-muted-foreground"> <div className="text-center py-8 text-muted-foreground">
@@ -271,8 +257,8 @@ export default function ViewDevicesDialog({
</Table> </Table>
</div> </div>
)} )}
</TabsContent> </div>
<TabsContent value="archived" className="mt-4"> <div>
{devices.filter((d) => d.archived) {devices.filter((d) => d.archived)
.length === 0 ? ( .length === 0 ? (
<div className="text-center py-8 text-muted-foreground"> <div className="text-center py-8 text-muted-foreground">
@@ -336,8 +322,8 @@ export default function ViewDevicesDialog({
</Table> </Table>
</div> </div>
)} )}
</TabsContent> </div>
</Tabs> </HorizontalTabs>
)} )}
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>