Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: Add create template from the UI #5427

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 61 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
300a763
feat: add examples to api
f0ssel Dec 6, 2022
da9fc00
add support for example id in route
f0ssel Dec 7, 2022
a9f04f3
move files
f0ssel Dec 7, 2022
90e6a9d
fix existing tests
f0ssel Dec 7, 2022
7084ceb
add tests
f0ssel Dec 7, 2022
50fb405
more tests
f0ssel Dec 7, 2022
b00a3b9
more tests
f0ssel Dec 7, 2022
86350be
Display starter templates
BrunoQuaresma Dec 8, 2022
c3b2e67
Add styles to the template card
BrunoQuaresma Dec 8, 2022
c144866
Mock entity and handler
BrunoQuaresma Dec 8, 2022
c133cc3
Add storybook
BrunoQuaresma Dec 8, 2022
e7aec29
Add loader
BrunoQuaresma Dec 8, 2022
3062814
Add tests
BrunoQuaresma Dec 8, 2022
d7455c1
Add basic page to starter template
BrunoQuaresma Dec 8, 2022
193c930
Add buttons
BrunoQuaresma Dec 8, 2022
9a5b1de
Fix title
BrunoQuaresma Dec 8, 2022
c820f4b
Addd storybook
BrunoQuaresma Dec 8, 2022
9829ad0
Add test
BrunoQuaresma Dec 8, 2022
c9d1c7a
Use translation
BrunoQuaresma Dec 8, 2022
85266d0
add icon and tag parsing
f0ssel Dec 9, 2022
9f3108f
remove extra test work
f0ssel Dec 9, 2022
0dce958
Merge branch 'f0ssel/examples' of github.com:coder/coder into bq/fe-e…
BrunoQuaresma Dec 9, 2022
3091bea
Merge branch 'main' of github.com:coder/coder into bq/fe-examples
BrunoQuaresma Dec 9, 2022
79442ef
Add icons to page header
BrunoQuaresma Dec 9, 2022
3529c74
Add filters
BrunoQuaresma Dec 9, 2022
efa624c
Improve markdown code
BrunoQuaresma Dec 9, 2022
e05a8dc
Merge branch 'main' of github.com:coder/coder into bq/fe-examples
BrunoQuaresma Dec 12, 2022
e05d148
Add filter
BrunoQuaresma Dec 12, 2022
4fc75b7
Add basic create template form structure
BrunoQuaresma Dec 12, 2022
9805c2f
Add translation
BrunoQuaresma Dec 12, 2022
439ec15
Add services and actions into machine
BrunoQuaresma Dec 12, 2022
ce82a85
Pre-fill info from example data
BrunoQuaresma Dec 12, 2022
36282fc
Create Icon fiels and remove extra console.log
BrunoQuaresma Dec 12, 2022
5b01321
Add basic API for template creation
BrunoQuaresma Dec 13, 2022
ca606a4
Merge branch 'main' of github.com:coder/coder into bq/fe-examples
BrunoQuaresma Dec 13, 2022
521803f
Fix create template from example id
BrunoQuaresma Dec 13, 2022
d39cabe
Show parameters
BrunoQuaresma Dec 14, 2022
fe71988
Add upload
BrunoQuaresma Dec 14, 2022
a9f3e18
Fix steps
BrunoQuaresma Dec 14, 2022
9ae5ed1
Update layout
BrunoQuaresma Dec 14, 2022
e19bd13
Merge branch 'main' of github.com:coder/coder into bq/fe-examples
BrunoQuaresma Dec 14, 2022
cc3a2cb
Update verbiage
BrunoQuaresma Dec 14, 2022
273a711
Merge branch 'main' of github.com:coder/coder into bq/fe-examples
BrunoQuaresma Dec 16, 2022
2d8430b
Use data
BrunoQuaresma Dec 16, 2022
a2e07fb
Show logs on error
BrunoQuaresma Dec 16, 2022
80a2989
Merge branch 'main' of github.com:coder/coder into bq/fe-examples
BrunoQuaresma Dec 19, 2022
43c8faa
Add templates link
BrunoQuaresma Dec 20, 2022
e64db78
Fix upload
BrunoQuaresma Dec 20, 2022
cec4b00
Add link to starter templates
BrunoQuaresma Dec 20, 2022
8b0f95b
Add empty state
BrunoQuaresma Dec 20, 2022
6de5c52
Create empty state and experimental tags
BrunoQuaresma Dec 20, 2022
228bc58
Add help tooltip
BrunoQuaresma Dec 20, 2022
155e901
Fix tests
BrunoQuaresma Dec 20, 2022
4edfed8
Lazy load starter template page
BrunoQuaresma Dec 20, 2022
30e017e
Apply suggestions from code review
BrunoQuaresma Dec 21, 2022
98a5f84
No need to trim display name and description
BrunoQuaresma Dec 21, 2022
6aa0bfa
Return undefined it is not n api error
BrunoQuaresma Dec 21, 2022
f8e8d33
Display error
BrunoQuaresma Dec 21, 2022
4b47137
Merge branch 'bq/fe-examples' of github.com:coder/coder into bq/fe-ex…
BrunoQuaresma Dec 21, 2022
be4de6a
Return error
BrunoQuaresma Dec 21, 2022
e75714f
Fix test
BrunoQuaresma Dec 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add upload
  • Loading branch information
