Skip to content

feat(site): Add change version for template admins #6988

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 4 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 37 additions & 2 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import axios, { AxiosRequestHeaders } from "axios"
import axios from "axios"
import dayjs from "dayjs"
import * as Types from "./types"
import { DeploymentConfig } from "./types"
Expand Down Expand Up @@ -64,7 +64,7 @@ if (token !== null && token.getAttribute("content") !== null) {
}
}

const CONTENT_TYPE_JSON: AxiosRequestHeaders = {
const CONTENT_TYPE_JSON = {
"Content-Type": "application/json",
}

Expand Down Expand Up @@ -974,6 +974,41 @@ export class MissingBuildParameters extends Error {
}
}

/** Steps to change the workspace version
* - Get the latest template to access the latest active version
* - Get the current build parameters
* - Get the template parameters
* - Update the build parameters and check if there are missed parameters for the new version
* - If there are missing parameters raise an error
* - Create a build with the version and updated build parameters
*/
export const changeWorkspaceVersion = async (
workspace: TypesGen.Workspace,
templateVersionId: string,
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
): Promise<TypesGen.WorkspaceBuild> => {
const [currentBuildParameters, templateParameters] = await Promise.all([
getWorkspaceBuildParameters(workspace.latest_build.id),
getTemplateVersionRichParameters(templateVersionId),
])

const missingParameters = getMissingParameters(
currentBuildParameters,
newBuildParameters,
templateParameters,
)

if (missingParameters.length > 0) {
throw new MissingBuildParameters(missingParameters)
}

return postWorkspaceBuild(workspace.id, {
transition: "start",
template_version_id: templateVersionId,
rich_parameter_values: newBuildParameters,
})
}

/** Steps to update the workspace
* - Get the latest template to access the latest active version
* - Get the current build parameters
Expand Down
7 changes: 6 additions & 1 deletion site/src/components/AvatarData/AvatarData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const AvatarData: FC<PropsWithChildren<AvatarDataProps>> = ({
>
{avatar}

<Stack spacing={0}>
<Stack spacing={0} className={styles.info}>
<span className={styles.title}>{title}</span>
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
</Stack>
Expand All @@ -42,6 +42,11 @@ export const AvatarData: FC<PropsWithChildren<AvatarDataProps>> = ({
const useStyles = makeStyles((theme) => ({
root: {
minHeight: theme.spacing(5), // Make it predictable for the skeleton
width: "100%",
},

info: {
width: "100%",
},

title: {
Expand Down
18 changes: 18 additions & 0 deletions site/src/components/DropdownButton/ActionCtas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { makeStyles } from "@material-ui/core/styles"
import BlockIcon from "@material-ui/icons/Block"
import CloudQueueIcon from "@material-ui/icons/CloudQueue"
import SettingsOutlined from "@material-ui/icons/SettingsOutlined"
import HistoryOutlined from "@material-ui/icons/HistoryOutlined"
import CropSquareIcon from "@material-ui/icons/CropSquare"
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"
import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline"
Expand Down Expand Up @@ -53,6 +54,23 @@ export const SettingsButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
)
}

export const ChangeVersionButton: FC<
React.PropsWithChildren<WorkspaceAction>
> = ({ handleAction }) => {
const styles = useStyles()

return (
<Button
variant="outlined"
className={styles.actionButton}
startIcon={<HistoryOutlined />}
onClick={handleAction}
>
Change version
</Button>
)
}

export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
handleAction,
}) => {
Expand Down
1 change: 0 additions & 1 deletion site/src/components/UserAutocomplete/UserAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
}) => {
const styles = useStyles()
const { t } = useTranslation("common")

const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false)
const [searchState, sendSearch] = useMachine(searchUserMachine)
const { searchResults } = searchState.context
Expand Down
6 changes: 6 additions & 0 deletions site/src/components/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ export interface WorkspaceProps {
handleUpdate: () => void
handleCancel: () => void
handleSettings: () => void
handleChangeVersion: () => void
isUpdating: boolean
workspace: TypesGen.Workspace
resources?: TypesGen.WorkspaceResource[]
builds?: TypesGen.WorkspaceBuild[]
canUpdateWorkspace: boolean
canUpdateTemplate: boolean
canChangeVersions: boolean
hideSSHButton?: boolean
hideVSCodeDesktopButton?: boolean
workspaceErrors: Partial<Record<WorkspaceErrors, Error | unknown>>
Expand All @@ -76,12 +78,14 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
handleUpdate,
handleCancel,
handleSettings,
handleChangeVersion,
workspace,
isUpdating,
resources,
builds,
canUpdateWorkspace,
canUpdateTemplate,
canChangeVersions,
workspaceErrors,
hideSSHButton,
hideVSCodeDesktopButton,
Expand Down Expand Up @@ -142,6 +146,8 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
handleUpdate={handleUpdate}
handleCancel={handleCancel}
handleSettings={handleSettings}
handleChangeVersion={handleChangeVersion}
canChangeVersions={canChangeVersions}
isUpdating={isUpdating}
/>
</Stack>
Expand Down
10 changes: 10 additions & 0 deletions site/src/components/WorkspaceActions/WorkspaceActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"
import { WorkspaceStatus } from "../../api/typesGenerated"
import {
ActionLoadingButton,
ChangeVersionButton,
DeleteButton,
DisabledButton,
SettingsButton,
Expand All @@ -22,8 +23,10 @@ export interface WorkspaceActionsProps {
handleUpdate: () => void
handleCancel: () => void
handleSettings: () => void
handleChangeVersion: () => void
isUpdating: boolean
children?: ReactNode
canChangeVersions: boolean
}

export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
Expand All @@ -35,7 +38,9 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
handleUpdate,
handleCancel,
handleSettings,
handleChangeVersion,
isUpdating,
canChangeVersions,
}) => {
const { t } = useTranslation("workspacePage")
const { canCancel, canAcceptJobs, actions } = buttonAbilities(workspaceStatus)
Expand All @@ -50,6 +55,11 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
[ButtonTypesEnum.settings]: (
<SettingsButton handleAction={handleSettings} />
),
[ButtonTypesEnum.changeVersion]: canChangeVersions ? (
<ChangeVersionButton handleAction={handleChangeVersion} />
) : (
<></>
),
[ButtonTypesEnum.start]: <StartButton handleAction={handleStart} />,
[ButtonTypesEnum.starting]: (
<ActionLoadingButton label={t("actionButton.starting")} />
Expand Down
5 changes: 5 additions & 0 deletions site/src/components/WorkspaceActions/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum ButtonTypesEnum {
update = "update",
updating = "updating",
settings = "settings",
changeVersion = "changeVersion",
// disabled buttons
canceling = "canceling",
deleted = "deleted",
Expand Down Expand Up @@ -44,6 +45,7 @@ const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
actions: [
ButtonTypesEnum.stop,
ButtonTypesEnum.settings,
ButtonTypesEnum.changeVersion,
ButtonTypesEnum.delete,
],
canCancel: false,
Expand All @@ -58,6 +60,7 @@ const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
actions: [
ButtonTypesEnum.start,
ButtonTypesEnum.settings,
ButtonTypesEnum.changeVersion,
ButtonTypesEnum.delete,
],
canCancel: false,
Expand All @@ -68,6 +71,7 @@ const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
ButtonTypesEnum.start,
ButtonTypesEnum.stop,
ButtonTypesEnum.settings,
ButtonTypesEnum.changeVersion,
ButtonTypesEnum.delete,
],
canCancel: false,
Expand All @@ -79,6 +83,7 @@ const statusToAbilities: Record<WorkspaceStatus, WorkspaceAbilities> = {
ButtonTypesEnum.start,
ButtonTypesEnum.stop,
ButtonTypesEnum.settings,
ButtonTypesEnum.changeVersion,
ButtonTypesEnum.delete,
],
canCancel: false,
Expand Down
125 changes: 125 additions & 0 deletions site/src/pages/WorkspacePage/ChangeVersionDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { DialogProps } from "components/Dialogs/Dialog"
import { FC, useRef, useState } from "react"
import { FormFields } from "components/Form/Form"
import TextField from "@material-ui/core/TextField"
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
import { Stack } from "components/Stack/Stack"
import { Template, TemplateVersion } from "api/typesGenerated"
import { Loader } from "components/Loader/Loader"
import Autocomplete from "@material-ui/lab/Autocomplete"
import { createDayString } from "util/createDayString"
import { AvatarData } from "components/AvatarData/AvatarData"
import { Pill } from "components/Pill/Pill"
import { Avatar } from "components/Avatar/Avatar"
import CircularProgress from "@material-ui/core/CircularProgress"

export type ChangeVersionDialogProps = DialogProps & {
template: Template | undefined
templateVersions: TemplateVersion[] | undefined
defaultTemplateVersion: TemplateVersion | undefined
onClose: () => void
onConfirm: (templateVersion: TemplateVersion) => void
}

export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
onConfirm,
onClose,
template,
templateVersions,
defaultTemplateVersion,
...dialogProps
}) => {
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false)
const selectedTemplateVersion = useRef<TemplateVersion | undefined>()

return (
<ConfirmDialog
{...dialogProps}
onClose={onClose}
onConfirm={() => {
if (selectedTemplateVersion.current) {
onConfirm(selectedTemplateVersion.current)
}
}}
hideCancel={false}
type="success"
cancelText="Cancel"
confirmText="Change"
title="Change version"
description={
<Stack>
<p>You are about to change the version of this workspace.</p>
{templateVersions ? (
<FormFields>
<Autocomplete
disableClearable
options={templateVersions}
defaultValue={defaultTemplateVersion}
id="template-version-autocomplete"
open={isAutocompleteOpen}
onChange={(_, newTemplateVersion) => {
selectedTemplateVersion.current =
newTemplateVersion ?? undefined
}}
onOpen={() => {
setIsAutocompleteOpen(true)
}}
onClose={() => {
setIsAutocompleteOpen(false)
}}
getOptionSelected={(
option: TemplateVersion,
value: TemplateVersion,
) => option.id === value.id}
getOptionLabel={(option) => option.name}
renderOption={(option: TemplateVersion) => (
<AvatarData
avatar={
<Avatar src={option.created_by.avatar_url}>
{option.name}
</Avatar>
}
title={
<Stack
direction="row"
justifyContent="space-between"
style={{ width: "100%" }}
>
{option.name}
{template?.active_version_id === option.id && (
<Pill text="Active" type="success" />
)}
</Stack>
}
subtitle={createDayString(option.created_at)}
/>
)}
renderInput={(params) => (
<TextField
{...params}
fullWidth
variant="outlined"
placeholder="Template version name"
InputProps={{
...params.InputProps,
endAdornment: (
<>
{!templateVersions ? (
<CircularProgress size={16} />
) : null}
{params.InputProps.endAdornment}
</>
),
}}
/>
)}
/>
</FormFields>
) : (
<Loader />
)}
</Stack>
}
/>
)
}
Loading