Merge pull request #909 from Infisical/team-city-branch-config

Add support for build-configuration environment variable sync for TeamCity integration
This commit is contained in:
BlackMagiq
2023-08-29 14:43:17 +01:00
committed by GitHub
23 changed files with 314 additions and 90 deletions
@@ -547,6 +547,57 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res
}); });
} }
/**
* Return list of build configs for TeamCity project with id [appId]
* @param req
* @param res
* @returns
*/
export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: Response) => {
const appId = req.query.appId as string;
interface TeamCityBuildConfig {
id: string;
name: string;
projectName: string;
projectId: string;
href: string;
webUrl: string;
}
interface GetTeamCityBuildConfigsRes {
count: number;
href: string;
buildType: TeamCityBuildConfig[];
}
if (appId && appId !== "") {
const { data: { buildType } } = (
await standardRequest.get<GetTeamCityBuildConfigsRes>(`${req.integrationAuth.url}/app/rest/buildTypes`, {
params: {
locator: `project:${appId}`
},
headers: {
Authorization: `Bearer ${req.accessToken}`,
Accept: "application/json",
},
})
);
return res.status(200).send({
buildConfigs: buildType.map((buildConfig) => ({
name: buildConfig.name,
buildConfigId: buildConfig.id
}))
});
}
return res.status(200).send({
buildConfigs: []
});
}
/** /**
* Delete integration authorization with id [integrationAuthId] * Delete integration authorization with id [integrationAuthId]
* @param req * @param req
@@ -236,7 +236,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
version: 1 version: 1
}, },
$unset: { $unset: {
'metadata.source': true as true "metadata.source": true as const
}, },
...u, ...u,
_id: new Types.ObjectId(u._id) _id: new Types.ObjectId(u._id)
-1
View File
@@ -9,7 +9,6 @@ import {
INTEGRATION_VERCEL INTEGRATION_VERCEL
} from "../variables"; } from "../variables";
import { UnauthorizedRequestError } from "../utils/errors"; import { UnauthorizedRequestError } from "../utils/errors";
import { syncSecretsToActiveIntegrationsQueue } from "../queues/integrations/syncSecretsToThirdPartyServices"
interface Update { interface Update {
workspace: string; workspace: string;
+1 -1
View File
@@ -850,7 +850,7 @@ const getAppsTeamCity = async ({
}, },
}) })
).data.project.slice(1); ).data.project.slice(1);
const apps = res.map((a: any) => { const apps = res.map((a: any) => {
return { return {
name: a.name, name: a.name,
+5 -5
View File
@@ -4,6 +4,8 @@ import {
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_BITBUCKET, INTEGRATION_BITBUCKET,
INTEGRATION_BITBUCKET_TOKEN_URL, INTEGRATION_BITBUCKET_TOKEN_URL,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GCP_TOKEN_URL,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITHUB_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
@@ -13,21 +15,19 @@ import {
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GCP_TOKEN_URL
} from "../variables"; } from "../variables";
import { import {
getClientIdGCPSecretManager,
getClientSecretGCPSecretManager,
getClientIdAzure, getClientIdAzure,
getClientIdBitBucket, getClientIdBitBucket,
getClientIdGCPSecretManager,
getClientIdGitHub, getClientIdGitHub,
getClientIdGitLab, getClientIdGitLab,
getClientIdNetlify, getClientIdNetlify,
getClientIdVercel, getClientIdVercel,
getClientSecretAzure, getClientSecretAzure,
getClientSecretBitBucket, getClientSecretBitBucket,
getClientSecretGCPSecretManager,
getClientSecretGitHub, getClientSecretGitHub,
getClientSecretGitLab, getClientSecretGitLab,
getClientSecretHeroku, getClientSecretHeroku,
+101 -34
View File
@@ -2185,7 +2185,7 @@ const syncSecretsTerraformCloud = async ({
}; };
/** /**
* Sync/push [secrets] to TeamCity project * Sync/push [secrets] to TeamCity project (and optionally build config)
* @param {Object} obj * @param {Object} obj
* @param {IIntegration} obj.integration - integration details * @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration * @param {Object} obj.secrets - secrets to push to integration
@@ -2207,57 +2207,124 @@ const syncSecretsTeamCity = async ({
value: string; value: string;
} }
// get secrets from Teamcity interface TeamCityBuildConfigParameter {
const res = ( name: string;
await standardRequest.get( value: string;
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, inherited: boolean;
}
interface GetTeamCityBuildConfigParametersRes {
href: string;
count: number;
property: TeamCityBuildConfigParameter[];
}
if (integration.targetEnvironment && integration.targetEnvironmentId) {
// case: sync to specific build-config in TeamCity project
const res = (await standardRequest.get<GetTeamCityBuildConfigParametersRes>(
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
Accept: "application/json" Accept: "application/json",
} },
} }
) ))
).data.property.reduce((obj: any, secret: TeamCitySecret) => { .data
const secretName = secret.name.replace(/^env\./, ""); .property
return { .filter((parameter) => !parameter.inherited)
...obj, .reduce((obj: any, secret: TeamCitySecret) => {
[secretName]: secret.value const secretName = secret.name.replace(/^env\./, "");
}; return {
}, {}); ...obj,
[secretName]: secret.value
for await (const key of Object.keys(secrets)) { };
if (!(key in res) || (key in res && secrets[key] !== res[key])) { }, {});
// case: secret does not exist in TeamCity or secret value has changed
// -> create/update secret for await (const key of Object.keys(secrets)) {
await standardRequest.post( if (!(key in res) || (key in res && secrets[key].value !== res[key])) {
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, // case: secret does not exist in TeamCity or secret value has changed
// -> create/update secret
await standardRequest.post(`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
{ {
name: `env.${key}`, name:`env.${key}`,
value: secrets[key].value value: secrets[key].value
}, },
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
Accept: "application/json" Accept: "application/json",
} },
} });
); }
} }
}
for await (const key of Object.keys(res)) { for await (const key of Object.keys(res)) {
if (!(key in secrets)) { if (!(key in secrets)) {
// delete secret // delete secret
await standardRequest.delete( await standardRequest.delete(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters/env.${key}`, `${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters/env.${key}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
}
} else {
// case: sync to TeamCity project
const res = (
await standardRequest.get(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`,
{ {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
Accept: "application/json" Accept: "application/json"
} }
} }
); )
).data.property.reduce((obj: any, secret: TeamCitySecret) => {
const secretName = secret.name.replace(/^env\./, "");
return {
...obj,
[secretName]: secret.value
};
}, {});
for await (const key of Object.keys(secrets)) {
if (!(key in res) || (key in res && secrets[key] !== res[key])) {
// case: secret does not exist in TeamCity or secret value has changed
// -> create/update secret
await standardRequest.post(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`,
{
name: `env.${key}`,
value: secrets[key].value
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
}
for await (const key of Object.keys(res)) {
if (!(key in secrets)) {
// delete secret
await standardRequest.delete(
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters/env.${key}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
} }
} }
}; };
+2 -2
View File
@@ -10,6 +10,7 @@ import {
INTEGRATION_CODEFRESH, INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
@@ -24,8 +25,7 @@ import {
INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_WINDMILL, INTEGRATION_WINDMILL
INTEGRATION_GCP_SECRET_MANAGER
} from "../variables"; } from "../variables";
import { Schema, Types, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
+2 -2
View File
@@ -12,6 +12,7 @@ import {
INTEGRATION_CODEFRESH, INTEGRATION_CODEFRESH,
INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_GCP_SECRET_MANAGER,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
INTEGRATION_HASHICORP_VAULT, INTEGRATION_HASHICORP_VAULT,
@@ -26,8 +27,7 @@ import {
INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TERRAFORM_CLOUD,
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_WINDMILL, INTEGRATION_WINDMILL
INTEGRATION_GCP_SECRET_MANAGER
} from "../variables"; } from "../variables";
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
@@ -1,16 +1,16 @@
import Queue, { Job } from "bull"; import Queue, { Job } from "bull";
import { ProbotOctokit } from "probot" import { ProbotOctokit } from "probot"
import { Commit, Committer, Repository } from "@octokit/webhooks-types"; import { Commit } from "@octokit/webhooks-types";
import TelemetryService from "../../services/TelemetryService"; import TelemetryService from "../../services/TelemetryService";
import { sendMail } from "../../helpers"; import { sendMail } from "../../helpers";
import GitRisks from "../../ee/models/gitRisks"; import GitRisks from "../../ee/models/gitRisks";
import { MembershipOrg, User } from "../../models"; import { MembershipOrg, User } from "../../models";
import { OWNER, ADMIN } from "../../variables"; import { ADMIN, OWNER } from "../../variables";
import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper"; import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper";
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
import { SecretMatch } from "../../ee/services/GithubSecretScanning/types"; import { SecretMatch } from "../../ee/services/GithubSecretScanning/types";
export const githubPushEventSecretScan = new Queue('github-push-event-secret-scanning', 'redis://redis:6379'); export const githubPushEventSecretScan = new Queue("github-push-event-secret-scanning", "redis://redis:6379");
type TScanPushEventQueueDetails = { type TScanPushEventQueueDetails = {
organizationId: string, organizationId: string,
+1 -1
View File
@@ -89,4 +89,4 @@ router.post(
integrationController.manualSync integrationController.manualSync
); );
export default router; export default router;
+14
View File
@@ -168,6 +168,20 @@ router.get(
integrationAuthController.getIntegrationAuthNorthflankSecretGroups integrationAuthController.getIntegrationAuthNorthflankSecretGroups
); );
router.get(
"/:integrationAuthId/teamcity/build-configs",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param("integrationAuthId").exists().isString(),
query("appId").exists().isString(),
validateRequest,
integrationAuthController.getIntegrationAuthTeamCityBuildConfigs
);
router.delete( router.delete(
"/:integrationAuthId", "/:integrationAuthId",
requireAuth({ requireAuth({
+1 -1
View File
@@ -1,4 +1,4 @@
import express, { Request, Response } from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { body, param, query } from "express-validator"; import { body, param, query } from "express-validator";
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

+23 -15
View File
@@ -11,21 +11,18 @@ Prerequisites:
![integrations](../../images/integrations.png) ![integrations](../../images/integrations.png)
## Enter your TeamCity API Token and Server URL ## Enter your TeamCity Access Token and Server URL
Obtain a TeamCity API Token in Profile > Access Tokens Obtain a TeamCity Access Token in Profile > Access Tokens
![integrations teamcity dashboard](../../images/integrations-teamcity-dashboard.png) ![integrations teamcity dashboard](../../images/integrations/teamcity/integrations-teamcity-dashboard.png)
![integrations teamcity tokens](../../images/integrations-teamcity-tokens.png) ![integrations teamcity token](../../images/integrations/teamcity/integrations-teamcity-token.png)
Obtain your TeamCity Server URL in Administration > Cloud Server Settings > Server URL <Note>
For this integration to work, the TeamCity Access Token must either have the
![integrations teamcity projects](../../images/integrations-teamcity-projects.png) **Same as current user** account-wide permission enabled or, if **Limit per project**
![integrations teamcity server url](../../images/integrations-teamcity-serverurl.png) is selected, then it must at minimum have the **View build configuration settings** and **Edit project** permissions enabled.
</Note>
Press on the TeamCity tile and input your TeamCity API Token and Server URL to grant Infisical access to your TeamCity account.
![integrations teamcity authorization](../../images/integrations-teamcity-auth.png)
<Info> <Info>
If this is your project's first cloud integration, then you'll have to grant If this is your project's first cloud integration, then you'll have to grant
@@ -34,9 +31,20 @@ Press on the TeamCity tile and input your TeamCity API Token and Server URL to g
the cloud platform. the cloud platform.
</Info> </Info>
Press on the TeamCity tile and input your TeamCity Access Token and Server URL to grant Infisical access to your TeamCity account.
![integrations teamcity authorization](../../images/integrations/teamcity/integrations-teamcity-auth.png)
## Start integration ## Start integration
Select which Infisical environment secrets, you want to sync to which TeamCity project and press create integration to start syncing secrets to TeamCity. Select which Infisical environment secrets you want to sync to which TeamCity project (and optionally build configuration) and press create integration to start syncing secrets to TeamCity.
![integrations teamcity](../../images/integrations-teamcity-create.png) ![integrations teamcity](../../images/integrations/teamcity/integrations-teamcity-create.png)
![integrations teamcity](../../images/integrations-teamcity.png)
<Note>
Infisical integrates with both TeamCity's project-level and build configuration-level environment variables.
To sync secrets to a specific build configuration in a TeamCity project, you can select a build configuration from the **TeamCity Build Config** dropdown; otherwise, leaving it empty will sync secrets to TeamCity at the project-level.
</Note>
![integrations teamcity](../../images/integrations/teamcity/integrations-teamcity.png)
@@ -7,6 +7,7 @@ export {
useGetIntegrationAuthNorthflankSecretGroups, useGetIntegrationAuthNorthflankSecretGroups,
useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices, useGetIntegrationAuthRailwayServices,
useGetIntegrationAuthTeamCityBuildConfigs,
useGetIntegrationAuthTeams, useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches, useGetIntegrationAuthVercelBranches,
useSaveIntegrationAccessToken useSaveIntegrationAccessToken
@@ -10,8 +10,8 @@ import {
IntegrationAuth, IntegrationAuth,
NorthflankSecretGroup, NorthflankSecretGroup,
Service, Service,
Team Team,
} from "./types"; TeamCityBuildConfig} from "./types";
const integrationAuthKeys = { const integrationAuthKeys = {
getIntegrationAuthById: (integrationAuthId: string) => getIntegrationAuthById: (integrationAuthId: string) =>
@@ -49,7 +49,14 @@ const integrationAuthKeys = {
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthNorthflankSecretGroups"] as const, }) => [{ integrationAuthId, appId }, "integrationAuthNorthflankSecretGroups"] as const,
getIntegrationAuthTeamCityBuildConfigs: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthTeamCityBranchConfigs"] as const,
}; };
const fetchIntegrationAuthById = async (integrationAuthId: string) => { const fetchIntegrationAuthById = async (integrationAuthId: string) => {
@@ -183,6 +190,27 @@ const fetchIntegrationAuthNorthflankSecretGroups = async ({
return secretGroups; return secretGroups;
}; };
const fetchIntegrationAuthTeamCityBuildConfigs = async ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
const {
data: { buildConfigs }
} = await apiRequest.get<{ buildConfigs: TeamCityBuildConfig[] }>(
`/api/v1/integration-auth/${integrationAuthId}/teamcity/build-configs`,
{
params: {
appId
}
}
);
return buildConfigs;
};
export const useGetIntegrationAuthById = (integrationAuthId: string) => { export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId), queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
@@ -312,6 +340,26 @@ export const useGetIntegrationAuthNorthflankSecretGroups = ({
}); });
}; };
export const useGetIntegrationAuthTeamCityBuildConfigs = ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthTeamCityBuildConfigs({
integrationAuthId,
appId
}),
queryFn: () => fetchIntegrationAuthTeamCityBuildConfigs({
integrationAuthId,
appId
}),
enabled: true
});
};
export const useAuthorizeIntegration = () => { export const useAuthorizeIntegration = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -40,4 +40,9 @@ export type BitBucketWorkspace = {
export type NorthflankSecretGroup = { export type NorthflankSecretGroup = {
name: string; name: string;
groupId: string; groupId: string;
}
export type TeamCityBuildConfig = {
name: string;
buildConfigId: string;
} }
@@ -17,13 +17,20 @@ import {
} from "../../../components/v2"; } from "../../../components/v2";
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById,
useGetIntegrationAuthTeamCityBuildConfigs
} from "../../../hooks/api/integrationAuth"; } from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace"; import { useGetWorkspaceById } from "../../../hooks/api/workspace";
export default function TeamCityCreateIntegrationPage() { export default function TeamCityCreateIntegrationPage() {
const router = useRouter(); const router = useRouter();
const { mutateAsync } = useCreateIntegration(); const { mutateAsync } = useCreateIntegration();
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetAppId, setTargetAppId] = useState("");
const [targetBuildConfigId, setTargetBuildConfigId] = useState<string>("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
@@ -32,11 +39,11 @@ export default function TeamCityCreateIntegrationPage() {
const { data: integrationAuthApps } = useGetIntegrationAuthApps({ const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "" integrationAuthId: (integrationAuthId as string) ?? ""
}); });
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const { data: targetBuildConfigs } = useGetIntegrationAuthTeamCityBuildConfigs({
const [targetApp, setTargetApp] = useState(""); integrationAuthId: (integrationAuthId as string) ?? "",
const [secretPath, setSecretPath] = useState("/"); appId: targetAppId
const [isLoading, setIsLoading] = useState(false); });
useEffect(() => { useEffect(() => {
if (workspace) { if (workspace) {
@@ -47,29 +54,31 @@ export default function TeamCityCreateIntegrationPage() {
useEffect(() => { useEffect(() => {
if (integrationAuthApps) { if (integrationAuthApps) {
if (integrationAuthApps.length > 0) { if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name); setTargetAppId(integrationAuthApps[0].appId as string);
} else { } else {
setTargetApp("none"); setTargetAppId("none");
} }
} }
}, [integrationAuthApps]); }, [integrationAuthApps]);
const handleButtonClick = async () => { const handleButtonClick = async () => {
try { try {
if (!integrationAuth?._id) return; if (!integrationAuth?._id) return;
setIsLoading(true); setIsLoading(true);
const targetEnvironment = targetBuildConfigs?.find(
(buildConfig) => buildConfig.buildConfigId === targetBuildConfigId
);
await mutateAsync({ await mutateAsync({
integrationAuthId: integrationAuth?._id, integrationAuthId: integrationAuth?._id,
isActive: true, isActive: true,
app: targetApp, app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name ?? null,
appId: appId: targetAppId,
integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null, targetEnvironment: targetEnvironment ? targetEnvironment.name : null,
targetEnvironmentId: null, targetEnvironmentId: targetEnvironment ? targetEnvironment.buildConfigId : null,
targetService: null, targetService: null,
targetServiceId: null, targetServiceId: null,
owner: null, owner: null,
@@ -86,12 +95,17 @@ export default function TeamCityCreateIntegrationPage() {
} }
}; };
const filteredBuildConfigs = targetBuildConfigs?.concat({
name: "",
buildConfigId: ""
});
return integrationAuth && return integrationAuth &&
workspace && workspace &&
selectedSourceEnvironment && selectedSourceEnvironment &&
integrationAuthApps && integrationAuthApps &&
targetApp ? ( filteredBuildConfigs &&
targetAppId ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8"> <Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">TeamCity Integration</CardTitle> <CardTitle className="text-center">TeamCity Integration</CardTitle>
@@ -120,16 +134,16 @@ export default function TeamCityCreateIntegrationPage() {
</FormControl> </FormControl>
<FormControl label="TeamCity Project" className="mt-4"> <FormControl label="TeamCity Project" className="mt-4">
<Select <Select
value={targetApp} value={targetAppId}
onValueChange={(val) => setTargetApp(val)} onValueChange={(val) => setTargetAppId(val)}
className="w-full border border-mineshaft-500" className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0} isDisabled={integrationAuthApps.length === 0}
> >
{integrationAuthApps.length > 0 ? ( {integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => ( integrationAuthApps.map((integrationAuthApp) => (
<SelectItem <SelectItem
value={integrationAuthApp.name} value={integrationAuthApp.appId as string}
key={`target-app-${integrationAuthApp.name}`} key={`target-app-${integrationAuthApp.appId as string}`}
> >
{integrationAuthApp.name} {integrationAuthApp.name}
</SelectItem> </SelectItem>
@@ -141,6 +155,22 @@ export default function TeamCityCreateIntegrationPage() {
)} )}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Team City Build Config (Optional)" className="mt-4">
<Select
value={targetBuildConfigId}
onValueChange={(val) => setTargetBuildConfigId(val)}
className="w-full border border-mineshaft-500"
>
{filteredBuildConfigs.map((buildConfig: any) => (
<SelectItem
value={buildConfig.buildConfigId}
key={`target-build-config-${buildConfig.buildConfigId}`}
>
{buildConfig.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" color="mineshaft"
@@ -106,6 +106,7 @@ export const IntegrationsSection = ({
integration.integration === "netlify" || integration.integration === "netlify" ||
integration.integration === "railway" || integration.integration === "railway" ||
integration.integration === "gitlab" || integration.integration === "gitlab" ||
integration.integration === "teamcity" ||
integration.integration === "bitbucket") && ( integration.integration === "bitbucket") && (
<div className="ml-4 flex flex-col"> <div className="ml-4 flex flex-col">
<FormLabel label="Target Environment" /> <FormLabel label="Target Environment" />