Skip to content

Commit

Permalink
POR-1828 require CI rerun when build settings update (#3682)
Browse files Browse the repository at this point in the history
  • Loading branch information
ianedwards authored Sep 28, 2023
1 parent 06ceabc commit 1b0e2dc
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 51 deletions.
18 changes: 10 additions & 8 deletions dashboard/src/lib/hooks/useAppValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import api from "shared/api";
import { match } from "ts-pattern";
import { z } from "zod";

export type AppValidationResult = {
validatedAppProto: PorterApp;
variables: Record<string, string>;
secrets: Record<string, string>;
};

export const useAppValidation = ({
deploymentTargetID,
creating = false,
Expand All @@ -19,13 +25,6 @@ export const useAppValidation = ({
}) => {
const { currentProject, currentCluster } = useContext(Context);

const removedEnvKeys = (
current: Record<string, string>,
previous: Record<string, string>
) => {
return Object.keys(previous).filter((key) => !current[key]);
};

const getBranchHead = async ({
projectID,
source,
Expand Down Expand Up @@ -62,7 +61,10 @@ export const useAppValidation = ({
};

const validateApp = useCallback(
async (data: PorterAppFormData, prevRevision?: PorterApp) => {
async (
data: PorterAppFormData,
prevRevision?: PorterApp
): Promise<AppValidationResult> => {
if (!currentProject || !currentCluster) {
throw new Error("No project or cluster selected");
}
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/lib/porter-apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const porterAppFormValidator = z.object({
app: clientAppValidator,
source: sourceValidator,
deletions: deletionValidator,
redeployOnSave: z.boolean().default(false),
});
export type PorterAppFormData = z.infer<typeof porterAppFormValidator>;

Expand Down
80 changes: 62 additions & 18 deletions dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import {
PorterAppFormData,
Expand All @@ -13,7 +13,10 @@ import TabSelector from "components/TabSelector";
import { useHistory } from "react-router";
import { match } from "ts-pattern";
import Overview from "./tabs/Overview";
import { useAppValidation } from "lib/hooks/useAppValidation";
import {
AppValidationResult,
useAppValidation,
} from "lib/hooks/useAppValidation";
import api from "shared/api";
import { useQueryClient } from "@tanstack/react-query";
import Settings from "./tabs/Settings";
Expand All @@ -32,6 +35,7 @@ import EventFocusView from "./tabs/activity-feed/events/focus-views/EventFocusVi
import { z } from "zod";
import { PorterApp } from "@porter-dev/api-contracts";
import JobsTab from "./tabs/JobsTab";
import ConfirmRedeployModal from "./ConfirmRedeployModal";

// commented out tabs are not yet implemented
// will be included as support is available based on data from app revisions rather than helm releases
Expand All @@ -58,7 +62,7 @@ type AppDataContainerProps = {
const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
const history = useHistory();
const queryClient = useQueryClient();
const [redeployOnSave, setRedeployOnSave] = useState(false);
const [confirmDeployModalOpen, setConfirmDeployModalOpen] = useState(false);

const {
porterApp,
Expand Down Expand Up @@ -158,13 +162,26 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
return dirty.every((f) => f === "expanded" || f === "id");
}, [isDirty, JSON.stringify(dirtyFields)]);

const buildIsDirty = useMemo(() => {
if (!isDirty) return false;

// get all entries in entire dirtyFields object that are true
const dirty = getAllDirtyFields(dirtyFields.app?.build ?? {});
return dirty.some((f) => f);
}, [isDirty, JSON.stringify(dirtyFields)]);

const onSubmit = handleSubmit(async (data) => {
try {
const { validatedAppProto, variables, secrets } = await validateApp(
const { variables, secrets, validatedAppProto } = await validateApp(
data,
latestProto
);

if (buildIsDirty && !data.redeployOnSave) {
setConfirmDeployModalOpen(true);
return;
}

// updates the default env group associated with this app to store app specific env vars
const res = await api.updateEnvironmentGroupV2(
"<token>",
Expand Down Expand Up @@ -213,11 +230,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
}
);

if (
redeployOnSave &&
latestSource.type === "github" &&
dirtyFields.app?.build
) {
if (latestSource.type === "github" && buildIsDirty) {
const res = await api.reRunGHWorkflow(
"<token>",
{},
Expand All @@ -235,8 +248,6 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
if (res.data != null) {
window.open(res.data, "_blank", "noreferrer");
}

setRedeployOnSave(false);
}

await queryClient.invalidateQueries([
Expand All @@ -260,6 +271,31 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
} catch (err) {}
});

const cancelRedeploy = useCallback(() => {
reset({
app: clientAppFromProto({
proto: previewRevision
? PorterApp.fromJsonString(atob(previewRevision.b64_app_proto))
: latestProto,
overrides: servicesFromYaml,
variables: appEnv?.variables,
secrets: appEnv?.secret_variables,
}),
source: latestSource,
deletions: {
envGroupNames: [],
serviceNames: [],
},
redeployOnSave: false,
});
setConfirmDeployModalOpen(false);
}, [previewRevision, latestProto, servicesFromYaml, appEnv, latestSource]);

const finalizeDeploy = useCallback(() => {
setConfirmDeployModalOpen(false);
onSubmit();
}, [onSubmit, setConfirmDeployModalOpen]);

useEffect(() => {
reset({
app: clientAppFromProto({
Expand All @@ -275,6 +311,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
envGroupNames: [],
serviceNames: [],
},
redeployOnSave: false,
});
}, [
servicesFromYaml,
Expand Down Expand Up @@ -306,7 +343,12 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
loadingText={"Updating..."}
height={"10px"}
status={isSubmitting ? "loading" : ""}
disabled={isSubmitting}
disabled={
isSubmitting ||
latestRevision.status === "CREATED" ||
latestRevision.status === "AWAITING_BUILD_ARTIFACT"
}
disabledTooltipMessage="Please wait for the build to complete before updating the app"
>
<Icon src={save} height={"13px"} />
<Spacer inline x={0.5} />
Expand Down Expand Up @@ -353,12 +395,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
{match(currentTab)
.with("activity", () => <Activity />)
.with("overview", () => <Overview />)
.with("build-settings", () => (
<BuildSettings
redeployOnSave={redeployOnSave}
setRedeployOnSave={setRedeployOnSave}
/>
))
.with("build-settings", () => <BuildSettings />)
.with("environment", () => (
<Environment latestSource={latestSource} />
))
Expand All @@ -370,6 +407,13 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
.otherwise(() => null)}
<Spacer y={2} />
</form>
{confirmDeployModalOpen ? (
<ConfirmRedeployModal
setOpen={setConfirmDeployModalOpen}
cancelRedeploy={cancelRedeploy}
finalizeDeploy={finalizeDeploy}
/>
) : null}
</FormProvider>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Button from "components/porter/Button";
import Modal from "components/porter/Modal";
import Spacer from "components/porter/Spacer";
import Text from "components/porter/Text";
import { PorterAppFormData } from "lib/porter-apps";
import React, { Dispatch, SetStateAction } from "react";
import { useFormContext } from "react-hook-form";
import styled from "styled-components";

type Props = {
cancelRedeploy: () => void;
setOpen: Dispatch<SetStateAction<boolean>>;
finalizeDeploy: () => void;
};

const ConfirmRedeployModal: React.FC<Props> = ({
cancelRedeploy,
setOpen,
finalizeDeploy,
}) => {
const { setValue } = useFormContext<PorterAppFormData>();

return (
<Modal closeModal={() => setOpen(false)}>
<Text size={16}>Confirm deploy</Text>
<Spacer y={0.5} />
<Text color="helper">
A change to your application's build settings has been detected.
Confirming this change will trigger a rerun of your application's CI
pipeline.
</Text>
<Spacer y={0.5} />

<ButtonContainer>
<Button
onClick={() => {
cancelRedeploy();
setOpen(false);
}}
color="#b91133"
>
Cancel
</Button>
<Button
onClick={() => {
setValue("redeployOnSave", true);
finalizeDeploy();
}}
>
Continue
</Button>
</ButtonContainer>
</Modal>
);
};

export default ConfirmRedeployModal;

const ButtonContainer = styled.div`
display: flex;
justify-content: flex-end;
column-gap: 0.5rem;
`;
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import React, { Dispatch, SetStateAction, useMemo } from "react";
import React, { useMemo } from "react";
import RepoSettings from "../../create-app/RepoSettings";
import { useFormContext } from "react-hook-form";
import { PorterAppFormData } from "lib/porter-apps";
import { useLatestRevision } from "../LatestRevisionContext";
import Spacer from "components/porter/Spacer";
import Checkbox from "components/porter/Checkbox";
import Text from "components/porter/Text";
import Button from "components/porter/Button";
import Error from "components/porter/Error";

type Props = {
redeployOnSave: boolean;
setRedeployOnSave: Dispatch<SetStateAction<boolean>>;
};

const BuildSettings: React.FC<Props> = ({
redeployOnSave,
setRedeployOnSave,
}) => {
const BuildSettings: React.FC = () => {
const {
watch,
formState: { isSubmitting, errors },
Expand Down Expand Up @@ -52,13 +42,6 @@ const BuildSettings: React.FC<Props> = ({
appExists
/>
<Spacer y={1} />
<Checkbox
checked={redeployOnSave}
toggleChecked={() => setRedeployOnSave(!redeployOnSave)}
>
<Text>Re-run build and deploy on save</Text>
</Checkbox>
<Spacer y={1} />
<Button
type="submit"
status={buttonStatus}
Expand All @@ -67,6 +50,7 @@ const BuildSettings: React.FC<Props> = ({
latestRevision.status === "CREATED" ||
latestRevision.status === "AWAITING_BUILD_ARTIFACT"
}
disabledTooltipMessage="Please wait for the build to complete before updating build settings"
>
Save build settings
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const Environment: React.FC<Props> = ({ latestSource }) => {
latestRevision.status === "CREATED" ||
latestRevision.status === "AWAITING_BUILD_ARTIFACT"
}
disabledTooltipMessage="Please wait for the build to complete before updating environment variables"
>
Update app
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ import { useAppStatus } from "lib/hooks/useAppStatus";

const Overview: React.FC = () => {
const { formState } = useFormContext<PorterAppFormData>();
const { porterApp, latestProto, latestRevision, projectId, clusterId, deploymentTarget } = useLatestRevision();
const {
porterApp,
latestProto,
latestRevision,
projectId,
clusterId,
deploymentTarget,
} = useLatestRevision();

const { serviceVersionStatus } = useAppStatus({
projectId,
Expand Down Expand Up @@ -77,6 +84,7 @@ const Overview: React.FC = () => {
latestRevision.status === "CREATED" ||
latestRevision.status === "AWAITING_BUILD_ARTIFACT"
}
disabledTooltipMessage="Please wait for the build to complete before updating services"
>
Update app
</Button>
Expand Down
Loading

0 comments on commit 1b0e2dc

Please sign in to comment.