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,16 +230,15 @@ 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 Configure the OAuth2/OIDC provider endpoints
endpoints and credentials and credentials
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
@ -246,9 +246,7 @@ export default function Page() {
<form <form
className="space-y-4" className="space-y-4"
id="create-idp-form" id="create-idp-form"
onSubmit={form.handleSubmit( onSubmit={form.handleSubmit(onSubmit)}
onSubmit
)}
> >
<FormField <FormField
control={form.control} control={form.control}
@ -286,9 +284,9 @@ export default function Page() {
/> />
</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>
@ -310,8 +308,7 @@ export default function Page() {
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
The OAuth2 The OAuth2 authorization
authorization
endpoint URL endpoint URL
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
@ -350,12 +347,11 @@ export default function Page() {
Important Information Important Information
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
After creating the identity After creating the identity provider,
provider, you will need to configure you will need to configure the callback
the callback URL in your identity URL in your identity provider's
provider's settings. The callback settings. The callback URL will be
URL will be provided after provided after successful creation.
successful creation.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
</SettingsSectionBody> </SettingsSectionBody>
@ -367,8 +363,8 @@ export default function Page() {
Token Configuration Token Configuration
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
Configure how to extract user Configure how to extract user information
information from the ID token from the ID token
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
@ -376,9 +372,7 @@ export default function Page() {
<form <form
className="space-y-4" className="space-y-4"
id="create-idp-form" id="create-idp-form"
onSubmit={form.handleSubmit( onSubmit={form.handleSubmit(onSubmit)}
onSubmit
)}
> >
<Alert variant="neutral"> <Alert variant="neutral">
<InfoIcon className="h-4 w-4" /> <InfoIcon className="h-4 w-4" />
@ -387,16 +381,15 @@ export default function Page() {
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
The paths below use JMESPath The paths below use JMESPath
syntax to extract values syntax to extract values from
from the ID token. the ID token.
<a <a
href="https://jmespath.org" href="https://jmespath.org"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center" className="text-primary hover:underline inline-flex items-center"
> >
Learn more about Learn more about JMESPath{" "}
JMESPath{" "}
<ExternalLink className="ml-1 h-4 w-4" /> <ExternalLink className="ml-1 h-4 w-4" />
</a> </a>
</AlertDescription> </AlertDescription>
@ -414,9 +407,9 @@ export default function Page() {
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
The JMESPath to the The JMESPath to the user
user identifier in identifier in the ID
the ID token token
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -429,16 +422,15 @@ export default function Page() {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
Email Path Email Path (Optional)
(Optional)
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
The JMESPath to the The JMESPath to the
user's email in the user's email in the ID
ID token token
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -458,8 +450,8 @@ export default function Page() {
</FormControl> </FormControl>
<FormDescription> <FormDescription>
The JMESPath to the The JMESPath to the
user's name in the user's name in the ID
ID token token
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -478,9 +470,8 @@ export default function Page() {
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Space-separated list Space-separated list of
of OAuth2 scopes to OAuth2 scopes to request
request
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -490,8 +481,7 @@ export default function Page() {
</Form> </Form>
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
</div> </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>;
} }