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

This commit is contained in:
Scott Wilson
2024-11-08 19:31:18 -08:00
parent 65d642113d
commit d4c911a28f
14 changed files with 525 additions and 186 deletions

View File

@@ -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({
method: "GET",
url: "/:integrationAuthId/northflank/secret-groups",

View File

@@ -20,6 +20,7 @@ import { getApps } from "./integration-app-list";
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import {
TBitbucketEnvironment,
TBitbucketWorkspace,
TChecklyGroups,
TDeleteIntegrationAuthByIdDTO,
@@ -30,6 +31,7 @@ import {
THerokuPipelineCoupling,
TIntegrationAuthAppsDTO,
TIntegrationAuthAwsKmsKeyDTO,
TIntegrationAuthBitbucketEnvironmentsDTO,
TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthGithubEnvsDTO,
@@ -1261,6 +1263,55 @@ export const integrationAuthServiceFactory = ({
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 ({
id,
actor,
@@ -1499,6 +1550,7 @@ export const integrationAuthServiceFactory = ({
getNorthFlankSecretGroups,
getTeamcityBuildConfigs,
getBitbucketWorkspaces,
getBitbucketEnvironments,
getIntegrationAccessToken,
duplicateIntegrationAuth
};

View File

@@ -99,6 +99,12 @@ export type TIntegrationAuthBitbucketWorkspaceDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthBitbucketEnvironmentsDTO = {
workspaceSlug: string;
repoSlug: string;
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthNorthflankSecretGroupDTO = {
id: string;
appId: string;
@@ -148,6 +154,13 @@ export type TBitbucketWorkspace = {
updated_on: string;
};
export type TBitbucketEnvironment = {
type: string;
uuid: string;
name: string;
slug: string;
};
export type TNorthflankSecretGroup = {
id: string;
name: string;

View File

@@ -334,7 +334,7 @@ export const getIntegrationOptions = async () => {
docsLink: ""
},
{
name: "BitBucket",
name: "Bitbucket",
slug: "bitbucket",
image: "BitBucket.png",
isAvailable: true,

View File

@@ -3631,7 +3631,16 @@ const syncSecretsBitBucket = async ({
const res: { [key: string]: BitbucketVariable } = {};
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) {
const { data }: { data: VariablesResponse } = await request.get(variablesUrl, {

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 KiB

View File

@@ -3,29 +3,37 @@ title: "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:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
<AccordionGroup>
<Accordion title="Push secrets to Bitbucket from Infisical">
<Steps>
<Step title="Authorize Infisical for Bitbucket">
Navigate to your project's integrations tab in Infisical.
<Steps>
<Step title="Authorize Infisical for Bitbucket">
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>
<Step title="Start integration">
Select which Infisical environment secrets you want to sync to which Bitbucket repo and press start integration to start syncing secrets to the repo.
Once created, your integration will begin syncing secrets to the configured repository or deployment
environment.
![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket.png)
</Step>
</Steps>
![integrations bitbucket](/images/integrations/bitbucket/integrations-bitbucket.png)
</Step>
</Steps>
</Accordion>
<Accordion title="Pull secrets in Bitbucket pipelines from Infisical">
@@ -36,7 +44,7 @@ Prerequisites:
<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.
![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket-env.png)
![integrations bitbucket](/images/integrations/bitbucket/integrations-bitbucket-env.png)
</Step>
<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.

View File

@@ -28,7 +28,7 @@ const integrationSlugNameMapping: Mapping = {
"cloudflare-workers": "Cloudflare Workers",
codefresh: "Codefresh",
"digital-ocean-app-platform": "Digital Ocean App Platform",
bitbucket: "BitBucket",
bitbucket: "Bitbucket",
"cloud-66": "Cloud 66",
northflank: "Northflank",
windmill: "Windmill",

View File

@@ -11,7 +11,7 @@ import { faChevronDown, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
const DropdownIndicator = (props: DropdownIndicatorProps) => {
const DropdownIndicator = <T,>(props: DropdownIndicatorProps<T>) => {
return (
<components.DropdownIndicator {...props}>
<FontAwesomeIcon icon={faChevronDown} size="xs" />
@@ -19,7 +19,7 @@ const DropdownIndicator = (props: DropdownIndicatorProps) => {
);
};
const ClearIndicator = (props: ClearIndicatorProps) => {
const ClearIndicator = <T,>(props: ClearIndicatorProps<T>) => {
return (
<components.ClearIndicator {...props}>
<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 (
<components.Option isSelected={isSelected} {...props}>
{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
isMulti
isMulti={isMulti}
closeMenuOnSelect={false}
hideSelectedOptions={false}
unstyled
@@ -75,11 +75,11 @@ export const MultiSelect = (props: Props) => (
control: ({ isFocused }) =>
twMerge(
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",
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",
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
multiValueLabel: () => "leading-6 text-sm",
@@ -94,7 +94,7 @@ export const MultiSelect = (props: Props) => (
option: ({ isFocused, isSelected }) =>
twMerge(
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"
),
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"

View File

@@ -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 { workspaceKeys } from "../workspace";
import {
App,
BitBucketEnvironment,
BitBucketWorkspace,
ChecklyGroup,
Environment,
@@ -94,6 +95,17 @@ const integrationAuthKeys = {
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const,
getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthBitbucketWorkspaces"] as const,
getIntegrationAuthBitBucketEnvironments: (
integrationAuthId: string,
workspaceSlug: string,
repoSlug: string
) =>
[
{ integrationAuthId },
workspaceSlug,
repoSlug,
"integrationAuthBitbucketEnvironments"
] as const,
getIntegrationAuthNorthflankSecretGroups: ({
integrationAuthId,
appId
@@ -403,6 +415,25 @@ const fetchIntegrationAuthBitBucketWorkspaces = async (integrationAuthId: string
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 ({
integrationAuthId,
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 = ({
integrationAuthId,
appId

View File

@@ -79,6 +79,12 @@ export type BitBucketWorkspace = {
slug: string;
};
export type BitBucketEnvironment = {
uuid: string;
name: string;
slug: string;
};
export type NorthflankSecretGroup = {
name: string;
groupId: string;

View File

@@ -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 queryString from "query-string";
import { useCreateIntegration } from "@app/hooks/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
MultiSelect,
Spinner
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useCreateIntegration,
useGetIntegrationAuthApps,
useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
useGetIntegrationAuthBitBucketWorkspaces
} from "@app/hooks/api";
import { useGetIntegrationAuthBitBucketEnvironments } from "@app/hooks/api/integrationAuth/queries";
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() {
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const createIntegration = useCreateIntegration();
const [targetAppId, setTargetAppId] = useState("");
const [targetEnvironmentId, setTargetEnvironmentId] = useState("");
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
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
const { watch, control, reset, handleSubmit } = useForm<TFormData>({
resolver: zodResolver(formSchema),
defaultValues: {
secretPath: "/"
}
});
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
const bitBucketWorkspace = watch("targetWorkspace");
const bitBucketRepo = watch("targetRepo");
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetAppId(integrationAuthApps[0].appId as string);
} else {
setTargetAppId("none");
}
}
}, [integrationAuthApps]);
const integrationAuthId = router.query.integrationAuthId as string;
useEffect(() => {
if (targetEnvironments) {
if (targetEnvironments.length > 0) {
setTargetEnvironmentId(targetEnvironments[0].slug);
} else {
setTargetEnvironmentId("none");
}
}
}, [targetEnvironments]);
const { currentWorkspace, isLoading: isProjectLoading } = useWorkspace();
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 {
setIsLoading(true);
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,
await createIntegration.mutateAsync({
integrationAuthId,
isActive: true,
app: targetApp.name,
appId: targetApp.appId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: targetEnvironment.name,
targetEnvironmentId: targetEnvironment.slug,
app: targetRepo.name,
appId: targetRepo.appId,
sourceEnvironment: sourceEnvironment.slug,
targetEnvironment: targetWorkspace.name,
targetEnvironmentId: targetWorkspace.slug,
...(scope.value === BitbucketScope.Env &&
targetEnvironment && {
targetService: targetEnvironment.name,
targetServiceId: targetEnvironment.uuid
}),
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
createNotification({
type: "success",
text: "Successfully created integration"
});
router.push(`/integrations/${currentWorkspace?.id}`);
} catch (err) {
createNotification({
type: "error",
text: "Failed to create integration"
});
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetEnvironments ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">BitBucket Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
useEffect(() => {
if (!bitbucketRepos || !bitbucketWorkspaces || !currentWorkspace) return;
reset({
targetRepo: bitbucketRepos[0],
targetWorkspace: bitbucketWorkspaces[0],
sourceEnvironment: currentWorkspace.environments[0],
secretPath: "/",
scope: ScopeOptions[0]
});
}, [bitbucketWorkspaces, bitbucketRepos, currentWorkspace]);
if (isProjectLoading || isBitbucketWorkspacesLoading || isBitbucketReposLoading)
return (
<div className="flex h-full w-full items-center justify-center p-24">
<Spinner />
</div>
);
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}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
<MultiSelect
closeMenuOnSelect
isMulti={false}
getOptionValue={(option) => option.uuid}
value={value}
getOptionLabel={(option) => option.name}
onChange={onChange}
options={bitbucketEnvironments}
placeholder={
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
onClick={handleButtonClick}
color="mineshaft"
type="submit"
colorSchema="primary"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
isLoading={createIntegration.isLoading}
isDisabled={createIntegration.isLoading || !bitbucketRepos?.length}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
</form>
);
}

View File

@@ -53,6 +53,8 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
case "aws-parameter-store":
case "rundeck":
return "Path";
case "bitbucket":
return "Repository";
case "github":
if (["github-env", "github-repo"].includes(integration.scope!)) {
return "Repository";
@@ -92,10 +94,18 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
};
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 (
["vercel", "netlify", "railway", "gitlab", "teamcity", "bitbucket"].includes(
integration.integration
) ||
["vercel", "netlify", "railway", "gitlab", "teamcity"].includes(integration.integration) ||
(integration.integration === "github" && integration.scope === "github-env")
) {
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;
};

View File

@@ -53,7 +53,7 @@ export const ConfiguredIntegrationItem = ({
{integration.secretPath}
</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" />
</div>
<div className="ml-4 flex flex-col">
@@ -107,6 +107,7 @@ export const ConfiguredIntegrationItem = ({
label={
(integration.integration === "qovery" && integration?.scope) ||
(integration.integration === "circleci" && "Project") ||
(integration.integration === "bitbucket" && "Repository") ||
(integration.integration === "aws-secret-manager" && "Secret") ||
(["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") ||
(integration?.integration === "terraform-cloud" && "Project") ||
@@ -133,7 +134,6 @@ export const ConfiguredIntegrationItem = ({
integration.integration === "railway" ||
integration.integration === "gitlab" ||
integration.integration === "teamcity" ||
integration.integration === "bitbucket" ||
(integration.integration === "github" && integration.scope === "github-env")) && (
<div className="ml-4 flex flex-col">
<FormLabel label="Target Environment" />
@@ -142,6 +142,24 @@ export const ConfiguredIntegrationItem = ({
</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 && (
<div className="ml-2">
<FormLabel label="Group" />