Merge remote-tracking branch 'origin/main' into feat/camunda-app-connection-and-secret-sync

This commit is contained in:
Sheen Capadngan
2025-04-11 21:53:56 +08:00
152 changed files with 4896 additions and 670 deletions

View File

@@ -23,6 +23,7 @@
"@hcaptcha/react-hcaptcha": "^1.11.0",
"@headlessui/react": "^1.7.19",
"@hookform/resolvers": "^3.9.1",
"@lexical/react": "^0.29.0",
"@lottiefiles/dotlottie-react": "^0.12.0",
"@octokit/rest": "^21.0.2",
"@peculiar/x509": "^1.12.3",
@@ -66,6 +67,7 @@
"jspdf": "^2.5.2",
"jsrp": "^0.2.4",
"jwt-decode": "^4.0.0",
"lexical": "^0.29.0",
"ms": "^2.1.3",
"nprogress": "^0.2.0",
"picomatch": "^4.0.2",
@@ -1570,6 +1572,260 @@
}
}
},
"node_modules/@lexical/clipboard": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.29.0.tgz",
"integrity": "sha512-llxZosYCwH13p2GfPfhAinukdvAZYxWuwf5md107X80hsE8TQJj25unjqTwRKQ+w/wD+hpmBMziU8+K/WTitWQ==",
"license": "MIT",
"dependencies": {
"@lexical/html": "0.29.0",
"@lexical/list": "0.29.0",
"@lexical/selection": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/code": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.29.0.tgz",
"integrity": "sha512-yKGzoKpyIO39Xf7OKLPpoCE5V8mTDCM3l3CDHZR3X1gM/VZQzf4jAiO3b06y9YkQ2fM8kqwchYu87wGvs8/iIQ==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.29.0",
"lexical": "0.29.0",
"prismjs": "^1.30.0"
}
},
"node_modules/@lexical/devtools-core": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.29.0.tgz",
"integrity": "sha512-uUq0m9ql/7mthp7Ho1vnG7Id6imQ5kD5mxUhX2lmgHretS+yAHGsGsGiPIVHdPWeVmUb2n4IVDJ+cJbUsUjQJw==",
"license": "MIT",
"dependencies": {
"@lexical/html": "0.29.0",
"@lexical/link": "0.29.0",
"@lexical/mark": "0.29.0",
"@lexical/table": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
},
"peerDependencies": {
"react": ">=17.x",
"react-dom": ">=17.x"
}
},
"node_modules/@lexical/dragon": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.29.0.tgz",
"integrity": "sha512-Zaky2jd/Pp1blAZqPeGNdyhxnVL4lwVjbWPxhfS1gbW4Q5CBQ3aD3B0T4ljiKfmRNJm004LJ9q7KjhlRbREvZA==",
"license": "MIT",
"dependencies": {
"lexical": "0.29.0"
}
},
"node_modules/@lexical/hashtag": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.29.0.tgz",
"integrity": "sha512-fa7s0Yi2RKz/GvgT5XU9fborx6VPU3VtvvEPaIXgyd6zXZRiOhD9rGypwB3oj4fMK1ndx2dX0m7SwhMJo48D8w==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/history": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.29.0.tgz",
"integrity": "sha512-OrCwZycp/yaq63mw511NutkwAB+W6WSchG1xTxlLh6nbc8jnbvKhCf4CGbnrvlhD7hTuzxJ8FI9/2M/2zv/mNQ==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/html": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.29.0.tgz",
"integrity": "sha512-+jV6ijppOpxpUGeXkGssXJbsAmFALfeLrgbM0xuZbxZ7RgYZ+5Atn00WjSno7+JV5EOuRkYmCNtS1tiHtXMY1g==",
"license": "MIT",
"dependencies": {
"@lexical/selection": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/link": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.29.0.tgz",
"integrity": "sha512-wGbKRF0x/6ZQHuCfr8m8qD1J0R1kFmWINBG2A1hUXPDf7UY5qm/nS2oKNDGpjiDMGwkVZ7n7WfzeBGO+KRe/Lg==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/list": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.29.0.tgz",
"integrity": "sha512-sWiof+i2ff8rL7KxJ3dxHLwyJfX423e1EVLmAdQEOPhyZJiNbeLTSNhNGsZ8FjFoBwvTTEDwuQZm3iT3hliKOg==",
"license": "MIT",
"dependencies": {
"@lexical/selection": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/mark": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.29.0.tgz",
"integrity": "sha512-UB3x6pyUdpZHRqF4tiajLnC1+Umvt7x8Rkkdi29aNNvzIWniVwGkBOlmvFus7x+4dOV1D1fydwiP4m38nGgLDw==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/markdown": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.29.0.tgz",
"integrity": "sha512-4Od8WoDoviv9DxJZVgrIORTIAzyoGOpztbGbIBXguGmwvy7NnHQDh9fZYIYRrdI1Awp1VVGdJ3ku/7KTgSOoRw==",
"license": "MIT",
"dependencies": {
"@lexical/code": "0.29.0",
"@lexical/link": "0.29.0",
"@lexical/list": "0.29.0",
"@lexical/rich-text": "0.29.0",
"@lexical/text": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/offset": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.29.0.tgz",
"integrity": "sha512-VyD2Ff3rBJpo++Fxvi3MNYmDELa+9nA0EgXqGRNb3MvRehRjHbaDbymtLMMHIwvbkF5lnra+ubStcTRQmoQxXw==",
"license": "MIT",
"dependencies": {
"lexical": "0.29.0"
}
},
"node_modules/@lexical/overflow": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.29.0.tgz",
"integrity": "sha512-IzH3M652Ej2gB2sK65N3yTgyiQAa3I3tqKbSnBRiXu/+isxHoCy/qRr9/kL63uy7zhGvgV+EYsoffQCawIFt8Q==",
"license": "MIT",
"dependencies": {
"lexical": "0.29.0"
}
},
"node_modules/@lexical/plain-text": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.29.0.tgz",
"integrity": "sha512-F5C3meDb2HmO0NmKJBVRkjmX9PNln6O1jXU/APJuSFBdvfcIWSY58ncHR4zy2M5LF1Q5PQMWyIay9p+SqOtY5A==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.29.0",
"@lexical/selection": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/react": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.29.0.tgz",
"integrity": "sha512-YMlnljW/jxmwSzsRv5UPatfOoMZXqxFmRIEltTUIQfrOFdqn+ssUtCpjE6xRD1oxD6KpSIekakzLs+y/8+7CuQ==",
"license": "MIT",
"dependencies": {
"@lexical/devtools-core": "0.29.0",
"@lexical/dragon": "0.29.0",
"@lexical/hashtag": "0.29.0",
"@lexical/history": "0.29.0",
"@lexical/link": "0.29.0",
"@lexical/list": "0.29.0",
"@lexical/mark": "0.29.0",
"@lexical/markdown": "0.29.0",
"@lexical/overflow": "0.29.0",
"@lexical/plain-text": "0.29.0",
"@lexical/rich-text": "0.29.0",
"@lexical/table": "0.29.0",
"@lexical/text": "0.29.0",
"@lexical/utils": "0.29.0",
"@lexical/yjs": "0.29.0",
"lexical": "0.29.0",
"react-error-boundary": "^3.1.4"
},
"peerDependencies": {
"react": ">=17.x",
"react-dom": ">=17.x"
}
},
"node_modules/@lexical/rich-text": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.29.0.tgz",
"integrity": "sha512-fSKgXGxJUOWo7dwSTUYFVBNNk4pPN8norsZfdmKM1kGDS1/GKuVzlzHLKZ7rQb8RLD5a43p4ifEL+28P+q0Qqg==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.29.0",
"@lexical/selection": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/selection": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.29.0.tgz",
"integrity": "sha512-lX9CRrXgKte65cozTHFXwUJ2fvZD92OEtos+YU+U40GJjf3NdheGeKDxDfOpF4AXrYRSszY7E0CzmIvuEs0p4A==",
"license": "MIT",
"dependencies": {
"lexical": "0.29.0"
}
},
"node_modules/@lexical/table": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.29.0.tgz",
"integrity": "sha512-Jdj32kBDeJh/0dGaZB14JggnEIS956/cN7grnLr7cmhhVzDicvLMBENSXQVEJAQVcSIU4G9EvxC7GJZ9VgqDnA==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.29.0",
"@lexical/utils": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/text": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.29.0.tgz",
"integrity": "sha512-QnNGr6ickTLk76o3PdxJjPwt//dpuh8idVfR73WdCIoAwkhiEPUxxTZERoMsudXj6O/lJ+/HhI61wVjLckYr3A==",
"license": "MIT",
"dependencies": {
"lexical": "0.29.0"
}
},
"node_modules/@lexical/utils": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.29.0.tgz",
"integrity": "sha512-y2hhWQDjcXdplsAaQMuZx6ht9u1I4BV5NynA+WKoQ3h8vKxzeDnpCxVOK/zxU1R5dhM/nilnFu7uhvrSeEn+TQ==",
"license": "MIT",
"dependencies": {
"@lexical/list": "0.29.0",
"@lexical/selection": "0.29.0",
"@lexical/table": "0.29.0",
"lexical": "0.29.0"
}
},
"node_modules/@lexical/yjs": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.29.0.tgz",
"integrity": "sha512-6IXWWlGkVJEzWP/+LcuKYJ9jmcFp8k7TT/jmz4V5gBD9Ut3swOGsIA/sQCtB9y7jad10csaDVmFdFzGNWKVH9A==",
"license": "MIT",
"dependencies": {
"@lexical/offset": "0.29.0",
"@lexical/selection": "0.29.0",
"lexical": "0.29.0"
},
"peerDependencies": {
"yjs": ">=13.5.22"
}
},
"node_modules/@lottiefiles/dotlottie-react": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.12.0.tgz",
@@ -8871,6 +9127,17 @@
"node": ">=10"
}
},
"node_modules/isomorphic.js": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/iterator.prototype": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz",
@@ -9100,6 +9367,34 @@
"node": ">= 0.8.0"
}
},
"node_modules/lexical": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/lexical/-/lexical-0.29.0.tgz",
"integrity": "sha512-eoBHUEn0LmExKeK6x2cFKU0FPaMk2Bc5HgiCzTiv5ymKtwWw7LeKcxaNPmLxRRdQpcWV1IMKjayAbw7Lt/Gu7w==",
"license": "MIT"
},
"node_modules/lib0": {
"version": "0.2.102",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.102.tgz",
"integrity": "sha512-g70kydI0I1sZU0ChO8mBbhw0oUW/8U0GHzygpvEIx8k+jgOpqnTSb/E+70toYVqHxBhrERD21TwD5QcZJQ40ZQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"isomorphic.js": "^0.2.4"
},
"bin": {
"0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
"0gentesthtml": "bin/gentesthtml.js",
"0serve": "bin/0serve.js"
},
"engines": {
"node": ">=16"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -10857,6 +11152,15 @@
}
}
},
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -11142,6 +11446,22 @@
"react": "^18.3.1"
}
},
"node_modules/react-error-boundary": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
"integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"engines": {
"node": ">=10",
"npm": ">=6"
},
"peerDependencies": {
"react": ">=16.13.1"
}
},
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
@@ -13587,9 +13907,9 @@
}
},
"node_modules/vite": {
"version": "5.4.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
"version": "5.4.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.16.tgz",
"integrity": "sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14131,6 +14451,24 @@
"node": ">=8"
}
},
"node_modules/yjs": {
"version": "13.6.24",
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.24.tgz",
"integrity": "sha512-xn/pYLTZa3uD1uDG8lpxfLRo5SR/rp0frdASOl2a71aYNvUXdWcLtVL91s2y7j+Q8ppmjZ9H3jsGVgoFMbT2VA==",
"license": "MIT",
"peer": true,
"dependencies": {
"lib0": "^0.2.99"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -27,6 +27,7 @@
"@hcaptcha/react-hcaptcha": "^1.11.0",
"@headlessui/react": "^1.7.19",
"@hookform/resolvers": "^3.9.1",
"@lexical/react": "^0.29.0",
"@lottiefiles/dotlottie-react": "^0.12.0",
"@octokit/rest": "^21.0.2",
"@peculiar/x509": "^1.12.3",
@@ -70,6 +71,7 @@
"jspdf": "^2.5.2",
"jsrp": "^0.2.4",
"jwt-decode": "^4.0.0",
"lexical": "^0.29.0",
"ms": "^2.1.3",
"nprogress": "^0.2.0",
"picomatch": "^4.0.2",

View File

@@ -12,6 +12,7 @@ import { DatabricksSyncFields } from "./DatabricksSyncFields";
import { GcpSyncFields } from "./GcpSyncFields";
import { GitHubSyncFields } from "./GitHubSyncFields";
import { HumanitecSyncFields } from "./HumanitecSyncFields";
import { VercelSyncFields } from "./VercelSyncFields";
export const SecretSyncDestinationFields = () => {
const { watch } = useFormContext<TSecretSyncForm>();
@@ -37,6 +38,8 @@ export const SecretSyncDestinationFields = () => {
return <HumanitecSyncFields />;
case SecretSync.Camunda:
return <CamundaSyncFields />;
case SecretSync.Vercel:
return <VercelSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}

View File

@@ -0,0 +1,195 @@
import { useMemo } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
import {
TVercelConnectionApp,
useVercelConnectionListOrganizations
} from "@app/hooks/api/appConnections/vercel";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
const vercelEnvironments = [
{ name: "Development", slug: "development" },
{ name: "Preview", slug: "preview" },
{ name: "Production", slug: "production" }
];
export const VercelSyncFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Vercel }
>();
const connectionId = useWatch({ name: "connection.id", control });
const currentApp = watch("destinationConfig.app");
const currentEnv = watch("destinationConfig.env");
const { data: projects, isLoading: isProjectsLoading } = useVercelConnectionListOrganizations(
connectionId,
{
enabled: Boolean(connectionId)
}
);
const selectedProject = projects
?.find((project) => project.apps.some((app) => app.id === currentApp))
?.apps.find((app) => app.id === currentApp);
const allApps =
projects?.flatMap((project) =>
project.apps.map((app) => ({ ...app, project: project.name, projectId: project.id }))
) || [];
const environmentOptions = useMemo(() => {
return vercelEnvironments
.map((env) => ({
key: env.slug,
type: env.slug,
name: env.name
}))
.concat(
selectedProject?.envs?.map((env) => ({
key: env.id,
type: env.type,
name: env.slug
})) || []
);
}, [currentApp]);
const previewBranchOptions =
selectedProject?.previewBranches?.map((branch) => ({
id: branch,
name: branch
})) || [];
const isPreviewEnvironment = currentEnv === "preview";
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.app", "");
setValue("destinationConfig.appName", "");
setValue("destinationConfig.env", "production");
setValue("destinationConfig.branch", "");
}}
/>
<Controller
name="destinationConfig.app"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vercel App"
helperText={
<Tooltip
className="max-w-md"
content="Ensure the project exists and the API token scope for this connection includes the desired project."
>
<div>
<span>Don&#39;t see the project you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isProjectsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={allApps.find((app) => app.id === value) ?? null}
onChange={(option) => {
const appId = (option as SingleValue<TVercelConnectionApp>)?.id ?? null;
onChange(appId);
setValue("destinationConfig.branch", "");
setValue(
"destinationConfig.teamId",
(option as SingleValue<TVercelConnectionApp>)?.projectId || ""
);
setValue(
"destinationConfig.appName",
(option as SingleValue<TVercelConnectionApp>)?.name || ""
);
}}
options={allApps}
placeholder="Select a project..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id.toString()}
groupBy="project"
/>
</FormControl>
)}
/>
<Controller
name="destinationConfig.env"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vercel App Environment"
>
<FilterableSelect
menuPlacement="top"
isDisabled={!connectionId || !currentApp}
value={
value
? {
key: environmentOptions.find((env) => env.key === value)?.key,
type: environmentOptions.find((env) => env.key === value)?.type,
name: environmentOptions.find((env) => env.key === value)?.name
}
: null
}
onChange={(option) => {
const envKey = (option as any)?.key ?? null;
onChange(envKey);
setValue("destinationConfig.branch", "");
}}
options={environmentOptions}
placeholder="Select an environment..."
getOptionLabel={(option) => option.name || option.key || ""}
getOptionValue={(option) => option.key || ""}
/>
</FormControl>
)}
/>
{isPreviewEnvironment && (
<Controller
name="destinationConfig.branch"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vercel Preview Branch (Optional)"
>
<FilterableSelect
menuPlacement="top"
isLoading={isProjectsLoading && Boolean(connectionId) && Boolean(currentApp)}
isDisabled={!connectionId || !currentApp}
value={previewBranchOptions.find((branch) => branch.id === value) ?? null}
onChange={(option) => onChange((option as SingleValue<{ id: string }>)?.id || "")}
options={previewBranchOptions}
placeholder="Select a branch..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option?.id || ""}
isClearable
/>
</FormControl>
)}
/>
)}
</>
);
};

View File

@@ -40,6 +40,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Databricks:
case SecretSync.Humanitec:
case SecretSync.Camunda:
case SecretSync.Vercel:
AdditionalSyncOptionsFieldsComponent = null;
break;
default:

View File

@@ -22,6 +22,7 @@ import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { VercelSyncReviewFields } from "./VercelSyncReviewFields";
export const SecretSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm>();
@@ -76,6 +77,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.Camunda:
DestinationFieldsComponent = <CamundaSyncReviewFields />;
break;
case SecretSync.Vercel:
DestinationFieldsComponent = <VercelSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}

View File

@@ -0,0 +1,23 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { VercelEnvironmentType } from "@app/hooks/api/secretSyncs/types/vercel-sync";
export const VercelSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Vercel }>();
const envId = watch("destinationConfig.env");
const branchId = watch("destinationConfig.branch");
const appName = watch("destinationConfig.appName");
return (
<>
<GenericFieldLabel label="Vercel App">{appName}</GenericFieldLabel>
<GenericFieldLabel label="Environment">{envId}</GenericFieldLabel>
{envId === VercelEnvironmentType.Preview && branchId && (
<GenericFieldLabel label="Preview Branch">{branchId}</GenericFieldLabel>
)}
</>
);
};

View File

@@ -10,6 +10,7 @@ import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-desti
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema";
import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema";
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema,
@@ -20,7 +21,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AzureAppConfigurationSyncDestinationSchema,
DatabricksSyncDestinationSchema,
HumanitecSyncDestinationSchema,
CamundaSyncDestinationSchema
CamundaSyncDestinationSchema,
VercelSyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema;

View File

@@ -0,0 +1,18 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { VercelEnvironmentType } from "@app/hooks/api/secretSyncs/types/vercel-sync";
export const VercelSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.Vercel),
destinationConfig: z.object({
app: z.string().trim().min(1, "Project required"),
appName: z.string().trim().min(1, "Project required"),
env: z.nativeEnum(VercelEnvironmentType).or(z.string()),
branch: z.string().trim().optional(),
teamId: z.string().trim()
})
})
);

View File

@@ -0,0 +1,159 @@
/* eslint-disable no-underscore-dangle */
import { forwardRef, InputHTMLAttributes } from "react";
import { InitialConfigType, LexicalComposer } from "@lexical/react/LexicalComposer";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
import { ReactNode } from "@tanstack/react-router";
import { cva, VariantProps } from "cva";
import { EditorState, LexicalEditor } from "lexical";
import { twMerge } from "tailwind-merge";
import { HighlightNode } from "./EditorHighlight";
import { EditorPlaceholderPlugin } from "./EditorPlaceholderPlugin";
// Catch any errors that occur during Lexical updates and log them
// or throw them as needed. If you don't throw them, Lexical will
// try to recover gracefully without losing user data.
function onError(error: Error) {
console.error(error);
}
const inputVariants = cva(
"input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-bunker-400/60 duration-100",
{
variants: {
size: {
xs: ["text-xs"],
sm: ["text-sm"],
md: ["text-md"],
lg: ["text-lg"]
},
isRounded: {
true: ["rounded-md"],
false: ""
},
variant: {
filled: ["bg-mineshaft-900", "text-gray-400"],
outline: ["bg-transparent"],
plain: "bg-transparent outline-none"
},
isError: {
true: "focus:ring-red/50 placeholder-red-300",
false: "focus:ring-primary-400/50 focus:ring-1"
}
},
compoundVariants: []
}
);
const inputParentContainerVariants = cva("inline-flex font-inter items-center border relative", {
variants: {
isRounded: {
true: ["rounded-md"],
false: ""
},
isError: {
true: "border-red",
false: "border-mineshaft-500"
},
isFullWidth: {
true: "w-full",
false: ""
},
variant: {
filled: ["bg-bunker-800", "text-gray-400"],
outline: ["bg-transparent"],
plain: "border-none"
}
}
});
type Props = Omit<
InputHTMLAttributes<HTMLDivElement>,
"size" | "onChange" | "placeholder" | "aria-placeholder"
> &
VariantProps<typeof inputVariants> & {
children?: ReactNode;
namespace?: string;
placeholder?: string;
isFullWidth?: boolean;
isRequired?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
isDisabled?: boolean;
isReadOnly?: boolean;
containerClassName?: string;
onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void;
initialValue?: string;
};
export const Editor = forwardRef<HTMLDivElement, Props>(
(
{
children,
namespace = "infisical-editor",
className,
containerClassName,
isRounded = true,
isFullWidth = true,
isDisabled,
isError = false,
isRequired,
leftIcon,
rightIcon,
variant = "filled",
size = "md",
isReadOnly,
placeholder,
onChange,
...props
},
ref
) => {
const initialConfig: InitialConfigType = {
namespace,
onError,
nodes: [HighlightNode]
};
return (
<div
className={inputParentContainerVariants({
isRounded,
isError,
isFullWidth,
variant,
className: containerClassName
})}
>
{leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>}
<LexicalComposer initialConfig={initialConfig}>
<PlainTextPlugin
contentEditable={
<ContentEditable
ref={ref}
aria-required={isRequired}
readOnly={isReadOnly}
disabled={isDisabled}
className={twMerge(
leftIcon ? "pl-10" : "pl-2.5",
rightIcon ? "pr-10" : "pr-2.5",
inputVariants({ className, isError, size, isRounded, variant })
)}
{...props}
placeholder={null}
/>
}
ErrorBoundary={LexicalErrorBoundary}
/>
<OnChangePlugin onChange={onChange} />
<EditorPlaceholderPlugin placeholder={placeholder} />
{children}
</LexicalComposer>
{rightIcon && <span className="absolute right-0 mr-3">{rightIcon}</span>}
</div>
);
}
);

