added basic heroku pipeline integration

This commit is contained in:
Vladyslav Matsiiako
2024-02-17 21:04:26 -08:00
parent a8678c14e8
commit 9012012503
14 changed files with 909 additions and 14 deletions
@@ -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;
+102 -10
View File
@@ -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">-&gt;</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">
@@ -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">