mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-15 09:42:14 +00:00
Compare commits
4 Commits
infisical/
...
daniel/ref
Author | SHA1 | Date | |
---|---|---|---|
da561e37c5 | |||
a29fb613b9 | |||
8f3d328b9a | |||
b7d683ee1b |
@ -42,7 +42,7 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
|
|||||||
})
|
})
|
||||||
.orWhere((qb) => {
|
.orWhere((qb) => {
|
||||||
void qb
|
void qb
|
||||||
.where("clientSecretNumUsesLimit", ">", 0)
|
.where("clientSecretNumUses", ">", 0)
|
||||||
.andWhere(
|
.andWhere(
|
||||||
"clientSecretNumUses",
|
"clientSecretNumUses",
|
||||||
">=",
|
">=",
|
||||||
|
@ -460,16 +460,21 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
*/
|
*/
|
||||||
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
|
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
|
||||||
const res = (
|
const res = (
|
||||||
await request.get<{ reponame: string }[]>(`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`, {
|
await request.get<{ reponame: string; username: string; vcs_url: string }[]>(
|
||||||
headers: {
|
`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`,
|
||||||
"Circle-Token": accessToken,
|
{
|
||||||
"Accept-Encoding": "application/json"
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
const apps = res?.map((a) => ({
|
const apps = res.map((a) => ({
|
||||||
name: a?.reponame
|
owner: a.username, // username maps to unique organization name in CircleCI
|
||||||
|
name: a.reponame, // reponame maps to project name within an organization in CircleCI
|
||||||
|
appId: a.vcs_url.split("/").pop() // vcs_url maps to the project id in CircleCI
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
|
@ -1929,22 +1929,62 @@ const syncSecretsCircleCI = async ({
|
|||||||
secrets: Record<string, { value: string; comment?: string }>;
|
secrets: Record<string, { value: string; comment?: string }>;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
const circleciOrganizationDetail = (
|
const getProjectSlug = async () => {
|
||||||
await request.get(`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`, {
|
const requestConfig = {
|
||||||
headers: {
|
headers: {
|
||||||
"Circle-Token": accessToken,
|
"Circle-Token": accessToken,
|
||||||
"Accept-Encoding": "application/json"
|
"Accept-Encoding": "application/json"
|
||||||
}
|
}
|
||||||
})
|
};
|
||||||
).data[0];
|
|
||||||
|
|
||||||
const { slug } = circleciOrganizationDetail;
|
try {
|
||||||
|
const projectDetails = (
|
||||||
|
await request.get<{ slug: string }>(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`,
|
||||||
|
requestConfig
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
return projectDetails.slug;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AxiosError) {
|
||||||
|
if (err.response?.data?.message !== "Not Found") {
|
||||||
|
throw new Error("Failed to get project slug from CircleCI during first attempt.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization
|
||||||
|
try {
|
||||||
|
const circleCiOrganization = (
|
||||||
|
await request.get<{ slug: string; name: string }[]>(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
|
||||||
|
requestConfig
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
// Case 1: This is a new integration where the organization name is stored under `integration.owner`
|
||||||
|
if (integration.owner) {
|
||||||
|
const org = circleCiOrganization.find((o) => o.name === integration.owner);
|
||||||
|
if (org) {
|
||||||
|
return `${org.slug}/${integration.app}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one
|
||||||
|
return `${circleCiOrganization[0].slug}/${integration.app}`;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error("Failed to get project slug from CircleCI during second attempt.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectSlug = await getProjectSlug();
|
||||||
|
|
||||||
// sync secrets to CircleCI
|
// sync secrets to CircleCI
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.keys(secrets).map(async (key) =>
|
Object.keys(secrets).map(async (key) =>
|
||||||
request.post(
|
request.post(
|
||||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
|
||||||
{
|
{
|
||||||
name: key,
|
name: key,
|
||||||
value: secrets[key].value
|
value: secrets[key].value
|
||||||
@ -1962,7 +2002,7 @@ const syncSecretsCircleCI = async ({
|
|||||||
// get secrets from CircleCI
|
// get secrets from CircleCI
|
||||||
const getSecretsRes = (
|
const getSecretsRes = (
|
||||||
await request.get<{ items: { name: string }[] }>(
|
await request.get<{ items: { name: string }[] }>(
|
||||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Circle-Token": accessToken,
|
"Circle-Token": accessToken,
|
||||||
@ -1976,15 +2016,12 @@ const syncSecretsCircleCI = async ({
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
getSecretsRes.map(async (sec) => {
|
getSecretsRes.map(async (sec) => {
|
||||||
if (!(sec.name in secrets)) {
|
if (!(sec.name in secrets)) {
|
||||||
return request.delete(
|
return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, {
|
||||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`,
|
headers: {
|
||||||
{
|
"Circle-Token": accessToken,
|
||||||
headers: {
|
"Content-Type": "application/json"
|
||||||
"Circle-Token": accessToken,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: "Elasticsearch"
|
title: "Elastic Search"
|
||||||
description: "Learn how to dynamically generate Elasticsearch user credentials."
|
description: "Learn how to dynamically generate Elastic Search user credentials."
|
||||||
---
|
---
|
||||||
|
|
||||||
The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch credentials on demand based on configured role.
|
The Infisical Elastic Search dynamic secret allows you to generate Elastic Search credentials on demand based on configured role.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
|
|||||||
For testing purposes, you can also use a highly privileged role like `superuser`, that will have full control over the cluster. This is not recommended in production environments following the principle of least privilege.
|
For testing purposes, you can also use a highly privileged role like `superuser`, that will have full control over the cluster. This is not recommended in production environments following the principle of least privilege.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
## Set up Dynamic Secrets with Elasticsearch
|
## Set up Dynamic Secrets with Elastic Search
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Open Secret Overview Dashboard">
|
<Step title="Open Secret Overview Dashboard">
|
||||||
@ -25,7 +25,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
|
|||||||
<Step title="Click on the 'Add Dynamic Secret' button">
|
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Select 'Elasticsearch'">
|
<Step title="Select 'Elastic Search'">
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Provide the inputs for dynamic secret parameters">
|
<Step title="Provide the inputs for dynamic secret parameters">
|
||||||
@ -42,11 +42,11 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Host" type="string" required>
|
<ParamField path="Host" type="string" required>
|
||||||
Your Elasticsearch host. This is the endpoint that your instance runs on. _(Example: https://your-cluster-ip)_
|
Your Elastic Search host. This is the endpoint that your instance runs on. _(Example: https://your-cluster-ip)_
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Port" type="string" required>
|
<ParamField path="Port" type="string" required>
|
||||||
The port that your Elasticsearch instance is running on. _(Example: 9200)_
|
The port that your Elastic Search instance is running on. _(Example: 9200)_
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Roles" type="string[]" required>
|
<ParamField path="Roles" type="string[]" required>
|
||||||
@ -54,7 +54,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
|
|||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Authentication Method" type="API Key | Username/Password" required>
|
<ParamField path="Authentication Method" type="API Key | Username/Password" required>
|
||||||
Select the authentication method you want to use to connect to your Elasticsearch instance.
|
Select the authentication method you want to use to connect to your Elastic Search instance.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Username" type="string" required>
|
<ParamField path="Username" type="string" required>
|
||||||
@ -124,4 +124,4 @@ To extend the life of the generated dynamic secret leases past its initial time
|
|||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
|
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
|
||||||
</Warning>
|
</Warning>
|
@ -69,7 +69,7 @@ export default function AWSParameterStoreAuthorizeIntegrationPage() {
|
|||||||
subTitle="After adding the details below, you will be prompted to set up an integration for a particular Infisical project and environment."
|
subTitle="After adding the details below, you will be prompted to set up an integration for a particular Infisical project and environment."
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<div className="inline flex items-center">
|
<div className="flex items-center">
|
||||||
<Image
|
<Image
|
||||||
src="/images/integrations/Amazon Web Services.png"
|
src="/images/integrations/Amazon Web Services.png"
|
||||||
height={35}
|
height={35}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@ -12,6 +12,7 @@ import {
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { useCreateIntegration } from "@app/hooks/api";
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -45,9 +46,10 @@ export default function CircleCICreateIntegrationPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||||
|
const [targetOrganization, setTargetOrganization] = useState("");
|
||||||
const [secretPath, setSecretPath] = useState("/");
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
|
||||||
const [targetApp, setTargetApp] = useState("");
|
const [targetProjectId, setTargetProjectId] = useState("");
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
@ -57,29 +59,40 @@ export default function CircleCICreateIntegrationPage() {
|
|||||||
}
|
}
|
||||||
}, [workspace]);
|
}, [workspace]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (integrationAuthApps) {
|
|
||||||
if (integrationAuthApps.length > 0) {
|
|
||||||
setTargetApp(integrationAuthApps[0]?.name);
|
|
||||||
} else {
|
|
||||||
setTargetApp("none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [integrationAuthApps]);
|
|
||||||
|
|
||||||
const handleButtonClick = async () => {
|
const handleButtonClick = async () => {
|
||||||
try {
|
try {
|
||||||
if (!integrationAuth?.id) return;
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
if (!targetProjectId || targetOrganization === "none") {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Please select a project"
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const selectedApp = integrationAuthApps?.find(
|
||||||
|
(integrationAuthApp) => integrationAuthApp.appId === targetProjectId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!selectedApp) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Invalid project selected"
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
integrationAuthId: integrationAuth?.id,
|
integrationAuthId: integrationAuth?.id,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
app: targetApp,
|
app: selectedApp.name, // project name
|
||||||
appId: integrationAuthApps?.find(
|
owner: selectedApp.owner, // organization name
|
||||||
(integrationAuthApp) => integrationAuthApp.name === targetApp
|
appId: selectedApp.appId, // project id (used for syncing)
|
||||||
)?.appId,
|
|
||||||
sourceEnvironment: selectedSourceEnvironment,
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
secretPath
|
secretPath
|
||||||
});
|
});
|
||||||
@ -92,11 +105,28 @@ export default function CircleCICreateIntegrationPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return integrationAuth &&
|
const filteredProjects = useMemo(() => {
|
||||||
workspace &&
|
if (!integrationAuthApps) return [];
|
||||||
selectedSourceEnvironment &&
|
|
||||||
integrationAuthApps &&
|
return integrationAuthApps.filter((integrationAuthApp) => {
|
||||||
targetApp ? (
|
return integrationAuthApp.owner === targetOrganization;
|
||||||
|
});
|
||||||
|
}, [integrationAuthApps, targetOrganization]);
|
||||||
|
|
||||||
|
const filteredOrganizations = useMemo(() => {
|
||||||
|
const organizations = new Set<string>();
|
||||||
|
|
||||||
|
if (integrationAuthApps) {
|
||||||
|
integrationAuthApps.forEach((integrationAuthApp) => {
|
||||||
|
if (!integrationAuthApp.owner) return;
|
||||||
|
organizations.add(integrationAuthApp.owner);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(organizations);
|
||||||
|
}, [integrationAuthApps]);
|
||||||
|
|
||||||
|
return integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps ? (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
<Head>
|
<Head>
|
||||||
<title>Set Up CircleCI Integration</title>
|
<title>Set Up CircleCI Integration</title>
|
||||||
@ -108,7 +138,7 @@ export default function CircleCICreateIntegrationPage() {
|
|||||||
subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI environment variables."
|
subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI environment variables."
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<div className="inline flex items-center pb-0.5">
|
<div className="flex items-center pb-0.5">
|
||||||
<Image
|
<Image
|
||||||
src="/images/integrations/Circle CI.png"
|
src="/images/integrations/Circle CI.png"
|
||||||
height={30}
|
height={30}
|
||||||
@ -131,6 +161,7 @@ export default function CircleCICreateIntegrationPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
||||||
<FormControl label="Project Environment" className="px-6">
|
<FormControl label="Project Environment" className="px-6">
|
||||||
<Select
|
<Select
|
||||||
value={selectedSourceEnvironment}
|
value={selectedSourceEnvironment}
|
||||||
@ -154,29 +185,55 @@ export default function CircleCICreateIntegrationPage() {
|
|||||||
placeholder="Provide a path, default is /"
|
placeholder="Provide a path, default is /"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl label="CircleCI Project" className="px-6">
|
|
||||||
|
<FormControl label="CircleCI Organization" className="px-6">
|
||||||
<Select
|
<Select
|
||||||
value={targetApp}
|
value={targetOrganization}
|
||||||
onValueChange={(val) => setTargetApp(val)}
|
onValueChange={(val) => {
|
||||||
|
setTargetOrganization(val);
|
||||||
|
setTargetProjectId("none");
|
||||||
|
}}
|
||||||
className="w-full border border-mineshaft-500"
|
className="w-full border border-mineshaft-500"
|
||||||
isDisabled={integrationAuthApps.length === 0}
|
isDisabled={filteredOrganizations.length === 0}
|
||||||
>
|
>
|
||||||
{integrationAuthApps.length > 0 ? (
|
{filteredOrganizations.length > 0 ? (
|
||||||
integrationAuthApps.map((integrationAuthApp) => (
|
filteredOrganizations.map((org) => (
|
||||||
<SelectItem
|
<SelectItem value={org} key={`target-org-${org}`}>
|
||||||
value={integrationAuthApp.name}
|
{org}
|
||||||
key={`target-app-${integrationAuthApp.name}`}
|
|
||||||
>
|
|
||||||
{integrationAuthApp.name}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<SelectItem value="none" key="target-app-none">
|
<SelectItem value="none" key="target-app-none">
|
||||||
No projects found
|
No organizations found
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
{targetOrganization && (
|
||||||
|
<FormControl label="CircleCI Project ID" className="px-6">
|
||||||
|
<Select
|
||||||
|
value={targetProjectId}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setTargetProjectId(val);
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
isDisabled={filteredProjects.length === 0}
|
||||||
|
>
|
||||||
|
{filteredProjects.length > 0 ? (
|
||||||
|
filteredProjects.map((project) => (
|
||||||
|
<SelectItem value={project.appId!} key={`target-project-${project.owner}`}>
|
||||||
|
{project.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No projects found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleButtonClick}
|
onClick={handleButtonClick}
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
|
@ -99,6 +99,7 @@ export const ConfiguredIntegrationItem = ({
|
|||||||
<FormLabel
|
<FormLabel
|
||||||
label={
|
label={
|
||||||
(integration.integration === "qovery" && integration?.scope) ||
|
(integration.integration === "qovery" && integration?.scope) ||
|
||||||
|
(integration.integration === "circleci" && "Project") ||
|
||||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
(integration.integration === "aws-secret-manager" && "Secret") ||
|
||||||
(["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") ||
|
(["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") ||
|
||||||
(integration?.integration === "terraform-cloud" && "Project") ||
|
(integration?.integration === "terraform-cloud" && "Project") ||
|
||||||
@ -142,6 +143,14 @@ export const ConfiguredIntegrationItem = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{integration.integration === "circleci" && integration.owner && (
|
||||||
|
<div className="ml-2">
|
||||||
|
<FormLabel label="Organization" />
|
||||||
|
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||||
|
{integration.owner}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{integration.integration === "terraform-cloud" && integration.targetService && (
|
{integration.integration === "terraform-cloud" && integration.targetService && (
|
||||||
<div className="ml-2">
|
<div className="ml-2">
|
||||||
<FormLabel label="Category" />
|
<FormLabel label="Category" />
|
||||||
|
Reference in New Issue
Block a user