-
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 12 commits
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
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,19 @@ | ||
import { API } from "api/api"; | ||
import type { AuthorizationRequest } from "api/typesGenerated"; | ||
import type { | ||
AuthorizationRequest, | ||
AuthorizationResponse, | ||
} from "api/typesGenerated"; | ||
|
||
const AUTHORIZATION_KEY = "authorization"; | ||
|
||
export const getAuthorizationKey = (req: AuthorizationRequest) => | ||
[AUTHORIZATION_KEY, req] as const; | ||
|
||
export const checkAuthorization = (req: AuthorizationRequest) => { | ||
export const checkAuthorization = <TResponse extends AuthorizationResponse>( | ||
req: AuthorizationRequest, | ||
) => { | ||
return { | ||
queryKey: getAuthorizationKey(req), | ||
queryFn: () => API.checkAuthorization(req), | ||
queryFn: () => API.checkAuthorization<TResponse>(req), | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ export const deploymentConfig = () => { | |
return { | ||
queryKey: deploymentConfigQueryKey, | ||
queryFn: API.getDeploymentConfig, | ||
staleTime: Number.POSITIVE_INFINITY, | ||
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. Just making sure: are we guaranteed that the config won't ever change after the initial request from the server? I know we already disable a lot of the RQ background refetch functionality, so I'm wondering if there's any harm in letting the data be marked as stale, since I'd think the data would still be refetched relatively infrequently 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. Yeap! To change these deployment configs even coderd would need to be restarted 😄 |
||
}; | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,12 +11,17 @@ import type { | |
WorkspacesResponse, | ||
} from "api/typesGenerated"; | ||
import type { Dayjs } from "dayjs"; | ||
import { | ||
type WorkspacePermissions, | ||
workspaceChecks, | ||
} from "modules/workspaces/permissions"; | ||
import type { ConnectionStatus } from "pages/TerminalPage/types"; | ||
import type { | ||
QueryClient, | ||
QueryOptions, | ||
UseMutationOptions, | ||
} from "react-query"; | ||
import { checkAuthorization } from "./authCheck"; | ||
import { disabledRefetchOptions } from "./util"; | ||
import { workspaceBuildsKey } from "./workspaceBuilds"; | ||
|
||
|
@@ -390,3 +395,14 @@ export const workspaceUsage = (options: WorkspaceUsageOptions) => { | |
refetchIntervalInBackground: true, | ||
}; | ||
}; | ||
|
||
export const workspacePermissions = (workspace?: Workspace) => { | ||
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. Could we add an explicit return type here? There's enough going on that I think it'd help communicate what this is supposed to be 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 see too much value on that since it is dynamic and other parts of the application would throw an error. But, if it is a must for you, I can do it for sure. |
||
return { | ||
...checkAuthorization<WorkspacePermissions>({ | ||
checks: workspace ? workspaceChecks(workspace) : {}, | ||
}), | ||
queryKey: ["workspaces", workspace?.id, "permissions"], | ||
enabled: !!workspace, | ||
staleTime: Number.POSITIVE_INFINITY, | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
import { templateVersionsQueryKey } from "api/queries/templates"; | ||
import { | ||
MockTemplateVersion, | ||
MockTemplateVersionWithMarkdownMessage, | ||
MockWorkspace, | ||
} from "testHelpers/entities"; | ||
import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; | ||
|
||
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, | ||
}, | ||
}, | ||
}; | ||
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. 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 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. Sure, just be aware it is not in the scope of this PR. It was just a renaming. |
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,43 @@ 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), | ||
select: (data) => data.reverse(), | ||
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 bug: we don't want the mutate any data that we select. Normally I'd recommend that we use |
||
}); | ||
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 +63,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 +114,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 +132,7 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({ | |
...params.InputProps, | ||
endAdornment: ( | ||
<> | ||
{!templateVersions ? ( | ||
<CircularProgress size={16} /> | ||
) : null} | ||
{!versions && <CircularProgress size={16} />} | ||
{params.InputProps.endAdornment} | ||
</> | ||
), | ||
|
@@ -144,16 +143,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 |
---|---|---|
|
@@ -221,8 +221,9 @@ const DownloadingItem: FC<DownloadingItemProps> = ({ file, giveUpTimeMs }) => { | |
function humanBlobSize(size: number) { | ||
const BLOB_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const; | ||
let i = 0; | ||
while (size > 1024 && i < BLOB_SIZE_UNITS.length) { | ||
size /= 1024; | ||
let sizeIterator = size; | ||
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. Nice catch 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'm not the biggest fan of changing parameters either, but in this case, this would never be a bug, since numbers are primitive values 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 have to say it was a Biome catch haha. |
||
while (sizeIterator > 1024 && i < BLOB_SIZE_UNITS.length) { | ||
sizeIterator /= 1024; | ||
Comment on lines
+224
to
+226
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. There's nothing wrong with the changes you made here, but if we're dealing with numbers that are big enough that we're expecting to need to divide it by 1024 multiple times, do we want to update anything to involve 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. It is only to humanize the number, so it is a fixed value of 1024 |
||
i++; | ||
} | ||
|
||
|
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.
I'm concerned about why we have a generic for a function that checks anything auth-based. Does the server really give back multiple return types based on what parameters we give it?
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.
Also, I'm a little concerned that with how the function is set up right now, we force anyone calling this function to pass in the correct type parameter. To some degree that's okay, since we have such a specific type constraint on here, I have to wonder if this is asking for trouble, not just as far as complexity, but also in terms of some other employee passing the wrong type in a few months from now
Two things come to mind:
params
so that we can infer the return type automatically. With less need to pass in types manually, there's less room for things to go wrongTResponse extends TypesGen.AuthorizationResponse = TypesGen.AuthorizationResponse
. That way, most users get back the baseAuthorizationResponse
type by default (which I'd imagine would be okay 99% of the time, since we haven't needed this constraint until now), and then if someone needs something more specific, they have the ability to pass in the type, and override the default return valueThere 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.
That is not possible right now since the response is dynamic and depends on the request parameters. However, I do agree it would be better to have something more predictable to avoid these kinda of errors.