mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +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-interactions',
|
||||
'storybook-dark-mode',
|
||||
'@storybook/addon-postcss'
|
||||
{
|
||||
name: '@storybook/addon-styling',
|
||||
options: {
|
||||
postCss: {
|
||||
implementation: require('postcss')
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/nextjs',
|
||||
|
||||
7944
frontend/package-lock.json
generated
7944
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
||||
"start": "next start",
|
||||
"start:docker": "next build && next start",
|
||||
"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",
|
||||
"storybook": "storybook dev -p 6006 -s ./public",
|
||||
"build-storybook": "storybook build"
|
||||
@@ -62,8 +62,8 @@
|
||||
"infisical-node": "^1.0.37",
|
||||
"jspdf": "^2.5.1",
|
||||
"jsrp": "^0.2.4",
|
||||
"lottie-react": "^2.4.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lottie-react": "^2.4.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"next": "^12.3.4",
|
||||
"posthog-js": "^1.58.0",
|
||||
@@ -91,14 +91,14 @@
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^7.0.0-beta.30",
|
||||
"@storybook/addon-interactions": "^7.0.0-beta.30",
|
||||
"@storybook/addon-links": "^7.0.0-beta.30",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/blocks": "^7.0.0-beta.30",
|
||||
"@storybook/nextjs": "^7.0.0-beta.30",
|
||||
"@storybook/react": "^7.0.0-beta.30",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@storybook/addon-essentials": "^7.0.23",
|
||||
"@storybook/addon-interactions": "^7.0.23",
|
||||
"@storybook/addon-links": "^7.0.23",
|
||||
"@storybook/addon-styling": "^1.3.0",
|
||||
"@storybook/blocks": "^7.0.23",
|
||||
"@storybook/nextjs": "^7.0.23",
|
||||
"@storybook/react": "^7.0.23",
|
||||
"@storybook/testing-library": "^0.2.0",
|
||||
"@tailwindcss/typography": "^0.5.4",
|
||||
"@types/jsrp": "^0.2.4",
|
||||
"@types/node": "18.11.9",
|
||||
@@ -118,12 +118,12 @@
|
||||
"eslint-plugin-react": "^7.32.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.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",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||
"storybook": "^7.0.0-beta.30",
|
||||
"storybook-dark-mode": "^2.0.5",
|
||||
"storybook": "^7.0.23",
|
||||
"storybook-dark-mode": "^3.0.0",
|
||||
"tailwindcss": "3.2",
|
||||
"typescript": "^4.9.3"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = {
|
||||
onChange?: (isOpen: boolean) => void;
|
||||
deleteKey: string;
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
onDeleteApproved: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -22,7 +23,8 @@ export const DeleteActionModal = ({
|
||||
onChange,
|
||||
deleteKey,
|
||||
onDeleteApproved,
|
||||
title
|
||||
title,
|
||||
subTitle = "This action is irreversible!"
|
||||
}: Props): JSX.Element => {
|
||||
const [inputData, setInputData] = useState("");
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
@@ -52,7 +54,7 @@ export const DeleteActionModal = ({
|
||||
>
|
||||
<ModalContent
|
||||
title={title}
|
||||
subTitle="This action is irreversible!"
|
||||
subTitle={subTitle}
|
||||
footerContent={
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
|
||||
@@ -11,7 +11,7 @@ export type 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}
|
||||
{isRequired && <span className="ml-1 text-red">*</span>}
|
||||
</Label.Root>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export {
|
||||
useGetAuthToken,
|
||||
useGetCommonPasswords,
|
||||
useRevokeAllSessions,
|
||||
useSendMfaToken,
|
||||
useVerifyMfaToken} from "./queries"
|
||||
useGetAuthToken,
|
||||
useGetCommonPasswords,
|
||||
useRevokeAllSessions,
|
||||
useSendMfaToken,
|
||||
useVerifyMfaToken
|
||||
} from "./queries";
|
||||
|
||||
1
frontend/src/hooks/api/bots/index.tsx
Normal file
1
frontend/src/hooks/api/bots/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { useGetWorkspaceBot, useUpdateBotActiveStatus } from "./queries";
|
||||
38
frontend/src/hooks/api/bots/queries.tsx
Normal file
38
frontend/src/hooks/api/bots/queries.tsx
Normal file
@@ -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));
|
||||
}
|
||||
});
|
||||
};
|
||||
20
frontend/src/hooks/api/bots/types.ts
Normal file
20
frontend/src/hooks/api/bots/types.ts
Normal file
@@ -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 "./bots";
|
||||
export * from "./incidentContacts";
|
||||
export * from "./integrationAuth";
|
||||
export * from "./integrations";
|
||||
export * from "./keys";
|
||||
export * from "./organization";
|
||||
export * from "./secretFolders";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById,
|
||||
useGetIntegrationAuthRailwayEnvironments,
|
||||
useGetIntegrationAuthRailwayServices,
|
||||
useGetIntegrationAuthTeams,
|
||||
useGetIntegrationAuthVercelBranches} from "./queries";
|
||||
useDeleteIntegrationAuth,
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById,
|
||||
useGetIntegrationAuthRailwayEnvironments,
|
||||
useGetIntegrationAuthRailwayServices,
|
||||
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 {
|
||||
App,
|
||||
Environment,
|
||||
IntegrationAuth,
|
||||
Service,
|
||||
Team
|
||||
} from "./types";
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { App, Environment, IntegrationAuth, Service, Team } from "./types";
|
||||
|
||||
const integrationAuthKeys = {
|
||||
getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuth"] as const,
|
||||
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) => [{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
|
||||
getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuthTeams"] as const,
|
||||
getIntegrationAuthVercelBranches: ({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}: {
|
||||
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 ({
|
||||
getIntegrationAuthById: (integrationAuthId: string) =>
|
||||
[{ integrationAuthId }, "integrationAuth"] as const,
|
||||
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) =>
|
||||
[{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
|
||||
getIntegrationAuthTeams: (integrationAuthId: string) =>
|
||||
[{ integrationAuthId }, "integrationAuthTeams"] as const,
|
||||
getIntegrationAuthVercelBranches: ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
}: {
|
||||
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 { data: { branches } } = await apiRequest.get<{ branches: string[] }>(`/api/v1/integration-auth/${integrationAuthId}/vercel/branches`, {
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
return branches;
|
||||
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,
|
||||
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 ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
const { data: { environments } } = await apiRequest.get<{ environments: Environment[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/environments`, {
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
return environments;
|
||||
}
|
||||
const {
|
||||
data: { environments }
|
||||
} = await apiRequest.get<{ environments: Environment[] }>(
|
||||
`/api/v1/integration-auth/${integrationAuthId}/railway/environments`,
|
||||
{
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return environments;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthRailwayServices = async ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
const { data: { services } } = await apiRequest.get<{ services: Service[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/services`, {
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
const {
|
||||
data: { services }
|
||||
} = await apiRequest.get<{ services: Service[] }>(
|
||||
`/api/v1/integration-auth/${integrationAuthId}/railway/services`,
|
||||
{
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return services;
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
||||
queryFn: () => fetchIntegrationAuthById(integrationAuthId),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
||||
queryFn: () => fetchIntegrationAuthById(integrationAuthId),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthApps = ({
|
||||
integrationAuthId,
|
||||
teamId
|
||||
integrationAuthId,
|
||||
teamId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
teamId?: string;
|
||||
integrationAuthId: string;
|
||||
teamId?: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
|
||||
queryFn: () => fetchIntegrationAuthApps({
|
||||
integrationAuthId,
|
||||
teamId
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
|
||||
queryFn: () =>
|
||||
fetchIntegrationAuthApps({
|
||||
integrationAuthId,
|
||||
teamId
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthTeams = (integrationAuthId: string) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthTeams(integrationAuthId),
|
||||
queryFn: () => fetchIntegrationAuthTeams(integrationAuthId),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthTeams(integrationAuthId),
|
||||
queryFn: () => fetchIntegrationAuthTeams(integrationAuthId),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthVercelBranches = ({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
queryFn: () => fetchIntegrationAuthVercelBranches({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}),
|
||||
queryFn: () =>
|
||||
fetchIntegrationAuthVercelBranches({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthRailwayEnvironments = ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
queryFn: () => fetchIntegrationAuthRailwayEnvironments({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}),
|
||||
queryFn: () =>
|
||||
fetchIntegrationAuthRailwayEnvironments({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthRailwayServices = ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
queryFn: () => fetchIntegrationAuthRailwayServices({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}),
|
||||
queryFn: () =>
|
||||
fetchIntegrationAuthRailwayServices({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}),
|
||||
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 = {
|
||||
_id: string;
|
||||
workspace: string;
|
||||
integration: string;
|
||||
teamId?: string;
|
||||
accountId?: string;
|
||||
}
|
||||
_id: string;
|
||||
integration: string;
|
||||
workspace: string;
|
||||
__v: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
algorithm: string;
|
||||
keyEncoding: string;
|
||||
};
|
||||
|
||||
export type App = {
|
||||
name: string;
|
||||
appId?: string;
|
||||
owner?: string;
|
||||
}
|
||||
name: string;
|
||||
appId?: string;
|
||||
owner?: string;
|
||||
};
|
||||
|
||||
export type Team = {
|
||||
name: string;
|
||||
teamId: string;
|
||||
}
|
||||
name: string;
|
||||
teamId: string;
|
||||
};
|
||||
|
||||
export type Environment = {
|
||||
name: string;
|
||||
environmentId: string;
|
||||
}
|
||||
name: string;
|
||||
environmentId: string;
|
||||
};
|
||||
|
||||
export type Service = {
|
||||
name: string;
|
||||
serviceId: string;
|
||||
}
|
||||
name: string;
|
||||
serviceId: string;
|
||||
};
|
||||
|
||||
1
frontend/src/hooks/api/integrations/index.tsx
Normal file
1
frontend/src/hooks/api/integrations/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { useDeleteIntegration,useGetCloudIntegrations } from "./queries";
|
||||
35
frontend/src/hooks/api/integrations/queries.tsx
Normal file
35
frontend/src/hooks/api/integrations/queries.tsx
Normal file
@@ -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));
|
||||
}
|
||||
});
|
||||
};
|
||||
33
frontend/src/hooks/api/integrations/types.ts
Normal file
33
frontend/src/hooks/api/integrations/types.ts
Normal file
@@ -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 { 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 { Organization } from "./organization/types";
|
||||
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
|
||||
|
||||
@@ -6,8 +6,10 @@ export {
|
||||
useGetUserWorkspaceMemberships,
|
||||
useGetUserWorkspaces,
|
||||
useGetUserWsEnvironments,
|
||||
useGetWorkspaceAuthorizations,
|
||||
useGetWorkspaceById,
|
||||
useGetWorkspaceIndexStatus,
|
||||
useGetWorkspaceIntegrations,
|
||||
useGetWorkspaceSecrets,
|
||||
useNameWorkspaceSecrets,
|
||||
useRenameWorkspace,
|
||||
|
||||
@@ -2,9 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
EncryptedSecret
|
||||
} from "../secrets/types";
|
||||
import { IntegrationAuth } from "../integrationAuth/types";
|
||||
import { TIntegration } from "../integrations/types";
|
||||
import { EncryptedSecret } from "../secrets/types";
|
||||
import {
|
||||
CreateEnvironmentDTO,
|
||||
CreateWorkspaceDTO,
|
||||
@@ -19,11 +19,14 @@ import {
|
||||
WorkspaceEnv
|
||||
} from "./types";
|
||||
|
||||
const workspaceKeys = {
|
||||
export const workspaceKeys = {
|
||||
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, "workspace"] 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"],
|
||||
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
|
||||
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
|
||||
getAllUserWorkspace: ["workspaces"] as const,
|
||||
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const
|
||||
};
|
||||
@@ -42,15 +45,17 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => {
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
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`
|
||||
);
|
||||
|
||||
|
||||
return secrets;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserWorkspaces = async () => {
|
||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
|
||||
@@ -63,15 +68,15 @@ export const useGetWorkspaceIndexStatus = (workspaceId: string) => {
|
||||
queryFn: () => fetchWorkspaceIndexStatus(workspaceId),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const useGetWorkspaceSecrets = (workspaceId: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceSecrets(workspaceId),
|
||||
queryFn: () => fetchWorkspaceSecrets(workspaceId),
|
||||
enabled: true
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetWorkspaceById = (workspaceId: string) => {
|
||||
return useQuery({
|
||||
@@ -126,7 +131,39 @@ export const useNameWorkspaceSecrets = () => {
|
||||
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
|
||||
export const useCreateWorkspace = () => {
|
||||
|
||||
@@ -1,409 +1,18 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json";
|
||||
|
||||
import ActivateBotDialog from "@app/components/basic/dialog/ActivateBotDialog";
|
||||
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 { IntegrationsPage } from "@app/views/IntegrationsPage";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
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;
|
||||
type Props = {
|
||||
frameworkIntegrations: typeof frameworkIntegrationOptions;
|
||||
};
|
||||
|
||||
const Integration = ({ frameworkIntegrations }: Props) => {
|
||||
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 (
|
||||
<div className="flex max-h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("integrations.title") })}</title>
|
||||
<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 name="og:description" content={t("integrations.description") as string} />
|
||||
</Head>
|
||||
<div className="no-scrollbar::-webkit-scrollbar h-screen max-h-[calc(100vh-10px)] w-full overflow-y-scroll pb-6 no-scrollbar">
|
||||
<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>
|
||||
<IntegrationsPage frameworkIntegrations={frameworkIntegrations} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
</CardTitle>
|
||||
<FormControl label="Infisical Project Environment" className="mt-2 px-6">
|
||||
<FormControl label="Infisical Project Environment" className="mt-4 px-6">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
@@ -114,7 +114,7 @@ export default function ChecklyCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<FormControl label="Secrets Path" className="mt-4 px-6">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
|
||||
105
frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx
Normal file
105
frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx
Normal file
@@ -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}`;
|
||||
222
frontend/src/views/IntegrationsPage/IntegrationsPage.tsx
Normal file
222
frontend/src/views/IntegrationsPage/IntegrationsPage.tsx
Normal file
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
1
frontend/src/views/IntegrationsPage/index.tsx
Normal file
1
frontend/src/views/IntegrationsPage/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { IntegrationsPage } from "./IntegrationsPage";
|
||||
Reference in New Issue
Block a user