Skip to content

Commit

Permalink
scaffold form for enabling preview envs (#3711)
Browse files Browse the repository at this point in the history
  • Loading branch information
ianedwards authored Oct 2, 2023
1 parent bd8965b commit 03d5b33
Show file tree
Hide file tree
Showing 11 changed files with 556 additions and 74 deletions.
56 changes: 46 additions & 10 deletions dashboard/src/lib/hooks/usePorterYaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ import { z } from "zod";

type PorterYamlStatus =
| {
loading: true;
detectedName: null;
detectedServices: null;
porterYamlFound: false;
}
loading: true;
detectedName: null;
detectedServices: null;
porterYamlFound: false;
}
| {
detectedServices: DetectedServices | null;
detectedName: string | null;
loading: false;
porterYamlFound: boolean;
};
detectedServices: DetectedServices | null;
detectedName: string | null;
loading: false;
porterYamlFound: boolean;
};

/*
*
Expand Down Expand Up @@ -114,6 +114,15 @@ export const usePorterYaml = ({
const data = await z
.object({
b64_app_proto: z.string(),
env_variables: z.record(z.string()).nullable(),
env_secrets: z.record(z.string()).nullable(),
preview_app: z
.object({
b64_app_proto: z.string(),
env_variables: z.record(z.string()).nullable(),
env_secrets: z.record(z.string()).nullable(),
})
.optional(),
})
.parseAsync(res.data);
const proto = PorterApp.fromJsonString(atob(data.b64_app_proto));
Expand All @@ -131,6 +140,33 @@ export const usePorterYaml = ({
});
}

if (data.preview_app) {
const previewProto = PorterApp.fromJsonString(
atob(data.preview_app.b64_app_proto)
);
const {
services: previewServices,
predeploy: previewPredeploy,
build: previewBuild,
} = serviceOverrides({
overrides: previewProto,
useDefaults,
});

if (previewServices.length || previewPredeploy || previewBuild) {
setDetectedServices((prev) => ({
...prev,
services: prev?.services ? prev.services : [],
previews: {
services: previewServices,
predeploy: previewPredeploy,
build: previewBuild,
variables: data.preview_app?.env_variables ?? {},
},
}));
}
}

if (proto.name) {
setDetectedName(proto.name);
}
Expand Down
81 changes: 81 additions & 0 deletions dashboard/src/lib/porter-apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,3 +394,84 @@ export function clientAppFromProto({
},
};
}

export function applyPreviewOverrides({
app,
overrides,
}: {
app: ClientPorterApp;
overrides: DetectedServices["previews"];
}): ClientPorterApp {
if (!overrides) {
return app;
}

const services = app.services.map((svc) => {
const override = overrides.services.find(
(s) => s.name.value === svc.name.value
);
if (override) {
const ds = deserializeService({
service: serializeService(svc),
override: serializeService(override),
});

if (ds.config.type == "web") {
ds.config.domains = [];
}
return ds;
}

if (svc.config.type == "web") {
svc.config.domains = [];
}
return svc;
});
const additionalServices = overrides.services
.filter((s) => !app.services.find((svc) => svc.name.value === s.name.value))
.map((svc) => deserializeService({ service: serializeService(svc) }));

app.services = [...services, ...additionalServices];

if (app.predeploy) {
const predeployOverride = overrides.predeploy;
if (predeployOverride) {
app.predeploy = [
deserializeService({
service: serializeService(app.predeploy[0]),
override: serializeService(predeployOverride),
}),
];
}
}

const envOverrides = overrides.variables;
if (envOverrides) {
const env = app.env.map((e) => {
const override = envOverrides[e.key];
if (override) {
return {
...e,
locked: true,
value: override,
};
}

return e;
});

const additionalEnv = Object.entries(envOverrides)
.filter(([key]) => !app.env.find((e) => e.key === key))
.map(([key, value]) => ({
key,
value,
hidden: false,
locked: true,
deleted: false,
}));

app.env = [...env, ...additionalEnv];
}

return app;
}
59 changes: 38 additions & 21 deletions dashboard/src/lib/porter-apps/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,43 @@ export type DetectedServices = {
services: ClientService[];
predeploy?: ClientService;
build?: BuildOptions;
previews?: {
services: ClientService[];
predeploy?: ClientService;
variables?: Record<string, string>;
};
};
type ClientServiceType = "web" | "worker" | "job" | "predeploy";

const webConfigValidator = z.object({
type: z.literal("web"),
autoscaling: autoscalingValidator.optional(),
domains: domainsValidator,
healthCheck: healthcheckValidator.optional(),
private: serviceBooleanValidator.optional(),
});
export type ClientWebConfig = z.infer<typeof webConfigValidator>;

const workerConfigValidator = z.object({
type: z.literal("worker"),
autoscaling: autoscalingValidator.optional(),
});
export type ClientWorkerConfig = z.infer<typeof workerConfigValidator>;

const jobConfigValidator = z.object({
type: z.literal("job"),
allowConcurrent: serviceBooleanValidator.optional(),
cron: serviceStringValidator,
suspendCron: serviceBooleanValidator.optional(),
timeoutSeconds: serviceNumberValidator,
});
export type ClientJobConfig = z.infer<typeof jobConfigValidator>;

const predeployConfigValidator = z.object({
type: z.literal("predeploy"),
});
export type ClientPredeployConfig = z.infer<typeof predeployConfigValidator>;

// serviceValidator is the validator for a ClientService
// This is used to validate a service when creating or updating an app
export const serviceValidator = z.object({
Expand All @@ -37,27 +71,10 @@ export const serviceValidator = z.object({
cpuCores: serviceNumberValidator,
ramMegabytes: serviceNumberValidator,
config: z.discriminatedUnion("type", [
z.object({
type: z.literal("web"),
autoscaling: autoscalingValidator.optional(),
domains: domainsValidator,
healthCheck: healthcheckValidator.optional(),
private: serviceBooleanValidator.optional(),
}),
z.object({
type: z.literal("worker"),
autoscaling: autoscalingValidator.optional(),
}),
z.object({
type: z.literal("job"),
allowConcurrent: serviceBooleanValidator.optional(),
cron: serviceStringValidator,
suspendCron: serviceBooleanValidator.optional(),
timeoutSeconds: serviceNumberValidator,
}),
z.object({
type: z.literal("predeploy"),
}),
webConfigValidator,
workerConfigValidator,
jobConfigValidator,
predeployConfigValidator,
]),
domainDeletions: z
.object({
Expand Down
42 changes: 25 additions & 17 deletions dashboard/src/main/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import AppView from "./app-dashboard/app-view/AppView";
import Apps from "./app-dashboard/apps/Apps";
import DeploymentTargetProvider from "shared/DeploymentTargetContext";
import PreviewEnvs from "./cluster-dashboard/preview-environments/v2/PreviewEnvs";
import SetupApp from "./cluster-dashboard/preview-environments/v2/setup-app/SetupApp";

// Guarded components
const GuardedProjectSettings = fakeGuardedRoute("settings", "", [
Expand Down Expand Up @@ -451,23 +452,7 @@ const Home: React.FC<Props> = (props) => {
<AppDashboard />
)}
</Route>
{currentProject?.validate_apply_v2 &&
currentProject.preview_envs_enabled ? (
<>
<Route path={`/preview-environments/apps/:appName/:tab`}>
<AppView />
</Route>
<Route exact path="/preview-environments/apps/:appName">
<AppView />
</Route>
<Route exact path={`/preview-environments/apps`}>
<Apps />
</Route>
<Route exact path={`/preview-environments`}>
<PreviewEnvs />
</Route>
</>
) : null}

<Route path="/addons/new">
<NewAddOnFlow />
</Route>
Expand Down Expand Up @@ -556,6 +541,29 @@ const Home: React.FC<Props> = (props) => {
path={"/project-settings"}
render={() => <GuardedProjectSettings />}
/>
{currentProject?.validate_apply_v2 &&
currentProject.preview_envs_enabled ? (
<>
<Route exact path="/preview-environments/configure">
<SetupApp />
</Route>
<Route
exact
path={`/preview-environments/apps/:appName/:tab`}
>
<AppView />
</Route>
<Route exact path="/preview-environments/apps/:appName">
<AppView />
</Route>
<Route exact path={`/preview-environments/apps`}>
<Apps />
</Route>
<Route exact path={`/preview-environments`}>
<PreviewEnvs />
</Route>
</>
) : null}
<Route path={"*"} render={() => <LaunchWrapper />} />
</Switch>
</ViewWrapper>
Expand Down
51 changes: 45 additions & 6 deletions dashboard/src/main/home/app-dashboard/app-view/tabs/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useContext, useEffect, useState } from "react";
import styled from "styled-components";
import { useHistory } from "react-router";

Expand All @@ -11,16 +11,21 @@ import { useLatestRevision } from "../LatestRevisionContext";
import api from "shared/api";
import { useAppAnalytics } from "lib/hooks/useAppAnalytics";
import { useQueryClient } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import { Context } from "shared/Context";

const Settings: React.FC = () => {
const { currentProject } = useContext(Context);
const queryClient = useQueryClient();
const history = useHistory();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const { porterApp, clusterId, projectId } = useLatestRevision();
const { updateAppStep } = useAppAnalytics();
const [isDeleting, setIsDeleting] = useState(false);

const [githubWorkflowFilename, setGithubWorkflowFilename] = useState(`porter_stack_${porterApp.name}.yml`);
const [githubWorkflowFilename, setGithubWorkflowFilename] = useState(
`porter_stack_${porterApp.name}.yml`
);

const workflowFileExists = useCallback(async () => {
try {
Expand Down Expand Up @@ -109,12 +114,20 @@ const Settings: React.FC = () => {
window.open(res.data.url, "_blank", "noreferrer");
}

updateAppStep({ step: "stack-deletion", deleteWorkflow: true, appName: porterApp.name });
updateAppStep({
step: "stack-deletion",
deleteWorkflow: true,
appName: porterApp.name,
});
history.push("/apps");
return;
}

updateAppStep({ step: "stack-deletion", deleteWorkflow: false, appName: porterApp.name });
updateAppStep({
step: "stack-deletion",
deleteWorkflow: false,
appName: porterApp.name,
});
history.push("/apps");
} catch (err) {
} finally {
Expand All @@ -126,12 +139,38 @@ const Settings: React.FC = () => {

return (
<StyledSettingsTab>
{currentProject?.preview_envs_enabled && (
<>
<Text size={16}>
Enable preview environments for "{porterApp.name}"
</Text>
<Spacer y={0.5} />
<Text color="helper">
Setup your application to automatically create preview environments
for each pull request.
</Text>
<Spacer y={0.5} />
<Link
to={`/preview-environments/configure?app_name=${porterApp.name}`}
>
<Button
type="button"
onClick={() => {
setIsDeleteModalOpen(true);
}}
>
Enable
</Button>
</Link>
<Spacer y={1} />
</>
)}
<Text size={16}>Delete "{porterApp.name}"</Text>
<Spacer y={1} />
<Spacer y={0.5} />
<Text color="helper">
Delete this application and all of its resources.
</Text>
<Spacer y={1} />
<Spacer y={0.5} />
<Button
type="button"
onClick={() => {
Expand Down
Loading

0 comments on commit 03d5b33

Please sign in to comment.