Skip to content

Commit d65eea8

Browse files
stirbyaslilacangrycub
authored
fix: fix workspace actions options (#13572) (#14071)
* fix: fix workspace actions options (#13572) (cherry picked from commit 07cd9ac) * fix: change time format string from 15:40 to 15:04 (#14033) * Change string format to constant value (cherry picked from commit eacdfb9) --------- Co-authored-by: Kayla Washburn-Love <mckayla@hey.com> Co-authored-by: Charlie Voiselle <464492+angrycub@users.noreply.github.com>
1 parent 2f0c2d7 commit d65eea8

File tree

7 files changed

+153
-109
lines changed

7 files changed

+153
-109
lines changed

enterprise/coderd/users.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"github.com/coder/coder/v2/codersdk"
1515
)
1616

17+
const TimeFormatHHMM = "15:04"
18+
1719
func (api *API) autostopRequirementEnabledMW(next http.Handler) http.Handler {
1820
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
1921
// Entitlement must be enabled.
@@ -66,7 +68,7 @@ func (api *API) userQuietHoursSchedule(rw http.ResponseWriter, r *http.Request)
6668
RawSchedule: opts.Schedule.String(),
6769
UserSet: opts.UserSet,
6870
UserCanSet: opts.UserCanSet,
69-
Time: opts.Schedule.TimeParsed().Format("15:40"),
71+
Time: opts.Schedule.TimeParsed().Format(TimeFormatHHMM),
7072
Timezone: opts.Schedule.Location().String(),
7173
Next: opts.Schedule.Next(time.Now().In(opts.Schedule.Location())),
7274
})
@@ -118,7 +120,7 @@ func (api *API) putUserQuietHoursSchedule(rw http.ResponseWriter, r *http.Reques
118120
RawSchedule: opts.Schedule.String(),
119121
UserSet: opts.UserSet,
120122
UserCanSet: opts.UserCanSet,
121-
Time: opts.Schedule.TimeParsed().Format("15:40"),
123+
Time: opts.Schedule.TimeParsed().Format(TimeFormatHHMM),
122124
Timezone: opts.Schedule.Location().String(),
123125
Next: opts.Schedule.Next(time.Now().In(opts.Schedule.Location())),
124126
})

enterprise/coderd/users_test.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import (
1111
"github.com/coder/coder/v2/coderd/coderdtest"
1212
"github.com/coder/coder/v2/coderd/schedule/cron"
1313
"github.com/coder/coder/v2/codersdk"
14+
"github.com/coder/coder/v2/enterprise/coderd"
1415
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
1516
"github.com/coder/coder/v2/enterprise/coderd/license"
1617
"github.com/coder/coder/v2/testutil"
1718
)
1819

