mirror of https://github.com/coder/coder.git
451 lines
14 KiB
TypeScript
451 lines
14 KiB
TypeScript
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
|
import { useFeatureVisibility } from "hooks/useFeatureVisibility";
|
|
import { FC, useEffect, useState } from "react";
|
|
import { Helmet } from "react-helmet-async";
|
|
import { useNavigate } from "react-router-dom";
|
|
import {
|
|
getDeadline,
|
|
getMaxDeadline,
|
|
getMaxDeadlineChange,
|
|
getMinDeadline,
|
|
} from "utils/schedule";
|
|
import { Workspace, WorkspaceErrors } from "./Workspace";
|
|
import { pageTitle } from "utils/page";
|
|
import { hasJobError } from "utils/workspace";
|
|
import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog";
|
|
import { ChangeVersionDialog } from "./ChangeVersionDialog";
|
|
import { useMutation, useQuery, useQueryClient } from "react-query";
|
|
import { MissingBuildParameters, restartWorkspace } from "api/api";
|
|
import {
|
|
ConfirmDialog,
|
|
ConfirmDialogProps,
|
|
} from "components/Dialogs/ConfirmDialog/ConfirmDialog";
|
|
import * as TypesGen from "api/typesGenerated";
|
|
import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection";
|
|
import { templateVersion, templateVersions } from "api/queries/templates";
|
|
import { Alert } from "components/Alert/Alert";
|
|
import { Stack } from "components/Stack/Stack";
|
|
import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs";
|
|
import {
|
|
activate,
|
|
changeVersion,
|
|
decreaseDeadline,
|
|
deleteWorkspace,
|
|
increaseDeadline,
|
|
updateWorkspace,
|
|
stopWorkspace,
|
|
startWorkspace,
|
|
cancelBuild,
|
|
} from "api/queries/workspaces";
|
|
import { getErrorMessage } from "api/errors";
|
|
import { displaySuccess, displayError } from "components/GlobalSnackbar/utils";
|
|
import { deploymentConfig, deploymentSSHConfig } from "api/queries/deployment";
|
|
import { WorkspacePermissions } from "./permissions";
|
|
import { workspaceResolveAutostart } from "api/queries/workspaceQuota";
|
|
import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog";
|
|
import dayjs from "dayjs";
|
|
|
|
interface WorkspaceReadyPageProps {
|
|
template: TypesGen.Template;
|
|
workspace: TypesGen.Workspace;
|
|
permissions: WorkspacePermissions;
|
|
builds: TypesGen.WorkspaceBuild[] | undefined;
|
|
buildsError: unknown;
|
|
onLoadMoreBuilds: () => void;
|
|
isLoadingMoreBuilds: boolean;
|
|
hasMoreBuilds: boolean;
|
|
}
|
|
|
|
export const WorkspaceReadyPage = ({
|
|
workspace,
|
|
template,
|
|
permissions,
|
|
builds,
|
|
buildsError,
|
|
onLoadMoreBuilds,
|
|
isLoadingMoreBuilds,
|
|
hasMoreBuilds,
|
|
}: WorkspaceReadyPageProps): JSX.Element => {
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const { buildInfo } = useDashboard();
|
|
const featureVisibility = useFeatureVisibility();
|
|
if (workspace === undefined) {
|
|
throw Error("Workspace is undefined");
|
|
}
|
|
|
|
// Debug mode
|
|
const { data: deploymentValues } = useQuery({
|
|
...deploymentConfig(),
|
|
enabled: permissions?.viewDeploymentValues,
|
|
});
|
|
const canRetryDebugMode = Boolean(
|
|
deploymentValues?.config.enable_terraform_debug_mode,
|
|
);
|
|
|
|
// Build logs
|
|
const buildLogs = useWorkspaceBuildLogs(workspace.latest_build.id);
|
|
const shouldDisplayBuildLogs =
|
|
hasJobError(workspace) ||
|
|
["canceling", "deleting", "pending", "starting", "stopping"].includes(
|
|
workspace.latest_build.status,
|
|
);
|
|
|
|
// Restart
|
|
const [confirmingRestart, setConfirmingRestart] = useState<{
|
|
open: boolean;
|
|
buildParameters?: TypesGen.WorkspaceBuildParameter[];
|
|
}>({ open: false });
|
|
const {
|
|
mutate: mutateRestartWorkspace,
|
|
error: restartBuildError,
|
|
isLoading: isRestarting,
|
|
} = useMutation({
|
|
mutationFn: restartWorkspace,
|
|
});
|
|
|
|
// Schedule controls
|
|
const deadline = getDeadline(workspace);
|
|
const onDeadlineChangeSuccess = () => {
|
|
displaySuccess("Updated workspace shutdown time.");
|
|
};
|
|
const onDeadlineChangeFails = (error: unknown) => {
|
|
displayError(
|
|
getErrorMessage(error, "Failed to update workspace shutdown time."),
|
|
);
|
|
};
|
|
const decreaseMutation = useMutation({
|
|
...decreaseDeadline(workspace),
|
|
onSuccess: onDeadlineChangeSuccess,
|
|
onError: onDeadlineChangeFails,
|
|
});
|
|
const increaseMutation = useMutation({
|
|
...increaseDeadline(workspace),
|
|
onSuccess: onDeadlineChangeSuccess,
|
|
onError: onDeadlineChangeFails,
|
|
});
|
|
|
|
// Auto start
|
|
const canAutostartResponse = useQuery(
|
|
workspaceResolveAutostart(workspace.id),
|
|
);
|
|
const canAutostart = !canAutostartResponse.data?.parameter_mismatch ?? false;
|
|
|
|
// SSH Prefix
|
|
const sshPrefixQuery = useQuery(deploymentSSHConfig());
|
|
|
|
// Favicon
|
|
const favicon = getFaviconByStatus(workspace.latest_build);
|
|
const [faviconTheme, setFaviconTheme] = useState<"light" | "dark">("dark");
|
|
useEffect(() => {
|
|
if (typeof window === "undefined" || !window.matchMedia) {
|
|
return;
|
|
}
|
|
|
|
const isDark = window.matchMedia("(prefers-color-scheme: dark)");
|
|
// We want the favicon the opposite of the theme.
|
|
setFaviconTheme(isDark.matches ? "light" : "dark");
|
|
}, []);
|
|
|
|
// Change version
|
|
const canChangeVersions = Boolean(permissions?.updateTemplate);
|
|
const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false);
|
|
const changeVersionMutation = useMutation(
|
|
changeVersion(workspace, queryClient),
|
|
);
|
|
|
|
// Versions
|
|
const { data: allVersions } = useQuery({
|
|
...templateVersions(workspace.template_id),
|
|
enabled: changeVersionDialogOpen,
|
|
});
|
|
const { data: latestVersion } = useQuery({
|
|
...templateVersion(workspace.template_active_version_id),
|
|
enabled: workspace.outdated,
|
|
});
|
|
|
|
// Update workspace
|
|
const canUpdateWorkspace = Boolean(permissions?.updateWorkspace);
|
|
const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false);
|
|
const updateWorkspaceMutation = useMutation(
|
|
updateWorkspace(workspace, queryClient),
|
|
);
|
|
|
|
// Delete workspace
|
|
const canDeleteWorkspace = Boolean(permissions?.updateWorkspace);
|
|
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false);
|
|
const deleteWorkspaceMutation = useMutation(
|
|
deleteWorkspace(workspace, queryClient),
|
|
);
|
|
|
|
// Activate workspace
|
|
const activateWorkspaceMutation = useMutation(
|
|
activate(workspace, queryClient),
|
|
);
|
|
|
|
// Stop workspace
|
|
const stopWorkspaceMutation = useMutation(
|
|
stopWorkspace(workspace, queryClient),
|
|
);
|
|
|
|
// Start workspace
|
|
const startWorkspaceMutation = useMutation(
|
|
startWorkspace(workspace, queryClient),
|
|
);
|
|
|
|
// Cancel build
|
|
const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient));
|
|
|
|
return (
|
|
<>
|
|
<Helmet>
|
|
<title>{pageTitle(`${workspace.owner_name}/${workspace.name}`)}</title>
|
|
<link
|
|
rel="alternate icon"
|
|
type="image/png"
|
|
href={`/favicons/${favicon}-${faviconTheme}.png`}
|
|
/>
|
|
<link
|
|
rel="icon"
|
|
type="image/svg+xml"
|
|
href={`/favicons/${favicon}-${faviconTheme}.svg`}
|
|
/>
|
|
</Helmet>
|
|
|
|
<Workspace
|
|
scheduleProps={{
|
|
onDeadlineMinus: decreaseMutation.mutate,
|
|
onDeadlinePlus: increaseMutation.mutate,
|
|
maxDeadlineDecrease: getMaxDeadlineChange(deadline, getMinDeadline()),
|
|
maxDeadlineIncrease: getMaxDeadlineChange(
|
|
getMaxDeadline(workspace),
|
|
deadline,
|
|
),
|
|
}}
|
|
isUpdating={updateWorkspaceMutation.isLoading}
|
|
isRestarting={isRestarting}
|
|
workspace={workspace}
|
|
handleStart={(buildParameters) => {
|
|
startWorkspaceMutation.mutate({ buildParameters });
|
|
}}
|
|
handleStop={() => {
|
|
stopWorkspaceMutation.mutate({});
|
|
}}
|
|
handleDelete={() => {
|
|
setIsConfirmingDelete(true);
|
|
}}
|
|
handleRestart={(buildParameters) => {
|
|
setConfirmingRestart({ open: true, buildParameters });
|
|
}}
|
|
handleUpdate={() => {
|
|
setIsConfirmingUpdate(true);
|
|
}}
|
|
handleCancel={cancelBuildMutation.mutate}
|
|
handleSettings={() => navigate("settings")}
|
|
handleBuildRetry={() => {
|
|
switch (workspace.latest_build.transition) {
|
|
case "start":
|
|
startWorkspaceMutation.mutate({ logLevel: "debug" });
|
|
break;
|
|
case "stop":
|
|
stopWorkspaceMutation.mutate({ logLevel: "debug" });
|
|
break;
|
|
case "delete":
|
|
deleteWorkspaceMutation.mutate({ logLevel: "debug" });
|
|
break;
|
|
}
|
|
}}
|
|
handleChangeVersion={() => {
|
|
setChangeVersionDialogOpen(true);
|
|
}}
|
|
handleDormantActivate={async () => {
|
|
try {
|
|
await activateWorkspaceMutation.mutateAsync();
|
|
} catch (e) {
|
|
const message = getErrorMessage(e, "Error activate workspace.");
|
|
displayError(message);
|
|
}
|
|
}}
|
|
resources={workspace.latest_build.resources}
|
|
builds={builds}
|
|
onLoadMoreBuilds={onLoadMoreBuilds}
|
|
isLoadingMoreBuilds={isLoadingMoreBuilds}
|
|
hasMoreBuilds={hasMoreBuilds}
|
|
canUpdateWorkspace={canUpdateWorkspace}
|
|
updateMessage={latestVersion?.message}
|
|
canRetryDebugMode={canRetryDebugMode}
|
|
canChangeVersions={canChangeVersions}
|
|
hideSSHButton={featureVisibility["browser_only"]}
|
|
hideVSCodeDesktopButton={featureVisibility["browser_only"]}
|
|
workspaceErrors={{
|
|
[WorkspaceErrors.GET_BUILDS_ERROR]: buildsError,
|
|
[WorkspaceErrors.BUILD_ERROR]:
|
|
restartBuildError ??
|
|
startWorkspaceMutation.error ??
|
|
stopWorkspaceMutation.error ??
|
|
deleteWorkspaceMutation.error ??
|
|
updateWorkspaceMutation.error,
|
|
[WorkspaceErrors.CANCELLATION_ERROR]: cancelBuildMutation.error,
|
|
}}
|
|
buildInfo={buildInfo}
|
|
sshPrefix={sshPrefixQuery.data?.hostname_prefix}
|
|
template={template}
|
|
buildLogs={
|
|
shouldDisplayBuildLogs && (
|
|
<WorkspaceBuildLogsSection logs={buildLogs} />
|
|
)
|
|
}
|
|
canAutostart={canAutostart}
|
|
/>
|
|
<WorkspaceDeleteDialog
|
|
workspace={workspace}
|
|
canUpdateTemplate={canDeleteWorkspace}
|
|
isOpen={isConfirmingDelete}
|
|
onCancel={() => {
|
|
setIsConfirmingDelete(false);
|
|
}}
|
|
onConfirm={(orphan) => {
|
|
deleteWorkspaceMutation.mutate({ orphan });
|
|
setIsConfirmingDelete(false);
|
|
}}
|
|
workspaceBuildDateStr={dayjs(workspace.created_at).fromNow()}
|
|
/>
|
|
<UpdateBuildParametersDialog
|
|
missedParameters={
|
|
changeVersionMutation.error instanceof MissingBuildParameters
|
|
? changeVersionMutation.error.parameters
|
|
: []
|
|
}
|
|
open={changeVersionMutation.error instanceof MissingBuildParameters}
|
|
onClose={() => {
|
|
changeVersionMutation.reset();
|
|
}}
|
|
onUpdate={(buildParameters) => {
|
|
if (changeVersionMutation.error instanceof MissingBuildParameters) {
|
|
changeVersionMutation.mutate({
|
|
versionId: changeVersionMutation.error.versionId,
|
|
buildParameters,
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
<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}
|
|
defaultTemplateVersion={allVersions?.find(
|
|
(v) => workspace.latest_build.template_version_id === v.id,
|
|
)}
|
|
open={changeVersionDialogOpen}
|
|
onClose={() => {
|
|
setChangeVersionDialogOpen(false);
|
|
}}
|
|
onConfirm={(templateVersion) => {
|
|
setChangeVersionDialogOpen(false);
|
|
changeVersionMutation.mutate({ versionId: templateVersion.id });
|
|
}}
|
|
/>
|
|
<WarningDialog
|
|
open={isConfirmingUpdate}
|
|
onConfirm={() => {
|
|
updateWorkspaceMutation.mutate(undefined);
|
|
setIsConfirmingUpdate(false);
|
|
}}
|
|
onClose={() => setIsConfirmingUpdate(false)}
|
|
title="Update and restart?"
|
|
confirmText="Update"
|
|
description={
|
|
<Stack>
|
|
<p>
|
|
Restarting your workspace will stop all running processes and{" "}
|
|
<strong>delete non-persistent data</strong>.
|
|
</p>
|
|
{latestVersion?.message && (
|
|
<Alert severity="info">{latestVersion.message}</Alert>
|
|
)}
|
|
</Stack>
|
|
}
|
|
/>
|
|
|
|
<WarningDialog
|
|
open={confirmingRestart.open}
|
|
onConfirm={() => {
|
|
mutateRestartWorkspace({
|
|
workspace,
|
|
buildParameters: confirmingRestart.buildParameters,
|
|
});
|
|
setConfirmingRestart({ open: false });
|
|
}}
|
|
onClose={() => setConfirmingRestart({ open: false })}
|
|
title="Restart your workspace?"
|
|
confirmText="Restart"
|
|
description={
|
|
<>
|
|
Restarting your workspace will stop all running processes and{" "}
|
|
<strong>delete non-persistent data</strong>.
|
|
</>
|
|
}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const WarningDialog: FC<
|
|
Pick<
|
|
ConfirmDialogProps,
|
|
"open" | "onClose" | "title" | "confirmText" | "description" | "onConfirm"
|
|
>
|
|
> = (props) => {
|
|
return <ConfirmDialog type="info" hideCancel={false} {...props} />;
|
|
};
|
|
|
|
// You can see the favicon designs here: https://www.figma.com/file/YIGBkXUcnRGz2ZKNmLaJQf/Coder-v2-Design?node-id=560%3A620
|
|
type FaviconType =
|
|
| "favicon"
|
|
| "favicon-success"
|
|
| "favicon-error"
|
|
| "favicon-warning"
|
|
| "favicon-running";
|
|
|
|
const getFaviconByStatus = (build: TypesGen.WorkspaceBuild): FaviconType => {
|
|
switch (build.status) {
|
|
case undefined:
|
|
return "favicon";
|
|
case "running":
|
|
return "favicon-success";
|
|
case "starting":
|
|
return "favicon-running";
|
|
case "stopping":
|
|
return "favicon-running";
|
|
case "stopped":
|
|
return "favicon";
|
|
case "deleting":
|
|
return "favicon";
|
|
case "deleted":
|
|
return "favicon";
|
|
case "canceling":
|
|
return "favicon-warning";
|
|
case "canceled":
|
|
return "favicon";
|
|
case "failed":
|
|
return "favicon-error";
|
|
case "pending":
|
|
return "favicon";
|
|
}
|
|
};
|