Merge pull request #2713 from scott-ray-wilson/bitbucket-integration-additions

Feature: Add Support for Deployment Environment Scope for Bitbucket Integration
This commit is contained in:
Scott Wilson
2024-11-11 11:27:12 -08:00
committed by GitHub
22 changed files with 529 additions and 202 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({
method: "GET",
url: "/:integrationAuthId/northflank/secret-groups",
@@ -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
};
@@ -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;
@@ -334,7 +334,7 @@ export const getIntegrationOptions = async () => {
docsLink: ""
},
{
name: "BitBucket",
name: "Bitbucket",
slug: "bitbucket",
image: "BitBucket.png",
isAvailable: true,
@@ -3631,7 +3631,14 @@ 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`;
const rootUrl = integration.targetServiceId
? // scope: deployment environment
`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/deployments_config/environments/${integration.targetServiceId}/variables`
: // scope: repository
`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables`;
let variablesUrl = rootUrl;
while (hasNextPage) {
const { data }: { data: VariablesResponse } = await request.get(variablesUrl, {
@@ -3658,7 +3665,7 @@ const syncSecretsBitBucket = async ({
if (key in res) {
// update existing secret
await request.put(
`${variablesUrl}/${res[key].uuid}`,
`${rootUrl}/${res[key].uuid}`,
{
key,
value: secrets[key].value,
@@ -3674,7 +3681,7 @@ const syncSecretsBitBucket = async ({
} else {
// create new secret
await request.post(
variablesUrl,
rootUrl,
{
key,
value: secrets[key].value,
Binary file not shown.

After

Width:  |  Height:  |  Size: 929 KiB

+15 -7
View File
@@ -3,6 +3,9 @@ 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)
@@ -13,17 +16,22 @@ Prerequisites:
<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.
![integrations bitbucket authorization](../../images/integrations/bitbucket/integrations-bitbucket-auth.png)
![integrations bitbucket authorization](/images/integrations/bitbucket/integrations-bitbucket.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.
<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)
![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket.png)
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>
@@ -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.
+1 -1
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",
@@ -7,11 +7,11 @@ import Select, {
Props
} from "react-select";
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
import { faChevronDown, faX } from "@fortawesome/free-solid-svg-icons";
import { faChevronDown, faXmark } 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} />
@@ -30,12 +30,12 @@ const ClearIndicator = (props: ClearIndicatorProps) => {
const MultiValueRemove = (props: MultiValueRemoveProps) => {
return (
<components.MultiValueRemove {...props}>
<FontAwesomeIcon icon={faX} size="xs" />
<FontAwesomeIcon icon={faXmark} size="xs" />
</components.MultiValueRemove>
);
};
const Option = ({ isSelected, children, ...props }: OptionProps) => {
const Option = <T,>({ isSelected, children, ...props }: OptionProps<T>) => {
return (
<components.Option isSelected={isSelected} {...props}>
{children}
@@ -46,10 +46,10 @@ const Option = ({ isSelected, children, ...props }: OptionProps) => {
);
};
export const MultiSelect = (props: Props) => (
export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: Props<T>) => (
<Select
isMulti
closeMenuOnSelect={false}
isMulti={isMulti}
closeMenuOnSelect={closeMenuOnSelect ?? !isMulti}
hideSelectedOptions={false}
unstyled
styles={{
@@ -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"
@@ -0,0 +1 @@
export * from "./FilterableSelect";
@@ -1 +0,0 @@
export * from "./MultiSelect";
+1 -1
View File
@@ -11,6 +11,7 @@ export * from "./Drawer";
export * from "./Dropdown";
export * from "./EmailServiceSetupModal";
export * from "./EmptyState";
export * from "./FilterableSelect";
export * from "./FontAwesomeSymbol";
export * from "./FormControl";
export * from "./HoverCardv2";
@@ -18,7 +19,6 @@ export * from "./IconButton";
export * from "./Input";
export * from "./Menu";
export * from "./Modal";
export * from "./MultiSelect";
export * from "./NoticeBanner";
export * from "./Pagination";
export * from "./Popoverv2";
@@ -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
@@ -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;
+1 -1
View File
@@ -282,7 +282,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
);
const { data: projectTemplates = [] } = useListProjectTemplates({
enabled: canReadProjectTemplates && subscription?.projectTemplates
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
});
const onCreateProject = async ({ name, addMembers, kmsKeyId, template }: TAddProjectFormData) => {
@@ -1,196 +1,300 @@
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,
FilterableSelect,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
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"
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"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
<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"
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
<FilterableSelect
getOptionValue={(option) => option.slug}
value={value}
getOptionLabel={(option) => option.name}
onChange={onChange}
options={currentWorkspace?.environments}
placeholder="Select a project environment"
isDisabled={!bitbucketWorkspaces?.length}
/>
</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>
/>
<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>
<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>
/>
<Controller
control={control}
name="targetWorkspace"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="Bitbucket Workspace"
>
<FilterableSelect
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">
<FilterableSelect
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">
<FilterableSelect
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"
>
<FilterableSelect
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>
)}
/>
)}
<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>
);
}
@@ -589,7 +589,7 @@ const OrganizationPage = () => {
);
const { data: projectTemplates = [] } = useListProjectTemplates({
enabled: canReadProjectTemplates && subscription?.projectTemplates
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
});
const isAddingProjectsAllowed = subscription?.workspaceLimit
@@ -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;
};
@@ -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" />
@@ -15,10 +15,10 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FilterableSelect,
FormControl,
Modal,
ModalContent,
MultiSelect
ModalContent
} from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import {
@@ -153,7 +153,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
errorText={errors.orgMemberships?.[0]?.message}
label="Invite users to project"
>
<MultiSelect
<FilterableSelect
className="w-full"
placeholder="Add one or more users..."
isMulti
@@ -6,7 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, MultiSelect } from "@app/components/v2";
import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { getKeyValue } from "@app/helpers/parseEnvVar";
@@ -148,7 +148,7 @@ export const CreateSecretForm = ({
)
}
>
<MultiSelect
<FilterableSelect
className="w-full"
placeholder="Select tags to assign to secret..."
isMulti
@@ -10,10 +10,10 @@ import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
FilterableSelect,
FormControl,
FormLabel,
Input,
MultiSelect,
Tooltip
} from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
@@ -249,7 +249,7 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
)
}
>
<MultiSelect
<FilterableSelect
className="w-full"
placeholder="Select tags to assign to secrets..."
isMulti