improvement: move sso/provision settings back to org settings tabs

This commit is contained in:
Scott Wilson
2025-07-11 10:58:35 -07:00
parent e35ac599f8
commit 1aa9be203e
29 changed files with 14 additions and 149 deletions
@@ -0,0 +1,214 @@
import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
FormControl,
FormLabel,
IconButton,
Input,
Select,
SelectItem,
Spinner
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission
} from "@app/context";
import { isCustomOrgRole } from "@app/helpers/roles";
import { useGetOrgRoles } from "@app/hooks/api";
import {
useGetExternalGroupOrgRoleMappings,
useUpdateExternalGroupOrgRoleMappings
} from "@app/hooks/api/externalGroupOrgRoleMappings";
const formSchema = z.object({
mappings: z
.object({
groupName: z.string().trim().min(1, { message: "Group name is required" }),
roleSlug: z.string()
})
.array()
});
type TForm = z.infer<typeof formSchema>;
export const ExternalGroupOrgRoleMappings = () => {
const { currentOrg } = useOrganization();
const { data: roles, isPending: isRolesLoading } = useGetOrgRoles(currentOrg.id);
const { data: mappings } = useGetExternalGroupOrgRoleMappings();
const updateMappings = useUpdateExternalGroupOrgRoleMappings();
const { permission } = useOrgPermission();
const {
control,
formState: { isDirty },
handleSubmit,
reset
} = useForm<TForm>({
defaultValues: { mappings: [] },
resolver: zodResolver(formSchema)
});
useEffect(() => {
if (!mappings || !roles) return;
reset({
mappings: mappings.map((mapping) => ({
groupName: mapping.groupName,
roleSlug:
mapping.role === "custom"
? roles.find((role) => mapping.roleId === role.id)!.slug
: mapping.role
}))
});
}, [mappings, roles]);
const mappingField = useFieldArray({ control, name: "mappings" });
const handleUpdateMappings = async (form: TForm) => {
try {
await updateMappings.mutateAsync(form);
createNotification({
text: "Group organization role mappings updated.",
type: "success"
});
} catch (e) {
console.error(e);
createNotification({
text: "Failed to update group organization role mappings.",
type: "error"
});
}
};
const disableScimEdit = permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
return (
<div className="py-4">
<h2 className="text-md text-mineshaft-100">SCIM Group to Organization Role Mappings</h2>
<p className="text-sm text-mineshaft-300">
Assign newly provisioned users a default organization role based on their SCIM group.
</p>
<form onSubmit={handleSubmit(handleUpdateMappings)} className="pt-4">
{isRolesLoading || isRolesLoading ? (
<Spinner className="self-center" size="sm" />
) : (
<div className="mb-2 flex flex-col space-y-2">
{mappingField.fields.map(({ id: scopeFieldId }, i) => (
<div key={scopeFieldId} className="flex items-end space-x-2">
<div className="flex-grow">
{i === 0 && (
<FormLabel
label="SCIM Group Name"
className="text-xs text-mineshaft-400"
tooltipClassName="max-w-md whitespace-pre-line"
tooltipText="The name associated with this group in your SCIM provider"
/>
)}
<Controller
control={control}
name={`mappings.${i}.groupName`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input
isDisabled={disableScimEdit}
{...field}
placeholder="SCIM group identifier..."
/>
</FormControl>
)}
/>
</div>
<div className="flex-1">
{i === 0 && (
<span className="text-xs text-mineshaft-400">
Role to Assign Users in this Group
</span>
)}
<Controller
control={control}
name={`mappings.${i}.roleSlug`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
isDisabled={disableScimEdit}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{roles?.map((role) => (
<SelectItem value={role.slug} key={`role-${role.id}`}>
{role.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="delete key"
className="bottom-0.5 h-9"
variant="outline_bg"
isDisabled={disableScimEdit}
onClick={() => {
mappingField.remove(i);
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
isDisabled={disableScimEdit}
onClick={() =>
mappingField.append({
groupName: "",
roleSlug: isCustomOrgRole(currentOrg!.defaultMembershipRole)
? roles?.find((role) => currentOrg?.defaultMembershipRole === role.id)
?.slug || ""
: currentOrg!.defaultMembershipRole
})
}
>
Add Mapping
</Button>
</div>
{isDirty && (
<div className="flex w-full justify-end">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Scim}>
{(isAllowed) => (
<Button isDisabled={!isAllowed} colorSchema="secondary" type="submit">
Update Mappings
</Button>
)}
</OrgPermissionCan>
</div>
)}
</div>
)}
</form>
</div>
);
};
@@ -0,0 +1,172 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal, FormControl, Input } from "@app/components/v2";
import {
useCreateGithubSyncOrgConfig,
useDeleteGithubSyncOrgConfig,
useUpdateGithubSyncOrgConfig
} from "@app/hooks/api";
import { TGithubOrgSyncConfig } from "@app/hooks/api/githubOrgSyncConfig/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
githubOrgName: z.string(),
githubOrgAccessToken: z.string().optional()
});
export type FormData = z.infer<typeof schema>;
type Props = {
data?: TGithubOrgSyncConfig;
popUp: UsePopUpState<["githubOrgSyncConfig", "deleteGithubOrgSyncConfig"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteGithubOrgSyncConfig"]>,
data?: {
scimTokenId: string;
}
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["githubOrgSyncConfig", "deleteGithubOrgSyncConfig"]>,
state?: boolean
) => void;
};
export const GithubOrgSyncConfigModal = ({
popUp,
handlePopUpOpen,
handlePopUpToggle,
data
}: Props) => {
const isUpdate = Boolean(data);
const { mutateAsync: createGithubSyncOrgConfig } = useCreateGithubSyncOrgConfig();
const { mutateAsync: updateGithubSyncOrgConfig } = useUpdateGithubSyncOrgConfig();
const { mutateAsync: deleteGithubSyncOrgConfig } = useDeleteGithubSyncOrgConfig();
const {
control,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
values: data ? { githubOrgName: data.githubOrgName } : undefined
});
const onFormSubmit = async ({ githubOrgName, githubOrgAccessToken }: FormData) => {
try {
if (isUpdate) {
await updateGithubSyncOrgConfig({
githubOrgName,
githubOrgAccessToken
});
createNotification({
text: "Successfully updated GitHub Organization Sync",
type: "success"
});
} else {
await createGithubSyncOrgConfig({
githubOrgName,
githubOrgAccessToken,
isActive: false
});
createNotification({
text: "Successfully created GitHub Organization Sync",
type: "success"
});
}
handlePopUpToggle("githubOrgSyncConfig");
} catch {
createNotification({
text: "Failed to setup GitHub Organization Sync",
type: "error"
});
}
};
const onDelete = async () => {
try {
await deleteGithubSyncOrgConfig();
handlePopUpToggle("deleteGithubOrgSyncConfig", false);
handlePopUpToggle("githubOrgSyncConfig", false);
createNotification({
text: "Successfully deleted GitHub Organization Sync",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete GitHub Organization Sync",
type: "error"
});
}
};
return (
<>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="githubOrgName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="GitHub Organization Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="example" />
</FormControl>
)}
/>
{/* <Controller
control={control}
name="githubOrgAccessToken"
render={({ field, fieldState: { error } }) => (
<FormControl
label="GitHub Org Scoped Access Token"
isError={Boolean(error)}
isOptional
errorText={error?.message}
helperText="A GitHub access token is required only for private organizations. It will not be visible after saving."
>
<Input {...field} placeholder="example" />
</FormControl>
)}
/> */}
<div className="flex gap-8 pt-4">
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
{isUpdate ? "Update" : "Configure"}
</Button>
<Button
variant="plain"
colorSchema="secondary"
onClick={() => handlePopUpToggle("githubOrgSyncConfig", false)}
>
Cancel
</Button>
<div className="flex-grow" />
{isUpdate && (
<Button
onClick={() => handlePopUpOpen("deleteGithubOrgSyncConfig")}
colorSchema="danger"
>
Delete
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={popUp.deleteGithubOrgSyncConfig.isOpen}
title="Are you sure you want to remove GitHub organization sync?"
onChange={(isOpen) => handlePopUpToggle("deleteGithubOrgSyncConfig", isOpen)}
deleteKey="confirm"
onDeleteApproved={onDelete}
/>
</>
);
};
@@ -0,0 +1,114 @@
import { useQuery } from "@tanstack/react-query";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, Modal, ModalContent, Skeleton, Spinner, Switch } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { githubOrgSyncConfigQueryKeys, useUpdateGithubSyncOrgConfig } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { GithubOrgSyncConfigModal } from "./GithubOrgSyncConfigModal";
export const OrgGithubSyncSection = () => {
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upgradePlan",
"githubOrgSyncConfig",
"deleteGithubOrgSyncConfig"
] as const);
const githubOrgSyncConfig = useQuery({
...githubOrgSyncConfigQueryKeys.get(),
enabled: subscription.githubOrgSync,
retry: false
});
const updateGithubSyncOrgConfig = useUpdateGithubSyncOrgConfig();
const isPending = subscription.githubOrgSync && githubOrgSyncConfig.isPending;
const data = !isPending && !githubOrgSyncConfig?.isError ? githubOrgSyncConfig?.data : undefined;
return (
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<p className="text-xl font-semibold text-gray-200">
Sync user groups from your GitHub Organization
</p>
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">GitHub Organization</h2>
<div className="flex gap-4">
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.GithubOrgSync}>
{(isAllowed) => (
<Button
onClick={() =>
subscription.githubOrgSync
? handlePopUpOpen("githubOrgSyncConfig")
: handlePopUpOpen("upgradePlan")
}
colorSchema="secondary"
isDisabled={!isAllowed}
isLoading={isPending}
>
Configure
</Button>
)}
</OrgPermissionCan>
</div>
</div>
<p className="text-sm text-mineshaft-300">
{isPending ? <Skeleton /> : null}
{data ? data?.githubOrgName : "Not configured"}
</p>
</div>
{data && (
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">Enable GitHub Sync</h2>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.GithubOrgSync}>
{(isAllowed) => (
<Switch
id="enable-sync"
onCheckedChange={(value) =>
updateGithubSyncOrgConfig.mutate({
isActive: value
})
}
isChecked={githubOrgSyncConfig?.data?.isActive ?? false}
isDisabled={!isAllowed}
>
{updateGithubSyncOrgConfig?.isPending && <Spinner size="xs" />}
</Switch>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Allow group provisioning/deprovisioning with GitHub
</p>
</div>
)}
<Modal
isOpen={popUp?.githubOrgSyncConfig?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("githubOrgSyncConfig", isOpen);
}}
>
<ModalContent
title="Manage GitHub Organization Sync"
subTitle="Sync your GitHub teams to Infisical organization groups"
>
<GithubOrgSyncConfigModal
data={data}
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
</ModalContent>
</Modal>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use GitHub Organization Plan if you switch to Infisical's Enterprise plan."
/>
</div>
);
};
@@ -0,0 +1,17 @@
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { OrgGithubSyncSection } from "./OrgGithubSyncSection";
import { OrgScimSection } from "./OrgSCIMSection";
export const OrgProvisioningTab = withPermission(
() => {
return (
<>
<OrgScimSection />
<OrgGithubSyncSection />
</>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Sso }
);
@@ -0,0 +1,118 @@
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, Switch } from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { useUpdateOrg } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { ExternalGroupOrgRoleMappings } from "./ExternalGroupOrgRoleMappings";
import { ScimTokenModal } from "./ScimTokenModal";
export const OrgScimSection = () => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"scimToken",
"deleteScimToken",
"upgradePlan"
] as const);
const { mutateAsync } = useUpdateOrg();
const addScimTokenBtnClick = () => {
if (subscription?.scim) {
handlePopUpOpen("scimToken");
} else {
handlePopUpOpen("upgradePlan");
}
};
const handleEnableSCIMToggle = async (value: boolean) => {
try {
if (!currentOrg?.id) return;
if (!subscription?.scim) {
handlePopUpOpen("upgradePlan");
return;
}
await mutateAsync({
orgId: currentOrg?.id,
scimEnabled: value
});
createNotification({
text: `Successfully ${value ? "enabled" : "disabled"} SCIM provisioning`,
type: "success"
});
} catch (err) {
createNotification({
text: (err as { response: { data: { message: string } } }).response.data.message,
type: "error"
});
}
};
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<p className="text-xl font-semibold text-gray-200">Provision users via SCIM</p>
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">SCIM</h2>
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.Scim}>
{(isAllowed) => (
<Button
onClick={addScimTokenBtnClick}
colorSchema="secondary"
isDisabled={!isAllowed}
>
Configure
</Button>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">Manage SCIM configuration</p>
</div>
<ExternalGroupOrgRoleMappings />
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">Enable SCIM</h2>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Scim}>
{(isAllowed) => (
<Switch
id="enable-scim"
onCheckedChange={(value) => {
if (subscription?.scim) {
handleEnableSCIMToggle(value);
} else {
handlePopUpOpen("upgradePlan");
}
}}
isChecked={currentOrg?.scimEnabled ?? false}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Allow member provisioning/deprovisioning with SCIM
</p>
</div>
<ScimTokenModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use SCIM Provisioning if you switch to Infisical's Enterprise plan."
/>
</div>
);
};
@@ -0,0 +1,318 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { faCheck, faCopy, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
DeleteActionModal,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useToggle } from "@app/hooks";
import { useCreateScimToken, useDeleteScimToken, useGetScimTokens } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
description: z.string().optional(),
ttlDays: z.string()
});
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["scimToken", "deleteScimToken"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteScimToken"]>,
data?: {
scimTokenId: string;
}
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["scimToken", "deleteScimToken"]>,
state?: boolean
) => void;
};
export const ScimTokenModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const { t } = useTranslation();
const [token, setToken] = useState("");
const [isScimUrlCopied, setIsScimUrlCopied] = useToggle(false);
const [isScimTokenCopied, setIsScimTokenCopied] = useToggle(false);
const { data, isPending } = useGetScimTokens(currentOrg?.id ?? "");
const { mutateAsync: createScimTokenMutateAsync } = useCreateScimToken();
const { mutateAsync: deleteScimTokenMutateAsync } = useDeleteScimToken();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
description: "",
ttlDays: "365"
}
});
useEffect(() => {
let timer: NodeJS.Timeout;
if (isScimUrlCopied) {
timer = setTimeout(() => setIsScimUrlCopied.off(), 2000);
}
if (isScimTokenCopied) {
timer = setTimeout(() => setIsScimTokenCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isScimTokenCopied, isScimUrlCopied]);
const onFormSubmit = async ({ description, ttlDays }: FormData) => {
try {
if (!currentOrg?.id) return;
const { scimToken } = await createScimTokenMutateAsync({
organizationId: currentOrg.id,
description,
ttlDays: Number(ttlDays)
});
setToken(scimToken);
createNotification({
text: "Successfully created SCIM token",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create SCIM token",
type: "error"
});
}
};
const onDeleteScimTokenSubmit = async (scimTokenId: string) => {
try {
if (!currentOrg?.id) return;
await deleteScimTokenMutateAsync({
organizationId: currentOrg.id,
scimTokenId
});
handlePopUpToggle("deleteScimToken", false);
createNotification({
text: "Successfully deleted SCIM token",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete SCIM token",
type: "error"
});
}
};
const hasToken = Boolean(token);
const scimUrl = `${window.origin}/api/v1/scim`;
return (
<Modal
isOpen={popUp?.scimToken?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("scimToken", isOpen);
reset();
setToken("");
}}
>
<ModalContent title="Manage SCIM credentials">
<h2 className="mb-4">SCIM URL</h2>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{scimUrl}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(scimUrl);
setIsScimUrlCopied.on();
}}
>
<FontAwesomeIcon icon={isScimUrlCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{t("common.click-to-copy")}
</span>
</IconButton>
</div>
<h2 className="mb-4">New SCIM Token</h2>
{hasToken ? (
<div>
<div className="mb-4 flex items-center justify-between">
<p>We will only show this token once</p>
<Button
colorSchema="secondary"
type="submit"
onClick={() => {
reset();
setToken("");
}}
>
Got it
</Button>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{token}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(token);
setIsScimTokenCopied.on();
}}
>
<FontAwesomeIcon icon={isScimTokenCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{t("common.click-to-copy")}
</span>
</IconButton>
</div>
</div>
) : (
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-8">
<Controller
control={control}
defaultValue=""
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Description (optional)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Description" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="365"
name="ttlDays"
render={({ field, fieldState: { error } }) => (
<FormControl label="TTL (days)" isError={Boolean(error)} errorText={error?.message}>
<div className="flex">
<Input {...field} placeholder="0" type="number" min="0" step="1" />
<Button
className="ml-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>
</div>
</FormControl>
)}
/>
</form>
)}
<h2 className="mb-4">SCIM Tokens</h2>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Description</Th>
<Th>Expires At</Th>
<Th>Created At</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="org-scim-tokens" />}
{!isPending &&
data &&
data.length > 0 &&
data.map(({ id, description, ttlDays, createdAt }) => {
let expiresAt;
if (ttlDays > 0) {
expiresAt = new Date(new Date(createdAt).getTime() + ttlDays * 86400 * 1000);
}
return (
<Tr className="h-10 items-center" key={`mi-client-secret-${id}`}>
<Td>{description === "" ? "-" : description}</Td>
<Td>{expiresAt ? format(expiresAt, "yyyy-MM-dd HH:mm:ss") : "-"}</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd HH:mm:ss")}</Td>
<Td>
<IconButton
onClick={() => {
handlePopUpOpen("deleteScimToken", {
scimTokenId: id
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
})}
{!isPending && data && data?.length === 0 && (
<Tr>
<Td colSpan={4}>
<EmptyState title="No SCIM tokens have been created yet" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
<DeleteActionModal
isOpen={popUp.deleteScimToken.isOpen}
title="Are you sure you want to delete the SCIM token?"
onChange={(isOpen) => handlePopUpToggle("scimToken", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const deleteScimTokenData = popUp?.deleteScimToken?.data as {
scimTokenId: string;
};
return onDeleteScimTokenSubmit(deleteScimTokenData.scimTokenId);
}}
/>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1 @@
export * from "./OrgProvisioningTab";