BrunoQuaresma committed Dec 14, 2022
commit fe71988b18f635eb77bed4e153b18a0174e7da9b
63 changes: 8 additions & 55 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -744,60 +744,13 @@ export const getTemplateExamples = async (
return response.data
}

// for creating a new template:
// 1. upload template tar or use the example ID
// 2. create template version
// 3. wait for it to complete
// 4. if the job failed with the missing parameter error then:
// a. prompt for params
// b. create template version again with the same file hash
// c. wait for it to complete
// 5.create template with the successful template version ID
// https://github.com/coder/coder/blob/b6703b11c6578b2f91a310d28b6a7e57f0069be6/cli/templatecreate.go#L169-L170
export const createValidTemplate = async (
organizationId: string,
exampleId: string,
// The template_version_id is calculated in the function
data: Omit<TypesGen.CreateTemplateRequest, "template_version_id">,
): Promise<TypesGen.Template> => {
// Step 2.
let version = await createTemplateVersion(organizationId, {
storage_method: "file",
example_id: exampleId,
provisioner: "terraform",
tags: {},
})

// Step 3.
let status = version.job.status
while (["pending", "running"].includes(status)) {
version = await getTemplateVersion(version.id)
status = version.job.status
}
if (status === "failed") {
console.error(version.job.error)
throw new Error(version.job.error)
}

// // Get schema and create a new template version with the parameters
// const schema = await getTemplateVersionSchema(version.id)
// version = await createTemplateVersion(organizationId, {
// storage_method: "file",
// example_id: exampleId,
// provisioner: "terraform",
// tags: {},
// parameter_values: schema.map((parameter) => ({
// name: parameter.name,
// source_scheme: parameter.default_source_scheme,
// destination_scheme: parameter.default_destination_scheme,
// source_value: parameter.default_source_value,
// })),
// })

// Step 5.
const template = await createTemplate(organizationId, {
...data,
template_version_id: version.id,
export const uploadTemplateFile = async (
file: File,
): Promise<TypesGen.UploadResponse> => {
const response = await axios.post("/api/v2/files", file, {
headers: {
"Content-Type": "application/x-tar",
},
})
return template
return response.data
}
12 changes: 11 additions & 1 deletion site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { FormFooter } from "components/FormFooter/FormFooter"
import { IconField } from "components/IconField/IconField"
import { ParameterInput } from "components/ParameterInput/ParameterInput"
import { Stack } from "components/Stack/Stack"
import {
TemplateUpload,
TemplateUploadProps,
} from "pages/CreateTemplatePage/TemplateUpload"
import { useFormik } from "formik"
import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate"
import { FC } from "react"
Expand Down Expand Up @@ -55,6 +59,7 @@ interface CreateTemplateFormProps {
isSubmitting: boolean
onCancel: () => void
onSubmit: (data: CreateTemplateData) => void
upload: TemplateUploadProps
}

export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
Expand All @@ -64,6 +69,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
isSubmitting,
onCancel,
onSubmit,
upload,
}) => {
const styles = useStyles()
const formFooterStyles = useFormFooterStyles()
Expand All @@ -90,7 +96,11 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
</div>

<Stack direction="column" className={styles.formSectionFields}>
{starterTemplate && <SelectedTemplate template={starterTemplate} />}
{starterTemplate ? (
<SelectedTemplate template={starterTemplate} />
) : (
<TemplateUpload {...upload} />
)}

<TextField
{...getFieldHelpers("name")}
Expand Down
12 changes: 11 additions & 1 deletion site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const CreateTemplatePage: FC = () => {
},
},
})
const { starterTemplate, parameters, error } = state.context
const { starterTemplate, parameters, error, file } = state.context
const shouldDisplayForm = !state.hasTag("loading")

