mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-01 08:34:53 +02:00
add info box to admin users table
This commit is contained in:
parent
189b739997
commit
3e94384cde
4 changed files with 310 additions and 271 deletions
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue