mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(integration-page): implemented new optimized integrations page
This commit is contained in:
@@ -1,409 +1,18 @@
|
|||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json";
|
import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json";
|
||||||
|
|
||||||
import ActivateBotDialog from "@app/components/basic/dialog/ActivateBotDialog";
|
import { IntegrationsPage } from "@app/views/IntegrationsPage";
|
||||||
import CloudIntegrationSection from "@app/components/integrations/CloudIntegrationSection";
|
|
||||||
import FrameworkIntegrationSection from "@app/components/integrations/FrameworkIntegrationSection";
|
|
||||||
import IntegrationSection from "@app/components/integrations/IntegrationSection";
|
|
||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
|
||||||
|
|
||||||
import {
|
type Props = {
|
||||||
decryptAssymmetric,
|
frameworkIntegrations: typeof frameworkIntegrationOptions;
|
||||||
encryptAssymmetric
|
};
|
||||||
} from "../../components/utilities/cryptography/crypto";
|
|
||||||
import getBot from "../api/bot/getBot";
|
|
||||||
import setBotActiveStatus from "../api/bot/setBotActiveStatus";
|
|
||||||
import deleteIntegration from "../api/integrations/DeleteIntegration";
|
|
||||||
import getIntegrationOptions from "../api/integrations/GetIntegrationOptions";
|
|
||||||
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
|
|
||||||
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
|
|
||||||
import getAWorkspace from "../api/workspace/getAWorkspace";
|
|
||||||
import getLatestFileKey from "../api/workspace/getLatestFileKey";
|
|
||||||
|
|
||||||
interface IntegrationAuth {
|
|
||||||
_id: string;
|
|
||||||
integration: string;
|
|
||||||
workspace: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Integration {
|
|
||||||
_id: string;
|
|
||||||
isActive: boolean;
|
|
||||||
app: string | null;
|
|
||||||
appId: string | null;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
environment: string;
|
|
||||||
integration: string;
|
|
||||||
targetEnvironment: string;
|
|
||||||
workspace: string;
|
|
||||||
secretPath:string;
|
|
||||||
integrationAuth: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IntegrationOption {
|
|
||||||
tenantId?: string;
|
|
||||||
clientId: string;
|
|
||||||
clientSlug?: string; // vercel-integration specific
|
|
||||||
docsLink: string;
|
|
||||||
image: string;
|
|
||||||
isAvailable: boolean;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Integrations() {
|
|
||||||
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
|
|
||||||
const [integrationAuths, setIntegrationAuths] = useState<IntegrationAuth[]>([]);
|
|
||||||
const [environments, setEnvironments] = useState<
|
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
}[]
|
|
||||||
>([]);
|
|
||||||
const [integrations, setIntegrations] = useState<Integration[]>([]);
|
|
||||||
// TODO: These will have its type when migratiing towards react-query
|
|
||||||
const [bot, setBot] = useState<any>(null);
|
|
||||||
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
|
|
||||||
const [selectedIntegrationOption, setSelectedIntegrationOption] =
|
|
||||||
useState<IntegrationOption | null>(null);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const workspaceId = router.query.id as string;
|
|
||||||
|
|
||||||
|
const Integration = ({ frameworkIntegrations }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const workspace = await getAWorkspace(workspaceId);
|
|
||||||
setEnvironments(workspace.environments);
|
|
||||||
|
|
||||||
// get cloud integration options
|
|
||||||
setCloudIntegrationOptions(await getIntegrationOptions());
|
|
||||||
|
|
||||||
// get project integration authorizations
|
|
||||||
setIntegrationAuths(
|
|
||||||
await getWorkspaceAuthorizations({
|
|
||||||
workspaceId
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// get project integrations
|
|
||||||
setIntegrations(
|
|
||||||
await getWorkspaceIntegrations({
|
|
||||||
workspaceId
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// get project bot
|
|
||||||
setBot(await getBot({ workspaceId }));
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activate bot for project by performing the following steps:
|
|
||||||
* 1. Get the (encrypted) project key
|
|
||||||
* 2. Decrypt project key with user's private key
|
|
||||||
* 3. Encrypt project key with bot's public key
|
|
||||||
* 4. Send encrypted project key to backend and set bot status to active
|
|
||||||
*/
|
|
||||||
const handleBotActivate = async () => {
|
|
||||||
let botKey;
|
|
||||||
try {
|
|
||||||
if (bot) {
|
|
||||||
// case: there is a bot
|
|
||||||
const key = await getLatestFileKey({ workspaceId });
|
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
|
||||||
|
|
||||||
if (!PRIVATE_KEY) {
|
|
||||||
throw new Error("Private Key missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
const WORKSPACE_KEY = decryptAssymmetric({
|
|
||||||
ciphertext: key.latestKey.encryptedKey,
|
|
||||||
nonce: key.latestKey.nonce,
|
|
||||||
publicKey: key.latestKey.sender.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
|
||||||
plaintext: WORKSPACE_KEY,
|
|
||||||
publicKey: bot.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
botKey = {
|
|
||||||
encryptedKey: ciphertext,
|
|
||||||
nonce
|
|
||||||
};
|
|
||||||
|
|
||||||
setBot(
|
|
||||||
(
|
|
||||||
await setBotActiveStatus({
|
|
||||||
botId: bot._id,
|
|
||||||
isActive: true,
|
|
||||||
botKey
|
|
||||||
})
|
|
||||||
).bot
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
|
|
||||||
try {
|
|
||||||
// generate CSRF token for OAuth2 code-token exchange integrations
|
|
||||||
const state = crypto.randomBytes(16).toString("hex");
|
|
||||||
localStorage.setItem("latestCSRFToken", state);
|
|
||||||
|
|
||||||
let link = "";
|
|
||||||
switch (integrationOption.slug) {
|
|
||||||
case "azure-key-vault":
|
|
||||||
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
|
|
||||||
break;
|
|
||||||
case "aws-parameter-store":
|
|
||||||
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
|
|
||||||
break;
|
|
||||||
case "aws-secret-manager":
|
|
||||||
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
|
|
||||||
break;
|
|
||||||
case "heroku":
|
|
||||||
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
|
|
||||||
break;
|
|
||||||
case "vercel":
|
|
||||||
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
|
|
||||||
break;
|
|
||||||
case "netlify":
|
|
||||||
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
|
|
||||||
break;
|
|
||||||
case "github":
|
|
||||||
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
|
|
||||||
break;
|
|
||||||
case "gitlab":
|
|
||||||
link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
|
|
||||||
break;
|
|
||||||
case "render":
|
|
||||||
link = `${window.location.origin}/integrations/render/authorize`;
|
|
||||||
break;
|
|
||||||
case "flyio":
|
|
||||||
link = `${window.location.origin}/integrations/flyio/authorize`;
|
|
||||||
break;
|
|
||||||
case "circleci":
|
|
||||||
link = `${window.location.origin}/integrations/circleci/authorize`;
|
|
||||||
break;
|
|
||||||
case "travisci":
|
|
||||||
link = `${window.location.origin}/integrations/travisci/authorize`;
|
|
||||||
break;
|
|
||||||
case "supabase":
|
|
||||||
link = `${window.location.origin}/integrations/supabase/authorize`;
|
|
||||||
break;
|
|
||||||
case "checkly":
|
|
||||||
link = `${window.location.origin}/integrations/checkly/authorize`;
|
|
||||||
break;
|
|
||||||
case "railway":
|
|
||||||
link = `${window.location.origin}/integrations/railway/authorize`;
|
|
||||||
break;
|
|
||||||
case "hashicorp-vault":
|
|
||||||
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
|
|
||||||
break;
|
|
||||||
case "cloudflare-pages":
|
|
||||||
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (link !== "") {
|
|
||||||
window.location.assign(link);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
|
|
||||||
try {
|
|
||||||
let link = "";
|
|
||||||
switch (integrationAuth.integration) {
|
|
||||||
case "azure-key-vault":
|
|
||||||
link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "aws-parameter-store":
|
|
||||||
link = `${window.location.origin}/integrations/aws-parameter-store/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "aws-secret-manager":
|
|
||||||
link = `${window.location.origin}/integrations/aws-secret-manager/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "heroku":
|
|
||||||
link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "vercel":
|
|
||||||
link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "netlify":
|
|
||||||
link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "github":
|
|
||||||
link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "gitlab":
|
|
||||||
link = `${window.location.origin}/integrations/gitlab/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "render":
|
|
||||||
link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "flyio":
|
|
||||||
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "circleci":
|
|
||||||
link = `${window.location.origin}/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "travisci":
|
|
||||||
link = `${window.location.origin}/integrations/travisci/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "supabase":
|
|
||||||
link = `${window.location.origin}/integrations/supabase/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "checkly":
|
|
||||||
link = `${window.location.origin}/integrations/checkly/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "railway":
|
|
||||||
link = `${window.location.origin}/integrations/railway/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "hashicorp-vault":
|
|
||||||
link = `${window.location.origin}/integrations/hashicorp-vault/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "cloudflare-pages":
|
|
||||||
link = `${window.location.origin}/integrations/cloudflare-pages/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (link !== "") {
|
|
||||||
window.location.assign(link);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open dialog to activate bot if bot is not active.
|
|
||||||
* Otherwise, start integration [integrationOption]
|
|
||||||
* @param {Object} integrationOption - an integration option
|
|
||||||
* @param {String} integrationOption.name
|
|
||||||
* @param {String} integrationOption.type
|
|
||||||
* @param {String} integrationOption.docsLink
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
|
|
||||||
try {
|
|
||||||
const integrationAuthX = integrationAuths.find(
|
|
||||||
(integrationAuth) => integrationAuth.integration === integrationOption.slug
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!bot.isActive) {
|
|
||||||
await handleBotActivate();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!integrationAuthX) {
|
|
||||||
// case: integration has not been authorized
|
|
||||||
handleUnauthorizedIntegrationOptionPress(integrationOption);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAuthorizedIntegrationOptionPress(integrationAuthX);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle deleting integration authorization [integrationAuth] and corresponding integrations from state where applicable
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {IntegrationAuth} obj.integrationAuth - integrationAuth to delete
|
|
||||||
*/
|
|
||||||
const handleDeleteIntegrationAuth = async ({
|
|
||||||
integrationAuth: deletedIntegrationAuth
|
|
||||||
}: {
|
|
||||||
integrationAuth: IntegrationAuth;
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
const newIntegrations = integrations.filter(
|
|
||||||
(integration) => integration.integrationAuth !== deletedIntegrationAuth._id
|
|
||||||
);
|
|
||||||
setIntegrationAuths(
|
|
||||||
integrationAuths.filter(
|
|
||||||
(integrationAuth) => integrationAuth._id !== deletedIntegrationAuth._id
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setIntegrations(newIntegrations);
|
|
||||||
|
|
||||||
// handle updating bot
|
|
||||||
if (newIntegrations.length < 1) {
|
|
||||||
// case: no integrations left
|
|
||||||
setBot(
|
|
||||||
(
|
|
||||||
await setBotActiveStatus({
|
|
||||||
botId: bot._id,
|
|
||||||
isActive: false
|
|
||||||
})
|
|
||||||
).bot
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle deleting integration [integration]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Integration} obj.integration - integration to delete
|
|
||||||
*/
|
|
||||||
const handleDeleteIntegration = async ({ integration }: { integration: Integration }) => {
|
|
||||||
try {
|
|
||||||
const deletedIntegration = await deleteIntegration({
|
|
||||||
integrationId: integration._id
|
|
||||||
});
|
|
||||||
|
|
||||||
const newIntegrations = integrations.filter((i) => i._id !== deletedIntegration._id);
|
|
||||||
setIntegrations(newIntegrations);
|
|
||||||
|
|
||||||
// handle updating bot
|
|
||||||
if (newIntegrations.length < 1) {
|
|
||||||
// case: no integrations left
|
|
||||||
setBot(
|
|
||||||
(
|
|
||||||
await setBotActiveStatus({
|
|
||||||
botId: bot._id,
|
|
||||||
isActive: false
|
|
||||||
})
|
|
||||||
).bot
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex max-h-full flex-col justify-between bg-bunker-800 text-white">
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t("common.head-title", { title: t("integrations.title") })}</title>
|
<title>{t("common.head-title", { title: t("integrations.title") })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
@@ -411,52 +20,26 @@ export default function Integrations() {
|
|||||||
<meta property="og:title" content="Manage your .env files in seconds" />
|
<meta property="og:title" content="Manage your .env files in seconds" />
|
||||||
<meta name="og:description" content={t("integrations.description") as string} />
|
<meta name="og:description" content={t("integrations.description") as string} />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="no-scrollbar::-webkit-scrollbar h-screen max-h-[calc(100vh-10px)] w-full overflow-y-scroll pb-6 no-scrollbar">
|
<IntegrationsPage frameworkIntegrations={frameworkIntegrations} />
|
||||||
<NavHeader pageName={t("integrations.title")} isProjectRelated />
|
</>
|
||||||
<ActivateBotDialog
|
|
||||||
isOpen={isActivateBotDialogOpen}
|
|
||||||
closeModal={() => setIsActivateBotDialogOpen(false)}
|
|
||||||
selectedIntegrationOption={selectedIntegrationOption}
|
|
||||||
integrationOptionPress={integrationOptionPress}
|
|
||||||
/>
|
|
||||||
<IntegrationSection
|
|
||||||
integrations={integrations}
|
|
||||||
setIntegrations={setIntegrations}
|
|
||||||
bot={bot}
|
|
||||||
setBot={setBot}
|
|
||||||
environments={environments}
|
|
||||||
handleDeleteIntegration={handleDeleteIntegration}
|
|
||||||
/>
|
|
||||||
{cloudIntegrationOptions.length > 0 && bot ? (
|
|
||||||
<CloudIntegrationSection
|
|
||||||
cloudIntegrationOptions={cloudIntegrationOptions}
|
|
||||||
setSelectedIntegrationOption={setSelectedIntegrationOption as any}
|
|
||||||
integrationOptionPress={(integrationOption: IntegrationOption) => {
|
|
||||||
if (!bot.isActive) {
|
|
||||||
// case: bot is not active -> open modal to activate bot
|
|
||||||
setIsActivateBotDialogOpen(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
integrationOptionPress(integrationOption);
|
|
||||||
}}
|
|
||||||
integrationAuths={integrationAuths}
|
|
||||||
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
|
||||||
<h1 className="text-3xl font-semibold">{t("integrations.cloud-integrations")}</h1>
|
|
||||||
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
|
|
||||||
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(elem => <div key={elem} className="bg-mineshaft-800 border border-mineshaft-600 animate-pulse h-32 rounded-md"/>)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<FrameworkIntegrationSection frameworks={frameworkIntegrationOptions as any} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
Integrations.requireAuth = true;
|
export const getStaticProps = () => {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
frameworkIntegrations: frameworkIntegrationOptions
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStaticPaths = async () => {
|
||||||
|
return {
|
||||||
|
paths: [], // indicates that no page needs be created at build time
|
||||||
|
fallback: "blocking" // indicates the type of fallback
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
Integration.requireAuth = true;
|
||||||
|
|
||||||
|
export default Integration;
|
||||||
|
|||||||
102
frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx
Normal file
102
frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
import { UserWsKeyPair, TCloudIntegration } from '@app/hooks/api/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
encryptAssymmetric
|
||||||
|
} from '../../components/utilities/cryptography/crypto';
|
||||||
|
|
||||||
|
export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||||
|
|
||||||
|
if (!PRIVATE_KEY) {
|
||||||
|
throw new Error('Private Key missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
const WORKSPACE_KEY = decryptAssymmetric({
|
||||||
|
ciphertext: latestKey.encryptedKey,
|
||||||
|
nonce: latestKey.nonce,
|
||||||
|
publicKey: latestKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
|
plaintext: WORKSPACE_KEY,
|
||||||
|
publicKey: botPublicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
return { encryptedKey: ciphertext, nonce };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => {
|
||||||
|
try {
|
||||||
|
// generate CSRF token for OAuth2 code-token exchange integrations
|
||||||
|
const state = crypto.randomBytes(16).toString('hex');
|
||||||
|
localStorage.setItem('latestCSRFToken', state);
|
||||||
|
|
||||||
|
let link = '';
|
||||||
|
switch (integrationOption.slug) {
|
||||||
|
case 'azure-key-vault':
|
||||||
|
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
|
||||||
|
break;
|
||||||
|
case 'aws-parameter-store':
|
||||||
|
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
|
||||||
|
break;
|
||||||
|
case 'aws-secret-manager':
|
||||||
|
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
|
||||||
|
break;
|
||||||
|
case 'heroku':
|
||||||
|
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
|
||||||
|
break;
|
||||||
|
case 'vercel':
|
||||||
|
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
|
||||||
|
break;
|
||||||
|
case 'netlify':
|
||||||
|
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
|
||||||
|
break;
|
||||||
|
case 'github':
|
||||||
|
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
|
||||||
|
break;
|
||||||
|
case 'gitlab':
|
||||||
|
link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
|
||||||
|
break;
|
||||||
|
case 'render':
|
||||||
|
link = `${window.location.origin}/integrations/render/authorize`;
|
||||||
|
break;
|
||||||
|
case 'flyio':
|
||||||
|
link = `${window.location.origin}/integrations/flyio/authorize`;
|
||||||
|
break;
|
||||||
|
case 'circleci':
|
||||||
|
link = `${window.location.origin}/integrations/circleci/authorize`;
|
||||||
|
break;
|
||||||
|
case 'travisci':
|
||||||
|
link = `${window.location.origin}/integrations/travisci/authorize`;
|
||||||
|
break;
|
||||||
|
case 'supabase':
|
||||||
|
link = `${window.location.origin}/integrations/supabase/authorize`;
|
||||||
|
break;
|
||||||
|
case 'checkly':
|
||||||
|
link = `${window.location.origin}/integrations/checkly/authorize`;
|
||||||
|
break;
|
||||||
|
case 'railway':
|
||||||
|
link = `${window.location.origin}/integrations/railway/authorize`;
|
||||||
|
break;
|
||||||
|
case 'hashicorp-vault':
|
||||||
|
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link !== '') {
|
||||||
|
window.location.assign(link);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const redirectToIntegrationAppConfigScreen = (provider: string, integrationAuthId: string) =>
|
||||||
|
`/integrations/${provider}/create?integrationAuthId=${integrationAuthId}`;
|
||||||
223
frontend/src/views/IntegrationsPage/IntegrationsPage.tsx
Normal file
223
frontend/src/views/IntegrationsPage/IntegrationsPage.tsx
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||||
|
import NavHeader from '@app/components/navigation/NavHeader';
|
||||||
|
import { Button,Modal, ModalContent } from '@app/components/v2';
|
||||||
|
import { useWorkspace } from '@app/context';
|
||||||
|
import { usePopUp } from '@app/hooks';
|
||||||
|
import {
|
||||||
|
useDeleteIntegration,
|
||||||
|
useDeleteIntegrationAuth,
|
||||||
|
useGetCloudIntegrations,
|
||||||
|
useGetUserWsKey,
|
||||||
|
useGetWorkspaceAuthorizations,
|
||||||
|
useGetWorkspaceBot,
|
||||||
|
useGetWorkspaceIntegrations,
|
||||||
|
useUpdateBotActiveStatus} from '@app/hooks/api';
|
||||||
|
import { IntegrationAuth } from '@app/hooks/api/types';
|
||||||
|
|
||||||
|
import { CloudIntegrationSection } from './components/CloudIntegrationSection';
|
||||||
|
import { FrameworkIntegrationSection } from './components/FrameworkIntegrationSection';
|
||||||
|
import { IntegrationsSection } from './components/IntegrationsSection';
|
||||||
|
import {
|
||||||
|
generateBotKey,
|
||||||
|
redirectForProviderAuth,
|
||||||
|
redirectToIntegrationAppConfigScreen
|
||||||
|
} from './IntegrationPage.utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IntegrationsPage = ({ frameworkIntegrations }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const workspaceId = currentWorkspace?._id || '';
|
||||||
|
const environments = currentWorkspace?.environments || [];
|
||||||
|
|
||||||
|
const { data: latestWsKey } = useGetUserWsKey(workspaceId);
|
||||||
|
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
|
'activeBot',
|
||||||
|
'revokeProviderPermissionConf',
|
||||||
|
'removeIntegrationConf'
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
|
||||||
|
useGetCloudIntegrations();
|
||||||
|
const { data: integrationAuths, isLoading: isIntegrationAuthLoading } =
|
||||||
|
useGetWorkspaceAuthorizations(
|
||||||
|
workspaceId,
|
||||||
|
useCallback((data: IntegrationAuth[]) => {
|
||||||
|
const groupBy: Record<string, IntegrationAuth> = {};
|
||||||
|
data.forEach((el) => {
|
||||||
|
groupBy[el.integration] = el;
|
||||||
|
});
|
||||||
|
return groupBy;
|
||||||
|
}, [])
|
||||||
|
);
|
||||||
|
// mutation
|
||||||
|
const {
|
||||||
|
data: integrations,
|
||||||
|
isLoading: isIntegrationLoading,
|
||||||
|
isFetching: isIntegrationFetching
|
||||||
|
} = useGetWorkspaceIntegrations(workspaceId);
|
||||||
|
|
||||||
|
const { data: bot } = useGetWorkspaceBot(workspaceId);
|
||||||
|
|
||||||
|
// mutation
|
||||||
|
const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } =
|
||||||
|
useUpdateBotActiveStatus();
|
||||||
|
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
|
||||||
|
const {
|
||||||
|
mutateAsync: deleteIntegrationAuth,
|
||||||
|
isLoading: isDeleteIntegrationAuthSuccess,
|
||||||
|
reset: resetDeleteIntegrationAuth
|
||||||
|
} = useDeleteIntegrationAuth();
|
||||||
|
|
||||||
|
// summary: this use effect is trigger when all integration auths are removed thus deactivate bot
|
||||||
|
// details: so onsuccessfully deleting an integration auth, immediately integration list is refeteched
|
||||||
|
// After the refetch is completed check if its empty. Then set bot active and reset the submit hook
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDeleteIntegrationAuthSuccess && !isIntegrationFetching && !integrations?.length) {
|
||||||
|
if (bot?._id)
|
||||||
|
updateBotActiveStatusSync({
|
||||||
|
isActive: false,
|
||||||
|
botId: bot._id,
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
resetDeleteIntegrationAuth();
|
||||||
|
}
|
||||||
|
}, [isIntegrationFetching, isDeleteIntegrationAuthSuccess, integrations?.length]);
|
||||||
|
|
||||||
|
const handleProviderIntegration = async (provider: string) => {
|
||||||
|
const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug);
|
||||||
|
if (!selectedCloudIntegration) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (bot && !bot.isActive) {
|
||||||
|
const botKey = generateBotKey(bot.publicKey, latestWsKey!);
|
||||||
|
await updateBotActiveStatus({
|
||||||
|
workspaceId,
|
||||||
|
botKey,
|
||||||
|
isActive: true,
|
||||||
|
botId: bot._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const integrationAuthForProvider = integrationAuths?.[provider];
|
||||||
|
if (!integrationAuthForProvider) {
|
||||||
|
redirectForProviderAuth(selectedCloudIntegration);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = redirectToIntegrationAppConfigScreen(provider, integrationAuthForProvider._id);
|
||||||
|
router.push(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// function to strat integration for a provider
|
||||||
|
// confirmation to user passing the bot key for provider to get secret access
|
||||||
|
const handleProviderIntegrationStart = (provider: string) => {
|
||||||
|
if (!bot?.isActive) {
|
||||||
|
handlePopUpOpen('activeBot', { provider });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleProviderIntegration(provider);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserAcceptBotCondition = () => {
|
||||||
|
const { provider } = popUp.activeBot?.data as { provider: string };
|
||||||
|
handleProviderIntegration(provider);
|
||||||
|
handlePopUpClose('activeBot');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
|
||||||
|
try {
|
||||||
|
await deleteIntegration({ id: integrationId, workspaceId });
|
||||||
|
if (cb) cb();
|
||||||
|
createNotification({
|
||||||
|
type: 'success',
|
||||||
|
text: 'Deleted integration'
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({
|
||||||
|
type: 'error',
|
||||||
|
text: 'Failed to delete integration'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => {
|
||||||
|
const integrationAuthForProvider = integrationAuths?.[provider];
|
||||||
|
if (!integrationAuthForProvider) return;
|
||||||
|
try {
|
||||||
|
await deleteIntegrationAuth({
|
||||||
|
id: integrationAuthForProvider._id,
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
if (cb) cb();
|
||||||
|
createNotification({
|
||||||
|
type: 'success',
|
||||||
|
text: 'Revoked provider authentication'
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
type: 'error',
|
||||||
|
text: 'Failed to revoke provider authentication'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-8 pb-12 text-white">
|
||||||
|
<NavHeader pageName={t('integrations.title')} isProjectRelated />
|
||||||
|
<IntegrationsSection
|
||||||
|
isLoading={isIntegrationLoading}
|
||||||
|
integrations={integrations}
|
||||||
|
environments={environments}
|
||||||
|
onIntegrationDelete={({ _id: id }, cb) => handleIntegrationDelete(id, cb)}
|
||||||
|
/>
|
||||||
|
<CloudIntegrationSection
|
||||||
|
isLoading={isCloudIntegrationsLoading || isIntegrationAuthLoading}
|
||||||
|
cloudIntegrations={cloudIntegrations}
|
||||||
|
integrationAuths={integrationAuths}
|
||||||
|
onIntegrationStart={handleProviderIntegrationStart}
|
||||||
|
onIntegrationRevoke={handleIntegrationAuthRevoke}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.activeBot?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle('activeBot', isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title={t('integrations.grant-access-to-secrets') as string}
|
||||||
|
footerContent={
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button onClick={() => handleUserAcceptBotCondition()}>
|
||||||
|
{t('integrations.grant-access-button') as string}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handlePopUpClose('activeBot')}
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="secondary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('integrations.why-infisical-needs-access')}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<FrameworkIntegrationSection frameworks={frameworkIntegrations} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { faCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
import { DeleteActionModal,Skeleton, Tooltip } from '@app/components/v2';
|
||||||
|
import { usePopUp } from '@app/hooks';
|
||||||
|
import { IntegrationAuth, TCloudIntegration } from '@app/hooks/api/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isLoading?: boolean;
|
||||||
|
integrationAuths?: Record<string, IntegrationAuth>;
|
||||||
|
cloudIntegrations?: TCloudIntegration[];
|
||||||
|
onIntegrationStart: (slug: string) => void;
|
||||||
|
// cb: handle popUpClose child->parent communication pattern
|
||||||
|
onIntegrationRevoke: (slug: string, cb: () => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TRevokeIntegrationPopUp = { provider: string };
|
||||||
|
|
||||||
|
export const CloudIntegrationSection = ({
|
||||||
|
isLoading,
|
||||||
|
cloudIntegrations = [],
|
||||||
|
integrationAuths = {},
|
||||||
|
onIntegrationStart,
|
||||||
|
onIntegrationRevoke
|
||||||
|
}: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
'deleteConfirmation'
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const isEmpty = !isLoading && !cloudIntegrations?.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||||
|
<h1 className="text-3xl font-semibold">{t('integrations.cloud-integrations')}</h1>
|
||||||
|
<p className="text-base text-gray-400">{t('integrations.click-to-start')}</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mx-6 grid grid-flow-dense gap-4"
|
||||||
|
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(256px, 1fr))' }}
|
||||||
|
>
|
||||||
|
{isLoading &&
|
||||||
|
Array.from({ length: 12 }).map((_, index) => (
|
||||||
|
<Skeleton className="h-32" key={`cloud-integration-skeleton-${index + 1}`} />
|
||||||
|
))}
|
||||||
|
{!isLoading &&
|
||||||
|
cloudIntegrations?.map((cloudIntegration) => (
|
||||||
|
<div
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className={`group relative ${
|
||||||
|
cloudIntegration.isAvailable
|
||||||
|
? 'cursor-pointer duration-200 hover:bg-mineshaft-700'
|
||||||
|
: 'opacity-50'
|
||||||
|
} flex h-32 flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!cloudIntegration.isAvailable) return;
|
||||||
|
onIntegrationStart(cloudIntegration.slug);
|
||||||
|
}}
|
||||||
|
key={cloudIntegration.slug}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`/images/integrations/${cloudIntegration.image}`}
|
||||||
|
height={70}
|
||||||
|
width={70}
|
||||||
|
alt="integration logo"
|
||||||
|
/>
|
||||||
|
{cloudIntegration.name.split(' ').length > 2 ? (
|
||||||
|
<div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||||
|
<div>{cloudIntegration.name.split(' ')[0]}</div>
|
||||||
|
<div className="text-base">
|
||||||
|
{cloudIntegration.name.split(' ')[1]} {cloudIntegration.name.split(' ')[2]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||||
|
{cloudIntegration.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{cloudIntegration.isAvailable &&
|
||||||
|
Boolean(integrationAuths?.[cloudIntegration.slug]) && (
|
||||||
|
<div className="absolute top-0 right-0 z-40 h-full">
|
||||||
|
<div className="relative h-full">
|
||||||
|
<div className="absolute top-0 right-0 w-24 flex-row items-center overflow-hidden whitespace-nowrap rounded-tr-md bg-primary py-0.5 px-2 text-xs text-black opacity-80 transition-all duration-300 group-hover:w-0 group-hover:p-0">
|
||||||
|
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
|
||||||
|
Authorized
|
||||||
|
</div>
|
||||||
|
<Tooltip content="Revoke Access">
|
||||||
|
<div
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={async (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
handlePopUpOpen('deleteConfirmation', {
|
||||||
|
provider: cloudIntegration.slug
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="absolute top-0 right-0 flex h-0 w-12 cursor-pointer items-center justify-center overflow-hidden rounded-r-md bg-red text-xs opacity-50 transition-all duration-300 hover:opacity-100 group-hover:h-full"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} size="xl" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{isEmpty && (
|
||||||
|
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
|
||||||
|
{Array.from({ length: 16 }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={`dummy-cloud-integration-${index + 1}`}
|
||||||
|
className="h-32 animate-pulse rounded-md border border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteConfirmation.isOpen}
|
||||||
|
title={`Are you sure want to revoke access ${
|
||||||
|
(popUp?.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider || ' '
|
||||||
|
}?`}
|
||||||
|
subTitle="This will remove all the secret integration of this provider!!!"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle('deleteConfirmation', isOpen)}
|
||||||
|
deleteKey={(popUp?.deleteConfirmation?.data as TRevokeIntegrationPopUp)?.provider || ''}
|
||||||
|
onDeleteApproved={async () => {
|
||||||
|
onIntegrationRevoke(
|
||||||
|
(popUp.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider,
|
||||||
|
() => handlePopUpClose('deleteConfirmation')
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { CloudIntegrationSection } from './CloudIntegrationSection';
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
frameworks: Array<{
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
slug: string;
|
||||||
|
docsLink: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FrameworkIntegrationSection = ({ frameworks }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mx-4 mt-12 mb-4 flex flex-col items-start justify-between px-2 text-xl">
|
||||||
|
<h1 className="text-3xl font-semibold">{t('integrations.framework-integrations')}</h1>
|
||||||
|
<p className="text-base text-gray-400">{t('integrations.click-to-setup')}</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mx-6 mt-4 grid grid-flow-dense gap-4"
|
||||||
|
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))' }}
|
||||||
|
>
|
||||||
|
{frameworks.map((framework) => (
|
||||||
|
<a
|
||||||
|
key={`framework-integration-${framework.slug}`}
|
||||||
|
href={framework.docsLink}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
className="relative flex h-32 cursor-pointer flex-row items-center justify-center rounded-md p-0.5 duration-200"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex h-full w-full cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 font-semibold text-gray-300 duration-200 hover:bg-mineshaft-700 group-hover:text-gray-200 ${
|
||||||
|
framework?.name?.split(' ').length > 1 ? 'px-1 text-sm' : 'px-2 text-xl'
|
||||||
|
} w-full max-w-xs text-center`}
|
||||||
|
>
|
||||||
|
{framework?.image && (
|
||||||
|
<img
|
||||||
|
src={`/images/integrations/${framework.image}.png`}
|
||||||
|
height={framework?.name ? 60 : 90}
|
||||||
|
width={framework?.name ? 60 : 90}
|
||||||
|
alt="integration logo"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{framework?.name && framework?.image && <div className="h-2" />}
|
||||||
|
{framework?.name && framework.name}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { FrameworkIntegrationSection } from './FrameworkIntegrationSection';
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { integrationSlugNameMapping } from 'public/data/frequentConstants';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Skeleton
|
||||||
|
} from '@app/components/v2';
|
||||||
|
import { usePopUp } from '@app/hooks';
|
||||||
|
import { TIntegration } from '@app/hooks/api/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
environments: Array<{ name: string; slug: string }>;
|
||||||
|
integrations?: TIntegration[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IntegrationsSection = ({
|
||||||
|
integrations = [],
|
||||||
|
environments = [],
|
||||||
|
isLoading,
|
||||||
|
onIntegrationDelete
|
||||||
|
}: Props) => {
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
'deleteConfirmation'
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="mx-4 mb-4 mt-6 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||||
|
<h1 className="text-3xl font-semibold">Current Integrations</h1>
|
||||||
|
<p className="text-base text-bunker-300">Manage integrations with third-party services.</p>
|
||||||
|
</div>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="p-6 pt-0">
|
||||||
|
<Skeleton className="h-28" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoading && !integrations.length && (
|
||||||
|
<EmptyState
|
||||||
|
className="mx-6 py-8"
|
||||||
|
title="No integrations found. Click on one of the below providers to sync secrets."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isLoading && (
|
||||||
|
<div className="flex flex-col space-y-4 p-6 pt-0">
|
||||||
|
{integrations?.map((integration) => (
|
||||||
|
<div
|
||||||
|
className="flex max-w-6xl justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 pb-2"
|
||||||
|
key={`integration-${integration?._id.toString()}`}
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<div>
|
||||||
|
<FormControl label="Environment">
|
||||||
|
<Select
|
||||||
|
value={integration.environment}
|
||||||
|
isDisabled={integration.isActive}
|
||||||
|
className="min-w-[8rem]"
|
||||||
|
>
|
||||||
|
{environments.map((environment) => {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
value={environment.slug}
|
||||||
|
key={`environment-${environment.slug}`}
|
||||||
|
>
|
||||||
|
{environment.name}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<div className="ml-2 flex flex-col">
|
||||||
|
<FormLabel label="Secret Path" />
|
||||||
|
<div className="min-w-[8rem] rounded-md 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 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||||
|
{integrationSlugNameMapping[integration.integration]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-2 flex flex-col">
|
||||||
|
<FormLabel label="App" />
|
||||||
|
<div className="min-w-[8rem] rounded-md bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||||
|
{integration.integration === 'hashicorp-vault'
|
||||||
|
? `${integration.app} - path: ${integration.path}`
|
||||||
|
: integration.app}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(integration.integration === 'vercel' ||
|
||||||
|
integration.integration === 'netlify' ||
|
||||||
|
integration.integration === 'railway' ||
|
||||||
|
integration.integration === 'gitlab') && (
|
||||||
|
<div className="ml-4 flex flex-col">
|
||||||
|
<FormLabel label="Target Environment" />
|
||||||
|
<div className="rounded-md bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||||
|
{integration.targetEnvironment}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex cursor-default items-center">
|
||||||
|
<div className="ml-2 opacity-80 duration-200 hover:opacity-100">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => handlePopUpOpen('deleteConfirmation', integration)}
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteConfirmation.isOpen}
|
||||||
|
title={`Are you sure want to remove ${
|
||||||
|
(popUp?.deleteConfirmation.data as TIntegration)?.integrationAuth || ' '
|
||||||
|
} integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || ' '}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle('deleteConfirmation', isOpen)}
|
||||||
|
deleteKey={(popUp?.deleteConfirmation?.data as TIntegration)?.app || ''}
|
||||||
|
onDeleteApproved={async () =>
|
||||||
|
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () =>
|
||||||
|
handlePopUpClose('deleteConfirmation')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { IntegrationsSection } from './IntegrationsSection';
|
||||||
1
frontend/src/views/IntegrationsPage/index.tsx
Normal file
1
frontend/src/views/IntegrationsPage/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { IntegrationsPage } from './IntegrationsPage';
|
||||||
Reference in New Issue
Block a user