Skip to content

Commit 2f5a403

Browse files
committed
grueling
1 parent 7b5cf9e commit 2f5a403

File tree

8 files changed

+106
-69
lines changed

8 files changed

+106
-69
lines changed

site/src/hooks/useTime.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,43 @@
11
import { useEffect, useState } from "react";
2+
import { useEffectEvent } from "./hookPolyfills";
3+
4+
interface UseTimeOptions {
5+
/**
6+
* Can be set to `true` to disable checking for updates in circumstances where it is known
7+
* that there is no work to do.
8+
*/
9+
disabled?: boolean;
10+
11+
/**
12+
* The amount of time in milliseconds that should pass between checking for updates.
13+
*/
14+
interval?: number;
15+
}
216

317
/**
418
* useTime allows a component to rerender over time without a corresponding state change.
519
* An example could be a relative timestamp (eg. "in 5 minutes") that should count down as it
620
* approaches.
7-
*
8-
* This hook should only be used in components that are very simple, and that will not
9-
* create a lot of unnecessary work for the reconciler. Given that this hook will result in
10-
* the entire subtree being rerendered on a frequent interval, it's important that the subtree
11-
* remains small.
12-
*
13-
* @param active Can optionally be set to false in circumstances where updating over time is
14-
* not necessary.
1521
*/
16-
export function useTime(active: boolean = true) {
17-
const [, setTick] = useState(0);
22+
export function useTime<T>(func: () => T, options: UseTimeOptions = {}): T {
23+
const [computedValue, setComputedValue] = useState(() => func());
24+
const { disabled = false, interval = 1000 } = options;
25+
26+
const thunk = useEffectEvent(func);
1827

1928
useEffect(() => {
20-
if (!active) {
29+
if (disabled) {
2130
return;
2231
}
2332

24-
const interval = setInterval(() => {
25-
setTick((i) => i + 1);
26-
}, 1000);
33+
const handle = setInterval(() => {
34+
setComputedValue(() => thunk());
35+
}, interval);
2736

2837
return () => {
29-
clearInterval(interval);
38+
clearInterval(handle);
3039
};
31-
}, [active]);
40+
}, [disabled, interval]);
41+
42+
return computedValue;
3243
}

