mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-29 12:12:10 +00:00
Merge branch 'dev' into alerting-rules
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1542,7 +1542,7 @@ export function InternalResourceForm({
|
|||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<span className="pl-1">
|
<span className="pl-1 font-normal">
|
||||||
{t(
|
{t(
|
||||||
"accessClientSelect"
|
"accessClientSelect"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user