From 6c533f89d317257e8d7738894fbe49240048cdc4 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 3 Dec 2024 14:53:33 -0800 Subject: [PATCH] feature: high-level integrations refactor --- .../public/images/integrations/GitHub.png | Bin 8640 -> 23562 bytes .../gcp-secret-manager/authorize.tsx | 6 + .../github/auth-mode-selection.tsx | 10 + .../pages/integrations/gitlab/authorize.tsx | 6 + .../integrations/select-integration-auth.tsx | 6 + .../IntegrationPage.utils.tsx | 47 ++ .../IntegrationsPage/IntegrationsPage.tsx | 91 +++- .../CloudIntegrationSection.tsx | 29 +- .../FrameworkIntegrationSection.tsx | 55 +-- .../IntegrationsSection.tsx | 79 ++-- .../components/IntegrationDetails.tsx | 137 ++++++ .../components/IntegrationRow.tsx | 191 ++++++++ .../components/IntegrationsTable.tsx | 415 ++++++++++++++++++ .../IntegrationsSection/components/index.ts | 1 + 14 files changed, 968 insertions(+), 105 deletions(-) create mode 100644 frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/IntegrationDetails.tsx create mode 100644 frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/IntegrationRow.tsx create mode 100644 frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/IntegrationsTable.tsx create mode 100644 frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/index.ts diff --git a/frontend/public/images/integrations/GitHub.png b/frontend/public/images/integrations/GitHub.png index 9490ffc6d2e158b266f719d58365905c1c5060cb..7492fcb54ae190dd7ec9861b6de90d0818470a26 100644 GIT binary patch literal 23562 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 literal 8640 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 diff --git a/frontend/src/pages/integrations/gcp-secret-manager/authorize.tsx b/frontend/src/pages/integrations/gcp-secret-manager/authorize.tsx index ad82a218c..cea246f2f 100644 --- a/frontend/src/pages/integrations/gcp-secret-manager/authorize.tsx +++ b/frontend/src/pages/integrations/gcp-secret-manager/authorize.tsx @@ -13,6 +13,7 @@ import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; import { useGetCloudIntegrations, useSaveIntegrationAccessToken } from "@app/hooks/api"; +import { createIntegrationMissingEnvVarsNotification } from "@app/views/IntegrationsPage/IntegrationPage.utils"; import { Button, Card, CardTitle, FormControl, TextArea } from "../../../components/v2"; @@ -46,6 +47,11 @@ export default function GCPSecretManagerAuthorizeIntegrationPage() { const state = crypto.randomBytes(16).toString("hex"); localStorage.setItem("latestCSRFToken", state); + if (!integrationOption.clientId) { + createIntegrationMissingEnvVarsNotification(integrationOption.slug); + return; + } + const link = `https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/cloud-platform&response_type=code&access_type=offline&state=${state}&redirect_uri=${window.location.origin}/integrations/gcp-secret-manager/oauth2/callback&client_id=${integrationOption.clientId}`; window.location.assign(link); }; diff --git a/frontend/src/pages/integrations/github/auth-mode-selection.tsx b/frontend/src/pages/integrations/github/auth-mode-selection.tsx index 5fcb4f0dd..4a512ff60 100644 --- a/frontend/src/pages/integrations/github/auth-mode-selection.tsx +++ b/frontend/src/pages/integrations/github/auth-mode-selection.tsx @@ -18,6 +18,7 @@ import { SelectItem } from "@app/components/v2"; import { useGetCloudIntegrations } from "@app/hooks/api"; +import { createIntegrationMissingEnvVarsNotification } from "@app/views/IntegrationsPage/IntegrationPage.utils"; enum AuthMethod { APP = "APP", @@ -84,6 +85,15 @@ export default function GithubIntegrationAuthModeSelectionPage() { if (selectedAuthMethod === AuthMethod.APP) { router.push("/integrations/select-integration-auth?integrationSlug=github"); } else { + if (!githubIntegration?.clientId) { + createIntegrationMissingEnvVarsNotification( + "githubactions", + "cicd", + "connecting-with-github-oauth" + ); + return; + } + const state = crypto.randomBytes(16).toString("hex"); localStorage.setItem("latestCSRFToken", state); diff --git a/frontend/src/pages/integrations/gitlab/authorize.tsx b/frontend/src/pages/integrations/gitlab/authorize.tsx index 380aad08e..d6c80c2c1 100644 --- a/frontend/src/pages/integrations/gitlab/authorize.tsx +++ b/frontend/src/pages/integrations/gitlab/authorize.tsx @@ -10,6 +10,7 @@ import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; import { useGetCloudIntegrations } from "@app/hooks/api"; +import { createIntegrationMissingEnvVarsNotification } from "@app/views/IntegrationsPage/IntegrationPage.utils"; import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; @@ -37,6 +38,11 @@ export default function GitLabAuthorizeIntegrationPage() { if (!integrationOption) return; + if (!integrationOption.clientId) { + createIntegrationMissingEnvVarsNotification(integrationOption.slug, "cicd"); + return; + } + const baseURL = (gitLabURL as string).trim() === "" ? "https://gitlab.com" : (gitLabURL as string).trim(); diff --git a/frontend/src/pages/integrations/select-integration-auth.tsx b/frontend/src/pages/integrations/select-integration-auth.tsx index a9d2766a4..1f9f17afa 100644 --- a/frontend/src/pages/integrations/select-integration-auth.tsx +++ b/frontend/src/pages/integrations/select-integration-auth.tsx @@ -13,6 +13,7 @@ import { useGetOrgIntegrationAuths } from "@app/hooks/api"; import { IntegrationAuth } from "@app/hooks/api/types"; +import { createIntegrationMissingEnvVarsNotification } from "@app/views/IntegrationsPage/IntegrationPage.utils"; export default function SelectIntegrationAuthPage() { const router = useRouter(); @@ -86,6 +87,11 @@ export default function SelectIntegrationAuthPage() { localStorage.setItem("latestCSRFToken", state); if (integrationSlug === "github") { + if (!currentIntegration?.clientSlug) { + createIntegrationMissingEnvVarsNotification("githubactions", "cicd"); + return; + } + // for now we only handle Github apps window.location.assign( `https://github.com/apps/${currentIntegration?.clientSlug}/installations/new?state=${state}` diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx index b20b15a09..e1a1ff6fb 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx @@ -1,5 +1,6 @@ import crypto from "crypto"; +import { createNotification } from "@app/components/notifications"; import { TCloudIntegration, UserWsKeyPair } from "@app/hooks/api/types"; import { @@ -30,6 +31,28 @@ export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) = return { encryptedKey: ciphertext, nonce }; }; +export const createIntegrationMissingEnvVarsNotification = ( + slug: string, + type: "cloud" | "cicd" = "cloud", + hashtag?: string +) => + createNotification({ + type: "error", + text: ( + + Click here to view docs + + ), + title: "Missing Environment Variables" + }); + export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => { try { // generate CSRF token for OAuth2 code-token exchange integrations @@ -42,9 +65,17 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => link = `${window.location.origin}/integrations/gcp-secret-manager/authorize`; break; case "azure-key-vault": + if (!integrationOption.clientId) { + createIntegrationMissingEnvVarsNotification(integrationOption.slug); + return; + } 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 "azure-app-configuration": + if (!integrationOption.clientId) { + createIntegrationMissingEnvVarsNotification(integrationOption.slug); + return; + } 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}`; break; case "aws-parameter-store": @@ -54,12 +85,24 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => link = `${window.location.origin}/integrations/aws-secret-manager/authorize`; break; case "heroku": + if (!integrationOption.clientId) { + createIntegrationMissingEnvVarsNotification(integrationOption.slug); + return; + } link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`; break; case "vercel": + if (!integrationOption.clientSlug) { + createIntegrationMissingEnvVarsNotification(integrationOption.slug); + return; + } link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`; break; case "netlify": + if (!integrationOption.clientId) { + createIntegrationMissingEnvVarsNotification(integrationOption.slug); + return; + } 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": @@ -111,6 +154,10 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => link = `${window.location.origin}/integrations/cloudflare-workers/authorize`; break; case "bitbucket": + if (!integrationOption.clientId) { + createIntegrationMissingEnvVarsNotification(integrationOption.slug, "cicd"); + return; + } 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}`; break; case "codefresh": diff --git a/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx b/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx index e44fd2539..3249e1eb5 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationsPage.tsx @@ -1,6 +1,8 @@ -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; +import { motion } from "framer-motion"; import { createNotification } from "@app/components/notifications"; +import { ContentLoader } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { withProjectPermission } from "@app/hoc"; import { @@ -28,11 +30,17 @@ type Props = { }>; }; +enum IntegrationView { + List = "list", + New = "new" +} + export const IntegrationsPage = withProjectPermission( ({ frameworkIntegrations, infrastructureIntegrations }: Props) => { const { currentWorkspace } = useWorkspace(); const workspaceId = currentWorkspace?.id || ""; const environments = currentWorkspace?.environments || []; + const [view, setView] = useState(IntegrationView.New); const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } = useGetCloudIntegrations(); @@ -56,7 +64,8 @@ export const IntegrationsPage = withProjectPermission( const { data: integrations, isLoading: isIntegrationLoading, - isFetching: isIntegrationFetching + isFetching: isIntegrationFetching, + isFetched: isIntegrationsFetched } = useGetWorkspaceIntegrations(workspaceId); const { mutateAsync: deleteIntegration } = useDeleteIntegration(); @@ -89,6 +98,10 @@ export const IntegrationsPage = withProjectPermission( 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; @@ -150,26 +163,64 @@ export const IntegrationsPage = withProjectPermission( } }; + if (isIntegrationLoading || isCloudIntegrationsLoading) + return ( +
+ +
+ ); + return ( -
- - - - +
+
+ {view === IntegrationView.List ? ( + + setView(IntegrationView.New)} + isLoading={isIntegrationLoading} + integrations={integrations} + environments={environments} + onIntegrationDelete={handleIntegrationDelete} + workspaceId={workspaceId} + /> + + ) : ( + + setView(IntegrationView.List) : undefined + } + isLoading={isCloudIntegrationsLoading || isIntegrationAuthLoading} + cloudIntegrations={cloudIntegrations} + integrationAuths={integrationAuths} + onIntegrationStart={handleProviderIntegrationStart} + onIntegrationRevoke={handleIntegrationAuthRevoke} + /> + + + + )} +
); }, - { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Integrations } + { + action: ProjectPermissionActions.Read, + subject: ProjectPermissionSub.Integrations + } ); diff --git a/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx b/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx index 164470289..b9d51c303 100644 --- a/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx @@ -1,11 +1,11 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faCheck, faChevronLeft, 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 { DeleteActionModal, Skeleton, Tooltip } from "@app/components/v2"; +import { Button, DeleteActionModal, Skeleton, Tooltip } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, @@ -22,6 +22,7 @@ type Props = { onIntegrationStart: (slug: string) => void; // cb: handle popUpClose child->parent communication pattern onIntegrationRevoke: (slug: string, cb: () => void) => void; + onViewActiveIntegrations?: () => void; }; type TRevokeIntegrationPopUp = { provider: string }; @@ -31,7 +32,8 @@ export const CloudIntegrationSection = ({ cloudIntegrations = [], integrationAuths = {}, onIntegrationStart, - onIntegrationRevoke + onIntegrationRevoke, + onViewActiveIntegrations }: Props) => { const { t } = useTranslation(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -60,11 +62,19 @@ export const CloudIntegrationSection = ({ )}
+ {onViewActiveIntegrations && ( + + )}

{t("integrations.cloud-integrations")}

{t("integrations.click-to-start")}

- -
+
{isLoading && Array.from({ length: 12 }).map((_, index) => ( @@ -79,7 +89,7 @@ export const CloudIntegrationSection = ({ 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`} + } 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 ( @@ -100,11 +110,12 @@ export const CloudIntegrationSection = ({ > integration logo -
+
{cloudIntegration.name}
{cloudIntegration.isAvailable && diff --git a/frontend/src/views/IntegrationsPage/components/FrameworkIntegrationSection/FrameworkIntegrationSection.tsx b/frontend/src/views/IntegrationsPage/components/FrameworkIntegrationSection/FrameworkIntegrationSection.tsx index 3b1df9bdd..a4e6bb586 100644 --- a/frontend/src/views/IntegrationsPage/components/FrameworkIntegrationSection/FrameworkIntegrationSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/FrameworkIntegrationSection/FrameworkIntegrationSection.tsx @@ -23,34 +23,29 @@ export const FrameworkIntegrationSection = ({ frameworks }: Props) => {

{t("integrations.framework-integrations")}

{t("integrations.click-to-setup")}

-
+
{sortedFrameworks.map((framework) => ( -
1 ? "px-1 text-sm" : "px-2 text-xl" - } w-full max-w-xs text-center`} - > - {framework?.image && ( - integration logo - )} - {framework?.name && framework?.image &&
} - {framework?.name && framework.name} -
+ {framework?.image && ( + integration logo + )} + {framework?.name && ( +
+ {framework.name} +
+ )}
))} { href="https://infisical.com/docs/cli/commands/run" 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" + 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" > -
- -
+ +
CLI
@@ -73,13 +65,10 @@ export const FrameworkIntegrationSection = ({ frameworks }: Props) => { href="https://infisical.com/docs/sdks/overview" 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" + 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" > -
- -
+ +
SDKs
diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx index 2fbcca15d..a09535f03 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -1,13 +1,16 @@ -import { Checkbox, DeleteActionModal, EmptyState, Skeleton } from "@app/components/v2"; -import { usePopUp, useToggle } from "@app/hooks"; -import { useSyncIntegration } from "@app/hooks/api/integrations/queries"; -import { TIntegration } from "@app/hooks/api/types"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { ConfiguredIntegrationItem } from "./ConfiguredIntegrationItem"; +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, @@ -15,6 +18,7 @@ type Props = { cb: () => void ) => Promise; workspaceId: string; + onAddIntegration: () => void; }; export const IntegrationsSection = ({ @@ -22,58 +26,47 @@ export const IntegrationsSection = ({ environments = [], isLoading, onIntegrationDelete, - workspaceId + workspaceId, + onAddIntegration, + cloudIntegrations = [] }: Props) => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "deleteConfirmation", "deleteSecretsConfirmation" ] as const); - const { mutate: syncIntegration } = useSyncIntegration(); const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false); return (
-

Current Integrations

+

Integrations

Manage integrations with third-party services.

- {isLoading && ( -
- +
+
+

Active Integrations

+
- )} - - {!isLoading && !integrations.length && ( -
- -
- )} - {!isLoading && ( -
- {integrations?.map((integration) => ( - { - syncIntegration({ - workspaceId, - id: integration.id, - lastUsed: integration.lastUsed as string - }); - }} - onRemoveIntegration={() => { - setShouldDeleteSecrets.off(); - handlePopUpOpen("deleteConfirmation", integration); - }} - integration={integration} - environments={environments} - /> - ))} -
- )} + { + setShouldDeleteSecrets.off(); + handlePopUpOpen("deleteConfirmation", integration); + }} + /> +
{ + return ( +
+ {integration.integration === "octopus-deploy" && ( +
+ +
+ {integration.targetEnvironment || integration.targetEnvironmentId} +
+
+ )} + {integration.integration === "qovery" && ( + <> +
+ +
{integration?.owner || "-"}
+
+
+ +
{integration?.targetService || "-"}
+
+
+ +
{integration?.targetEnvironment || "-"}
+
+ + )} + {!( + integration.integration === "aws-secret-manager" && + integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE + ) && ( +
+ +
+ {(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} +
+
+ )} + {(integration.integration === "vercel" || + integration.integration === "netlify" || + integration.integration === "railway" || + integration.integration === "gitlab" || + integration.integration === "teamcity" || + (integration.integration === "github" && integration.scope === "github-env")) && ( +
+ +
+ {integration.targetEnvironment || integration.targetEnvironmentId} +
+
+ )} + {integration.integration === "bitbucket" && ( + <> + {integration.targetServiceId && ( +
+ +
+ {integration.targetService || integration.targetServiceId} +
+
+ )} +
+ +
+ {integration.targetEnvironment || integration.targetEnvironmentId} +
+
+ + )} + {integration.integration === "checkly" && integration.targetService && ( +
+ +
{integration.targetService}
+
+ )} + {integration.integration === "circleci" && integration.owner && ( +
+ +
{integration.owner}
+
+ )} + {integration.integration === "terraform-cloud" && integration.targetService && ( +
+ +
{integration.targetService}
+
+ )} + {(integration.integration === "checkly" || integration.integration === "github") && ( +
+ +
{integration?.metadata?.secretSuffix || "-"}
+
+ )} + {integration.integration === "github" && integration.metadata?.githubVisibility ? ( +
+ {/* 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"} +
+ ) : undefined} +
+ ); +}; diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/IntegrationRow.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/IntegrationRow.tsx new file mode 100644 index 000000000..5579c7f76 --- /dev/null +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/IntegrationRow.tsx @@ -0,0 +1,191 @@ +import { useMemo } from "react"; +import { useRouter } from "next/router"; +import { + faCalendarCheck, + faCheck, + faInfoCircle, + faRefresh, + faTrash, + faWarning, + faXmark +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Badge, IconButton, Td, Tooltip, Tr } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { TCloudIntegration } from "@app/hooks/api/integrations/types"; +import { TIntegration } from "@app/hooks/api/types"; + +import { 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 router = useRouter(); + + const { id, secretPath, syncMessage, isSynced } = integration; + + const failureMessage = useMemo(() => { + if (isSynced === false) { + if (syncMessage) + try { + // format if json + return JSON.stringify(JSON.parse(syncMessage), null, 2); + } catch (e) { + return syncMessage; + } + + return "An Unknown Error Occurred."; + } + return null; + }, [isSynced, syncMessage]); + + return ( + router.push(`/integrations/details/${integration.id}`)} + className="group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700" + key={`integration-${id}`} + > + +
+ {`${cloudIntegration?.name} + {cloudIntegration?.name} +
+ + + +

{secretPath}

+
{" "} + + {environment?.name ?? "-"} + +
+

+ {(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} +

+ } + > + + +
+ + + {" "} + {typeof integration.isSynced !== "boolean" ? ( + + Pending Sync + + ) : ( + + {integration.lastUsed && ( +
+
+ +
Last Synced
+
+
+ {format(new Date(integration.lastUsed!), "yyyy-MM-dd, hh:mm aaa")} +
+
+ )} + {failureMessage && ( +
+
+ +
Failure Reason
+
+
{failureMessage}
+
+ )} +
+ } + > +
+ +
+ +
{integration.isSynced ? "Synced" : "Not Synced"}
+
+
+
+ + )} + + +
+ + { + e.stopPropagation(); + onManualSyncIntegration(); + }} + ariaLabel="sync" + colorSchema="secondary" + variant="plain" + > + + + + + {(isAllowed: boolean) => ( + + { + e.stopPropagation(); + onRemoveIntegration(); + }} + ariaLabel="delete" + isDisabled={!isAllowed} + colorSchema="danger" + variant="plain" + > + + + + )} + +
+ + + ); +}; diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/IntegrationsTable.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/IntegrationsTable.tsx new file mode 100644 index 000000000..f61624451 --- /dev/null +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/IntegrationsTable.tsx @@ -0,0 +1,415 @@ +import { 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 { 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" +} + +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(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.Status, { initPerPage: 20 }); + + const environmentMap = new Map(environments.map((env) => [env.id, env])); + + const filteredIntegrations = useMemo( + () => + integrations + .filter(({ integration, secretPath, envId, isSynced }) => { + 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)) return false; + + return ( + integration.replace("-", " ").toLowerCase().includes(search.trim().toLowerCase()) || + secretPath.replace("-", " ").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.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 ( +
+
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search integrations..." + className="flex-1" + /> + + + + + + + + + + Status + {Object.values(IntegrationStatus).map((status) => ( + { + 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) && ( + + ) + } + iconPos="right" + > +
+ + {status.replace("-", " ")} +
+
+ ))} + Integration + {[...new Set(integrations.map(({ integration }) => integration))].map((integration) => ( + { + 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) && ( + + ) + } + iconPos="right" + > +
+ {`${cloudIntegrationMap.get(integration)!.name} + {cloudIntegrationMap.get(integration)!.name} +
+
+ ))} + Environment + {environments.map((env) => ( + { + e.preventDefault(); + setFilters((prev) => ({ + ...prev, + integrations: 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) && ( + + ) + } + iconPos="right" + > + {env.name} + + ))} +
+
+
+ + + + + + + + + + + + + {filteredIntegrations.slice(offset, perPage * page).map((integration) => ( + { + syncIntegration({ + workspaceId, + id: integration.id, + lastUsed: integration.lastUsed as string + }); + }} + onRemoveIntegration={() => onDeleteIntegration(integration)} + integration={integration} + environment={environmentMap.get(integration.envId)} + /> + ))} + +
+
+ Integration + handleSort(IntegrationsOrderBy.App)} + > + + +
+
+
+ Source Path + handleSort(IntegrationsOrderBy.SecretPath)} + > + + +
+
+
+ Source Environment + handleSort(IntegrationsOrderBy.Environment)} + > + + +
+
Destination +
+ Status + handleSort(IntegrationsOrderBy.Status)} + > + + +
+
+
+ {Boolean(filteredIntegrations.length) && ( + + )} + {!isLoading && !filteredIntegrations?.length && ( + + )} +
+
+ ); +}; diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/index.ts b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/index.ts new file mode 100644 index 000000000..d9567592c --- /dev/null +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/index.ts @@ -0,0 +1 @@ +export * from "./IntegrationsTable";