Skip to content

Commit 29e9b9e

Browse files
feat(site): Add change version for template admins (coder#6988)
1 parent c12bc39 commit 29e9b9e

File tree

10 files changed

+311
-6
lines changed

10 files changed

+311
-6
lines changed

site/src/api/api.ts

+37-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import axios, { AxiosRequestHeaders } from "axios"
1+
import axios from "axios"
22
import dayjs from "dayjs"
33
import * as Types from "./types"
44
import { DeploymentConfig } from "./types"
@@ -65,7 +65,7 @@ if (token !== null && token.getAttribute("content") !== null) {
6565
}
6666
}
6767

68-
const CONTENT_TYPE_JSON: AxiosRequestHeaders = {
68+
const CONTENT_TYPE_JSON = {
6969
"Content-Type": "application/json",
7070
}
7171

@@ -975,6 +975,41 @@ export class MissingBuildParameters extends Error {
975975
}
976976
}
977977

978+
/** Steps to change the workspace version
979+
* - Get the latest template to access the latest active version
980+
* - Get the current build parameters
981+
* - Get the template parameters
982+
* - Update the build parameters and check if there are missed parameters for the new version
983+
* - If there are missing parameters raise an error
984+
* - Create a build with the version and updated build parameters
985+
*/
986+
export const changeWorkspaceVersion = async (
987+
workspace: TypesGen.Workspace,
988+
templateVersionId: string,
989+
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
990+
): Promise<TypesGen.WorkspaceBuild> => {
991+
const [currentBuildParameters, templateParameters] = await Promise.all([
992+
getWorkspaceBuildParameters(workspace.latest_build.id),
993+
getTemplateVersionRichParameters(templateVersionId),
994+
])
995+
996+
const missingParameters = getMissingParameters(
997+
currentBuildParameters,
998+
newBuildParameters,
999+
templateParameters,
1000+
)
1001+
1002+
if (missingParameters.length > 0) {
1003+
throw new MissingBuildParameters(missingParameters)
1004+
}
1005+
1006+
return postWorkspaceBuild(workspace.id, {
1007+
transition: "start",
1008+
template_version_id: templateVersionId,
1009+
rich_parameter_values: newBuildParameters,
1010+
})
1011+
}
1012+
9781013
/** Steps to update the workspace
9791014
* - Get the latest template to access the latest active version
9801015
* - Get the current build parameters

site/src/components/AvatarData/AvatarData.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const AvatarData: FC<PropsWithChildren<AvatarDataProps>> = ({
3131
>
3232
{avatar}
3333

34-
<Stack spacing={0}>
34+
<Stack spacing={0} className={styles.info}>
3535
<span className={styles.title}>{title}</span>
3636
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
3737
</Stack>
@@ -42,6 +42,11 @@ export const AvatarData: FC<PropsWithChildren<AvatarDataProps>> = ({
4242
const useStyles = makeStyles((theme) => ({
4343
root: {
4444
minHeight: theme.spacing(5), // Make it predictable for the skeleton
45+
width: "100%",
46+
},
47+
48+
info: {
49+
width: "100%",
4550
},
4651

4752
title: {

site/src/components/DropdownButton/ActionCtas.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { makeStyles } from "@material-ui/core/styles"
44
import BlockIcon from "@material-ui/icons/Block"
55
import CloudQueueIcon from "@material-ui/icons/CloudQueue"
66
import SettingsOutlined from "@material-ui/icons/SettingsOutlined"
7+
import HistoryOutlined from "@material-ui/icons/HistoryOutlined"
78
import CropSquareIcon from "@material-ui/icons/CropSquare"
89
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"
910
import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline"
@@ -53,6 +54,23 @@ export const SettingsButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
5354
)
5455
}
5556

57+
export const ChangeVersionButton: FC<
58+
React.PropsWithChildren<WorkspaceAction>
59+
> = ({ handleAction }) => {
60+
const styles = useStyles()
61+
62+
return (
63+
<Button
64+
variant="outlined"
65+
className={styles.actionButton}
66+
startIcon={<HistoryOutlined />}
67+
onClick={handleAction}
68+
>
69+
Change version
70+
</Button>
71+
)
72+
}
73+
5674
export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
5775
handleAction,
5876
}) => {

site/src/components/UserAutocomplete/UserAutocomplete.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
2626
}) => {
2727
const styles = useStyles()
2828
const { t } = useTranslation("common")
29-
3029
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false)
3130
const [searchState, sendSearch] = useMachine(searchUserMachine)
3231
const { searchResults } = searchState.context

site/src/components/Workspace/Workspace.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@ export interface WorkspaceProps {
4747
handleUpdate: () => void
4848
handleCancel: () => void
4949
handleSettings: () => void
50+
handleChangeVersion: () => void
5051
isUpdating: boolean
5152
workspace: TypesGen.Workspace
5253
resources?: TypesGen.WorkspaceResource[]
5354
builds?: TypesGen.WorkspaceBuild[]
5455
canUpdateWorkspace: boolean
5556
canUpdateTemplate: boolean
57+
canChangeVersions: boolean
5658
hideSSHButton?: boolean
5759
hideVSCodeDesktopButton?: boolean
5860
workspaceErrors: Partial<Record<WorkspaceErrors, Error | unknown>>
@@ -76,12 +78,14 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
7678
handleUpdate,
7779
handleCancel,
7880
handleSettings,
81+
handleChangeVersion,
7982
workspace,
8083
isUpdating,
8184
resources,
8285
builds,
8386
canUpdateWorkspace,
8487
canUpdateTemplate,
88+
canChangeVersions,
8589
workspaceErrors,
8690
hideSSHButton,
8791
hideVSCodeDesktopButton,
@@ -142,6 +146,8 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
142146
handleUpdate={handleUpdate}
143147
handleCancel={handleCancel}
144148
handleSettings={handleSettings}
149+
handleChangeVersion={handleChangeVersion}
150+
canChangeVersions={canChangeVersions}
145151
isUpdating={isUpdating}
146152
/>
147153
</Stack>

site/src/components/WorkspaceActions/WorkspaceActions.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"
44
import { WorkspaceStatus } from "../../api/typesGenerated"
55
import {
66
ActionLoadingButton,
7+
ChangeVersionButton,
78
DeleteButton,
89
DisabledButton,
910
SettingsButton,
@@ -22,8 +23,10 @@ export interface WorkspaceActionsProps {
2223
handleUpdate: () => void
2324
handleCancel: () => void
2425
handleSettings: () => void
26+
handleChangeVersion: () => void
2527
isUpdating: boolean
2628
children?: ReactNode
29+
canChangeVersions: boolean
2730
}
2831

2932
export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
@@ -35,7 +38,9 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
3538
handleUpdate,
3639
handleCancel,
3740
handleSettings,
41+
handleChangeVersion,
3842
isUpdating,
43+
canChangeVersions,
3944
}) => {
4045
const { t } = useTranslation("workspacePage")
4146
const { canCancel, canAcceptJobs, actions } = buttonAbilities(workspaceStatus)
@@ -50,6 +55,11 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
5055
[ButtonTypesEnum.settings]: (
5156
<SettingsButton handleAction={handleSettings} />
5257
),
58+
[ButtonTypesEnum.changeVersion]: canChangeVersions ? (
59+
<ChangeVersionButton handleAction={handleChangeVersion} />
60+
) : (
61+
<></>
62+
),
5363
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
5464
[ButtonTypesEnum.starting]: (
5565
<ActionLoadingButton label={t("actionButton.starting")} />

site/src/components/WorkspaceActions/constants.ts

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export enum ButtonTypesEnum {
1212
update = "update",
1313
updating = "updating",
1414
settings = "settings",
15+
changeVersion = "changeVersion",
1516
// disabled buttons
1617
canceling = "canceling",
1718
deleted = "deleted",
@@ -44,6 +45,7 @@ const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
4445
actions: [
4546
ButtonTypesEnum.stop,
4647
ButtonTypesEnum.settings,
48+
ButtonTypesEnum.changeVersion,
4749
ButtonTypesEnum.delete,
4850
],
4951
canCancel: false,
@@ -58,6 +60,7 @@ const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
5860
actions: [
5961
ButtonTypesEnum.start,
6062
ButtonTypesEnum.settings,
63+
ButtonTypesEnum.changeVersion,
6164
ButtonTypesEnum.delete,
6265
],
6366
canCancel: false,
@@ -68,6 +71,7 @@ const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
6871
ButtonTypesEnum.start,
6972
ButtonTypesEnum.stop,
7073
ButtonTypesEnum.settings,
74+
ButtonTypesEnum.changeVersion,
7175
ButtonTypesEnum.delete,
7276
],
7377
canCancel: false,
@@ -79,6 +83,7 @@ const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
7983
ButtonTypesEnum.start,
8084
ButtonTypesEnum.stop,
8185
ButtonTypesEnum.settings,
86+
ButtonTypesEnum.changeVersion,
8287
ButtonTypesEnum.delete,
8388
],
8489
canCancel: false,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { DialogProps } from "components/Dialogs/Dialog"
2+
import { FC, useRef, useState } from "react"
3+
import { FormFields } from "components/Form/Form"
4+
import TextField from "@material-ui/core/TextField"
5+
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
6+
import { Stack } from "components/Stack/Stack"
7+
import { Template, TemplateVersion } from "api/typesGenerated"
8+
import { Loader } from "components/Loader/Loader"
9+
import Autocomplete from "@material-ui/lab/Autocomplete"
10+
import { createDayString } from "util/createDayString"
11+
import { AvatarData } from "components/AvatarData/AvatarData"
12+
import { Pill } from "components/Pill/Pill"
13+
import { Avatar } from "components/Avatar/Avatar"
14+
import CircularProgress from "@material-ui/core/CircularProgress"
15+
16+
export type ChangeVersionDialogProps = DialogProps & {
17+
template: Template | undefined
18+
templateVersions: TemplateVersion[] | undefined
19+
defaultTemplateVersion: TemplateVersion | undefined
20+
onClose: () => void
21+
onConfirm: (templateVersion: TemplateVersion) => void
22+
}
23+
24+
export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
25+
onConfirm,
26+
onClose,
27+
template,
28+
templateVersions,
29+
defaultTemplateVersion,
30+
...dialogProps
31+
}) => {
32+
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false)
33+
const selectedTemplateVersion = useRef<TemplateVersion | undefined>()
34+
35+
return (
36+
<ConfirmDialog
37+
{...dialogProps}
38+
onClose={onClose}
39+
onConfirm={() => {
40+
if (selectedTemplateVersion.current) {
41+
onConfirm(selectedTemplateVersion.current)
42+
}
43+
}}
44+
hideCancel={false}
45+
type="success"
46+
cancelText="Cancel"
47+
confirmText="Change"
48+
title="Change version"
49+
description={
50+
<Stack>
51+
<p>You are about to change the version of this workspace.</p>
52+
{templateVersions ? (
53+
<FormFields>
54+
<Autocomplete
55+
disableClearable
56+
options={templateVersions}
57+
defaultValue={defaultTemplateVersion}
58+
id="template-version-autocomplete"
59+
open={isAutocompleteOpen}
60+
onChange={(_, newTemplateVersion) => {
61+
selectedTemplateVersion.current =
62+
newTemplateVersion ?? undefined
63+
}}
64+
onOpen={() => {
65+
setIsAutocompleteOpen(true)
66+
}}
67+
onClose={() => {
68+
setIsAutocompleteOpen(false)
69+
}}
70+
getOptionSelected={(
71+
option: TemplateVersion,
72+
value: TemplateVersion,
73+
) => option.id === value.id}
74+
getOptionLabel={(option) => option.name}
75+
renderOption={(option: TemplateVersion) => (
76+
<AvatarData
77+
avatar={
78+
<Avatar src={option.created_by.avatar_url}>
79+
{option.name}
80+
</Avatar>
81+
}
82+
title={
83+
<Stack
84+
direction="row"
85+
justifyContent="space-between"
86+
style={{ width: "100%" }}
87+
>
88+
{option.name}
89+
{template?.active_version_id === option.id && (
90+
<Pill text="Active" type="success" />
91+
)}
92+
</Stack>
93+
}
94+
subtitle={createDayString(option.created_at)}
95+
/>
96+
)}
97+
renderInput={(params) => (
98+
<TextField
99+
{...params}
100+
fullWidth
101+
variant="outlined"
102+
placeholder="Template version name"
103+
InputProps={{
104+
...params.InputProps,
105+
endAdornment: (
106+
<>
107+
{!templateVersions ? (
108+
<CircularProgress size={16} />
109+
) : null}
110+
{params.InputProps.endAdornment}
111+
</>
112+
),
113+
}}
114+
/>
115+
)}
116+
/>
117+
</FormFields>
118+
) : (
119+
<Loader />
120+
)}
121+
</Stack>
122+
}
123+
/>
124+
)
125+
}

0 commit comments

Comments
 (0)