View File

@@ -0,0 +1,127 @@
/* eslint-disable no-underscore-dangle,@typescript-eslint/class-methods-use-this */
import { useCallback, useEffect } from "react";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useLexicalTextEntity } from "@lexical/react/useLexicalTextEntity";
import {
$applyNodeReplacement,
EditorConfig,
LexicalNode,
SerializedTextNode,
Spread,
TextNode
} from "lexical";
type HighlightTheme = { contentClassName: string };
type Trigger = { startTrigger: string; endTrigger: string };
export type SerializedHighlightNode = Spread<
{
__highlightTheme: HighlightTheme;
__trigger: Trigger;
},
SerializedTextNode
>;
export class HighlightNode extends TextNode {
__highlightTheme: HighlightTheme;
__trigger: Trigger;
constructor(
text: string,
highlightTheme: HighlightTheme = {
contentClassName: "ph-no-capture text-yellow-200/80"
},
trigger: Trigger = { startTrigger: "${", endTrigger: "}" },
key?: string
) {
super(text, key);
this.__highlightTheme = highlightTheme;
this.__trigger = trigger;
}
static getType(): string {
return "highlight";
}
static clone(node: HighlightNode): HighlightNode {
return new HighlightNode(node.__text, node.__highlightTheme, node.__trigger, node.__key);
}
static importJSON(serializedNode: SerializedHighlightNode): HighlightNode {
return $applyNodeReplacement(new HighlightNode("")).updateFromJSON(serializedNode);
}
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
dom.style.cursor = "default";
dom.className = this.__highlightTheme.contentClassName;
return dom;
}
canInsertTextBefore(): boolean {
return false;
}
canInsertTextAfter(): boolean {
return false;
}
isTextEntity(): true {
return true;
}
}
export function $createKeywordNode(keyword: string = ""): HighlightNode {
return $applyNodeReplacement(new HighlightNode(keyword));
}
export function $isKeywordNode(node: LexicalNode | null | undefined): boolean {
return node instanceof HighlightNode;
}
type Props = {
contentClassName?: string;
startTrigger?: string;
endTrigger?: string;
};
export const EditorHighlightPlugin = ({
endTrigger = "}",
startTrigger = "${",
contentClassName = "ph-no-capture text-yellow-200/80"
}: Props) => {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!editor.hasNodes([HighlightNode])) {
throw new Error("HighlightsPlugin: HighlightsNode not registered on editor");
}
}, [editor]);
const createKeywordNode = useCallback((textNode: TextNode): HighlightNode => {
return $applyNodeReplacement(
new HighlightNode(
textNode.getTextContent(),
{ contentClassName },
{ startTrigger, endTrigger }
)
);
}, []);
const getKeywordMatch = useCallback((text: string) => {
for (let i = 0; i < text.length; i += 1) {
if (text.slice(i, i + 2) === startTrigger) {
const closingBracketIndex = text.indexOf(endTrigger, i + 2);
if (closingBracketIndex !== -1) {
return { start: i, end: closingBracketIndex + 1 };
}
return null;
}
}
return null;
}, []);
useLexicalTextEntity<HighlightNode>(getKeywordMatch, HighlightNode, createKeywordNode);
return null;
};

