add info box to admin users table

This commit is contained in:
miloschwartz 2025-04-17 21:21:41 -04:00
parent 189b739997
commit 3e94384cde
No known key found for this signature in database
4 changed files with 310 additions and 271 deletions

View file

@ -6,6 +6,7 @@ import {
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionForm,
SettingsSectionGrid,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
@ -229,269 +230,258 @@ export default function Page() {
</SettingsSection>
{form.watch("type") === "oidc" && (
<>
<div className="grid md:grid-cols-2 gap-6">
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
OAuth2/OIDC Configuration
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure the OAuth2/OIDC provider
endpoints and credentials
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<Form {...form}>
<form
className="space-y-4"
id="create-idp-form"
onSubmit={form.handleSubmit(
onSubmit
<SettingsSectionGrid cols={2}>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
OAuth2/OIDC Configuration
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure the OAuth2/OIDC provider endpoints
and credentials
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<Form {...form}>
<form
className="space-y-4"
id="create-idp-form"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>
Client ID
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The OAuth2 client ID
from your identity
provider
</FormDescription>
<FormMessage />
</FormItem>
)}
>
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>
Client ID
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The OAuth2 client ID
from your identity
provider
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
/>
<FormField
control={form.control}
name="clientSecret"
render={({ field }) => (
<FormItem>
<FormLabel>
Client Secret
</FormLabel>
<FormControl>
<Input
type="password"
{...field}
/>
</FormControl>
<FormDescription>
The OAuth2 client
secret from your
identity provider
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="authUrl"
render={({ field }) => (
<FormItem>
<FormLabel>
Authorization URL
</FormLabel>
<FormControl>
<Input
placeholder="https://your-idp.com/oauth2/authorize"
{...field}
/>
</FormControl>
<FormDescription>
The OAuth2
authorization
endpoint URL
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="tokenUrl"
render={({ field }) => (
<FormItem>
<FormLabel>
Token URL
</FormLabel>
<FormControl>
<Input
placeholder="https://your-idp.com/oauth2/token"
{...field}
/>
</FormControl>
<FormDescription>
The OAuth2 token
endpoint URL
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<Alert variant="neutral">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
Important Information
</AlertTitle>
<AlertDescription>
After creating the identity
provider, you will need to configure
the callback URL in your identity
provider's settings. The callback
URL will be provided after
successful creation.
</AlertDescription>
</Alert>
</SettingsSectionBody>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Token Configuration
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure how to extract user
information from the ID token
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<Form {...form}>
<form
className="space-y-4"
id="create-idp-form"
onSubmit={form.handleSubmit(
onSubmit
<FormField
control={form.control}
name="clientSecret"
render={({ field }) => (
<FormItem>
<FormLabel>
Client Secret
</FormLabel>
<FormControl>
<Input
type="password"
{...field}
/>
</FormControl>
<FormDescription>
The OAuth2 client secret
from your identity
provider
</FormDescription>
<FormMessage />
</FormItem>
)}
>
<Alert variant="neutral">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
About JMESPath
</AlertTitle>
<AlertDescription>
The paths below use JMESPath
syntax to extract values
from the ID token.
<a
href="https://jmespath.org"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center"
>
Learn more about
JMESPath{" "}
<ExternalLink className="ml-1 h-4 w-4" />
</a>
</AlertDescription>
</Alert>
/>
<FormField
control={form.control}
name="identifierPath"
render={({ field }) => (
<FormItem>
<FormLabel>
Identifier Path
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the
user identifier in
the ID token
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="authUrl"
render={({ field }) => (
<FormItem>
<FormLabel>
Authorization URL
</FormLabel>
<FormControl>
<Input
placeholder="https://your-idp.com/oauth2/authorize"
{...field}
/>
</FormControl>
<FormDescription>
The OAuth2 authorization
endpoint URL
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="emailPath"
render={({ field }) => (
<FormItem>
<FormLabel>
Email Path
(Optional)
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the
user's email in the
ID token
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="tokenUrl"
render={({ field }) => (
<FormItem>
<FormLabel>
Token URL
</FormLabel>
<FormControl>
<Input
placeholder="https://your-idp.com/oauth2/token"
{...field}
/>
</FormControl>
<FormDescription>
The OAuth2 token
endpoint URL
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<FormField
control={form.control}
name="namePath"
render={({ field }) => (
<FormItem>
<FormLabel>
Name Path (Optional)
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the
user's name in the
ID token
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Alert variant="neutral">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
Important Information
</AlertTitle>
<AlertDescription>
After creating the identity provider,
you will need to configure the callback
URL in your identity provider's
settings. The callback URL will be
provided after successful creation.
</AlertDescription>
</Alert>
</SettingsSectionBody>
</SettingsSection>
<FormField
control={form.control}
name="scopes"
render={({ field }) => (
<FormItem>
<FormLabel>
Scopes
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Space-separated list
of OAuth2 scopes to
request
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionBody>
</SettingsSection>
</div>
</>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Token Configuration
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure how to extract user information
from the ID token
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<Form {...form}>
<form
className="space-y-4"
id="create-idp-form"
onSubmit={form.handleSubmit(onSubmit)}
>
<Alert variant="neutral">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
About JMESPath
</AlertTitle>
<AlertDescription>
The paths below use JMESPath
syntax to extract values from
the ID token.
<a
href="https://jmespath.org"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center"
>
Learn more about JMESPath{" "}
<ExternalLink className="ml-1 h-4 w-4" />
</a>
</AlertDescription>
</Alert>
<FormField
control={form.control}
name="identifierPath"
render={({ field }) => (
<FormItem>
<FormLabel>
Identifier Path
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the user
identifier in the ID
token
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="emailPath"
render={({ field }) => (
<FormItem>
<FormLabel>
Email Path (Optional)
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the
user's email in the ID
token
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="namePath"
render={({ field }) => (
<FormItem>
<FormLabel>
Name Path (Optional)
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the
user's name in the ID
token
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="scopes"
render={({ field }) => (
<FormItem>
<FormLabel>
Scopes
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Space-separated list of
OAuth2 scopes to request
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionBody>
</SettingsSection>
</SettingsSectionGrid>
)}
</SettingsContainer>

View file

@ -4,6 +4,8 @@ import { AxiosResponse } from "axios";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { AdminListUsersResponse } from "@server/routers/user/adminListUsers";
import UsersTable, { GlobalUserRow } from "./AdminUsersTable";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { InfoIcon } from "lucide-react";
type PageProps = {
params: Promise<{ orgId: string }>;
@ -43,6 +45,13 @@ export default async function UsersPage(props: PageProps) {
title="Manage All Users"
description="View and manage all users in the system"
/>
<Alert variant="neutral" className="mb-6">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">About User Management</AlertTitle>
<AlertDescription>
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.
</AlertDescription>
</Alert>
<UsersTable users={userRows} />
</>
);

View file

@ -22,6 +22,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { Breadcrumbs } from "@app/components/Breadcrumbs";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useUserContext } from "@app/hooks/useUserContext";
interface LayoutProps {
children: React.ReactNode;
@ -57,6 +58,7 @@ export function Layout({
const { env } = useEnvContext();
const pathname = usePathname();
const isAdminPage = pathname?.startsWith("/admin");
const { user } = useUserContext();
return (
<div className="flex h-screen overflow-hidden">
@ -135,7 +137,7 @@ export function Layout({
<div className="flex-1">
<SidebarNav items={navItems} />
</div>
{!isAdminPage && (
{!isAdminPage && user.serverAdmin && (
<div className="mt-8 pt-4 border-t">
<Link
href="/admin"

View file

@ -1,31 +1,69 @@
export function SettingsContainer({ children }: { children: React.ReactNode }) {
return <div className="space-y-6">{children}</div>
return <div className="space-y-6">{children}</div>;
}
export function SettingsSection({ children }: { children: React.ReactNode }) {
return <div className="border rounded-lg bg-card p-5">{children}</div>
return <div className="border rounded-lg bg-card p-5">{children}</div>;
}
export function SettingsSectionHeader({ children }: { children: React.ReactNode }) {
return <div className="text-lg space-y-0.5 pb-6">{children}</div>
export function SettingsSectionHeader({
children
}: {
children: React.ReactNode;
}) {
return <div className="text-lg space-y-0.5 pb-6">{children}</div>;
}
export function SettingsSectionForm({ children }: { children: React.ReactNode }) {
return <div className="max-w-xl">{children}</div>
export function SettingsSectionForm({
children
}: {
children: React.ReactNode;
}) {
return <div className="max-w-xl">{children}</div>;
}
export function SettingsSectionTitle({ children }: { children: React.ReactNode }) {
return <h2 className="text-1xl font-bold tracking-tight flex items-center gap-2">{children}</h2>
export function SettingsSectionTitle({
children
}: {
children: React.ReactNode;
}) {
return (
<h2 className="text-1xl font-bold tracking-tight flex items-center gap-2">
{children}
</h2>
);
}
export function SettingsSectionDescription({ children }: { children: React.ReactNode }) {
return <p className="text-muted-foreground text-sm">{children}</p>
export function SettingsSectionDescription({
children
}: {
children: React.ReactNode;
}) {
return <p className="text-muted-foreground text-sm">{children}</p>;
}
export function SettingsSectionBody({ children }: { children: React.ReactNode }) {
return <div className="space-y-5">{children}</div>
export function SettingsSectionBody({
children
}: {
children: React.ReactNode;
}) {
return <div className="space-y-5">{children}</div>;
}
export function SettingsSectionFooter({ children }: { children: React.ReactNode }) {
return <div className="flex justify-end space-x-4 mt-8">{children}</div>
export function SettingsSectionFooter({
children
}: {
children: React.ReactNode;
}) {
return <div className="flex justify-end space-x-4 mt-8">{children}</div>;
}
export function SettingsSectionGrid({
children,
cols
}: {
children: React.ReactNode;
cols: number;
}) {
return <div className={`grid md:grid-cols-${cols} gap-6`}>{children}</div>;
}