mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 19:16:22 +00:00
feat: implemented ui and api for managing user,identity metadata
This commit is contained in:
@@ -67,12 +67,13 @@ export const useCreateIdentity = () => {
|
||||
export const useUpdateIdentity = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<Identity, {}, UpdateIdentityDTO>({
|
||||
mutationFn: async ({ identityId, name, role }) => {
|
||||
mutationFn: async ({ identityId, name, role, metadata }) => {
|
||||
const {
|
||||
data: { identity }
|
||||
} = await apiRequest.patch(`/api/v1/identities/${identityId}`, {
|
||||
name,
|
||||
role
|
||||
role,
|
||||
metadata
|
||||
});
|
||||
|
||||
return identity;
|
||||
|
||||
@@ -37,6 +37,7 @@ export type IdentityMembershipOrg = {
|
||||
id: string;
|
||||
identity: Identity;
|
||||
organization: string;
|
||||
metadata: { key: string; value: string; id: string }[];
|
||||
role: "admin" | "member" | "viewer" | "no-access" | "custom";
|
||||
customRole?: TOrgRole;
|
||||
createdAt: string;
|
||||
@@ -79,6 +80,7 @@ export type CreateIdentityDTO = {
|
||||
name: string;
|
||||
organizationId: string;
|
||||
role?: string;
|
||||
metadata?: { key: string; value: string }[];
|
||||
};
|
||||
|
||||
export type UpdateIdentityDTO = {
|
||||
@@ -86,6 +88,7 @@ export type UpdateIdentityDTO = {
|
||||
name?: string;
|
||||
role?: string;
|
||||
organizationId: string;
|
||||
metadata?: { key: string; value: string }[];
|
||||
};
|
||||
|
||||
export type DeleteIdentityDTO = {
|
||||
|
||||
@@ -235,12 +235,13 @@ export const useUpdateOrgMembership = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, UpdateOrgMembershipDTO>({
|
||||
mutationFn: ({ organizationId, membershipId, role, isActive }) => {
|
||||
mutationFn: ({ organizationId, membershipId, role, isActive, metadata }) => {
|
||||
return apiRequest.patch(
|
||||
`/api/v2/organizations/${organizationId}/memberships/${membershipId}`,
|
||||
{
|
||||
role,
|
||||
isActive
|
||||
isActive,
|
||||
metadata
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -46,6 +46,7 @@ export type UserEnc = {
|
||||
|
||||
export type OrgUser = {
|
||||
id: string;
|
||||
metadata: { key: string; value: string; id: string }[];
|
||||
user: {
|
||||
username: string;
|
||||
email?: string;
|
||||
@@ -142,6 +143,7 @@ export type UpdateOrgMembershipDTO = {
|
||||
membershipId: string;
|
||||
role?: string;
|
||||
isActive?: boolean;
|
||||
metadata?: { key: string; value: string }[];
|
||||
};
|
||||
|
||||
export type DeletOrgMembershipDTO = {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck, faCopy, faKey, faPencil } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { IconButton, Tag, Tooltip } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { useGetIdentityById } from "@app/hooks/api";
|
||||
@@ -40,7 +40,8 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
||||
identityId,
|
||||
name: data.identity.name,
|
||||
role: data.role,
|
||||
customRole: data.customRole
|
||||
customRole: data.customRole,
|
||||
metadata: data.metadata
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -77,10 +78,36 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
|
||||
<p className="text-sm text-mineshaft-300">{data.identity.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Organization Role</p>
|
||||
<p className="text-sm text-mineshaft-300">{data.role}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Metadata</p>
|
||||
{data?.metadata?.length ? (
|
||||
<div className="mt-1 flex gap-2 text-sm text-mineshaft-300">
|
||||
{data.metadata?.map((el) => (
|
||||
<div key={el.id} className="flex items-center">
|
||||
<Tag
|
||||
size="xs"
|
||||
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
|
||||
>
|
||||
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
|
||||
<div>{el.key}</div>
|
||||
</Tag>
|
||||
<Tag
|
||||
size="xs"
|
||||
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
|
||||
>
|
||||
<div>{el.value}</div>
|
||||
</Tag>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-mineshaft-300">-</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
+90
-11
@@ -1,13 +1,17 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/router";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
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 {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
@@ -22,14 +26,21 @@ import {
|
||||
} from "@app/hooks/api/identities";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = yup
|
||||
const schema = z
|
||||
.object({
|
||||
name: yup.string().required("MI name is required"),
|
||||
role: yup.string()
|
||||
name: z.string(),
|
||||
role: z.string(),
|
||||
metadata: z
|
||||
.object({
|
||||
key: z.string().trim().min(1),
|
||||
value: z.string().trim().min(1)
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["identity"]>;
|
||||
@@ -61,12 +72,17 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema),
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
name: ""
|
||||
}
|
||||
});
|
||||
|
||||
const metadataFormFields = useFieldArray({
|
||||
control,
|
||||
name: "metadata"
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const identity = popUp?.identity?.data as {
|
||||
identityId: string;
|
||||
@@ -93,7 +109,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}
|
||||
}, [popUp?.identity?.data, roles]);
|
||||
|
||||
const onFormSubmit = async ({ name, role }: FormData) => {
|
||||
const onFormSubmit = async ({ name, role, metadata }: FormData) => {
|
||||
try {
|
||||
const identity = popUp?.identity?.data as {
|
||||
identityId: string;
|
||||
@@ -108,7 +124,8 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
identityId: identity.identityId,
|
||||
name,
|
||||
role: role || undefined,
|
||||
organizationId: orgId
|
||||
organizationId: orgId,
|
||||
metadata
|
||||
});
|
||||
|
||||
handlePopUpToggle("identity", false);
|
||||
@@ -118,7 +135,8 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { id: createdId } = await createMutateAsync({
|
||||
name,
|
||||
role: role || undefined,
|
||||
organizationId: orgId
|
||||
organizationId: orgId,
|
||||
metadata
|
||||
});
|
||||
|
||||
await addMutateAsync({
|
||||
@@ -207,6 +225,67 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<FormLabel label="Metadata" />
|
||||
</div>
|
||||
<div className="mb-3 flex flex-col space-y-2">
|
||||
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
|
||||
<div key={metadataFieldId} className="flex items-end space-x-2">
|
||||
<div className="flex-grow">
|
||||
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`metadata.${i}.key`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
{i === 0 && (
|
||||
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`metadata.${i}.value`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
ariaLabel="delete key"
|
||||
className="bottom-0.5 h-9"
|
||||
variant="outline_bg"
|
||||
onClick={() => metadataFormFields.remove(i)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-2 flex justify-end">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => metadataFormFields.append({ key: "", value: "" })}
|
||||
>
|
||||
Add Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
||||
@@ -3,13 +3,14 @@ import {
|
||||
faCheckCircle,
|
||||
faCircleXmark,
|
||||
faCopy,
|
||||
faKey,
|
||||
faPencil
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, IconButton, Tooltip } from "@app/components/v2";
|
||||
import { Button, IconButton, Tag, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -98,7 +99,8 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
||||
onClick={() => {
|
||||
handlePopUpOpen("orgMembership", {
|
||||
membershipId: membership.id,
|
||||
role: membership.role
|
||||
role: membership.role,
|
||||
metadata: membership.metadata
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -162,10 +164,36 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Organization Role</p>
|
||||
<p className="text-sm text-mineshaft-300">{roleName ?? "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
|
||||
<p className="text-sm text-mineshaft-300">{getStatus(membership)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Metadata</p>
|
||||
{membership?.metadata?.length ? (
|
||||
<div className="mt-1 flex gap-2 text-sm text-mineshaft-300">
|
||||
{membership.metadata?.map((el) => (
|
||||
<div key={el.id} className="flex items-center">
|
||||
<Tag
|
||||
size="xs"
|
||||
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
|
||||
>
|
||||
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
|
||||
<div>{el.key}</div>
|
||||
</Tag>
|
||||
<Tag
|
||||
size="xs"
|
||||
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
|
||||
>
|
||||
<div>{el.value}</div>
|
||||
</Tag>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-mineshaft-300">-</p>
|
||||
)}
|
||||
</div>
|
||||
{membership.isActive &&
|
||||
(membership.status === "invited" || membership.status === "verified") &&
|
||||
membership.user.email &&
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
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 { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import { useGetOrgRoles, useUpdateOrgMembership } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z.object({
|
||||
role: z.string()
|
||||
role: z.string(),
|
||||
metadata: z
|
||||
.object({
|
||||
key: z.string().trim().min(1),
|
||||
value: z.string().trim().min(1)
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
@@ -39,9 +58,15 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const metadataFormFields = useFieldArray({
|
||||
control,
|
||||
name: "metadata"
|
||||
});
|
||||
|
||||
const popUpData = popUp?.orgMembership?.data as {
|
||||
membershipId: string;
|
||||
role: string;
|
||||
metadata: { key: string; value: string }[];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -49,7 +74,8 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
||||
|
||||
if (popUpData) {
|
||||
reset({
|
||||
role: popUpData.role
|
||||
role: popUpData.role,
|
||||
metadata: popUpData.metadata
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
@@ -58,14 +84,15 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
||||
}
|
||||
}, [popUp?.orgMembership?.data, roles]);
|
||||
|
||||
const onFormSubmit = async ({ role }: FormData) => {
|
||||
const onFormSubmit = async ({ role, metadata }: FormData) => {
|
||||
try {
|
||||
if (!orgId) return;
|
||||
|
||||
await updateOrgMembership({
|
||||
organizationId: orgId,
|
||||
membershipId: popUpData.membershipId,
|
||||
role
|
||||
role,
|
||||
metadata
|
||||
});
|
||||
|
||||
handlePopUpToggle("orgMembership", false);
|
||||
@@ -135,6 +162,67 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<FormLabel label="Metadata" />
|
||||
</div>
|
||||
<div className="mb-3 flex flex-col space-y-2">
|
||||
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
|
||||
<div key={metadataFieldId} className="flex items-end space-x-2">
|
||||
<div className="flex-grow">
|
||||
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`metadata.${i}.key`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
{i === 0 && (
|
||||
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`metadata.${i}.value`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
ariaLabel="delete key"
|
||||
className="bottom-0.5 h-9"
|
||||
variant="outline_bg"
|
||||
onClick={() => metadataFormFields.remove(i)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-2 flex justify-end">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => metadataFormFields.append({ key: "", value: "" })}
|
||||
>
|
||||
Add Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
||||
Reference in New Issue
Block a user