mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Display and edit template icons in the UI (#3598)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import InputAdornment from "@material-ui/core/InputAdornment"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import { Template, UpdateTemplateMeta } from "api/typesGenerated"
|
||||
import { FormFooter } from "components/FormFooter/FormFooter"
|
||||
@@ -10,6 +12,7 @@ import * as Yup from "yup"
|
||||
export const Language = {
|
||||
nameLabel: "Name",
|
||||
descriptionLabel: "Description",
|
||||
iconLabel: "Icon",
|
||||
maxTtlLabel: "Max TTL",
|
||||
// This is the same from the CLI on https://github.com/coder/coder/blob/546157b63ef9204658acf58cb653aa9936b70c49/cli/templateedit.go#L59
|
||||
maxTtlHelperText: "Edit the template maximum time before shutdown in milliseconds",
|
||||
@@ -45,6 +48,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
max_ttl_ms: template.max_ttl_ms,
|
||||
icon: template.icon,
|
||||
},
|
||||
validationSchema,
|
||||
onSubmit: (data) => {
|
||||
@@ -53,6 +57,8 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
||||
initialTouched,
|
||||
})
|
||||
const getFieldHelpers = getFormHelpersWithError<UpdateTemplateMeta>(form, error)
|
||||
const styles = useStyles()
|
||||
const hasIcon = form.values.icon && form.values.icon !== ""
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit} aria-label={Language.formAriaLabel}>
|
||||
@@ -77,6 +83,29 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers("icon")}
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
label={Language.iconLabel}
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
endAdornment: hasIcon ? (
|
||||
<InputAdornment position="end">
|
||||
<img
|
||||
alt=""
|
||||
src={form.values.icon}
|
||||
className={styles.adornment}
|
||||
// This prevent browser to display the ugly error icon if the
|
||||
// image path is wrong or user didn't finish typing the url
|
||||
onError={(e) => (e.currentTarget.style.display = "none")}
|
||||
onLoad={(e) => (e.currentTarget.style.display = "inline")}
|
||||
/>
|
||||
</InputAdornment>
|
||||
) : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers("max_ttl_ms")}
|
||||
helperText={Language.maxTtlHelperText}
|
||||
@@ -92,3 +121,10 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
adornment: {
|
||||
width: theme.spacing(3),
|
||||
height: theme.spacing(3),
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -23,6 +23,7 @@ const fillAndSubmitForm = async ({
|
||||
name,
|
||||
description,
|
||||
max_ttl_ms,
|
||||
icon,
|
||||
}: Omit<Required<UpdateTemplateMeta>, "min_autostart_interval_ms">) => {
|
||||
const nameField = await screen.findByLabelText(FormLanguage.nameLabel)
|
||||
await userEvent.clear(nameField)
|
||||
@@ -32,6 +33,10 @@ const fillAndSubmitForm = async ({
|
||||
await userEvent.clear(descriptionField)
|
||||
await userEvent.type(descriptionField, description)
|
||||
|
||||
const iconField = await screen.findByLabelText(FormLanguage.iconLabel)
|
||||
await userEvent.clear(iconField)
|
||||
await userEvent.type(iconField, icon)
|
||||
|
||||
const maxTtlField = await screen.findByLabelText(FormLanguage.maxTtlLabel)
|
||||
await userEvent.clear(maxTtlField)
|
||||
await userEvent.type(maxTtlField, max_ttl_ms.toString())
|
||||
@@ -54,7 +59,7 @@ describe("TemplateSettingsPage", () => {
|
||||
name: "edited-template-name",
|
||||
description: "Edited description",
|
||||
max_ttl_ms: 4000,
|
||||
icon: "/icons/new-icon.png",
|
||||
icon: "/icon/code.svg",
|
||||
}
|
||||
jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
|
||||
...MockTemplate,
|
||||
|
||||
@@ -17,11 +17,13 @@ AllStates.args = {
|
||||
{
|
||||
...MockTemplate,
|
||||
description: "🚀 Some magical template that does some magical things!",
|
||||
icon: "/icon/goland.svg",
|
||||
},
|
||||
{
|
||||
...MockTemplate,
|
||||
workspace_owner_count: 150,
|
||||
description: "😮 Wow, this one has a bunch of usage!",
|
||||
icon: "",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -142,6 +142,8 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
|
||||
)}
|
||||
{props.templates?.map((template) => {
|
||||
const templatePageLink = `/templates/${template.name}`
|
||||
const hasIcon = template.icon && template.icon !== ""
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={template.id}
|
||||
@@ -160,6 +162,13 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
|
||||
title={template.name}
|
||||
subtitle={template.description}
|
||||
highlightTitle
|
||||
avatar={
|
||||
hasIcon ? (
|
||||
<div className={styles.templateIconWrapper}>
|
||||
<img alt="" src={template.icon} />
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</TableCellLink>
|
||||
|
||||
@@ -211,4 +220,10 @@ const useStyles = makeStyles((theme) => ({
|
||||
arrowCell: {
|
||||
display: "flex",
|
||||
},
|
||||
templateIconWrapper: {
|
||||
// Same size then the avatar component
|
||||
width: 36,
|
||||
height: 36,
|
||||
padding: 2,
|
||||
},
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user