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

Skip to content

Commit c505e8b

Browse files
feat: Add create template from the UI (#5427)
1 parent 43b61ce commit c505e8b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2540
-228
lines changed

site/src/AppRouter.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ const GitAuthPage = lazy(() => import("./pages/GitAuthPage/GitAuthPage"))
9292
const TemplateVersionPage = lazy(
9393
() => import("./pages/TemplateVersionPage/TemplateVersionPage"),
9494
)
95+
const StarterTemplatesPage = lazy(
96+
() => import("./pages/StarterTemplatesPage/StarterTemplatesPage"),
97+
)
98+
const StarterTemplatePage = lazy(
99+
() => import("pages/StarterTemplatePage/StarterTemplatePage"),
100+
)
101+
const CreateTemplatePage = lazy(
102+
() => import("./pages/CreateTemplatePage/CreateTemplatePage"),
103+
)
95104

96105
export const AppRouter: FC = () => {
97106
const xServices = useContext(XServiceContext)
@@ -141,6 +150,26 @@ export const AppRouter: FC = () => {
141150
}
142151
/>
143152

153+
<Route path="starter-templates">
154+
<Route
155+
index
156+
element={
157+
<AuthAndFrame>
158+
<StarterTemplatesPage />
159+
</AuthAndFrame>
160+
}
161+
/>
162+
163+
<Route
164+
path=":exampleId"
165+
element={
166+
<AuthAndFrame>
167+
<StarterTemplatePage />
168+
</AuthAndFrame>
169+
}
170+
></Route>
171+
</Route>
172+
144173
<Route path="templates">
145174
<Route
146175
index
@@ -151,6 +180,15 @@ export const AppRouter: FC = () => {
151180
}
152181
/>
153182

183+
<Route
184+
path="new"
185+
element={
186+
<RequireAuth>
187+
<CreateTemplatePage />
188+
</RequireAuth>
189+
}
190+
/>
191+
154192
<Route path=":template">
155193
<Route
156194
index

site/src/api/api.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,37 @@ export const getPreviousTemplateVersionByName = async (
259259
}
260260
}
261261

262+
export const createTemplateVersion = async (
263+
organizationId: string,
264+
data: TypesGen.CreateTemplateVersionRequest,
265+
): Promise<TypesGen.TemplateVersion> => {
266+
const response = await axios.post<TypesGen.TemplateVersion>(
267+
`/api/v2/organizations/${organizationId}/templateversions`,
268+
data,
269+
)
270+
return response.data
271+
}
272+
273+
export const getTemplateVersionParameters = async (
274+
versionId: string,
275+
): Promise<TypesGen.Parameter[]> => {
276+
const response = await axios.get(
277+
`/api/v2/templateversions/${versionId}/parameters`,
278+
)
279+
return response.data
280+
}
281+
282+
export const createTemplate = async (
283+
organizationId: string,
284+
data: TypesGen.CreateTemplateRequest,
285+
): Promise<TypesGen.Template> => {
286+
const response = await axios.post(
287+
`/api/v2/organizations/${organizationId}/templates`,
288+
data,
289+
)
290+
return response.data
291+
}
292+
262293
export const updateTemplateMeta = async (
263294
templateId: string,
264295
data: TypesGen.UpdateTemplateMeta,
@@ -703,3 +734,32 @@ export const setServiceBanner = async (
703734
const response = await axios.put(`/api/v2/service-banner`, b)
704735
return response.data
705736
}
737+
738+
export const getTemplateExamples = async (
739+
organizationId: string,
740+
): Promise<TypesGen.TemplateExample[]> => {
741+
const response = await axios.get(
742+
`/api/v2/organizations/${organizationId}/templates/examples`,
743+
)
744+
return response.data
745+
}
746+
747+
export const uploadTemplateFile = async (
748+
file: File,
749+
): Promise<TypesGen.UploadResponse> => {
750+
const response = await axios.post("/api/v2/files", file, {
751+
headers: {
752+
"Content-Type": "application/x-tar",
753+
},
754+
})
755+
return response.data
756+
}
757+
758+
export const getTemplateVersionLogs = async (
759+
versionId: string,
760+
): Promise<TypesGen.ProvisionerJobLog[]> => {
761+
const response = await axios.get<TypesGen.ProvisionerJobLog[]>(
762+
`/api/v2/templateversions/${versionId}/logs`,
763+
)
764+
return response.data
765+
}

site/src/api/errors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ export const mapApiErrorToFieldErrors = (
6363
return result
6464
}
6565

66+
export const isApiValidationError = (error: unknown): error is ApiError => {
67+
return isApiError(error) && hasApiFieldErrors(error)
68+
}
69+
6670
/**
6771
*
6872
* @param error
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import Button from "@material-ui/core/Button"
2+
import InputAdornment from "@material-ui/core/InputAdornment"
3+
import Popover from "@material-ui/core/Popover"
4+
import TextField, { TextFieldProps } from "@material-ui/core/TextField"
5+
import { OpenDropdown } from "components/DropdownArrows/DropdownArrows"
6+
import { useRef, FC, useState } from "react"
7+
import Picker from "@emoji-mart/react"
8+
import { makeStyles } from "@material-ui/core/styles"
9+
import { colors } from "theme/colors"
10+
import { useTranslation } from "react-i18next"
11+
import data from "@emoji-mart/data/sets/14/twitter.json"
12+
13+
export const IconField: FC<
14+
TextFieldProps & { onPickEmoji: (value: string) => void }
15+
> = ({ onPickEmoji, ...textFieldProps }) => {
16+
if (
17+
typeof textFieldProps.value !== "string" &&
18+
typeof textFieldProps.value !== "undefined"
19+
) {
20+
throw new Error(`Invalid icon value "${typeof textFieldProps.value}"`)
21+
}
22+
23+
const styles = useStyles()
24+
const emojiButtonRef = useRef<HTMLButtonElement>(null)
25+
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false)
26+
const { t } = useTranslation("templateSettingsPage")
27+
const hasIcon = textFieldProps.value && textFieldProps.value !== ""
28+
29+
return (
30+
<div className={styles.iconField}>
31+
<TextField
32+
{...textFieldProps}
33+
fullWidth
34+
label={t("iconLabel")}
35+
variant="outlined"
36+
InputProps={{
37+
endAdornment: hasIcon ? (
38+
<InputAdornment position="end" className={styles.adornment}>
39+
<img
40+
alt=""
41+
src={textFieldProps.value}
42+
// This prevent browser to display the ugly error icon if the
43+
// image path is wrong or user didn't finish typing the url
44+
onError={(e) => (e.currentTarget.style.display = "none")}
45+
onLoad={(e) => (e.currentTarget.style.display = "inline")}
46+
/>
47+
</InputAdornment>
48+
) : undefined,
49+
}}
50+
/>
51+
52+
<Button
53+
fullWidth
54+
ref={emojiButtonRef}
55+
variant="outlined"
56+
size="small"
57+
endIcon={<OpenDropdown />}
58+
onClick={() => {
59+
setIsEmojiPickerOpen((v) => !v)
60+
}}
61+
>
62+
{t("selectEmoji")}
63+
</Button>
64+
65+
<Popover
66+
id="emoji"
67+
open={isEmojiPickerOpen}
68+
anchorEl={emojiButtonRef.current}
69+
onClose={() => {
70+
setIsEmojiPickerOpen(false)
71+
}}
72+
>
73+
<Picker
74+
theme="dark"
75+
data={data}
76+
onEmojiSelect={(emojiData) => {
77+
// See: https://github.com/missive/emoji-mart/issues/51#issuecomment-287353222
78+
const value = `/emojis/${emojiData.unified.replace(
79+
/-fe0f$/,
80+
"",
81+
)}.png`
82+
onPickEmoji(value)
83+
setIsEmojiPickerOpen(false)
84+
}}
85+
/>
86+
</Popover>
87+
</div>
88+
)
89+
}
90+
91+
const useStyles = makeStyles((theme) => ({
92+
"@global": {
93+
"em-emoji-picker": {
94+
"--rgb-background": theme.palette.background.paper,
95+
"--rgb-input": colors.gray[17],
96+
"--rgb-color": colors.gray[4],
97+
},
98+
},
99+
adornment: {
100+
width: theme.spacing(3),
101+
height: theme.spacing(3),
102+
display: "flex",
103+
alignItems: "center",
104+
justifyContent: "center",
105+
106+
"& img": {
107+
maxWidth: "100%",
108+
},
109+
},
110+
iconField: {
111+
paddingBottom: theme.spacing(0.5),
112+
},
113+
}))

site/src/components/Logs/Logs.tsx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { makeStyles } from "@material-ui/core/styles"
2+
import { LogLevel } from "api/typesGenerated"
23
import dayjs from "dayjs"
34
import { FC } from "react"
45
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
@@ -7,6 +8,7 @@ import { combineClasses } from "../../util/combineClasses"
78
interface Line {
89
time: string
910
output: string
11+
level: LogLevel
1012
}
1113

1214
export interface LogsProps {
@@ -22,15 +24,17 @@ export const Logs: FC<React.PropsWithChildren<LogsProps>> = ({
2224

2325
return (
2426
<div className={combineClasses([className, styles.root])}>
25-
{lines.map((line, idx) => (
26-
<div className={styles.line} key={idx}>
27-
<span className={styles.time}>
28-
{dayjs(line.time).format(`HH:mm:ss.SSS`)}
29-
</span>
30-
<span className={styles.space}>&nbsp;&nbsp;&nbsp;&nbsp;</span>
31-
<span>{line.output}</span>
32-
</div>
33-
))}
27+
<div className={styles.scrollWrapper}>
28+
{lines.map((line, idx) => (
29+
<div className={combineClasses([styles.line, line.level])} key={idx}>
30+
<span className={styles.time}>
31+
{dayjs(line.time).format(`HH:mm:ss.SSS`)}
32+
</span>
33+
<span className={styles.space}>&nbsp;&nbsp;&nbsp;&nbsp;</span>
34+
<span>{line.output}</span>
35+
</div>
36+
))}
37+
</div>
3438
</div>
3539
)
3640
}
@@ -43,13 +47,25 @@ const useStyles = makeStyles((theme) => ({
4347
fontFamily: MONOSPACE_FONT_FAMILY,
4448
fontSize: 13,
4549
wordBreak: "break-all",
46-
padding: theme.spacing(2),
50+
padding: theme.spacing(2, 0),
4751
borderRadius: theme.shape.borderRadius,
4852
overflowX: "auto",
4953
},
54+
scrollWrapper: {
55+
width: "fit-content",
56+
},
5057
line: {
5158
// Whitespace is significant in terminal output for alignment
5259
whiteSpace: "pre",
60+
padding: theme.spacing(0, 3),
61+
62+
"&.error": {
63+
backgroundColor: theme.palette.error.dark,
64+
},
65+
66+
"&.warning": {
67+
backgroundColor: theme.palette.warning.dark,
68+
},
5369
},
5470
space: {
5571
userSelect: "none",

site/src/components/Markdown/Markdown.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ export const Markdown: FC<{ children: string }> = ({ children }) => {
4848
<SyntaxHighlighter
4949
style={darcula}
5050
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- this can be undefined
51-
language={match[1] ?? "language-shell"}
51+
language={match[1].toLowerCase() ?? "language-shell"}
5252
useInlineStyles={false}
5353
// Use inline styles does not work correctly
5454
// https://github.com/react-syntax-highlighter/react-syntax-highlighter/issues/329
5555
codeTagProps={{ style: {} }}
5656
{...props}
5757
>
58-
{String(children).replace(/\n$/, "")}
58+
{String(children)}
5959
</SyntaxHighlighter>
6060
) : (
6161
<code className={styles.codeWithoutLanguage} {...props}>
@@ -135,19 +135,24 @@ const useStyles = makeStyles((theme) => ({
135135
background: theme.palette.background.paperLight,
136136
borderRadius: theme.shape.borderRadius,
137137
padding: theme.spacing(2, 3),
138+
overflowX: "auto",
138139

139140
"& code": {
140141
color: theme.palette.text.secondary,
141142
},
142143

143-
"& .key, & .property": {
144+
"& .key, & .property, & .inserted, .keyword": {
144145
color: colors.turquoise[7],
145146
},
147+
148+
"& .deleted": {
149+
color: theme.palette.error.light,
150+
},
146151
},
147152
},
148153

149154
codeWithoutLanguage: {
150-
padding: theme.spacing(0.5, 1),
155+
padding: theme.spacing(0.125, 0.5),
151156
background: theme.palette.divider,
152157
borderRadius: 4,
153158
color: theme.palette.text.primary,

0 commit comments

Comments
 (0)