const onCancel = () => {
Expand Down Expand Up @@ -58,6 +58,16 @@ const CreateTemplatePage: FC = () => {
data,
})
}}
upload={{
file,
isUploading: state.matches("uploading"),
onRemove: () => {
send("REMOVE_FILE")
},
onUpload: (file) => {
send({ type: "UPLOAD_FILE", file })
},
}}
/>
)}
</FullPageHorizontalForm>
Expand Down
136 changes: 136 additions & 0 deletions site/src/pages/CreateTemplatePage/TemplateUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { makeStyles } from "@material-ui/core/styles"
import { Stack } from "components/Stack/Stack"
import { FC, useRef } from "react"
import UploadIcon from "@material-ui/icons/CloudUploadOutlined"
import { useClickable } from "hooks/useClickable"
import CircularProgress from "@material-ui/core/CircularProgress"
import { combineClasses } from "util/combineClasses"
import IconButton from "@material-ui/core/IconButton"
import RemoveIcon from "@material-ui/icons/DeleteOutline"
import FileIcon from "@material-ui/icons/FolderOutlined"

export interface TemplateUploadProps {
isUploading: boolean
onUpload: (file: File) => void
onRemove: () => void
file?: File
}

export const TemplateUpload: FC<TemplateUploadProps> = ({
isUploading,
onUpload,
onRemove,
file,
}) => {
const styles = useStyles()
const inputRef = useRef<HTMLInputElement>(null)
const clickable = useClickable(() => {
if (inputRef.current) {
inputRef.current.click()
}
})

if (!isUploading && file) {
return (
<Stack
className={styles.file}
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Stack direction="row" alignItems="center">
<FileIcon />
<span>{file.name}</span>
</Stack>

<IconButton title="Remove file" size="small" onClick={onRemove}>
<RemoveIcon />
</IconButton>
</Stack>
)
}

return (
<>
<div
className={combineClasses({
[styles.root]: true,
[styles.disabled]: isUploading,
})}
{...clickable}
>
<Stack alignItems="center" spacing={1}>
{isUploading ? (
<CircularProgress size={32} />
) : (
<UploadIcon className={styles.icon} />
)}

<Stack alignItems="center" spacing={0.5}>
<span className={styles.title}>Upload template</span>
<span className={styles.description}>
The template needs to be in a .tar file
</span>
</Stack>
</Stack>
</div>

<input
type="file"
ref={inputRef}
className={styles.input}
accept=".tar"
onChange={(event) => {
const file = event.currentTarget.files?.[0]
if (file) {
onUpload(file)
}
}}
/>
</>
)
}

const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: theme.shape.borderRadius,
border: `2px dashed ${theme.palette.divider}`,
padding: theme.spacing(6),
cursor: "pointer",

"&:hover": {
backgroundColor: theme.palette.background.paper,
},
},

disabled: {
pointerEvents: "none",
opacity: 0.75,
},