20+
const TimeFormatHHMM = coderd.TimeFormatHHMM
21+
1922
func TestUserQuietHours(t *testing.T) {
2023
t.Parallel()
2124

@@ -41,15 +44,17 @@ func TestUserQuietHours(t *testing.T) {
4144

4245
t.Run("OK", func(t *testing.T) {
4346
t.Parallel()
44-
45-
defaultQuietHoursSchedule := "CRON_TZ=America/Chicago 0 1 * * *"
47+
// Using 10 for minutes lets us test a format bug in which values greater
48+
// than 5 were causing the API to explode because the time was returned
49+
// incorrectly
50+
defaultQuietHoursSchedule := "CRON_TZ=America/Chicago 10 1 * * *"
4651
defaultScheduleParsed, err := cron.Daily(defaultQuietHoursSchedule)
4752
require.NoError(t, err)
4853
nextTime := defaultScheduleParsed.Next(time.Now().In(defaultScheduleParsed.Location()))
4954
if time.Until(nextTime) < time.Hour {
5055
// Use a different default schedule instead, because we want to avoid
5156
// the schedule "ticking over" during this test run.
52-
defaultQuietHoursSchedule = "CRON_TZ=America/Chicago 0 13 * * *"
57+
defaultQuietHoursSchedule = "CRON_TZ=America/Chicago 10 13 * * *"
5358
defaultScheduleParsed, err = cron.Daily(defaultQuietHoursSchedule)
5459
require.NoError(t, err)
5560
}
@@ -78,7 +83,7 @@ func TestUserQuietHours(t *testing.T) {
7883
require.NoError(t, err)
7984
require.Equal(t, defaultScheduleParsed.String(), sched1.RawSchedule)
8085
require.False(t, sched1.UserSet)
81-
require.Equal(t, defaultScheduleParsed.TimeParsed().Format("15:40"), sched1.Time)
86+
require.Equal(t, defaultScheduleParsed.TimeParsed().Format(TimeFormatHHMM), sched1.Time)
8287
require.Equal(t, defaultScheduleParsed.Location().String(), sched1.Timezone)
8388
require.WithinDuration(t, defaultScheduleParsed.Next(time.Now()), sched1.Next, 15*time.Second)
8489

@@ -101,7 +106,7 @@ func TestUserQuietHours(t *testing.T) {
101106
require.NoError(t, err)
102107
require.Equal(t, customScheduleParsed.String(), sched2.RawSchedule)
103108
require.True(t, sched2.UserSet)
104-
require.Equal(t, customScheduleParsed.TimeParsed().Format("15:40"), sched2.Time)
109+
require.Equal(t, customScheduleParsed.TimeParsed().Format(TimeFormatHHMM), sched2.Time)
105110
require.Equal(t, customScheduleParsed.Location().String(), sched2.Timezone)
106111
require.WithinDuration(t, customScheduleParsed.Next(time.Now()), sched2.Next, 15*time.Second)
107112

@@ -110,7 +115,7 @@ func TestUserQuietHours(t *testing.T) {
110115
require.NoError(t, err)
111116
require.Equal(t, customScheduleParsed.String(), sched3.RawSchedule)
112117
require.True(t, sched3.UserSet)
113-
require.Equal(t, customScheduleParsed.TimeParsed().Format("15:40"), sched3.Time)
118+
require.Equal(t, customScheduleParsed.TimeParsed().Format(TimeFormatHHMM), sched3.Time)
114119
require.Equal(t, customScheduleParsed.Location().String(), sched3.Timezone)
115120
require.WithinDuration(t, customScheduleParsed.Next(time.Now()), sched3.Next, 15*time.Second)
116121

site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx

+18-6
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,21 @@ export const StartButton: FC<ActionButtonPropsWithWorkspace> = ({
9797
);
9898
};
9999

100+
export const UpdateAndStartButton: FC<ActionButtonProps> = ({
101+
handleAction,
102+
}) => {
103+
return (
104+
<Tooltip title="This template requires automatic updates on workspace startup. Contact your administrator if you want to preserve the template version.">
105+
<TopbarButton
106+
startIcon={<PlayCircleOutlineIcon />}
107+
onClick={() => handleAction()}
108+
>
109+
Update and start&hellip;
110+
</TopbarButton>
111+
</Tooltip>
112+
);
113+
};
114+
100115
export const StopButton: FC<ActionButtonProps> = ({
101116
handleAction,
102117
loading,
@@ -146,16 +161,13 @@ export const RestartButton: FC<ActionButtonPropsWithWorkspace> = ({
146161
);
147162
};
148163

149-
export const UpdateAndStartButton: FC<ActionButtonProps> = ({
164+
export const UpdateAndRestartButton: FC<ActionButtonProps> = ({
150165
handleAction,
151166
}) => {
152167
return (
153168
<Tooltip title="This template requires automatic updates on workspace startup. Contact your administrator if you want to preserve the template version.">
154-
<TopbarButton
155-
startIcon={<PlayCircleOutlineIcon />}
156-
onClick={() => handleAction()}
157-
>
158-
Update and start&hellip;
169+
<TopbarButton startIcon={<ReplayIcon />} onClick={() => handleAction()}>
170+
Update and restart&hellip;
159171
</TopbarButton>
160172
</Tooltip>
161173
);

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

+69-33
Original file line numberDiff line numberDiff line change
@@ -26,93 +26,128 @@ export const Running: Story = {
2626
},
2727
};
2828

29-
export const Stopping: Story = {
29+
export const RunningUpdateAvailable: Story = {
30+
name: "Running (Update available)",
3031
args: {
31-
workspace: Mocks.MockStoppingWorkspace,
32+
workspace: {
33+
...Mocks.MockWorkspace,
34+
outdated: true,
35+
},
3236
},
3337
};
3438

35-
export const Stopped: Story = {
39+
export const RunningRequireActiveVersion: Story = {
40+
name: "Running (No required update)",
3641
args: {
37-
workspace: Mocks.MockStoppedWorkspace,
42+
workspace: {
43+
...Mocks.MockWorkspace,
44+
template_require_active_version: true,
45+
},
3846
},
3947
};
4048

41-
export const Canceling: Story = {
49+
export const RunningUpdateRequired: Story = {
50+
name: "Running (Update Required)",
4251
args: {
43-
workspace: Mocks.MockCancelingWorkspace,
52+
workspace: {
53+
...Mocks.MockWorkspace,
54+
template_require_active_version: true,
55+
outdated: true,
56+
},
4457
},
4558
};
4659

47-
export const Canceled: Story = {
60+
export const Stopping: Story = {
4861
args: {
49-
workspace: Mocks.MockCanceledWorkspace,
62+
workspace: Mocks.MockStoppingWorkspace,
5063
},
5164
};
5265

53-
export const Deleting: Story = {
66+
export const Stopped: Story = {
5467
args: {
55-
workspace: Mocks.MockDeletingWorkspace,
68+
workspace: Mocks.MockStoppedWorkspace,
5669
},
5770
};
5871

59-
export const Deleted: Story = {
72+
export const StoppedUpdateAvailable: Story = {
73+
name: "Stopped (Update available)",
6074
args: {
61-
workspace: Mocks.MockDeletedWorkspace,
75+
workspace: {
76+
...Mocks.MockStoppedWorkspace,
77+
outdated: true,
78+
},
6279
},
6380
};
6481

65-
export const Outdated: Story = {
82+
export const StoppedRequireActiveVersion: Story = {
83+
name: "Stopped (No required update)",
84+
args: {
85+
workspace: {
86+
...Mocks.MockStoppedWorkspace,
87+
template_require_active_version: true,
88+
},
89+
},
90+
};
91+
92+
export const StoppedUpdateRequired: Story = {
93+
name: "Stopped (Update Required)",
94+
args: {
95+
workspace: {
96+
...Mocks.MockStoppedWorkspace,
97+
template_require_active_version: true,
98+
outdated: true,
99+
},
100+
},
101+
};
102+
103+
export const Updating: Story = {
66104
args: {
67105
workspace: Mocks.MockOutdatedWorkspace,
106+
isUpdating: true,
68107
},
69108
};
70109

71-
export const Failed: Story = {
110+
export const Restarting: Story = {
72111
args: {
73-
workspace: Mocks.MockFailedWorkspace,
112+
workspace: Mocks.MockStoppingWorkspace,
113+
isRestarting: true,
74114
},
75115
};
76116

77-
export const FailedWithDebug: Story = {
117+
export const Canceling: Story = {
78118
args: {
79-
workspace: Mocks.MockFailedWorkspace,
80-
canDebug: true,
119+
workspace: Mocks.MockCancelingWorkspace,
81120
},
82121
};
83122

84-
export const Updating: Story = {
123+
export const Deleting: Story = {
85124
args: {
86-
isUpdating: true,
87-
workspace: Mocks.MockOutdatedWorkspace,
125+
workspace: Mocks.MockDeletingWorkspace,
88126
},
89127
};
90128

91-
export const RequireActiveVersionStarted: Story = {
129+
export const Deleted: Story = {
92130
args: {
93-
workspace: Mocks.MockOutdatedRunningWorkspaceRequireActiveVersion,
94-
canChangeVersions: false,
131+
workspace: Mocks.MockDeletedWorkspace,
95132
},
96133
};
97134

98-
export const RequireActiveVersionStopped: Story = {
135+
export const Outdated: Story = {
99136
args: {
100-
workspace: Mocks.MockOutdatedStoppedWorkspaceRequireActiveVersion,
101-
canChangeVersions: false,
137+
workspace: Mocks.MockOutdatedWorkspace,
102138
},
103139
};
104140

105-
export const AlwaysUpdateStarted: Story = {
141+
export const Failed: Story = {
106142
args: {
107-
workspace: Mocks.MockOutdatedRunningWorkspaceAlwaysUpdate,
108-
canChangeVersions: true,
143+
workspace: Mocks.MockFailedWorkspace,
109144
},
110145
};
111146

112-
export const AlwaysUpdateStopped: Story = {
147+
export const FailedWithDebug: Story = {
113148
args: {
114-
workspace: Mocks.MockOutdatedStoppedWorkspaceAlwaysUpdate,
115-
canChangeVersions: true,
149+
workspace: Mocks.MockFailedWorkspace,
150+
canDebug: true,
116151
},
117152
};
118153

@@ -125,6 +160,7 @@ export const CancelShownForOwner: Story = {
125160
isOwner: true,
126161
},
127162
};
163+
128164
export const CancelShownForUser: Story = {
129165
args: {
130166
workspace: Mocks.MockStartingWorkspace,

site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx

+15-28
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
ActivateButton,
2626
FavoriteButton,
2727
UpdateAndStartButton,
28+
UpdateAndRestartButton,
2829
} from "./Buttons";
2930
import { type ActionType, abilitiesByWorkspaceStatus } from "./constants";
3031
import { DebugButton } from "./DebugButton";
@@ -85,12 +86,12 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
8586

8687
const mustUpdate = mustUpdateWorkspace(workspace, canChangeVersions);
8788
const tooltipText = getTooltipText(workspace, mustUpdate, canChangeVersions);
88-
const canBeUpdated = workspace.outdated && canAcceptJobs;
8989

9090
// A mapping of button type to the corresponding React component
9191
const buttonMapping: Record<ActionType, ReactNode> = {
9292
update: <UpdateButton handleAction={handleUpdate} />,
9393
updateAndStart: <UpdateAndStartButton handleAction={handleUpdate} />,
94+
updateAndRestart: <UpdateAndRestartButton handleAction={handleUpdate} />,
9495
updating: <UpdateButton loading handleAction={handleUpdate} />,
9596
start: (
9697
<StartButton
@@ -148,43 +149,29 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
148149
enableBuildParameters={workspace.latest_build.transition === "start"}
149150
/>
150151
),
151-
toggleFavorite: (
152-
<FavoriteButton
153-
workspaceID={workspace.id}
154-
isFavorite={workspace.favorite}
155-
onToggle={handleToggleFavorite}
156-
/>
157-
),
158152
};
159153

160154
return (
161155
<div
162156
css={{ display: "flex", alignItems: "center", gap: 8 }}
163157
data-testid="workspace-actions"
164158
>
165-
{canBeUpdated && (
166-
<>
167-
{isUpdating
168-
? buttonMapping.updating
169-
: workspace.template_require_active_version
170-
? buttonMapping.updateAndStart
171-
: buttonMapping.update}
172-
</>
173-
)}
174-
175-
{!canBeUpdated &&
176-
workspace.template_require_active_version &&
177-
buttonMapping.start}
178-
179-
{isRestarting
180-
? buttonMapping.restarting
181-
: actions.map((action) => (
182-
<Fragment key={action}>{buttonMapping[action]}</Fragment>
183-
))}
159+
{/* Restarting must be handled separately, because it otherwise would appear as stopping */}
160+
{isUpdating
161+
? buttonMapping.updating
162+
: isRestarting
163+
? buttonMapping.restarting
164+
: actions.map((action) => (
165+
<Fragment key={action}>{buttonMapping[action]}</Fragment>
166+
))}
184167

185168
{showCancel && <CancelButton handleAction={handleCancel} />}
186169

187-
{buttonMapping.toggleFavorite}
170+
<FavoriteButton
171+
workspaceID={workspace.id}
172+
isFavorite={workspace.favorite}
173+
onToggle={handleToggleFavorite}
174+
/>
188175

189176
<MoreMenu>
190177
<MoreMenuTrigger>

0 commit comments

Comments
 (0)