-
Notifications
You must be signed in to change notification settings - Fork 894
feat: add extra workspace actions in the workspaces table #17775
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
Changes from 1 commit
496473f
22759e3
2f3cce0
f7e9ba9
a3c4908
d5ae7e1
0c65d4f
3b5f20b
207094a
c79952b
be967dd
139548c
84b65a8
96a6ffd
62413e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
import { | ||
MockTemplate, | ||
MockTemplateVersion, | ||
MockTemplateVersionWithMarkdownMessage, | ||
MockWorkspace, | ||
} from "testHelpers/entities"; | ||
import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; | ||
import { templateVersionsQueryKey } from "api/queries/templates"; | ||
|
||
const noMessage = { | ||
...MockTemplateVersion, | ||
id: "no-message", | ||
message: "", | ||
}; | ||
|
||
const meta: Meta<typeof ChangeWorkspaceVersionDialog> = { | ||
title: "modules/workspaces/ChangeWorkspaceVersionDialog", | ||
component: ChangeWorkspaceVersionDialog, | ||
args: { | ||
open: true, | ||
workspace: MockWorkspace, | ||
}, | ||
parameters: { | ||
queries: [ | ||
{ | ||
key: templateVersionsQueryKey(MockWorkspace.template_id), | ||
data: [ | ||
MockTemplateVersion, | ||
MockTemplateVersionWithMarkdownMessage, | ||
noMessage, | ||
], | ||
}, | ||
], | ||
}, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof ChangeWorkspaceVersionDialog>; | ||
|
||
export const NoVersionSelected: Story = {}; | ||
|
||
export const NoMessage: Story = { | ||
args: { | ||
workspace: { | ||
...MockWorkspace, | ||
template_active_version_id: noMessage.id, | ||
}, | ||
}, | ||
}; | ||
|
||
export const ShortMessage: Story = { | ||
args: { | ||
workspace: { | ||
...MockWorkspace, | ||
template_active_version_id: MockTemplateVersion.id, | ||
}, | ||
}, | ||
}; | ||
|
||
export const LongMessage: Story = { | ||
args: { | ||
workspace: { | ||
...MockWorkspace, | ||
template_active_version_id: MockTemplateVersionWithMarkdownMessage.id, | ||
}, | ||
}, | ||
}; | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,8 @@ import AlertTitle from "@mui/material/AlertTitle"; | |
import Autocomplete from "@mui/material/Autocomplete"; | ||
import CircularProgress from "@mui/material/CircularProgress"; | ||
import TextField from "@mui/material/TextField"; | ||
import type { Template, TemplateVersion } from "api/typesGenerated"; | ||
import { templateVersions } from "api/queries/templates"; | ||
import type { TemplateVersion, Workspace } from "api/typesGenerated"; | ||
import { Alert } from "components/Alert/Alert"; | ||
import { Avatar } from "components/Avatar/Avatar"; | ||
import { AvatarData } from "components/Avatar/AvatarData"; | ||
|
@@ -15,41 +16,40 @@ import { Pill } from "components/Pill/Pill"; | |
import { Stack } from "components/Stack/Stack"; | ||
import { InfoIcon } from "lucide-react"; | ||
import { TemplateUpdateMessage } from "modules/templates/TemplateUpdateMessage"; | ||
import { type FC, useRef, useState } from "react"; | ||
import { type FC, useEffect, useState } from "react"; | ||
import { useQuery } from "react-query"; | ||
import { createDayString } from "utils/createDayString"; | ||
|
||
export type ChangeVersionDialogProps = DialogProps & { | ||
template: Template | undefined; | ||
templateVersions: TemplateVersion[] | undefined; | ||
defaultTemplateVersion: TemplateVersion | undefined; | ||
export type ChangeWorkspaceVersionDialogProps = DialogProps & { | ||
workspace: Workspace; | ||
onClose: () => void; | ||
onConfirm: (templateVersion: TemplateVersion) => void; | ||
onConfirm: (version: TemplateVersion) => void; | ||
}; | ||
|
||
export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({ | ||
onConfirm, | ||
onClose, | ||
template, | ||
templateVersions, | ||
defaultTemplateVersion, | ||
...dialogProps | ||
}) => { | ||
export const ChangeWorkspaceVersionDialog: FC< | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's worth blocking on this, but I think it's worth thinking about for the future: do we want to limit the number of versions we show by default? Right now, we're loading everything, which makes the UI chug for a second, and I think that realistically, most users aren't going to be switching to anything that's more than 10 versions older than the most recent one We have a lot of versions from the past two years, and I could see some customers having more than us, just because their companies are so much bigger There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I totally agree with you, but I have to say it is not on the scope for this PR. Feel free to open an issue tho |
||
ChangeWorkspaceVersionDialogProps | ||
> = ({ workspace, onClose, onConfirm, ...dialogProps }) => { | ||
const { data: versions } = useQuery(templateVersions(workspace.template_id)); | ||
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false); | ||
const selectedTemplateVersion = useRef<TemplateVersion | undefined>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oof, yeah, good call on getting rid of the ref, since reading from that isn't render-safe |
||
defaultTemplateVersion, | ||
const activeVersion = versions?.find( | ||
(v) => workspace.template_active_version_id === v.id, | ||
); | ||
const version = selectedTemplateVersion.current; | ||
const validTemplateVersions = templateVersions?.filter((version) => { | ||
return version.job.status === "succeeded"; | ||
}); | ||
const [selectedVersion, setSelectedVersion] = useState<TemplateVersion>(); | ||
const validVersions = versions?.filter((v) => v.job.status === "succeeded"); | ||
|
||
useEffect(() => { | ||
if (activeVersion) { | ||
setSelectedVersion(activeVersion); | ||
} | ||
}, [activeVersion]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a user input race condition. What happens if:
I feel like it'd actually be better if we got rid of this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If anything, I feel like maybe we could change things so that if there's a new version, that makes us display a message somewhere. That way, their selection stays the same, but the user knows that they can go back to the dropdown and try a new option that's there There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch! |
||
|
||
return ( | ||
<ConfirmDialog | ||
{...dialogProps} | ||
onClose={onClose} | ||
onConfirm={() => { | ||
if (selectedTemplateVersion.current) { | ||
onConfirm(selectedTemplateVersion.current); | ||
if (selectedVersion) { | ||
onConfirm(selectedVersion); | ||
} | ||
}} | ||
hideCancel={false} | ||
|
@@ -60,18 +60,17 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({ | |
description={ | ||
<Stack> | ||
<p>You are about to change the version of this workspace.</p> | ||
{validTemplateVersions ? ( | ||
{validVersions ? ( | ||
<> | ||
<FormFields> | ||
<Autocomplete | ||
disableClearable | ||
options={validTemplateVersions} | ||
defaultValue={defaultTemplateVersion} | ||
options={validVersions} | ||
defaultValue={activeVersion} | ||
id="template-version-autocomplete" | ||
open={isAutocompleteOpen} | ||
onChange={(_, newTemplateVersion) => { | ||
selectedTemplateVersion.current = | ||
newTemplateVersion ?? undefined; | ||
setSelectedVersion(newTemplateVersion); | ||
}} | ||
onOpen={() => { | ||
setIsAutocompleteOpen(true); | ||
|
@@ -112,9 +111,8 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({ | |
/> | ||
)} | ||
</Stack> | ||
{template?.active_version_id === option.id && ( | ||
<Pill type="success">Active</Pill> | ||
)} | ||
{workspace.template_active_version_id === | ||
option.id && <Pill type="success">Active</Pill>} | ||
</Stack> | ||
} | ||
subtitle={createDayString(option.created_at)} | ||
|
@@ -131,9 +129,7 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({ | |
...params.InputProps, | ||
endAdornment: ( | ||
<> | ||
{!templateVersions ? ( | ||
<CircularProgress size={16} /> | ||
) : null} | ||
{!versions && <CircularProgress size={16} />} | ||
{params.InputProps.endAdornment} | ||
</> | ||
), | ||
|
@@ -144,16 +140,16 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({ | |
)} | ||
/> | ||
</FormFields> | ||
{version && ( | ||
{selectedVersion && ( | ||
<> | ||
{version.message && ( | ||
{selectedVersion.message && ( | ||
<TemplateUpdateMessage> | ||
{version.message} | ||
{selectedVersion.message} | ||
</TemplateUpdateMessage> | ||
)} | ||
<Alert severity="info"> | ||
<AlertTitle> | ||
Published by {version.created_by.username} | ||
Published by {selectedVersion.created_by.username} | ||
</AlertTitle> | ||
</Alert> | ||
</> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
import { MissingBuildParameters } from "api/api"; | ||
import { changeVersion } from "api/queries/workspaces"; | ||
import type { Workspace } from "api/typesGenerated"; | ||
import { Button } from "components/Button/Button"; | ||
import { | ||
DropdownMenu, | ||
|
@@ -14,31 +17,42 @@ import { | |
CopyIcon, | ||
DownloadIcon, | ||
} from "lucide-react"; | ||
import { UpdateBuildParametersDialog } from "pages/WorkspacePage/UpdateBuildParametersDialog"; | ||
import { DownloadLogsDialog } from "pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog"; | ||
import { useState, type FC } from "react"; | ||
import { useMutation, useQueryClient } from "react-query"; | ||
import { Link as RouterLink } from "react-router-dom"; | ||
import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; | ||
|
||
type WorkspaceMoreActionsProps = { | ||
workspace: Workspace; | ||
isDuplicationReady: boolean; | ||
disabled?: boolean; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two points of feedback on this prop:
![]()
You have more context than I do, but I was thinking:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Though I guess if we want to make sure the dots always appear for aesthetic consistency, no matter what transitions we have, another option is to make it so that the dropdowns have conditional rendering tied to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be solved not in the component level, but in the data level. What I want to say is, when stopping the workspace, or triggering a build, we should revalidate the workspaces data to represent the correct state of each workspace. I'm going to take a look into that 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lol, update the data is more complex than I thought because of pagination so I will just close the menu when the button becomes disabled. |
||
onDuplicate: () => void; | ||
onDelete: () => void; | ||
onChangeVersion?: () => void; | ||
permissions?: { | ||
changeWorkspaceVersion?: boolean; | ||
}; | ||
onDuplicate: () => void; | ||
onDelete: () => void; | ||
}; | ||
|
||
export const WorkspaceMoreActions: FC<WorkspaceMoreActionsProps> = ({ | ||
workspace, | ||
disabled, | ||
permissions, | ||
isDuplicationReady, | ||
onDuplicate, | ||
onDelete, | ||
onChangeVersion, | ||
permissions, | ||
}) => { | ||
const queryClient = useQueryClient(); | ||
|
||
// Download logs | ||
const [isDownloadDialogOpen, setIsDownloadDialogOpen] = useState(false); | ||
const canChangeVersion = permissions?.changeWorkspaceVersion !== false; | ||
|
||
// Change version | ||
const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); | ||
const changeVersionMutation = useMutation( | ||
changeVersion(workspace, queryClient), | ||
); | ||
|
||
return ( | ||
<> | ||
|
@@ -64,8 +78,12 @@ export const WorkspaceMoreActions: FC<WorkspaceMoreActionsProps> = ({ | |
</RouterLink> | ||
</DropdownMenuItem> | ||
|
||
{onChangeVersion && canChangeVersion && ( | ||
<DropdownMenuItem onClick={onChangeVersion}> | ||
{permissions?.changeWorkspaceVersion && ( | ||
<DropdownMenuItem | ||
onClick={() => { | ||
setChangeVersionDialogOpen(true); | ||
}} | ||
> | ||
<HistoryIcon /> | ||
Change version… | ||
</DropdownMenuItem> | ||
|
@@ -96,6 +114,48 @@ export const WorkspaceMoreActions: FC<WorkspaceMoreActionsProps> = ({ | |
</DropdownMenuItem> | ||
</DropdownMenuContent> | ||
</DropdownMenu> | ||
|
||
<DownloadLogsDialog | ||
workspace={workspace} | ||
open={isDownloadDialogOpen} | ||
onClose={() => setIsDownloadDialogOpen(false)} | ||
/> | ||
|
||
<UpdateBuildParametersDialog | ||
missedParameters={ | ||
isMissingBuildParameters(changeVersionMutation.error) | ||
? changeVersionMutation.error.parameters | ||
: [] | ||
} | ||
open={isMissingBuildParameters(changeVersionMutation.error)} | ||
onClose={() => { | ||
changeVersionMutation.reset(); | ||
}} | ||
onUpdate={(buildParameters) => { | ||
if (isMissingBuildParameters(changeVersionMutation.error)) { | ||
changeVersionMutation.mutate({ | ||
versionId: changeVersionMutation.error.versionId, | ||
buildParameters, | ||
}); | ||
} | ||
}} | ||
/> | ||
|
||
<ChangeWorkspaceVersionDialog | ||
workspace={workspace} | ||
open={changeVersionDialogOpen} | ||
onClose={() => { | ||
setChangeVersionDialogOpen(false); | ||
}} | ||
onConfirm={(version) => { | ||
setChangeVersionDialogOpen(false); | ||
changeVersionMutation.mutate({ versionId: version.id }); | ||
}} | ||
/> | ||
</> | ||
); | ||
}; | ||
|
||
const isMissingBuildParameters = (e: unknown): e is MissingBuildParameters => { | ||
return Boolean(e && e instanceof MissingBuildParameters); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need this function. We can get rid of the
So this can be cleaned up to const isMissingBuildParameters = (e: unknown): e is MissingBuildParameters => {
return e instanceof MissingBuildParameters;
}; And that point, I think we can get away with just using the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is true 👍 |
||
}; |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we rename these stories? When I'm looking at them in the list of stories, it's not clear why they're different, especially when the only input that changed is a template ID
Maybe something like
PlaintextMessage
andMarkdownMessage
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, just be aware it is not in the scope of this PR. It was just a renaming.