mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 18:15:53 +00:00
Merge remote-tracking branch 'origin/main' into feat/native-slack-integration
This commit is contained in:
@@ -9,36 +9,42 @@ import { Tooltip } from "../Tooltip";
|
||||
export type FormLabelProps = {
|
||||
id?: string;
|
||||
isRequired?: boolean;
|
||||
isOptional?:boolean;
|
||||
isOptional?: boolean;
|
||||
label?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
className?: string;
|
||||
tooltipText?: string;
|
||||
tooltipClassName?: string;
|
||||
tooltipText?: ReactNode;
|
||||
};
|
||||
|
||||
export const FormLabel = ({ id, label, isRequired, icon, className,isOptional, tooltipText }: FormLabelProps) => (
|
||||
export const FormLabel = ({
|
||||
id,
|
||||
label,
|
||||
isRequired,
|
||||
icon,
|
||||
className,
|
||||
isOptional,
|
||||
tooltipClassName,
|
||||
tooltipText
|
||||
}: FormLabelProps) => (
|
||||
<Label.Root
|
||||
className={twMerge(
|
||||
"mb-0.5 ml-1 flex items-center text-sm font-normal text-mineshaft-400",
|
||||
"mb-0.5 flex items-center text-sm font-normal text-mineshaft-400",
|
||||
className
|
||||
)}
|
||||
htmlFor={id}
|
||||
>
|
||||
{label}
|
||||
{isRequired && <span className="ml-1 text-red">*</span>}
|
||||
{isOptional && <span className="ml-1 text-gray-500 italic text-xs">- Optional</span>}
|
||||
{isOptional && <span className="ml-1 text-xs italic text-gray-500">- Optional</span>}
|
||||
{icon && !tooltipText && (
|
||||
<span className="ml-2 cursor-default text-mineshaft-300 hover:text-mineshaft-200">
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
{tooltipText && (
|
||||
<Tooltip content={tooltipText}>
|
||||
<FontAwesomeIcon
|
||||
icon={faQuestionCircle}
|
||||
size="1x"
|
||||
className="ml-2"
|
||||
/>
|
||||
<Tooltip content={tooltipText} className={tooltipClassName}>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="1x" className="ml-2" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Label.Root>
|
||||
|
||||
@@ -20,7 +20,9 @@ export enum DynamicSecretProviders {
|
||||
Cassandra = "cassandra",
|
||||
AwsIam = "aws-iam",
|
||||
Redis = "redis",
|
||||
AwsElastiCache = "aws-elasticache"
|
||||
AwsElastiCache = "aws-elasticache",
|
||||
MongoAtlas = "mongo-db-atlas",
|
||||
ElasticSearch = "elastic-search"
|
||||
}
|
||||
|
||||
export enum SqlProviders {
|
||||
@@ -97,6 +99,44 @@ export type TDynamicSecretProvider =
|
||||
revocationStatement: string;
|
||||
ca?: string | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: DynamicSecretProviders.MongoAtlas;
|
||||
inputs: {
|
||||
adminPublicKey: string;
|
||||
adminPrivateKey: string;
|
||||
groupId: string;
|
||||
roles: {
|
||||
databaseName: string;
|
||||
roleName: string;
|
||||
collectionName?: string;
|
||||
}[];
|
||||
scopes?: {
|
||||
name: string;
|
||||
type: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: DynamicSecretProviders.ElasticSearch;
|
||||
inputs: {
|
||||
host: string;
|
||||
port: number;
|
||||
ca?: string | undefined;
|
||||
roles: string[];
|
||||
|
||||
auth:
|
||||
| {
|
||||
type: "user";
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
| {
|
||||
type: "api-key";
|
||||
apiKey: string;
|
||||
apiKeyId: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type TCreateDynamicSecretDTO = {
|
||||
|
||||
@@ -71,6 +71,8 @@ export const useCreateIntegration = () => {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
githubVisibility?: string;
|
||||
githubVisibilityRepoIds?: string[];
|
||||
kmsKeyId?: string;
|
||||
shouldDisableDelete?: boolean;
|
||||
shouldMaskSecrets?: boolean;
|
||||
|
||||
@@ -34,6 +34,9 @@ export type TIntegration = {
|
||||
syncMessage?: string;
|
||||
__v: number;
|
||||
metadata?: {
|
||||
githubVisibility?: string;
|
||||
githubVisibilityRepoIds?: string[];
|
||||
|
||||
secretSuffix?: string;
|
||||
syncBehavior?: IntegrationSyncBehavior;
|
||||
mappingBehavior?: IntegrationMappingBehavior;
|
||||
|
||||
@@ -7,7 +7,11 @@ import { TSharedSecret, TViewSharedSecretResponse } from "./types";
|
||||
export const secretSharingKeys = {
|
||||
allSharedSecrets: () => ["sharedSecrets"] as const,
|
||||
specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) =>
|
||||
[...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const
|
||||
[...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const,
|
||||
getSecretById: (arg: { id: string; hashedHex: string; password?: string }) => [
|
||||
"shared-secret",
|
||||
arg
|
||||
]
|
||||
};
|
||||
|
||||
export const useGetSharedSecrets = ({
|
||||
@@ -38,31 +42,28 @@ export const useGetSharedSecrets = ({
|
||||
|
||||
export const useGetActiveSharedSecretById = ({
|
||||
sharedSecretId,
|
||||
hashedHex
|
||||
hashedHex,
|
||||
password
|
||||
}: {
|
||||
sharedSecretId: string;
|
||||
hashedHex: string;
|
||||
password?: string;
|
||||
}) => {
|
||||
return useQuery<TViewSharedSecretResponse, [string]>({
|
||||
enabled: Boolean(sharedSecretId) && Boolean(hashedHex),
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({
|
||||
hashedHex
|
||||
});
|
||||
|
||||
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
||||
return useQuery<TViewSharedSecretResponse>(
|
||||
secretSharingKeys.getSecretById({ id: sharedSecretId, hashedHex, password }),
|
||||
async () => {
|
||||
const { data } = await apiRequest.post<TViewSharedSecretResponse>(
|
||||
`/api/v1/secret-sharing/public/${sharedSecretId}`,
|
||||
{
|
||||
params
|
||||
hashedHex,
|
||||
password
|
||||
}
|
||||
);
|
||||
return {
|
||||
encryptedValue: data.encryptedValue,
|
||||
iv: data.iv,
|
||||
tag: data.tag,
|
||||
accessType: data.accessType,
|
||||
orgName: data.orgName
|
||||
};
|
||||
|
||||
return data;
|
||||
},
|
||||
{
|
||||
enabled: Boolean(sharedSecretId) && Boolean(hashedHex)
|
||||
}
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ export type TSharedSecret = {
|
||||
|
||||
export type TCreateSharedSecretRequest = {
|
||||
name?: string;
|
||||
password?: string;
|
||||
encryptedValue: string;
|
||||
hashedHex: string;
|
||||
iv: string;
|
||||
@@ -25,11 +26,14 @@ export type TCreateSharedSecretRequest = {
|
||||
};
|
||||
|
||||
export type TViewSharedSecretResponse = {
|
||||
encryptedValue: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
accessType: SecretSharingAccessType;
|
||||
orgName?: string;
|
||||
isPasswordProtected: boolean;
|
||||
secret: {
|
||||
encryptedValue: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
accessType: SecretSharingAccessType;
|
||||
orgName?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TDeleteSharedSecretRequest = {
|
||||
@@ -40,3 +44,4 @@ export enum SecretSharingAccessType {
|
||||
Anyone = "anyone",
|
||||
Organization = "organization"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
@@ -53,6 +53,21 @@ enum TabSections {
|
||||
Options = "options"
|
||||
}
|
||||
|
||||
const secretsVisibility = [
|
||||
{
|
||||
value: "selected",
|
||||
label: "Select repositories"
|
||||
},
|
||||
{
|
||||
value: "all",
|
||||
label: "All public repositories"
|
||||
},
|
||||
{
|
||||
value: "private",
|
||||
label: "All private repositories"
|
||||
}
|
||||
] as const;
|
||||
|
||||
const targetEnv = ["github-repo", "github-org", "github-env"] as const;
|
||||
type TargetEnv = (typeof targetEnv)[number];
|
||||
|
||||
@@ -63,9 +78,12 @@ const schema = yup.object({
|
||||
shouldEnableDelete: yup.boolean().optional(),
|
||||
scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(),
|
||||
|
||||
repoIds: yup.mixed().when("scope", {
|
||||
is: "github-repo",
|
||||
then: yup.array(yup.string().required()).min(1, "Select at least one repositories")
|
||||
// Explanation: If scope is (github-repo) OR (github-org AND visibility is set to selected), then repoIds is required
|
||||
repoIds: yup.mixed().when(["scope", "visibility"], {
|
||||
is: (scope: string, visibility: string) =>
|
||||
scope === "github-repo" || (scope === "github-org" && visibility === "selected"),
|
||||
then: yup.array(yup.string().required()).min(1, "Select at least one repository"),
|
||||
otherwise: yup.mixed().notRequired()
|
||||
}),
|
||||
|
||||
repoId: yup.mixed().when("scope", {
|
||||
@@ -91,6 +109,10 @@ const schema = yup.object({
|
||||
orgId: yup.mixed().when("scope", {
|
||||
is: "github-org",
|
||||
then: yup.string().required("Organization is required")
|
||||
}),
|
||||
visibility: yup.mixed().when("scope", {
|
||||
is: "github-org",
|
||||
then: yup.string().required("Visibility is required")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -121,6 +143,7 @@ export default function GitHubCreateIntegrationPage() {
|
||||
secretPath: "/",
|
||||
scope: "github-repo",
|
||||
repoIds: [],
|
||||
visibility: "all",
|
||||
shouldEnableDelete: false
|
||||
}
|
||||
});
|
||||
@@ -130,6 +153,7 @@ export default function GitHubCreateIntegrationPage() {
|
||||
const repoIds = watch("repoIds");
|
||||
const repoName = watch("repoName");
|
||||
const repoOwner = watch("repoOwner");
|
||||
const selectedOrgId = watch("orgId");
|
||||
|
||||
const { data: integrationAuthGithubEnvs } = useGetIntegrationAuthGithubEnvs(
|
||||
integrationAuthId as string,
|
||||
@@ -196,6 +220,8 @@ export default function GitHubCreateIntegrationPage() {
|
||||
scope: data.scope,
|
||||
owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name,
|
||||
metadata: {
|
||||
githubVisibility: data.visibility,
|
||||
githubVisibilityRepoIds: data.repoIds,
|
||||
secretSuffix: data.secretSuffix,
|
||||
shouldEnableDelete: data.shouldEnableDelete
|
||||
}
|
||||
@@ -242,6 +268,15 @@ export default function GitHubCreateIntegrationPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const selectedOrganization = useMemo(() => {
|
||||
if (!integrationAuthApps) return null;
|
||||
|
||||
return integrationAuthApps.filter(
|
||||
(authApp) =>
|
||||
integrationAuthOrgs?.find((e) => e.orgId === selectedOrgId)?.name === authApp.owner
|
||||
);
|
||||
}, [selectedOrgId, integrationAuthApps]);
|
||||
|
||||
return integrationAuth && workspace && integrationAuthApps ? (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center py-4">
|
||||
<Head>
|
||||
@@ -339,7 +374,10 @@ export default function GitHubCreateIntegrationPage() {
|
||||
<FormControl label="Scope" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
onValueChange={onChange}
|
||||
onValueChange={(e) => {
|
||||
setValue("repoIds", []);
|
||||
onChange(e);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
<SelectItem value="github-org">Organization</SelectItem>
|
||||
@@ -430,32 +468,143 @@ export default function GitHubCreateIntegrationPage() {
|
||||
/>
|
||||
)}
|
||||
{scope === "github-org" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="orgId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Organization"
|
||||
errorText={
|
||||
integrationAuthOrgs?.length ? error?.message : "No organizations found"
|
||||
}
|
||||
isError={Boolean(integrationAuthOrgs?.length && error?.message)}
|
||||
>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={onChange}
|
||||
className="w-full border border-mineshaft-500"
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="orgId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Organization"
|
||||
errorText={
|
||||
integrationAuthOrgs?.length ? error?.message : "No organizations found"
|
||||
}
|
||||
isError={Boolean(integrationAuthOrgs?.length && error?.message)}
|
||||
>
|
||||
{integrationAuthOrgs &&
|
||||
integrationAuthOrgs.map(({ name, orgId }) => (
|
||||
<SelectItem key={`github-organization-${orgId}`} value={orgId}>
|
||||
{name}
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={onChange}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{integrationAuthOrgs &&
|
||||
integrationAuthOrgs.map(({ name, orgId }) => (
|
||||
<SelectItem key={`github-organization-${orgId}`} value={orgId}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="visibility"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="Select which type of repositories that the secrets should be synced to."
|
||||
label="Visibility"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={onChange}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{secretsVisibility.map(({ label, value }) => (
|
||||
<SelectItem key={`github-visibility-${value}`} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{watch("visibility") === "selected" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="repoIds"
|
||||
render={({ field: { onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Selected Repositories"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{integrationAuthApps.length > 0 ? (
|
||||
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
{repoIds.length === 1
|
||||
? integrationAuthApps?.reduce(
|
||||
(acc, { appId, name, owner }) =>
|
||||
repoIds[0] === appId ? `${owner}/${name}` : acc,
|
||||
""
|
||||
)
|
||||
: `${repoIds.length} repositories selected`}
|
||||
<FontAwesomeIcon icon={faAngleDown} className="text-xs" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
No repositories found
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="thin-scrollbar z-[100] max-h-80 overflow-y-scroll"
|
||||
>
|
||||
{selectedOrganization ? (
|
||||
selectedOrganization.map((integrationAuthApp) => {
|
||||
const isSelected = repoIds.includes(
|
||||
String(integrationAuthApp.appId)
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (repoIds.includes(String(integrationAuthApp.appId))) {
|
||||
onChange(
|
||||
repoIds.filter(
|
||||
(appId: string) =>
|
||||
appId !== String(integrationAuthApp.appId)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
onChange([
|
||||
...repoIds,
|
||||
String(integrationAuthApp.appId)
|
||||
]);
|
||||
}
|
||||
}}
|
||||
key={`repos-id-${integrationAuthApp.appId}`}
|
||||
icon={
|
||||
isSelected ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="pr-0.5 text-primary"
|
||||
/>
|
||||
) : (
|
||||
<div className="pl-[1.01rem]" />
|
||||
)
|
||||
}
|
||||
iconPos="left"
|
||||
className="w-[28.4rem] text-sm"
|
||||
>
|
||||
{integrationAuthApp.owner}/{integrationAuthApp.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{scope === "github-env" && (
|
||||
<Controller
|
||||
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import {
|
||||
faArrowRight,
|
||||
faCalendarCheck,
|
||||
faRefresh,
|
||||
faWarning,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormLabel, IconButton, Tag, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
||||
import { TIntegration } from "@app/hooks/api/types";
|
||||
|
||||
type IProps = {
|
||||
integration: TIntegration;
|
||||
environments: Array<{ name: string; slug: string; id: string }>;
|
||||
onRemoveIntegration: VoidFunction;
|
||||
onManualSyncIntegration: VoidFunction;
|
||||
};
|
||||
|
||||
export const ConfiguredIntegrationItem = ({
|
||||
integration,
|
||||
environments,
|
||||
onRemoveIntegration,
|
||||
onManualSyncIntegration
|
||||
}: IProps) => {
|
||||
return (
|
||||
<div
|
||||
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3"
|
||||
key={`integration-${integration?.id.toString()}`}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel label="Environment" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{environments.find((e) => e.id === integration.envId)?.name || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel label="Secret Path" />
|
||||
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration.secretPath}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full items-center">
|
||||
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400" />
|
||||
</div>
|
||||
<div className="ml-4 flex flex-col">
|
||||
<FormLabel
|
||||
tooltipText={
|
||||
integration.integration === "github" ? (
|
||||
<div className="text-xs">
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{integration.metadata?.githubVisibility === "selected"
|
||||
? "Syncing to selected repositories in the organization. "
|
||||
: integration.metadata?.githubVisibility === "private"
|
||||
? "Syncing to all private repositories in the organization"
|
||||
: "Syncing to all public and private repositories in the organization"}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
label="Integration"
|
||||
/>
|
||||
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integrationSlugNameMapping[integration.integration]}
|
||||
</div>
|
||||
</div>
|
||||
{integration.integration === "qovery" && (
|
||||
<div className="flex flex-row">
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel label="Org" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration?.owner || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel label="Project" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration?.targetService || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel label="Env" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration?.targetEnvironment || "-"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!(
|
||||
integration.integration === "aws-secret-manager" &&
|
||||
integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE
|
||||
) && (
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel
|
||||
label={
|
||||
(integration.integration === "qovery" && integration?.scope) ||
|
||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
||||
(["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") ||
|
||||
(integration?.integration === "terraform-cloud" && "Project") ||
|
||||
(integration?.scope === "github-org" && "Organization") ||
|
||||
(["github-repo", "github-env"].includes(integration?.scope as string) &&
|
||||
"Repository") ||
|
||||
"App"
|
||||
}
|
||||
/>
|
||||
<div className="no-scrollbar::-webkit-scrollbar min-w-[8rem] max-w-[12rem] overflow-scroll whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200 no-scrollbar">
|
||||
{(integration.integration === "hashicorp-vault" &&
|
||||
`${integration.app} - path: ${integration.path}`) ||
|
||||
(integration.scope === "github-org" && `${integration.owner}`) ||
|
||||
(["aws-parameter-store", "rundeck"].includes(integration.integration) &&
|
||||
`${integration.path}`) ||
|
||||
(integration.scope?.startsWith("github-") &&
|
||||
`${integration.owner}/${integration.app}`) ||
|
||||
integration.app}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(integration.integration === "vercel" ||
|
||||
integration.integration === "netlify" ||
|
||||
integration.integration === "railway" ||
|
||||
integration.integration === "gitlab" ||
|
||||
integration.integration === "teamcity" ||
|
||||
integration.integration === "bitbucket" ||
|
||||
(integration.integration === "github" && integration.scope === "github-env")) && (
|
||||
<div className="ml-4 flex flex-col">
|
||||
<FormLabel label="Target Environment" />
|
||||
<div className="overflow-clip text-ellipsis whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration.targetEnvironment || integration.targetEnvironmentId}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "checkly" && integration.targetService && (
|
||||
<div className="ml-2">
|
||||
<FormLabel label="Group" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration.targetService}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "terraform-cloud" && integration.targetService && (
|
||||
<div className="ml-2">
|
||||
<FormLabel label="Category" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration.targetService}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(integration.integration === "checkly" || integration.integration === "github") && (
|
||||
<div className="ml-2">
|
||||
<FormLabel label="Secret Suffix" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration?.metadata?.secretSuffix || "-"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-[1.5rem] flex cursor-default">
|
||||
{integration.isSynced != null && integration.lastUsed != null && (
|
||||
<Tag key={integration.id} className={integration.isSynced ? "bg-green-800" : "bg-red/80"}>
|
||||
<Tooltip
|
||||
center
|
||||
className="max-w-xs whitespace-normal break-words"
|
||||
content={
|
||||
<div className="flex max-h-[10rem] flex-col overflow-auto ">
|
||||
<div className="flex self-start">
|
||||
<FontAwesomeIcon icon={faCalendarCheck} className="pt-0.5 pr-2 text-sm" />
|
||||
<div className="text-sm">Last sync</div>
|
||||
</div>
|
||||
<div className="pl-5 text-left text-xs">
|
||||
{format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")}
|
||||
</div>
|
||||
{!integration.isSynced && (
|
||||
<>
|
||||
<div className="mt-2 flex self-start">
|
||||
<FontAwesomeIcon icon={faXmark} className="pt-1 pr-2 text-sm" />
|
||||
<div className="text-sm">Fail reason</div>
|
||||
</div>
|
||||
<div className="pl-5 text-left text-xs">{integration.syncMessage}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center space-x-2 text-white">
|
||||
<div>{integration.isSynced ? "Synced" : "Not synced"}</div>
|
||||
{!integration.isSynced && <FontAwesomeIcon icon={faWarning} />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Tag>
|
||||
)}
|
||||
<div className="mr-1 flex items-end opacity-80 duration-200 hover:opacity-100">
|
||||
<Tooltip className="text-center" content="Manually sync integration secrets">
|
||||
<Button
|
||||
onClick={() => onManualSyncIntegration()}
|
||||
className="max-w-[2.5rem] border-none bg-mineshaft-500"
|
||||
>
|
||||
<FontAwesomeIcon icon={faRefresh} className="px-1 text-bunker-200" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Integrations}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<div className="flex items-end opacity-80 duration-200 hover:opacity-100">
|
||||
<Tooltip content="Remove Integration">
|
||||
<IconButton
|
||||
onClick={() => onRemoveIntegration()}
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="danger"
|
||||
variant="star"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="px-0.5" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+19
-221
@@ -1,27 +1,10 @@
|
||||
import { faCalendarCheck } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faArrowRight, faRefresh, faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Skeleton,
|
||||
Tag,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { Checkbox, DeleteActionModal, EmptyState, Skeleton } from "@app/components/v2";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
|
||||
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
||||
import { TIntegration } from "@app/hooks/api/types";
|
||||
|
||||
import { ConfiguredIntegrationItem } from "./ConfiguredIntegrationItem";
|
||||
|
||||
type Props = {
|
||||
environments: Array<{ name: string; slug: string; id: string }>;
|
||||
integrations?: TIntegration[];
|
||||
@@ -72,207 +55,22 @@ export const IntegrationsSection = ({
|
||||
{!isLoading && (
|
||||
<div className="flex min-w-max flex-col space-y-4 p-6 pt-0">
|
||||
{integrations?.map((integration) => (
|
||||
<div
|
||||
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3"
|
||||
key={`integration-${integration?.id.toString()}`}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel label="Environment" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{environments.find((e) => e.id === integration.envId)?.name || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel label="Secret Path" />
|
||||
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration.secretPath}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full items-center">
|
||||
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400" />
|
||||
</div>
|
||||
<div className="ml-4 flex flex-col">
|
||||
<FormLabel label="Integration" />
|
||||
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integrationSlugNameMapping[integration.integration]}
|
||||
</div>
|
||||
</div>
|
||||
{integration.integration === "qovery" && (
|
||||
<div className="flex flex-row">
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel label="Org" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration?.owner || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel label="Project" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration?.targetService || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel label="Env" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration?.targetEnvironment || "-"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!(
|
||||
integration.integration === "aws-secret-manager" &&
|
||||
integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE
|
||||
) && (
|
||||
<div className="ml-2 flex flex-col">
|
||||
<FormLabel
|
||||
label={
|
||||
(integration.integration === "qovery" && integration?.scope) ||
|
||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
||||
(["aws-parameter-store", "rundeck"].includes(integration.integration) &&
|
||||
"Path") ||
|
||||
(integration?.integration === "terraform-cloud" && "Project") ||
|
||||
(integration?.scope === "github-org" && "Organization") ||
|
||||
(["github-repo", "github-env"].includes(integration?.scope as string) &&
|
||||
"Repository") ||
|
||||
"App"
|
||||
}
|
||||
/>
|
||||
<div className="no-scrollbar::-webkit-scrollbar min-w-[8rem] max-w-[12rem] overflow-scroll whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200 no-scrollbar">
|
||||
{(integration.integration === "hashicorp-vault" &&
|
||||
`${integration.app} - path: ${integration.path}`) ||
|
||||
(integration.scope === "github-org" && `${integration.owner}`) ||
|
||||
(["aws-parameter-store", "rundeck"].includes(integration.integration) &&
|
||||
`${integration.path}`) ||
|
||||
(integration.scope?.startsWith("github-") &&
|
||||
`${integration.owner}/${integration.app}`) ||
|
||||
integration.app}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(integration.integration === "vercel" ||
|
||||
integration.integration === "netlify" ||
|
||||
integration.integration === "railway" ||
|
||||
integration.integration === "gitlab" ||
|
||||
integration.integration === "teamcity" ||
|
||||
integration.integration === "bitbucket" ||
|
||||
(integration.integration === "github" && integration.scope === "github-env")) && (
|
||||
<div className="ml-4 flex flex-col">
|
||||
<FormLabel label="Target Environment" />
|
||||
<div className="overflow-clip text-ellipsis whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration.targetEnvironment || integration.targetEnvironmentId}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "checkly" && integration.targetService && (
|
||||
<div className="ml-2">
|
||||
<FormLabel label="Group" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration.targetService}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "terraform-cloud" && integration.targetService && (
|
||||
<div className="ml-2">
|
||||
<FormLabel label="Category" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration.targetService}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(integration.integration === "checkly" ||
|
||||
integration.integration === "github") && (
|
||||
<div className="ml-2">
|
||||
<FormLabel label="Secret Suffix" />
|
||||
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||
{integration?.metadata?.secretSuffix || "-"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-[1.5rem] flex cursor-default">
|
||||
{integration.isSynced != null && integration.lastUsed != null && (
|
||||
<Tag
|
||||
key={integration.id}
|
||||
className={integration.isSynced ? "bg-green-800" : "bg-red/80"}
|
||||
>
|
||||
<Tooltip
|
||||
center
|
||||
className="max-w-xs whitespace-normal break-words"
|
||||
content={
|
||||
<div className="flex max-h-[10rem] flex-col overflow-auto ">
|
||||
<div className="flex self-start">
|
||||
<FontAwesomeIcon
|
||||
icon={faCalendarCheck}
|
||||
className="pt-0.5 pr-2 text-sm"
|
||||
/>
|
||||
<div className="text-sm">Last sync</div>
|
||||
</div>
|
||||
<div className="pl-5 text-left text-xs">
|
||||
{format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")}
|
||||
</div>
|
||||
{!integration.isSynced && (
|
||||
<>
|
||||
<div className="mt-2 flex self-start">
|
||||
<FontAwesomeIcon icon={faXmark} className="pt-1 pr-2 text-sm" />
|
||||
<div className="text-sm">Fail reason</div>
|
||||
</div>
|
||||
<div className="pl-5 text-left text-xs">
|
||||
{integration.syncMessage}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center space-x-2 text-white">
|
||||
<div>{integration.isSynced ? "Synced" : "Not synced"}</div>
|
||||
{!integration.isSynced && <FontAwesomeIcon icon={faWarning} />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Tag>
|
||||
)}
|
||||
<div className="mr-1 flex items-end opacity-80 duration-200 hover:opacity-100">
|
||||
<Tooltip className="text-center" content="Manually sync integration secrets">
|
||||
<Button
|
||||
onClick={() =>
|
||||
syncIntegration({
|
||||
workspaceId,
|
||||
id: integration.id,
|
||||
lastUsed: integration.lastUsed as string
|
||||
})
|
||||
}
|
||||
className="max-w-[2.5rem] border-none bg-mineshaft-500"
|
||||
>
|
||||
<FontAwesomeIcon icon={faRefresh} className="px-1 text-bunker-200" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Integrations}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<div className="flex items-end opacity-80 duration-200 hover:opacity-100">
|
||||
<Tooltip content="Remove Integration">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setShouldDeleteSecrets.off();
|
||||
handlePopUpOpen("deleteConfirmation", integration);
|
||||
}}
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="danger"
|
||||
variant="star"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="px-0.5" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<ConfiguredIntegrationItem
|
||||
key={`integration-${integration.id}`}
|
||||
onManualSyncIntegration={() => {
|
||||
syncIntegration({
|
||||
workspaceId,
|
||||
id: integration.id,
|
||||
lastUsed: integration.lastUsed as string
|
||||
});
|
||||
}}
|
||||
onRemoveIntegration={() => {
|
||||
setShouldDeleteSecrets.off();
|
||||
handlePopUpOpen("deleteConfirmation", integration);
|
||||
}}
|
||||
integration={integration}
|
||||
environments={environments}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
+57
-7
@@ -1,4 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { DiRedis } from "react-icons/di";
|
||||
import { SiApachecassandra, SiElasticsearch, SiMongodb } from "react-icons/si";
|
||||
import { faAws } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faDatabase } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -10,6 +12,8 @@ import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm";
|
||||
import { AwsIamInputForm } from "./AwsIamInputForm";
|
||||
import { CassandraInputForm } from "./CassandraInputForm";
|
||||
import { ElasticSearchInputForm } from "./ElasticSearchInputForm";
|
||||
import { MongoAtlasInputForm } from "./MongoAtlasInputForm";
|
||||
import { RedisInputForm } from "./RedisInputForm";
|
||||
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
|
||||
|
||||
@@ -28,29 +32,39 @@ enum WizardSteps {
|
||||
|
||||
const DYNAMIC_SECRET_LIST = [
|
||||
{
|
||||
icon: faDatabase,
|
||||
icon: <FontAwesomeIcon icon={faDatabase} size="lg" />,
|
||||
provider: DynamicSecretProviders.SqlDatabase,
|
||||
title: "SQL\nDatabase"
|
||||
},
|
||||
{
|
||||
icon: faDatabase,
|
||||
icon: <SiApachecassandra size="2rem" />,
|
||||
provider: DynamicSecretProviders.Cassandra,
|
||||
title: "Cassandra"
|
||||
},
|
||||
{
|
||||
icon: faDatabase,
|
||||
icon: <DiRedis size="2rem" />,
|
||||
provider: DynamicSecretProviders.Redis,
|
||||
title: "Redis"
|
||||
},
|
||||
{
|
||||
icon: faAws,
|
||||
icon: <FontAwesomeIcon icon={faAws} size="lg" />,
|
||||
provider: DynamicSecretProviders.AwsElastiCache,
|
||||
title: "AWS ElastiCache"
|
||||
},
|
||||
{
|
||||
icon: faAws,
|
||||
icon: <FontAwesomeIcon icon={faAws} size="lg" />,
|
||||
provider: DynamicSecretProviders.AwsIam,
|
||||
title: "AWS IAM"
|
||||
},
|
||||
{
|
||||
icon: <SiMongodb size="2rem" />,
|
||||
provider: DynamicSecretProviders.MongoAtlas,
|
||||
title: "Mongo Atlas"
|
||||
},
|
||||
{
|
||||
icon: <SiElasticsearch size="2rem" />,
|
||||
provider: DynamicSecretProviders.ElasticSearch,
|
||||
title: "Elastic Search"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -87,7 +101,7 @@ export const CreateDynamicSecretForm = ({
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<div className="mb-4 text-mineshaft-300">Select a service to connect to:</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{DYNAMIC_SECRET_LIST.map(({ icon, provider, title }) => (
|
||||
<div
|
||||
key={`dynamic-secret-provider-${provider}`}
|
||||
@@ -105,7 +119,7 @@ export const CreateDynamicSecretForm = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} size="lg" />
|
||||
{icon}
|
||||
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -202,6 +216,42 @@ export const CreateDynamicSecretForm = ({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.ProviderInputs &&
|
||||
selectedProvider === DynamicSecretProviders.MongoAtlas && (
|
||||
<motion.div
|
||||
key="dynamic-atlas-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<MongoAtlasInputForm
|
||||
onCompleted={handleFormReset}
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.ProviderInputs &&
|
||||
selectedProvider === DynamicSecretProviders.ElasticSearch && (
|
||||
<motion.div
|
||||
key="dynamic-elastic-search-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<ElasticSearchInputForm
|
||||
onCompleted={handleFormReset}
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
+425
@@ -0,0 +1,425 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Link from "next/link";
|
||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const authMethods = [
|
||||
{
|
||||
label: "Username/Password",
|
||||
value: "user"
|
||||
},
|
||||
{
|
||||
label: "API Key",
|
||||
value: "api-key"
|
||||
}
|
||||
] as const;
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
host: z.string().trim().min(1),
|
||||
port: z.coerce.number(),
|
||||
|
||||
// two auth types "user, apikey"
|
||||
auth: z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("user"),
|
||||
username: z.string().trim(),
|
||||
password: z.string().trim()
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("api-key"),
|
||||
apiKey: z.string().trim(),
|
||||
apiKeyId: z.string().trim()
|
||||
})
|
||||
]),
|
||||
|
||||
roles: z.array(z.string().trim().min(1)).min(1, "At least one role is required"),
|
||||
ca: z.string().optional()
|
||||
}),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onCompleted: () => void;
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
export const ElasticSearchInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
provider: {
|
||||
auth: {
|
||||
type: "user"
|
||||
},
|
||||
roles: ["superuser"],
|
||||
port: 443
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isLoading) return;
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.ElasticSearch, inputs: provider },
|
||||
maxTTL,
|
||||
name,
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
});
|
||||
onCompleted();
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const selectedAuthType = watch("provider.auth.type");
|
||||
const selectedRoles = watch("provider.roles");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="dynamic-secret" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
defaultValue="1h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
defaultValue="24h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.host"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Host"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
placeholder="https://fgy543ws2w35dfh7jdaafa12ha.aws-us-east-1.io"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.port"
|
||||
defaultValue={443}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Port"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.auth.type"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Authentication Method"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="w-full"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
const authFields = [
|
||||
"provider.auth.username",
|
||||
"provider.auth.password",
|
||||
"provider.auth.apiKey",
|
||||
"provider.auth.apiKeyId"
|
||||
] as const;
|
||||
|
||||
authFields.forEach((f) => {
|
||||
setValue(f, "");
|
||||
});
|
||||
|
||||
field.onChange(e);
|
||||
}}
|
||||
>
|
||||
{authMethods.map((authType) => (
|
||||
<SelectItem value={authType.value} key={`auth-method-${authType.value}`}>
|
||||
{authType.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={
|
||||
selectedAuthType === "user"
|
||||
? "provider.auth.username"
|
||||
: "provider.auth.apiKeyId"
|
||||
}
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={selectedAuthType === "user" ? "Username" : "API Key ID"}
|
||||
className="w-full"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={
|
||||
selectedAuthType === "user" ? "provider.auth.password" : "provider.auth.apiKey"
|
||||
}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={selectedAuthType === "user" ? "Password" : "API Key"}
|
||||
className="w-full"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-3 flex flex-col">
|
||||
<FormLabel
|
||||
className="mb-2"
|
||||
label="Roles"
|
||||
tooltipText={
|
||||
<div className="space-y-4">
|
||||
<p>Select which role(s) to assign the users provisioned by Infisical.</p>
|
||||
<p>
|
||||
There is a wide range of in-built roles in Elastic Search. Some include,
|
||||
superuser, apm_user, kibana_admin, monitoring_user, and many more. You can{" "}
|
||||
<Link
|
||||
passHref
|
||||
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-roles.html"
|
||||
>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<span className="cursor-pointer text-primary-400">
|
||||
read more about roles here
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
You can also assign custom roles by providing the name of the custom role in
|
||||
the input field.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-col -space-y-2">
|
||||
{selectedRoles.map((_, i) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`provider.roles.${i}`}
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`role-${i}`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
|
||||
<div className="flex h-9 items-center gap-2">
|
||||
<Input
|
||||
placeholder="Insert role name, (superuser, kibana_admin, custom_role)"
|
||||
className="mb-0 flex-grow"
|
||||
{...field}
|
||||
/>
|
||||
<IconButton
|
||||
isDisabled={selectedRoles.length === 1}
|
||||
ariaLabel="delete key"
|
||||
className="h-9"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (selectedRoles && selectedRoles?.length > 1) {
|
||||
setValue(
|
||||
"provider.roles",
|
||||
selectedRoles.filter((__, idx) => idx !== i)
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
className="mb-3"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
setValue("provider.roles", [...selectedRoles, ""]);
|
||||
}}
|
||||
>
|
||||
Add Role
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.ca"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isOptional
|
||||
label="CA (SSL)"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+455
@@ -0,0 +1,455 @@
|
||||
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 ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
adminPublicKey: z.string().trim().min(1),
|
||||
adminPrivateKey: z.string().trim().min(1),
|
||||
groupId: z.string().trim().min(1),
|
||||
roles: z
|
||||
.object({
|
||||
collectionName: z.string().optional(),
|
||||
databaseName: z.string().min(1),
|
||||
roleName: z.string().min(1)
|
||||
})
|
||||
.array()
|
||||
.min(1),
|
||||
scopes: z
|
||||
.object({
|
||||
name: z.string().min(1),
|
||||
type: z.string().min(1)
|
||||
})
|
||||
.array()
|
||||
}),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onCompleted: () => void;
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
const ATLAS_SCOPE_TYPES = [
|
||||
{
|
||||
label: "Cluster",
|
||||
value: "CLUSTER"
|
||||
},
|
||||
{
|
||||
label: "Data Lake",
|
||||
value: "DATA_LAKE"
|
||||
},
|
||||
{
|
||||
label: "Stream",
|
||||
value: "STREAM"
|
||||
}
|
||||
];
|
||||
|
||||
export const MongoAtlasInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
getValues,
|
||||
setValue,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
provider: {
|
||||
roles: [{ databaseName: "", roleName: "" }]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const roleFields = useFieldArray({
|
||||
control,
|
||||
name: "provider.roles"
|
||||
});
|
||||
|
||||
const scopeFields = useFieldArray({
|
||||
control,
|
||||
name: "provider.scopes"
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isLoading) return;
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.MongoAtlas, inputs: provider },
|
||||
maxTTL,
|
||||
name,
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
});
|
||||
onCompleted();
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="dynamic-secret" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
defaultValue="1h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
defaultValue="24h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.adminPublicKey"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Admin Public Key"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.adminPrivateKey"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Admin Private Key"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.groupId"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Group/Project ID"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="Unique 24-hexadecimal digit string that identifies your project"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormLabel label="Roles" />
|
||||
<div className="mb-3 flex flex-col space-y-2">
|
||||
{roleFields.fields.map(({ id: roleFieldId }, i) => (
|
||||
<div key={roleFieldId} className="flex items-end space-x-2">
|
||||
<div className="flex-grow">
|
||||
{i === 0 && <span className="text-xs text-mineshaft-400">Database Name</span>}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`provider.roles.${i}.databaseName`}
|
||||
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="Collection Name"
|
||||
className="text-xs text-mineshaft-400"
|
||||
isOptional
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`provider.roles.${i}.collectionName`}
|
||||
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="Role"
|
||||
className="text-xs text-mineshaft-400"
|
||||
tooltipClassName="max-w-md whitespace-pre-line"
|
||||
tooltipText={`Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.
|
||||
Built-in: atlasAdmin, backup, clusterMonitor, dbAdmin, dbAdminAnyDatabase, enableSharding, read, readAnyDatabase, readWrite, readWriteAnyDatabase.`}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`provider.roles.${i}.roleName`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
ariaLabel="delete key"
|
||||
className="bottom-0.5 h-9"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const roles = getValues("provider.roles");
|
||||
if (roles && roles?.length > 1) {
|
||||
roleFields.remove(i);
|
||||
} else {
|
||||
setValue("provider.roles", [{ databaseName: "", roleName: "" }]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => roleFields.append({ databaseName: "", roleName: "" })}
|
||||
>
|
||||
Add Role
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="advance-section">
|
||||
<AccordionTrigger>Advanced</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<FormLabel
|
||||
label="Scopes"
|
||||
isOptional
|
||||
className="mb-2"
|
||||
tooltipClassName="max-w-md whitespace-pre-line"
|
||||
tooltipText="List that contains clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances that this database user can access. If omitted, MongoDB Cloud grants the database user access to all the clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances in the project."
|
||||
/>
|
||||
<div className="mb-2 flex flex-col space-y-2">
|
||||
{scopeFields.fields.map(({ id: scopeFieldId }, i) => (
|
||||
<div key={scopeFieldId} className="flex items-end space-x-2">
|
||||
<div className="flex-grow">
|
||||
{i === 0 && (
|
||||
<FormLabel
|
||||
label="Label"
|
||||
className="text-xs text-mineshaft-400"
|
||||
tooltipClassName="max-w-md whitespace-pre-line"
|
||||
tooltipText="Human-readable label that identifies the cluster or MongoDB Atlas Data Lake that this database user can access."
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`provider.scopes.${i}.name`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Input {...field} placeholder="Cluster or data lake id" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
{i === 0 && <span className="text-xs text-mineshaft-400">Type</span>}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`provider.scopes.${i}.type`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{ATLAS_SCOPE_TYPES.map((el) => (
|
||||
<SelectItem value={el.value} key={`atlas-scope-${el.value}`}>
|
||||
{el.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
ariaLabel="delete key"
|
||||
className="bottom-0.5 h-9"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
scopeFields.remove(i);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() =>
|
||||
scopeFields.append({ name: "", type: ATLAS_SCOPE_TYPES[0].value })
|
||||
}
|
||||
>
|
||||
Add Scope
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+20
-1
@@ -57,7 +57,8 @@ const OutputDisplay = ({
|
||||
const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
|
||||
if (
|
||||
provider === DynamicSecretProviders.SqlDatabase ||
|
||||
provider === DynamicSecretProviders.Cassandra
|
||||
provider === DynamicSecretProviders.Cassandra ||
|
||||
provider === DynamicSecretProviders.MongoAtlas
|
||||
) {
|
||||
const { DB_PASSWORD, DB_USERNAME } = data as { DB_USERNAME: string; DB_PASSWORD: string };
|
||||
return (
|
||||
@@ -138,6 +139,24 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (provider === DynamicSecretProviders.ElasticSearch) {
|
||||
const { DB_USERNAME, DB_PASSWORD } = data as {
|
||||
DB_USERNAME: string;
|
||||
DB_PASSWORD: string;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<OutputDisplay label="Username" value={DB_USERNAME} />
|
||||
<OutputDisplay
|
||||
label="Password"
|
||||
value={DB_PASSWORD}
|
||||
helperText="Important: Copy these credentials now. You will not be able to see them again after you close the modal."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
+429
@@ -0,0 +1,429 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Link from "next/link";
|
||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const authMethods = [
|
||||
{
|
||||
label: "Username/Password",
|
||||
value: "user"
|
||||
},
|
||||
{
|
||||
label: "API Key",
|
||||
value: "api-key"
|
||||
}
|
||||
] as const;
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z.object({
|
||||
host: z.string().trim().min(1),
|
||||
port: z.coerce.number(),
|
||||
|
||||
// two auth types "user, apikey"
|
||||
auth: z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("user"),
|
||||
username: z.string().trim(),
|
||||
password: z.string().trim()
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("api-key"),
|
||||
apiKey: z.string().trim(),
|
||||
apiKeyId: z.string().trim()
|
||||
})
|
||||
]),
|
||||
|
||||
roles: z.array(z.string().trim().min(1)).min(1, "At least one role is required"),
|
||||
ca: z.string().optional()
|
||||
}),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
dynamicSecret: TDynamicSecret & { inputs: unknown };
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
projectSlug: string;
|
||||
};
|
||||
|
||||
export const EditDynamicSecretElasticSearchForm = ({
|
||||
onClose,
|
||||
dynamicSecret,
|
||||
secretPath,
|
||||
environment,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isLoading) return;
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
path: secretPath,
|
||||
projectSlug,
|
||||
environmentSlug: environment,
|
||||
data: {
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated dynamic secret"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const selectedAuthType = watch("inputs.auth.type");
|
||||
const selectedRoles = watch("inputs.roles");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="newName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="dynamic-secret" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
defaultValue="1h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
defaultValue="24h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.host"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Host"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.port"
|
||||
defaultValue={443}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Port"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.auth.type"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Authentication Method"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="w-full"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
const authFields = [
|
||||
"inputs.auth.username",
|
||||
"inputs.auth.password",
|
||||
"inputs.auth.apiKey",
|
||||
"inputs.auth.apiKeyId"
|
||||
] as const;
|
||||
|
||||
authFields.forEach((f) => {
|
||||
setValue(f, "");
|
||||
});
|
||||
|
||||
field.onChange(e);
|
||||
}}
|
||||
>
|
||||
{authMethods.map((authType) => (
|
||||
<SelectItem value={authType.value} key={`auth-method-${authType.value}`}>
|
||||
{authType.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={
|
||||
selectedAuthType === "user" ? "inputs.auth.username" : "inputs.auth.apiKeyId"
|
||||
}
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={selectedAuthType === "user" ? "Username" : "API Key ID"}
|
||||
className="w-full"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={selectedAuthType === "user" ? "inputs.auth.password" : "inputs.auth.apiKey"}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={selectedAuthType === "user" ? "Password" : "API Key"}
|
||||
className="w-full"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative mb-3 flex flex-col">
|
||||
<FormLabel
|
||||
label="Roles"
|
||||
tooltipText={
|
||||
<div className="space-y-4">
|
||||
<p>Select which role(s) to assign the users provisioned by Infisical.</p>
|
||||
<p>
|
||||
There is a wide range of in-built roles in Elastic Search. Some include,
|
||||
superuser, apm_user, kibana_admin, monitoring_user, and many more. You can{" "}
|
||||
<Link
|
||||
passHref
|
||||
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-roles.html"
|
||||
>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<span className="cursor-pointer text-primary-400">
|
||||
read more about roles here
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
You can also assign custom roles by providing the name of the custom role in
|
||||
the input field.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{selectedRoles.map((_, i) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`inputs.roles.${i}`}
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`role-${i}`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-grow">
|
||||
<FormControl isError={Boolean(error?.message)} errorText={error?.message}>
|
||||
<Input
|
||||
placeholder="Insert role name, (superuser, kibana_admin, custom_role)"
|
||||
className="mb-0 flex-grow"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
isDisabled={selectedRoles.length === 1}
|
||||
ariaLabel="delete key"
|
||||
className="bottom-2 h-9"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (selectedRoles && selectedRoles?.length > 1) {
|
||||
setValue(
|
||||
"inputs.roles",
|
||||
selectedRoles.filter((__, idx) => idx !== i)
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
className="bottom-2"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
setValue("inputs.roles", [...selectedRoles, ""]);
|
||||
}}
|
||||
>
|
||||
Add Role
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.ca"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isOptional
|
||||
label="CA (SSL)"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+37
@@ -7,6 +7,8 @@ import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { EditDynamicSecretAwsElastiCacheProviderForm } from "./EditDynamicSecretAwsElastiCacheProviderForm";
|
||||
import { EditDynamicSecretAwsIamForm } from "./EditDynamicSecretAwsIamForm";
|
||||
import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm";
|
||||
import { EditDynamicSecretElasticSearchForm } from "./EditDynamicSecretElasticSearchForm";
|
||||
import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm";
|
||||
import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProviderForm";
|
||||
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
|
||||
|
||||
@@ -128,6 +130,41 @@ export const EditDynamicSecretForm = ({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{dynamicSecretDetails?.type === DynamicSecretProviders.MongoAtlas && (
|
||||
<motion.div
|
||||
key="mongo-atlas-edit"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<EditDynamicSecretMongoAtlasForm
|
||||
onClose={onClose}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
dynamicSecret={dynamicSecretDetails}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{dynamicSecretDetails?.type === DynamicSecretProviders.ElasticSearch && (
|
||||
<motion.div
|
||||
key="elastic-search-edit"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<EditDynamicSecretElasticSearchForm
|
||||
onClose={onClose}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
dynamicSecret={dynamicSecretDetails}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
+466
@@ -0,0 +1,466 @@
|
||||
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 ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
.object({
|
||||
adminPublicKey: z.string().trim().min(1),
|
||||
adminPrivateKey: z.string().trim().min(1),
|
||||
groupId: z.string().trim().min(1),
|
||||
roles: z
|
||||
.object({
|
||||
collectionName: z.string().optional(),
|
||||
databaseName: z.string().min(1),
|
||||
roleName: z.string().min(1)
|
||||
})
|
||||
.array()
|
||||
.min(1),
|
||||
scopes: z
|
||||
.object({
|
||||
name: z.string().min(1),
|
||||
type: z.string().min(1)
|
||||
})
|
||||
.array()
|
||||
})
|
||||
.partial(),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
dynamicSecret: TDynamicSecret & { inputs: unknown };
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
projectSlug: string;
|
||||
};
|
||||
|
||||
const ATLAS_SCOPE_TYPES = [
|
||||
{
|
||||
label: "Cluster",
|
||||
value: "CLUSTER"
|
||||
},
|
||||
{
|
||||
label: "Data Lake",
|
||||
value: "DATA_LAKE"
|
||||
},
|
||||
{
|
||||
label: "Stream",
|
||||
value: "STREAM"
|
||||
}
|
||||
];
|
||||
|
||||
export const EditDynamicSecretMongoAtlasForm = ({
|
||||
onClose,
|
||||
dynamicSecret,
|
||||
environment,
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
getValues,
|
||||
setValue,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const roleFields = useFieldArray({
|
||||
control,
|
||||
name: "inputs.roles"
|
||||
});
|
||||
|
||||
const scopeFields = useFieldArray({
|
||||
control,
|
||||
name: "inputs.scopes"
|
||||
});
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isLoading) return;
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
path: secretPath,
|
||||
projectSlug,
|
||||
environmentSlug: environment,
|
||||
data: {
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated dynamic secret"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
name="newName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="DYN-1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.adminPublicKey"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Admin Public Key"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.adminPrivateKey"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Admin Private Key"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.groupId"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Group/Project ID"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="Unique 24-hexadecimal digit string that identifies your project"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormLabel label="Roles" />
|
||||
<div className="mb-3 flex flex-col space-y-2">
|
||||
{roleFields.fields.map(({ id: roleFieldId }, i) => (
|
||||
<div key={roleFieldId} className="flex items-end space-x-2">
|
||||
<div className="flex-grow">
|
||||
{i === 0 && <span className="text-xs text-mineshaft-400">Database Name</span>}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`inputs.roles.${i}.databaseName`}
|
||||
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="Collection Name"
|
||||
className="text-xs text-mineshaft-400"
|
||||
isOptional
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`inputs.roles.${i}.collectionName`}
|
||||
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="Role"
|
||||
className="text-xs text-mineshaft-400"
|
||||
tooltipClassName="max-w-md whitespace-pre-line"
|
||||
tooltipText={`Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.
|
||||
Built-in: atlasAdmin, backup, clusterMonitor, dbAdmin, dbAdminAnyDatabase, enableSharding, read, readAnyDatabase, readWrite, readWriteAnyDatabase.`}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`inputs.roles.${i}.roleName`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
ariaLabel="delete key"
|
||||
className="bottom-0.5 h-9"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const roles = getValues("inputs.roles");
|
||||
if (roles && roles?.length > 1) {
|
||||
roleFields.remove(i);
|
||||
} else {
|
||||
setValue("inputs.roles", [{ databaseName: "", roleName: "" }]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => roleFields.append({ databaseName: "", roleName: "" })}
|
||||
>
|
||||
Add Role
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="advance-section">
|
||||
<AccordionTrigger>Advanced</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<FormLabel
|
||||
label="Scopes"
|
||||
isOptional
|
||||
className="mb-2"
|
||||
tooltipClassName="max-w-md whitespace-pre-line"
|
||||
tooltipText="List that contains clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances that this database user can access. If omitted, MongoDB Cloud grants the database user access to all the clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances in the project."
|
||||
/>
|
||||
<div className="mb-2 flex flex-col space-y-2">
|
||||
{scopeFields.fields.map(({ id: scopeFieldId }, i) => (
|
||||
<div key={scopeFieldId} className="flex items-end space-x-2">
|
||||
<div className="flex-grow">
|
||||
{i === 0 && (
|
||||
<FormLabel
|
||||
label="Label"
|
||||
className="text-xs text-mineshaft-400"
|
||||
tooltipClassName="max-w-md whitespace-pre-line"
|
||||
tooltipText="Human-readable label that identifies the cluster or MongoDB Atlas Data Lake that this database user can access."
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`inputs.scopes.${i}.name`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Input {...field} placeholder="Cluster or data lake id" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
{i === 0 && <span className="text-xs text-mineshaft-400">Type</span>}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`inputs.scopes.${i}.type`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{ATLAS_SCOPE_TYPES.map((el) => (
|
||||
<SelectItem value={el.value} key={`atlas-scope-${el.value}`}>
|
||||
{el.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
ariaLabel="delete key"
|
||||
className="bottom-0.5 h-9"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
scopeFields.remove(i);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() =>
|
||||
scopeFields.append({ name: "", type: ATLAS_SCOPE_TYPES[0].value })
|
||||
}
|
||||
>
|
||||
Add Scope
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -32,7 +32,8 @@ const viewLimitOptions = [
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().optional(),
|
||||
secret: z.string(),
|
||||
password: z.string().optional(),
|
||||
secret: z.string().min(1),
|
||||
expiresIn: z.string(),
|
||||
viewLimit: z.string(),
|
||||
accessType: z.nativeEnum(SecretSharingAccessType).optional()
|
||||
@@ -67,7 +68,14 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
||||
}
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ name, secret, expiresIn, viewLimit, accessType }: FormData) => {
|
||||
const onFormSubmit = async ({
|
||||
name,
|
||||
password,
|
||||
secret,
|
||||
expiresIn,
|
||||
viewLimit,
|
||||
accessType
|
||||
}: FormData) => {
|
||||
try {
|
||||
const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
|
||||
|
||||
@@ -80,6 +88,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
||||
|
||||
const { id } = await createSharedSecret.mutateAsync({
|
||||
name,
|
||||
password,
|
||||
encryptedValue: ciphertext,
|
||||
hashedHex,
|
||||
iv,
|
||||
@@ -149,6 +158,20 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isOptional
|
||||
>
|
||||
<Input {...field} placeholder="Password" type="password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresIn"
|
||||
|
||||
@@ -1,26 +1,43 @@
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing";
|
||||
|
||||
import { SecretContainer, SecretErrorContainer } from "./components";
|
||||
import { PasswordContainer,SecretContainer, SecretErrorContainer } from "./components";
|
||||
|
||||
export const ViewSecretPublicPage = () => {
|
||||
const router = useRouter();
|
||||
const [password, setPassword] = useState<string>();
|
||||
const { id, key: urlEncodedPublicKey } = router.query;
|
||||
|
||||
const [hashedHex, key] = urlEncodedPublicKey
|
||||
? urlEncodedPublicKey.toString().split("-")
|
||||
: ["", ""];
|
||||
|
||||
const { data: secret, error } = useGetActiveSharedSecretById({
|
||||
const {
|
||||
data: fetchSecret,
|
||||
error,
|
||||
isLoading,
|
||||
isFetching
|
||||
} = useGetActiveSharedSecretById({
|
||||
sharedSecretId: id as string,
|
||||
hashedHex
|
||||
hashedHex,
|
||||
password
|
||||
});
|
||||
|
||||
const isInvalidCredential =
|
||||
((error as AxiosError)?.response?.data as { message: string })?.message ===
|
||||
"Invalid credentials";
|
||||
|
||||
const shouldShowPasswordPrompt =
|
||||
isInvalidCredential || (fetchSecret?.isPasswordProtected && !fetchSecret.secret);
|
||||
const isValidatingPassword = Boolean(password) && isFetching;
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col justify-between overflow-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
|
||||
<div />
|
||||
@@ -52,8 +69,23 @@ export const ViewSecretPublicPage = () => {
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{secret && key && <SecretContainer secret={secret} secretKey={key} />}
|
||||
{error && <SecretErrorContainer />}
|
||||
{(shouldShowPasswordPrompt || isValidatingPassword) && (
|
||||
<PasswordContainer
|
||||
isSubmitting={isValidatingPassword}
|
||||
onPasswordSubmit={(el) => {
|
||||
setPassword(el);
|
||||
}}
|
||||
isInvalidCredential={!isFetching && isInvalidCredential}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && (
|
||||
<>
|
||||
{!error && fetchSecret?.secret && key && (
|
||||
<SecretContainer secret={fetchSecret.secret} secretKey={key} />
|
||||
)}
|
||||
{error && !isInvalidCredential && <SecretErrorContainer />}
|
||||
</>
|
||||
)}
|
||||
<div className="m-auto my-8 flex w-full">
|
||||
<div className="w-full border-t border-mineshaft-600" />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faArrowRight, faSpinner } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
onPasswordSubmit: (val: any) => void;
|
||||
isSubmitting?: boolean;
|
||||
isInvalidCredential?: boolean;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
password: z.string()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const PasswordContainer = ({
|
||||
onPasswordSubmit,
|
||||
isSubmitting,
|
||||
isInvalidCredential
|
||||
}: Props) => {
|
||||
const { control, handleSubmit } = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ password }: FormData) => {
|
||||
onPasswordSubmit(password);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error) || isInvalidCredential}
|
||||
errorText={isInvalidCredential ? "Invalid credential" : error?.message}
|
||||
isRequired
|
||||
label="Password"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 rounded-md">
|
||||
<Input {...field} placeholder="Enter Password to view secret" type="password" />
|
||||
<div className="flex">
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className={isSubmitting ? "fa-spin" : ""}
|
||||
icon={isSubmitting ? faSpinner : faArrowRight}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
<Button
|
||||
className="w-full bg-mineshaft-700 py-3 text-bunker-200"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
onClick={() => window.open("https://app.infisical.com/share-secret", "_blank")}
|
||||
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="pl-2" />}
|
||||
>
|
||||
Share your own secret
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -14,7 +14,7 @@ import { useTimedReset, useToggle } from "@app/hooks";
|
||||
import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing";
|
||||
|
||||
type Props = {
|
||||
secret: TViewSharedSecretResponse;
|
||||
secret: TViewSharedSecretResponse["secret"];
|
||||
secretKey: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { PasswordContainer } from "./PasswordContainer";
|
||||
export { SecretContainer } from "./SecretContainer";
|
||||
export { SecretErrorContainer } from "./SecretErrorContainer";
|
||||
|
||||
Reference in New Issue
Block a user