mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 14:27:30 +00:00
Merge pull request #684 from akhilmhdh/feat/integrations-page-revamp
integrations page revamp
This commit is contained in:
@@ -6,7 +6,14 @@ module.exports = {
|
|||||||
'@storybook/addon-essentials',
|
'@storybook/addon-essentials',
|
||||||
'@storybook/addon-interactions',
|
'@storybook/addon-interactions',
|
||||||
'storybook-dark-mode',
|
'storybook-dark-mode',
|
||||||
'@storybook/addon-postcss'
|
{
|
||||||
|
name: '@storybook/addon-styling',
|
||||||
|
options: {
|
||||||
|
postCss: {
|
||||||
|
implementation: require('postcss')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
framework: {
|
framework: {
|
||||||
name: '@storybook/nextjs',
|
name: '@storybook/nextjs',
|
||||||
|
|||||||
Generated
+4493
-3451
File diff suppressed because it is too large
Load Diff
+13
-13
@@ -7,7 +7,7 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"start:docker": "next build && next start",
|
"start:docker": "next build && next start",
|
||||||
"lint": "eslint --ext js,ts,tsx ./src",
|
"lint": "eslint --ext js,ts,tsx ./src",
|
||||||
"lint-and-fix": "eslint --fix --ext js,ts,tsx ./src",
|
"lint:fix": "eslint --fix --ext js,ts,tsx ./src",
|
||||||
"type-check": "tsc --project tsconfig.json",
|
"type-check": "tsc --project tsconfig.json",
|
||||||
"storybook": "storybook dev -p 6006 -s ./public",
|
"storybook": "storybook dev -p 6006 -s ./public",
|
||||||
"build-storybook": "storybook build"
|
"build-storybook": "storybook build"
|
||||||
@@ -62,8 +62,8 @@
|
|||||||
"infisical-node": "^1.0.37",
|
"infisical-node": "^1.0.37",
|
||||||
"jspdf": "^2.5.1",
|
"jspdf": "^2.5.1",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
"lottie-react": "^2.4.0",
|
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
|
"lottie-react": "^2.4.0",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"next": "^12.3.4",
|
"next": "^12.3.4",
|
||||||
"posthog-js": "^1.58.0",
|
"posthog-js": "^1.58.0",
|
||||||
@@ -91,14 +91,14 @@
|
|||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-essentials": "^7.0.0-beta.30",
|
"@storybook/addon-essentials": "^7.0.23",
|
||||||
"@storybook/addon-interactions": "^7.0.0-beta.30",
|
"@storybook/addon-interactions": "^7.0.23",
|
||||||
"@storybook/addon-links": "^7.0.0-beta.30",
|
"@storybook/addon-links": "^7.0.23",
|
||||||
"@storybook/addon-postcss": "^2.0.0",
|
"@storybook/addon-styling": "^1.3.0",
|
||||||
"@storybook/blocks": "^7.0.0-beta.30",
|
"@storybook/blocks": "^7.0.23",
|
||||||
"@storybook/nextjs": "^7.0.0-beta.30",
|
"@storybook/nextjs": "^7.0.23",
|
||||||
"@storybook/react": "^7.0.0-beta.30",
|
"@storybook/react": "^7.0.23",
|
||||||
"@storybook/testing-library": "^0.0.13",
|
"@storybook/testing-library": "^0.2.0",
|
||||||
"@tailwindcss/typography": "^0.5.4",
|
"@tailwindcss/typography": "^0.5.4",
|
||||||
"@types/jsrp": "^0.2.4",
|
"@types/jsrp": "^0.2.4",
|
||||||
"@types/node": "18.11.9",
|
"@types/node": "18.11.9",
|
||||||
@@ -118,12 +118,12 @@
|
|||||||
"eslint-plugin-react": "^7.32.0",
|
"eslint-plugin-react": "^7.32.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
"eslint-plugin-storybook": "^0.6.10",
|
"eslint-plugin-storybook": "^0.6.12",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
"prettier": "^2.8.3",
|
"prettier": "^2.8.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||||
"storybook": "^7.0.0-beta.30",
|
"storybook": "^7.0.23",
|
||||||
"storybook-dark-mode": "^2.0.5",
|
"storybook-dark-mode": "^3.0.0",
|
||||||
"tailwindcss": "3.2",
|
"tailwindcss": "3.2",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Props = {
|
|||||||
onChange?: (isOpen: boolean) => void;
|
onChange?: (isOpen: boolean) => void;
|
||||||
deleteKey: string;
|
deleteKey: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
subTitle?: string;
|
||||||
onDeleteApproved: () => Promise<void>;
|
onDeleteApproved: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,7 +23,8 @@ export const DeleteActionModal = ({
|
|||||||
onChange,
|
onChange,
|
||||||
deleteKey,
|
deleteKey,
|
||||||
onDeleteApproved,
|
onDeleteApproved,
|
||||||
title
|
title,
|
||||||
|
subTitle = "This action is irreversible!"
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const [inputData, setInputData] = useState("");
|
const [inputData, setInputData] = useState("");
|
||||||
const [isLoading, setIsLoading] = useToggle();
|
const [isLoading, setIsLoading] = useToggle();
|
||||||
@@ -52,7 +54,7 @@ export const DeleteActionModal = ({
|
|||||||
>
|
>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={title}
|
title={title}
|
||||||
subTitle="This action is irreversible!"
|
subTitle={subTitle}
|
||||||
footerContent={
|
footerContent={
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export type FormLabelProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const FormLabel = ({ id, label, isRequired }: FormLabelProps) => (
|
export const FormLabel = ({ id, label, isRequired }: FormLabelProps) => (
|
||||||
<Label.Root className="mb-1 ml-0.5 block text-sm font-medium text-mineshaft-300" htmlFor={id}>
|
<Label.Root className="mb-0.5 ml-1 block text-sm font-normal text-mineshaft-400" htmlFor={id}>
|
||||||
{label}
|
{label}
|
||||||
{isRequired && <span className="ml-1 text-red">*</span>}
|
{isRequired && <span className="ml-1 text-red">*</span>}
|
||||||
</Label.Root>
|
</Label.Root>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export {
|
export {
|
||||||
useGetAuthToken,
|
useGetAuthToken,
|
||||||
useGetCommonPasswords,
|
useGetCommonPasswords,
|
||||||
useRevokeAllSessions,
|
useRevokeAllSessions,
|
||||||
useSendMfaToken,
|
useSendMfaToken,
|
||||||
useVerifyMfaToken} from "./queries"
|
useVerifyMfaToken
|
||||||
|
} from "./queries";
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { useGetWorkspaceBot, useUpdateBotActiveStatus } from "./queries";
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { TBot, TSetBotActiveStatusDto } from "./types";
|
||||||
|
|
||||||
|
const queryKeys = {
|
||||||
|
getBot: (workspaceId: string) => [{ workspaceId }, "bot"] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchWorkspaceBot = async (workspaceId: string) => {
|
||||||
|
const { data } = await apiRequest.get<{ bot: TBot }>(`/api/v1/bot/${workspaceId}`);
|
||||||
|
return data.bot;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetWorkspaceBot = (workspaceId: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: queryKeys.getBot(workspaceId),
|
||||||
|
queryFn: () => fetchWorkspaceBot(workspaceId),
|
||||||
|
enabled: Boolean(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
// mutation
|
||||||
|
|
||||||
|
export const useUpdateBotActiveStatus = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, TSetBotActiveStatusDto>({
|
||||||
|
mutationFn: ({ botId, isActive, botKey }) =>
|
||||||
|
apiRequest.patch(`/api/v1/bot/${botId}/active`, {
|
||||||
|
isActive,
|
||||||
|
botKey
|
||||||
|
}),
|
||||||
|
onSuccess: (_, { workspaceId }) => {
|
||||||
|
queryClient.invalidateQueries(queryKeys.getBot(workspaceId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export type TBot = {
|
||||||
|
_id: string;
|
||||||
|
name: string;
|
||||||
|
workspace: string;
|
||||||
|
isActive: boolean;
|
||||||
|
publicKey: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
__v: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSetBotActiveStatusDto = {
|
||||||
|
workspaceId: string;
|
||||||
|
botId: string;
|
||||||
|
isActive: boolean;
|
||||||
|
botKey?: {
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
|
export * from "./bots";
|
||||||
export * from "./incidentContacts";
|
export * from "./incidentContacts";
|
||||||
|
export * from "./integrationAuth";
|
||||||
|
export * from "./integrations";
|
||||||
export * from "./keys";
|
export * from "./keys";
|
||||||
export * from "./organization";
|
export * from "./organization";
|
||||||
export * from "./secretFolders";
|
export * from "./secretFolders";
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
export {
|
export {
|
||||||
useGetIntegrationAuthApps,
|
useDeleteIntegrationAuth,
|
||||||
useGetIntegrationAuthById,
|
useGetIntegrationAuthApps,
|
||||||
useGetIntegrationAuthRailwayEnvironments,
|
useGetIntegrationAuthById,
|
||||||
useGetIntegrationAuthRailwayServices,
|
useGetIntegrationAuthRailwayEnvironments,
|
||||||
useGetIntegrationAuthTeams,
|
useGetIntegrationAuthRailwayServices,
|
||||||
useGetIntegrationAuthVercelBranches} from "./queries";
|
useGetIntegrationAuthTeams,
|
||||||
|
useGetIntegrationAuthVercelBranches
|
||||||
|
} from "./queries";
|
||||||
|
|||||||
@@ -1,204 +1,237 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import {
|
import { workspaceKeys } from "../workspace/queries";
|
||||||
App,
|
import { App, Environment, IntegrationAuth, Service, Team } from "./types";
|
||||||
Environment,
|
|
||||||
IntegrationAuth,
|
|
||||||
Service,
|
|
||||||
Team
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
const integrationAuthKeys = {
|
const integrationAuthKeys = {
|
||||||
getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuth"] as const,
|
getIntegrationAuthById: (integrationAuthId: string) =>
|
||||||
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) => [{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
|
[{ integrationAuthId }, "integrationAuth"] as const,
|
||||||
getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuthTeams"] as const,
|
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) =>
|
||||||
getIntegrationAuthVercelBranches: ({
|
[{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
|
||||||
integrationAuthId,
|
getIntegrationAuthTeams: (integrationAuthId: string) =>
|
||||||
appId,
|
[{ integrationAuthId }, "integrationAuthTeams"] as const,
|
||||||
}: {
|
getIntegrationAuthVercelBranches: ({
|
||||||
integrationAuthId: string;
|
|
||||||
appId: string;
|
|
||||||
}) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
|
|
||||||
getIntegrationAuthRailwayEnvironments: ({
|
|
||||||
integrationAuthId,
|
|
||||||
appId
|
|
||||||
}: {
|
|
||||||
integrationAuthId: string;
|
|
||||||
appId: string;
|
|
||||||
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayEnvironments"] as const,
|
|
||||||
getIntegrationAuthRailwayServices: ({
|
|
||||||
integrationAuthId,
|
|
||||||
appId
|
|
||||||
}: {
|
|
||||||
integrationAuthId: string;
|
|
||||||
appId: string;
|
|
||||||
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
|
||||||
const { data } = await apiRequest.get<{ integrationAuth: IntegrationAuth }>(`/api/v1/integration-auth/${integrationAuthId}`);
|
|
||||||
return data.integrationAuth;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchIntegrationAuthApps = async ({
|
|
||||||
integrationAuthId,
|
|
||||||
teamId
|
|
||||||
}: {
|
|
||||||
integrationAuthId: string;
|
|
||||||
teamId?: string;
|
|
||||||
}) => {
|
|
||||||
const searchParams = new URLSearchParams(teamId ? { teamId } : undefined);
|
|
||||||
const { data } = await apiRequest.get<{ apps: App[] }>(
|
|
||||||
`/api/v1/integration-auth/${integrationAuthId}/apps`,
|
|
||||||
{ params: searchParams }
|
|
||||||
);
|
|
||||||
return data.apps;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
|
|
||||||
const { data } = await apiRequest.get<{ teams: Team[] }>(`/api/v1/integration-auth/${integrationAuthId}/teams`);
|
|
||||||
return data.teams;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchIntegrationAuthVercelBranches = async ({
|
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
appId
|
appId
|
||||||
}: {
|
}: {
|
||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
|
}) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
|
||||||
|
getIntegrationAuthRailwayEnvironments: ({
|
||||||
|
integrationAuthId,
|
||||||
|
appId
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
appId: string;
|
||||||
|
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayEnvironments"] as const,
|
||||||
|
getIntegrationAuthRailwayServices: ({
|
||||||
|
integrationAuthId,
|
||||||
|
appId
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
appId: string;
|
||||||
|
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
||||||
|
const { data } = await apiRequest.get<{ integrationAuth: IntegrationAuth }>(
|
||||||
|
`/api/v1/integration-auth/${integrationAuthId}`
|
||||||
|
);
|
||||||
|
return data.integrationAuth;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchIntegrationAuthApps = async ({
|
||||||
|
integrationAuthId,
|
||||||
|
teamId
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
teamId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { data: { branches } } = await apiRequest.get<{ branches: string[] }>(`/api/v1/integration-auth/${integrationAuthId}/vercel/branches`, {
|
const searchParams = new URLSearchParams(teamId ? { teamId } : undefined);
|
||||||
params: {
|
const { data } = await apiRequest.get<{ apps: App[] }>(
|
||||||
appId
|
`/api/v1/integration-auth/${integrationAuthId}/apps`,
|
||||||
}
|
{ params: searchParams }
|
||||||
});
|
);
|
||||||
|
return data.apps;
|
||||||
return branches;
|
};
|
||||||
|
|
||||||
|
const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
|
||||||
|
const { data } = await apiRequest.get<{ teams: Team[] }>(
|
||||||
|
`/api/v1/integration-auth/${integrationAuthId}/teams`
|
||||||
|
);
|
||||||
|
return data.teams;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchIntegrationAuthVercelBranches = async ({
|
||||||
|
integrationAuthId,
|
||||||
|
appId
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
appId: string;
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
data: { branches }
|
||||||
|
} = await apiRequest.get<{ branches: string[] }>(
|
||||||
|
`/api/v1/integration-auth/${integrationAuthId}/vercel/branches`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
appId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return branches;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchIntegrationAuthRailwayEnvironments = async ({
|
const fetchIntegrationAuthRailwayEnvironments = async ({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
appId
|
appId
|
||||||
}: {
|
}: {
|
||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { data: { environments } } = await apiRequest.get<{ environments: Environment[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/environments`, {
|
const {
|
||||||
params: {
|
data: { environments }
|
||||||
appId
|
} = await apiRequest.get<{ environments: Environment[] }>(
|
||||||
}
|
`/api/v1/integration-auth/${integrationAuthId}/railway/environments`,
|
||||||
});
|
{
|
||||||
|
params: {
|
||||||
return environments;
|
appId
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return environments;
|
||||||
|
};
|
||||||
|
|
||||||
const fetchIntegrationAuthRailwayServices = async ({
|
const fetchIntegrationAuthRailwayServices = async ({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
appId
|
appId
|
||||||
}: {
|
}: {
|
||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { data: { services } } = await apiRequest.get<{ services: Service[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/services`, {
|
const {
|
||||||
params: {
|
data: { services }
|
||||||
appId
|
} = await apiRequest.get<{ services: Service[] }>(
|
||||||
}
|
`/api/v1/integration-auth/${integrationAuthId}/railway/services`,
|
||||||
});
|
{
|
||||||
|
params: {
|
||||||
return services;
|
appId
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
||||||
queryFn: () => fetchIntegrationAuthById(integrationAuthId),
|
queryFn: () => fetchIntegrationAuthById(integrationAuthId),
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useGetIntegrationAuthApps = ({
|
export const useGetIntegrationAuthApps = ({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
teamId
|
teamId
|
||||||
}: {
|
}: {
|
||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
|
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
|
||||||
queryFn: () => fetchIntegrationAuthApps({
|
queryFn: () =>
|
||||||
integrationAuthId,
|
fetchIntegrationAuthApps({
|
||||||
teamId
|
integrationAuthId,
|
||||||
}),
|
teamId
|
||||||
enabled: true
|
}),
|
||||||
});
|
enabled: true
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetIntegrationAuthTeams = (integrationAuthId: string) => {
|
export const useGetIntegrationAuthTeams = (integrationAuthId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: integrationAuthKeys.getIntegrationAuthTeams(integrationAuthId),
|
queryKey: integrationAuthKeys.getIntegrationAuthTeams(integrationAuthId),
|
||||||
queryFn: () => fetchIntegrationAuthTeams(integrationAuthId),
|
queryFn: () => fetchIntegrationAuthTeams(integrationAuthId),
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useGetIntegrationAuthVercelBranches = ({
|
export const useGetIntegrationAuthVercelBranches = ({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
appId,
|
appId
|
||||||
}: {
|
}: {
|
||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
}) => {
|
}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({
|
queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
appId,
|
appId
|
||||||
}),
|
}),
|
||||||
queryFn: () => fetchIntegrationAuthVercelBranches({
|
queryFn: () =>
|
||||||
integrationAuthId,
|
fetchIntegrationAuthVercelBranches({
|
||||||
appId,
|
integrationAuthId,
|
||||||
}),
|
appId
|
||||||
enabled: true
|
}),
|
||||||
});
|
enabled: true
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetIntegrationAuthRailwayEnvironments = ({
|
export const useGetIntegrationAuthRailwayEnvironments = ({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
appId
|
appId
|
||||||
}: {
|
}: {
|
||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
}) => {
|
}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({
|
queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
appId,
|
appId
|
||||||
}),
|
}),
|
||||||
queryFn: () => fetchIntegrationAuthRailwayEnvironments({
|
queryFn: () =>
|
||||||
integrationAuthId,
|
fetchIntegrationAuthRailwayEnvironments({
|
||||||
appId,
|
integrationAuthId,
|
||||||
}),
|
appId
|
||||||
enabled: true
|
}),
|
||||||
});
|
enabled: true
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetIntegrationAuthRailwayServices = ({
|
export const useGetIntegrationAuthRailwayServices = ({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
appId
|
appId
|
||||||
}: {
|
}: {
|
||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
}) => {
|
}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
|
queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
appId,
|
appId
|
||||||
}),
|
}),
|
||||||
queryFn: () => fetchIntegrationAuthRailwayServices({
|
queryFn: () =>
|
||||||
integrationAuthId,
|
fetchIntegrationAuthRailwayServices({
|
||||||
appId,
|
integrationAuthId,
|
||||||
}),
|
appId
|
||||||
enabled: true
|
}),
|
||||||
});
|
enabled: true
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteIntegrationAuth = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, { id: string; workspaceId: string }>({
|
||||||
|
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration-auth/${id}`),
|
||||||
|
onSuccess: (_, { workspaceId }) => {
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId));
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
export type IntegrationAuth = {
|
export type IntegrationAuth = {
|
||||||
_id: string;
|
_id: string;
|
||||||
workspace: string;
|
integration: string;
|
||||||
integration: string;
|
workspace: string;
|
||||||
teamId?: string;
|
__v: number;
|
||||||
accountId?: string;
|
createdAt: string;
|
||||||
}
|
updatedAt: string;
|
||||||
|
algorithm: string;
|
||||||
|
keyEncoding: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type App = {
|
export type App = {
|
||||||
name: string;
|
name: string;
|
||||||
appId?: string;
|
appId?: string;
|
||||||
owner?: string;
|
owner?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Team = {
|
export type Team = {
|
||||||
name: string;
|
name: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Environment = {
|
export type Environment = {
|
||||||
name: string;
|
name: string;
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Service = {
|
export type Service = {
|
||||||
name: string;
|
name: string;
|
||||||
serviceId: string;
|
serviceId: string;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { useDeleteIntegration,useGetCloudIntegrations } from "./queries";
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { workspaceKeys } from "../workspace/queries";
|
||||||
|
import { TCloudIntegration } from "./types";
|
||||||
|
|
||||||
|
export const integrationQueryKeys = {
|
||||||
|
getIntegrations: () => ["integrations"] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchIntegrations = async () => {
|
||||||
|
const { data } = await apiRequest.get<{ integrationOptions: TCloudIntegration[] }>(
|
||||||
|
"/api/v1/integration-auth/integration-options"
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.integrationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetCloudIntegrations = () =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: integrationQueryKeys.getIntegrations(),
|
||||||
|
queryFn: () => fetchIntegrations()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useDeleteIntegration = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, { id: string; workspaceId: string }>({
|
||||||
|
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`),
|
||||||
|
onSuccess: (_, { workspaceId }) => {
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export type TCloudIntegration = {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
image: string;
|
||||||
|
isAvailable: boolean;
|
||||||
|
type: string;
|
||||||
|
clientId: string;
|
||||||
|
docsLink: string;
|
||||||
|
clientSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIntegration = {
|
||||||
|
_id: string;
|
||||||
|
workspace: string;
|
||||||
|
environment: string;
|
||||||
|
isActive: boolean;
|
||||||
|
url: any;
|
||||||
|
app: string;
|
||||||
|
appId: string;
|
||||||
|
targetEnvironment: string;
|
||||||
|
targetEnvironmentId: string;
|
||||||
|
targetService: string;
|
||||||
|
targetServiceId: string;
|
||||||
|
owner: string;
|
||||||
|
path: string;
|
||||||
|
region: string;
|
||||||
|
integration: string;
|
||||||
|
integrationAuth: string;
|
||||||
|
secretPath: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
__v: number;
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
export type { GetAuthTokenAPI } from "./auth/types";
|
export type { GetAuthTokenAPI } from "./auth/types";
|
||||||
export type { IncidentContact } from "./incidentContacts/types";
|
export type { IncidentContact } from "./incidentContacts/types";
|
||||||
|
export type { IntegrationAuth } from "./integrationAuth/types";
|
||||||
|
export type { TCloudIntegration, TIntegration } from "./integrations/types";
|
||||||
export type { UserWsKeyPair } from "./keys/types";
|
export type { UserWsKeyPair } from "./keys/types";
|
||||||
export type { Organization } from "./organization/types";
|
export type { Organization } from "./organization/types";
|
||||||
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
|
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ export {
|
|||||||
useGetUserWorkspaceMemberships,
|
useGetUserWorkspaceMemberships,
|
||||||
useGetUserWorkspaces,
|
useGetUserWorkspaces,
|
||||||
useGetUserWsEnvironments,
|
useGetUserWsEnvironments,
|
||||||
|
useGetWorkspaceAuthorizations,
|
||||||
useGetWorkspaceById,
|
useGetWorkspaceById,
|
||||||
useGetWorkspaceIndexStatus,
|
useGetWorkspaceIndexStatus,
|
||||||
|
useGetWorkspaceIntegrations,
|
||||||
useGetWorkspaceSecrets,
|
useGetWorkspaceSecrets,
|
||||||
useNameWorkspaceSecrets,
|
useNameWorkspaceSecrets,
|
||||||
useRenameWorkspace,
|
useRenameWorkspace,
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import {
|
import { IntegrationAuth } from "../integrationAuth/types";
|
||||||
EncryptedSecret
|
import { TIntegration } from "../integrations/types";
|
||||||
} from "../secrets/types";
|
import { EncryptedSecret } from "../secrets/types";
|
||||||
import {
|
import {
|
||||||
CreateEnvironmentDTO,
|
CreateEnvironmentDTO,
|
||||||
CreateWorkspaceDTO,
|
CreateWorkspaceDTO,
|
||||||
@@ -19,11 +19,14 @@ import {
|
|||||||
WorkspaceEnv
|
WorkspaceEnv
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const workspaceKeys = {
|
export const workspaceKeys = {
|
||||||
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, "workspace"] as const,
|
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, "workspace"] as const,
|
||||||
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const,
|
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const,
|
||||||
getWorkspaceIndexStatus: (workspaceId: string) => [{ workspaceId}, "workspace-index-status"] as const,
|
getWorkspaceIndexStatus: (workspaceId: string) =>
|
||||||
|
[{ workspaceId }, "workspace-index-status"] as const,
|
||||||
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
|
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
|
||||||
|
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
|
||||||
|
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
|
||||||
getAllUserWorkspace: ["workspaces"] as const,
|
getAllUserWorkspace: ["workspaces"] as const,
|
||||||
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const
|
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const
|
||||||
};
|
};
|
||||||
@@ -42,15 +45,17 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
};
|
||||||
|
|
||||||
const fetchWorkspaceSecrets = async (workspaceId: string) => {
|
const fetchWorkspaceSecrets = async (workspaceId: string) => {
|
||||||
const { data: { secrets } } = await apiRequest.get<{ secrets: EncryptedSecret[] }>(
|
const {
|
||||||
|
data: { secrets }
|
||||||
|
} = await apiRequest.get<{ secrets: EncryptedSecret[] }>(
|
||||||
`/api/v3/workspaces/${workspaceId}/secrets`
|
`/api/v3/workspaces/${workspaceId}/secrets`
|
||||||
);
|
);
|
||||||
|
|
||||||
return secrets;
|
return secrets;
|
||||||
}
|
};
|
||||||
|
|
||||||
const fetchUserWorkspaces = async () => {
|
const fetchUserWorkspaces = async () => {
|
||||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
|
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
|
||||||
@@ -63,15 +68,15 @@ export const useGetWorkspaceIndexStatus = (workspaceId: string) => {
|
|||||||
queryFn: () => fetchWorkspaceIndexStatus(workspaceId),
|
queryFn: () => fetchWorkspaceIndexStatus(workspaceId),
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useGetWorkspaceSecrets = (workspaceId: string) => {
|
export const useGetWorkspaceSecrets = (workspaceId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: workspaceKeys.getWorkspaceSecrets(workspaceId),
|
queryKey: workspaceKeys.getWorkspaceSecrets(workspaceId),
|
||||||
queryFn: () => fetchWorkspaceSecrets(workspaceId),
|
queryFn: () => fetchWorkspaceSecrets(workspaceId),
|
||||||
enabled: true
|
enabled: true
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useGetWorkspaceById = (workspaceId: string) => {
|
export const useGetWorkspaceById = (workspaceId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -126,7 +131,39 @@ export const useNameWorkspaceSecrets = () => {
|
|||||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIndexStatus(variables.workspaceId));
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIndexStatus(variables.workspaceId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const fetchWorkspaceAuthorization = async (workspaceId: string) => {
|
||||||
|
const { data } = await apiRequest.get<{ authorizations: IntegrationAuth[] }>(
|
||||||
|
`/api/v1/workspace/${workspaceId}/authorizations`
|
||||||
|
);
|
||||||
|
return data.authorizations;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetWorkspaceAuthorizations = <TData = IntegrationAuth[],>(
|
||||||
|
workspaceId: string,
|
||||||
|
select?: (data: IntegrationAuth[]) => TData
|
||||||
|
) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: workspaceKeys.getWorkspaceAuthorization(workspaceId),
|
||||||
|
queryFn: () => fetchWorkspaceAuthorization(workspaceId),
|
||||||
|
enabled: Boolean(workspaceId),
|
||||||
|
select
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchWorkspaceIntegrations = async (workspaceId: string) => {
|
||||||
|
const { data } = await apiRequest.get<{ integrations: TIntegration[] }>(
|
||||||
|
`/api/v1/workspace/${workspaceId}/integrations`
|
||||||
|
);
|
||||||
|
return data.integrations;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetWorkspaceIntegrations = (workspaceId: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: workspaceKeys.getWorkspaceIntegrations(workspaceId),
|
||||||
|
queryFn: () => fetchWorkspaceIntegrations(workspaceId),
|
||||||
|
enabled: Boolean(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
// mutation
|
// mutation
|
||||||
export const useCreateWorkspace = () => {
|
export const useCreateWorkspace = () => {
|
||||||
|
|||||||
@@ -1,409 +1,18 @@
|
|||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json";
|
import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json";
|
||||||
|
|
||||||
import ActivateBotDialog from "@app/components/basic/dialog/ActivateBotDialog";
|
import { IntegrationsPage } from "@app/views/IntegrationsPage";
|
||||||
import CloudIntegrationSection from "@app/components/integrations/CloudIntegrationSection";
|
|
||||||
import FrameworkIntegrationSection from "@app/components/integrations/FrameworkIntegrationSection";
|
|
||||||
import IntegrationSection from "@app/components/integrations/IntegrationSection";
|
|
||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
|
||||||
|
|
||||||
import {
|
type Props = {
|
||||||
decryptAssymmetric,
|
frameworkIntegrations: typeof frameworkIntegrationOptions;
|
||||||
encryptAssymmetric
|
};
|
||||||
} from "../../components/utilities/cryptography/crypto";
|
|
||||||
import getBot from "../api/bot/getBot";
|
|
||||||
import setBotActiveStatus from "../api/bot/setBotActiveStatus";
|
|
||||||
import deleteIntegration from "../api/integrations/DeleteIntegration";
|
|
||||||
import getIntegrationOptions from "../api/integrations/GetIntegrationOptions";
|
|
||||||
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
|
|
||||||
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
|
|
||||||
import getAWorkspace from "../api/workspace/getAWorkspace";
|
|
||||||
import getLatestFileKey from "../api/workspace/getLatestFileKey";
|
|
||||||
|
|
||||||
interface IntegrationAuth {
|
|
||||||
_id: string;
|
|
||||||
integration: string;
|
|
||||||
workspace: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Integration {
|
|
||||||
_id: string;
|
|
||||||
isActive: boolean;
|
|
||||||
app: string | null;
|
|
||||||
appId: string | null;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
environment: string;
|
|
||||||
integration: string;
|
|
||||||
targetEnvironment: string;
|
|
||||||
workspace: string;
|
|
||||||
secretPath:string;
|
|
||||||
integrationAuth: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IntegrationOption {
|
|
||||||
tenantId?: string;
|
|
||||||
clientId: string;
|
|
||||||
clientSlug?: string; // vercel-integration specific
|
|
||||||
docsLink: string;
|
|
||||||
image: string;
|
|
||||||
isAvailable: boolean;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Integrations() {
|
|
||||||
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
|
|
||||||
const [integrationAuths, setIntegrationAuths] = useState<IntegrationAuth[]>([]);
|
|
||||||
const [environments, setEnvironments] = useState<
|
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
}[]
|
|
||||||
>([]);
|
|
||||||
const [integrations, setIntegrations] = useState<Integration[]>([]);
|
|
||||||
// TODO: These will have its type when migratiing towards react-query
|
|
||||||
const [bot, setBot] = useState<any>(null);
|
|
||||||
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
|
|
||||||
const [selectedIntegrationOption, setSelectedIntegrationOption] =
|
|
||||||
useState<IntegrationOption | null>(null);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const workspaceId = router.query.id as string;
|
|
||||||
|
|
||||||
|
const Integration = ({ frameworkIntegrations }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const workspace = await getAWorkspace(workspaceId);
|
|
||||||
setEnvironments(workspace.environments);
|
|
||||||
|
|
||||||
// get cloud integration options
|
|
||||||
setCloudIntegrationOptions(await getIntegrationOptions());
|
|
||||||
|
|
||||||
// get project integration authorizations
|
|
||||||
setIntegrationAuths(
|
|
||||||
await getWorkspaceAuthorizations({
|
|
||||||
workspaceId
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// get project integrations
|
|
||||||
setIntegrations(
|
|
||||||
await getWorkspaceIntegrations({
|
|
||||||
workspaceId
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// get project bot
|
|
||||||
setBot(await getBot({ workspaceId }));
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activate bot for project by performing the following steps:
|
|
||||||
* 1. Get the (encrypted) project key
|
|
||||||
* 2. Decrypt project key with user's private key
|
|
||||||
* 3. Encrypt project key with bot's public key
|
|
||||||
* 4. Send encrypted project key to backend and set bot status to active
|
|
||||||
*/
|
|
||||||
const handleBotActivate = async () => {
|
|
||||||
let botKey;
|
|
||||||
try {
|
|
||||||
if (bot) {
|
|
||||||
// case: there is a bot
|
|
||||||
const key = await getLatestFileKey({ workspaceId });
|
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
|
||||||
|
|
||||||
if (!PRIVATE_KEY) {
|
|
||||||
throw new Error("Private Key missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
const WORKSPACE_KEY = decryptAssymmetric({
|
|
||||||
ciphertext: key.latestKey.encryptedKey,
|
|
||||||
nonce: key.latestKey.nonce,
|
|
||||||
publicKey: key.latestKey.sender.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
|
||||||
plaintext: WORKSPACE_KEY,
|
|
||||||
publicKey: bot.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
botKey = {
|
|
||||||
encryptedKey: ciphertext,
|
|
||||||
nonce
|
|
||||||
};
|
|
||||||
|
|
||||||
setBot(
|
|
||||||
(
|
|
||||||
await setBotActiveStatus({
|
|
||||||
botId: bot._id,
|
|
||||||
isActive: true,
|
|
||||||
botKey
|
|
||||||
})
|
|
||||||
).bot
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
|
|
||||||
try {
|
|
||||||
// generate CSRF token for OAuth2 code-token exchange integrations
|
|
||||||
const state = crypto.randomBytes(16).toString("hex");
|
|
||||||
localStorage.setItem("latestCSRFToken", state);
|
|
||||||
|
|
||||||
let link = "";
|
|
||||||
switch (integrationOption.slug) {
|
|
||||||
case "azure-key-vault":
|
|
||||||
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
|
|
||||||
break;
|
|
||||||
case "aws-parameter-store":
|
|
||||||
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
|
|
||||||
break;
|
|
||||||
case "aws-secret-manager":
|
|
||||||
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
|
|
||||||
break;
|
|
||||||
case "heroku":
|
|
||||||
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
|
|
||||||
break;
|
|
||||||
case "vercel":
|
|
||||||
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
|
|
||||||
break;
|
|
||||||
case "netlify":
|
|
||||||
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
|
|
||||||
break;
|
|
||||||
case "github":
|
|
||||||
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
|
|
||||||
break;
|
|
||||||
case "gitlab":
|
|
||||||
link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
|
|
||||||
break;
|
|
||||||
case "render":
|
|
||||||
link = `${window.location.origin}/integrations/render/authorize`;
|
|
||||||
break;
|
|
||||||
case "flyio":
|
|
||||||
link = `${window.location.origin}/integrations/flyio/authorize`;
|
|
||||||
break;
|
|
||||||
case "circleci":
|
|
||||||
link = `${window.location.origin}/integrations/circleci/authorize`;
|
|
||||||
break;
|
|
||||||
case "travisci":
|
|
||||||
link = `${window.location.origin}/integrations/travisci/authorize`;
|
|
||||||
break;
|
|
||||||
case "supabase":
|
|
||||||
link = `${window.location.origin}/integrations/supabase/authorize`;
|
|
||||||
break;
|
|
||||||
case "checkly":
|
|
||||||
link = `${window.location.origin}/integrations/checkly/authorize`;
|
|
||||||
break;
|
|
||||||
case "railway":
|
|
||||||
link = `${window.location.origin}/integrations/railway/authorize`;
|
|
||||||
break;
|
|
||||||
case "hashicorp-vault":
|
|
||||||
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
|
|
||||||
break;
|
|
||||||
case "cloudflare-pages":
|
|
||||||
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (link !== "") {
|
|
||||||
window.location.assign(link);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
|
|
||||||
try {
|
|
||||||
let link = "";
|
|
||||||
switch (integrationAuth.integration) {
|
|
||||||
case "azure-key-vault":
|
|
||||||
link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "aws-parameter-store":
|
|
||||||
link = `${window.location.origin}/integrations/aws-parameter-store/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "aws-secret-manager":
|
|
||||||
link = `${window.location.origin}/integrations/aws-secret-manager/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "heroku":
|
|
||||||
link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "vercel":
|
|
||||||
link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "netlify":
|
|
||||||
link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "github":
|
|
||||||
link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "gitlab":
|
|
||||||
link = `${window.location.origin}/integrations/gitlab/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "render":
|
|
||||||
link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "flyio":
|
|
||||||
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "circleci":
|
|
||||||
link = `${window.location.origin}/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "travisci":
|
|
||||||
link = `${window.location.origin}/integrations/travisci/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "supabase":
|
|
||||||
link = `${window.location.origin}/integrations/supabase/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "checkly":
|
|
||||||
link = `${window.location.origin}/integrations/checkly/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "railway":
|
|
||||||
link = `${window.location.origin}/integrations/railway/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "hashicorp-vault":
|
|
||||||
link = `${window.location.origin}/integrations/hashicorp-vault/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
case "cloudflare-pages":
|
|
||||||
link = `${window.location.origin}/integrations/cloudflare-pages/create?integrationAuthId=${integrationAuth._id}`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (link !== "") {
|
|
||||||
window.location.assign(link);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open dialog to activate bot if bot is not active.
|
|
||||||
* Otherwise, start integration [integrationOption]
|
|
||||||
* @param {Object} integrationOption - an integration option
|
|
||||||
* @param {String} integrationOption.name
|
|
||||||
* @param {String} integrationOption.type
|
|
||||||
* @param {String} integrationOption.docsLink
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
|
|
||||||
try {
|
|
||||||
const integrationAuthX = integrationAuths.find(
|
|
||||||
(integrationAuth) => integrationAuth.integration === integrationOption.slug
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!bot.isActive) {
|
|
||||||
await handleBotActivate();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!integrationAuthX) {
|
|
||||||
// case: integration has not been authorized
|
|
||||||
handleUnauthorizedIntegrationOptionPress(integrationOption);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAuthorizedIntegrationOptionPress(integrationAuthX);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle deleting integration authorization [integrationAuth] and corresponding integrations from state where applicable
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {IntegrationAuth} obj.integrationAuth - integrationAuth to delete
|
|
||||||
*/
|
|
||||||
const handleDeleteIntegrationAuth = async ({
|
|
||||||
integrationAuth: deletedIntegrationAuth
|
|
||||||
}: {
|
|
||||||
integrationAuth: IntegrationAuth;
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
const newIntegrations = integrations.filter(
|
|
||||||
(integration) => integration.integrationAuth !== deletedIntegrationAuth._id
|
|
||||||
);
|
|
||||||
setIntegrationAuths(
|
|
||||||
integrationAuths.filter(
|
|
||||||
(integrationAuth) => integrationAuth._id !== deletedIntegrationAuth._id
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setIntegrations(newIntegrations);
|
|
||||||
|
|
||||||
// handle updating bot
|
|
||||||
if (newIntegrations.length < 1) {
|
|
||||||
// case: no integrations left
|
|
||||||
setBot(
|
|
||||||
(
|
|
||||||
await setBotActiveStatus({
|
|
||||||
botId: bot._id,
|
|
||||||
isActive: false
|
|
||||||
})
|
|
||||||
).bot
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle deleting integration [integration]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Integration} obj.integration - integration to delete
|
|
||||||
*/
|
|
||||||
const handleDeleteIntegration = async ({ integration }: { integration: Integration }) => {
|
|
||||||
try {
|
|
||||||
const deletedIntegration = await deleteIntegration({
|
|
||||||
integrationId: integration._id
|
|
||||||
});
|
|
||||||
|
|
||||||
const newIntegrations = integrations.filter((i) => i._id !== deletedIntegration._id);
|
|
||||||
setIntegrations(newIntegrations);
|
|
||||||
|
|
||||||
// handle updating bot
|
|
||||||
if (newIntegrations.length < 1) {
|
|
||||||
// case: no integrations left
|
|
||||||
setBot(
|
|
||||||
(
|
|
||||||
await setBotActiveStatus({
|
|
||||||
botId: bot._id,
|
|
||||||
isActive: false
|
|
||||||
})
|
|
||||||
).bot
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex max-h-full flex-col justify-between bg-bunker-800 text-white">
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t("common.head-title", { title: t("integrations.title") })}</title>
|
<title>{t("common.head-title", { title: t("integrations.title") })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
@@ -411,52 +20,26 @@ export default function Integrations() {
|
|||||||
<meta property="og:title" content="Manage your .env files in seconds" />
|
<meta property="og:title" content="Manage your .env files in seconds" />
|
||||||
<meta name="og:description" content={t("integrations.description") as string} />
|
<meta name="og:description" content={t("integrations.description") as string} />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="no-scrollbar::-webkit-scrollbar h-screen max-h-[calc(100vh-10px)] w-full overflow-y-scroll pb-6 no-scrollbar">
|
<IntegrationsPage frameworkIntegrations={frameworkIntegrations} />
|
||||||
<NavHeader pageName={t("integrations.title")} isProjectRelated />
|
</>
|
||||||
<ActivateBotDialog
|
|
||||||
isOpen={isActivateBotDialogOpen}
|
|
||||||
closeModal={() => setIsActivateBotDialogOpen(false)}
|
|
||||||
selectedIntegrationOption={selectedIntegrationOption}
|
|
||||||
integrationOptionPress={integrationOptionPress}
|
|
||||||
/>
|
|
||||||
<IntegrationSection
|
|
||||||
integrations={integrations}
|
|
||||||
setIntegrations={setIntegrations}
|
|
||||||
bot={bot}
|
|
||||||
setBot={setBot}
|
|
||||||
environments={environments}
|
|
||||||
handleDeleteIntegration={handleDeleteIntegration}
|
|
||||||
/>
|
|
||||||
{cloudIntegrationOptions.length > 0 && bot ? (
|
|
||||||
<CloudIntegrationSection
|
|
||||||
cloudIntegrationOptions={cloudIntegrationOptions}
|
|
||||||
setSelectedIntegrationOption={setSelectedIntegrationOption as any}
|
|
||||||
integrationOptionPress={(integrationOption: IntegrationOption) => {
|
|
||||||
if (!bot.isActive) {
|
|
||||||
// case: bot is not active -> open modal to activate bot
|
|
||||||
setIsActivateBotDialogOpen(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
integrationOptionPress(integrationOption);
|
|
||||||
}}
|
|
||||||
integrationAuths={integrationAuths}
|
|
||||||
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
|
||||||
<h1 className="text-3xl font-semibold">{t("integrations.cloud-integrations")}</h1>
|
|
||||||
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
|
|
||||||
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(elem => <div key={elem} className="bg-mineshaft-800 border border-mineshaft-600 animate-pulse h-32 rounded-md"/>)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<FrameworkIntegrationSection frameworks={frameworkIntegrationOptions as any} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
Integrations.requireAuth = true;
|
export const getStaticProps = () => {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
frameworkIntegrations: frameworkIntegrationOptions
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStaticPaths = async () => {
|
||||||
|
return {
|
||||||
|
paths: [], // indicates that no page needs be created at build time
|
||||||
|
fallback: "blocking" // indicates the type of fallback
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
Integration.requireAuth = true;
|
||||||
|
|
||||||
|
export default Integration;
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export default function ChecklyCreateIntegrationPage() {
|
|||||||
>
|
>
|
||||||
Checkly Integration
|
Checkly Integration
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<FormControl label="Infisical Project Environment" className="mt-2 px-6">
|
<FormControl label="Infisical Project Environment" className="mt-4 px-6">
|
||||||
<Select
|
<Select
|
||||||
value={selectedSourceEnvironment}
|
value={selectedSourceEnvironment}
|
||||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
@@ -114,7 +114,7 @@ export default function ChecklyCreateIntegrationPage() {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl label="Secrets Path">
|
<FormControl label="Secrets Path" className="mt-4 px-6">
|
||||||
<Input
|
<Input
|
||||||
value={secretPath}
|
value={secretPath}
|
||||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { TCloudIntegration,UserWsKeyPair } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
encryptAssymmetric
|
||||||
|
} from "../../components/utilities/cryptography/crypto";
|
||||||
|
|
||||||
|
export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||||
|
|
||||||
|
if (!PRIVATE_KEY) {
|
||||||
|
throw new Error("Private Key missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
const WORKSPACE_KEY = decryptAssymmetric({
|
||||||
|
ciphertext: latestKey.encryptedKey,
|
||||||
|
nonce: latestKey.nonce,
|
||||||
|
publicKey: latestKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
|
plaintext: WORKSPACE_KEY,
|
||||||
|
publicKey: botPublicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
return { encryptedKey: ciphertext, nonce };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => {
|
||||||
|
try {
|
||||||
|
// generate CSRF token for OAuth2 code-token exchange integrations
|
||||||
|
const state = crypto.randomBytes(16).toString("hex");
|
||||||
|
localStorage.setItem("latestCSRFToken", state);
|
||||||
|
|
||||||
|
let link = "";
|
||||||
|
switch (integrationOption.slug) {
|
||||||
|
case "azure-key-vault":
|
||||||
|
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
|
||||||
|
break;
|
||||||
|
case "aws-parameter-store":
|
||||||
|
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
|
||||||
|
break;
|
||||||
|
case "aws-secret-manager":
|
||||||
|
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
|
||||||
|
break;
|
||||||
|
case "heroku":
|
||||||
|
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
|
||||||
|
break;
|
||||||
|
case "vercel":
|
||||||
|
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
|
||||||
|
break;
|
||||||
|
case "netlify":
|
||||||
|
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
|
||||||
|
break;
|
||||||
|
case "github":
|
||||||
|
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
|
||||||
|
break;
|
||||||
|
case "gitlab":
|
||||||
|
link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
|
||||||
|
break;
|
||||||
|
case "render":
|
||||||
|
link = `${window.location.origin}/integrations/render/authorize`;
|
||||||
|
break;
|
||||||
|
case "flyio":
|
||||||
|
link = `${window.location.origin}/integrations/flyio/authorize`;
|
||||||
|
break;
|
||||||
|
case "circleci":
|
||||||
|
link = `${window.location.origin}/integrations/circleci/authorize`;
|
||||||
|
break;
|
||||||
|
case "travisci":
|
||||||
|
link = `${window.location.origin}/integrations/travisci/authorize`;
|
||||||
|
break;
|
||||||
|
case "supabase":
|
||||||
|
link = `${window.location.origin}/integrations/supabase/authorize`;
|
||||||
|
break;
|
||||||
|
case "checkly":
|
||||||
|
link = `${window.location.origin}/integrations/checkly/authorize`;
|
||||||
|
break;
|
||||||
|
case "railway":
|
||||||
|
link = `${window.location.origin}/integrations/railway/authorize`;
|
||||||
|
break;
|
||||||
|
case "hashicorp-vault":
|
||||||
|
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
|
||||||
|
break;
|
||||||
|
case "cloudflare-pages":
|
||||||
|
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link !== "") {
|
||||||
|
window.location.assign(link);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const redirectToIntegrationAppConfigScreen = (provider: string, integrationAuthId: string) =>
|
||||||
|
`/integrations/${provider}/create?integrationAuthId=${integrationAuthId}`;
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
|
import { Button, Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useDeleteIntegration,
|
||||||
|
useDeleteIntegrationAuth,
|
||||||
|
useGetCloudIntegrations,
|
||||||
|
useGetUserWsKey,
|
||||||
|
useGetWorkspaceAuthorizations,
|
||||||
|
useGetWorkspaceBot,
|
||||||
|
useGetWorkspaceIntegrations,
|
||||||
|
useUpdateBotActiveStatus
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { IntegrationAuth } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
import { CloudIntegrationSection } from "./components/CloudIntegrationSection";
|
||||||
|
import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSection";
|
||||||
|
import { IntegrationsSection } from "./components/IntegrationsSection";
|
||||||
|
import {
|
||||||
|
generateBotKey,
|
||||||
|
redirectForProviderAuth,
|
||||||
|
redirectToIntegrationAppConfigScreen
|
||||||
|
} from "./IntegrationPage.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IntegrationsPage = ({ frameworkIntegrations }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const workspaceId = currentWorkspace?._id || "";
|
||||||
|
const environments = currentWorkspace?.environments || [];
|
||||||
|
|
||||||
|
const { data: latestWsKey } = useGetUserWsKey(workspaceId);
|
||||||
|
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
|
"activeBot"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
|
||||||
|
useGetCloudIntegrations();
|
||||||
|
const { data: integrationAuths, isLoading: isIntegrationAuthLoading } =
|
||||||
|
useGetWorkspaceAuthorizations(
|
||||||
|
workspaceId,
|
||||||
|
useCallback((data: IntegrationAuth[]) => {
|
||||||
|
const groupBy: Record<string, IntegrationAuth> = {};
|
||||||
|
data.forEach((el) => {
|
||||||
|
groupBy[el.integration] = el;
|
||||||
|
});
|
||||||
|
return groupBy;
|
||||||
|
}, [])
|
||||||
|
);
|
||||||
|
// mutation
|
||||||
|
const {
|
||||||
|
data: integrations,
|
||||||
|
isLoading: isIntegrationLoading,
|
||||||
|
isFetching: isIntegrationFetching
|
||||||
|
} = useGetWorkspaceIntegrations(workspaceId);
|
||||||
|
|
||||||
|
const { data: bot } = useGetWorkspaceBot(workspaceId);
|
||||||
|
|
||||||
|
// mutation
|
||||||
|
const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } =
|
||||||
|
useUpdateBotActiveStatus();
|
||||||
|
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
|
||||||
|
const {
|
||||||
|
mutateAsync: deleteIntegrationAuth,
|
||||||
|
isLoading: isDeleteIntegrationAuthSuccess,
|
||||||
|
reset: resetDeleteIntegrationAuth
|
||||||
|
} = useDeleteIntegrationAuth();
|
||||||
|
|
||||||
|
// summary: this use effect is trigger when all integration auths are removed thus deactivate bot
|
||||||
|
// details: so onsuccessfully deleting an integration auth, immediately integration list is refeteched
|
||||||
|
// After the refetch is completed check if its empty. Then set bot active and reset the submit hook
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDeleteIntegrationAuthSuccess && !isIntegrationFetching && !integrations?.length) {
|
||||||
|
if (bot?._id)
|
||||||
|
updateBotActiveStatusSync({
|
||||||
|
isActive: false,
|
||||||
|
botId: bot._id,
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
resetDeleteIntegrationAuth();
|
||||||
|
}
|
||||||
|
}, [isIntegrationFetching, isDeleteIntegrationAuthSuccess, integrations?.length]);
|
||||||
|
|
||||||
|
const handleProviderIntegration = async (provider: string) => {
|
||||||
|
const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug);
|
||||||
|
if (!selectedCloudIntegration) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (bot && !bot.isActive) {
|
||||||
|
const botKey = generateBotKey(bot.publicKey, latestWsKey!);
|
||||||
|
await updateBotActiveStatus({
|
||||||
|
workspaceId,
|
||||||
|
botKey,
|
||||||
|
isActive: true,
|
||||||
|
botId: bot._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const integrationAuthForProvider = integrationAuths?.[provider];
|
||||||
|
if (!integrationAuthForProvider) {
|
||||||
|
redirectForProviderAuth(selectedCloudIntegration);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = redirectToIntegrationAppConfigScreen(provider, integrationAuthForProvider._id);
|
||||||
|
router.push(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// function to strat integration for a provider
|
||||||
|
// confirmation to user passing the bot key for provider to get secret access
|
||||||
|
const handleProviderIntegrationStart = (provider: string) => {
|
||||||
|
if (!bot?.isActive) {
|
||||||
|
handlePopUpOpen("activeBot", { provider });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleProviderIntegration(provider);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserAcceptBotCondition = () => {
|
||||||
|
const { provider } = popUp.activeBot?.data as { provider: string };
|
||||||
|
handleProviderIntegration(provider);
|
||||||
|
handlePopUpClose("activeBot");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
|
||||||
|
try {
|
||||||
|
await deleteIntegration({ id: integrationId, workspaceId });
|
||||||
|
if (cb) cb();
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Deleted integration"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to delete integration"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => {
|
||||||
|
const integrationAuthForProvider = integrationAuths?.[provider];
|
||||||
|
if (!integrationAuthForProvider) return;
|
||||||
|
try {
|
||||||
|
await deleteIntegrationAuth({
|
||||||
|
id: integrationAuthForProvider._id,
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
if (cb) cb();
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Revoked provider authentication"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to revoke provider authentication"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto max-w-7xl px-8 pb-12 text-white">
|
||||||
|
<NavHeader pageName={t("integrations.title")} isProjectRelated />
|
||||||
|
<IntegrationsSection
|
||||||
|
isLoading={isIntegrationLoading}
|
||||||
|
integrations={integrations}
|
||||||
|
environments={environments}
|
||||||
|
onIntegrationDelete={({ _id: id }, cb) => handleIntegrationDelete(id, cb)}
|
||||||
|
/>
|
||||||
|
<CloudIntegrationSection
|
||||||
|
isLoading={isCloudIntegrationsLoading || isIntegrationAuthLoading}
|
||||||
|
cloudIntegrations={cloudIntegrations}
|
||||||
|
integrationAuths={integrationAuths}
|
||||||
|
onIntegrationStart={handleProviderIntegrationStart}
|
||||||
|
onIntegrationRevoke={handleIntegrationAuthRevoke}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.activeBot?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("activeBot", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title={t("integrations.grant-access-to-secrets") as string}
|
||||||
|
footerContent={
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button onClick={() => handleUserAcceptBotCondition()}>
|
||||||
|
{t("integrations.grant-access-button") as string}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handlePopUpClose("activeBot")}
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="secondary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("integrations.why-infisical-needs-access")}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<FrameworkIntegrationSection frameworks={frameworkIntegrations} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { DeleteActionModal,Skeleton, Tooltip } from "@app/components/v2";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isLoading?: boolean;
|
||||||
|
integrationAuths?: Record<string, IntegrationAuth>;
|
||||||
|
cloudIntegrations?: TCloudIntegration[];
|
||||||
|
onIntegrationStart: (slug: string) => void;
|
||||||
|
// cb: handle popUpClose child->parent communication pattern
|
||||||
|
onIntegrationRevoke: (slug: string, cb: () => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TRevokeIntegrationPopUp = { provider: string };
|
||||||
|
|
||||||
|
export const CloudIntegrationSection = ({
|
||||||
|
isLoading,
|
||||||
|
cloudIntegrations = [],
|
||||||
|
integrationAuths = {},
|
||||||
|
onIntegrationStart,
|
||||||
|
onIntegrationRevoke
|
||||||
|
}: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"deleteConfirmation"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const isEmpty = !isLoading && !cloudIntegrations?.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||||
|
<h1 className="text-3xl font-semibold">{t("integrations.cloud-integrations")}</h1>
|
||||||
|
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mx-6 grid grid-flow-dense gap-4"
|
||||||
|
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(256px, 1fr))" }}
|
||||||
|
>
|
||||||
|
{isLoading &&
|
||||||
|
Array.from({ length: 12 }).map((_, index) => (
|
||||||
|
<Skeleton className="h-32" key={`cloud-integration-skeleton-${index + 1}`} />
|
||||||
|
))}
|
||||||
|
{!isLoading &&
|
||||||
|
cloudIntegrations?.map((cloudIntegration) => (
|
||||||
|
<div
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className={`group relative ${
|
||||||
|
cloudIntegration.isAvailable
|
||||||
|
? "cursor-pointer duration-200 hover:bg-mineshaft-700"
|
||||||
|
: "opacity-50"
|
||||||
|
} flex h-32 flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!cloudIntegration.isAvailable) return;
|
||||||
|
onIntegrationStart(cloudIntegration.slug);
|
||||||
|
}}
|
||||||
|
key={cloudIntegration.slug}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`/images/integrations/${cloudIntegration.image}`}
|
||||||
|
height={70}
|
||||||
|
width={70}
|
||||||
|
alt="integration logo"
|
||||||
|
/>
|
||||||
|
{cloudIntegration.name.split(" ").length > 2 ? (
|
||||||
|
<div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||||
|
<div>{cloudIntegration.name.split(" ")[0]}</div>
|
||||||
|
<div className="text-base">
|
||||||
|
{cloudIntegration.name.split(" ")[1]} {cloudIntegration.name.split(" ")[2]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||||
|
{cloudIntegration.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{cloudIntegration.isAvailable &&
|
||||||
|
Boolean(integrationAuths?.[cloudIntegration.slug]) && (
|
||||||
|
<div className="absolute top-0 right-0 z-40 h-full">
|
||||||
|
<div className="relative h-full">
|
||||||
|
<div className="absolute top-0 right-0 w-24 flex-row items-center overflow-hidden whitespace-nowrap rounded-tr-md rounded-bl-md bg-primary py-0.5 px-2 text-xs text-black opacity-80 transition-all duration-300 group-hover:w-0 group-hover:p-0">
|
||||||
|
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
|
||||||
|
Authorized
|
||||||
|
</div>
|
||||||
|
<Tooltip content="Revoke Access">
|
||||||
|
<div
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={async (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
handlePopUpOpen("deleteConfirmation", {
|
||||||
|
provider: cloudIntegration.slug
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="absolute top-0 right-0 flex h-0 w-12 cursor-pointer items-center justify-center overflow-hidden rounded-r-md bg-red text-xs opacity-50 transition-all duration-300 hover:opacity-100 group-hover:h-full"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} size="xl" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{isEmpty && (
|
||||||
|
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
|
||||||
|
{Array.from({ length: 16 }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={`dummy-cloud-integration-${index + 1}`}
|
||||||
|
className="h-32 animate-pulse rounded-md border border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteConfirmation.isOpen}
|
||||||
|
title={`Are you sure want to revoke access ${
|
||||||
|
(popUp?.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider || " "
|
||||||
|
}?`}
|
||||||
|
subTitle="This will remove all the secret integration of this provider!!!"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
|
||||||
|
deleteKey={(popUp?.deleteConfirmation?.data as TRevokeIntegrationPopUp)?.provider || ""}
|
||||||
|
onDeleteApproved={async () => {
|
||||||
|
onIntegrationRevoke(
|
||||||
|
(popUp.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider,
|
||||||
|
() => handlePopUpClose("deleteConfirmation")
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { CloudIntegrationSection } from "./CloudIntegrationSection";
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
frameworks: Array<{
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
slug: string;
|
||||||
|
docsLink: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FrameworkIntegrationSection = ({ frameworks }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mx-4 mt-12 mb-4 flex flex-col items-start justify-between px-2 text-xl">
|
||||||
|
<h1 className="text-3xl font-semibold">{t("integrations.framework-integrations")}</h1>
|
||||||
|
<p className="text-base text-gray-400">{t("integrations.click-to-setup")}</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mx-6 mt-4 grid grid-flow-dense gap-3"
|
||||||
|
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))" }}
|
||||||
|
>
|
||||||
|
{frameworks.map((framework) => (
|
||||||
|
<a
|
||||||
|
key={`framework-integration-${framework.slug}`}
|
||||||
|
href={framework.docsLink}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
className="relative flex h-32 cursor-pointer flex-row items-center justify-center rounded-md p-0.5 duration-200"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex h-full w-full cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 font-semibold text-gray-300 duration-200 hover:bg-mineshaft-700 group-hover:text-gray-200 ${
|
||||||
|
framework?.name?.split(" ").length > 1 ? "px-1 text-sm" : "px-2 text-xl"
|
||||||
|
} w-full max-w-xs text-center`}
|
||||||
|
>
|
||||||
|
{framework?.image && (
|
||||||
|
<img
|
||||||
|
src={`/images/integrations/${framework.image}.png`}
|
||||||
|
height={framework?.name ? 60 : 90}
|
||||||
|
width={framework?.name ? 60 : 90}
|
||||||
|
alt="integration logo"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{framework?.name && framework?.image && <div className="h-2" />}
|
||||||
|
{framework?.name && framework.name}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { FrameworkIntegrationSection } from "./FrameworkIntegrationSection";
|
||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Skeleton,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { TIntegration } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
environments: Array<{ name: string; slug: string }>;
|
||||||
|
integrations?: TIntegration[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IntegrationsSection = ({
|
||||||
|
integrations = [],
|
||||||
|
environments = [],
|
||||||
|
isLoading,
|
||||||
|
onIntegrationDelete
|
||||||
|
}: Props) => {
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"deleteConfirmation"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="mx-4 mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
|
||||||
|
<h1 className="text-3xl font-semibold">Current Integrations</h1>
|
||||||
|
<p className="text-base text-bunker-300">Manage integrations with third-party services.</p>
|
||||||
|
</div>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="p-6 pt-0">
|
||||||
|
<Skeleton className="h-28" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoading && !integrations.length && (
|
||||||
|
<EmptyState
|
||||||
|
className="mx-6 rounded-md border border-mineshaft-700 pt-8 pb-4"
|
||||||
|
title="No integrations found. Click on one of the below providers to sync secrets."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isLoading && (
|
||||||
|
<div className="flex flex-col space-y-4 p-6 pt-0">
|
||||||
|
{integrations?.map((integration) => (
|
||||||
|
<div
|
||||||
|
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 pb-2"
|
||||||
|
key={`integration-${integration?._id.toString()}`}
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<div>
|
||||||
|
<FormControl label="Environment">
|
||||||
|
<Select
|
||||||
|
value={integration.environment}
|
||||||
|
isDisabled={integration.isActive}
|
||||||
|
className="min-w-[8rem] border border-mineshaft-700"
|
||||||
|
>
|
||||||
|
{environments.map((environment) => {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
value={environment.slug}
|
||||||
|
key={`environment-${environment.slug}`}
|
||||||
|
>
|
||||||
|
{environment.name}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<div className="ml-2 flex flex-col">
|
||||||
|
<FormLabel label="Secret Path" />
|
||||||
|
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||||
|
{integration.secretPath}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-full items-center">
|
||||||
|
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 flex flex-col">
|
||||||
|
<FormLabel label="Integration" />
|
||||||
|
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||||
|
{integrationSlugNameMapping[integration.integration]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-2 flex flex-col">
|
||||||
|
<FormLabel label="App" />
|
||||||
|
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||||
|
{integration.integration === "hashicorp-vault"
|
||||||
|
? `${integration.app} - path: ${integration.path}`
|
||||||
|
: integration.app}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(integration.integration === "vercel" ||
|
||||||
|
integration.integration === "netlify" ||
|
||||||
|
integration.integration === "railway" ||
|
||||||
|
integration.integration === "gitlab") && (
|
||||||
|
<div className="ml-4 flex flex-col">
|
||||||
|
<FormLabel label="Target Environment" />
|
||||||
|
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||||
|
{integration.targetEnvironment}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex cursor-default items-center">
|
||||||
|
<div className="ml-2 opacity-80 duration-200 hover:opacity-100">
|
||||||
|
<Tooltip content="Remove Integration">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => handlePopUpOpen("deleteConfirmation", integration)}
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="star"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} className="px-0.5" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</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 || " "}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
|
||||||
|
deleteKey={(popUp?.deleteConfirmation?.data as TIntegration)?.app || ""}
|
||||||
|
onDeleteApproved={async () =>
|
||||||
|
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () =>
|
||||||
|
handlePopUpClose("deleteConfirmation")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { IntegrationsSection } from "./IntegrationsSection";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { IntegrationsPage } from "./IntegrationsPage";
|
||||||
Reference in New Issue
Block a user