mirror of https://github.com/coder/coder.git
Compare commits
21 Commits
126efb2bf2
...
685c5fdbdb
Author | SHA1 | Date |
---|---|---|
Steven Masley | 685c5fdbdb | |
Steven Masley | 38623111c8 | |
Steven Masley | f5e09cd130 | |
Colin Adler | 13dd526f11 | |
recanman | b20c63c185 | |
Michael Brewer | 060f023174 | |
Steven Masley | 604227e46d | |
Steven Masley | c25f6c415e | |
Steven Masley | 50501e88d1 | |
Steven Masley | f4d7c0d11e | |
Steven Masley | 8fdaf2c44c | |
Steven Masley | de3f81d9a9 | |
Steven Masley | e82b2c34dc | |
Steven Masley | f104148e96 | |
Steven Masley | 736df67786 | |
Steven Masley | 2a09af35d3 | |
Steven Masley | f4dbfb5913 | |
Steven Masley | e12e721236 | |
Steven Masley | ae89373050 | |
Steven Masley | 5294740a62 | |
Steven Masley | c8107597a5 |
|
@ -222,5 +222,7 @@
|
|||
"go.testFlags": ["-short", "-coverpkg=./..."],
|
||||
// We often use a version of TypeScript that's ahead of the version shipped
|
||||
// with VS Code.
|
||||
"typescript.tsdk": "./site/node_modules/typescript/lib"
|
||||
"typescript.tsdk": "./site/node_modules/typescript/lib",
|
||||
// Playwright tests in VSCode will open a browser to live "view" the test.
|
||||
"playwright.reuseBrowser": true
|
||||
}
|
||||
|
|
|
@ -287,7 +287,8 @@ func (r *RootCmd) login() *serpent.Command {
|
|||
}
|
||||
|
||||
sessionToken, err = cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: "Paste your token here:",
|
||||
Text: "Paste your token here:",
|
||||
Secret: true,
|
||||
Validate: func(token string) error {
|
||||
client.SetSessionToken(token)
|
||||
_, err := client.User(ctx, codersdk.Me)
|
||||
|
|
|
@ -1441,7 +1441,7 @@ func newProvisionerDaemon(
|
|||
|
||||
connector[string(database.ProvisionerTypeTerraform)] = sdkproto.NewDRPCProvisionerClient(terraformClient)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown provisioner type %q", provisionerType)
|
||||
return nil, xerrors.Errorf("unknown provisioner type %q", provisionerType)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -79,6 +80,10 @@ func (r *RootCmd) ssh() *serpent.Command {
|
|||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Prevent unnecessary logs from the stdlib from messing up the TTY.
|
||||
// See: https://github.com/coder/coder/issues/13144
|
||||
log.SetOutput(io.Discard)
|
||||
|
||||
logger := inv.Logger
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
#!/sbin/openrc-run
|
||||
name=coder
|
||||
description="Coder - Self-hosted developer workspaces on your infra"
|
||||
document="https://coder.com/docs/coder-oss"
|
||||
|
||||
depend() {
|
||||
need net
|
||||
after net-online
|
||||
use dns logger
|
||||
}
|
||||
|
||||
checkpath --directory --owner coder:coder --mode 0700 /var/cache/coder
|
||||
|
||||
start_pre() {
|
||||
if [ ! -f /etc/coder.d/coder.env ]; then
|
||||
eerror "/etc/coder.d/coder.env file does not exist"
|
||||
return 1
|
||||
fi
|
||||
# Read and export environment variables ignoring comment lines and blank lines
|
||||
while IFS= read -r line; do
|
||||
# Skip blank or comment lines
|
||||
if [ -z "$line" ] || [[ "$line" =~ ^# ]]; then
|
||||
continue
|
||||
fi
|
||||
export "$line"
|
||||
done < /etc/coder.d/coder.env
|
||||
}
|
||||
|
||||
command="/usr/bin/coder"
|
||||
command_args="server"
|
||||
command_user="coder:coder"
|
||||
command_background="yes"
|
||||
pidfile="/run/coder.pid"
|
||||
|
||||
restart="always"
|
||||
restart_delay="5"
|
||||
|
||||
stop_timeout="90"
|
|
@ -0,0 +1,39 @@
|
|||
#!/sbin/openrc-run
|
||||
name=coder-workspace-proxy
|
||||
description="Coder - external workspace proxy server"
|
||||
document="https://coder.com/docs/coder-oss"
|
||||
|
||||
depend() {
|
||||
need net
|
||||
after net-online
|
||||
use dns logger
|
||||
}
|
||||
|
||||
checkpath --directory --owner coder:coder --mode 0700 /var/cache/coder
|
||||
|
||||
start_pre() {
|
||||
if [ ! -f /etc/coder.d/coder-workspace-proxy.env ]; then
|
||||
eerror "/etc/coder.d/coder-workspace-proxy.env file does not exist"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Read and export environment variables ignoring comment lines and blank lines
|
||||
while IFS= read -r line; do
|
||||
# Skip blank or comment lines
|
||||
if [ -z "$line" ] || [[ "$line" =~ ^# ]]; then
|
||||
continue
|
||||
fi
|
||||
export "$line"
|
||||
done < /etc/coder.d/coder-workspace-proxy.env
|
||||
}
|
||||
|
||||
command="/usr/bin/coder"
|
||||
command_args="workspace-proxy server"
|
||||
command_user="coder:coder"
|
||||
command_background="yes"
|
||||
pidfile="/run/coder-workspace-proxy.pid"
|
||||
|
||||
restart="always"
|
||||
restart_delay="5"
|
||||
|
||||
stop_timeout="90"
|
|
@ -0,0 +1,29 @@
|
|||
name: coder
|
||||
platform: linux
|
||||
arch: "${GOARCH}"
|
||||
version: "${CODER_VERSION}"
|
||||
version_schema: semver
|
||||
release: 1
|
||||
|
||||
vendor: Coder
|
||||
homepage: https://coder.com
|
||||
maintainer: Coder <support@coder.com>
|
||||
description: |
|
||||
Provision development environments with infrastructure with code
|
||||
license: AGPL-3.0
|
||||
suggests:
|
||||
- postgresql
|
||||
|
||||
scripts:
|
||||
preinstall: preinstall.sh
|
||||
|
||||
contents:
|
||||
- src: coder
|
||||
dst: /usr/bin/coder
|
||||
- src: coder.env
|
||||
dst: /etc/coder.d/coder.env
|
||||
type: "config|noreplace"
|
||||
- src: coder-workspace-proxy-openrc
|
||||
dst: /etc/init.d/coder-workspace-proxy
|
||||
- src: coder-openrc
|
||||
dst: /etc/init.d/coder
|
|
@ -89,9 +89,16 @@ ln "$(realpath scripts/linux-pkg/coder.service)" "$temp_dir/"
|
|||
ln "$(realpath scripts/linux-pkg/nfpm.yaml)" "$temp_dir/"
|
||||
ln "$(realpath scripts/linux-pkg/preinstall.sh)" "$temp_dir/"
|
||||
|
||||
nfpm_config_file="nfpm.yaml"
|
||||
|
||||
# Use nfpm-alpine.yaml when building for Alpine (OpenRC).
|
||||
if [[ "$format" == "apk" ]]; then
|
||||
nfpm_config_file="nfpm-alpine.yaml"
|
||||
fi
|
||||
|
||||
pushd "$temp_dir"
|
||||
GOARCH="$arch" CODER_VERSION="$version" nfpm package \
|
||||
-f nfpm.yaml \
|
||||
-f "$nfpm_config_file" \
|
||||
-p "$format" \
|
||||
-t "$output_path" \
|
||||
1>&2
|
||||
|
|
|
@ -39,6 +39,10 @@ export const requireEnterpriseTests = Boolean(
|
|||
);
|
||||
export const enterpriseLicense = process.env.CODER_E2E_ENTERPRISE_LICENSE ?? "";
|
||||
|
||||
// Disabling terraform tests is optional for environments without Docker + Terraform.
|
||||
// By default, we opt into these tests.
|
||||
export const requireTerraformTests = !process.env.CODER_E2E_DISABLE_TERRAFORM;
|
||||
|
||||
// Fake experiments to verify that site presents them as enabled.
|
||||
export const e2eFakeExperiment1 = "e2e-fake-experiment-1";
|
||||
export const e2eFakeExperiment2 = "e2e-fake-experiment-2";
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
enterpriseLicense,
|
||||
prometheusPort,
|
||||
requireEnterpriseTests,
|
||||
requireTerraformTests,
|
||||
} from "./constants";
|
||||
import { expectUrl } from "./expectUrl";
|
||||
import {
|
||||
|
@ -43,6 +44,15 @@ export function requiresEnterpriseLicense() {
|
|||
test.skip(!enterpriseLicense);
|
||||
}
|
||||
|
||||
// requiresTerraform by default is enabled.
|
||||
export function requiresTerraform() {
|
||||
if (requireTerraformTests) {
|
||||
return;
|
||||
}
|
||||
|
||||
test.skip(!requireTerraformTests);
|
||||
}
|
||||
|
||||
// createWorkspace creates a workspace for a template.
|
||||
// It does not wait for it to be running, but it does navigate to the page.
|
||||
export const createWorkspace = async (
|
||||
|
@ -149,25 +159,52 @@ export const verifyParameters = async (
|
|||
}
|
||||
};
|
||||
|
||||
// StarterTemplates are ids of starter templates that can be used in place of
|
||||
// the responses payload. These starter templates will require real provisioners.
|
||||
export enum StarterTemplates {
|
||||
STARTER_DOCKER = "docker",
|
||||
}
|
||||
|
||||
function isStarterTemplate(
|
||||
input: EchoProvisionerResponses | StarterTemplates | undefined,
|
||||
): input is StarterTemplates {
|
||||
if (!input) {
|
||||
return false;
|
||||
}
|
||||
return typeof input === "string";
|
||||
}
|
||||
|
||||
// createTemplate navigates to the /templates/new page and uploads a template
|
||||
// with the resources provided in the responses argument.
|
||||
export const createTemplate = async (
|
||||
page: Page,
|
||||
responses?: EchoProvisionerResponses,
|
||||
responses?: EchoProvisionerResponses | StarterTemplates,
|
||||
): Promise<string> => {
|
||||
// Required to have templates submit their provisioner type as echo!
|
||||
await page.addInitScript({
|
||||
content: "window.playwright = true",
|
||||
});
|
||||
let path = "/templates/new";
|
||||
if (isStarterTemplate(responses)) {
|
||||
path += `?exampleId=${responses}`;
|
||||
}
|
||||
|
||||
await page.goto("/templates/new", { waitUntil: "domcontentloaded" });
|
||||
await page.goto(path, { waitUntil: "domcontentloaded" });
|
||||
await expectUrl(page).toHavePathName("/templates/new");
|
||||
|
||||
await page.getByTestId("file-upload").setInputFiles({
|
||||
buffer: await createTemplateVersionTar(responses),
|
||||
mimeType: "application/x-tar",
|
||||
name: "template.tar",
|
||||
});
|
||||
if (!isStarterTemplate(responses)) {
|
||||
await page
|
||||
.locator(`xpath=//input[@data-testid="provisioner-type-input"]`)
|
||||
.evaluate((el: HTMLElement) => {
|
||||
// This is a little jank, but the "setAttribute" updates the HTML, but not the formik values.
|
||||
el.setAttribute("value", "echo");
|
||||
// This '.click()' activates the onClick handler that tells the input to update it's formik value.
|
||||
el.click();
|
||||
});
|
||||
|
||||
await page.getByTestId("file-upload").setInputFiles({
|
||||
buffer: await createTemplateVersionTar(responses),
|
||||
mimeType: "application/x-tar",
|
||||
name: "template.tar",
|
||||
});
|
||||
}
|
||||
|
||||
const name = randomName();
|
||||
await page.getByLabel("Name *").fill(name);
|
||||
await page.getByTestId("form-submit").click();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { defineConfig } from "@playwright/test";
|
||||
import { execSync } from "child_process";
|
||||
import * as path from "path";
|
||||
import {
|
||||
coderMain,
|
||||
|
@ -7,6 +8,7 @@ import {
|
|||
e2eFakeExperiment1,
|
||||
e2eFakeExperiment2,
|
||||
gitAuth,
|
||||
requireTerraformTests,
|
||||
} from "./constants";
|
||||
|
||||
export const wsEndpoint = process.env.CODER_E2E_WS_ENDPOINT;
|
||||
|
@ -14,6 +16,25 @@ export const wsEndpoint = process.env.CODER_E2E_WS_ENDPOINT;
|
|||
// This is where auth cookies are stored!
|
||||
export const storageState = path.join(__dirname, ".auth.json");
|
||||
|
||||
if (requireTerraformTests) {
|
||||
try {
|
||||
// If running terraform tests, verify the requirements exist in the
|
||||
// environment.
|
||||
//
|
||||
// These execs will throw an error if the status code is non-zero.
|
||||
// So if both these work, then we can launch terraform provisioners.
|
||||
execSync("terraform --version");
|
||||
execSync("docker --version");
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
"Terraform provisioners require docker & terraform. " +
|
||||
"At least one of these is not present in the runtime environment. To check yourself:\n" +
|
||||
"\t$ terraform --version\n" +
|
||||
"\t$ docker --version",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const localURL = (port: number, path: string): string => {
|
||||
return `http://localhost:${port}${path}`;
|
||||
};
|
||||
|
@ -54,13 +75,14 @@ export default defineConfig({
|
|||
`go run -tags embed ${coderMain} server`,
|
||||
"--global-config $(mktemp -d -t e2e-XXXXXXXXXX)",
|
||||
`--access-url=http://localhost:${coderPort}`,
|
||||
`--http-address=localhost:${coderPort}`,
|
||||
`--http-address=0.0.0.0:${coderPort}`,
|
||||
"--in-memory",
|
||||
"--telemetry=false",
|
||||
"--dangerous-disable-rate-limits",
|
||||
"--provisioner-daemons 10",
|
||||
// TODO: Enable some terraform provisioners
|
||||
"--provisioner-types=echo",
|
||||
`--provisioner-types=echo${requireTerraformTests ? ",terraform" : ""}`,
|
||||
`--provisioner-daemons=10`,
|
||||
"--web-terminal-renderer=dom",
|
||||
"--pprof-enable",
|
||||
]
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import {
|
||||
StarterTemplates,
|
||||
createTemplate,
|
||||
createWorkspace,
|
||||
echoResponsesWithParameters,
|
||||
requiresTerraform,
|
||||
verifyParameters,
|
||||
} from "../helpers";
|
||||
import { beforeCoderTest } from "../hooks";
|
||||
|
@ -147,3 +149,43 @@ test("create workspace with disable_param search params", async ({ page }) => {
|
|||
await expect(page.getByLabel(/First parameter/i)).toBeDisabled();
|
||||
await expect(page.getByLabel(/Second parameter/i)).toBeDisabled();
|
||||
});
|
||||
|
||||
test("create docker workspace", async ({ page }) => {
|
||||
test.skip(
|
||||
true,
|
||||
"creating docker containers is currently leaky. They are not cleaned up when the tests are over.",
|
||||
);
|
||||
requiresTerraform();
|
||||
const template = await createTemplate(page, StarterTemplates.STARTER_DOCKER);
|
||||
|
||||
const _ = await createWorkspace(page, template);
|
||||
|
||||
// The workspace agents must be ready before we try to interact with the workspace.
|
||||
await page.waitForSelector(
|
||||
`//div[@role="status"][@data-testid="agent-status-ready"]`,
|
||||
{
|
||||
state: "visible",
|
||||
},
|
||||
);
|
||||
|
||||
// Wait for the terminal button to be visible, and click it.
|
||||
const terminalButton =
|
||||
"//a[@data-testid='terminal'][normalize-space()='Terminal']";
|
||||
await page.waitForSelector(terminalButton, {
|
||||
state: "visible",
|
||||
});
|
||||
|
||||
// We can't click the terminal button because that opens a new tab.
|
||||
// So grab the href, and manually navigate.
|
||||
const terminalPageURL = await page.getAttribute(terminalButton, "href");
|
||||
expect(terminalPageURL).not.toBeNull();
|
||||
await page.goto(terminalPageURL!, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await page.waitForSelector(
|
||||
`//textarea[contains(@class,"xterm-helper-textarea")]`,
|
||||
{
|
||||
state: "visible",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import TextField from "@mui/material/TextField";
|
||||
import { useFormik } from "formik";
|
||||
import { Field, FormikProvider, useFormik } from "formik";
|
||||
import camelCase from "lodash/camelCase";
|
||||
import capitalize from "lodash/capitalize";
|
||||
import type { FC } from "react";
|
||||
import * as Yup from "yup";
|
||||
import type {
|
||||
ProvisionerJobLog,
|
||||
ProvisionerType,
|
||||
Template,
|
||||
TemplateExample,
|
||||
TemplateVersionVariable,
|
||||
|
@ -50,6 +51,7 @@ export interface CreateTemplateData {
|
|||
parameter_values_by_name?: Record<string, string>;
|
||||
user_variable_values?: VariableValue[];
|
||||
allow_everyone_group_access: boolean;
|
||||
provisioner_type: ProvisionerType;
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
|
@ -81,6 +83,7 @@ const defaultInitialValues: CreateTemplateData = {
|
|||
allow_user_autostart: false,
|
||||
allow_user_autostop: false,
|
||||
allow_everyone_group_access: true,
|
||||
provisioner_type: "terraform",
|
||||
};
|
||||
|
||||
type GetInitialValuesParams = {
|
||||
|
@ -190,133 +193,154 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
|||
const getFieldHelpers = getFormHelpers<CreateTemplateData>(form, error);
|
||||
|
||||
return (
|
||||
<HorizontalForm onSubmit={form.handleSubmit}>
|
||||
{/* General info */}
|
||||
<FormSection
|
||||
title="General"
|
||||
description="The name is used to identify the template in URLs and the API."
|
||||
>
|
||||
<FormFields>
|
||||
{"starterTemplate" in props && (
|
||||
<SelectedTemplate template={props.starterTemplate} />
|
||||
)}
|
||||
{"copiedTemplate" in props && (
|
||||
<SelectedTemplate template={props.copiedTemplate} />
|
||||
)}
|
||||
{"upload" in props && (
|
||||
<TemplateUpload
|
||||
{...props.upload}
|
||||
onUpload={async (file) => {
|
||||
await fillNameAndDisplayWithFilename(file.name, form);
|
||||
props.upload.onUpload(file);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
disabled={isSubmitting}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
autoFocus
|
||||
fullWidth
|
||||
required
|
||||
label="Name"
|
||||
/>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
|
||||
{/* Display info */}
|
||||
<FormSection
|
||||
title="Display"
|
||||
description="A friendly name, description, and icon to help developers identify your template."
|
||||
>
|
||||
<FormFields>
|
||||
<TextField
|
||||
{...getFieldHelpers("display_name")}
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
label="Display name"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers("description", {
|
||||
maxLength: MAX_DESCRIPTION_CHAR_LIMIT,
|
||||
})}
|
||||
disabled={isSubmitting}
|
||||
rows={5}
|
||||
multiline
|
||||
fullWidth
|
||||
label="Description"
|
||||
/>
|
||||
|
||||
<IconField
|
||||
{...getFieldHelpers("icon")}
|
||||
disabled={isSubmitting}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
fullWidth
|
||||
onPickEmoji={(value) => form.setFieldValue("icon", value)}
|
||||
/>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
|
||||
{/* Variables */}
|
||||
{variables && variables.length > 0 && (
|
||||
<FormikProvider value={form}>
|
||||
<HorizontalForm onSubmit={form.handleSubmit}>
|
||||
{/* General info */}
|
||||
<FormSection
|
||||
ref={variablesSectionRef}
|
||||
title="Variables"
|
||||
description="Input variables allow you to customize templates without altering their source code."
|
||||
title="General"
|
||||
description="The name is used to identify the template in URLs and the API."
|
||||
>
|
||||
<FormFields>
|
||||
{variables.map((variable, index) => (
|
||||
<VariableInput
|
||||
defaultValue={variable.value}
|
||||
variable={variable}
|
||||
disabled={isSubmitting}
|
||||
key={variable.name}
|
||||
onChange={async (value) => {
|
||||
await form.setFieldValue("user_variable_values." + index, {
|
||||
name: variable.name,
|
||||
value,
|
||||
});
|
||||
{"starterTemplate" in props && (
|
||||
<SelectedTemplate template={props.starterTemplate} />
|
||||
)}
|
||||
{"copiedTemplate" in props && (
|
||||
<SelectedTemplate template={props.copiedTemplate} />
|
||||
)}
|
||||
{"upload" in props && (
|
||||
<TemplateUpload
|
||||
{...props.upload}
|
||||
onUpload={async (file) => {
|
||||
await fillNameAndDisplayWithFilename(file.name, form);
|
||||
props.upload.onUpload(file);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
|
||||
{/*
|
||||
This value is always "terraform" in production.
|
||||
For testing purposes, we expose this as a hidden form element
|
||||
that can be changed. For example, to "echo"
|
||||
*/}
|
||||
<Field
|
||||
type="hidden"
|
||||
{...getFieldHelpers("provisioner_type")}
|
||||
data-testid="provisioner-type-input"
|
||||
label="Provisioner type"
|
||||
// This is a bit jank, but when you call 'setAttribute('value', 'echo') from playwright, the formik form
|
||||
// is not updated. So calling 'click' will also update formik. This is super weird, but I cannot find another
|
||||
// way
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Not sure what the actual type is here.
|
||||
onClick={async (e: any) => {
|
||||
await form.setFieldValue("provisioner_type", e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
disabled={isSubmitting}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
autoFocus
|
||||
fullWidth
|
||||
required
|
||||
label="Name"
|
||||
/>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
<div className="flex items-center">
|
||||
<FormFooter
|
||||
extraActions={
|
||||
logs && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenBuildLogsDrawer}
|
||||
css={(theme) => ({
|
||||
backgroundColor: "transparent",
|
||||
border: 0,
|
||||
fontWeight: 500,
|
||||
fontSize: 14,
|
||||
cursor: "pointer",
|
||||
color: theme.palette.text.secondary,
|
||||
{/* Display info */}
|
||||
<FormSection
|
||||
title="Display"
|
||||
description="A friendly name, description, and icon to help developers identify your template."
|
||||
>
|
||||
<FormFields>
|
||||
<TextField
|
||||
{...getFieldHelpers("display_name")}
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
label="Display name"
|
||||
/>
|
||||
|
||||
"&:hover": {
|
||||
textDecoration: "underline",
|
||||
textUnderlineOffset: 4,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
})}
|
||||
>
|
||||
Show build logs
|
||||
</button>
|
||||
)
|
||||
}
|
||||
onCancel={onCancel}
|
||||
isLoading={isSubmitting}
|
||||
submitLabel={jobError ? "Retry" : "Create template"}
|
||||
/>
|
||||
</div>
|
||||
</HorizontalForm>
|
||||
<TextField
|
||||
{...getFieldHelpers("description", {
|
||||
maxLength: MAX_DESCRIPTION_CHAR_LIMIT,
|
||||
})}
|
||||
disabled={isSubmitting}
|
||||
rows={5}
|
||||
multiline
|
||||
fullWidth
|
||||
label="Description"
|
||||
/>
|
||||
|
||||
<IconField
|
||||
{...getFieldHelpers("icon")}
|
||||
disabled={isSubmitting}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
fullWidth
|
||||
onPickEmoji={(value) => form.setFieldValue("icon", value)}
|
||||
/>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
|
||||
{/* Variables */}
|
||||
{variables && variables.length > 0 && (
|
||||
<FormSection
|
||||
ref={variablesSectionRef}
|
||||
title="Variables"
|
||||
description="Input variables allow you to customize templates without altering their source code."
|
||||
>
|
||||
<FormFields>
|
||||
{variables.map((variable, index) => (
|
||||
<VariableInput
|
||||
defaultValue={variable.value}
|
||||
variable={variable}
|
||||
disabled={isSubmitting}
|
||||
key={variable.name}
|
||||
onChange={async (value) => {
|
||||
await form.setFieldValue("user_variable_values." + index, {
|
||||
name: variable.name,
|
||||
value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
<div className="flex items-center">
|
||||
<FormFooter
|
||||
extraActions={
|
||||
logs && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenBuildLogsDrawer}
|
||||
css={(theme) => ({
|
||||
backgroundColor: "transparent",
|
||||
border: 0,
|
||||
fontWeight: 500,
|
||||
fontSize: 14,
|
||||
cursor: "pointer",
|
||||
color: theme.palette.text.secondary,
|
||||
|
||||
"&:hover": {
|
||||
textDecoration: "underline",
|
||||
textUnderlineOffset: 4,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
})}
|
||||
>
|
||||
Show build logs
|
||||
</button>
|
||||
)
|
||||
}
|
||||
onCancel={onCancel}
|
||||
isLoading={isSubmitting}
|
||||
submitLabel={jobError ? "Retry" : "Create template"}
|
||||
/>
|
||||
</div>
|
||||
</HorizontalForm>
|
||||
</FormikProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -82,6 +82,7 @@ export const DuplicateTemplateView: FC<CreateTemplatePageViewProps> = ({
|
|||
version: firstVersionFromFile(
|
||||
templateVersionQuery.data!.job.file_id,
|
||||
formData.user_variable_values,
|
||||
formData.provisioner_type,
|
||||
),
|
||||
template: newTemplate(formData),
|
||||
});
|
||||
|
|
|
@ -64,6 +64,7 @@ export const UploadTemplateView: FC<CreateTemplatePageViewProps> = ({
|
|||
version: firstVersionFromFile(
|
||||
uploadedFile!.hash,
|
||||
formData.user_variable_values,
|
||||
formData.provisioner_type,
|
||||
),
|
||||
template: newTemplate(formData),
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type {
|
||||
CreateTemplateVersionRequest,
|
||||
Entitlements,
|
||||
ProvisionerType,
|
||||
TemplateExample,
|
||||
|
@ -7,10 +8,6 @@ import type {
|
|||
import { calculateAutostopRequirementDaysValue } from "utils/schedule";
|
||||
import type { CreateTemplateData } from "./CreateTemplateForm";
|
||||
|
||||
const provisioner: ProvisionerType =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Playwright needs to use a different provisioner type!
|
||||
typeof (window as any).playwright !== "undefined" ? "echo" : "terraform";
|
||||
|
||||
export const newTemplate = (formData: CreateTemplateData) => {
|
||||
const { autostop_requirement_days_of_week, autostop_requirement_weeks } =
|
||||
formData;
|
||||
|
@ -56,10 +53,11 @@ export const getFormPermissions = (entitlements: Entitlements) => {
|
|||
export const firstVersionFromFile = (
|
||||
fileId: string,
|
||||
variables: VariableValue[] | undefined,
|
||||
) => {
|
||||
provisionerType: ProvisionerType,
|
||||
): CreateTemplateVersionRequest => {
|
||||
return {
|
||||
storage_method: "file" as const,
|
||||
provisioner: provisioner,
|
||||
provisioner: provisionerType,
|
||||
user_variable_values: variables,
|
||||
file_id: fileId,
|
||||
tags: {},
|
||||
|
@ -69,10 +67,11 @@ export const firstVersionFromFile = (
|
|||
export const firstVersionFromExample = (
|
||||
example: TemplateExample,
|
||||
variables: VariableValue[] | undefined,
|
||||
) => {
|
||||
): CreateTemplateVersionRequest => {
|
||||
return {
|
||||
storage_method: "file" as const,
|
||||
provisioner: provisioner,
|
||||
// All starter templates are for the terraform provisioner type.
|
||||
provisioner: "terraform",
|
||||
user_variable_values: variables,
|
||||
example_id: example.id,
|
||||
tags: {},
|
||||
|
|
Loading…
Reference in New Issue