View File

@@ -0,0 +1,22 @@
import { useEffect } from "react";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useLexicalIsTextContentEmpty } from "@lexical/react/useLexicalIsTextContentEmpty";
export const EditorPlaceholderPlugin = ({ placeholder }: { placeholder: string | undefined }) => {
const [editor] = useLexicalComposerContext();
const isEmpty = useLexicalIsTextContentEmpty(editor);
/* Set the placeholder on root. */
useEffect(() => {
const rootElement = editor.getRootElement() as HTMLElement;
if (rootElement) {
if (isEmpty && placeholder) {
rootElement.setAttribute("placeholder", placeholder);
} else {
rootElement.removeAttribute("placeholder");
}
}
}, [editor, isEmpty]); // eslint-disable-line
return null;
};

View File

@@ -0,0 +1,2 @@
export { Editor } from "./Editor";
export { EditorHighlightPlugin } from "./EditorHighlight";

View File

@@ -11,6 +11,7 @@ export * from "./DatePicker";
export * from "./DeleteActionModal";
export * from "./Drawer";
export * from "./Dropdown";
export * from "./Editor";
export * from "./EmailServiceSetupModal";
export * from "./EmptyState";
export * from "./FilterableSelect";

View File

@@ -13,7 +13,8 @@ import {
HumanitecConnectionMethod,
MsSqlConnectionMethod,
PostgresConnectionMethod,
TAppConnection
TAppConnection,
VercelConnectionMethod
} from "@app/hooks/api/appConnections/types";
export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: string }> = {
@@ -30,6 +31,7 @@ export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: st
},
[AppConnection.Databricks]: { name: "Databricks", image: "Databricks.png" },
[AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" },
[AppConnection.Vercel]: { name: "Vercel", image: "Vercel.png" },
[AppConnection.Postgres]: { name: "PostgreSQL", image: "Postgres.png" },
[AppConnection.MsSql]: { name: "Microsoft SQL Server", image: "MsSql.png" },
[AppConnection.Camunda]: { name: "Camunda", image: "Camunda.png" }
@@ -54,6 +56,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case CamundaConnectionMethod.ClientCredentials:
return { name: "Client Credentials", icon: faKey };
case HumanitecConnectionMethod.ApiToken:
case VercelConnectionMethod.ApiToken:
return { name: "API Token", icon: faKey };
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:

View File

@@ -27,6 +27,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
[SecretSync.Camunda]: {
name: "Camunda",
image: "Camunda.png"
},
[SecretSync.Vercel]: {
name: "Vercel",
image: "Vercel.png"
}
};
@@ -39,7 +43,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
[SecretSync.Databricks]: AppConnection.Databricks,
[SecretSync.Humanitec]: AppConnection.Humanitec,
[SecretSync.Camunda]: AppConnection.Camunda
[SecretSync.Camunda]: AppConnection.Camunda,
[SecretSync.Vercel]: AppConnection.Vercel
};
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<

View File

@@ -6,6 +6,7 @@ export enum AppConnection {
AzureAppConfiguration = "azure-app-configuration",
Databricks = "databricks",
Humanitec = "humanitec",
Vercel = "vercel",
Postgres = "postgres",
MsSql = "mssql",
Camunda = "camunda"

View File

@@ -39,6 +39,10 @@ export type THumanitecConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Humanitec;
};
export type TVercelConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Vercel;
};
export type TPostgresConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Postgres;
};
@@ -59,6 +63,7 @@ export type TAppConnectionOption =
| TAzureKeyVaultConnectionOption
| TDatabricksConnectionOption
| THumanitecConnectionOption
| TVercelConnectionOption
| TPostgresConnectionOption
| TMsSqlConnectionOption
| TCamundaConnectionOption;
@@ -71,6 +76,7 @@ export type TAppConnectionOptionMap = {
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption;
[AppConnection.Databricks]: TDatabricksConnectionOption;
[AppConnection.Humanitec]: THumanitecConnectionOption;
[AppConnection.Vercel]: TVercelConnectionOption;
[AppConnection.Postgres]: TPostgresConnectionOption;
[AppConnection.MsSql]: TMsSqlConnectionOption;
[AppConnection.Camunda]: TCamundaConnectionOption;

View File

@@ -10,6 +10,7 @@ import { TGitHubConnection } from "./github-connection";
import { THumanitecConnection } from "./humanitec-connection";
import { TMsSqlConnection } from "./mssql-connection";
import { TPostgresConnection } from "./postgres-connection";
import { TVercelConnection } from "./vercel-connection";
export * from "./aws-connection";
export * from "./azure-app-configuration-connection";
@@ -21,6 +22,7 @@ export * from "./github-connection";
export * from "./humanitec-connection";
export * from "./mssql-connection";
export * from "./postgres-connection";
export * from "./vercel-connection";
export type TAppConnection =
| TAwsConnection
@@ -30,6 +32,7 @@ export type TAppConnection =
| TAzureAppConfigurationConnection
| TDatabricksConnection
| THumanitecConnection
| TVercelConnection
| TPostgresConnection
| TMsSqlConnection
| TCamundaConnection;
@@ -67,6 +70,7 @@ export type TAppConnectionMap = {
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
[AppConnection.Databricks]: TDatabricksConnection;
[AppConnection.Humanitec]: THumanitecConnection;
[AppConnection.Vercel]: TVercelConnection;
[AppConnection.Postgres]: TPostgresConnection;
[AppConnection.MsSql]: TMsSqlConnection;
[AppConnection.Camunda]: TCamundaConnection;

View File

@@ -0,0 +1,13 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum VercelConnectionMethod {
ApiToken = "api-token"
}
export type TVercelConnection = TRootAppConnection & { app: AppConnection.Vercel } & {
method: VercelConnectionMethod.ApiToken;
credentials: {
apiToken: string;
};
};

View File

@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";

View File

@@ -0,0 +1,37 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
import { TVercelConnectionOrganization } from "./types";
const vercelConnectionKeys = {
all: [...appConnectionKeys.all, "vercel"] as const,
listOrganizations: (connectionId: string) =>
[...vercelConnectionKeys.all, "organizations", connectionId] as const
};
export const useVercelConnectionListOrganizations = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TVercelConnectionOrganization[],
unknown,
TVercelConnectionOrganization[],
ReturnType<typeof vercelConnectionKeys.listOrganizations>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: vercelConnectionKeys.listOrganizations(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TVercelConnectionOrganization[]>(
`/api/v1/app-connections/vercel/${connectionId}/projects`
);
return data;
},
...options
});
};

