mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 00:29:34 +00:00
added basic heroku pipeline integration
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type RadioGroupProps = RadioGroupPrimitive.RadioGroupProps;
|
||||
|
||||
// Note this component is not customizable (Heroku integration and potentially other pages depend on it)
|
||||
export const RadioGroup = ({ className, children, ...props }: RadioGroupProps) => (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={twMerge("flex flex-row gap-5 px-6 mb-6", className)}
|
||||
defaultValue="App"
|
||||
aria-label="View density"
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<RadioGroupPrimitive.Item
|
||||
className="bg-bunker-400/20 w-[20px] h-[20px] rounded-full hover:bg-bunker-400/40 border border-bunker-400/60 duration-200 outline-none cursor-default"
|
||||
value="App"
|
||||
id="r1"
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center w-full h-full relative after:content-[''] after:block after:w-[11px] after:h-[11px] after:rounded-[50%] after:bg-primary" />
|
||||
</RadioGroupPrimitive.Item>
|
||||
<label className="text-bunker-200 text-sm leading-none pl-2" htmlFor="r1">
|
||||
App
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<RadioGroupPrimitive.Item
|
||||
className="bg-bunker-400/20 w-[22px] h-[22px] rounded-full hover:bg-bunker-400/40 border border-bunker-400/60 duration-200 outline-none cursor-default"
|
||||
value="Pipeline"
|
||||
id="r2"
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center w-full h-full relative after:content-[''] after:block after:w-[13px] after:h-[13px] after:rounded-[50%] after:bg-primary" />
|
||||
</RadioGroupPrimitive.Item>
|
||||
<label className="text-bunker-200 text-sm leading-none pl-2" htmlFor="r2">
|
||||
Pipeline
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroupPrimitive.Root>
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
export type { RadioGroupProps } from "./RadioGroup";
|
||||
export { RadioGroup } from "./RadioGroup";
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
BitBucketWorkspace,
|
||||
ChecklyGroup,
|
||||
Environment,
|
||||
HerokuPipelineCoupling,
|
||||
IntegrationAuth,
|
||||
NorthflankSecretGroup,
|
||||
Org,
|
||||
@@ -63,6 +64,8 @@ const integrationAuthKeys = {
|
||||
environmentId: string;
|
||||
scope: "job" | "application" | "container";
|
||||
}) => [{ integrationAuthId, environmentId, scope }, "integrationAuthQoveryScopes"] as const,
|
||||
getIntegrationAuthHerokuPipelines: ({ integrationAuthId }: { integrationAuthId: string; }) =>
|
||||
[{ integrationAuthId}, "integrationAuthHerokuPipelines"] as const,
|
||||
getIntegrationAuthRailwayEnvironments: ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
@@ -289,6 +292,20 @@ const fetchIntegrationAuthQoveryScopes = async ({
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthHerokuPipelines = async ({ integrationAuthId }: {
|
||||
integrationAuthId: string;
|
||||
}) => {
|
||||
const {
|
||||
data: { pipelines }
|
||||
} = await apiRequest.get<{ pipelines: HerokuPipelineCoupling[] }>(
|
||||
`/api/v1/integration-auth/${integrationAuthId}/heroku/pipelines`
|
||||
);
|
||||
|
||||
console.log(99999, pipelines)
|
||||
|
||||
return pipelines;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthRailwayEnvironments = async ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
@@ -540,6 +557,23 @@ export const useGetIntegrationAuthQoveryScopes = ({
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthHerokuPipelines = ({
|
||||
integrationAuthId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthHerokuPipelines({
|
||||
integrationAuthId
|
||||
}),
|
||||
queryFn: () =>
|
||||
fetchIntegrationAuthHerokuPipelines({
|
||||
integrationAuthId
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthRailwayEnvironments = ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
|
||||
@@ -17,6 +17,17 @@ export type App = {
|
||||
secretGroups?: string[];
|
||||
};
|
||||
|
||||
export type Pipeline = {
|
||||
pipelineId: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type HerokuPipelineCoupling = {
|
||||
app: { appId: string };
|
||||
stage: string;
|
||||
pipeline: { pipelineId: string; name: string };
|
||||
};
|
||||
|
||||
export type Team = {
|
||||
name: string;
|
||||
teamId: string;
|
||||
|
||||
@@ -7,7 +7,10 @@ import { faArrowUpRightFromSquare, faBookOpen, faBugs, faCircleInfo } from "@for
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { RadioGroup } from "@app/components/v2/RadioGroup";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { useGetIntegrationAuthHerokuPipelines } from "@app/hooks/api/integrationAuth/queries";
|
||||
import { App, Pipeline } from "@app/hooks/api/integrationAuth/types";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -22,11 +25,12 @@ import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
} from "../../../hooks/api/integrationAuth";
|
||||
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
||||
import { useCreateWsEnvironment, useGetWorkspaceById } from "../../../hooks/api/workspace";
|
||||
|
||||
export default function HerokuCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
const { mutateAsync } = useCreateIntegration();
|
||||
const { mutateAsync: mutateAsyncEnv } = useCreateWsEnvironment();
|
||||
|
||||
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
||||
|
||||
@@ -36,9 +40,17 @@ export default function HerokuCreateIntegrationPage() {
|
||||
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||
});
|
||||
|
||||
const { data: integrationAuthPipelineCouplings } = useGetIntegrationAuthHerokuPipelines({
|
||||
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||
const [uniquePipelines, setUniquePipelines] = useState<Pipeline[]>();
|
||||
const [selectedPipeline, setSelectedPipeline] = useState("");
|
||||
const [selectedPipelineApps, setSelectedPipelineApps] = useState<App[]>();
|
||||
const [targetApp, setTargetApp] = useState("");
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [integrationType, setIntegrationType] = useState("App");
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -48,6 +60,36 @@ export default function HerokuCreateIntegrationPage() {
|
||||
}
|
||||
}, [workspace]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthPipelineCouplings) {
|
||||
const uniquePipelinesConst = Array.from(
|
||||
new Set(
|
||||
integrationAuthPipelineCouplings
|
||||
.map(({ pipeline: { pipelineId, name } }) => ({
|
||||
name,
|
||||
pipelineId
|
||||
}))
|
||||
.map((obj) => JSON.stringify(obj))
|
||||
)).map((str) => JSON.parse(str)) as { pipelineId: string; name: string }[]
|
||||
setUniquePipelines(uniquePipelinesConst);
|
||||
if (uniquePipelinesConst) {
|
||||
if (uniquePipelinesConst!.length > 0) {
|
||||
setSelectedPipeline(uniquePipelinesConst![0].name);
|
||||
} else {
|
||||
setSelectedPipeline("none");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [integrationAuthPipelineCouplings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthPipelineCouplings) {
|
||||
setSelectedPipelineApps(integrationAuthApps?.filter(app => integrationAuthPipelineCouplings
|
||||
.filter((pipelineCoupling) => pipelineCoupling.pipeline.name === selectedPipeline)
|
||||
.map(coupling => coupling.app.appId).includes(String(app.appId))))
|
||||
}
|
||||
}, [selectedPipeline]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
if (integrationAuthApps.length > 0) {
|
||||
@@ -64,13 +106,32 @@ export default function HerokuCreateIntegrationPage() {
|
||||
|
||||
if (!integrationAuth?.id) return;
|
||||
|
||||
await mutateAsync({
|
||||
integrationAuthId: integrationAuth?.id,
|
||||
isActive: true,
|
||||
app: targetApp,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
secretPath
|
||||
});
|
||||
if (integrationType === "App") {
|
||||
await mutateAsync({
|
||||
integrationAuthId: integrationAuth?.id,
|
||||
isActive: true,
|
||||
app: targetApp,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
secretPath
|
||||
});
|
||||
} else if (integrationType === "Pipeline") {
|
||||
selectedPipelineApps?.map(async (app, index) => {
|
||||
setTimeout(async () => {
|
||||
await mutateAsyncEnv({
|
||||
workspaceId: String(localStorage.getItem("projectData.id")),
|
||||
name: app.name,
|
||||
slug: app.name.toLowerCase().replaceAll(" ", "-")
|
||||
});
|
||||
await mutateAsync({
|
||||
integrationAuthId: integrationAuth?.id,
|
||||
isActive: true,
|
||||
app: app.name,
|
||||
sourceEnvironment: app.name.toLowerCase().replaceAll(" ", "-"),
|
||||
secretPath
|
||||
})
|
||||
}, 1000*index)
|
||||
})
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
||||
@@ -118,6 +179,36 @@ export default function HerokuCreateIntegrationPage() {
|
||||
</Link>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<RadioGroup
|
||||
value={integrationType}
|
||||
onValueChange={(val) => setIntegrationType(val)}
|
||||
/>
|
||||
{integrationType === "Pipeline" && <>
|
||||
<FormControl label="Heroku Pipeline" className="px-6">
|
||||
<Select
|
||||
value={selectedPipeline}
|
||||
onValueChange={(val) => setSelectedPipeline(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{uniquePipelines?.map((pipeline) => (
|
||||
<SelectItem
|
||||
value={pipeline.name}
|
||||
key={`source-environment-${pipeline.name}`}
|
||||
>
|
||||
{pipeline.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<div className="px-6 text-sm mb-4 inline-block text-bunker-300">
|
||||
After creating the integration, the following Heroku apps will be automatically synced to Infisical:
|
||||
<div className="flex flex-col">
|
||||
{selectedPipelineApps?.map(app => <p key={app.name} className="text-bunker-200 pl-0.5 inline-flex"><span className="text-primary pr-1">-></span> {app.name}</p>)}
|
||||
</div>
|
||||
From then on, every new app in the selected pipeline will be synced to Infisical, too.
|
||||
</div>
|
||||
</>}
|
||||
{integrationType === "App" && <>
|
||||
<FormControl label="Project Environment" className="px-6">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
@@ -163,7 +254,7 @@ export default function HerokuCreateIntegrationPage() {
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormControl></>}
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
@@ -175,6 +266,7 @@ export default function HerokuCreateIntegrationPage() {
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
{integrationType === "App" && <>
|
||||
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
||||
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||
<div className="flex flex-row items-center">
|
||||
@@ -185,7 +277,7 @@ export default function HerokuCreateIntegrationPage() {
|
||||
After creating an integration, your secrets will start syncing immediately. This might
|
||||
cause an unexpected override of current secrets in Heroku with secrets from Infisical.
|
||||
</span>
|
||||
</div>
|
||||
</div></>}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
|
||||
+2
-2
@@ -62,7 +62,7 @@ export const IntegrationsSection = ({
|
||||
<Link href={`/project/${workspaceId}/settings`} passHref>
|
||||
<a className="underline underline-offset-2">project settings </a>
|
||||
</Link>
|
||||
to re-enable it .
|
||||
to re-enable it.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
@@ -80,7 +80,7 @@ export const IntegrationsSection = ({
|
||||
<div className="flex flex-col space-y-4 p-6 pt-0">
|
||||
{integrations?.map((integration) => (
|
||||
<div
|
||||
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 pb-2"
|
||||
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3 pb-0"
|
||||
key={`integration-${integration?.id.toString()}`}
|
||||
>
|
||||
<div className="flex">
|
||||
|
||||
Reference in New Issue
Block a user