site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const WorkspaceOutdatedTooltipContent: FC<TooltipProps> = ({
8787
<div css={styles.bold}>Message</div>
8888
<div>
8989
{activeVersion ? (
90-
activeVersion.message === "" ? (
90+
activeVersion.message ? (
9191
"No message"
9292
) : (
9393
activeVersion.message

site/src/pages/WorkspacePage/ActivityStatus.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ export const ActivityStatus: FC<ActivityStatusProps> = ({
2020
}) => {
2121
const usedAt = dayjs(workspace.last_used_at).tz(dayjs.tz.guess());
2222

23-
// Don't bother updating if `status` will need to change before anything can happen.
24-
useTime(status === "ready" || status === "connected");
25-
2623
switch (status) {
2724
case "ready":
2825
return <Pill type="active">Ready</Pill>;

site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import RemoveIcon from "@mui/icons-material/RemoveOutlined";
66
import ScheduleOutlined from "@mui/icons-material/ScheduleOutlined";
77
import Tooltip from "@mui/material/Tooltip";
88
import { visuallyHidden } from "@mui/utils";
9-
import { type Dayjs } from "dayjs";
9+
import dayjs, { type Dayjs } from "dayjs";
1010
import { forwardRef, type FC, useRef } from "react";
1111
import { useMutation, useQueryClient } from "react-query";
1212
import { Link as RouterLink } from "react-router-dom";
@@ -187,11 +187,8 @@ const AutoStopDisplay: FC<AutoStopDisplayProps> = ({
187187
status,
188188
template,
189189
}) => {
190-
useTime();
191-
const { message, tooltip, danger } = autostopDisplay(
192-
workspace,
193-
status,
194-
template,
190+
const { message, tooltip, danger } = useTime(() =>
191+
autostopDisplay(workspace, status, template),
195192
);
196193

197194
const display = (
@@ -250,11 +247,14 @@ export const shouldDisplayScheduleControls = (
250247
workspace: Workspace,
251248
status: WorkspaceActivityStatus,
252249
): boolean => {
250+
const now = dayjs();
253251
const willAutoStop = isWorkspaceOn(workspace) && hasDeadline(workspace);
254252
const willAutoStart = !isWorkspaceOn(workspace) && hasAutoStart(workspace);
255-
const hasActivity =
256-
status === "connected" && !workspace.latest_build.max_deadline;
257-
return (willAutoStop || willAutoStart) && !hasActivity;
253+
const noRequiredStopSoon =
254+
status === "connected" &&
255+
(!workspace.latest_build.max_deadline ||
256+
dayjs(workspace.latest_build.max_deadline).isAfter(now.add(2, "hour")));
257+
return (willAutoStop && !noRequiredStopSoon) || willAutoStart;
258258
};
259259

260260
const styles = {

site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const meta: Meta<typeof WorkspaceTopbar> = {
2828
workspace: baseWorkspace,
2929
template: MockTemplate,
3030
latestVersion: MockTemplateVersion,
31+
canUpdateWorkspace: true,
3132
},
3233
parameters: {
3334
layout: "fullscreen",
@@ -112,6 +113,25 @@ export const ConnectedWithDeadline: Story = {
112113
},
113114
};
114115
export const ConnectedWithMaxDeadline: Story = {
116+
args: {
117+
workspace: {
118+
...MockWorkspace,
119+
get last_used_at() {
120+
return new Date().toISOString();
121+
},
122+
latest_build: {
123+
...MockWorkspace.latest_build,
124+
get deadline() {
125+
return addHours(new Date(), 1).toISOString();
126+
},
127+
get max_deadline() {
128+
return addHours(new Date(), 8).toISOString();
129+
},
130+
},
131+
},
132+
},
133+
};
134+
export const ConnectedWithMaxDeadlineSoon: Story = {
115135
args: {
116136
workspace: {
117137
...MockWorkspace,

site/src/pages/WorkspacePage/WorkspaceTopbar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useQuery } from "react-query";
99
import { Link as RouterLink } from "react-router-dom";
1010
import type * as TypesGen from "api/typesGenerated";
1111
import { workspaceQuota } from "api/queries/workspaceQuota";
12+
import { useTime } from "hooks/useTime";
1213
import { WorkspaceStatusBadge } from "modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge";
1314
import { useDashboard } from "modules/dashboard/useDashboard";
1415
import { getWorkspaceActivityStatus } from "modules/workspaces/activity";
@@ -108,7 +109,7 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
108109
allowAdvancedScheduling,
109110
);
110111

111-
const activityStatus = getWorkspaceActivityStatus(workspace);
112+
const activityStatus = useTime(() => getWorkspaceActivityStatus(workspace));
112113

113114
return (
114115
<Topbar css={{ gridArea: "topbar" }}>

site/src/pages/WorkspacesPage/LastUsed.tsx

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,29 +32,33 @@ interface LastUsedProps {
3232
}
3333

3434
export const LastUsed: FC<LastUsedProps> = ({ lastUsedAt }) => {
35-
useTime();
3635
const theme = useTheme();
37-
const t = dayjs(lastUsedAt);
38-
const now = dayjs();
39-
let message = t.fromNow();
40-
let circle = (
41-
<Circle color={theme.palette.text.secondary} variant="outlined" />
42-
);
4336

44-
if (t.isAfter(now.subtract(1, "hour"))) {
45-
circle = <Circle color={theme.roles.success.fill.solid} />;
46-
// Since the agent reports on a 10m interval,
47-
// the last_used_at can be inaccurate when recent.
48-
message = "Now";
49-
} else if (t.isAfter(now.subtract(3, "day"))) {
50-
circle = <Circle color={theme.palette.text.secondary} />;
51-
} else if (t.isAfter(now.subtract(1, "month"))) {
52-
circle = <Circle color={theme.roles.warning.fill.solid} />;
53-
} else if (t.isAfter(now.subtract(100, "year"))) {
54-
circle = <Circle color={theme.roles.error.fill.solid} />;
55-
} else {
56-
message = "Never";
57-
}
37+
const [circle, message] = useTime(() => {
38+
const t = dayjs(lastUsedAt);
39+
const now = dayjs();
40+
let message = t.fromNow();
41+
let circle = (
42+
<Circle color={theme.palette.text.secondary} variant="outlined" />
43+
);
44+
45+
if (t.isAfter(now.subtract(1, "hour"))) {
46+
circle = <Circle color={theme.roles.success.fill.solid} />;
47+
// Since the agent reports on a 10m interval,
48+
// the last_used_at can be inaccurate when recent.
49+
message = "Now";
50+
} else if (t.isAfter(now.subtract(3, "day"))) {
51+
circle = <Circle color={theme.palette.text.secondary} />;
52+
} else if (t.isAfter(now.subtract(1, "month"))) {
53+
circle = <Circle color={theme.roles.warning.fill.solid} />;
54+
} else if (t.isAfter(now.subtract(100, "year"))) {
55+
circle = <Circle color={theme.roles.error.fill.solid} />;
56+
} else {
57+
message = "Never";
58+
}
59+
60+
return [circle, message];
61+
});
5862

5963
return (
6064
<Stack

site/src/utils/schedule.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -114,29 +114,33 @@ export const autostopDisplay = (
114114
const deadline = dayjs(workspace.latest_build.deadline).tz(
115115
dayjs.tz.guess(),
116116
);
117+
const maxDeadline = dayjs(workspace.latest_build.max_deadline);
117118
const now = dayjs(workspace.latest_build.deadline);
119+
120+
if (activityStatus === "connected") {
121+
if (maxDeadline.isBefore(now.add(2, "hour"))) {
122+
return {
123+
message: `Required to stop soon`,
124+
tooltip: (
125+
<>
126+
<HelpTooltipTitle>Upcoming stop required</HelpTooltipTitle>
127+
This workspace will be required to stop by{" "}
128+
{dayjs(workspace.latest_build.max_deadline).format(
129+
"MMMM D [at] h:mm A",
130+
)}
131+
. You can restart your workspace before then to avoid
132+
interruption.
133+
</>
134+
),
135+
danger: true,
136+
};
137+
}
138+
}
139+
118140
if (isShuttingDown(workspace, deadline)) {
119141
return {
120142
message: Language.workspaceShuttingDownLabel,
121143
};
122-
} else if (
123-
activityStatus === "connected" &&
124-
deadline.isBefore(now.add(2, "hour"))
125-
) {
126-
return {
127-
message: `Required to stop soon`,
128-
tooltip: (
129-
<>
130-
<HelpTooltipTitle>Upcoming stop required</HelpTooltipTitle>
131-
This workspace will be required to stop by{" "}
132-
{dayjs(workspace.latest_build.max_deadline).format(
133-
"MMMM D [at] h:mm A",
134-
)}
135-
. You can restart your workspace before then to avoid interruption.
136-
</>
137-
),
138-
danger: true,
139-
};
140144
} else {
141145
let title = (
142146
<HelpTooltipTitle>Template Autostop requirement</HelpTooltipTitle>

0 commit comments

Comments
 (0)