♻️ replace roles & user selectors in machines & create user

This commit is contained in:
Fred KISSIE
2026-04-28 05:08:20 +02:00
parent 27b2ec309d
commit ddaa9c32a7
11 changed files with 211 additions and 337 deletions

View File

@@ -1,44 +1,40 @@
"use client";
import IdpTypeBadge from "@app/components/IdpTypeBadge";
import OrgRolesTagField from "@app/components/OrgRolesTagField";
import {
SettingsContainer,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionFooter,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
import { Button } from "@app/components/ui/button";
import { Checkbox } from "@app/components/ui/checkbox";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
FormLabel
} from "@app/components/ui/form";
import { Checkbox } from "@app/components/ui/checkbox";
import OrgRolesTagField from "@app/components/OrgRolesTagField";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AxiosResponse } from "axios";
import { useEffect, useState } from "react";
import { build } from "@server/build";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { UserType } from "@server/types/UserTypes";
import { useTranslations } from "next-intl";
import { useParams } from "next/navigation";
import { useActionState, useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { ListRolesResponse } from "@server/routers/role";
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
import { useParams } from "next/navigation";
import { Button } from "@app/components/ui/button";
import {
SettingsContainer,
SettingsSection,
SettingsSectionHeader,
SettingsSectionTitle,
SettingsSectionDescription,
SettingsSectionBody,
SettingsSectionForm,
SettingsSectionFooter
} from "@app/components/Settings";
import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
import IdpTypeBadge from "@app/components/IdpTypeBadge";
import { UserType } from "@server/types/UserTypes";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { build } from "@server/build";
const accessControlsFormSchema = z.object({
username: z.string(),
@@ -59,12 +55,6 @@ export default function AccessControlsPage() {
const { orgId } = useParams();
const [loading, setLoading] = useState(false);
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
const [activeRoleTagIndex, setActiveRoleTagIndex] = useState<number | null>(
null
);
const t = useTranslations();
const { isPaidUser } = usePaidStatus();
const isPaid = isPaidUser(tierMatrix.fullRbac);
@@ -97,44 +87,21 @@ export default function AccessControlsPage() {
text: r.name
}))
);
}, [user.userId, currentRoleIds.join(",")]);
useEffect(() => {
async function fetchRoles() {
const res = await api
.get<AxiosResponse<ListRolesResponse>>(`/org/${orgId}/roles`)
.catch((e) => {
console.error(e);
toast({
variant: "destructive",
title: t("accessRoleErrorFetch"),
description: formatAxiosError(
e,
t("accessRoleErrorFetchDescription")
)
});
});
if (res?.status === 200) {
setRoles(res.data.data.roles);
}
}
fetchRoles();
form.setValue("autoProvisioned", user.autoProvisioned || false);
}, []);
const allRoleOptions = roles.map((role) => ({
id: role.roleId.toString(),
text: role.name
}));
}, [user.userId, user.autoProvisioned, currentRoleIds.join(",")]);
const paywallMessage =
build === "saas"
? t("singleRolePerUserPlanNotice")
: t("singleRolePerUserEditionNotice");
async function onSubmit(values: z.infer<typeof accessControlsFormSchema>) {
const [, action, isSubmitting] = useActionState(onSubmit, null);
async function onSubmit() {
const isValid = await form.trigger();
if (!isValid) return;
const values = form.getValues();
if (values.roles.length === 0) {
toast({
variant: "destructive",
@@ -144,7 +111,6 @@ export default function AccessControlsPage() {
return;
}
setLoading(true);
try {
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
const updateRoleRequest = supportsMultipleRolesPerUser
@@ -184,7 +150,6 @@ export default function AccessControlsPage() {
)
});
}
setLoading(false);
}
return (
@@ -203,7 +168,7 @@ export default function AccessControlsPage() {
<SettingsSectionForm>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
action={action}
className="space-y-4"
id="access-controls-form"
>
@@ -226,9 +191,7 @@ export default function AccessControlsPage() {
<OrgRolesTagField
form={form}
name="roles"
label={t("roles")}
placeholder={t("accessRoleSelect2")}
allRoleOptions={allRoleOptions}
orgId={orgId as string}
supportsMultipleRolesPerUser={
supportsMultipleRolesPerUser
}
@@ -236,9 +199,6 @@ export default function AccessControlsPage() {
showMultiRolePaywallMessage
}
paywallMessage={paywallMessage}
loading={loading}
activeTagIndex={activeRoleTagIndex}
setActiveTagIndex={setActiveRoleTagIndex}
/>
{user.idpAutoProvision && (
@@ -277,8 +237,8 @@ export default function AccessControlsPage() {
<SettingsSectionFooter>
<Button
type="submit"
loading={loading}
disabled={loading}
loading={isSubmitting}
disabled={isSubmitting}
form="access-controls-form"
>
{t("accessControlsSubmit")}

View File

@@ -13,7 +13,7 @@ import { StrategyOption, StrategySelect } from "@app/components/StrategySelect";
import HeaderTitle from "@app/components/SettingsSectionTitle";
import { Button } from "@app/components/ui/button";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useActionState, useState } from "react";
import {
Form,
FormControl,
@@ -91,7 +91,7 @@ export default function Page() {
"internal"
);
const [inviteLink, setInviteLink] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [expiresInDays, setExpiresInDays] = useState(1);
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
const [idps, setIdps] = useState<IdpOption[]>([]);
@@ -311,10 +311,29 @@ export default function Page() {
setUserOptions(options);
}, [idps, t]);
async function onSubmitInternal(
values: z.infer<typeof internalFormSchema>
) {
setLoading(true);
const [, submitInternalAction, isSubmittingInternal] = useActionState(
onSubmitInternal,
null
);
const [, submitGoogleAzureAction, isSubmittingGoogleAzure] = useActionState(
onSubmitGoogleAzure,
null
);
const [, submitGenericOidcAction, isSubmittingGenericOidc] = useActionState(
onSubmitGenericOidc,
null
);
const loading =
isSubmittingInternal ||
isSubmittingGoogleAzure ||
isSubmittingGenericOidc;
async function onSubmitInternal() {
const isValid = await internalForm.trigger();
if (!isValid) return;
const values = internalForm.getValues();
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
@@ -357,25 +376,24 @@ export default function Page() {
setExpiresInDays(parseInt(values.validForHours) / 24);
}
setLoading(false);
}
async function onSubmitGoogleAzure(
values: z.infer<typeof googleAzureFormSchema>
) {
async function onSubmitGoogleAzure() {
const isValid = await googleAzureForm.trigger();
if (!isValid) return;
const values = googleAzureForm.getValues();
const selectedUserOption = userOptions.find(
(opt) => opt.id === selectedOption
);
if (!selectedUserOption?.idpId) return;
setLoading(true);
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
const res = await api
.put(`/org/${orgId}/user`, {
username: values.email, // Use email as username for Google/Azure
username: values.email,
email: values.email || undefined,
name: values.name,
type: "oidc",
@@ -401,20 +419,19 @@ export default function Page() {
});
router.push(`/${orgId}/settings/access/users`);
}
setLoading(false);
}
async function onSubmitGenericOidc(
values: z.infer<typeof genericOidcFormSchema>
) {
async function onSubmitGenericOidc() {
const isValid = await genericOidcForm.trigger();
if (!isValid) return;
const values = genericOidcForm.getValues();
const selectedUserOption = userOptions.find(
(opt) => opt.id === selectedOption
);
if (!selectedUserOption?.idpId) return;
setLoading(true);
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
const res = await api
@@ -445,8 +462,6 @@ export default function Page() {
});
router.push(`/${orgId}/settings/access/users`);
}
setLoading(false);
}
return (
@@ -513,9 +528,9 @@ export default function Page() {
<SettingsSectionForm>
<Form {...internalForm}>
<form
onSubmit={internalForm.handleSubmit(
onSubmitInternal
)}
action={
submitInternalAction
}
className="space-y-4"
id="create-user-form"
>
@@ -595,13 +610,7 @@ export default function Page() {
<OrgRolesTagField
form={internalForm}
name="roles"
label={t("roles")}
placeholder={t(
"accessRoleSelect2"
)}
allRoleOptions={
allRoleOptions
}
orgId={orgId as string}
supportsMultipleRolesPerUser={
supportsMultipleRolesPerUser
}
@@ -611,13 +620,6 @@ export default function Page() {
paywallMessage={
invitePaywallMessage
}
loading={loading}
activeTagIndex={
activeInviteRoleTagIndex
}
setActiveTagIndex={
setActiveInviteRoleTagIndex
}
/>
{env.email.emailEnabled && (
@@ -712,9 +714,9 @@ export default function Page() {
})() && (
<Form {...googleAzureForm}>
<form
onSubmit={googleAzureForm.handleSubmit(
onSubmitGoogleAzure
)}
action={
submitGoogleAzureAction
}
className="space-y-4"
id="create-user-form"
>
@@ -763,13 +765,7 @@ export default function Page() {
<OrgRolesTagField
form={googleAzureForm}
name="roles"
label={t("roles")}
placeholder={t(
"accessRoleSelect2"
)}
allRoleOptions={
allRoleOptions
}
orgId={orgId as string}
supportsMultipleRolesPerUser={
supportsMultipleRolesPerUser
}
@@ -779,13 +775,6 @@ export default function Page() {
paywallMessage={
invitePaywallMessage
}
loading={loading}
activeTagIndex={
activeOidcRoleTagIndex
}
setActiveTagIndex={
setActiveOidcRoleTagIndex
}
/>
</form>
</Form>
@@ -808,9 +797,9 @@ export default function Page() {
})() && (
<Form {...genericOidcForm}>
<form
onSubmit={genericOidcForm.handleSubmit(
onSubmitGenericOidc
)}
action={
submitGenericOidcAction
}
className="space-y-4"
id="create-user-form"
>
@@ -888,13 +877,7 @@ export default function Page() {
<OrgRolesTagField
form={genericOidcForm}
name="roles"
label={t("roles")}
placeholder={t(
"accessRoleSelect2"
)}
allRoleOptions={
allRoleOptions
}
orgId={orgId as string}
supportsMultipleRolesPerUser={
supportsMultipleRolesPerUser
}
@@ -904,13 +887,6 @@ export default function Page() {
paywallMessage={
invitePaywallMessage
}
loading={loading}
activeTagIndex={
activeOidcRoleTagIndex
}
setActiveTagIndex={
setActiveOidcRoleTagIndex
}
/>
</form>
</Form>