mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add inline actions into workspaces table (#17636)
Related to https://github.com/coder/coder/issues/17311 This PR adds inline actions in the workspaces page. It is a bit different of the [original design](https://www.figma.com/design/OR75XeUI0Z3ksqt1mHsNQw/Workspace-views?node-id=656-3979&m=dev) because I'm splitting the work into three phases that I will explain in more details in the demo. https://github.com/user-attachments/assets/6383375e-ed10-45d1-b5d5-b4421e86d158
This commit is contained in:
@@ -139,13 +139,9 @@ function workspacesKey(config: WorkspacesRequest = {}) {
|
||||
}
|
||||
|
||||
export function workspaces(config: WorkspacesRequest = {}) {
|
||||
// Duplicates some of the work from workspacesKey, but that felt better than
|
||||
// letting invisible properties sneak into the query logic
|
||||
const { q, limit } = config;
|
||||
|
||||
return {
|
||||
queryKey: workspacesKey(config),
|
||||
queryFn: () => API.getWorkspaces({ q, limit }),
|
||||
queryFn: () => API.getWorkspaces(config),
|
||||
} as const satisfies QueryOptions<WorkspacesResponse>;
|
||||
}
|
||||
|
||||
@@ -281,7 +277,10 @@ const updateWorkspaceBuild = async (
|
||||
build.workspace_owner_name,
|
||||
build.workspace_name,
|
||||
);
|
||||
const previousData = queryClient.getQueryData(workspaceKey) as Workspace;
|
||||
const previousData = queryClient.getQueryData<Workspace>(workspaceKey);
|
||||
if (!previousData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the build returned is newer than the previous build that could be
|
||||
// updated from web socket
|
||||
|
||||
@@ -35,7 +35,14 @@ export const DialogActionButtons: FC<DialogActionButtonsProps> = ({
|
||||
return (
|
||||
<>
|
||||
{onCancel && (
|
||||
<Button disabled={confirmLoading} onClick={onCancel} variant="outline">
|
||||
<Button
|
||||
disabled={confirmLoading}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCancel();
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
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 { UpdateBuildParametersDialog } 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: () => void;
|
||||
isUpdating: boolean;
|
||||
dialogs: {
|
||||
updateConfirmation: UpdateConfirmationDialogProps;
|
||||
missingBuildParameters: MissingBuildParametersDialogProps;
|
||||
};
|
||||
};
|
||||
|
||||
export const useWorkspaceUpdate = ({
|
||||
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 = () => {
|
||||
setIsConfirmingUpdate(true);
|
||||
};
|
||||
|
||||
const confirmUpdate = (buildParameters: WorkspaceBuildParameter[] = []) => {
|
||||
updateWorkspaceMutation.mutate(buildParameters);
|
||||
setIsConfirmingUpdate(false);
|
||||
};
|
||||
|
||||
return {
|
||||
update,
|
||||
isUpdating: updateWorkspaceMutation.isLoading,
|
||||
dialogs: {
|
||||
updateConfirmation: {
|
||||
open: isConfirmingUpdate,
|
||||
onClose: () => setIsConfirmingUpdate(false),
|
||||
onConfirm: () => confirmUpdate(),
|
||||
latestVersion,
|
||||
},
|
||||
missingBuildParameters: {
|
||||
error: updateWorkspaceMutation.error,
|
||||
onClose: () => {
|
||||
updateWorkspaceMutation.reset();
|
||||
},
|
||||
onUpdate: (buildParameters: WorkspaceBuildParameter[]) => {
|
||||
if (updateWorkspaceMutation.error instanceof MissingBuildParameters) {
|
||||
confirmUpdate(buildParameters);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type WorkspaceUpdateDialogsProps = {
|
||||
updateConfirmation: UpdateConfirmationDialogProps;
|
||||
missingBuildParameters: MissingBuildParametersDialogProps;
|
||||
};
|
||||
|
||||
export const WorkspaceUpdateDialogs: FC<WorkspaceUpdateDialogsProps> = ({
|
||||
updateConfirmation,
|
||||
missingBuildParameters,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<UpdateConfirmationDialog {...updateConfirmation} />
|
||||
<MissingBuildParametersDialog {...missingBuildParameters} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+12
-4
@@ -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: hasPermissionToCancel,
|
||||
canAcceptJobs: false,
|
||||
};
|
||||
}
|
||||
@@ -83,7 +91,7 @@ export const abilitiesByWorkspaceStatus = (
|
||||
case "stopping": {
|
||||
return {
|
||||
actions: ["stopping"],
|
||||
canCancel: true,
|
||||
canCancel: hasPermissionToCancel,
|
||||
canAcceptJobs: false,
|
||||
};
|
||||
}
|
||||
@@ -115,7 +123,7 @@ export const abilitiesByWorkspaceStatus = (
|
||||
case "failed": {
|
||||
const actions: ActionType[] = ["retry"];
|
||||
|
||||
if (canDebug) {
|
||||
if (permissions.canDebug) {
|
||||
actions.push("debug");
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,5 +1,6 @@
|
||||
import type { Template, Workspace } from "api/typesGenerated";
|
||||
import { compareAsc } from "date-fns";
|
||||
import { calcOffset } from "hooks/usePagination";
|
||||
import { useWorkspacesData } from "pages/WorkspacesPage/data";
|
||||
import type { TemplateScheduleFormValues } from "./formHelpers";
|
||||
|
||||
@@ -9,9 +10,9 @@ export const useWorkspacesToGoDormant = (
|
||||
fromDate: Date,
|
||||
) => {
|
||||
const { data } = useWorkspacesData({
|
||||
page: 0,
|
||||
offset: calcOffset(0, 0),
|
||||
limit: 0,
|
||||
query: `template:${template.name}`,
|
||||
q: `template:${template.name}`,
|
||||
});
|
||||
|
||||
return data?.workspaces?.filter((workspace: Workspace) => {
|
||||
@@ -40,9 +41,9 @@ export const useWorkspacesToBeDeleted = (
|
||||
fromDate: Date,
|
||||
) => {
|
||||
const { data } = useWorkspacesData({
|
||||
page: 0,
|
||||
offset: calcOffset(0, 0),
|
||||
limit: 0,
|
||||
query: `template:${template.name} dormant:true`,
|
||||
q: `template:${template.name} dormant:true`,
|
||||
});
|
||||
return data?.workspaces?.filter((workspace: Workspace) => {
|
||||
if (!workspace.dormant_at || !formValues.time_til_dormant_autodelete_ms) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import type { ProvisionerJobLog } from "api/typesGenerated";
|
||||
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext";
|
||||
import * as Mocks from "testHelpers/entities";
|
||||
import { withDashboardProvider } from "testHelpers/storybook";
|
||||
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
|
||||
import { Workspace } from "./Workspace";
|
||||
import type { WorkspacePermissions } from "./permissions";
|
||||
|
||||
@@ -40,8 +40,10 @@ const meta: Meta<typeof Workspace> = {
|
||||
data: Mocks.MockListeningPortsResponse,
|
||||
},
|
||||
],
|
||||
user: Mocks.MockUser,
|
||||
},
|
||||
decorators: [
|
||||
withAuthProvider,
|
||||
withDashboardProvider,
|
||||
(Story) => (
|
||||
<ProxyContext.Provider
|
||||
|
||||
@@ -53,7 +53,6 @@ export interface WorkspaceProps {
|
||||
buildLogs?: TypesGen.ProvisionerJobLog[];
|
||||
latestVersion?: TypesGen.TemplateVersion;
|
||||
permissions: WorkspacePermissions;
|
||||
isOwner: boolean;
|
||||
timings?: TypesGen.WorkspaceBuildTimings;
|
||||
}
|
||||
|
||||
@@ -86,7 +85,6 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
buildLogs,
|
||||
latestVersion,
|
||||
permissions,
|
||||
isOwner,
|
||||
timings,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -161,7 +159,6 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
isUpdating={isUpdating}
|
||||
isRestarting={isRestarting}
|
||||
canUpdateWorkspace={permissions.updateWorkspace}
|
||||
isOwner={isOwner}
|
||||
template={template}
|
||||
permissions={permissions}
|
||||
latestVersion={latestVersion}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { expect, userEvent, within } from "@storybook/test";
|
||||
import { agentLogsKey, buildLogsKey } from "api/queries/workspaces";
|
||||
import * as Mocks from "testHelpers/entities";
|
||||
import {
|
||||
withAuthProvider,
|
||||
withDashboardProvider,
|
||||
withDesktopViewport,
|
||||
} from "testHelpers/storybook";
|
||||
@@ -14,7 +15,10 @@ const meta: Meta<typeof WorkspaceActions> = {
|
||||
args: {
|
||||
isUpdating: false,
|
||||
},
|
||||
decorators: [withDashboardProvider, withDesktopViewport],
|
||||
decorators: [withDashboardProvider, withDesktopViewport, withAuthProvider],
|
||||
parameters: {
|
||||
user: Mocks.MockUser,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -163,14 +167,15 @@ export const CancelShownForOwner: Story = {
|
||||
...Mocks.MockStartingWorkspace,
|
||||
template_allow_user_cancel_workspace_jobs: false,
|
||||
},
|
||||
isOwner: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const CancelShownForUser: Story = {
|
||||
args: {
|
||||
workspace: Mocks.MockStartingWorkspace,
|
||||
isOwner: false,
|
||||
},
|
||||
parameters: {
|
||||
user: Mocks.MockUser2,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -180,7 +185,9 @@ export const CancelHiddenForUser: Story = {
|
||||
...Mocks.MockStartingWorkspace,
|
||||
template_allow_user_cancel_workspace_jobs: false,
|
||||
},
|
||||
isOwner: false,
|
||||
},
|
||||
parameters: {
|
||||
user: Mocks.MockUser2,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,12 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "components/DropdownMenu/DropdownMenu";
|
||||
import { useAuthenticated } from "hooks/useAuthenticated";
|
||||
import { EllipsisVertical } from "lucide-react";
|
||||
import {
|
||||
type ActionType,
|
||||
abilitiesByWorkspaceStatus,
|
||||
} from "modules/workspaces/actions";
|
||||
import { useWorkspaceDuplication } from "pages/CreateWorkspacePage/useWorkspaceDuplication";
|
||||
import { type FC, Fragment, type ReactNode, useState } from "react";
|
||||
import { mustUpdateWorkspace } from "utils/workspace";
|
||||
@@ -31,7 +36,6 @@ import {
|
||||
import { DebugButton } from "./DebugButton";
|
||||
import { DownloadLogsDialog } from "./DownloadLogsDialog";
|
||||
import { RetryButton } from "./RetryButton";
|
||||
import { type ActionType, abilitiesByWorkspaceStatus } from "./constants";
|
||||
|
||||
export interface WorkspaceActionsProps {
|
||||
workspace: Workspace;
|
||||
@@ -52,7 +56,6 @@ export interface WorkspaceActionsProps {
|
||||
children?: ReactNode;
|
||||
canChangeVersions: boolean;
|
||||
canDebug: boolean;
|
||||
isOwner: boolean;
|
||||
}
|
||||
|
||||
export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
@@ -73,20 +76,19 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
isRestarting,
|
||||
canChangeVersions,
|
||||
canDebug,
|
||||
isOwner,
|
||||
}) => {
|
||||
const { duplicateWorkspace, isDuplicationReady } =
|
||||
useWorkspaceDuplication(workspace);
|
||||
|
||||
const [isDownloadDialogOpen, setIsDownloadDialogOpen] = useState(false);
|
||||
|
||||
const { user } = useAuthenticated();
|
||||
const isOwner =
|
||||
user.roles.find((role) => role.name === "owner") !== undefined;
|
||||
const { actions, canCancel, canAcceptJobs } = abilitiesByWorkspaceStatus(
|
||||
workspace,
|
||||
canDebug,
|
||||
{ canDebug, isOwner },
|
||||
);
|
||||
const showCancel =
|
||||
canCancel &&
|
||||
(workspace.template_allow_user_cancel_workspace_jobs || isOwner);
|
||||
|
||||
const mustUpdate = mustUpdateWorkspace(workspace, canChangeVersions);
|
||||
const tooltipText = getTooltipText(workspace, mustUpdate, canChangeVersions);
|
||||
@@ -169,7 +171,7 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
<Fragment key={action}>{buttonMapping[action]}</Fragment>
|
||||
))}
|
||||
|
||||
{showCancel && <CancelButton handleAction={handleCancel} />}
|
||||
{canCancel && <CancelButton handleAction={handleCancel} />}
|
||||
|
||||
<FavoriteButton
|
||||
workspaceID={workspace.id}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
startWorkspace,
|
||||
stopWorkspace,
|
||||
toggleFavorite,
|
||||
updateWorkspace,
|
||||
} from "api/queries/workspaces";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import {
|
||||
@@ -20,13 +19,14 @@ import {
|
||||
type ConfirmDialogProps,
|
||||
} from "components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { displayError } from "components/GlobalSnackbar/utils";
|
||||
import { MemoizedInlineMarkdown } from "components/Markdown/Markdown";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import dayjs from "dayjs";
|
||||
import { useAuthenticated } from "hooks";
|
||||
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
|
||||
import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs";
|
||||
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
|
||||
import {
|
||||
WorkspaceUpdateDialogs,
|
||||
useWorkspaceUpdate,
|
||||
} from "modules/workspaces/WorkspaceUpdateDialogs";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
@@ -59,10 +59,6 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
throw Error("Workspace is undefined");
|
||||
}
|
||||
|
||||
// Owner
|
||||
const { user: me } = useAuthenticated();
|
||||
const isOwner = me.roles.find((role) => role.name === "owner") !== undefined;
|
||||
|
||||
// Debug mode
|
||||
const { data: deploymentValues } = useQuery({
|
||||
...deploymentConfig(),
|
||||
@@ -120,10 +116,10 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
});
|
||||
|
||||
// Update workspace
|
||||
const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false);
|
||||
const updateWorkspaceMutation = useMutation(
|
||||
updateWorkspace(workspace, queryClient),
|
||||
);
|
||||
const workspaceUpdate = useWorkspaceUpdate({
|
||||
workspace,
|
||||
latestVersion,
|
||||
});
|
||||
|
||||
// If a user can update the template then they can force a delete
|
||||
// (via orphan).
|
||||
@@ -233,7 +229,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
|
||||
<Workspace
|
||||
permissions={permissions}
|
||||
isUpdating={updateWorkspaceMutation.isLoading}
|
||||
isUpdating={workspaceUpdate.isUpdating}
|
||||
isRestarting={isRestarting}
|
||||
workspace={workspace}
|
||||
handleStart={(buildParameters) => {
|
||||
@@ -248,9 +244,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
handleRestart={(buildParameters) => {
|
||||
setConfirmingRestart({ open: true, buildParameters });
|
||||
}}
|
||||
handleUpdate={() => {
|
||||
setIsConfirmingUpdate(true);
|
||||
}}
|
||||
handleUpdate={workspaceUpdate.update}
|
||||
handleCancel={cancelBuildMutation.mutate}
|
||||
handleSettings={() => navigate("settings")}
|
||||
handleRetry={handleRetry}
|
||||
@@ -280,7 +274,6 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
sshPrefix={sshPrefixQuery.data?.hostname_prefix}
|
||||
template={template}
|
||||
buildLogs={buildLogs}
|
||||
isOwner={isOwner}
|
||||
timings={timingsQuery.data}
|
||||
/>
|
||||
|
||||
@@ -318,23 +311,6 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
<UpdateBuildParametersDialog
|
||||
missedParameters={
|
||||
updateWorkspaceMutation.error instanceof MissingBuildParameters
|
||||
? updateWorkspaceMutation.error.parameters
|
||||
: []
|
||||
}
|
||||
open={updateWorkspaceMutation.error instanceof MissingBuildParameters}
|
||||
onClose={() => {
|
||||
updateWorkspaceMutation.reset();
|
||||
}}
|
||||
onUpdate={(buildParameters) => {
|
||||
if (updateWorkspaceMutation.error instanceof MissingBuildParameters) {
|
||||
updateWorkspaceMutation.mutate(buildParameters);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ChangeVersionDialog
|
||||
templateVersions={allVersions?.reverse()}
|
||||
template={template}
|
||||
@@ -351,31 +327,6 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
<WarningDialog
|
||||
open={isConfirmingUpdate}
|
||||
onConfirm={() => {
|
||||
updateWorkspaceMutation.mutate(undefined);
|
||||
setIsConfirmingUpdate(false);
|
||||
}}
|
||||
onClose={() => setIsConfirmingUpdate(false)}
|
||||
title="Update workspace?"
|
||||
confirmText="Update"
|
||||
description={
|
||||
<Stack>
|
||||
<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>
|
||||
)}
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
|
||||
<WarningDialog
|
||||
open={confirmingRestart.open}
|
||||
onConfirm={() => {
|
||||
@@ -395,6 +346,8 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<WorkspaceUpdateDialogs {...workspaceUpdate.dialogs} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
MockUser,
|
||||
MockWorkspace,
|
||||
} from "testHelpers/entities";
|
||||
import { withDashboardProvider } from "testHelpers/storybook";
|
||||
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
|
||||
import { WorkspaceTopbar } from "./WorkspaceTopbar";
|
||||
|
||||
// We want a workspace without a deadline to not pollute the screenshot. Also
|
||||
@@ -28,7 +28,7 @@ const baseWorkspace: Workspace = {
|
||||
const meta: Meta<typeof WorkspaceTopbar> = {
|
||||
title: "pages/WorkspacePage/WorkspaceTopbar",
|
||||
component: WorkspaceTopbar,
|
||||
decorators: [withDashboardProvider],
|
||||
decorators: [withAuthProvider, withDashboardProvider],
|
||||
args: {
|
||||
workspace: baseWorkspace,
|
||||
template: MockTemplate,
|
||||
@@ -41,6 +41,7 @@ const meta: Meta<typeof WorkspaceTopbar> = {
|
||||
chromatic: {
|
||||
diffThreshold: 0.6,
|
||||
},
|
||||
user: MockUser,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ export interface WorkspaceProps {
|
||||
canDebugMode: boolean;
|
||||
handleRetry: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void;
|
||||
handleDebug: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void;
|
||||
isOwner: boolean;
|
||||
template: TypesGen.Template;
|
||||
permissions: WorkspacePermissions;
|
||||
latestVersion?: TypesGen.TemplateVersion;
|
||||
@@ -81,7 +80,6 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
|
||||
canDebugMode,
|
||||
handleRetry,
|
||||
handleDebug,
|
||||
isOwner,
|
||||
template,
|
||||
latestVersion,
|
||||
permissions,
|
||||
@@ -262,7 +260,6 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
|
||||
canChangeVersions={canChangeVersions}
|
||||
isUpdating={isUpdating}
|
||||
isRestarting={isRestarting}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -264,7 +264,7 @@ describe("WorkspacesPage", () => {
|
||||
await user.click(getWorkspaceCheckbox(workspaces[0]));
|
||||
await user.click(getWorkspaceCheckbox(workspaces[1]));
|
||||
await user.click(screen.getByRole("button", { name: /actions/i }));
|
||||
const stopButton = await screen.findByText(/stop/i);
|
||||
const stopButton = await screen.findByRole("menuitem", { name: /stop/i });
|
||||
await user.click(stopButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -291,7 +291,7 @@ describe("WorkspacesPage", () => {
|
||||
await user.click(getWorkspaceCheckbox(workspaces[0]));
|
||||
await user.click(getWorkspaceCheckbox(workspaces[1]));
|
||||
await user.click(screen.getByRole("button", { name: /actions/i }));
|
||||
const startButton = await screen.findByText(/start/i);
|
||||
const startButton = await screen.findByRole("menuitem", { name: /start/i });
|
||||
await user.click(startButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { getErrorDetail, getErrorMessage } from "api/errors";
|
||||
import { workspacePermissionsByOrganization } from "api/queries/organizations";
|
||||
import { templates } from "api/queries/templates";
|
||||
import type { Workspace } from "api/typesGenerated";
|
||||
import { useFilter } from "components/Filter/Filter";
|
||||
import { useUserFilterMenu } from "components/Filter/UserFilter";
|
||||
import { displayError } from "components/GlobalSnackbar/utils";
|
||||
import { useAuthenticated } from "hooks";
|
||||
import { useEffectEvent } from "hooks/hookPolyfills";
|
||||
import { usePagination } from "hooks/usePagination";
|
||||
@@ -10,7 +12,7 @@ import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import { useOrganizationsFilterMenu } from "modules/tableFiltering/options";
|
||||
import { type FC, useEffect, useMemo, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useQuery } from "react-query";
|
||||
import { useQuery, useQueryClient } from "react-query";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { BatchDeleteConfirmation } from "./BatchDeleteConfirmation";
|
||||
@@ -35,6 +37,7 @@ function useSafeSearchParams() {
|
||||
}
|
||||
|
||||
const WorkspacesPage: FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
// If we use a useSearchParams for each hook, the values will not be in sync.
|
||||
// So we have to use a single one, centralizing the values, and pass it to
|
||||
// each hook.
|
||||
@@ -72,7 +75,7 @@ const WorkspacesPage: FC = () => {
|
||||
|
||||
const { data, error, queryKey, refetch } = useWorkspacesData({
|
||||
...pagination,
|
||||
query: filterProps.filter.query,
|
||||
q: filterProps.filter.query,
|
||||
});
|
||||
|
||||
const updateWorkspace = useWorkspaceUpdate(queryKey);
|
||||
@@ -128,6 +131,17 @@ const WorkspacesPage: FC = () => {
|
||||
onUpdateAll={() => setConfirmingBatchAction("update")}
|
||||
onStartAll={() => batchActions.startAll(checkedWorkspaces)}
|
||||
onStopAll={() => batchActions.stopAll(checkedWorkspaces)}
|
||||
onActionSuccess={async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey,
|
||||
});
|
||||
}}
|
||||
onActionError={(error) => {
|
||||
displayError(
|
||||
getErrorMessage(error, "Failed to perform action"),
|
||||
getErrorDetail(error),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<BatchDeleteConfirmation
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
MockWorkspaceAppStatus,
|
||||
mockApiError,
|
||||
} from "testHelpers/entities";
|
||||
import { withDashboardProvider } from "testHelpers/storybook";
|
||||
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
|
||||
import { WorkspacesPageView } from "./WorkspacesPageView";
|
||||
|
||||
const createWorkspace = (
|
||||
@@ -144,8 +144,10 @@ const meta: Meta<typeof WorkspacesPageView> = {
|
||||
data: MockBuildInfo,
|
||||
},
|
||||
],
|
||||
user: MockUser,
|
||||
},
|
||||
decorators: [
|
||||
withAuthProvider,
|
||||
withDashboardProvider,
|
||||
(Story) => (
|
||||
<ProxyContext.Provider
|
||||
|
||||
@@ -65,6 +65,8 @@ export interface WorkspacesPageViewProps {
|
||||
templates: TemplateQuery["data"];
|
||||
canCreateTemplate: boolean;
|
||||
canChangeVersions: boolean;
|
||||
onActionSuccess: () => Promise<void>;
|
||||
onActionError: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
|
||||
@@ -88,6 +90,8 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
|
||||
templatesFetchStatus,
|
||||
canCreateTemplate,
|
||||
canChangeVersions,
|
||||
onActionSuccess,
|
||||
onActionError,
|
||||
}) => {
|
||||
// Let's say the user has 5 workspaces, but tried to hit page 100, which does
|
||||
// not exist. In this case, the page is not valid and we want to show a better
|
||||
@@ -224,6 +228,8 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
|
||||
onCheckChange={onCheckChange}
|
||||
canCheckWorkspaces={canCheckWorkspaces}
|
||||
templates={templates}
|
||||
onActionSuccess={onActionSuccess}
|
||||
onActionError={onActionError}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@ import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
|
||||
import Star from "@mui/icons-material/Star";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { templateVersion } from "api/queries/templates";
|
||||
import {
|
||||
cancelBuild,
|
||||
deleteWorkspace,
|
||||
startWorkspace,
|
||||
stopWorkspace,
|
||||
} from "api/queries/workspaces";
|
||||
import type {
|
||||
Template,
|
||||
Workspace,
|
||||
@@ -11,7 +18,9 @@ import type {
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { AvatarData } from "components/Avatar/AvatarData";
|
||||
import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import {
|
||||
StatusIndicator,
|
||||
@@ -30,14 +39,33 @@ import {
|
||||
TableLoaderSkeleton,
|
||||
TableRowSkeleton,
|
||||
} from "components/TableLoader/TableLoader";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { useAuthenticated } from "hooks";
|
||||
import { useClickableTableRow } from "hooks/useClickableTableRow";
|
||||
import { BanIcon, PlayIcon, RefreshCcwIcon, SquareIcon } from "lucide-react";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus";
|
||||
import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge";
|
||||
import { WorkspaceOutdatedTooltip } from "modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip";
|
||||
import { type FC, type ReactNode, useMemo } from "react";
|
||||
import {
|
||||
WorkspaceUpdateDialogs,
|
||||
useWorkspaceUpdate,
|
||||
} from "modules/workspaces/WorkspaceUpdateDialogs";
|
||||
import { abilitiesByWorkspaceStatus } from "modules/workspaces/actions";
|
||||
import {
|
||||
type FC,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { cn } from "utils/cn";
|
||||
import {
|
||||
@@ -60,6 +88,8 @@ export interface WorkspacesTableProps {
|
||||
canCheckWorkspaces: boolean;
|
||||
templates?: Template[];
|
||||
canCreateTemplate: boolean;
|
||||
onActionSuccess: () => Promise<void>;
|
||||
onActionError: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
@@ -71,6 +101,8 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
canCheckWorkspaces,
|
||||
templates,
|
||||
canCreateTemplate,
|
||||
onActionSuccess,
|
||||
onActionError,
|
||||
}) => {
|
||||
const dashboard = useDashboard();
|
||||
const workspaceIDToAppByStatus = useMemo(() => {
|
||||
@@ -139,6 +171,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
<TableHead className="w-2/6">Template</TableHead>
|
||||
<TableHead className="w-2/6">Status</TableHead>
|
||||
<TableHead className="w-0" />
|
||||
<TableHead className="w-0" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="[&_td]:h-[72px]">
|
||||
@@ -260,10 +293,14 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
|
||||
</TableCell>
|
||||
|
||||
<WorkspaceStatusCell workspace={workspace} />
|
||||
|
||||
<WorkspaceActionsCell
|
||||
workspace={workspace}
|
||||
onActionSuccess={onActionSuccess}
|
||||
onActionError={onActionError}
|
||||
/>
|
||||
<TableCell>
|
||||
<div className="flex pl-4">
|
||||
<KeyboardArrowRight className="text-content-secondary w-5 h-5" />
|
||||
<div className="flex">
|
||||
<KeyboardArrowRight className="text-content-secondary size-icon-sm" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</WorkspacesRow>
|
||||
@@ -289,7 +326,7 @@ const WorkspacesRow: FC<WorkspacesRowProps> = ({
|
||||
|
||||
const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`;
|
||||
const openLinkInNewTab = () => window.open(workspacePageLink, "_blank");
|
||||
const clickableProps = useClickableTableRow({
|
||||
const { role, hover, ...clickableProps } = useClickableTableRow({
|
||||
onMiddleClick: openLinkInNewTab,
|
||||
onClick: (event) => {
|
||||
// Order of booleans actually matters here for Windows-Mac compatibility;
|
||||
@@ -341,7 +378,12 @@ const TableLoader: FC<TableLoaderProps> = ({ canCheckWorkspaces }) => {
|
||||
<Skeleton variant="text" width="50%" />
|
||||
</TableCell>
|
||||
<TableCell className="w-0">
|
||||
<Skeleton variant="text" width="25%" />
|
||||
<Skeleton variant="rounded" width={40} height={40} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex">
|
||||
<KeyboardArrowRight className="text-content-disabled size-icon-sm" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRowSkeleton>
|
||||
</TableLoaderSkeleton>
|
||||
@@ -399,3 +441,188 @@ const WorkspaceStatusCell: FC<WorkspaceStatusCellProps> = ({ workspace }) => {
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
||||
type WorkspaceActionsCellProps = {
|
||||
workspace: Workspace;
|
||||
onActionSuccess: () => Promise<void>;
|
||||
onActionError: (error: unknown) => void;
|
||||
};
|
||||
|
||||
const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({
|
||||
workspace,
|
||||
onActionSuccess,
|
||||
onActionError,
|
||||
}) => {
|
||||
const { user } = useAuthenticated();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const abilities = abilitiesByWorkspaceStatus(workspace, {
|
||||
canDebug: false,
|
||||
isOwner: user.roles.find((role) => role.name === "owner") !== undefined,
|
||||
});
|
||||
|
||||
const startWorkspaceOptions = startWorkspace(workspace, queryClient);
|
||||
const startWorkspaceMutation = useMutation({
|
||||
...startWorkspaceOptions,
|
||||
onSuccess: async (build) => {
|
||||
startWorkspaceOptions.onSuccess(build);
|
||||
await onActionSuccess();
|
||||
},
|
||||
onError: onActionError,
|
||||
});
|
||||
|
||||
const stopWorkspaceOptions = stopWorkspace(workspace, queryClient);
|
||||
const stopWorkspaceMutation = useMutation({
|
||||
...stopWorkspaceOptions,
|
||||
onSuccess: async (build) => {
|
||||
stopWorkspaceOptions.onSuccess(build);
|
||||
await onActionSuccess();
|
||||
},
|
||||
onError: onActionError,
|
||||
});
|
||||
|
||||
const cancelJobOptions = cancelBuild(workspace, queryClient);
|
||||
const cancelBuildMutation = useMutation({
|
||||
...cancelJobOptions,
|
||||
onSuccess: async () => {
|
||||
cancelJobOptions.onSuccess();
|
||||
await onActionSuccess();
|
||||
},
|
||||
onError: onActionError,
|
||||
});
|
||||
|
||||
const { data: latestVersion } = useQuery({
|
||||
...templateVersion(workspace.template_active_version_id),
|
||||
enabled: workspace.outdated,
|
||||
});
|
||||
const workspaceUpdate = useWorkspaceUpdate({
|
||||
workspace,
|
||||
latestVersion,
|
||||
onSuccess: onActionSuccess,
|
||||
onError: onActionError,
|
||||
});
|
||||
|
||||
const deleteWorkspaceOptions = deleteWorkspace(workspace, queryClient);
|
||||
const deleteWorkspaceMutation = useMutation({
|
||||
...deleteWorkspaceOptions,
|
||||
onSuccess: async (build) => {
|
||||
deleteWorkspaceOptions.onSuccess(build);
|
||||
await onActionSuccess();
|
||||
},
|
||||
onError: onActionError,
|
||||
});
|
||||
|
||||
const isRetrying =
|
||||
startWorkspaceMutation.isLoading ||
|
||||
stopWorkspaceMutation.isLoading ||
|
||||
deleteWorkspaceMutation.isLoading;
|
||||
|
||||
const retry = () => {
|
||||
switch (workspace.latest_build.transition) {
|
||||
case "start":
|
||||
startWorkspaceMutation.mutate({});
|
||||
break;
|
||||
case "stop":
|
||||
stopWorkspaceMutation.mutate({});
|
||||
break;
|
||||
case "delete":
|
||||
deleteWorkspaceMutation.mutate({});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableCell>
|
||||
<div className="flex gap-1 justify-end">
|
||||
{abilities.actions.includes("start") && (
|
||||
<PrimaryAction
|
||||
onClick={() => startWorkspaceMutation.mutate({})}
|
||||
isLoading={startWorkspaceMutation.isLoading}
|
||||
label="Start workspace"
|
||||
>
|
||||
<PlayIcon />
|
||||
</PrimaryAction>
|
||||
)}
|
||||
|
||||
{abilities.actions.includes("updateAndStart") && (
|
||||
<>
|
||||
<PrimaryAction
|
||||
onClick={workspaceUpdate.update}
|
||||
isLoading={workspaceUpdate.isUpdating}
|
||||
label="Update and start workspace"
|
||||
>
|
||||
<PlayIcon />
|
||||
</PrimaryAction>
|
||||
<WorkspaceUpdateDialogs {...workspaceUpdate.dialogs} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{abilities.actions.includes("stop") && (
|
||||
<PrimaryAction
|
||||
onClick={() => {
|
||||
stopWorkspaceMutation.mutate({});
|
||||
}}
|
||||
isLoading={stopWorkspaceMutation.isLoading}
|
||||
label="Stop workspace"
|
||||
>
|
||||
<SquareIcon />
|
||||
</PrimaryAction>
|
||||
)}
|
||||
|
||||
{abilities.canCancel && (
|
||||
<PrimaryAction
|
||||
onClick={cancelBuildMutation.mutate}
|
||||
isLoading={cancelBuildMutation.isLoading}
|
||||
label="Cancel build"
|
||||
>
|
||||
<BanIcon />
|
||||
</PrimaryAction>
|
||||
)}
|
||||
|
||||
{abilities.actions.includes("retry") && (
|
||||
<PrimaryAction
|
||||
onClick={retry}
|
||||
isLoading={isRetrying}
|
||||
label="Retry build"
|
||||
>
|
||||
<RefreshCcwIcon />
|
||||
</PrimaryAction>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
||||
type PrimaryActionProps = PropsWithChildren<{
|
||||
onClick: () => void;
|
||||
isLoading: boolean;
|
||||
label: string;
|
||||
}>;
|
||||
|
||||
const PrimaryAction: FC<PrimaryActionProps> = ({
|
||||
onClick,
|
||||
isLoading,
|
||||
label,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-lg"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<Spinner loading={isLoading}>{children}</Spinner>
|
||||
<span className="sr-only">{label}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { API } from "api/api";
|
||||
import { getErrorMessage } from "api/errors";
|
||||
import { workspaces } from "api/queries/workspaces";
|
||||
import type {
|
||||
Workspace,
|
||||
WorkspaceBuild,
|
||||
WorkspacesRequest,
|
||||
WorkspacesResponse,
|
||||
} from "api/typesGenerated";
|
||||
import { displayError } from "components/GlobalSnackbar/utils";
|
||||
@@ -14,27 +16,11 @@ import {
|
||||
useQueryClient,
|
||||
} from "react-query";
|
||||
|
||||
type UseWorkspacesDataParams = {
|
||||
page: number;
|
||||
limit: number;
|
||||
query: string;
|
||||
};
|
||||
|
||||
export const useWorkspacesData = ({
|
||||
page,
|
||||
limit,
|
||||
query,
|
||||
}: UseWorkspacesDataParams) => {
|
||||
const queryKey = ["workspaces", query, page];
|
||||
export const useWorkspacesData = (req: WorkspacesRequest) => {
|
||||
const [shouldRefetch, setShouldRefetch] = useState(true);
|
||||
const workspacesQueryOptions = workspaces(req);
|
||||
const result = useQuery({
|
||||
queryKey,
|
||||
queryFn: () =>
|
||||
API.getWorkspaces({
|
||||
q: query,
|
||||
limit: limit,
|
||||
offset: page <= 0 ? 0 : (page - 1) * limit,
|
||||
}),
|
||||
...workspacesQueryOptions,
|
||||
onSuccess: () => {
|
||||
setShouldRefetch(true);
|
||||
},
|
||||
@@ -46,7 +32,7 @@ export const useWorkspacesData = ({
|
||||
|
||||
return {
|
||||
...result,
|
||||
queryKey,
|
||||
queryKey: workspacesQueryOptions.queryKey,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user