Skip to content

feat(site): Duplicate template #6853

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Mar 30, 2023
2 changes: 1 addition & 1 deletion site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ export const getTemplateVersionGitAuth = async (

export const getTemplateVersionParameters = async (
versionId: string,
): Promise<TypesGen.Parameter[]> => {
): Promise<TypesGen.ComputedParameter[]> => {
const response = await axios.get(
`/api/v2/templateversions/${versionId}/parameters`,
)
Expand Down
10 changes: 10 additions & 0 deletions site/src/components/TemplateLayout/TemplatePageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const Language = {
createButton: "Create workspace",
deleteButton: "Delete",
editFilesButton: "Edit files",
duplicateButton: "Duplicate",
}

const TemplateMenu: FC<{
Expand Down Expand Up @@ -67,6 +68,15 @@ const TemplateMenu: FC<{
>
{Language.settingsButton}
</MenuItem>
{canEditFiles && (
<MenuItem
onClick={handleClose}
component={RouterLink}
to={`/templates/new?copyTemplate=${templateName}`}
>
{Language.duplicateButton}
</MenuItem>
)}
{canEditFiles && (
<MenuItem
component={RouterLink}
Expand Down
90 changes: 74 additions & 16 deletions site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import TextField from "@material-ui/core/TextField"
import {
ParameterSchema,
ProvisionerJobLog,
Template,
TemplateExample,
TemplateVersionVariable,
} from "api/typesGenerated"
Expand Down Expand Up @@ -106,28 +107,74 @@ const defaultInitialValues: CreateTemplateData = {
allow_user_cancel_workspace_jobs: false,
}

const getInitialValues = (
canSetMaxTTL: boolean,
starterTemplate?: TemplateExample,
) => {
type GetInitialValuesParams = {
fromExample?: TemplateExample
fromCopy?: Template
parameters?: ParameterSchema[]
variables?: TemplateVersionVariable[]
canSetMaxTTL: boolean
}

const getInitialValues = ({
fromExample,
fromCopy,
canSetMaxTTL,
variables,
parameters,
}: GetInitialValuesParams) => {
let initialValues = defaultInitialValues

if (!canSetMaxTTL) {
initialValues = {
...initialValues,
max_ttl_hours: 0,
}
}
if (!starterTemplate) {
return initialValues

if (fromExample) {
initialValues = {
...initialValues,
name: fromExample.id,
display_name: fromExample.name,
icon: fromExample.icon,
description: fromExample.description,
}
}

if (fromCopy) {
initialValues = {
...initialValues,
...fromCopy,
name: `${fromCopy.name}-copy`,
display_name: fromCopy.display_name
? `${fromCopy.display_name} Copy`
: "",
}
}

if (variables) {
variables.forEach((variable) => {
if (!initialValues.user_variable_values) {
initialValues.user_variable_values = []
}
initialValues.user_variable_values.push({
name: variable.name,
value: variable.sensitive ? "" : variable.value,
})
})
}

return {
...initialValues,
name: starterTemplate.id,
display_name: starterTemplate.name,
icon: starterTemplate.icon,
description: starterTemplate.description,
if (parameters) {
parameters.forEach((parameter) => {
if (!initialValues.parameter_values_by_name) {
initialValues.parameter_values_by_name = {}
}
initialValues.parameter_values_by_name[parameter.name] =
parameter.default_source_value
})
}

return initialValues
}

export interface CreateTemplateFormProps {
Expand All @@ -142,12 +189,14 @@ export interface CreateTemplateFormProps {
jobError?: string
logs?: ProvisionerJobLog[]
canSetMaxTTL: boolean
copiedTemplate?: Template
}

export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
onCancel,
onSubmit,
starterTemplate,
copiedTemplate,
parameters,
variables,
isSubmitting,
Expand All @@ -159,7 +208,13 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
}) => {
const styles = useStyles()
const form = useFormik<CreateTemplateData>({
initialValues: getInitialValues(canSetMaxTTL, starterTemplate),
initialValues: getInitialValues({
canSetMaxTTL,
fromExample: starterTemplate,
fromCopy: copiedTemplate,
variables,
parameters,
}),
validationSchema,
onSubmit,
})
Expand All @@ -177,6 +232,8 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
<FormFields>
{starterTemplate ? (
<SelectedTemplate template={starterTemplate} />
) : copiedTemplate ? (
<SelectedTemplate template={copiedTemplate} />
) : (
<TemplateUpload
{...upload}
Expand Down Expand Up @@ -329,7 +386,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
</FormSection>

{/* Parameters */}
{parameters && (
{parameters && parameters.length > 0 && (
<FormSection
title={t("form.parameters.title")}
description={t("form.parameters.description")}
Expand All @@ -353,21 +410,22 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
)}

{/* Variables */}
{variables && (
{variables && variables.length > 0 && (
<FormSection
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: value,
value,
})
}}
/>
Expand Down
2 changes: 2 additions & 0 deletions site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const CreateTemplatePage: FC = () => {
context: {
organizationId,
exampleId: searchParams.get("exampleId"),
templateNameToCopy: searchParams.get("copyTemplate"),
},
actions: {
onCreate: (_, { data }) => {
Expand Down Expand Up @@ -67,6 +68,7 @@ const CreateTemplatePage: FC = () => {

{shouldDisplayForm && (
<CreateTemplateForm
copiedTemplate={state.context.copiedTemplate}
canSetMaxTTL={canSetMaxTTL}
error={error}
starterTemplate={starterTemplate}
Expand Down
108 changes: 108 additions & 0 deletions site/src/xServices/createTemplate/createTemplateXService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
uploadTemplateFile,
getTemplateVersionLogs,
getTemplateVersionVariables,
getTemplateByName,
getTemplateVersionParameters,
} from "api/api"
import {
CreateTemplateVersionRequest,
Expand Down Expand Up @@ -61,6 +63,9 @@ interface CreateTemplateContext {
// uploadedFile is the response from the server to use in the API
file?: File
uploadResponse?: UploadResponse
// When wanting to copy a Template
templateNameToCopy: string | null // It can be null because it is passed from query string
copiedTemplate?: Template
}

export const createTemplateMachine =
Expand Down Expand Up @@ -106,6 +111,14 @@ export const createTemplateMachine =
loadVersionLogs: {
data: ProvisionerJobLog[]
}
copyTemplateData: {
data: {
template: Template
version: TemplateVersion
parameters: ParameterSchema[]
variables: TemplateVersionVariable[]
}
}
},
},
tsTypes: {} as import("./createTemplateXService.typegen").Typegen0,
Expand All @@ -114,6 +127,10 @@ export const createTemplateMachine =
starting: {
always: [
{ target: "loadingStarterTemplate", cond: "isExampleProvided" },
{
target: "copyingTemplateData",
cond: "isTemplateIdToCopyProvided",
},
{ target: "idle" },
],
tags: ["loading"],
Expand All @@ -132,6 +149,27 @@ export const createTemplateMachine =
},
tags: ["loading"],
},
copyingTemplateData: {
invoke: {
src: "copyTemplateData",
onDone: [
{
target: "creating.promptParametersAndVariables",
actions: ["assignCopiedTemplateData"],
cond: "hasParametersOrVariables",
},
{
target: "idle",
actions: ["assignCopiedTemplateData"],
},
],
onError: {
target: "idle",
actions: ["assignError"],
},
},
tags: ["loading"],
},
idle: {
on: {
CREATE: {
Expand Down Expand Up @@ -292,10 +330,56 @@ export const createTemplateMachine =
}
return starterTemplate
},
copyTemplateData: async ({ organizationId, templateNameToCopy }) => {
if (!organizationId) {
throw new Error("No organization ID provided")
}
if (!templateNameToCopy) {
throw new Error("No template name to copy provided")
}
const template = await getTemplateByName(
organizationId,
templateNameToCopy,
)
const [version, schemaParameters, computedParameters, variables] =
await Promise.all([
getTemplateVersion(template.active_version_id),
getTemplateVersionSchema(template.active_version_id),
getTemplateVersionParameters(template.active_version_id),
getTemplateVersionVariables(template.active_version_id),
])

// Recreate parameters with default_source_value from the already
// computed version parameters
const parameters: ParameterSchema[] = []
computedParameters.forEach((computedParameter) => {
const schema = schemaParameters.find(
(schema) => schema.name === computedParameter.name,
)
if (!schema) {
throw new Error(
`Parameter ${computedParameter.name} not found in schema`,
)
}
parameters.push({
...schema,
default_source_value: computedParameter.source_value,
})
})

return {
template,
version,
parameters,
variables,
}
},
createFirstVersion: async ({
organizationId,
templateNameToCopy,
exampleId,
uploadResponse,
version,
}) => {
if (exampleId) {
return createTemplateVersion(organizationId, {
Expand All @@ -306,6 +390,19 @@ export const createTemplateMachine =
})
}

if (templateNameToCopy) {
if (!version) {
throw new Error("Not able to copy without having a version")
}

return createTemplateVersion(organizationId, {
storage_method: "file",
file_id: version.job.file_id,
provisioner: "terraform",
tags: {},
})
}

if (uploadResponse) {
return createTemplateVersion(organizationId, {
storage_method: "file",
Expand Down Expand Up @@ -456,9 +553,17 @@ export const createTemplateMachine =
uploadResponse: (_) => undefined,
}),
assignJobLogs: assign({ jobLogs: (_, { data }) => data }),
assignCopiedTemplateData: assign({
copiedTemplate: (_, { data }) => data.template,
version: (_, { data }) => data.version,
parameters: (_, { data }) => data.parameters,
variables: (_, { data }) => data.variables,
}),
},
guards: {
isExampleProvided: ({ exampleId }) => Boolean(exampleId),
isTemplateIdToCopyProvided: ({ templateNameToCopy }) =>
Boolean(templateNameToCopy),
isNotUsingExample: ({ exampleId }) => !exampleId,
hasFile: ({ file }) => Boolean(file),
hasFailed: (_, { data }) =>
Expand All @@ -469,6 +574,9 @@ export const createTemplateMachine =
),
hasNoParametersOrVariables: (_, { data }) =>
data.parameters === undefined && data.variables === undefined,
hasParametersOrVariables: (_, { data }) => {
return data.parameters.length > 0 || data.variables.length > 0
},
},
},
)
Expand Down