icon: {
fontSize: theme.spacing(8),
},

title: {
fontSize: theme.spacing(2),
},

description: {
color: theme.palette.text.secondary,
},

input: {
display: "none",
},

file: {
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`,
padding: theme.spacing(2),
background: theme.palette.background.paper,
},
}))
55 changes: 54 additions & 1 deletion site/src/xServices/createTemplate/createTemplateXService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,30 @@ import {
getTemplateVersion,
createTemplate,
getTemplateVersionSchema,
uploadTemplateFile,
} from "api/api"
import {
CreateTemplateRequest,
ParameterSchema,
Template,
TemplateExample,
TemplateVersion,
UploadResponse,
} from "api/typesGenerated"
import { displayError } from "components/GlobalSnackbar/utils"
import { assign, createMachine } from "xstate"

// for creating a new template:
// 1. upload template tar or use the example ID
// 2. create template version
// 3. wait for it to complete
// 4. if the job failed with the missing parameter error then:
// a. prompt for params
// b. create template version again with the same file hash
// c. wait for it to complete
// 5.create template with the successful template version ID
// https://github.com/coder/coder/blob/b6703b11c6578b2f91a310d28b6a7e57f0069be6/cli/templatecreate.go#L169-L170

export interface CreateTemplateData {
name: string
display_name: string
Expand All @@ -33,6 +46,10 @@ interface CreateTemplateContext {
version?: TemplateVersion
templateData?: CreateTemplateData
parameters?: ParameterSchema[]
// file is used in the FE to show the filename and some other visual stuff
// uploadedFile is the response from the server to use in the API
file?: File
uploadResponse?: UploadResponse
}

export const createTemplateMachine =
Expand All @@ -43,8 +60,14 @@ export const createTemplateMachine =
predictableActionArguments: true,
schema: {
context: {} as CreateTemplateContext,
events: {} as { type: "CREATE"; data: CreateTemplateData },
events: {} as
| { type: "CREATE"; data: CreateTemplateData }
| { type: "UPLOAD_FILE"; file: File }
| { type: "REMOVE_FILE" },
services: {} as {
uploadFile: {
data: UploadResponse
}
loadStarterTemplate: {
data: TemplateExample
}
Expand Down Expand Up @@ -92,6 +115,26 @@ export const createTemplateMachine =
target: "creating",
actions: ["assignTemplateData"],
},
UPLOAD_FILE: {
actions: ["assignFile"],
target: "uploading",
},
REMOVE_FILE: {
actions: ["removeFile"],
},
},
},
uploading: {
invoke: {
src: "uploadFile",
onDone: {
target: "idle",
actions: ["assignUploadResponse"],
},
onError: {
target: "idle",
actions: ["displayUploadError", "removeFile"],
},
},
},
creating: {
Expand Down Expand Up @@ -179,6 +222,7 @@ export const createTemplateMachine =
},
{
services: {
uploadFile: (_, { file }) => uploadTemplateFile(file),
loadStarterTemplate: async ({ organizationId, exampleId }) => {
if (!exampleId) {
throw new Error(`Example ID is not defined.`)
Expand Down Expand Up @@ -270,12 +314,21 @@ export const createTemplateMachine =
displayJobError: (_, { data }) => {
displayError("Provisioner job failed.", data.job.error)
},
displayUploadError: () => {
displayError("Error on upload the file.")
},
assignStarterTemplate: assign({
starterTemplate: (_, { data }) => data,
}),
assignVersion: assign({ version: (_, { data }) => data }),
assignTemplateData: assign({ templateData: (_, { data }) => data }),
assignParameters: assign({ parameters: (_, { data }) => data }),
assignFile: assign({ file: (_, { file }) => file }),
assignUploadResponse: assign({ uploadResponse: (_, { data }) => data }),
removeFile: assign({
file: (_) => undefined,
uploadResponse: (_) => undefined,
}),
},
guards: {
isExampleProvided: ({ exampleId }) => Boolean(exampleId),
Expand Down