feature: add support for deployment environment scope for bitbucket and refactor bitbucket create UI

This commit is contained in:
Scott Wilson
2024-11-11 09:47:23 -08:00
parent 65d642113d
commit d4c911a28f
14 changed files with 525 additions and 186 deletions
@@ -891,6 +891,48 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
} }
}); });
server.route({
method: "GET",
url: "/:integrationAuthId/bitbucket/environments",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
integrationAuthId: z.string().trim()
}),
querystring: z.object({
workspaceSlug: z.string().trim().min(1, { message: "Workspace slug required" }),
repoSlug: z.string().trim().min(1, { message: "Repo slug required" })
}),
response: {
200: z.object({
environments: z
.object({
name: z.string(),
slug: z.string(),
uuid: z.string(),
type: z.string()
})
.array()
})
}
},
handler: async (req) => {
const environments = await server.services.integrationAuth.getBitbucketEnvironments({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
workspaceSlug: req.query.workspaceSlug,
repoSlug: req.query.repoSlug
});
return { environments };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/:integrationAuthId/northflank/secret-groups", url: "/:integrationAuthId/northflank/secret-groups",
@@ -20,6 +20,7 @@ import { getApps } from "./integration-app-list";
import { TIntegrationAuthDALFactory } from "./integration-auth-dal"; import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema"; import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import { import {
TBitbucketEnvironment,
TBitbucketWorkspace, TBitbucketWorkspace,
TChecklyGroups, TChecklyGroups,
TDeleteIntegrationAuthByIdDTO, TDeleteIntegrationAuthByIdDTO,
@@ -30,6 +31,7 @@ import {
THerokuPipelineCoupling, THerokuPipelineCoupling,
TIntegrationAuthAppsDTO, TIntegrationAuthAppsDTO,
TIntegrationAuthAwsKmsKeyDTO, TIntegrationAuthAwsKmsKeyDTO,
TIntegrationAuthBitbucketEnvironmentsDTO,
TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO, TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthGithubEnvsDTO, TIntegrationAuthGithubEnvsDTO,
@@ -1261,6 +1263,55 @@ export const integrationAuthServiceFactory = ({
return workspaces; return workspaces;
}; };
const getBitbucketEnvironments = async ({
workspaceSlug,
repoSlug,
actorId,
actor,
actorOrgId,
actorAuthMethod,
id
}: TIntegrationAuthBitbucketEnvironmentsDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const environments: TBitbucketEnvironment[] = [];
let hasNextPage = true;
let environmentsUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}/${repoSlug}/environments`;
while (hasNextPage) {
// eslint-disable-next-line
const { data }: { data: { values: TBitbucketEnvironment[]; next: string } } = await request.get(environmentsUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
});
if (data?.values.length > 0) {
environments.push(...data.values);
}
if (data.next) {
environmentsUrl = data.next;
} else {
hasNextPage = false;
}
}
return environments;
};
const getNorthFlankSecretGroups = async ({ const getNorthFlankSecretGroups = async ({
id, id,
actor, actor,
@@ -1499,6 +1550,7 @@ export const integrationAuthServiceFactory = ({
getNorthFlankSecretGroups, getNorthFlankSecretGroups,
getTeamcityBuildConfigs, getTeamcityBuildConfigs,
getBitbucketWorkspaces, getBitbucketWorkspaces,
getBitbucketEnvironments,
getIntegrationAccessToken, getIntegrationAccessToken,
duplicateIntegrationAuth duplicateIntegrationAuth
}; };
@@ -99,6 +99,12 @@ export type TIntegrationAuthBitbucketWorkspaceDTO = {
id: string; id: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthBitbucketEnvironmentsDTO = {
workspaceSlug: string;
repoSlug: string;
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthNorthflankSecretGroupDTO = { export type TIntegrationAuthNorthflankSecretGroupDTO = {
id: string; id: string;
appId: string; appId: string;
@@ -148,6 +154,13 @@ export type TBitbucketWorkspace = {
updated_on: string; updated_on: string;
}; };
export type TBitbucketEnvironment = {
type: string;
uuid: string;
name: string;
slug: string;
};
export type TNorthflankSecretGroup = { export type TNorthflankSecretGroup = {
id: string; id: string;
name: string; name: string;
@@ -334,7 +334,7 @@ export const getIntegrationOptions = async () => {
docsLink: "" docsLink: ""
}, },
{ {
name: "BitBucket", name: "Bitbucket",
slug: "bitbucket", slug: "bitbucket",
image: "BitBucket.png", image: "BitBucket.png",
isAvailable: true, isAvailable: true,
@@ -3631,7 +3631,16 @@ const syncSecretsBitBucket = async ({
const res: { [key: string]: BitbucketVariable } = {}; const res: { [key: string]: BitbucketVariable } = {};
let hasNextPage = true; let hasNextPage = true;
let variablesUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables`;
let variablesUrl: string;
if (integration.targetServiceId) {
// scope: deployment environment
variablesUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/deployments_config/environments/${integration.targetServiceId}/variables`;
} else {
// scope: repository
variablesUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables`;
}
while (hasNextPage) { while (hasNextPage) {
const { data }: { data: VariablesResponse } = await request.get(variablesUrl, { const { data }: { data: VariablesResponse } = await request.get(variablesUrl, {
Binary file not shown.

After

Width:  |  Height:  |  Size: 929 KiB

+21 -13
View File
@@ -3,29 +3,37 @@ title: "Bitbucket"
description: "How to sync secrets from Infisical to Bitbucket" description: "How to sync secrets from Infisical to Bitbucket"
--- ---
Infisical lets you sync secrets to Bitbucket at the repository-level and deployment environment-level.
Prerequisites: Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com) - Set up and add envars to [Infisical Cloud](https://app.infisical.com)
<AccordionGroup> <AccordionGroup>
<Accordion title="Push secrets to Bitbucket from Infisical"> <Accordion title="Push secrets to Bitbucket from Infisical">
<Steps> <Steps>
<Step title="Authorize Infisical for Bitbucket"> <Step title="Authorize Infisical for Bitbucket">
Navigate to your project's integrations tab in Infisical. Navigate to your project's integrations tab in Infisical.
![integrations](../../images/integrations.png) ![integrations](/images/integrations.png)
Press on the Bitbucket tile and grant Infisical access to your Bitbucket account. Press on the Bitbucket tile and grant Infisical access to your Bitbucket account.
![integrations bitbucket authorization](../../images/integrations/bitbucket/integrations-bitbucket-auth.png) ![integrations bitbucket authorization](/images/integrations/bitbucket/integrations-bitbucket.png)
</Step>
<Step title='Configure integration'>
Select which workspace, repository, and optionally, deployment environment, you'd like to sync your secrets
to.
![integrations configure
bitbucket](/images/integrations/bitbucket/integrations-bitbucket-configuration.png)
</Step> Once created, your integration will begin syncing secrets to the configured repository or deployment
<Step title="Start integration"> environment.
Select which Infisical environment secrets you want to sync to which Bitbucket repo and press start integration to start syncing secrets to the repo.
![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket.png) ![integrations bitbucket](/images/integrations/bitbucket/integrations-bitbucket.png)
</Step> </Step>
</Steps> </Steps>
</Accordion> </Accordion>
<Accordion title="Pull secrets in Bitbucket pipelines from Infisical"> <Accordion title="Pull secrets in Bitbucket pipelines from Infisical">
@@ -36,7 +44,7 @@ Prerequisites:
<Step title="Initialize Bitbucket variables"> <Step title="Initialize Bitbucket variables">
Create Bitbucket variables (can be either workspace, repository, or deployment-level) to store Machine Identity Client ID and Client Secret. Create Bitbucket variables (can be either workspace, repository, or deployment-level) to store Machine Identity Client ID and Client Secret.
![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket-env.png) ![integrations bitbucket](/images/integrations/bitbucket/integrations-bitbucket-env.png)
</Step> </Step>
<Step title="Integrate Infisical secrets into the pipeline"> <Step title="Integrate Infisical secrets into the pipeline">
Edit your Bitbucket pipeline YAML file to include the use of the Infisical CLI to fetch and inject secrets into any script or command within the pipeline. Edit your Bitbucket pipeline YAML file to include the use of the Infisical CLI to fetch and inject secrets into any script or command within the pipeline.
+1 -1
View File
@@ -28,7 +28,7 @@ const integrationSlugNameMapping: Mapping = {
"cloudflare-workers": "Cloudflare Workers", "cloudflare-workers": "Cloudflare Workers",
codefresh: "Codefresh", codefresh: "Codefresh",
"digital-ocean-app-platform": "Digital Ocean App Platform", "digital-ocean-app-platform": "Digital Ocean App Platform",
bitbucket: "BitBucket", bitbucket: "Bitbucket",
"cloud-66": "Cloud 66", "cloud-66": "Cloud 66",
northflank: "Northflank", northflank: "Northflank",
windmill: "Windmill", windmill: "Windmill",
@@ -11,7 +11,7 @@ import { faChevronDown, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
const DropdownIndicator = (props: DropdownIndicatorProps) => { const DropdownIndicator = <T,>(props: DropdownIndicatorProps<T>) => {
return ( return (
<components.DropdownIndicator {...props}> <components.DropdownIndicator {...props}>
<FontAwesomeIcon icon={faChevronDown} size="xs" /> <FontAwesomeIcon icon={faChevronDown} size="xs" />
@@ -19,7 +19,7 @@ const DropdownIndicator = (props: DropdownIndicatorProps) => {
); );
}; };
const ClearIndicator = (props: ClearIndicatorProps) => { const ClearIndicator = <T,>(props: ClearIndicatorProps<T>) => {
return ( return (
<components.ClearIndicator {...props}> <components.ClearIndicator {...props}>
<FontAwesomeIcon icon={faCircleXmark} /> <FontAwesomeIcon icon={faCircleXmark} />
@@ -35,7 +35,7 @@ const MultiValueRemove = (props: MultiValueRemoveProps) => {
); );
}; };
const Option = ({ isSelected, children, ...props }: OptionProps) => { const Option = <T,>({ isSelected, children, ...props }: OptionProps<T>) => {
return ( return (
<components.Option isSelected={isSelected} {...props}> <components.Option isSelected={isSelected} {...props}>
{children} {children}
@@ -46,9 +46,9 @@ const Option = ({ isSelected, children, ...props }: OptionProps) => {
); );
}; };
export const MultiSelect = (props: Props) => ( export const MultiSelect = <T,>({ isMulti = true, ...props }: Props<T>) => (
<Select <Select
isMulti isMulti={isMulti}
closeMenuOnSelect={false} closeMenuOnSelect={false}
hideSelectedOptions={false} hideSelectedOptions={false}
unstyled unstyled
@@ -75,11 +75,11 @@ export const MultiSelect = (props: Props) => (
control: ({ isFocused }) => control: ({ isFocused }) =>
twMerge( twMerge(
isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400", isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
"border w-full p-0.5 rounded-md font-inter bg-mineshaft-900 hover:cursor-pointer" "border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 hover:cursor-pointer"
), ),
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5", placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
input: () => "pl-1 py-0.5", input: () => "pl-1 py-0.5",
valueContainer: () => "p-1 max-h-[14rem] !overflow-y-scroll gap-1", valueContainer: () => `p-1 max-h-[14rem] ${isMulti ? "!overflow-y-scroll" : ""} gap-1`,
singleValue: () => "leading-7 ml-1", singleValue: () => "leading-7 ml-1",
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5", multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
multiValueLabel: () => "leading-6 text-sm", multiValueLabel: () => "leading-6 text-sm",
@@ -94,7 +94,7 @@ export const MultiSelect = (props: Props) => (
option: ({ isFocused, isSelected }) => option: ({ isFocused, isSelected }) =>
twMerge( twMerge(
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600", isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
isSelected && "text-mineshaft-400", isSelected && "text-mineshaft-200",
"hover:cursor-pointer text-xs px-3 py-2" "hover:cursor-pointer text-xs px-3 py-2"
), ),
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md" noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
@@ -1,10 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace"; import { workspaceKeys } from "../workspace";
import { import {
App, App,
BitBucketEnvironment,
BitBucketWorkspace, BitBucketWorkspace,
ChecklyGroup, ChecklyGroup,
Environment, Environment,
@@ -94,6 +95,17 @@ const integrationAuthKeys = {
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const, }) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const,
getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) => getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthBitbucketWorkspaces"] as const, [{ integrationAuthId }, "integrationAuthBitbucketWorkspaces"] as const,
getIntegrationAuthBitBucketEnvironments: (
integrationAuthId: string,
workspaceSlug: string,
repoSlug: string
) =>
[
{ integrationAuthId },
workspaceSlug,
repoSlug,
"integrationAuthBitbucketEnvironments"
] as const,
getIntegrationAuthNorthflankSecretGroups: ({ getIntegrationAuthNorthflankSecretGroups: ({
integrationAuthId, integrationAuthId,
appId appId
@@ -403,6 +415,25 @@ const fetchIntegrationAuthBitBucketWorkspaces = async (integrationAuthId: string
return workspaces; return workspaces;
}; };
const fetchIntegrationAuthBitBucketEnvironments = async (
integrationAuthId: string,
workspaceSlug: string,
repoSlug: string
) => {
const {
data: { environments }
} = await apiRequest.get<{ environments: BitBucketEnvironment[] }>(
`/api/v1/integration-auth/${integrationAuthId}/bitbucket/environments`,
{
params: {
workspaceSlug,
repoSlug
}
}
);
return environments;
};
const fetchIntegrationAuthNorthflankSecretGroups = async ({ const fetchIntegrationAuthNorthflankSecretGroups = async ({
integrationAuthId, integrationAuthId,
appId appId
@@ -727,6 +758,30 @@ export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: stri
}); });
}; };
export const useGetIntegrationAuthBitBucketEnvironments = (
{
integrationAuthId,
workspaceSlug,
repoSlug
}: {
integrationAuthId: string;
workspaceSlug: string;
repoSlug: string;
},
options?: UseQueryOptions<BitBucketEnvironment[]>
) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthBitBucketEnvironments(
integrationAuthId,
workspaceSlug,
repoSlug
),
queryFn: () =>
fetchIntegrationAuthBitBucketEnvironments(integrationAuthId, workspaceSlug, repoSlug),
...options
});
};
export const useGetIntegrationAuthNorthflankSecretGroups = ({ export const useGetIntegrationAuthNorthflankSecretGroups = ({
integrationAuthId, integrationAuthId,
appId appId
@@ -79,6 +79,12 @@ export type BitBucketWorkspace = {
slug: string; slug: string;
}; };
export type BitBucketEnvironment = {
uuid: string;
name: string;
slug: string;
};
export type NorthflankSecretGroup = { export type NorthflankSecretGroup = {
name: string; name: string;
groupId: string; groupId: string;
@@ -1,196 +1,310 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { SiBitbucket } from "react-icons/si";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import queryString from "query-string"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useCreateIntegration } from "@app/hooks/api";
import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
Card, Card,
CardTitle, CardTitle,
FormControl, FormControl,
Input, Input,
Select, MultiSelect,
SelectItem Spinner
} from "../../../components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { import {
useCreateIntegration,
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthBitBucketWorkspaces, useGetIntegrationAuthBitBucketWorkspaces
useGetIntegrationAuthById } from "@app/hooks/api";
} from "../../../hooks/api/integrationAuth"; import { useGetIntegrationAuthBitBucketEnvironments } from "@app/hooks/api/integrationAuth/queries";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
enum BitbucketScope {
Repo = "repo",
Env = "environment"
}
const ScopeOptions = [
{
label: "Repository",
value: BitbucketScope.Repo
},
{
label: "Deployment Environment",
value: BitbucketScope.Env
}
];
const formSchema = z
.object({
secretPath: z.string().default("/"),
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
targetRepo: z.object({ name: z.string(), appId: z.string() }),
targetWorkspace: z.object({ name: z.string(), slug: z.string() }),
targetEnvironment: z.object({ name: z.string(), uuid: z.string() }).optional(),
scope: z.object({ label: z.string(), value: z.nativeEnum(BitbucketScope) })
})
.superRefine((val, ctx) => {
if (val.scope.value === BitbucketScope.Env && !val.targetEnvironment) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["targetEnvironment"],
message: "Required"
});
}
});
type TFormData = z.infer<typeof formSchema>;
export default function BitBucketCreateIntegrationPage() { export default function BitBucketCreateIntegrationPage() {
const router = useRouter(); const router = useRouter();
const { mutateAsync } = useCreateIntegration(); const createIntegration = useCreateIntegration();
const [targetAppId, setTargetAppId] = useState(""); const { watch, control, reset, handleSubmit } = useForm<TFormData>({
const [targetEnvironmentId, setTargetEnvironmentId] = useState(""); resolver: zodResolver(formSchema),
defaultValues: {
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); secretPath: "/"
const [secretPath, setSecretPath] = useState("/"); }
const [isLoading, setIsLoading] = useState(false);
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: targetEnvironments } = useGetIntegrationAuthBitBucketWorkspaces(
(integrationAuthId as string) ?? ""
);
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "",
workspaceSlug: targetEnvironmentId
}); });
useEffect(() => { const bitBucketWorkspace = watch("targetWorkspace");
if (workspace) { const bitBucketRepo = watch("targetRepo");
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => { const integrationAuthId = router.query.integrationAuthId as string;
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetAppId(integrationAuthApps[0].appId as string);
} else {
setTargetAppId("none");
}
}
}, [integrationAuthApps]);
useEffect(() => { const { currentWorkspace, isLoading: isProjectLoading } = useWorkspace();
if (targetEnvironments) {
if (targetEnvironments.length > 0) {
setTargetEnvironmentId(targetEnvironments[0].slug);
} else {
setTargetEnvironmentId("none");
}
}
}, [targetEnvironments]);
const handleButtonClick = async () => { const { data: bitbucketWorkspaces, isLoading: isBitbucketWorkspacesLoading } =
useGetIntegrationAuthBitBucketWorkspaces((integrationAuthId as string) ?? "");
const { data: bitbucketRepos, isLoading: isBitbucketReposLoading } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "",
workspaceSlug: bitBucketWorkspace?.slug
});
const { data: bitbucketEnvironments } = useGetIntegrationAuthBitBucketEnvironments(
{
integrationAuthId: (integrationAuthId as string) ?? "",
workspaceSlug: bitBucketWorkspace?.slug,
repoSlug: bitBucketRepo?.appId
},
{ enabled: Boolean(bitBucketWorkspace?.slug && bitBucketRepo?.appId) }
);
const onSubmit = async ({
targetRepo,
sourceEnvironment,
targetWorkspace,
secretPath,
targetEnvironment,
scope
}: TFormData) => {
try { try {
setIsLoading(true); await createIntegration.mutateAsync({
integrationAuthId,
if (!integrationAuth?.id) return;
const targetApp = integrationAuthApps?.find(
(integrationAuthApp) => integrationAuthApp.appId === targetAppId
);
const targetEnvironment = targetEnvironments?.find(
(environment) => environment.slug === targetEnvironmentId
);
if (!targetApp || !targetApp.appId || !targetEnvironment) return;
await mutateAsync({
integrationAuthId: integrationAuth?.id,
isActive: true, isActive: true,
app: targetApp.name, app: targetRepo.name,
appId: targetApp.appId, appId: targetRepo.appId,
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: sourceEnvironment.slug,
targetEnvironment: targetEnvironment.name, targetEnvironment: targetWorkspace.name,
targetEnvironmentId: targetEnvironment.slug, targetEnvironmentId: targetWorkspace.slug,
...(scope.value === BitbucketScope.Env &&
targetEnvironment && {
targetService: targetEnvironment.name,
targetServiceId: targetEnvironment.uuid
}),
secretPath secretPath
}); });
setIsLoading(false); createNotification({
type: "success",
router.push(`/integrations/${localStorage.getItem("projectData.id")}`); text: "Successfully created integration"
});
router.push(`/integrations/${currentWorkspace?.id}`);
} catch (err) { } catch (err) {
createNotification({
type: "error",
text: "Failed to create integration"
});
console.error(err); console.error(err);
} }
}; };
return integrationAuth && useEffect(() => {
workspace && if (!bitbucketRepos || !bitbucketWorkspaces || !currentWorkspace) return;
selectedSourceEnvironment &&
integrationAuthApps && reset({
targetEnvironments ? ( targetRepo: bitbucketRepos[0],
<div className="flex h-full w-full items-center justify-center"> targetWorkspace: bitbucketWorkspaces[0],
<Card className="max-w-md rounded-md p-8"> sourceEnvironment: currentWorkspace.environments[0],
<CardTitle className="text-center">BitBucket Integration</CardTitle> secretPath: "/",
<FormControl label="Project Environment" className="mt-4"> scope: ScopeOptions[0]
<Select });
value={selectedSourceEnvironment} }, [bitbucketWorkspaces, bitbucketRepos, currentWorkspace]);
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500" if (isProjectLoading || isBitbucketWorkspacesLoading || isBitbucketReposLoading)
> return (
{workspace?.environments.map((sourceEnvironment) => ( <div className="flex h-full w-full items-center justify-center p-24">
<SelectItem <Spinner />
value={sourceEnvironment.slug} </div>
key={`source-environment-${sourceEnvironment.slug}`} );
const scope = watch("scope");
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full items-center justify-center"
>
<Card className="max-w-md rounded-md p-8 pt-4">
<CardTitle className=" text-center">
<SiBitbucket size="1.2rem" className="mr-2 mb-1 inline-block" />
Bitbucket Integration
</CardTitle>
<Controller
control={control}
name="sourceEnvironment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="Project Environment"
>
<MultiSelect
closeMenuOnSelect
isMulti={false}
getOptionValue={(option) => option.slug}
value={value}
getOptionLabel={(option) => option.name}
onChange={onChange}
options={currentWorkspace?.environments}
placeholder="Select a project environment"
isDisabled={!bitbucketWorkspaces?.length}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} label="Secrets Path">
<Input
value={value}
onChange={onChange}
placeholder={'Provide a path (defaults to "/")'}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="targetWorkspace"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="Bitbucket Workspace"
>
<MultiSelect
closeMenuOnSelect
isMulti={false}
getOptionValue={(option) => option.slug}
value={value}
getOptionLabel={(option) => option.name}
onChange={onChange}
options={bitbucketWorkspaces}
placeholder={
bitbucketWorkspaces?.length ? "Select a workspace..." : "No workspaces found..."
}
isDisabled={!bitbucketWorkspaces?.length}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="targetRepo"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)} label="Bitbucket Repo">
<MultiSelect
closeMenuOnSelect
isMulti={false}
getOptionValue={(option) => option.appId!}
value={value}
getOptionLabel={(option) => option.name}
onChange={onChange}
options={bitbucketRepos}
placeholder={
bitbucketRepos?.length ? "Select a repository..." : "No repositories found..."
}
isDisabled={!bitbucketRepos?.length}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="scope"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)} label="Scope">
<MultiSelect
closeMenuOnSelect
isMulti={false}
value={value}
getOptionValue={(option) => option.value}
getOptionLabel={(option) => option.label}
onChange={onChange}
options={ScopeOptions}
/>
</FormControl>
)}
/>
{scope?.value === BitbucketScope.Env && (
<Controller
control={control}
name="targetEnvironment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="Bitbucket Deployment Environment"
> >
{sourceEnvironment.name} <MultiSelect
</SelectItem> closeMenuOnSelect
))} isMulti={false}
</Select> getOptionValue={(option) => option.uuid}
</FormControl> value={value}
<FormControl label="Secrets Path"> getOptionLabel={(option) => option.name}
<Input onChange={onChange}
value={secretPath} options={bitbucketEnvironments}
onChange={(evt) => setSecretPath(evt.target.value)} placeholder={
placeholder="Provide a path, default is /" bitbucketEnvironments?.length
? "Select an environment..."
: "No environments found..."
}
isDisabled={!bitbucketEnvironments?.length}
/>
</FormControl>
)}
/> />
</FormControl> )}
<FormControl label="BitBucket Workspace">
<Select
value={targetEnvironmentId}
onValueChange={(val) => setTargetEnvironmentId(val)}
className="w-full border border-mineshaft-500"
isDisabled={targetEnvironments.length === 0}
>
{targetEnvironments.length > 0 ? (
targetEnvironments.map((targetEnvironment) => (
<SelectItem
value={targetEnvironment.slug as string}
key={`target-environment-${targetEnvironment.slug as string}`}
>
{targetEnvironment.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-environment-none">
No workspaces found
</SelectItem>
)}
</Select>
</FormControl>
<FormControl label="BitBucket Repo">
<Select
value={targetAppId}
onValueChange={(val) => setTargetAppId(val)}
className="w-full border border-mineshaft-500"
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.appId as string}
key={`target-app-${integrationAuthApp.appId as string}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No repositories found
</SelectItem>
)}
</Select>
</FormControl>
<Button <Button
onClick={handleButtonClick} type="submit"
color="mineshaft" colorSchema="primary"
className="mt-4" className="mt-4"
isLoading={isLoading} isLoading={createIntegration.isLoading}
isDisabled={integrationAuthApps.length === 0} isDisabled={createIntegration.isLoading || !bitbucketRepos?.length}
> >
Create Integration Create Integration
</Button> </Button>
</Card> </Card>
</div> </form>
) : (
<div />
); );
} }
@@ -53,6 +53,8 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
case "aws-parameter-store": case "aws-parameter-store":
case "rundeck": case "rundeck":
return "Path"; return "Path";
case "bitbucket":
return "Repository";
case "github": case "github":
if (["github-env", "github-repo"].includes(integration.scope!)) { if (["github-env", "github-repo"].includes(integration.scope!)) {
return "Repository"; return "Repository";
@@ -92,10 +94,18 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
}; };
const targetEnvironmentDetails = () => { const targetEnvironmentDetails = () => {
if (integration.integration === "bitbucket") {
return (
<div className="flex flex-col">
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Workspace" />
<div className="text-sm text-mineshaft-300">
{integration.targetEnvironment || integration.targetEnvironmentId}
</div>
</div>
);
}
if ( if (
["vercel", "netlify", "railway", "gitlab", "teamcity", "bitbucket"].includes( ["vercel", "netlify", "railway", "gitlab", "teamcity"].includes(integration.integration) ||
integration.integration
) ||
(integration.integration === "github" && integration.scope === "github-env") (integration.integration === "github" && integration.scope === "github-env")
) { ) {
return ( return (
@@ -153,6 +163,18 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
); );
} }
if (integration.integration === "bitbucket" && integration.targetServiceId) {
return (
<div>
<FormLabel
className="text-sm font-semibold text-mineshaft-300"
label="Deployment Environment"
/>
<div className="text-sm text-mineshaft-300">{integration.targetService}</div>
</div>
);
}
return null; return null;
}; };
@@ -53,7 +53,7 @@ export const ConfiguredIntegrationItem = ({
{integration.secretPath} {integration.secretPath}
</div> </div>
</div> </div>
<div className="flex h-full items-center"> <div className="mt-3 flex h-full items-center">
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400" /> <FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400" />
</div> </div>
<div className="ml-4 flex flex-col"> <div className="ml-4 flex flex-col">
@@ -107,6 +107,7 @@ export const ConfiguredIntegrationItem = ({
label={ label={
(integration.integration === "qovery" && integration?.scope) || (integration.integration === "qovery" && integration?.scope) ||
(integration.integration === "circleci" && "Project") || (integration.integration === "circleci" && "Project") ||
(integration.integration === "bitbucket" && "Repository") ||
(integration.integration === "aws-secret-manager" && "Secret") || (integration.integration === "aws-secret-manager" && "Secret") ||
(["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") || (["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") ||
(integration?.integration === "terraform-cloud" && "Project") || (integration?.integration === "terraform-cloud" && "Project") ||
@@ -133,7 +134,6 @@ export const ConfiguredIntegrationItem = ({
integration.integration === "railway" || integration.integration === "railway" ||
integration.integration === "gitlab" || integration.integration === "gitlab" ||
integration.integration === "teamcity" || integration.integration === "teamcity" ||
integration.integration === "bitbucket" ||
(integration.integration === "github" && integration.scope === "github-env")) && ( (integration.integration === "github" && integration.scope === "github-env")) && (
<div className="ml-4 flex flex-col"> <div className="ml-4 flex flex-col">
<FormLabel label="Target Environment" /> <FormLabel label="Target Environment" />
@@ -142,6 +142,24 @@ export const ConfiguredIntegrationItem = ({
</div> </div>
</div> </div>
)} )}
{integration.integration === "bitbucket" && (
<>
{integration.targetServiceId && (
<div className="ml-2 flex flex-col">
<FormLabel label="Environment" />
<div className="min-w-[8rem] overflow-clip text-ellipsis whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.targetService || integration.targetServiceId}
</div>
</div>
)}
<div className="ml-2 flex flex-col">
<FormLabel label="Workspace" />
<div className="overflow-clip text-ellipsis whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.targetEnvironment || integration.targetEnvironmentId}
</div>
</div>
</>
)}
{integration.integration === "checkly" && integration.targetService && ( {integration.integration === "checkly" && integration.targetService && (
<div className="ml-2"> <div className="ml-2">
<FormLabel label="Group" /> <FormLabel label="Group" />