mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 13:26:31 +00:00
Merge pull request #2310 from Infisical/daniel/azure-devops-integration
Feat: Azure DevOps Integration
This commit is contained in:
@@ -98,6 +98,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
|
||||||
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
|
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
||||||
type: provider.type,
|
type: provider.type,
|
||||||
version: 1,
|
version: 1,
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
|||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
teamId: z.string().trim().optional(),
|
teamId: z.string().trim().optional(),
|
||||||
|
azureDevOpsOrgName: z.string().trim().optional(),
|
||||||
workspaceSlug: z.string().trim().optional()
|
workspaceSlug: z.string().trim().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -1030,11 +1030,31 @@ const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAppsAzureDevOps = async ({ accessToken, orgName }: { accessToken: string; orgName: string }) => {
|
||||||
|
const res = (
|
||||||
|
await request.get<{ count: number; value: Record<string, string>[] }>(
|
||||||
|
`${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/projects?api-version=7.2-preview.2`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
const apps = res.value.map((a) => ({
|
||||||
|
name: a.name,
|
||||||
|
appId: a.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
export const getApps = async ({
|
export const getApps = async ({
|
||||||
integration,
|
integration,
|
||||||
accessToken,
|
accessToken,
|
||||||
accessId,
|
accessId,
|
||||||
teamId,
|
teamId,
|
||||||
|
azureDevOpsOrgName,
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
url
|
url
|
||||||
}: {
|
}: {
|
||||||
@@ -1042,6 +1062,7 @@ export const getApps = async ({
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
accessId?: string;
|
accessId?: string;
|
||||||
teamId?: string | null;
|
teamId?: string | null;
|
||||||
|
azureDevOpsOrgName?: string | null;
|
||||||
workspaceSlug?: string;
|
workspaceSlug?: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
}): Promise<App[]> => {
|
}): Promise<App[]> => {
|
||||||
@@ -1184,6 +1205,12 @@ export const getApps = async ({
|
|||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
|
|
||||||
|
case Integrations.AZURE_DEVOPS:
|
||||||
|
return getAppsAzureDevOps({
|
||||||
|
accessToken,
|
||||||
|
orgName: azureDevOpsOrgName as string
|
||||||
|
});
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new BadRequestError({ message: "integration not found" });
|
throw new BadRequestError({ message: "integration not found" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -440,6 +440,7 @@ export const integrationAuthServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
teamId,
|
teamId,
|
||||||
|
azureDevOpsOrgName,
|
||||||
id,
|
id,
|
||||||
workspaceSlug
|
workspaceSlug
|
||||||
}: TIntegrationAuthAppsDTO) => {
|
}: TIntegrationAuthAppsDTO) => {
|
||||||
@@ -462,6 +463,7 @@ export const integrationAuthServiceFactory = ({
|
|||||||
accessToken,
|
accessToken,
|
||||||
accessId,
|
accessId,
|
||||||
teamId,
|
teamId,
|
||||||
|
azureDevOpsOrgName,
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
url: integrationAuth.url
|
url: integrationAuth.url
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { TIntegrations } from "@app/db/schemas";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TGetIntegrationAuthDTO = {
|
export type TGetIntegrationAuthDTO = {
|
||||||
@@ -28,6 +29,7 @@ export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
|
|||||||
export type TIntegrationAuthAppsDTO = {
|
export type TIntegrationAuthAppsDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
|
azureDevOpsOrgName?: string;
|
||||||
workspaceSlug?: string;
|
workspaceSlug?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
@@ -163,3 +165,13 @@ export type TTeamCityBuildConfig = {
|
|||||||
href: string;
|
href: string;
|
||||||
webUrl: string;
|
webUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TIntegrationsWithEnvironment = TIntegrations & {
|
||||||
|
environment?:
|
||||||
|
| {
|
||||||
|
id?: string | null | undefined;
|
||||||
|
name?: string | null | undefined;
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
};
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ export enum Integrations {
|
|||||||
CLOUD_66 = "cloud-66",
|
CLOUD_66 = "cloud-66",
|
||||||
NORTHFLANK = "northflank",
|
NORTHFLANK = "northflank",
|
||||||
HASURA_CLOUD = "hasura-cloud",
|
HASURA_CLOUD = "hasura-cloud",
|
||||||
RUNDECK = "rundeck"
|
RUNDECK = "rundeck",
|
||||||
|
AZURE_DEVOPS = "azure-devops"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum IntegrationType {
|
export enum IntegrationType {
|
||||||
@@ -88,6 +89,7 @@ export enum IntegrationUrls {
|
|||||||
CLOUD_66_API_URL = "https://app.cloud66.com/api",
|
CLOUD_66_API_URL = "https://app.cloud66.com/api",
|
||||||
NORTHFLANK_API_URL = "https://api.northflank.com",
|
NORTHFLANK_API_URL = "https://api.northflank.com",
|
||||||
HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql",
|
HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql",
|
||||||
|
AZURE_DEVOPS_API_URL = "https://dev.azure.com",
|
||||||
|
|
||||||
GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com",
|
GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com",
|
||||||
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
|
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
|
||||||
@@ -378,6 +380,15 @@ export const getIntegrationOptions = async () => {
|
|||||||
type: "pat",
|
type: "pat",
|
||||||
clientId: "",
|
clientId: "",
|
||||||
docsLink: ""
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Azure DevOps",
|
||||||
|
slug: "azure-devops",
|
||||||
|
image: "Microsoft Azure.png",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
|
|||||||
|
|
||||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||||
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||||
|
import { TIntegrationsWithEnvironment } from "./integration-auth-types";
|
||||||
import {
|
import {
|
||||||
IntegrationInitialSyncBehavior,
|
IntegrationInitialSyncBehavior,
|
||||||
IntegrationMappingBehavior,
|
IntegrationMappingBehavior,
|
||||||
@@ -2075,6 +2076,116 @@ const syncSecretsTravisCI = async ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to GitLab repo with name [integration.app]
|
||||||
|
*/
|
||||||
|
const syncSecretsAzureDevops = async ({
|
||||||
|
integrationAuth,
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integrationAuth: TIntegrationAuths;
|
||||||
|
integration: TIntegrationsWithEnvironment;
|
||||||
|
secrets: Record<string, { value: string; comment?: string }>;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
if (!integration.appId || !integration.app) {
|
||||||
|
throw new Error("Azure DevOps: orgId and projectId are required");
|
||||||
|
}
|
||||||
|
if (!integration.environment || !integration.environment.name) {
|
||||||
|
throw new Error("Azure DevOps: environment is required");
|
||||||
|
}
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Basic ${accessToken}`
|
||||||
|
};
|
||||||
|
const azureDevopsApiUrl = integrationAuth.url ? `${integrationAuth.url}` : IntegrationUrls.AZURE_DEVOPS_API_URL;
|
||||||
|
|
||||||
|
const getEnvGroupId = async (orgId: string, project: string, env: string) => {
|
||||||
|
let groupId;
|
||||||
|
const url: string | null =
|
||||||
|
`${azureDevopsApiUrl}/${orgId}/${project}/_apis/distributedtask/variablegroups?api-version=7.2-preview.2`;
|
||||||
|
|
||||||
|
const response = await request.get(url, { headers });
|
||||||
|
for (const group of response.data.value) {
|
||||||
|
const groupName = group.name;
|
||||||
|
if (groupName === env) {
|
||||||
|
groupId = group.id;
|
||||||
|
return { groupId, groupName };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { groupId: "", groupName: "" };
|
||||||
|
};
|
||||||
|
|
||||||
|
const { groupId, groupName } = await getEnvGroupId(integration.app, integration.appId, integration.environment.name);
|
||||||
|
|
||||||
|
const variables: Record<string, { value: string }> = {};
|
||||||
|
for (const key of Object.keys(secrets)) {
|
||||||
|
variables[key] = { value: secrets[key].value };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groupId) {
|
||||||
|
// create new variable group if not present
|
||||||
|
const url = `${azureDevopsApiUrl}/${integration.app}/_apis/distributedtask/variablegroups?api-version=7.2-preview.2`;
|
||||||
|
const config = {
|
||||||
|
method: "POST",
|
||||||
|
url,
|
||||||
|
data: {
|
||||||
|
name: integration.environment.name,
|
||||||
|
description: integration.environment.name,
|
||||||
|
type: "Vsts",
|
||||||
|
owner: "Library",
|
||||||
|
variables,
|
||||||
|
variableGroupProjectReferences: [
|
||||||
|
{
|
||||||
|
name: integration.environment.name,
|
||||||
|
projectReference: {
|
||||||
|
name: integration.appId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
headers
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await request.post(url, config.data, config.headers);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(`Azure DevOps: Failed to create variable group: ${res.statusText}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// sync variables for pre-existing variable group
|
||||||
|
const url = `${azureDevopsApiUrl}/${integration.app}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.2-preview.2`;
|
||||||
|
const config = {
|
||||||
|
method: "PUT",
|
||||||
|
url,
|
||||||
|
data: {
|
||||||
|
name: groupName,
|
||||||
|
description: groupName,
|
||||||
|
type: "Vsts",
|
||||||
|
owner: "Library",
|
||||||
|
variables,
|
||||||
|
variableGroupProjectReferences: [
|
||||||
|
{
|
||||||
|
name: groupName,
|
||||||
|
projectReference: {
|
||||||
|
name: integration.appId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
headers
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const res = await request.put(url, config.data, config.headers);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(`Azure DevOps: Failed to update variable group: ${res.statusText}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync/push [secrets] to GitLab repo with name [integration.app]
|
* Sync/push [secrets] to GitLab repo with name [integration.app]
|
||||||
*/
|
*/
|
||||||
@@ -3714,6 +3825,15 @@ export const syncIntegrationSecrets = async ({
|
|||||||
updateManySecretsRawFn
|
updateManySecretsRawFn
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Integrations.AZURE_DEVOPS:
|
||||||
|
await syncSecretsAzureDevops({
|
||||||
|
integrationAuth,
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
case Integrations.AWS_PARAMETER_STORE:
|
case Integrations.AWS_PARAMETER_STORE:
|
||||||
response = await syncSecretsAWSParameterStore({
|
response = await syncSecretsAWSParameterStore({
|
||||||
integration,
|
integration,
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ const integrationSlugNameMapping: Mapping = {
|
|||||||
windmill: "Windmill",
|
windmill: "Windmill",
|
||||||
"gcp-secret-manager": "GCP Secret Manager",
|
"gcp-secret-manager": "GCP Secret Manager",
|
||||||
"hasura-cloud": "Hasura Cloud",
|
"hasura-cloud": "Hasura Cloud",
|
||||||
rundeck: "Rundeck"
|
rundeck: "Rundeck",
|
||||||
|
"azure-devops": "Azure DevOps"
|
||||||
};
|
};
|
||||||
|
|
||||||
const envMapping: Mapping = {
|
const envMapping: Mapping = {
|
||||||
|
|||||||
@@ -120,16 +120,22 @@ const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
|||||||
const fetchIntegrationAuthApps = async ({
|
const fetchIntegrationAuthApps = async ({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
teamId,
|
teamId,
|
||||||
|
azureDevOpsOrgName,
|
||||||
workspaceSlug
|
workspaceSlug
|
||||||
}: {
|
}: {
|
||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
|
azureDevOpsOrgName?: string;
|
||||||
workspaceSlug?: string;
|
workspaceSlug?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
if (teamId) {
|
if (teamId) {
|
||||||
params.teamId = teamId;
|
params.teamId = teamId;
|
||||||
}
|
}
|
||||||
|
if (azureDevOpsOrgName) {
|
||||||
|
params.azureDevOpsOrgName = azureDevOpsOrgName;
|
||||||
|
}
|
||||||
|
|
||||||
if (workspaceSlug) {
|
if (workspaceSlug) {
|
||||||
params.workspaceSlug = workspaceSlug;
|
params.workspaceSlug = workspaceSlug;
|
||||||
}
|
}
|
||||||
@@ -452,10 +458,12 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
|||||||
export const useGetIntegrationAuthApps = ({
|
export const useGetIntegrationAuthApps = ({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
teamId,
|
teamId,
|
||||||
|
azureDevOpsOrgName,
|
||||||
workspaceSlug
|
workspaceSlug
|
||||||
}: {
|
}: {
|
||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
|
azureDevOpsOrgName?: string;
|
||||||
workspaceSlug?: string;
|
workspaceSlug?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -464,6 +472,7 @@ export const useGetIntegrationAuthApps = ({
|
|||||||
fetchIntegrationAuthApps({
|
fetchIntegrationAuthApps({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
teamId,
|
teamId,
|
||||||
|
azureDevOpsOrgName,
|
||||||
workspaceSlug
|
workspaceSlug
|
||||||
}),
|
}),
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
|
||||||
|
|
||||||
|
export default function AzureDevopsCreateIntegrationPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { mutateAsync } = useSaveIntegrationAccessToken();
|
||||||
|
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
const [devopsOrgName, setDevopsOrgName] = useState("");
|
||||||
|
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
setApiKeyErrorText("");
|
||||||
|
if (apiKey.length === 0) {
|
||||||
|
setApiKeyErrorText("API Key cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
localStorage.setItem("azure-devops-org-name", devopsOrgName);
|
||||||
|
|
||||||
|
const integrationAuth = await mutateAsync({
|
||||||
|
workspaceId: localStorage.getItem("projectData.id"),
|
||||||
|
integration: "azure-devops",
|
||||||
|
accessToken: btoa(`:${apiKey}`) // This is a base64 encoding of the API key without any username
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
router.push(`/integrations/azure-devops/create?integrationAuthId=${integrationAuth.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Card className="max-w-md rounded-md p-8">
|
||||||
|
<CardTitle className="text-center">AzureDevops Integration</CardTitle>
|
||||||
|
<FormControl
|
||||||
|
label="AzureDevops API Token"
|
||||||
|
errorText={apiKeyErrorText}
|
||||||
|
isError={apiKeyErrorText !== "" ?? false}
|
||||||
|
>
|
||||||
|
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
label="Azure DevOps Organization Name"
|
||||||
|
tooltipText="This is the slug of the organization. An example would be 'my-acme-org'"
|
||||||
|
errorText={apiKeyErrorText}
|
||||||
|
isError={apiKeyErrorText !== "" ?? false}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder=""
|
||||||
|
value={devopsOrgName}
|
||||||
|
onChange={(e) => setDevopsOrgName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className="mt-4"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Connect to AzureDevOps
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AzureDevopsCreateIntegrationPage.requireAuth = true;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import queryString from "query-string";
|
||||||
|
|
||||||
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "../../../components/v2";
|
||||||
|
import {
|
||||||
|
useGetIntegrationAuthApps,
|
||||||
|
useGetIntegrationAuthById
|
||||||
|
} from "../../../hooks/api/integrationAuth";
|
||||||
|
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
||||||
|
|
||||||
|
export default function AzureDevopsCreateIntegrationPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
||||||
|
|
||||||
|
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
|
||||||
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
|
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||||
|
integrationAuthId: (integrationAuthId as string) ?? "",
|
||||||
|
azureDevOpsOrgName: localStorage.getItem("azure-devops-org-name") ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
const [targetApp, setTargetApp] = useState("");
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (workspace) {
|
||||||
|
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||||
|
}
|
||||||
|
}, [workspace]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
if (integrationAuthApps.length > 0) {
|
||||||
|
setTargetApp(integrationAuthApps[0].name);
|
||||||
|
} else {
|
||||||
|
setTargetApp("none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
integrationAuthId: integrationAuth?.id,
|
||||||
|
isActive: true,
|
||||||
|
app: localStorage.getItem("azure-devops-org-name") || "",
|
||||||
|
appId: targetApp,
|
||||||
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
|
secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return integrationAuth &&
|
||||||
|
workspace &&
|
||||||
|
selectedSourceEnvironment &&
|
||||||
|
integrationAuthApps &&
|
||||||
|
targetApp ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Card className="max-w-md rounded-md p-8">
|
||||||
|
<CardTitle className="text-center">AzureDevops 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}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Secrets Path">
|
||||||
|
<Input
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="AzureDevops Project" className="mt-4">
|
||||||
|
<Select
|
||||||
|
value={targetApp}
|
||||||
|
onValueChange={(val) => setTargetApp(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
{integrationAuthApps.length > 0 ? (
|
||||||
|
integrationAuthApps.map((integrationAuthApp) => (
|
||||||
|
<SelectItem
|
||||||
|
value={integrationAuthApp.name}
|
||||||
|
key={`target-environment-${integrationAuthApp.name}`}
|
||||||
|
>
|
||||||
|
{integrationAuthApp.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No projects found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
className="mt-4"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={integrationAuthApps.length === 0}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AzureDevopsCreateIntegrationPage.requireAuth = true;
|
||||||
@@ -131,6 +131,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
|
|||||||
case "rundeck":
|
case "rundeck":
|
||||||
link = `${window.location.origin}/integrations/rundeck/authorize`;
|
link = `${window.location.origin}/integrations/rundeck/authorize`;
|
||||||
break;
|
break;
|
||||||
|
case "azure-devops":
|
||||||
|
link = `${window.location.origin}/integrations/azure-devops/authorize`;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user