clean up ui pass 1

This commit is contained in:
miloschwartz
2025-06-30 09:33:48 -07:00
parent 3b6a44e683
commit a0381eb2c6
82 changed files with 17618 additions and 17258 deletions
@@ -20,7 +20,7 @@ import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useUserContext } from "@app/hooks/useUserContext";
import { useTranslations } from 'next-intl';
import { useTranslations } from "next-intl";
export type UserRow = {
id: string;
@@ -51,65 +51,6 @@ export default function UsersTable({ users: u }: UsersTableProps) {
const t = useTranslations();
const columns: ColumnDef<UserRow>[] = [
{
id: "dots",
cell: ({ row }) => {
const userRow = row.original;
return (
<>
<div>
{userRow.isOwner && (
<MoreHorizontal className="h-4 w-4 opacity-0" />
)}
{!userRow.isOwner && (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
{t('openMenu')}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
className="block w-full"
>
<DropdownMenuItem>
{t('accessUsersManage')}
</DropdownMenuItem>
</Link>
{`${userRow.username}-${userRow.idpId}` !==
`${user?.username}-${userRow.idpId}` && (
<DropdownMenuItem
onClick={() => {
setIsDeleteModalOpen(
true
);
setSelectedUser(
userRow
);
}}
>
<span className="text-red-500">
{t('accessUserRemove')}
</span>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</div>
</>
);
}
},
{
accessorKey: "displayUsername",
header: ({ column }) => {
@@ -120,7 +61,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('username')}
{t("username")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -136,7 +77,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('identityProvider')}
{t("identityProvider")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -152,7 +93,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('role')}
{t("role")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -176,12 +117,68 @@ export default function UsersTable({ users: u }: UsersTableProps) {
const userRow = row.original;
return (
<div className="flex items-center justify-end">
<>
<div>
{userRow.isOwner && (
<MoreHorizontal className="h-4 w-4 opacity-0" />
)}
{!userRow.isOwner && (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
className="block w-full"
>
<DropdownMenuItem>
{t("accessUsersManage")}
</DropdownMenuItem>
</Link>
{`${userRow.username}-${userRow.idpId}` !==
`${user?.username}-${userRow.idpId}` && (
<DropdownMenuItem
onClick={() => {
setIsDeleteModalOpen(
true
);
setSelectedUser(
userRow
);
}}
>
<span className="text-red-500">
{t(
"accessUserRemove"
)}
</span>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</div>
</>
{userRow.isOwner && (
<Button
variant="ghost"
className="opacity-0 cursor-default"
variant={"secondary"}
className="ml-2"
size="sm"
disabled={true}
>
{t('placeholder')}
{t("manage")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
)}
{!userRow.isOwner && (
@@ -189,10 +186,12 @@ export default function UsersTable({ users: u }: UsersTableProps) {
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
>
<Button
variant={"outlinePrimary"}
variant={"secondary"}
className="ml-2"
size="sm"
disabled={userRow.isOwner}
>
{t('manage')}
{t("manage")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@@ -210,10 +209,10 @@ export default function UsersTable({ users: u }: UsersTableProps) {
.catch((e) => {
toast({
variant: "destructive",
title: t('userErrorOrgRemove'),
title: t("userErrorOrgRemove"),
description: formatAxiosError(
e,
t('userErrorOrgRemoveDescription')
t("userErrorOrgRemoveDescription")
)
});
});
@@ -221,8 +220,10 @@ export default function UsersTable({ users: u }: UsersTableProps) {
if (res && res.status === 200) {
toast({
variant: "default",
title: t('userOrgRemoved'),
description: t('userOrgRemovedDescription', {email: selectedUser.email || ""})
title: t("userOrgRemoved"),
description: t("userOrgRemovedDescription", {
email: selectedUser.email || ""
})
});
setUsers((prev) =>
@@ -244,19 +245,21 @@ export default function UsersTable({ users: u }: UsersTableProps) {
dialog={
<div className="space-y-4">
<p>
{t('userQuestionOrgRemove', {email: selectedUser?.email || selectedUser?.name || selectedUser?.username || ""})}
{t("userQuestionOrgRemove", {
email:
selectedUser?.email ||
selectedUser?.name ||
selectedUser?.username ||
""
})}
</p>
<p>
{t('userMessageOrgRemove')}
</p>
<p>{t("userMessageOrgRemove")}</p>
<p>
{t('userMessageOrgConfirm')}
</p>
<p>{t("userMessageOrgConfirm")}</p>
</div>
}
buttonText={t('userRemoveOrgConfirm')}
buttonText={t("userRemoveOrgConfirm")}
onConfirm={removeUser}
string={
selectedUser?.email ||
@@ -264,14 +267,16 @@ export default function UsersTable({ users: u }: UsersTableProps) {
selectedUser?.username ||
""
}
title={t('userRemoveOrg')}
title={t("userRemoveOrg")}
/>
<UsersDataTable
columns={columns}
data={users}
inviteUser={() => {
router.push(`/${org?.org.orgId}/settings/access/users/create`);
router.push(
`/${org?.org.orgId}/settings/access/users/create`
);
}}
/>
</>
@@ -5,17 +5,9 @@ import { authCookieHeader } from "@app/lib/api/cookies";
import { GetOrgUserResponse } from "@server/routers/user";
import OrgUserProvider from "@app/providers/OrgUserProvider";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import Link from "next/link";
import { cache } from "react";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { getTranslations } from 'next-intl/server';
import { getTranslations } from "next-intl/server";
interface UserLayoutProps {
children: React.ReactNode;
@@ -45,7 +37,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
const navItems = [
{
title: t('accessControls'),
title: t("accessControls"),
href: "/{orgId}/settings/access/users/{userId}/access-controls"
}
];
@@ -54,12 +46,10 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
<>
<SettingsSectionTitle
title={`${user?.email}`}
description={t('userDescription2')}
description={t("userDescription2")}
/>
<OrgUserProvider orgUser={user}>
<HorizontalTabs items={navItems}>
{children}
</HorizontalTabs>
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
</OrgUserProvider>
</>
);
@@ -78,40 +78,42 @@ export default function Page() {
const [dataLoaded, setDataLoaded] = useState(false);
const internalFormSchema = z.object({
email: z.string().email({ message: t('emailInvalid') }),
validForHours: z.string().min(1, { message: t('inviteValidityDuration') }),
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') })
email: z.string().email({ message: t("emailInvalid") }),
validForHours: z
.string()
.min(1, { message: t("inviteValidityDuration") }),
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") })
});
const externalFormSchema = z.object({
username: z.string().min(1, { message: t('usernameRequired') }),
username: z.string().min(1, { message: t("usernameRequired") }),
email: z
.string()
.email({ message: t('emailInvalid') })
.email({ message: t("emailInvalid") })
.optional()
.or(z.literal("")),
name: z.string().optional(),
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') }),
idpId: z.string().min(1, { message: t('idpSelectPlease') })
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") }),
idpId: z.string().min(1, { message: t("idpSelectPlease") })
});
const formatIdpType = (type: string) => {
switch (type.toLowerCase()) {
case "oidc":
return t('idpGenericOidc');
return t("idpGenericOidc");
default:
return type;
}
};
const validFor = [
{ hours: 24, name: t('day', {count: 1}) },
{ hours: 48, name: t('day', {count: 2}) },
{ hours: 72, name: t('day', {count: 3}) },
{ hours: 96, name: t('day', {count: 4}) },
{ hours: 120, name: t('day', {count: 5}) },
{ hours: 144, name: t('day', {count: 6}) },
{ hours: 168, name: t('day', {count: 7}) }
{ hours: 24, name: t("day", { count: 1 }) },
{ hours: 48, name: t("day", { count: 2 }) },
{ hours: 72, name: t("day", { count: 3 }) },
{ hours: 96, name: t("day", { count: 4 }) },
{ hours: 120, name: t("day", { count: 5 }) },
{ hours: 144, name: t("day", { count: 6 }) },
{ hours: 168, name: t("day", { count: 7 }) }
];
const internalForm = useForm<z.infer<typeof internalFormSchema>>({
@@ -157,10 +159,10 @@ export default function Page() {
console.error(e);
toast({
variant: "destructive",
title: t('accessRoleErrorFetch'),
title: t("accessRoleErrorFetch"),
description: formatAxiosError(
e,
t('accessRoleErrorFetchDescription')
t("accessRoleErrorFetchDescription")
)
});
});
@@ -180,10 +182,10 @@ export default function Page() {
console.error(e);
toast({
variant: "destructive",
title: t('idpErrorFetch'),
title: t("idpErrorFetch"),
description: formatAxiosError(
e,
t('idpErrorFetchDescription')
t("idpErrorFetchDescription")
)
});
});
@@ -220,16 +222,16 @@ export default function Page() {
if (e.response?.status === 409) {
toast({
variant: "destructive",
title: t('userErrorExists'),
description: t('userErrorExistsDescription')
title: t("userErrorExists"),
description: t("userErrorExistsDescription")
});
} else {
toast({
variant: "destructive",
title: t('inviteError'),
title: t("inviteError"),
description: formatAxiosError(
e,
t('inviteErrorDescription')
t("inviteErrorDescription")
)
});
}
@@ -239,8 +241,8 @@ export default function Page() {
setInviteLink(res.data.data.inviteLink);
toast({
variant: "default",
title: t('userInvited'),
description: t('userInvitedDescription')
title: t("userInvited"),
description: t("userInvitedDescription")
});
setExpiresInDays(parseInt(values.validForHours) / 24);
@@ -266,10 +268,10 @@ export default function Page() {
.catch((e) => {
toast({
variant: "destructive",
title: t('userErrorCreate'),
title: t("userErrorCreate"),
description: formatAxiosError(
e,
t('userErrorCreateDescription')
t("userErrorCreateDescription")
)
});
});
@@ -277,8 +279,8 @@ export default function Page() {
if (res && res.status === 201) {
toast({
variant: "default",
title: t('userCreated'),
description: t('userCreatedDescription')
title: t("userCreated"),
description: t("userCreatedDescription")
});
router.push(`/${orgId}/settings/access/users`);
}
@@ -289,13 +291,13 @@ export default function Page() {
const userTypes: ReadonlyArray<UserTypeOption> = [
{
id: "internal",
title: t('userTypeInternal'),
description: t('userTypeInternalDescription')
title: t("userTypeInternal"),
description: t("userTypeInternalDescription")
},
{
id: "oidc",
title: t('userTypeExternal'),
description: t('userTypeExternalDescription')
title: t("userTypeExternal"),
description: t("userTypeExternalDescription")
}
];
@@ -303,8 +305,8 @@ export default function Page() {
<>
<div className="flex justify-between">
<HeaderTitle
title={t('accessUserCreate')}
description={t('accessUserCreateDescription')}
title={t("accessUserCreate")}
description={t("accessUserCreateDescription")}
/>
<Button
variant="outline"
@@ -312,219 +314,239 @@ export default function Page() {
router.push(`/${orgId}/settings/access/users`);
}}
>
{t('userSeeAll')}
{t("userSeeAll")}
</Button>
</div>
<div>
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('userTypeTitle')}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('userTypeDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<StrategySelect
options={userTypes}
defaultValue={userType || undefined}
onChange={(value) => {
setUserType(value as UserType);
if (value === "internal") {
internalForm.reset();
} else if (value === "oidc") {
externalForm.reset();
setSelectedIdp(null);
}
}}
cols={2}
/>
</SettingsSectionBody>
</SettingsSection>
{!inviteLink && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("userTypeTitle")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("userTypeDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<StrategySelect
options={userTypes}
defaultValue={userType || undefined}
onChange={(value) => {
setUserType(value as UserType);
if (value === "internal") {
internalForm.reset();
} else if (value === "oidc") {
externalForm.reset();
setSelectedIdp(null);
}
}}
cols={2}
/>
</SettingsSectionBody>
</SettingsSection>
)}
{userType === "internal" && dataLoaded && (
<>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('userSettings')}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('userSettingsDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...internalForm}>
<form
onSubmit={internalForm.handleSubmit(
onSubmitInternal
)}
className="space-y-4"
id="create-user-form"
>
<FormField
control={
internalForm.control
}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('email')}
</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
{!inviteLink ? (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("userSettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("userSettingsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...internalForm}>
<form
onSubmit={internalForm.handleSubmit(
onSubmitInternal
)}
/>
{env.email.emailEnabled && (
<div className="flex items-center space-x-2">
<Checkbox
id="send-email"
checked={sendEmail}
onCheckedChange={(
e
) =>
setSendEmail(
e as boolean
)
}
/>
<label
htmlFor="send-email"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t('inviteEmailSent')}
</label>
</div>
)}
<FormField
control={
internalForm.control
}
name="validForHours"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('inviteValid')}
</FormLabel>
<Select
onValueChange={
field.onChange
}
defaultValue={
field.value
}
>
className="space-y-4"
id="create-user-form"
>
<FormField
control={
internalForm.control
}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("email")}
</FormLabel>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('selectDuration')} />
</SelectTrigger>
<Input
{...field}
/>
</FormControl>
<SelectContent>
{validFor.map(
(
option
) => (
<SelectItem
key={
option.hours
}
value={option.hours.toString()}
>
{
option.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={
internalForm.control
}
name="roleId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('role')}
</FormLabel>
<Select
onValueChange={
field.onChange
<FormField
control={
internalForm.control
}
name="validForHours"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"inviteValid"
)}
</FormLabel>
<Select
onValueChange={
field.onChange
}
defaultValue={
field.value
}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue
placeholder={t(
"selectDuration"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{validFor.map(
(
option
) => (
<SelectItem
key={
option.hours
}
value={option.hours.toString()}
>
{
option.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={
internalForm.control
}
name="roleId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("role")}
</FormLabel>
<Select
onValueChange={
field.onChange
}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue
placeholder={t(
"accessRoleSelect"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{roles.map(
(
role
) => (
<SelectItem
key={
role.roleId
}
value={role.roleId.toString()}
>
{
role.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{env.email.emailEnabled && (
<div className="flex items-center space-x-2">
<Checkbox
id="send-email"
checked={sendEmail}
onCheckedChange={(
e
) =>
setSendEmail(
e as boolean
)
}
/>
<label
htmlFor="send-email"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('accessRoleSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
{roles.map(
(
role
) => (
<SelectItem
key={
role.roleId
}
value={role.roleId.toString()}
>
{
role.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
{t(
"inviteEmailSent"
)}
</label>
</div>
)}
/>
{inviteLink && (
<div className="max-w-md space-y-4">
{sendEmail && (
<p>
{t('inviteEmailSentDescription')}
</p>
)}
{!sendEmail && (
<p>
{t('inviteSentDescription')}
</p>
)}
<p>
{t('inviteExpiresIn', {days: expiresInDays})}
</p>
<CopyTextBox
text={inviteLink}
wrapText={false}
/>
</div>
)}
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
) : (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("userInvited")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{sendEmail
? t("inviteEmailSentDescription")
: t("inviteSentDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<div className="space-y-4">
<p>
{t("inviteExpiresIn", {
days: expiresInDays
})}
</p>
<CopyTextBox
text={inviteLink}
wrapText={false}
/>
</div>
</SettingsSectionBody>
</SettingsSection>
)}
</>
)}
@@ -533,16 +555,16 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('idpTitle')}
{t("idpTitle")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('idpSelect')}
{t("idpSelect")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
{idps.length === 0 ? (
<p className="text-muted-foreground">
{t('idpNotConfigured')}
{t("idpNotConfigured")}
</p>
) : (
<Form {...externalForm}>
@@ -596,10 +618,10 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('userSettings')}
{t("userSettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('userSettingsDescription')}
{t("userSettingsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -620,7 +642,9 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('username')}
{t(
"username"
)}
</FormLabel>
<FormControl>
<Input
@@ -628,7 +652,9 @@ export default function Page() {
/>
</FormControl>
<p className="text-sm text-muted-foreground mt-1">
{t('usernameUniq')}
{t(
"usernameUniq"
)}
</p>
<FormMessage />
</FormItem>
@@ -643,7 +669,9 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('emailOptional')}
{t(
"emailOptional"
)}
</FormLabel>
<FormControl>
<Input
@@ -663,7 +691,9 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('nameOptional')}
{t(
"nameOptional"
)}
</FormLabel>
<FormControl>
<Input
@@ -683,7 +713,7 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('role')}
{t("role")}
</FormLabel>
<Select
onValueChange={
@@ -691,8 +721,12 @@ export default function Page() {
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('accessRoleSelect')} />
<SelectTrigger className="w-full">
<SelectValue
placeholder={t(
"accessRoleSelect"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
@@ -736,19 +770,17 @@ export default function Page() {
router.push(`/${orgId}/settings/access/users`);
}}
>
{t('cancel')}
{t("cancel")}
</Button>
{userType && dataLoaded && (
<Button
type="submit"
form="create-user-form"
type={inviteLink ? "button" : "submit"}
form={inviteLink ? undefined : "create-user-form"}
loading={loading}
disabled={
loading ||
(userType === "internal" && inviteLink !== null)
}
disabled={loading}
onClick={inviteLink ? () => router.push(`/${orgId}/settings/access/users`) : undefined}
>
{t('accessUserCreate')}
{inviteLink ? t("done") : t("accessUserCreate")}
</Button>
)}
</div>