View File

@@ -0,0 +1,30 @@
export type TVercelApp = {
id: string;
name: string;
envs: { id: string; name: string }[];
};
export type TVercelConnectionEnvironment = {
id: string;
slug: string;
type: string;
target?: string[];
gitBranch?: string;
createdAt?: number;
updatedAt?: number;
};
export type TVercelConnectionApp = {
id: string;
name: string;
envs?: TVercelConnectionEnvironment[];
previewBranches?: string[];
projectId: string;
};
export type TVercelConnectionOrganization = {
id: string;
name: string;
slug: string;
apps: TVercelConnectionApp[];
};

View File

@@ -46,5 +46,6 @@ export {
useGetIdentityTokenAuth,
useGetIdentityTokensTokenAuth,
useGetIdentityUniversalAuth,
useGetIdentityUniversalAuthClientSecrets
useGetIdentityUniversalAuthClientSecrets,
useSearchIdentities
} from "./queries";

View File

@@ -15,11 +15,13 @@ import {
IdentityMembershipOrg,
IdentityOidcAuth,
IdentityTokenAuth,
IdentityUniversalAuth
IdentityUniversalAuth,
TSearchIdentitiesDTO
} from "./types";
export const identitiesKeys = {
getIdentityById: (identityId: string) => [{ identityId }, "identity"] as const,
searchIdentities: (dto: TSearchIdentitiesDTO) => ["identity", "search", dto] as const,
getIdentityUniversalAuth: (identityId: string) =>
[{ identityId }, "identity-universal-auth"] as const,
getIdentityUniversalAuthClientSecrets: (identityId: string) =>
@@ -53,6 +55,26 @@ export const useGetIdentityById = (identityId: string) => {
});
};
export const useSearchIdentities = (dto: TSearchIdentitiesDTO) => {
const { limit, search, offset, orderBy, orderDirection } = dto;
return useQuery({
queryKey: identitiesKeys.searchIdentities(dto),
queryFn: async () => {
const { data } = await apiRequest.post<{
identities: IdentityMembershipOrg[];
totalCount: number;
}>("/api/v1/identities/search", {
limit,
offset,
orderBy,
orderDirection,
search
});
return data;
}
});
};
export const useGetIdentityProjectMemberships = (identityId: string) => {
return useQuery({
enabled: Boolean(identityId),

View File

@@ -1,3 +1,5 @@
import { OrderByDirection } from "../generic/types";
import { OrgIdentityOrderBy } from "../organization/types";
import { TOrgRole } from "../roles/types";
import { ProjectUserMembershipTemporaryMode, Workspace } from "../workspace/types";
import { IdentityAuthMethod, IdentityJwtConfigurationType } from "./enums";
@@ -540,3 +542,14 @@ export type TProjectIdentitiesList = {
identityMemberships: IdentityMembership[];
totalCount: number;
};
export type TSearchIdentitiesDTO = {
limit?: number;
offset?: number;
orderBy?: OrgIdentityOrderBy;
orderDirection?: OrderByDirection;
search: {
name?: { $contains: string };
role?: { $in: string[] };
};
};

View File

@@ -7,7 +7,8 @@ export enum SecretSync {
AzureAppConfiguration = "azure-app-configuration",
Databricks = "databricks",
Humanitec = "humanitec",
Camunda = "camunda"
Camunda = "camunda",
Vercel = "vercel"
}
export enum SecretSyncStatus {

View File

@@ -10,6 +10,7 @@ import { TAzureKeyVaultSync } from "./azure-key-vault-sync";
import { TCamundaSync } from "./camunda-sync";
import { TGcpSync } from "./gcp-sync";
import { THumanitecSync } from "./humanitec-sync";
import { TVercelSync } from "./vercel-sync";
export type TSecretSyncOption = {
name: string;
@@ -26,7 +27,8 @@ export type TSecretSync =
| TAzureAppConfigurationSync
| TDatabricksSync
| THumanitecSync
| TCamundaSync;
| TCamundaSync
| TVercelSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };

View File

@@ -0,0 +1,27 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export const VercelEnvironmentType = {
Development: "development",
Preview: "preview",
Production: "production"
} as const;
export type VercelEnvironment = (typeof VercelEnvironmentType)[keyof typeof VercelEnvironmentType];
export type TVercelSync = TRootSecretSync & {
destination: SecretSync.Vercel;
destinationConfig: {
app: string;
env: VercelEnvironment | string;
branch?: string;
appName?: string;
teamId: string;
};
connection: {
app: AppConnection.Vercel;
name: string;
id: string;
};
};

View File

@@ -191,3 +191,10 @@ html {
#nprogress .bar {
@apply bg-primary-400;
}
[contentEditable="true"]:before {
content: attr(placeholder);
position: absolute;
top: 0.5rem;
@apply text-sm text-gray-500 opacity-50;
}

View File

@@ -33,14 +33,12 @@ import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/Identity
import { AuthPanel } from "./components/AuthPanel";
import { EncryptionPanel } from "./components/EncryptionPanel";
import { IntegrationPanel } from "./components/IntegrationPanel";
import { RateLimitPanel } from "./components/RateLimitPanel";
import { UserPanel } from "./components/UserPanel";
enum TabSections {
Settings = "settings",
Encryption = "encryption",
Auth = "auth",
RateLimit = "rate-limit",
Integrations = "integrations",
Users = "users",
Identities = "identities",
@@ -163,7 +161,6 @@ export const OverviewPage = () => {
<Tab value={TabSections.Settings}>General</Tab>
<Tab value={TabSections.Encryption}>Encryption</Tab>
<Tab value={TabSections.Auth}>Authentication</Tab>
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
<Tab value={TabSections.Integrations}>Integrations</Tab>
<Tab value={TabSections.Users}>User Identities</Tab>
<Tab value={TabSections.Identities}>Machine Identities</Tab>
@@ -262,7 +259,6 @@ export const OverviewPage = () => {
<SelectClear
selectValue={defaultAuthOrgId}
onClear={() => {
console.log("clearing");
onChange("");
}}
>
@@ -403,9 +399,6 @@ export const OverviewPage = () => {
<TabPanel value={TabSections.Auth}>
<AuthPanel />
</TabPanel>
<TabPanel value={TabSections.RateLimit}>
<RateLimitPanel />
</TabPanel>
<TabPanel value={TabSections.Integrations}>
<IntegrationPanel />
</TabPanel>

View File

@@ -1,240 +0,0 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { Button, ContentLoader, FormControl, Input } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useGetRateLimit, useUpdateRateLimit } from "@app/hooks/api";
const formSchema = z.object({
readRateLimit: z.number(),
writeRateLimit: z.number(),
secretsRateLimit: z.number(),
authRateLimit: z.number(),
inviteUserRateLimit: z.number(),
mfaRateLimit: z.number(),
publicEndpointLimit: z.number()
});
type TRateLimitForm = z.infer<typeof formSchema>;
export const RateLimitPanel = () => {
const { data: rateLimit, isPending } = useGetRateLimit();
const { subscription } = useSubscription();
const { mutateAsync: updateRateLimit } = useUpdateRateLimit();
const { handlePopUpToggle, handlePopUpOpen, popUp } = usePopUp(["upgradePlan"] as const);
const {
control,
handleSubmit,
formState: { isSubmitting, isDirty }
} = useForm<TRateLimitForm>({
resolver: zodResolver(formSchema),
values: {
// eslint-disable-next-line
readRateLimit: rateLimit?.readRateLimit ?? 600,
writeRateLimit: rateLimit?.writeRateLimit ?? 200,
secretsRateLimit: rateLimit?.secretsRateLimit ?? 60,
authRateLimit: rateLimit?.authRateLimit ?? 60,
inviteUserRateLimit: rateLimit?.inviteUserRateLimit ?? 30,
mfaRateLimit: rateLimit?.mfaRateLimit ?? 20,
publicEndpointLimit: rateLimit?.publicEndpointLimit ?? 30
}
});
const onRateLimitFormSubmit = async (formData: TRateLimitForm) => {
try {
if (subscription && !subscription.customRateLimits) {
handlePopUpOpen("upgradePlan");
return;
}
const {
readRateLimit,
writeRateLimit,
secretsRateLimit,
authRateLimit,
inviteUserRateLimit,
mfaRateLimit,
publicEndpointLimit
} = formData;
await updateRateLimit({
readRateLimit,
writeRateLimit,
secretsRateLimit,
authRateLimit,
inviteUserRateLimit,
mfaRateLimit,
publicEndpointLimit
});
createNotification({
text: "Rate limits have been successfully updated. Please allow at least 10 minutes for the changes to take effect.",
type: "success"
});
} catch (e) {
console.error(e);
createNotification({
type: "error",
text: "Failed to update rate limiting setting."
});
}
};
return isPending ? (
<ContentLoader />
) : (
<form
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
onSubmit={handleSubmit(onRateLimitFormSubmit)}
>
<div className="mb-8 flex flex-col justify-start">
<div className="mb-4 text-xl font-semibold text-mineshaft-100">Configure rate limits</div>
<Controller
control={control}
name="readRateLimit"
defaultValue={300}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Global read requests per minute"
className="w-72"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue={300}
name="writeRateLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Global write requests per minute"
className="w-72"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue={300}
name="secretsRateLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret requests per minute"
className="w-72"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue={300}
name="authRateLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Auth requests per minute"
className="w-72"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue={300}
name="inviteUserRateLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="User invitation requests per minute"
className="w-72"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue={300}
name="mfaRateLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Multi factor auth requests per minute"
className="w-72"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue={300}
name="publicEndpointLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret sharing requests per minute"
className="w-72"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
</div>
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting || !isDirty}>
Save
</Button>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can configure custom rate limits if you switch to Infisical's Enterprise plan."
/>
</form>
);
};

View File

@@ -1,7 +1,10 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import {
faArrowDown,
faArrowUp,
faEllipsis,
faFilter,
faMagnifyingGlass,
faServer
} from "@fortawesome/free-solid-svg-icons";
@@ -12,14 +15,19 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
FormControl,
IconButton,
Input,
Pagination,
Popover,
PopoverContent,
PopoverTrigger,
Select,
SelectItem,
Spinner,
@@ -30,11 +38,12 @@ import {
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
import { useGetOrgRoles, useSearchIdentities, useUpdateIdentity } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -68,22 +77,22 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
page,
setPerPage
} = usePagination<OrgIdentityOrderBy>(OrgIdentityOrderBy.Name);
const [filteredRoles, setFilteredRoles] = useState<string[]>([]);
const organizationId = currentOrg?.id || "";
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
const { data, isPending, isFetching } = useGetIdentityMembershipOrgs(
{
organizationId,
offset,
limit,
orderDirection,
orderBy,
search: debouncedSearch
},
{ placeholderData: (prevData) => prevData }
);
const { data, isPending, isFetching } = useSearchIdentities({
offset,
limit,
orderDirection,
orderBy,
search: {
name: debouncedSearch ? { $contains: debouncedSearch } : undefined,
role: filteredRoles?.length ? { $in: filteredRoles } : undefined
}
});
const { totalCount = 0 } = data ?? {};
useResetPageHelper({
@@ -91,6 +100,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
offset,
setPage
});
const filterForm = useForm<{ roles: string }>();
const { data: roles } = useGetOrgRoles(organizationId);
@@ -132,13 +142,78 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
return (
<div>
<Input
containerClassName="mb-4"
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search identities by name..."
/>
<div className="mb-4 flex items-center space-x-2">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search identities by name..."
/>
<div>
<Popover>
<PopoverTrigger>
<IconButton
ariaLabel="filter"
variant="outline_bg"
className={filteredRoles?.length ? "border-primary" : ""}
>
<Tooltip content="Advance Filter">
<FontAwesomeIcon icon={faFilter} />
</Tooltip>
</IconButton>
</PopoverTrigger>
<PopoverContent className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl">
<div className="mb-4 border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
Advance Filter
</div>
<form
onSubmit={filterForm.handleSubmit((el) => {
setFilteredRoles(el.roles?.split(",")?.filter(Boolean) || []);
})}
>
<Controller
control={filterForm.control}
name="roles"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Roles"
helperText="Eg: admin,viewer"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="flex items-center space-x-2">
<Button
type="submit"
size="xs"
colorSchema="primary"
variant="outline_bg"
className="mt-4"
>
Apply Filter
</Button>
{Boolean(filteredRoles.length) && (
<Button
size="xs"
variant="link"
className="ml-4 mt-4"
onClick={() => {
filterForm.reset({ roles: "" });
setFilteredRoles([]);
}}
>
Clear
</Button>
)}
</div>
</form>
</PopoverContent>
</Popover>
</div>
</div>
<TableContainer>
<Table>
<THead>
@@ -190,7 +265,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
<TBody>
{isPending && <TableSkeleton columns={3} innerKey="org-identities" />}
{!isPending &&
data?.identityMemberships.map(({ identity: { id, name }, role, customRole }) => {
data?.identities?.map(({ identity: { id, name }, role, customRole }) => {
return (
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
@@ -307,10 +382,10 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isPending && data && data?.identityMemberships.length === 0 && (
{!isPending && data && data?.identities.length === 0 && (
<EmptyState
title={
debouncedSearch.trim().length > 0
debouncedSearch.trim().length > 0 || filteredRoles?.length > 0
? "No identities match search filter"
: "No identities have been created in this organization"
}

View File

@@ -258,7 +258,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
</Th>
<Th className="w-1/3">
<div className="flex items-center">
Email
Username
<IconButton
variant="plain"
className={`ml-2 ${orderBy === OrgMembersOrderBy.Email ? "" : "opacity-30"}`}
@@ -478,7 +478,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
onClick={(e) => {
e.stopPropagation();
if (currentOrg?.scimEnabled) {
if (currentOrg?.scimEnabled && isActive) {
createNotification({
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
type: "error"

View File

@@ -19,6 +19,7 @@ import { GitHubConnectionForm } from "./GitHubConnectionForm";
import { HumanitecConnectionForm } from "./HumanitecConnectionForm";
import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
import { PostgresConnectionForm } from "./PostgresConnectionForm";
import { VercelConnectionForm } from "./VercelConnectionForm";
type FormProps = {
onComplete: (appConnection: TAppConnection) => void;
@@ -71,6 +72,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <DatabricksConnectionForm onSubmit={onSubmit} />;
case AppConnection.Humanitec:
return <HumanitecConnectionForm onSubmit={onSubmit} />;
case AppConnection.Vercel:
return <VercelConnectionForm onSubmit={onSubmit} />;
case AppConnection.Postgres:
return <PostgresConnectionForm onSubmit={onSubmit} />;
case AppConnection.MsSql:
@@ -127,6 +130,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <DatabricksConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Humanitec:
return <HumanitecConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Vercel:
return <VercelConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Postgres:
return <PostgresConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.MsSql:

View File

@@ -0,0 +1,135 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
TVercelConnection,
VercelConnectionMethod
} from "@app/hooks/api/appConnections/types/vercel-connection";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TVercelConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.Vercel)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(VercelConnectionMethod.ApiToken),
credentials: z.object({
apiToken: z.string().trim().min(1, "Service API Token required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const VercelConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.Vercel,
method: VercelConnectionMethod.ApiToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.Vercel].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(VercelConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.apiToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="API Token"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Vercel"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

@@ -115,8 +115,29 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
});
} else {
const inviteeEmails = selectedMembers
.map((member) => member?.user.username as string)
.filter(Boolean);
.map((member) => {
if (!member) return null;
if (member.user.email) {
return member.user.email;
}
if (member.user.username) {
return member.user.username;
}
return null;
})
.filter(Boolean) as string[];
if (inviteeEmails.length !== selectedMembers.length) {
createNotification({
text: "Failed to add users to project. One or more users were invalid.",
type: "error"
});
return;
}
if (inviteeEmails.length || newInvitees.length) {
await addMembersToProject({
inviteeEmails: [...inviteeEmails, ...newInvitees],

View File

@@ -86,7 +86,20 @@ const Page = () => {
title={identityMembershipDetails?.identity?.name}
description={`Identity joined on ${identityMembershipDetails?.createdAt && formatRelative(new Date(identityMembershipDetails?.createdAt || ""), new Date())}`}
>
<div>
<div className="flex items-center gap-2">
<Button
variant="outline_bg"
size="xs"
onClick={() => {
navigator.clipboard.writeText(identityMembershipDetails.id);
createNotification({
text: "Membership ID copied to clipboard",
type: "success"
});
}}
>
Copy Membership ID
</Button>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Identity, {

View File

@@ -9,6 +9,7 @@ import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
import { VercelSyncDestinationCol } from "./VercelSyncDestinationCol";
type Props = {
secretSync: TSecretSync;
@@ -34,6 +35,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <HumanitecSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Camunda:
return <CamundaDestinationSyncCol secretSync={secretSync} />;
case SecretSync.Vercel:
return <VercelSyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`

View File

@@ -0,0 +1,14 @@
import { TVercelSync } from "@app/hooks/api/secretSyncs/types/vercel-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TVercelSync;
};
export const VercelSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};

View File

@@ -77,6 +77,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.clusterUUID;
secondaryText = "Cluster";
break;
case SecretSync.Vercel:
primaryText = destinationConfig.appName || destinationConfig.app;
secondaryText = destinationConfig.env;
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}

View File

@@ -19,6 +19,7 @@ import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinat
import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection";
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
import { VercelSyncDestinationSection } from "./VercelSyncDestinationSection";
type Props = {
secretSync: TSecretSync;
@@ -61,6 +62,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.Camunda:
DestinationComponents = <CamundaSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Vercel:
DestinationComponents = <VercelSyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}

View File

@@ -0,0 +1,36 @@
import { ReactNode } from "react";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TVercelSync, VercelEnvironmentType } from "@app/hooks/api/secretSyncs/types/vercel-sync";
type Props = {
secretSync: TVercelSync;
};
export const VercelSyncDestinationSection = ({ secretSync }: Props) => {
const { destinationConfig } = secretSync;
let Components: ReactNode;
if (destinationConfig.env === VercelEnvironmentType.Preview && destinationConfig.branch) {
Components = (
<>
<GenericFieldLabel label="Vercel App">
{destinationConfig.appName || destinationConfig.app}
</GenericFieldLabel>
<GenericFieldLabel label="Environment">{destinationConfig.env}</GenericFieldLabel>
<GenericFieldLabel label="Preview Branch">{destinationConfig.branch}</GenericFieldLabel>
</>
);
} else {
Components = (
<>
<GenericFieldLabel label="Vercel App">
{destinationConfig.appName || destinationConfig.app}
</GenericFieldLabel>
<GenericFieldLabel label="Environment">{destinationConfig.env}</GenericFieldLabel>
</>
);
}
return Components;
};

View File

@@ -49,6 +49,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.Databricks:
case SecretSync.Humanitec:
case SecretSync.Camunda:
case SecretSync.Vercel:
AdditionalSyncOptionsComponent = null;
break;
default: