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

View file

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

View file

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

View file

@ -1,31 +1,69 @@
export function SettingsContainer({ children }: { children: React.ReactNode }) { 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 }) { 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 }) { export function SettingsSectionHeader({
return <div className="text-lg space-y-0.5 pb-6">{children}</div> children
}: {
children: React.ReactNode;
}) {
return <div className="text-lg space-y-0.5 pb-6">{children}</div>;
} }
export function SettingsSectionForm({ children }: { children: React.ReactNode }) { export function SettingsSectionForm({
return <div className="max-w-xl">{children}</div> children
}: {
children: React.ReactNode;
}) {
return <div className="max-w-xl">{children}</div>;
} }
export function SettingsSectionTitle({ children }: { children: React.ReactNode }) { export function SettingsSectionTitle({
return <h2 className="text-1xl font-bold tracking-tight flex items-center gap-2">{children}</h2> 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 }) { export function SettingsSectionDescription({
return <p className="text-muted-foreground text-sm">{children}</p> children
}: {
children: React.ReactNode;
}) {
return <p className="text-muted-foreground text-sm">{children}</p>;
} }
export function SettingsSectionBody({ children }: { children: React.ReactNode }) { export function SettingsSectionBody({
return <div className="space-y-5">{children}</div> children
}: {
children: React.ReactNode;
}) {
return <div className="space-y-5">{children}</div>;
} }
export function SettingsSectionFooter({ children }: { children: React.ReactNode }) { export function SettingsSectionFooter({
return <div className="flex justify-end space-x-4 mt-8">{children}</div> 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>;
} }