mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Feat: Integration UI improvements
This commit is contained in:
@@ -4,7 +4,7 @@ import { IntegrationsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { INTEGRATION } from "@app/lib/api-docs";
|
||||
import { removeTrailingSlash, shake } from "@app/lib/fn";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -154,6 +154,48 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:integrationId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get an integration by integration id",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
integration: IntegrationsSchema.extend({
|
||||
environment: z.object({
|
||||
slug: z.string().trim(),
|
||||
name: z.string().trim(),
|
||||
id: z.string().trim()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const integration = await server.services.integration.getIntegration({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.integrationId
|
||||
});
|
||||
|
||||
return { integration };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:integrationId",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
||||
@@ -19,6 +19,7 @@ import { TIntegrationDALFactory } from "./integration-dal";
|
||||
import {
|
||||
TCreateIntegrationDTO,
|
||||
TDeleteIntegrationDTO,
|
||||
TGetIntegrationDTO,
|
||||
TSyncIntegrationDTO,
|
||||
TUpdateIntegrationDTO
|
||||
} from "./integration-types";
|
||||
@@ -180,6 +181,27 @@ export const integrationServiceFactory = ({
|
||||
return updatedIntegration;
|
||||
};
|
||||
|
||||
const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
|
||||
const integration = await integrationDAL.findById(id);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
integration?.projectId || "",
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
|
||||
if (!integration) {
|
||||
throw new NotFoundError({
|
||||
message: "Integration not found"
|
||||
});
|
||||
}
|
||||
|
||||
return { ...integration, envId: integration.environment.id };
|
||||
};
|
||||
|
||||
const deleteIntegration = async ({
|
||||
actorId,
|
||||
id,
|
||||
@@ -289,6 +311,7 @@ export const integrationServiceFactory = ({
|
||||
updateIntegration,
|
||||
deleteIntegration,
|
||||
listIntegrationByProject,
|
||||
getIntegration,
|
||||
syncIntegration
|
||||
};
|
||||
};
|
||||
|
||||
@@ -39,6 +39,10 @@ export type TCreateIntegrationDTO = {
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetIntegrationDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateIntegrationDTO = {
|
||||
id: string;
|
||||
app?: string;
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
export { useCreateIntegration, useDeleteIntegration, useGetCloudIntegrations } from "./queries";
|
||||
export {
|
||||
useCreateIntegration,
|
||||
useDeleteIntegration,
|
||||
useGetCloudIntegrations,
|
||||
useGetIntegration
|
||||
} from "./queries";
|
||||
|
||||
@@ -5,9 +5,11 @@ import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { TCloudIntegration } from "./types";
|
||||
import { TCloudIntegration, TIntegrationWithEnv } from "./types";
|
||||
|
||||
export const integrationQueryKeys = {
|
||||
getIntegrations: () => ["integrations"] as const
|
||||
getIntegrations: () => ["integrations"] as const,
|
||||
getIntegration: (id: string) => ["integration", id] as const
|
||||
};
|
||||
|
||||
const fetchIntegrations = async () => {
|
||||
@@ -18,6 +20,14 @@ const fetchIntegrations = async () => {
|
||||
return data.integrationOptions;
|
||||
};
|
||||
|
||||
const fetchIntegration = async (id: string) => {
|
||||
const { data } = await apiRequest.get<{ integration: TIntegrationWithEnv }>(
|
||||
`/api/v1/integration/${id}`
|
||||
);
|
||||
|
||||
return data.integration;
|
||||
};
|
||||
|
||||
export const useGetCloudIntegrations = () =>
|
||||
useQuery({
|
||||
queryKey: integrationQueryKeys.getIntegrations(),
|
||||
@@ -128,6 +138,14 @@ export const useDeleteIntegration = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegration = (integrationId: string) => {
|
||||
return useQuery({
|
||||
enabled: Boolean(integrationId),
|
||||
queryKey: integrationQueryKeys.getIntegration(integrationId),
|
||||
queryFn: () => fetchIntegration(integrationId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useSyncIntegration = () => {
|
||||
return useMutation<{}, {}, { id: string; workspaceId: string; lastUsed: string }>({
|
||||
mutationFn: ({ id }) => apiRequest.post(`/api/v1/integration/${id}/sync`),
|
||||
|
||||
@@ -47,6 +47,14 @@ export type TIntegration = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TIntegrationWithEnv = TIntegration & {
|
||||
environment: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
};
|
||||
|
||||
export enum IntegrationSyncBehavior {
|
||||
OVERWRITE_TARGET = "overwrite-target",
|
||||
PREFER_TARGET = "prefer-target",
|
||||
|
||||
23
frontend/src/pages/integrations/details/[integrationId].tsx
Normal file
23
frontend/src/pages/integrations/details/[integrationId].tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { IntegrationDetails } from "@app/views/IntegrationsPage/DetailsPage/DetailsPage";
|
||||
|
||||
export default function IntegrationsDetailsPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Integration Details | Infisical</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<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} />
|
||||
</Head>
|
||||
<IntegrationDetails />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
IntegrationsDetailsPage.requireAuth = true;
|
||||
115
frontend/src/views/IntegrationsPage/DetailsPage/DetailsPage.tsx
Normal file
115
frontend/src/views/IntegrationsPage/DetailsPage/DetailsPage.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useRouter } from "next/router";
|
||||
import { faChevronLeft, faEllipsis, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useUser, useWorkspace } from "@app/context";
|
||||
import { useGetIntegration } from "@app/hooks/api";
|
||||
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IntegrationDetailsSection } from "./components/IntegrationDetailsSection";
|
||||
|
||||
// import { UserDetailsSection, UserOrgMembershipModal, UserProjectsSection } from "./components";
|
||||
|
||||
export const IntegrationDetails = () => {
|
||||
const router = useRouter();
|
||||
const integrationId = router.query.integrationId as string;
|
||||
|
||||
const { data: integration } = useGetIntegration(integrationId);
|
||||
|
||||
const projectId = useWorkspace().currentWorkspace?.id;
|
||||
const { mutateAsync: syncIntegration } = useSyncIntegration();
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"removeMember",
|
||||
"orgMembership",
|
||||
"deactivateMember",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
return integration ? (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
|
||||
<Button
|
||||
variant="link"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||
onClick={() => {
|
||||
router.push(`/integrations/${projectId}`);
|
||||
}}
|
||||
className="mb-4"
|
||||
>
|
||||
Integrations
|
||||
</Button>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-3xl font-semibold text-white">
|
||||
{integrationSlugNameMapping[integration.integration]} Integration
|
||||
</p>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await syncIntegration({
|
||||
id: integration.id,
|
||||
lastUsed: integration.lastUsed!,
|
||||
workspaceId: projectId!
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faRefresh} />
|
||||
Manually Sync
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Delete} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() => {}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
Delete Integration
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<IntegrationDetailsSection integration={integration} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
@@ -0,0 +1,241 @@
|
||||
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { FormLabel } from "@app/components/v2";
|
||||
import { IntegrationMappingBehavior, TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
|
||||
|
||||
type Props = {
|
||||
integration: TIntegrationWithEnv;
|
||||
};
|
||||
|
||||
export const IntegrationDetailsSection = ({ integration }: Props) => {
|
||||
const specifcQoveryDetails = () => {
|
||||
if (integration.integration !== "qovery") return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-col">
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Organization" />
|
||||
<div className="text-sm text-mineshaft-300">{integration?.owner || "-"}</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Project" />
|
||||
<div className="text-sm text-mineshaft-300">{integration?.targetService || "-"}</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<FormLabel
|
||||
className="text-sm font-semibold text-mineshaft-300"
|
||||
label="Target Environment"
|
||||
/>
|
||||
<div className="text-sm text-mineshaft-300">{integration?.targetEnvironment || "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const isNotAwsManagerOneToOneDetails = () => {
|
||||
const isAwsSecretManagerOneToOne =
|
||||
integration.integration === "aws-secret-manager" &&
|
||||
integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE;
|
||||
|
||||
if (isAwsSecretManagerOneToOne) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formLabel = () => {
|
||||
switch (integration.integration) {
|
||||
case "qovery":
|
||||
return integration.scope;
|
||||
case "circleci":
|
||||
case "terraform-cloud":
|
||||
return "Project";
|
||||
case "aws-secret-manager":
|
||||
return "Secret";
|
||||
case "aws-parameter-store":
|
||||
case "rundeck":
|
||||
return "Path";
|
||||
case "github":
|
||||
if (["github-env", "github-repo"].includes(integration.scope!)) {
|
||||
return "Repository";
|
||||
}
|
||||
return "Organization";
|
||||
|
||||
default:
|
||||
return "App";
|
||||
}
|
||||
};
|
||||
|
||||
const contents = () => {
|
||||
switch (integration.integration) {
|
||||
case "hashicorp-vault":
|
||||
return `${integration.app} - path: ${integration.path}`;
|
||||
case "github":
|
||||
if (integration.scope === "github-org") {
|
||||
return `${integration.owner}`;
|
||||
}
|
||||
return `${integration.owner}/${integration.app}`;
|
||||
|
||||
case "aws-parameter-store":
|
||||
case "rundeck":
|
||||
return `${integration.path}aaaaa`;
|
||||
|
||||
default:
|
||||
return `${integration.app}`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label={formLabel()} />
|
||||
<div className="text-sm text-mineshaft-300">{contents()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const targetEnvironmentDetails = () => {
|
||||
if (
|
||||
["vercel", "netlify", "railway", "gitlab", "teamcity", "bitbucket"].includes(
|
||||
integration.integration
|
||||
) ||
|
||||
(integration.integration === "github" && integration.scope === "github-env")
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<FormLabel
|
||||
className="text-sm font-semibold text-mineshaft-300"
|
||||
label="Target Environment"
|
||||
/>
|
||||
<div className="text-sm text-mineshaft-300">
|
||||
{integration.targetEnvironment || integration.targetEnvironmentId}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const generalIntegrationSpecificDetails = () => {
|
||||
if (integration.integration === "checkly" && integration.targetService) {
|
||||
return (
|
||||
<div>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Group" />
|
||||
<div className="text-sm text-mineshaft-300">{integration.targetService}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (integration.integration === "circleci" && integration.owner) {
|
||||
return (
|
||||
<div>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Organization" />
|
||||
<div className="text-sm text-mineshaft-300">{integration.owner}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (integration.integration === "terraform-cloud" && integration.targetService) {
|
||||
return (
|
||||
<div>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Category" />
|
||||
<div className="text-sm text-mineshaft-300">{integration.targetService}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (integration.integration === "checkly" || integration.integration === "github") {
|
||||
return (
|
||||
<div>
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Secret Suffix" />
|
||||
<div className="text-sm text-mineshaft-300">
|
||||
{integration?.metadata?.secretSuffix || "-"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Integration Details</h3>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{integrationSlugNameMapping[integration.integration]}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Sync Status</p>
|
||||
<div className="flex items-center">
|
||||
<p
|
||||
className={twMerge(
|
||||
"mr-2 text-sm font-medium",
|
||||
integration.isSynced ? "text-green-500" : "text-red-500"
|
||||
)}
|
||||
>
|
||||
{integration.isSynced ? "Synced" : "Not Synced"}
|
||||
</p>
|
||||
<FontAwesomeIcon
|
||||
size="sm"
|
||||
className={twMerge(integration.isSynced ? "text-green-500" : "text-red-500")}
|
||||
icon={integration.isSynced ? faCheckCircle : faCircleXmark}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{!integration.isSynced && integration.syncMessage && (
|
||||
<>
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Latest sync error</p>
|
||||
<p className="text-sm text-mineshaft-300">{integration.syncMessage}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Connection</h3>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<FormLabel className="my-2" label="Source" />
|
||||
|
||||
<div className="space-y-2 rounded-lg border border-mineshaft-700 bg-mineshaft-800 p-2">
|
||||
<div className="flex flex-col">
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Environment" />
|
||||
<div className="text-sm text-mineshaft-300">{integration.environment.name}</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Secret Path" />
|
||||
<div className="text-sm text-mineshaft-300">{integration.secretPath}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormLabel className="my-2" label="Destination" />
|
||||
<div className="space-y-2 rounded-lg border border-mineshaft-700 bg-mineshaft-800 p-2">
|
||||
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Platform" />
|
||||
<div className="text-sm text-mineshaft-300">
|
||||
{integrationSlugNameMapping[integration.integration]}
|
||||
</div>
|
||||
|
||||
{specifcQoveryDetails()}
|
||||
{isNotAwsManagerOneToOneDetails()}
|
||||
{targetEnvironmentDetails()}
|
||||
{generalIntegrationSpecificDetails()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,6 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faArrowRight,
|
||||
faCalendarCheck,
|
||||
@@ -28,9 +31,12 @@ export const ConfiguredIntegrationItem = ({
|
||||
onRemoveIntegration,
|
||||
onManualSyncIntegration
|
||||
}: IProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3"
|
||||
onClick={() => router.push(`/integrations/details/${integration.id}`)}
|
||||
className="max-w-8xl flex cursor-pointer justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3 transition-all hover:bg-mineshaft-700"
|
||||
key={`integration-${integration?.id.toString()}`}
|
||||
>
|
||||
<div className="flex">
|
||||
|
||||
Reference in New Issue
Block a user