mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 08:34:52 +00:00
Merge pull request #2897 from Infisical/feat/resource-metadata
feat: resource metadata
This commit is contained in:
+2
-1
@@ -32,7 +32,8 @@ const metadataMappings: Record<keyof NonNullable<TIntegrationWithEnv["metadata"]
|
||||
shouldEnableDelete: "GitHub Secret Deletion Enabled",
|
||||
octopusDeployScopeValues: "Octopus Deploy Scope Values",
|
||||
awsIamRole: "AWS IAM Role",
|
||||
region: "Region"
|
||||
region: "Region",
|
||||
metadataSyncMode: "Metadata Sync Mode"
|
||||
} as const;
|
||||
|
||||
export const IntegrationSettingsSection = ({ integration }: Props) => {
|
||||
|
||||
+92
-7
@@ -1,4 +1,4 @@
|
||||
import { faExclamationTriangle, faInfo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faExclamationTriangle, faInfo, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
@@ -18,7 +18,10 @@ import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/h
|
||||
export type Props = {
|
||||
op: CommitType;
|
||||
secretVersion?: SecretV3Raw;
|
||||
newVersion?: Omit<TSecretApprovalSecChange, "tags"> & { tags?: WsTag[] };
|
||||
newVersion?: Omit<TSecretApprovalSecChange, "tags"> & {
|
||||
tags?: WsTag[];
|
||||
secretMetadata?: { key: string; value: string }[];
|
||||
};
|
||||
presentSecretVersionNumber: number;
|
||||
hasMerged?: boolean;
|
||||
conflicts: Array<{ secretId: string; op: CommitType }>;
|
||||
@@ -80,11 +83,12 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
{op === CommitType.UPDATE && <Th className="w-12" />}
|
||||
<Th className="min-table-row">Secret</Th>
|
||||
<Th>Value</Th>
|
||||
<Th className="min-table-row">Comment</Th>
|
||||
<Th className="min-table-row">Tags</Th>
|
||||
{op === CommitType.UPDATE && <Th className="w-12 shrink-0" />}
|
||||
<Th className="w-48 shrink-0">Secret</Th>
|
||||
<Th className="min-w-0 flex-1">Value</Th>
|
||||
<Th className="w-24 shrink-0">Comment</Th>
|
||||
<Th className="w-24 shrink-0">Tags</Th>
|
||||
<Th className="w-40 shrink-0">Metadata</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
{op === CommitType.UPDATE ? (
|
||||
@@ -110,6 +114,33 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
</Tag>
|
||||
))}
|
||||
</Td>
|
||||
<Td>
|
||||
{secretVersion?.secretMetadata?.length ? (
|
||||
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
|
||||
{secretVersion.secretMetadata?.map((el) => (
|
||||
<div key={el.key} 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 className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{el.value}
|
||||
</div>
|
||||
</Tag>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-mineshaft-300">-</p>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td className="text-green-600">NEW</Td>
|
||||
@@ -132,6 +163,33 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
</Tag>
|
||||
))}
|
||||
</Td>
|
||||
<Td>
|
||||
{newVersion?.secretMetadata?.length ? (
|
||||
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
|
||||
{newVersion.secretMetadata?.map((el) => (
|
||||
<div key={el.key} 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 className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{el.value}
|
||||
</div>
|
||||
</Tag>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-mineshaft-300">-</p>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
</TBody>
|
||||
) : (
|
||||
@@ -173,6 +231,33 @@ export const SecretApprovalRequestChangeItem = ({
|
||||
)
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{newVersion?.secretMetadata?.length ? (
|
||||
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
|
||||
{newVersion.secretMetadata?.map((el) => (
|
||||
<div key={el.key} 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 className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{el.value}
|
||||
</div>
|
||||
</Tag>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-mineshaft-300">-</p>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
</TBody>
|
||||
)}
|
||||
|
||||
+76
-5
@@ -8,7 +8,8 @@ import {
|
||||
faClock,
|
||||
faPlus,
|
||||
faShare,
|
||||
faTag
|
||||
faTag,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Switch,
|
||||
@@ -87,11 +89,16 @@ export const SecretDetailSidebar = ({
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
const tagFields = useFieldArray({
|
||||
control,
|
||||
name: "tags"
|
||||
});
|
||||
|
||||
const metadataFormFields = useFieldArray({
|
||||
control,
|
||||
name: "secretMetadata"
|
||||
});
|
||||
|
||||
const secretKey = secret?.key || "";
|
||||
const selectedTags = watch("tags", []) || [];
|
||||
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
|
||||
@@ -153,10 +160,10 @@ export const SecretDetailSidebar = ({
|
||||
if (selectedTagsGroupById?.[tag.id]) {
|
||||
const tagPos = selectedTags.findIndex(({ id }) => id === tag.id);
|
||||
if (tagPos !== -1) {
|
||||
remove(tagPos);
|
||||
tagFields.remove(tagPos);
|
||||
}
|
||||
} else {
|
||||
append(tag);
|
||||
tagFields.append(tag);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -277,9 +284,73 @@ export const SecretDetailSidebar = ({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormControl label="Metadata">
|
||||
<div className="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={`secretMetadata.${i}.key`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input {...field} className="max-h-8" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
{i === 0 && (
|
||||
<FormLabel
|
||||
label="Value"
|
||||
className="text-xs text-mineshaft-400"
|
||||
isOptional
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`secretMetadata.${i}.value`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input {...field} className="max-h-8" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
ariaLabel="delete key"
|
||||
className="bottom-0.5 max-h-8"
|
||||
variant="outline_bg"
|
||||
onClick={() => metadataFormFields.remove(i)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => metadataFormFields.append({ key: "", value: "" })}
|
||||
>
|
||||
Add Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormControl label="Tags" className="">
|
||||
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
|
||||
{fields.map(({ tagColor, id: formId, slug, id }) => (
|
||||
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
key={formId}
|
||||
|
||||
+15
-6
@@ -78,7 +78,8 @@ export const SecretListView = ({
|
||||
tags,
|
||||
skipMultilineEncoding,
|
||||
newKey,
|
||||
secretId
|
||||
secretId,
|
||||
secretMetadata
|
||||
}: Partial<{
|
||||
value: string;
|
||||
comment: string;
|
||||
@@ -88,6 +89,7 @@ export const SecretListView = ({
|
||||
skipMultilineEncoding: boolean;
|
||||
newKey: string;
|
||||
secretId: string;
|
||||
secretMetadata?: { key: string; value: string }[];
|
||||
}> = {}
|
||||
) => {
|
||||
if (operation === "delete") {
|
||||
@@ -115,7 +117,8 @@ export const SecretListView = ({
|
||||
secretReminderRepeatDays: reminderRepeatDays,
|
||||
secretReminderNote: reminderNote,
|
||||
skipMultilineEncoding,
|
||||
newSecretName: newKey
|
||||
newSecretName: newKey,
|
||||
secretMetadata
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -138,7 +141,10 @@ export const SecretListView = ({
|
||||
const handleSaveSecret = useCallback(
|
||||
async (
|
||||
orgSecret: SecretV3RawSanitized,
|
||||
modSecret: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] },
|
||||
modSecret: Omit<SecretV3RawSanitized, "tags"> & {
|
||||
tags?: { id: string }[];
|
||||
secretMetadata?: { key: string; value: string }[];
|
||||
},
|
||||
cb?: () => void
|
||||
) => {
|
||||
const { key: oldKey } = orgSecret;
|
||||
@@ -151,7 +157,8 @@ export const SecretListView = ({
|
||||
tags,
|
||||
comment,
|
||||
reminderRepeatDays,
|
||||
reminderNote
|
||||
reminderNote,
|
||||
secretMetadata
|
||||
} = modSecret;
|
||||
const hasKeyChanged = oldKey !== key && key;
|
||||
|
||||
@@ -166,7 +173,8 @@ export const SecretListView = ({
|
||||
"comment",
|
||||
"skipMultilineEncoding",
|
||||
"reminderRepeatDays",
|
||||
"reminderNote"
|
||||
"reminderNote",
|
||||
"secretMetadata"
|
||||
] as const
|
||||
).every((el) => orgSecret[el] === modSecret[el]) && isSameTags;
|
||||
|
||||
@@ -199,7 +207,8 @@ export const SecretListView = ({
|
||||
reminderNote,
|
||||
secretId: orgSecret.id,
|
||||
newKey: hasKeyChanged ? key : undefined,
|
||||
skipMultilineEncoding: modSecret.skipMultilineEncoding
|
||||
skipMultilineEncoding: modSecret.skipMultilineEncoding,
|
||||
secretMetadata
|
||||
});
|
||||
if (cb) cb();
|
||||
}
|
||||
|
||||
+7
-1
@@ -47,7 +47,13 @@ export const formSchema = z.object({
|
||||
.nullable()
|
||||
.optional(),
|
||||
reminderNote: z.string().trim().nullable().optional(),
|
||||
|
||||
secretMetadata: z
|
||||
.object({
|
||||
key: z.string().trim().min(1),
|
||||
value: z.string().trim().default("")
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
tags: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
|
||||
+90
-28
@@ -34,7 +34,10 @@ import { useWorkspace } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth";
|
||||
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
|
||||
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
||||
import {
|
||||
IntegrationMappingBehavior,
|
||||
IntegrationMetadataSyncMode
|
||||
} from "@app/hooks/api/integrations/types";
|
||||
|
||||
enum TabSections {
|
||||
Connection = "connection",
|
||||
@@ -94,6 +97,7 @@ const schema = z
|
||||
mappingBehavior: z.nativeEnum(IntegrationMappingBehavior),
|
||||
kmsKeyId: z.string().optional(),
|
||||
shouldTag: z.boolean().optional(),
|
||||
metadataSyncMode: z.nativeEnum(IntegrationMetadataSyncMode).optional(),
|
||||
tags: z
|
||||
.object({
|
||||
key: z.string(),
|
||||
@@ -136,6 +140,7 @@ export const AwsSecretManagerConfigurePage = () => {
|
||||
});
|
||||
|
||||
const shouldTagState = watch("shouldTag");
|
||||
const selectedMetadataSyncMode = watch("metadataSyncMode");
|
||||
const selectedSourceEnvironment = watch("sourceEnvironment");
|
||||
const selectedAWSRegion = watch("awsRegion");
|
||||
const selectedMappingBehavior = watch("mappingBehavior");
|
||||
@@ -171,7 +176,8 @@ export const AwsSecretManagerConfigurePage = () => {
|
||||
tags,
|
||||
secretPrefix,
|
||||
kmsKeyId,
|
||||
mappingBehavior
|
||||
mappingBehavior,
|
||||
metadataSyncMode
|
||||
}: TFormSchema) => {
|
||||
try {
|
||||
if (!integrationAuth?.id) return;
|
||||
@@ -186,7 +192,8 @@ export const AwsSecretManagerConfigurePage = () => {
|
||||
metadata: {
|
||||
...(shouldTag
|
||||
? {
|
||||
secretAWSTag: tags
|
||||
secretAWSTag: tags,
|
||||
metadataSyncMode
|
||||
}
|
||||
: {}),
|
||||
...(secretPrefix && { secretPrefix }),
|
||||
@@ -339,7 +346,12 @@ export const AwsSecretManagerConfigurePage = () => {
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
onValueChange={(e) => {
|
||||
if (e === IntegrationMappingBehavior.MANY_TO_ONE) {
|
||||
setValue("metadataSyncMode", IntegrationMetadataSyncMode.CUSTOM);
|
||||
}
|
||||
onChange(e);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
dropdownContainerClassName="max-w-full"
|
||||
>
|
||||
@@ -386,14 +398,25 @@ export const AwsSecretManagerConfigurePage = () => {
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<div className="ml-1 mt-2">
|
||||
<div className="mb-3 ml-1 mt-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="shouldTag"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch
|
||||
id="tag-aws"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (
|
||||
isChecked &&
|
||||
selectedMappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE
|
||||
) {
|
||||
setValue(
|
||||
"metadataSyncMode",
|
||||
IntegrationMetadataSyncMode.SECRET_METADATA
|
||||
);
|
||||
}
|
||||
onChange(isChecked);
|
||||
}}
|
||||
isChecked={value}
|
||||
>
|
||||
Tag in AWS Secrets Manager
|
||||
@@ -401,37 +424,76 @@ export const AwsSecretManagerConfigurePage = () => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{shouldTagState && (
|
||||
<div className="mt-4 flex justify-between">
|
||||
{shouldTagState &&
|
||||
selectedMappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="tags.0.key"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
name="metadataSyncMode"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Tag Key"
|
||||
label="Tag Sync Mode"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input placeholder="managed-by" {...field} />
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
onValueChange={(e) => {
|
||||
setValue("tags", []);
|
||||
onChange(e);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
dropdownContainerClassName="max-w-full"
|
||||
>
|
||||
<SelectItem
|
||||
value={IntegrationMetadataSyncMode.SECRET_METADATA}
|
||||
className="text-left"
|
||||
key={`sync-mode-${IntegrationMetadataSyncMode.SECRET_METADATA}`}
|
||||
>
|
||||
Secret Metadata
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value={IntegrationMetadataSyncMode.CUSTOM}
|
||||
className="text-left"
|
||||
key={`sync-mode-${IntegrationMetadataSyncMode.CUSTOM}`}
|
||||
>
|
||||
Custom
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="tags.0.value"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Tag Value"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input placeholder="infisical" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
)}
|
||||
{shouldTagState &&
|
||||
selectedMetadataSyncMode === IntegrationMetadataSyncMode.CUSTOM && (
|
||||
<div className="mt-4 flex justify-between">
|
||||
<Controller
|
||||
control={control}
|
||||
name="tags.0.key"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Tag Key"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input placeholder="managed-by" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="tags.0.value"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Tag Value"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Input placeholder="infisical" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPrefix"
|
||||
|
||||
Reference in New Issue
Block a user