mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 16:52:38 +00:00
feat: renamed old frontend and fixed some minor bugs
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { ContentLoader } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useDeleteIntegration,
|
||||
useDeleteIntegrationAuths,
|
||||
useGetCloudIntegrations,
|
||||
useGetWorkspaceAuthorizations,
|
||||
useGetWorkspaceIntegrations
|
||||
} from "@app/hooks/api";
|
||||
import { IntegrationAuth } from "@app/hooks/api/types";
|
||||
|
||||
import { CloudIntegrationSection } from "./components/CloudIntegrationSection";
|
||||
import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSection";
|
||||
import { InfrastructureIntegrationSection } from "./components/InfrastructureIntegrationSection/InfrastructureIntegrationSection";
|
||||
import { IntegrationsSection } from "./components/IntegrationsSection";
|
||||
import { redirectForProviderAuth } from "./IntegrationsListPage.utils";
|
||||
|
||||
enum IntegrationView {
|
||||
List = "list",
|
||||
New = "new"
|
||||
}
|
||||
|
||||
const Page = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const navigate = useNavigate();
|
||||
const { environments, id: workspaceId } = currentWorkspace;
|
||||
const [view, setView] = useState<IntegrationView>(IntegrationView.New);
|
||||
|
||||
const { data: cloudIntegrations, isPending: isCloudIntegrationsLoading } =
|
||||
useGetCloudIntegrations();
|
||||
|
||||
const {
|
||||
data: integrationAuths,
|
||||
isPending: isIntegrationAuthLoading,
|
||||
isFetching: isIntegrationAuthFetching
|
||||
} = useGetWorkspaceAuthorizations(
|
||||
workspaceId,
|
||||
useCallback((data: IntegrationAuth[]) => {
|
||||
const groupBy: Record<string, IntegrationAuth> = {};
|
||||
data.forEach((el) => {
|
||||
groupBy[el.integration] = el;
|
||||
});
|
||||
return groupBy;
|
||||
}, [])
|
||||
);
|
||||
|
||||
// mutation
|
||||
const {
|
||||
data: integrations,
|
||||
isPending: isIntegrationLoading,
|
||||
isFetching: isIntegrationFetching,
|
||||
isFetched: isIntegrationsFetched
|
||||
} = useGetWorkspaceIntegrations(workspaceId);
|
||||
|
||||
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
|
||||
const {
|
||||
mutateAsync: deleteIntegrationAuths,
|
||||
isSuccess: isDeleteIntegrationAuthSuccess,
|
||||
reset: resetDeleteIntegrationAuths
|
||||
} = useDeleteIntegrationAuths();
|
||||
|
||||
const isIntegrationsAuthorizedEmpty = !Object.keys(integrationAuths || {}).length;
|
||||
const isIntegrationsEmpty = !integrations?.length;
|
||||
// summary: this use effect is trigger when all integration auths are removed thus deactivate bot
|
||||
// details: so on successfully 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 for isSuccess to go back to false
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDeleteIntegrationAuthSuccess &&
|
||||
!isIntegrationFetching &&
|
||||
!isIntegrationAuthFetching &&
|
||||
isIntegrationsAuthorizedEmpty &&
|
||||
isIntegrationsEmpty
|
||||
) {
|
||||
resetDeleteIntegrationAuths();
|
||||
}
|
||||
}, [
|
||||
isIntegrationFetching,
|
||||
isDeleteIntegrationAuthSuccess,
|
||||
isIntegrationAuthFetching,
|
||||
isIntegrationsAuthorizedEmpty,
|
||||
isIntegrationsEmpty
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setView(integrations?.length ? IntegrationView.List : IntegrationView.New);
|
||||
}, [isIntegrationsFetched]);
|
||||
|
||||
const handleProviderIntegration = async (provider: string) => {
|
||||
const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug);
|
||||
if (!selectedCloudIntegration) return;
|
||||
|
||||
try {
|
||||
redirectForProviderAuth(currentWorkspace.id, navigate, selectedCloudIntegration);
|
||||
} 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) => {
|
||||
handleProviderIntegration(provider);
|
||||
};
|
||||
|
||||
const handleIntegrationDelete = async (
|
||||
integrationId: string,
|
||||
shouldDeleteIntegrationSecrets: boolean,
|
||||
cb: () => void
|
||||
) => {
|
||||
try {
|
||||
await deleteIntegration({ id: integrationId, workspaceId, shouldDeleteIntegrationSecrets });
|
||||
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 deleteIntegrationAuths({
|
||||
integration: provider,
|
||||
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"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isIntegrationLoading || isCloudIntegrationsLoading)
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<ContentLoader text={["Loading integrations..."]} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container relative mx-auto max-w-7xl pb-12 text-white">
|
||||
<div className="relative">
|
||||
{view === IntegrationView.List ? (
|
||||
<motion.div
|
||||
key="view-integrations"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="w-full"
|
||||
>
|
||||
<IntegrationsSection
|
||||
cloudIntegrations={cloudIntegrations}
|
||||
onAddIntegration={() => setView(IntegrationView.New)}
|
||||
isLoading={isIntegrationLoading}
|
||||
integrations={integrations}
|
||||
environments={environments}
|
||||
onIntegrationDelete={handleIntegrationDelete}
|
||||
workspaceId={workspaceId}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="add-integration"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="w-full"
|
||||
>
|
||||
<CloudIntegrationSection
|
||||
onViewActiveIntegrations={
|
||||
integrations?.length ? () => setView(IntegrationView.List) : undefined
|
||||
}
|
||||
isLoading={isCloudIntegrationsLoading || isIntegrationAuthLoading}
|
||||
cloudIntegrations={cloudIntegrations}
|
||||
integrationAuths={integrationAuths}
|
||||
onIntegrationStart={handleProviderIntegrationStart}
|
||||
onIntegrationRevoke={handleIntegrationAuthRevoke}
|
||||
/>
|
||||
<FrameworkIntegrationSection />
|
||||
<InfrastructureIntegrationSection />
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const IntegrationsListPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: t("integrations.title") })}</title>
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content="Manage your .env files in seconds" />
|
||||
<meta name="og:description" content={t("integrations.description") as string} />
|
||||
</Helmet>
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
passThrough={false}
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.Integrations}
|
||||
>
|
||||
<Page />
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { NavigateFn } from "@tanstack/react-router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { localStorageService } from "@app/helpers/localStorage";
|
||||
import { TCloudIntegration, UserWsKeyPair } from "@app/hooks/api/types";
|
||||
|
||||
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 createIntegrationMissingEnvVarsNotification = (
|
||||
slug: string,
|
||||
type: "cloud" | "cicd" = "cloud",
|
||||
hashtag?: string
|
||||
) =>
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: (
|
||||
<a
|
||||
href={`https://infisical.com/docs/integrations/${type}/${slug}${
|
||||
hashtag ? `#${hashtag}` : ""
|
||||
}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Click here to view docs
|
||||
</a>
|
||||
),
|
||||
title: "Missing Environment Variables"
|
||||
});
|
||||
|
||||
export const redirectForProviderAuth = (
|
||||
projectId: string,
|
||||
navigate: NavigateFn,
|
||||
integrationOption: TCloudIntegration
|
||||
) => {
|
||||
try {
|
||||
// generate CSRF token for OAuth2 code-token exchange integrations
|
||||
const state = crypto.randomBytes(16).toString("hex");
|
||||
localStorage.setItem("latestCSRFToken", state);
|
||||
localStorageService.setIntegrationProjectId(projectId);
|
||||
|
||||
switch (integrationOption.slug) {
|
||||
case "gcp-secret-manager":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/gcp-secret-manager/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "azure-key-vault": {
|
||||
if (!integrationOption.clientId) {
|
||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||
return;
|
||||
}
|
||||
const 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}`;
|
||||
window.location.assign(link);
|
||||
break;
|
||||
}
|
||||
case "azure-app-configuration": {
|
||||
if (!integrationOption.clientId) {
|
||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||
return;
|
||||
}
|
||||
const link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-app-configuration/oauth2/callback&response_mode=query&scope=https://azconfig.io/.default openid offline_access&state=${state}`;
|
||||
window.location.assign(link);
|
||||
break;
|
||||
}
|
||||
case "aws-parameter-store":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/aws-parameter-store/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "aws-secret-manager":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/aws-secret-manager/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "heroku": {
|
||||
if (!integrationOption.clientId) {
|
||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||
return;
|
||||
}
|
||||
const link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
|
||||
window.location.assign(link);
|
||||
break;
|
||||
}
|
||||
case "vercel": {
|
||||
if (!integrationOption.clientSlug) {
|
||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||
return;
|
||||
}
|
||||
const link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
|
||||
window.location.assign(link);
|
||||
break;
|
||||
}
|
||||
case "netlify": {
|
||||
if (!integrationOption.clientId) {
|
||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug);
|
||||
return;
|
||||
}
|
||||
const link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
|
||||
|
||||
window.location.assign(link);
|
||||
break;
|
||||
}
|
||||
case "github":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/github/auth-mode-selection",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "gitlab":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/gitlab/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "render":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/render/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "flyio":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/flyio/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "circleci":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/circleci/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "databricks":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/databricks/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "laravel-forge":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/laravel-forge/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "travisci":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/travisci/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "supabase":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/supabase/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "checkly":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/checkly/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "qovery":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/qovery/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "railway":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/railway/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "terraform-cloud":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/terraform-cloud/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "hashicorp-vault":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/hashicorp-vault/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "cloudflare-pages":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/cloudflare-pages/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "cloudflare-workers":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/cloudflare-workers/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "bitbucket": {
|
||||
if (!integrationOption.clientId) {
|
||||
createIntegrationMissingEnvVarsNotification(integrationOption.slug, "cicd");
|
||||
return;
|
||||
}
|
||||
const link = `https://bitbucket.org/site/oauth2/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/bitbucket/oauth2/callback&state=${state}`;
|
||||
window.location.assign(link);
|
||||
break;
|
||||
}
|
||||
case "codefresh":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/codefresh/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "digital-ocean-app-platform":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/digital-ocean-app-platform/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "cloud-66":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/cloud-66/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "northflank":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/northflank/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "windmill":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/windmill/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "teamcity":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/teamcity/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "hasura-cloud":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/hasura-cloud/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "rundeck":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/rundeck/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "azure-devops":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/azure-devops/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "octopus-deploy":
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/octopus-deploy/authorize",
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
faCheck,
|
||||
faChevronLeft,
|
||||
faMagnifyingGlass,
|
||||
faSearch,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { NoEnvironmentsBanner } from "@app/components/integrations/NoEnvironmentsBanner";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
Input,
|
||||
Skeleton,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
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;
|
||||
onViewActiveIntegrations?: () => void;
|
||||
};
|
||||
|
||||
type TRevokeIntegrationPopUp = { provider: string };
|
||||
|
||||
export const CloudIntegrationSection = ({
|
||||
isLoading,
|
||||
cloudIntegrations = [],
|
||||
integrationAuths = {},
|
||||
onIntegrationStart,
|
||||
onIntegrationRevoke,
|
||||
onViewActiveIntegrations
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"deleteConfirmation"
|
||||
] as const);
|
||||
const { permission } = useProjectPermission();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const isEmpty = !isLoading && !cloudIntegrations?.length;
|
||||
|
||||
const sortedCloudIntegrations = useMemo(() => {
|
||||
const sortedIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (currentWorkspace?.environments.length === 0) {
|
||||
return sortedIntegrations.map((integration) => ({ ...integration, isAvailable: false }));
|
||||
}
|
||||
|
||||
return sortedIntegrations;
|
||||
}, [cloudIntegrations, currentWorkspace?.environments]);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filteredIntegrations = sortedCloudIntegrations?.filter((cloudIntegration) =>
|
||||
cloudIntegration.name.toLowerCase().includes(search.toLowerCase().trim())
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="px-5">
|
||||
{currentWorkspace?.environments.length === 0 && (
|
||||
<NoEnvironmentsBanner projectId={currentWorkspace.id} />
|
||||
)}
|
||||
</div>
|
||||
<div className="m-4 mt-7 flex flex-col items-start justify-between px-2 text-xl">
|
||||
{onViewActiveIntegrations && (
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={onViewActiveIntegrations}
|
||||
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||
>
|
||||
Back to Integrations
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex w-full flex-col justify-between gap-4 whitespace-nowrap lg:flex-row lg:items-end lg:gap-8">
|
||||
<div className="flex-1">
|
||||
<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>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search cloud integrations..."
|
||||
containerClassName="flex-1 h-min text-base"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-6 grid grid-cols-3 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7">
|
||||
{isLoading &&
|
||||
Array.from({ length: 12 }).map((_, index) => (
|
||||
<Skeleton className="h-32" key={`cloud-integration-skeleton-${index + 1}`} />
|
||||
))}
|
||||
|
||||
{!isLoading && filteredIntegrations.length ? (
|
||||
filteredIntegrations.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-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4`}
|
||||
onClick={() => {
|
||||
if (!cloudIntegration.isAvailable) return;
|
||||
if (
|
||||
permission.cannot(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Integrations
|
||||
)
|
||||
) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "You do not have permission to create an integration"
|
||||
});
|
||||
return;
|
||||
}
|
||||
onIntegrationStart(cloudIntegration.slug);
|
||||
}}
|
||||
key={cloudIntegration.slug}
|
||||
>
|
||||
<img
|
||||
src={`/images/integrations/${cloudIntegration.image}`}
|
||||
height={60}
|
||||
width={60}
|
||||
className="mt-auto"
|
||||
alt="integration logo"
|
||||
/>
|
||||
<div className="mt-auto max-w-xs text-center text-sm font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{cloudIntegration.name}
|
||||
</div>
|
||||
{cloudIntegration.isAvailable &&
|
||||
Boolean(integrationAuths?.[cloudIntegration.slug]) && (
|
||||
<div className="absolute right-0 top-0 z-30 h-full">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute right-0 top-0 w-24 flex-row items-center overflow-hidden whitespace-nowrap rounded-bl-md rounded-tr-md bg-primary px-2 py-0.5 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 right-0 top-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>
|
||||
))
|
||||
) : (
|
||||
<EmptyState
|
||||
className="col-span-full h-32 w-full rounded-md bg-transparent pt-14"
|
||||
title="No cloud integrations match search..."
|
||||
icon={faSearch}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { CloudIntegrationSection } from "./CloudIntegrationSection";
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faKeyboard } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faComputer } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import frameworks from "../json/frameworkIntegrations.json";
|
||||
|
||||
export const FrameworkIntegrationSection = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sortedFrameworks = frameworks.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-4 mb-4 mt-12 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-cols-3 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7">
|
||||
{sortedFrameworks.map((framework) => (
|
||||
<a
|
||||
key={`framework-integration-${framework.slug}`}
|
||||
href={framework.docsLink}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="relative flex h-32 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 duration-200 hover:bg-mineshaft-700"
|
||||
>
|
||||
{framework?.image && (
|
||||
<img
|
||||
src={`/images/integrations/${framework.image}.png`}
|
||||
height={60}
|
||||
width={60}
|
||||
className="mt-auto"
|
||||
alt="integration logo"
|
||||
/>
|
||||
)}
|
||||
{framework?.name && (
|
||||
<div className="mt-auto max-w-xs text-center text-sm font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{framework.name}
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href="https://infisical.com/docs/cli/commands/run"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="relative flex h-32 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 duration-200 hover:bg-mineshaft-700"
|
||||
>
|
||||
<FontAwesomeIcon className="mt-auto text-5xl text-white/90" icon={faKeyboard} />
|
||||
<div className="mt-auto max-w-xs text-center text-sm font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
CLI
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://infisical.com/docs/sdks/overview"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="relative flex h-32 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 duration-200 hover:bg-mineshaft-700"
|
||||
>
|
||||
<FontAwesomeIcon className="mt-auto text-5xl text-white/90" icon={faComputer} />
|
||||
<div className="mt-auto max-w-xs text-center text-sm font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
SDKs
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { FrameworkIntegrationSection } from "./FrameworkIntegrationSection";
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import integrations from "../json/infrastructureIntegrations.json";
|
||||
|
||||
export const InfrastructureIntegrationSection = () => {
|
||||
const sortedIntegrations = integrations.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-4 mb-4 mt-12 flex flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">Infrastructure Integrations</h1>
|
||||
<p className="text-base text-gray-400">
|
||||
Click on of the integration to read the documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-6 grid grid-cols-2 gap-4 lg:grid-cols-3 2xl:grid-cols-4">
|
||||
{sortedIntegrations.map((integration) => (
|
||||
<a
|
||||
key={`framework-integration-${integration.slug}`}
|
||||
href={integration.docsLink}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="relative flex h-32 w-full cursor-pointer flex-row items-center justify-center rounded-md p-0.5 duration-200"
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="group relative flex h-32 w-full cursor-pointer flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 duration-200 hover:bg-mineshaft-700"
|
||||
key={integration?.name}
|
||||
>
|
||||
<img
|
||||
src={`/images/integrations/${integration.image}.png`}
|
||||
height={integration?.name ? 60 : 90}
|
||||
width={integration?.name ? 60 : 90}
|
||||
alt="integration logo"
|
||||
/>
|
||||
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{integration?.name && integration.name}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { InfrastructureIntegrationSection } from "./InfrastructureIntegrationSection";
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button, Checkbox, DeleteActionModal } from "@app/components/v2";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { TCloudIntegration, TIntegration } from "@app/hooks/api/types";
|
||||
|
||||
import { IntegrationsTable } from "./components";
|
||||
|
||||
type Props = {
|
||||
environments: Array<{ name: string; slug: string; id: string }>;
|
||||
integrations?: TIntegration[];
|
||||
cloudIntegrations?: TCloudIntegration[];
|
||||
isLoading?: boolean;
|
||||
onIntegrationDelete: (
|
||||
integrationId: string,
|
||||
shouldDeleteIntegrationSecrets: boolean,
|
||||
cb: () => void
|
||||
) => Promise<void>;
|
||||
workspaceId: string;
|
||||
onAddIntegration: () => void;
|
||||
};
|
||||
|
||||
export const IntegrationsSection = ({
|
||||
integrations = [],
|
||||
environments = [],
|
||||
isLoading,
|
||||
onIntegrationDelete,
|
||||
workspaceId,
|
||||
onAddIntegration,
|
||||
cloudIntegrations = []
|
||||
}: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"deleteConfirmation",
|
||||
"deleteSecretsConfirmation"
|
||||
] as const);
|
||||
|
||||
const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false);
|
||||
|
||||
return (
|
||||
<div className="mx-6 mb-8">
|
||||
<div className="mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">Integrations</h1>
|
||||
<p className="text-base text-bunker-300">Manage integrations with third-party services.</p>
|
||||
</div>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Active Integrations</p>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={onAddIntegration}
|
||||
>
|
||||
Add Integration
|
||||
</Button>
|
||||
</div>
|
||||
<IntegrationsTable
|
||||
cloudIntegrations={cloudIntegrations}
|
||||
integrations={integrations}
|
||||
isLoading={isLoading}
|
||||
workspaceId={workspaceId}
|
||||
environments={environments}
|
||||
onDeleteIntegration={(integration) => {
|
||||
setShouldDeleteSecrets.off();
|
||||
handlePopUpOpen("deleteConfirmation", integration);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteConfirmation.isOpen}
|
||||
title={`Are you sure want to remove ${
|
||||
(popUp?.deleteConfirmation.data as TIntegration)?.integration || " "
|
||||
} integration for ${
|
||||
(popUp?.deleteConfirmation.data as TIntegration)?.app || "this project"
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
|
||||
deleteKey={
|
||||
((popUp?.deleteConfirmation?.data as TIntegration)?.integration ===
|
||||
"azure-app-configuration" &&
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.app
|
||||
?.split("//")[1]
|
||||
?.split(".")[0]) ||
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.app ||
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.owner ||
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.path ||
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.integration ||
|
||||
""
|
||||
}
|
||||
onDeleteApproved={async () => {
|
||||
if (shouldDeleteSecrets) {
|
||||
handlePopUpOpen("deleteSecretsConfirmation");
|
||||
return;
|
||||
}
|
||||
|
||||
await onIntegrationDelete(
|
||||
(popUp?.deleteConfirmation.data as TIntegration).id,
|
||||
false,
|
||||
() => handlePopUpClose("deleteConfirmation")
|
||||
);
|
||||
}}
|
||||
>
|
||||
{(popUp?.deleteConfirmation?.data as TIntegration)?.integration === "github" && (
|
||||
<div className="mt-4">
|
||||
<Checkbox
|
||||
id="delete-integration-secrets"
|
||||
checkIndicatorBg="text-white"
|
||||
onCheckedChange={() => setShouldDeleteSecrets.toggle()}
|
||||
>
|
||||
Delete previously synced secrets from the destination
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
</DeleteActionModal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSecretsConfirmation.isOpen}
|
||||
title={`Are you sure you also want to delete secrets on ${
|
||||
(popUp?.deleteConfirmation.data as TIntegration)?.integration
|
||||
}?`}
|
||||
subTitle="By confirming, you acknowledge that all secrets managed by this integration will be removed from the destination. This action is irreversible."
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretsConfirmation", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={async () => {
|
||||
await onIntegrationDelete(
|
||||
(popUp?.deleteConfirmation.data as TIntegration).id,
|
||||
true,
|
||||
() => {
|
||||
handlePopUpClose("deleteSecretsConfirmation");
|
||||
handlePopUpClose("deleteConfirmation");
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { FormLabel } from "@app/components/v2";
|
||||
import { CircleCiScope } from "@app/hooks/api/integrationAuth/types";
|
||||
import { IntegrationMappingBehavior, TIntegration } from "@app/hooks/api/integrations/types";
|
||||
|
||||
type Props = {
|
||||
integration: TIntegration;
|
||||
};
|
||||
|
||||
const FIELD_CLASSNAME =
|
||||
"truncate rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200";
|
||||
|
||||
export const getIntegrationDestination = (integration: TIntegration) =>
|
||||
(integration.integration === "hashicorp-vault" &&
|
||||
`${integration.app} - path: ${integration.path}`) ||
|
||||
(integration.scope === "github-org" && `${integration.owner}`) ||
|
||||
(["aws-parameter-store", "rundeck"].includes(integration.integration) && `${integration.path}`) ||
|
||||
(integration.scope?.startsWith("github-") && `${integration.owner}/${integration.app}`) ||
|
||||
integration.app ||
|
||||
"-";
|
||||
|
||||
export const IntegrationDetails = ({ integration }: Props) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-2">
|
||||
{integration.integration === "octopus-deploy" && (
|
||||
<div>
|
||||
<FormLabel label="Space" />
|
||||
<div className={FIELD_CLASSNAME}>
|
||||
{integration.targetEnvironment || integration.targetEnvironmentId}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "qovery" && (
|
||||
<>
|
||||
<div>
|
||||
<FormLabel label="Org" />
|
||||
<div className={FIELD_CLASSNAME}>{integration?.owner || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel label="Project" />
|
||||
<div className={FIELD_CLASSNAME}>{integration?.targetService || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel label="Env" />
|
||||
<div className={FIELD_CLASSNAME}>{integration?.targetEnvironment || "-"}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!(
|
||||
integration.integration === "aws-secret-manager" &&
|
||||
integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE
|
||||
) && (
|
||||
<div>
|
||||
<FormLabel
|
||||
label={
|
||||
(integration.integration === "qovery" && integration?.scope) ||
|
||||
(integration.integration === "circleci" &&
|
||||
(integration.scope === CircleCiScope.Context ? "Context" : "Project")) ||
|
||||
(integration.integration === "bitbucket" && "Repository") ||
|
||||
(integration.integration === "octopus-deploy" && "Project") ||
|
||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
||||
(["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") ||
|
||||
(integration?.integration === "terraform-cloud" && "Project") ||
|
||||
(integration?.scope === "github-org" && "Organization") ||
|
||||
(["github-repo", "github-env"].includes(integration?.scope as string) &&
|
||||
"Repository") ||
|
||||
"App"
|
||||
}
|
||||
/>
|
||||
<div className={FIELD_CLASSNAME}>{getIntegrationDestination(integration)}</div>
|
||||
</div>
|
||||
)}
|
||||
{(integration.integration === "vercel" ||
|
||||
integration.integration === "netlify" ||
|
||||
integration.integration === "railway" ||
|
||||
integration.integration === "gitlab" ||
|
||||
integration.integration === "teamcity" ||
|
||||
(integration.integration === "github" && integration.scope === "github-env")) && (
|
||||
<div>
|
||||
<FormLabel label="Target Environment" />
|
||||
<div className={FIELD_CLASSNAME}>
|
||||
{integration.targetEnvironment || integration.targetEnvironmentId}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "bitbucket" && (
|
||||
<>
|
||||
{integration.targetServiceId && (
|
||||
<div>
|
||||
<FormLabel label="Environment" />
|
||||
<div className={FIELD_CLASSNAME}>
|
||||
{integration.targetService || integration.targetServiceId}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<FormLabel label="Workspace" />
|
||||
<div className={FIELD_CLASSNAME}>
|
||||
{integration.targetEnvironment || integration.targetEnvironmentId}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{integration.integration === "checkly" && integration.targetService && (
|
||||
<div>
|
||||
<FormLabel label="Group" />
|
||||
<div className={FIELD_CLASSNAME}>{integration.targetService}</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "circleci" && integration.owner && (
|
||||
<div>
|
||||
<FormLabel label="Organization" />
|
||||
<div className={FIELD_CLASSNAME}>{integration.owner}</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "terraform-cloud" && integration.targetService && (
|
||||
<div>
|
||||
<FormLabel label="Category" />
|
||||
<div className={FIELD_CLASSNAME}>{integration.targetService}</div>
|
||||
</div>
|
||||
)}
|
||||
{(integration.integration === "checkly" || integration.integration === "github") &&
|
||||
integration?.metadata?.secretSuffix && (
|
||||
<div>
|
||||
<FormLabel label="Secret Suffix" />
|
||||
<div className={FIELD_CLASSNAME}>{integration.metadata.secretSuffix}</div>
|
||||
</div>
|
||||
)}
|
||||
{integration.integration === "github" && integration.metadata?.githubVisibility ? (
|
||||
<div className="mt-2 text-xs text-mineshaft-200">
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{integration.metadata?.githubVisibility === "selected"
|
||||
? "* Syncing to selected repositories in the organization. "
|
||||
: integration.metadata?.githubVisibility === "private"
|
||||
? "* Syncing to all private repositories in the organization"
|
||||
: "* Syncing to all public and private repositories in the organization"}
|
||||
</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
faCalendarCheck,
|
||||
faCheck,
|
||||
faInfoCircle,
|
||||
faRefresh,
|
||||
faTrash,
|
||||
faWarning,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Badge, IconButton, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { TCloudIntegration } from "@app/hooks/api/integrations/types";
|
||||
import { TIntegration } from "@app/hooks/api/types";
|
||||
|
||||
import { getIntegrationDestination, IntegrationDetails } from "./IntegrationDetails";
|
||||
|
||||
type IProps = {
|
||||
integration: TIntegration;
|
||||
environment?: { name: string; slug: string; id: string };
|
||||
onRemoveIntegration: VoidFunction;
|
||||
onManualSyncIntegration: VoidFunction;
|
||||
cloudIntegration: TCloudIntegration;
|
||||
};
|
||||
|
||||
export const IntegrationRow = ({
|
||||
integration,
|
||||
environment,
|
||||
onRemoveIntegration,
|
||||
onManualSyncIntegration,
|
||||
cloudIntegration
|
||||
}: IProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { id, secretPath, syncMessage, isSynced } = integration;
|
||||
|
||||
const failureMessage = useMemo(() => {
|
||||
if (isSynced === false) {
|
||||
if (syncMessage)
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(syncMessage), null, 2);
|
||||
} catch {
|
||||
return syncMessage;
|
||||
}
|
||||
|
||||
return "An Unknown Error Occurred.";
|
||||
}
|
||||
return null;
|
||||
}, [isSynced, syncMessage]);
|
||||
|
||||
return (
|
||||
<Tr
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/secret-manager/$projectId/integrations/$integrationId",
|
||||
params: {
|
||||
integrationId: integration.id,
|
||||
projectId: currentWorkspace.id
|
||||
}
|
||||
})
|
||||
}
|
||||
className={twMerge(
|
||||
"group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700",
|
||||
isSynced === false && "bg-red/5 hover:bg-red/10"
|
||||
)}
|
||||
key={`integration-${id}`}
|
||||
>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={`${cloudIntegration?.name} integration`}
|
||||
src={`/images/integrations/${cloudIntegration?.image}`}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<span className="hidden lg:inline">{cloudIntegration?.name}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="!min-w-[8rem] max-w-0">
|
||||
<Tooltip side="top" className="max-w-2xl break-words" content={secretPath}>
|
||||
<p className="truncate">{secretPath}</p>
|
||||
</Tooltip>{" "}
|
||||
</Td>
|
||||
<Td>{environment?.name ?? "-"}</Td>
|
||||
<Td className="!min-w-[5rem] max-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="truncate">{getIntegrationDestination(integration)}</p>
|
||||
<Tooltip
|
||||
position="left"
|
||||
className="min-w-[20rem] max-w-lg"
|
||||
content={<IntegrationDetails integration={integration} />}
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{" "}
|
||||
{typeof integration.isSynced !== "boolean" ? (
|
||||
<Badge variant="primary" key={integration.id}>
|
||||
Pending Sync
|
||||
</Badge>
|
||||
) : (
|
||||
<Tooltip
|
||||
position="left"
|
||||
className="max-w-sm"
|
||||
content={
|
||||
<div className="flex flex-col gap-2 py-1">
|
||||
{integration.lastUsed && (
|
||||
<div>
|
||||
<div
|
||||
className={`mb-2 flex self-start ${!isSynced ? "text-yellow" : "text-green"}`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faCalendarCheck}
|
||||
className="ml-1 pr-1.5 pt-0.5 text-sm"
|
||||
/>
|
||||
<div className="text-xs">Last Synced</div>
|
||||
</div>
|
||||
<div className="rounded bg-mineshaft-600 p-2 text-xs">
|
||||
{format(new Date(integration.lastUsed!), "yyyy-MM-dd, hh:mm aaa")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{failureMessage && (
|
||||
<div>
|
||||
<div className="mb-2 flex self-start text-red">
|
||||
<FontAwesomeIcon icon={faXmark} className="ml-1 pr-1.5 pt-0.5 text-sm" />
|
||||
<div className="text-xs">Failure Reason</div>
|
||||
</div>
|
||||
<div className="rounded bg-mineshaft-600 p-2 text-xs">{failureMessage}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="w-min whitespace-nowrap">
|
||||
<Badge variant={integration.isSynced ? "success" : "danger"} key={integration.id}>
|
||||
<div className="flex items-center space-x-1">
|
||||
<FontAwesomeIcon icon={integration.isSynced ? faCheck : faWarning} />
|
||||
<div>{integration.isSynced ? "Synced" : "Not Synced"}</div>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex gap-2 whitespace-nowrap">
|
||||
<Tooltip className="max-w-sm text-center" content="Manually Sync">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onManualSyncIntegration();
|
||||
}}
|
||||
ariaLabel="sync"
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faRefresh} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Integrations}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<Tooltip content="Remove Integration">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveIntegration();
|
||||
}}
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className="px-1" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
+448
@@ -0,0 +1,448 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCheck,
|
||||
faClock,
|
||||
faFilter,
|
||||
faMagnifyingGlass,
|
||||
faPlug,
|
||||
faSearch,
|
||||
faWarning
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
|
||||
import { TCloudIntegration, TIntegration } from "@app/hooks/api/integrations/types";
|
||||
|
||||
import { getIntegrationDestination } from "./IntegrationDetails";
|
||||
import { IntegrationRow } from "./IntegrationRow";
|
||||
|
||||
type Props = {
|
||||
integrations?: TIntegration[];
|
||||
cloudIntegrations?: TCloudIntegration[];
|
||||
workspaceId: string;
|
||||
isLoading?: boolean;
|
||||
environments: Array<{ name: string; slug: string; id: string }>;
|
||||
onDeleteIntegration: (integration: TIntegration) => void;
|
||||
};
|
||||
|
||||
enum IntegrationsOrderBy {
|
||||
App = "app",
|
||||
Status = "status",
|
||||
SecretPath = "secretPath",
|
||||
Environment = "environment",
|
||||
Destination = "destination"
|
||||
}
|
||||
|
||||
enum IntegrationStatus {
|
||||
Synced = "synced",
|
||||
NotSynced = "not-synced",
|
||||
PendingSync = "pending-sync"
|
||||
}
|
||||
|
||||
type IntegrationFilters = {
|
||||
environmentIds: string[];
|
||||
integrations: string[];
|
||||
status: IntegrationStatus[];
|
||||
};
|
||||
|
||||
const STATUS_ICON_MAP = {
|
||||
[IntegrationStatus.Synced]: { icon: faCheck, className: "text-green" },
|
||||
[IntegrationStatus.NotSynced]: { icon: faWarning, className: "text-red" },
|
||||
[IntegrationStatus.PendingSync]: { icon: faClock, className: "text-yellow" }
|
||||
};
|
||||
|
||||
export const IntegrationsTable = ({
|
||||
integrations = [],
|
||||
cloudIntegrations = [],
|
||||
workspaceId,
|
||||
environments,
|
||||
onDeleteIntegration,
|
||||
isLoading
|
||||
}: Props) => {
|
||||
const { mutate: syncIntegration } = useSyncIntegration();
|
||||
|
||||
const initialFilters = useMemo(
|
||||
() => ({
|
||||
environmentIds: environments.map((env) => env.id),
|
||||
integrations: [...new Set(integrations.map(({ integration }) => integration))],
|
||||
status: Object.values(IntegrationStatus)
|
||||
}),
|
||||
[environments, integrations]
|
||||
);
|
||||
|
||||
const [filters, setFilters] = useState<IntegrationFilters>(initialFilters);
|
||||
|
||||
const cloudIntegrationMap = useMemo(() => {
|
||||
return new Map(
|
||||
cloudIntegrations.map((cloudIntegration) => [cloudIntegration.slug, cloudIntegration])
|
||||
);
|
||||
}, [cloudIntegrations]);
|
||||
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
setPage,
|
||||
page,
|
||||
perPage,
|
||||
setPerPage,
|
||||
offset,
|
||||
orderDirection,
|
||||
toggleOrderDirection,
|
||||
orderBy,
|
||||
setOrderDirection,
|
||||
setOrderBy
|
||||
} = usePagination<IntegrationsOrderBy>(IntegrationsOrderBy.App, { initPerPage: 20 });
|
||||
|
||||
useEffect(() => {
|
||||
if (integrations?.some((integration) => integration.isSynced === false))
|
||||
setOrderBy(IntegrationsOrderBy.Status);
|
||||
}, []);
|
||||
|
||||
const environmentMap = new Map(environments.map((env) => [env.id, env]));
|
||||
|
||||
const filteredIntegrations = useMemo(
|
||||
() =>
|
||||
integrations
|
||||
.filter((integration) => {
|
||||
const { secretPath, envId, isSynced } = integration;
|
||||
|
||||
if (!filters.status.includes(IntegrationStatus.Synced) && isSynced) return false;
|
||||
if (!filters.status.includes(IntegrationStatus.NotSynced) && isSynced === false)
|
||||
return false;
|
||||
if (
|
||||
!filters.status.includes(IntegrationStatus.PendingSync) &&
|
||||
typeof isSynced !== "boolean"
|
||||
)
|
||||
return false;
|
||||
|
||||
if (!filters.integrations.includes(integration.integration)) return false;
|
||||
|
||||
if (!filters.environmentIds.includes(envId)) return false;
|
||||
|
||||
return (
|
||||
integration.integration
|
||||
.replace("-", " ")
|
||||
.toLowerCase()
|
||||
.includes(search.trim().toLowerCase()) ||
|
||||
secretPath.replace("-", " ").toLowerCase().includes(search.trim().toLowerCase()) ||
|
||||
getIntegrationDestination(integration)
|
||||
.toLowerCase()
|
||||
.includes(search.trim().toLowerCase()) ||
|
||||
environmentMap
|
||||
.get(envId)
|
||||
?.name.replace("-", " ")
|
||||
.toLowerCase()
|
||||
.includes(search.trim().toLowerCase())
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const [integrationOne, integrationTwo] =
|
||||
orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||
|
||||
switch (orderBy) {
|
||||
case IntegrationsOrderBy.SecretPath:
|
||||
return integrationOne.secretPath
|
||||
.toLowerCase()
|
||||
.localeCompare(integrationTwo.secretPath.toLowerCase());
|
||||
case IntegrationsOrderBy.Environment:
|
||||
return (environmentMap.get(integrationOne.envId)?.name ?? "-")
|
||||
.toLowerCase()
|
||||
.localeCompare(
|
||||
(environmentMap.get(integrationTwo.envId)?.name ?? "-").toLowerCase()
|
||||
);
|
||||
case IntegrationsOrderBy.Destination:
|
||||
return getIntegrationDestination(integrationOne)
|
||||
.toLowerCase()
|
||||
.localeCompare(getIntegrationDestination(integrationTwo).toLowerCase());
|
||||
case IntegrationsOrderBy.Status:
|
||||
if (typeof integrationOne.isSynced !== "boolean") return 1; // Place undefined at the end
|
||||
if (typeof integrationTwo.isSynced !== "boolean") return -1;
|
||||
|
||||
return Number(integrationOne.isSynced) - Number(integrationTwo.isSynced);
|
||||
case IntegrationsOrderBy.App:
|
||||
default:
|
||||
return integrationOne.integration
|
||||
.toLowerCase()
|
||||
.localeCompare(integrationTwo.integration.toLowerCase());
|
||||
}
|
||||
}),
|
||||
[integrations, orderDirection, search, orderBy, filters]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredIntegrations.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const handleSort = (column: IntegrationsOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
toggleOrderDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const getClassName = (col: IntegrationsOrderBy) =>
|
||||
twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||
|
||||
const getColSortIcon = (col: IntegrationsOrderBy) =>
|
||||
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||
|
||||
const isTableFiltered =
|
||||
filters.integrations.length !== initialFilters.integrations.length ||
|
||||
filters.environmentIds.length !== initialFilters.environmentIds.length ||
|
||||
filters.status.length !== initialFilters.status.length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search integrations..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Environments"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||
isTableFiltered && "border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<Tooltip content="Filter Integrations" className="mb-2">
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
|
||||
<DropdownMenuLabel>Status</DropdownMenuLabel>
|
||||
{Object.values(IntegrationStatus).map((status) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
status: prev.status.includes(status)
|
||||
? prev.status.filter((s) => s !== status)
|
||||
: [...prev.status, status]
|
||||
}));
|
||||
}}
|
||||
key={status}
|
||||
icon={
|
||||
filters.status.includes(status) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon
|
||||
icon={STATUS_ICON_MAP[status].icon}
|
||||
className={STATUS_ICON_MAP[status].className}
|
||||
/>
|
||||
<span className="capitalize">{status.replace("-", " ")}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuLabel>Integration</DropdownMenuLabel>
|
||||
{[...new Set(integrations.map(({ integration }) => integration))].map((integration) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
integrations: prev.integrations.includes(integration)
|
||||
? prev.integrations.filter((i) => i !== integration)
|
||||
: [...prev.integrations, integration]
|
||||
}));
|
||||
}}
|
||||
key={integration}
|
||||
icon={
|
||||
filters.integrations.includes(integration) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={`${cloudIntegrationMap.get(integration)!.name} integration`}
|
||||
src={`/images/integrations/${cloudIntegrationMap.get(integration)!.image}`}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<span className="capitalize">{cloudIntegrationMap.get(integration)!.name}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuLabel>Environment</DropdownMenuLabel>
|
||||
{environments.map((env) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
environmentIds: prev.environmentIds.includes(env.id)
|
||||
? prev.environmentIds.filter((i) => i !== env.id)
|
||||
: [...prev.environmentIds, env.id]
|
||||
}));
|
||||
}}
|
||||
key={env.id}
|
||||
icon={
|
||||
filters.environmentIds.includes(env.id) && (
|
||||
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<span className="capitalize">{env.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-[25%]">
|
||||
<div className="flex items-center">
|
||||
Integration
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(IntegrationsOrderBy.App)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(IntegrationsOrderBy.App)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(IntegrationsOrderBy.App)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/5">
|
||||
<div className="flex items-center">
|
||||
Source Path
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(IntegrationsOrderBy.SecretPath)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(IntegrationsOrderBy.SecretPath)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(IntegrationsOrderBy.SecretPath)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/5">
|
||||
<div className="flex items-center">
|
||||
Source Environment
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(IntegrationsOrderBy.Environment)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(IntegrationsOrderBy.Environment)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(IntegrationsOrderBy.Environment)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/5">
|
||||
<div className="flex items-center">
|
||||
Destination
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(IntegrationsOrderBy.Destination)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(IntegrationsOrderBy.Destination)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(IntegrationsOrderBy.Destination)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/5">
|
||||
<div className="flex items-center">
|
||||
Status
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(IntegrationsOrderBy.Status)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(IntegrationsOrderBy.Status)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(IntegrationsOrderBy.Status)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{filteredIntegrations.slice(offset, perPage * page).map((integration) => (
|
||||
<IntegrationRow
|
||||
cloudIntegration={cloudIntegrationMap.get(integration.integration)!}
|
||||
key={`integration-${integration.id}`}
|
||||
onManualSyncIntegration={() => {
|
||||
syncIntegration({
|
||||
workspaceId,
|
||||
id: integration.id,
|
||||
lastUsed: integration.lastUsed as string
|
||||
});
|
||||
}}
|
||||
onRemoveIntegration={() => onDeleteIntegration(integration)}
|
||||
integration={integration}
|
||||
environment={environmentMap.get(integration.envId)}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(filteredIntegrations.length) && (
|
||||
<Pagination
|
||||
count={filteredIntegrations.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && !filteredIntegrations?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
integrations.length
|
||||
? "No integrations match search..."
|
||||
: "This project has no integrations configured"
|
||||
}
|
||||
icon={integrations.length ? faSearch : faPlug}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./IntegrationsTable";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { IntegrationsSection } from "./IntegrationsSection";
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"name": "React",
|
||||
"slug": "react",
|
||||
"image": "React",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/react"
|
||||
},
|
||||
{
|
||||
"name": "Vue",
|
||||
"slug": "vue",
|
||||
"image": "Vue",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/vue"
|
||||
},
|
||||
{
|
||||
"name": "Express",
|
||||
"slug": "express",
|
||||
"image": "Express",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/express"
|
||||
},
|
||||
{
|
||||
"name": "Next.js",
|
||||
"slug": "nextjs",
|
||||
"image": "Next.js",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/nextjs"
|
||||
},
|
||||
{
|
||||
"name": "SvelteKit",
|
||||
"slug": "sveltekit",
|
||||
"image": "SvelteKit",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/sveltekit"
|
||||
},
|
||||
{
|
||||
"name": "Django",
|
||||
"slug": "django",
|
||||
"image": "Django",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/django"
|
||||
},
|
||||
{
|
||||
"name": "NestJS",
|
||||
"slug": "nestjs",
|
||||
"image": "NestJS",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/nestjs"
|
||||
},
|
||||
{
|
||||
"name": "Nuxt",
|
||||
"slug": "nuxt",
|
||||
"image": "Nuxt",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/nuxt"
|
||||
},
|
||||
{
|
||||
"name": "Gatsby",
|
||||
"slug": "gatsby",
|
||||
"image": "Gatsby",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/gatsby"
|
||||
},
|
||||
{
|
||||
"name": "Remix",
|
||||
"slug": "remix",
|
||||
"image": "Remix",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/remix"
|
||||
},
|
||||
{
|
||||
"name": "Vite",
|
||||
"slug": "vite",
|
||||
"image": "Vite",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/vite"
|
||||
},
|
||||
{
|
||||
"name": "Fiber",
|
||||
"slug": "fiber",
|
||||
"image": "Fiber",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/fiber"
|
||||
},
|
||||
{
|
||||
"name": "Flask",
|
||||
"slug": "flask",
|
||||
"image": "Flask",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/flask"
|
||||
},
|
||||
{
|
||||
"name": "Laravel",
|
||||
"slug": "laravel",
|
||||
"image": "Laravel",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/laravel"
|
||||
},
|
||||
{
|
||||
"name": "Rails",
|
||||
"slug": "rails",
|
||||
"image": "Rails",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/rails"
|
||||
}
|
||||
]
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
[
|
||||
{
|
||||
"name": "Docker",
|
||||
"slug": "docker",
|
||||
"image": "Docker",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/docker"
|
||||
},
|
||||
{
|
||||
"name": "Docker Compose",
|
||||
"slug": "docker-compose",
|
||||
"image": "Docker Compose",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/docker-compose"
|
||||
},
|
||||
{
|
||||
"name": "Kubernetes",
|
||||
"slug": "kubernetes",
|
||||
"image": "Kubernetes",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/kubernetes"
|
||||
},
|
||||
{
|
||||
"name": "Terraform",
|
||||
"slug": "terraform",
|
||||
"image": "Terraform",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/terraform"
|
||||
},
|
||||
{
|
||||
"name": "Jenkins",
|
||||
"slug": "jenkins",
|
||||
"image": "Jenkins",
|
||||
"docsLink": "https://infisical.com/docs/integrations/cicd/jenkins"
|
||||
},
|
||||
{
|
||||
"name": "Infisical Agent",
|
||||
"slug": "agent",
|
||||
"image": "Agent",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/infisical-agent"
|
||||
},
|
||||
{
|
||||
"name": "Amazon ECS",
|
||||
"slug": "ecs",
|
||||
"image": "ECS",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/ecs-with-agent"
|
||||
},
|
||||
{
|
||||
"name": "Ansible",
|
||||
"slug": "ansible",
|
||||
"image": "Ansible",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/ansible"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { IntegrationsListPage } from "./IntegrationsListPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/secret-manager/$projectId/_secret-manager-layout/integrations/"
|
||||
)({
|
||||
component: IntegrationsListPage
|
||||
});
|
||||
Reference in New Issue
Block a user