-
Notifications
You must be signed in to change notification settings - Fork 943
feat: add inline actions into workspaces table #17636
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 7 commits
0ba8372
25d986e
50d9d74
a20a188
6f054b9
fcc5a27
10dcd68
766463d
c14442c
281cf1a
71c6369
3502a22
7d2b8b3
61dc162
ddcb1e9
53c4332
86337e2
821f9d0
b70e28f
b790963
0987ed1
c1d3046
f797506
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 |
---|---|---|
|
@@ -9,7 +9,7 @@ export const usePagination = ({ | |
const [searchParams, setSearchParams] = searchParamsResult; | ||
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1; | ||
const limit = DEFAULT_RECORDS_PER_PAGE; | ||
const offset = page <= 0 ? 0 : (page - 1) * limit; | ||
const offset = calcOffset(page, limit); | ||
|
||
const goToPage = (page: number) => { | ||
searchParams.set("page", page.toString()); | ||
|
@@ -23,3 +23,7 @@ export const usePagination = ({ | |
offset, | ||
}; | ||
}; | ||
|
||
export const calcOffset = (page: number, limit: number) => { | ||
return page <= 0 ? 0 : (page - 1) * limit; | ||
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 can be turned into 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. Also, do we want to put this function in this file? It's being imported by a lot of files that don't care about the hook 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. I just didn't want to create a module |
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,11 @@ const actionTypes = [ | |
|
||
export type ActionType = (typeof actionTypes)[number]; | ||
|
||
type ActionPermissions = { | ||
canDebug: boolean; | ||
isOwner: boolean; | ||
}; | ||
|
||
type WorkspaceAbilities = { | ||
actions: readonly ActionType[]; | ||
canCancel: boolean; | ||
|
@@ -42,8 +47,11 @@ type WorkspaceAbilities = { | |
|
||
export const abilitiesByWorkspaceStatus = ( | ||
workspace: Workspace, | ||
canDebug: boolean, | ||
permissions: ActionPermissions, | ||
): WorkspaceAbilities => { | ||
const hasPermissionToCancel = | ||
workspace.template_allow_user_cancel_workspace_jobs || permissions.isOwner; | ||
|
||
if (workspace.dormant_at) { | ||
return { | ||
actions: ["activate"], | ||
|
@@ -58,7 +66,7 @@ export const abilitiesByWorkspaceStatus = ( | |
case "starting": { | ||
return { | ||
actions: ["starting"], | ||
canCancel: true, | ||
canCancel: true && hasPermissionToCancel, | ||
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. The |
||
canAcceptJobs: false, | ||
}; | ||
} | ||
|
@@ -83,7 +91,7 @@ export const abilitiesByWorkspaceStatus = ( | |
case "stopping": { | ||
return { | ||
actions: ["stopping"], | ||
canCancel: true, | ||
canCancel: true && hasPermissionToCancel, | ||
canAcceptJobs: false, | ||
}; | ||
} | ||
|
@@ -115,7 +123,7 @@ export const abilitiesByWorkspaceStatus = ( | |
case "failed": { | ||
const actions: ActionType[] = ["retry"]; | ||
|
||
if (canDebug) { | ||
if (permissions.canDebug) { | ||
actions.push("debug"); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import { MissingBuildParameters } from "api/api"; | ||
import { updateWorkspace } from "api/queries/workspaces"; | ||
import type { | ||
TemplateVersion, | ||
Workspace, | ||
WorkspaceBuild, | ||
WorkspaceBuildParameter, | ||
} from "api/typesGenerated"; | ||
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; | ||
import { MemoizedInlineMarkdown } from "components/Markdown/Markdown"; | ||
import { UpdateBuild F438 ParametersDialog } from "pages/WorkspacePage/UpdateBuildParametersDialog"; | ||
import { type FC, useState } from "react"; | ||
import { useMutation, useQueryClient } from "react-query"; | ||
|
||
type UseWorkspaceUpdateOptions = { | ||
workspace: Workspace; | ||
latestVersion: TemplateVersion | undefined; | ||
onSuccess?: (build: WorkspaceBuild) => void; | ||
onError?: (error: unknown) => void; | ||
}; | ||
|
||
type UseWorkspaceUpdateResult = { | ||
update: ( | ||
hasConfirmed?: boolean, | ||
buildParameters?: WorkspaceBuildParameter[], | ||
) => void; | ||
isUpdating: boolean; | ||
dialogs: { | ||
updateConfirmation: UpdateConfirmationDialogProps; | ||
missingBuildParameters: MissingBuildParametersDialogProps; | ||
}; | ||
}; | ||
|
||
export const useWorkspaceUpdate = ({ | ||
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. Actually, now that I think about it, do we need to export the custom hook? We could keep it as an implementation detail, but couldn't we update 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 you show me an example? Or a draft the interface/usage of it? |
||
workspace, | ||
latestVersion, | ||
onSuccess, | ||
onError, | ||
}: UseWorkspaceUpdateOptions): UseWorkspaceUpdateResult => { | ||
const queryClient = useQueryClient(); | ||
const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false); | ||
|
||
const updateWorkspaceOptions = updateWorkspace(workspace, queryClient); | ||
const updateWorkspaceMutation = useMutation({ | ||
...updateWorkspaceOptions, | ||
onSuccess: (build: WorkspaceBuild) => { | ||
updateWorkspaceOptions.onSuccess(build); | ||
onSuccess?.(build); | ||
}, | ||
onError, | ||
}); | ||
|
||
const update = ( | ||
hasConfirmed = false, | ||
buildParameters: WorkspaceBuildParameter[] = [], | ||
) => { | ||
if (!hasConfirmed) { | ||
setIsConfirmingUpdate(true); | ||
return; | ||
} | ||
|
||
updateWorkspaceMutation.mutate(buildParameters); | ||
setIsConfirmingUpdate(false); | ||
}; | ||
|
||
return { | ||
update, | ||
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 opposed to having a general function that handles both the confirmed case and the non-confirmed case, but I'd prefer for that to be kept as an implementation detail. I feel like there's a risk of accidentally calling I think it'd be better if we did something like this: update: () => update() This also makes sure that the types can't lie to us 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 see... Maybe in this case, we could have two functions |
||
isUpdating: updateWorkspaceMutation.isLoading, | ||
dialogs: { | ||
updateConfirmation: { | ||
open: isConfirmingUpdate, | ||
onClose: () => setIsConfirmingUpdate(false), | ||
onConfirm: () => update(true), | ||
latestVersion, | ||
}, | ||
missingBuildParameters: { | ||
error: updateWorkspaceMutation.error, | ||
onClose: () => { | ||
updateWorkspaceMutation.reset(); | ||
}, | ||
onUpdate: (buildParameters: WorkspaceBuildParameter[]) => { | ||
if (updateWorkspaceMutation.error instanceof MissingBuildParameters) { | ||
update(true, buildParameters); | ||
} | ||
}, | ||
}, | ||
}, | ||
}; | ||
}; | ||
|
||
type WorkspaceUpdateDialogsProps = { | ||
updateConfirmation: UpdateConfirmationDialogProps; | ||
missingBuildParameters: MissingBuildParametersDialogProps; | ||
}; | ||
|
||
export const WorkspaceUpdateDialogs: FC<WorkspaceUpdateDialogsProps> = ({ | ||
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. Because this file is exporting components that aren't simple pass-through components like context providers, I feel like it's probably better to rename the file to put more emphasis on the components over the hook (could probably name it WorkspaceUpdateDialogs?). The hook just feels like an implementation detail 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. Especially since this isn't the first 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. Makes total sense 👍 |
||
updateConfirmation, | ||
missingBuildParameters, | ||
}) => { | ||
return ( | ||
<> | ||
<UpdateConfirmationDialog {...updateConfirmation} /> | ||
<MissingBuildParametersDialog {...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'm also wondering: do we want to make sure that both dialogs are mounted together? I feel like passing two separate, custom objects as props is a bit much, but I don't know if we get away with splitting them up as separate exports 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 was wondering about that too 🤔. Since they are suppose to always working together I thought that would be better to enforce this by exporting only one component with the two dialogs, but I'm open for suggestions. |
||
); | ||
}; | ||
|
||
type UpdateConfirmationDialogProps = { | ||
open: boolean; | ||
onClose: () => void; | ||
onConfirm: () => void; | ||
latestVersion?: TemplateVersion; | ||
}; | ||
|
||
const UpdateConfirmationDialog: FC<UpdateConfirmationDialogProps> = ({ | ||
latestVersion, | ||
...dialogProps | ||
}) => { | ||
return ( | ||
<ConfirmDialog | ||
{...dialogProps} | ||
hideCancel={false} | ||
title="Update workspace?" | ||
confirmText="Update" | ||
description={ | ||
<div className="flex flex-col gap-2"> | ||
<p> | ||
Updating your workspace will start the workspace on the latest | ||
template version. This can{" "} | ||
<strong>delete non-persistent data</strong>. | ||
</p> | ||
{latestVersion?.message && ( | ||
<MemoizedInlineMarkdown allowedElements={["ol", "ul", "li"]}> | ||
{latestVersion.message} | ||
</MemoizedInlineMarkdown> | ||
)} | ||
</div> | ||
} | ||
/> | ||
); | ||
}; | ||
|
||
type MissingBuildParametersDialogProps = { | ||
error: unknown; | ||
onClose: () => void; | ||
onUpdate: (buildParameters: WorkspaceBuildParameter[]) => void; | ||
}; | ||
|
||
const MissingBuildParametersDialog: FC<MissingBuildParametersDialogProps> = ({ | ||
error, | ||
...dialogProps | ||
}) => { | ||
return ( | ||
<UpdateBuildParametersDialog | ||
missedParameters={ | ||
error instanceof MissingBuildParameters ? error.parameters : [] | ||
} | ||
open={error instanceof MissingBuildParameters} | ||
{...dialogProps} | ||
/> | ||
); | ||
}; |
Uh oh!
There was an error while loading. Please reload this page.