From 5c443de624501d40aac2c1de3b45d6be1728f94f Mon Sep 17 00:00:00 2001 From: johnstcn Date: Tue, 7 Jun 2022 14:37:30 +0000 Subject: [PATCH 01/17] RED: update unit test with new desired behaviour --- coderd/autobuild/executor/lifecycle_executor_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/coderd/autobuild/executor/lifecycle_executor_test.go b/coderd/autobuild/executor/lifecycle_executor_test.go index 5b1045aea2cab..8560ae05479f7 100644 --- a/coderd/autobuild/executor/lifecycle_executor_test.go +++ b/coderd/autobuild/executor/lifecycle_executor_test.go @@ -440,18 +440,19 @@ func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) { err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: nil}) require.NoError(t, err) - // When: the autobuild executor ticks after the deadline + // When: the autobuild executor ticks after the original deadline go func() { tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute) close(tickCh) }() - // Then: the workspace should still stop - sorry! + // Then: the workspace should not stop stats := <-statsCh assert.NoError(t, stats.Error) - assert.Len(t, stats.Transitions, 1) - assert.Contains(t, stats.Transitions, workspace.ID) - assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID]) + assert.Len(t, stats.Transitions, 0) + // And: the deadline should be updated + updated := coderdtest.MustWorkspace(t, client, workspace.ID) + assert.Zero(t, updated.LatestBuild.Deadline) } func TestExecutorAutostartMultipleOK(t *testing.T) { From c2127c1e6530b11a2d0f38104a0ec35011e9cc30 Mon Sep 17 00:00:00 2001 From: johnstcn Date: Tue, 7 Jun 2022 20:47:13 +0000 Subject: [PATCH 02/17] cli: warn user upon ttl-triggered workspace shutdown --- cli/ttl.go | 38 ++++++++++++++++++++++++ cli/ttl_test.go | 77 ++++++++++++++++++++++++++++++++++--------------- 2 files changed, 91 insertions(+), 24 deletions(-) diff --git a/cli/ttl.go b/cli/ttl.go index cce6ef84a9345..1beadbb3d37f2 100644 --- a/cli/ttl.go +++ b/cli/ttl.go @@ -1,12 +1,14 @@ package cli import ( + "errors" "fmt" "time" "github.com/spf13/cobra" "golang.org/x/xerrors" + "github.com/coder/coder/cli/cliui" "github.com/coder/coder/codersdk" ) @@ -89,6 +91,27 @@ func ttlset() *cobra.Command { _, _ = fmt.Fprintf(cmd.OutOrStdout(), "warning: ttl rounded down to %s\n", truncated) } + if changed, newDeadline := changedNewDeadline(workspace, truncated); changed { + // For the purposes of the user, "less than a minute" is essentially the same as "immediately". + timeRemaining := time.Until(newDeadline).Truncate(time.Minute) + humanRemaining := "in " + timeRemaining.String() + if timeRemaining <= 0 { + humanRemaining = "immediately" + } + _, err = cliui.Prompt(cmd, cliui.PromptOptions{ + Text: fmt.Sprintf( + "Workspace %q will be stopped %s. Are you sure?", + workspace.Name, + humanRemaining, + ), + Default: "yes", + IsConfirm: true, + }) + if errors.Is(err, cliui.Canceled) { + return nil + } + } + millis := truncated.Milliseconds() if err = client.UpdateWorkspaceTTL(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ TTLMillis: &millis, @@ -131,3 +154,18 @@ func ttlunset() *cobra.Command { }, } } + +func changedNewDeadline(ws codersdk.Workspace, newTTL time.Duration) (changed bool, newDeadline time.Time) { + if ws.LatestBuild.Transition != codersdk.WorkspaceTransitionStart { + // not running + return false, newDeadline + } + + if ws.LatestBuild.Job.CompletedAt == nil { + // still building + return false, newDeadline + } + + newDeadline = ws.LatestBuild.Job.CompletedAt.Add(newTTL) + return true, newDeadline +} diff --git a/cli/ttl_test.go b/cli/ttl_test.go index 00a0f29fd3811..92ca201c81a44 100644 --- a/cli/ttl_test.go +++ b/cli/ttl_test.go @@ -3,16 +3,19 @@ package cli_test import ( "bytes" "context" + "fmt" "strings" "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/util/ptr" "github.com/coder/coder/codersdk" + "github.com/coder/coder/pty/ptytest" ) func TestTTL(t *testing.T) { @@ -22,33 +25,29 @@ func TestTTL(t *testing.T) { t.Parallel() var ( - ctx = context.Background() client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) user = coderdtest.CreateFirstUser(t, client) version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) + template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + ttl = 7*time.Hour + 30*time.Minute + 30*time.Second + workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) { + cwr.TTLMillis = ptr.Ref(ttl.Milliseconds()) + }) cmdArgs = []string{"ttl", "show", workspace.Name} - ttl = 8*time.Hour + 30*time.Minute + 30*time.Second stdoutBuf = &bytes.Buffer{} ) - err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ - TTLMillis: ptr.Ref(ttl.Milliseconds()), - }) - require.NoError(t, err) - cmd, root := clitest.New(t, cmdArgs...) clitest.SetupConfig(t, client, root) cmd.SetOut(stdoutBuf) - err = cmd.Execute() + err := cmd.Execute() require.NoError(t, err, "unexpected error") require.Equal(t, ttl.Truncate(time.Minute).String(), strings.TrimSpace(stdoutBuf.String())) }) - t.Run("SetUnsetOK", func(t *testing.T) { + t.Run("UnsetOK", func(t *testing.T) { t.Parallel() var ( @@ -58,9 +57,11 @@ func TestTTL(t *testing.T) { version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID) ttl = 8*time.Hour + 30*time.Minute + 30*time.Second - cmdArgs = []string{"ttl", "set", workspace.Name, ttl.String()} + workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) { + cwr.TTLMillis = ptr.Ref(ttl.Milliseconds()) + }) + cmdArgs = []string{"ttl", "unset", workspace.Name} stdoutBuf = &bytes.Buffer{} ) @@ -71,24 +72,52 @@ func TestTTL(t *testing.T) { err := cmd.Execute() require.NoError(t, err, "unexpected error") - // Ensure ttl updated + // Ensure ttl unset updated, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err, "fetch updated workspace") - require.Equal(t, ttl.Truncate(time.Minute), time.Duration(*updated.TTLMillis)*time.Millisecond) - require.Contains(t, stdoutBuf.String(), "warning: ttl rounded down") + require.Nil(t, updated.TTLMillis, "expected ttl to not be set") + }) - // unset schedule - cmd, root = clitest.New(t, "ttl", "unset", workspace.Name) - clitest.SetupConfig(t, client, root) - cmd.SetOut(stdoutBuf) + t.Run("SetOK", func(t *testing.T) { + t.Parallel() - err = cmd.Execute() - require.NoError(t, err, "unexpected error") + var ( + ctx = context.Background() + client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) + user = coderdtest.CreateFirstUser(t, client) + version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + ttl = 8*time.Hour + 30*time.Minute + 30*time.Second + workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) { + cwr.TTLMillis = ptr.Ref(ttl.Milliseconds()) + }) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + cmdArgs = []string{"ttl", "set", workspace.Name, ttl.String()} + done = make(chan struct{}) + ) + cmd, root := clitest.New(t, cmdArgs...) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t) + cmd.SetIn(pty.Input()) + cmd.SetOut(pty.Output()) + + go func() { + defer close(done) + err := cmd.Execute() + assert.NoError(t, err, "unexpected error") + }() + + pty.ExpectMatch(fmt.Sprintf("warning: ttl rounded down to %s", ttl.Truncate(time.Minute))) + pty.ExpectMatch(fmt.Sprintf("Workspace %q will be stopped in 8h29m0s. Are you sure?", workspace.Name)) + pty.WriteLine("yes") // Ensure ttl updated - updated, err = client.Workspace(ctx, workspace.ID) + updated, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err, "fetch updated workspace") - require.Nil(t, updated.TTLMillis, "expected ttl to not be set") + require.Equal(t, ttl.Truncate(time.Minute), time.Duration(*updated.TTLMillis)*time.Millisecond) + + <-done }) t.Run("ZeroInvalid", func(t *testing.T) { From 07320a42a94185c9bc539c73e1fe61514c7f7e33 Mon Sep 17 00:00:00 2001 From: johnstcn Date: Wed, 8 Jun 2022 12:41:22 +0000 Subject: [PATCH 03/17] GREEN: do the thing --- coderd/workspaces.go | 48 ++++++++++++++++++++++++++++++++++++++- coderd/workspaces_test.go | 21 +++++++++++------ 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 25fd67d581cba..7f76c04876a4f 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -539,11 +539,57 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { }) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: "Internal error updating workspace TTL.", + Message: "Error updating workspace TTL!", Detail: err.Error(), }) return } + + // Also extend the workspace deadline if the workspace is running + latestBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "Error fetching latest workspace build!", + Detail: err.Error(), + }) + return + } + + if latestBuild.Transition != database.WorkspaceTransitionStart { + httpapi.Write(rw, http.StatusOK, nil) + return + } + + if latestBuild.UpdatedAt.IsZero() { + // Build in progress; provisionerd should update with the new TTL. + httpapi.Write(rw, http.StatusOK, nil) + return + } + + var newDeadline time.Time + if dbTTL.Valid { + newDeadline = latestBuild.UpdatedAt.Add(time.Duration(dbTTL.Int64)) + } + + err = api.Database.UpdateWorkspaceBuildByID( + r.Context(), + database.UpdateWorkspaceBuildByIDParams{ + ID: latestBuild.ID, + UpdatedAt: latestBuild.UpdatedAt, + ProvisionerState: latestBuild.ProvisionerState, + Deadline: newDeadline, + }, + ) + + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "Internal error extending workspace deadline.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(rw, http.StatusOK, nil) } func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) { diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 665c2d0a49d88..73b8be1ca5136 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -521,19 +521,16 @@ func TestWorkspaceUpdateAutostart(t *testing.T) { name: "invalid location", schedule: ptr.Ref("CRON_TZ=Imaginary/Place 30 9 * * 1-5"), expectedError: "parse schedule: provided bad location Imaginary/Place: unknown time zone Imaginary/Place", - // expectedError: "status code 500: Invalid autostart schedule\n\tError: parse schedule: provided bad location Imaginary/Place: unknown time zone Imaginary/Place", }, { name: "invalid schedule", schedule: ptr.Ref("asdf asdf asdf "), expectedError: `validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ= prefix`, - // expectedError: "status code 500: Invalid autostart schedule\n\tError: validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ= prefix", }, { name: "only 3 values", schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 *"), expectedError: `validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ= prefix`, - // expectedError: "status code 500: Invalid autostart schedule\n\tError: validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ= prefix", }, } @@ -611,16 +608,23 @@ func TestWorkspaceUpdateTTL(t *testing.T) { t.Parallel() testCases := []struct { - name string - ttlMillis *int64 - expectedError string - modifyTemplate func(*codersdk.CreateTemplateRequest) + name string + ttlMillis *int64 + expectedError string + expectedDeadline time.Time + modifyTemplate func(*codersdk.CreateTemplateRequest) }{ { name: "disable ttl", ttlMillis: nil, expectedError: "", }, + { + name: "update ttl", + ttlMillis: ptr.Ref(12 * time.Hour.Milliseconds()), + expectedError: "", + expectedDeadline: time.Now().Add(12 * time.Hour), + }, { name: "below minimum ttl", ttlMillis: ptr.Ref((30 * time.Second).Milliseconds()), @@ -686,6 +690,9 @@ func TestWorkspaceUpdateTTL(t *testing.T) { require.NoError(t, err, "fetch updated workspace") require.Equal(t, testCase.ttlMillis, updated.TTLMillis, "expected autostop ttl to equal requested") + if !testCase.expectedDeadline.IsZero() { + require.WithinDuration(t, testCase.expectedDeadline, updated.LatestBuild.Deadline, time.Minute, "expected autostop deadline to be equal expected") + } }) } From 7846dd8640c1b28af6959c4cfe178e8bf292756d Mon Sep 17 00:00:00 2001 From: johnstcn Date: Wed, 8 Jun 2022 12:58:08 +0000 Subject: [PATCH 04/17] REFACTOR: coderd: putWorkspaceTTL: wrap in tx --- coderd/workspaces.go | 81 ++++++++++++++++++--------------------- coderd/workspaces_test.go | 29 +++++++------- 2 files changed, 54 insertions(+), 56 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 7f76c04876a4f..5e02cc152a63c 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -533,57 +533,52 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { return } - err = api.Database.UpdateWorkspaceTTL(r.Context(), database.UpdateWorkspaceTTLParams{ - ID: workspace.ID, - Ttl: dbTTL, - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: "Error updating workspace TTL!", - Detail: err.Error(), - }) - return - } + err = api.Database.InTx(func(s database.Store) error { + if err := s.UpdateWorkspaceTTL(r.Context(), database.UpdateWorkspaceTTLParams{ + ID: workspace.ID, + Ttl: dbTTL, + }); err != nil { + return xerrors.Errorf("update workspace TTL: %w", err) + } - // Also extend the workspace deadline if the workspace is running - latestBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: "Error fetching latest workspace build!", - Detail: err.Error(), - }) - return - } + // Also extend the workspace deadline if the workspace is running + latestBuild, err := s.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) + if err != nil { + return xerrors.Errorf("get latest workspace build: %w", err) + } - if latestBuild.Transition != database.WorkspaceTransitionStart { - httpapi.Write(rw, http.StatusOK, nil) - return - } + if latestBuild.Transition != database.WorkspaceTransitionStart { + return nil // nothing to do + } - if latestBuild.UpdatedAt.IsZero() { - // Build in progress; provisionerd should update with the new TTL. - httpapi.Write(rw, http.StatusOK, nil) - return - } + if latestBuild.UpdatedAt.IsZero() { + // Build in progress; provisionerd should update with the new TTL. + return nil + } - var newDeadline time.Time - if dbTTL.Valid { - newDeadline = latestBuild.UpdatedAt.Add(time.Duration(dbTTL.Int64)) - } + var newDeadline time.Time + if dbTTL.Valid { + newDeadline = latestBuild.UpdatedAt.Add(time.Duration(dbTTL.Int64)) + } - err = api.Database.UpdateWorkspaceBuildByID( - r.Context(), - database.UpdateWorkspaceBuildByIDParams{ - ID: latestBuild.ID, - UpdatedAt: latestBuild.UpdatedAt, - ProvisionerState: latestBuild.ProvisionerState, - Deadline: newDeadline, - }, - ) + if err := s.UpdateWorkspaceBuildByID( + r.Context(), + database.UpdateWorkspaceBuildByIDParams{ + ID: latestBuild.ID, + UpdatedAt: latestBuild.UpdatedAt, + ProvisionerState: latestBuild.ProvisionerState, + Deadline: newDeadline, + }, + ); err != nil { + return xerrors.Errorf("update workspace deadline: %w", err) + } + + return nil + }) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: "Internal error extending workspace deadline.", + Message: "Error updating workspace time until shutdown!", Detail: err.Error(), }) return diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 73b8be1ca5136..9244991078a33 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -611,19 +611,20 @@ func TestWorkspaceUpdateTTL(t *testing.T) { name string ttlMillis *int64 expectedError string - expectedDeadline time.Time + expectedDeadline *time.Time modifyTemplate func(*codersdk.CreateTemplateRequest) }{ { - name: "disable ttl", - ttlMillis: nil, - expectedError: "", + name: "disable ttl", + ttlMillis: nil, + expectedError: "", + expectedDeadline: ptr.Ref(time.Time{}), }, { name: "update ttl", ttlMillis: ptr.Ref(12 * time.Hour.Milliseconds()), expectedError: "", - expectedDeadline: time.Now().Add(12 * time.Hour), + expectedDeadline: ptr.Ref(time.Now().Add(12 * time.Hour)), }, { name: "below minimum ttl", @@ -631,14 +632,16 @@ func TestWorkspaceUpdateTTL(t *testing.T) { expectedError: "ttl must be at least one minute", }, { - name: "minimum ttl", - ttlMillis: ptr.Ref(time.Minute.Milliseconds()), - expectedError: "", + name: "minimum ttl", + ttlMillis: ptr.Ref(time.Minute.Milliseconds()), + expectedError: "", + expectedDeadline: ptr.Ref(time.Now().Add(time.Minute)), }, { - name: "maximum ttl", - ttlMillis: ptr.Ref((24 * 7 * time.Hour).Milliseconds()), - expectedError: "", + name: "maximum ttl", + ttlMillis: ptr.Ref((24 * 7 * time.Hour).Milliseconds()), + expectedError: "", + expectedDeadline: ptr.Ref(time.Now().Add(24 * 7 * time.Hour)), }, { name: "above maximum ttl", @@ -690,8 +693,8 @@ func TestWorkspaceUpdateTTL(t *testing.T) { require.NoError(t, err, "fetch updated workspace") require.Equal(t, testCase.ttlMillis, updated.TTLMillis, "expected autostop ttl to equal requested") - if !testCase.expectedDeadline.IsZero() { - require.WithinDuration(t, testCase.expectedDeadline, updated.LatestBuild.Deadline, time.Minute, "expected autostop deadline to be equal expected") + if testCase.expectedDeadline != nil { + require.WithinDuration(t, *testCase.expectedDeadline, updated.LatestBuild.Deadline, time.Minute, "expected autostop deadline to be equal expected") } }) } From 6d7187d470ca21a27ae02428493c8602be82a1eb Mon Sep 17 00:00:00 2001 From: johnstcn Date: Wed, 8 Jun 2022 14:08:20 +0000 Subject: [PATCH 05/17] update unit tests --- .../executor/lifecycle_executor_test.go | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/coderd/autobuild/executor/lifecycle_executor_test.go b/coderd/autobuild/executor/lifecycle_executor_test.go index 8560ae05479f7..be435b7730ab8 100644 --- a/coderd/autobuild/executor/lifecycle_executor_test.go +++ b/coderd/autobuild/executor/lifecycle_executor_test.go @@ -440,19 +440,41 @@ func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) { err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: nil}) require.NoError(t, err) + // Then: the deadline should be the zero value + updated := coderdtest.MustWorkspace(t, client, workspace.ID) + assert.Zero(t, updated.LatestBuild.Deadline) + // When: the autobuild executor ticks after the original deadline go func() { tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute) - close(tickCh) }() // Then: the workspace should not stop stats := <-statsCh assert.NoError(t, stats.Error) assert.Len(t, stats.Transitions, 0) - // And: the deadline should be updated - updated := coderdtest.MustWorkspace(t, client, workspace.ID) - assert.Zero(t, updated.LatestBuild.Deadline) + + // Given: the user changes their mind again and wants to enable auto-stop + newTTL := 8 * time.Hour + expectedDeadline := workspace.LatestBuild.UpdatedAt.Add(newTTL) + err = client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: ptr.Ref(newTTL.Milliseconds())}) + require.NoError(t, err) + + // Then: the deadline should be updated based on the TTL + updated = coderdtest.MustWorkspace(t, client, workspace.ID) + assert.WithinDuration(t, expectedDeadline, updated.LatestBuild.Deadline, time.Minute) + + // When: the relentless onward march of time continues + go func() { + tickCh <- workspace.LatestBuild.Deadline.Add(newTTL + time.Minute) + close(tickCh) + }() + + // Then: the workspace should stop + stats = <-statsCh + assert.NoError(t, stats.Error) + assert.Len(t, stats.Transitions, 1) + assert.Equal(t, stats.Transitions[workspace.ID], database.WorkspaceTransitionStop) } func TestExecutorAutostartMultipleOK(t *testing.T) { From daa9a5febfdf1e0e61521a23c52231b2a1df267b Mon Sep 17 00:00:00 2001 From: johnstcn Date: Wed, 8 Jun 2022 21:20:35 +0000 Subject: [PATCH 06/17] feat: WorkspaceScheduleForm: show shutdown time when editing workspace TTL --- .../WorkspaceScheduleForm.stories.tsx | 107 +++++++++++++++++- .../WorkspaceScheduleForm.tsx | 57 ++++++++-- .../WorkspaceSchedulePage.tsx | 2 + 3 files changed, 151 insertions(+), 15 deletions(-) diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx index 27d62f2482a0c..f3a3d02d6dcc3 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx @@ -1,6 +1,19 @@ import { action } from "@storybook/addon-actions" import { Story } from "@storybook/react" -import { WorkspaceScheduleForm, WorkspaceScheduleFormProps } from "./WorkspaceScheduleForm" +import dayjs from "dayjs" +import advancedFormat from "dayjs/plugin/advancedFormat" +import timezone from "dayjs/plugin/timezone" +import utc from "dayjs/plugin/utc" +import * as Mocks from "../../testHelpers/entities" +import { + WorkspaceScheduleForm, + WorkspaceScheduleFormInitialValues, + WorkspaceScheduleFormProps, +} from "./WorkspaceScheduleForm" + +dayjs.extend(advancedFormat) +dayjs.extend(utc) +dayjs.extend(timezone) export default { title: "components/WorkspaceScheduleForm", @@ -9,8 +22,96 @@ export default { const Template: Story = (args) => -export const Example = Template.bind({}) -Example.args = { +export const WorkspaceNotRunning = Template.bind({}) +WorkspaceNotRunning.args = { + now: dayjs("2022-05-17T17:40:00Z"), + initialValues: { + ...WorkspaceScheduleFormInitialValues, + timezone: "UTC", + }, + workspace: { + ...Mocks.MockWorkspace, + latest_build: { + ...Mocks.MockWorkspaceBuild, + transition: "stop", + updated_at: "2022-05-17T17:39:00Z", + }, + }, + onCancel: () => action("onCancel"), + onSubmit: () => action("onSubmit"), +} + +export const WorkspaceWillNotShutDown = Template.bind({}) +WorkspaceWillNotShutDown.args = { + now: dayjs("2022-05-17T17:40:00Z"), + initialValues: { + ...WorkspaceScheduleFormInitialValues, + timezone: "UTC", + ttl: 0, + }, + workspace: { + ...Mocks.MockWorkspace, + latest_build: { + ...Mocks.MockWorkspaceBuild, + updated_at: "2022-05-17T17:39:00Z", + }, + }, + onCancel: () => action("onCancel"), + onSubmit: () => action("onSubmit"), +} + +export const WorkspaceWillShutdown = Template.bind({}) +WorkspaceWillShutdown.args = { + now: dayjs("2022-05-17T17:40:00Z"), + initialValues: { + ...WorkspaceScheduleFormInitialValues, + timezone: "UTC", + }, + workspace: { + ...Mocks.MockWorkspace, + latest_build: { + ...Mocks.MockWorkspaceBuild, + updated_at: "2022-05-17T17:39:00Z", + }, + }, + onCancel: () => action("onCancel"), + onSubmit: () => action("onSubmit"), +} + +export const WorkspaceWillShutdownSoon = Template.bind({}) +WorkspaceWillShutdownSoon.args = { + now: dayjs("2022-05-17T18:10:00Z"), + initialValues: { + ...WorkspaceScheduleFormInitialValues, + timezone: "UTC", + ttl: 1, + }, + workspace: { + ...Mocks.MockWorkspace, + latest_build: { + ...Mocks.MockWorkspaceBuild, + updated_at: "2022-05-17T17:39:00Z", + }, + }, + onCancel: () => action("onCancel"), + onSubmit: () => action("onSubmit"), +} + +export const WorkspaceWillShutdownImmediately = Template.bind({}) +WorkspaceWillShutdownImmediately.args = { + now: dayjs("2022-05-17T18:40:00Z"), + initialValues: { + ...WorkspaceScheduleFormInitialValues, + timezone: "UTC", + ttl: 1, + }, + workspace: { + ...Mocks.MockWorkspace, + latest_build: { + ...Mocks.MockWorkspaceBuild, + updated_at: "2022-05-17T17:39:00Z", + }, + }, onCancel: () => action("onCancel"), onSubmit: () => action("onSubmit"), } diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx index e4bafbecd1546..a6c7fd5033fa2 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx @@ -8,12 +8,14 @@ import MenuItem from "@material-ui/core/MenuItem" import makeStyles from "@material-ui/core/styles/makeStyles" import TextField from "@material-ui/core/TextField" import dayjs from "dayjs" +import advancedFormat from "dayjs/plugin/advancedFormat" import timezone from "dayjs/plugin/timezone" import utc from "dayjs/plugin/utc" import { useFormik } from "formik" import { FC } from "react" import * as Yup from "yup" import { FieldErrors } from "../../api/errors" +import { Workspace, WorkspaceBuild } from "../../api/typesGenerated" import { getFormHelpers } from "../../util/formUtils" import { FormFooter } from "../FormFooter/FormFooter" import { FullPageForm } from "../FullPageForm/FullPageForm" @@ -23,6 +25,7 @@ import { zones } from "./zones" // REMARK: timezone plugin depends on UTC // // SEE: https://day.js.org/docs/en/timezone/timezone +dayjs.extend(advancedFormat) dayjs.extend(utc) dayjs.extend(timezone) @@ -44,14 +47,21 @@ export const Language = { timezoneLabel: "Timezone", ttlLabel: "Time until shutdown (hours)", ttlHelperText: "Your workspace will automatically shut down after this amount of time has elapsed.", + ttlCausesShutdownHelperText: "Your workspace will shut down ", + ttlCausesShutdownAt: "at ", + ttlCausesShutdownImmediately: "immediately!", + ttlCausesShutdownSoon: "within 30 minutes.", + ttlCausesNoShutdownHelperText: "Your workspace will not automatically shut down.", } export interface WorkspaceScheduleFormProps { fieldErrors?: FieldErrors initialValues?: WorkspaceScheduleFormValues isLoading: boolean + now: dayjs.Dayjs onCancel: () => void onSubmit: (values: WorkspaceScheduleFormValues) => void + workspace: Workspace } export interface WorkspaceScheduleFormValues { @@ -68,6 +78,20 @@ export interface WorkspaceScheduleFormValues { ttl: number } +export const WorkspaceScheduleFormInitialValues = { + sunday: false, + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: false, + + startTime: "09:30", + timezone: "", + ttl: 5, +} + export const validationSchema = Yup.object({ sunday: Yup.boolean(), monday: Yup.boolean().test("at-least-one-day", Language.errorNoDayOfWeek, function (value) { @@ -154,21 +178,13 @@ export const validationSchema = Yup.object({ export const WorkspaceScheduleForm: FC = ({ fieldErrors, initialValues = { - sunday: false, - monday: true, - tuesday: true, - wednesday: true, - thursday: true, - friday: true, - saturday: false, - - startTime: "09:30", - timezone: "", - ttl: 5, + ...WorkspaceScheduleFormInitialValues, }, isLoading, + now: now, onCancel, onSubmit, + workspace, }) => { const styles = useStyles() @@ -248,7 +264,7 @@ export const WorkspaceScheduleForm: FC = ({ = ({ ) } +const ttlShutdownAt = (now: dayjs.Dayjs, workspace: Workspace, tz: string, newTTL: number): string => { + if (workspace.latest_build.transition !== "start") { + return Language.ttlHelperText + } + if (newTTL === 0) { + return Language.ttlCausesNoShutdownHelperText + } + const newDeadline = dayjs(workspace.latest_build.updated_at).add(newTTL, "hour") + if (newDeadline.isBefore(now)) { + return Language.ttlCausesShutdownHelperText + Language.ttlCausesShutdownImmediately + } + if (newDeadline.isBefore(now.add(30, "minute"))) { + return Language.ttlCausesShutdownHelperText + Language.ttlCausesShutdownSoon + } + return Language.ttlCausesShutdownHelperText + Language.ttlCausesShutdownAt + newDeadline.tz(tz).format("hh:mm A z") +} + const useStyles = makeStyles({ form: { "& input": { diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 00af835a415a8..815adc489a777 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -170,6 +170,8 @@ export const WorkspaceSchedulePage: React.FC = () => { } else if (scheduleState.matches("presentForm") || scheduleState.matches("submittingSchedule")) { return ( Date: Wed, 8 Jun 2022 21:38:07 +0000 Subject: [PATCH 07/17] make it prettier --- .../WorkspaceScheduleForm/WorkspaceScheduleForm.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx index a6c7fd5033fa2..f23bba3e54927 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx @@ -15,7 +15,7 @@ import { useFormik } from "formik" import { FC } from "react" import * as Yup from "yup" import { FieldErrors } from "../../api/errors" -import { Workspace, WorkspaceBuild } from "../../api/typesGenerated" +import { Workspace } from "../../api/typesGenerated" import { getFormHelpers } from "../../util/formUtils" import { FormFooter } from "../FormFooter/FormFooter" import { FullPageForm } from "../FullPageForm/FullPageForm" @@ -47,8 +47,8 @@ export const Language = { timezoneLabel: "Timezone", ttlLabel: "Time until shutdown (hours)", ttlHelperText: "Your workspace will automatically shut down after this amount of time has elapsed.", - ttlCausesShutdownHelperText: "Your workspace will shut down ", - ttlCausesShutdownAt: "at ", + ttlCausesShutdownHelperText: "Your workspace will shut down", + ttlCausesShutdownAt: "at", ttlCausesShutdownImmediately: "immediately!", ttlCausesShutdownSoon: "within 30 minutes.", ttlCausesNoShutdownHelperText: "Your workspace will not automatically shut down.", @@ -287,12 +287,13 @@ const ttlShutdownAt = (now: dayjs.Dayjs, workspace: Workspace, tz: string, newTT } const newDeadline = dayjs(workspace.latest_build.updated_at).add(newTTL, "hour") if (newDeadline.isBefore(now)) { - return Language.ttlCausesShutdownHelperText + Language.ttlCausesShutdownImmediately + return `⚠️ ${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownImmediately} ⚠️` } if (newDeadline.isBefore(now.add(30, "minute"))) { - return Language.ttlCausesShutdownHelperText + Language.ttlCausesShutdownSoon + return `⚠️ ${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownSoon} ⚠️` } - return Language.ttlCausesShutdownHelperText + Language.ttlCausesShutdownAt + newDeadline.tz(tz).format("hh:mm A z") + const newDeadlineString = newDeadline.tz(tz).format("hh:mm A z") + return `${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownAt} ${newDeadlineString}.` } const useStyles = makeStyles({ From 85da57bdc4753821b11adc762e1d66f72462510c Mon Sep 17 00:00:00 2001 From: johnstcn Date: Thu, 9 Jun 2022 10:15:40 +0000 Subject: [PATCH 08/17] hopefully fix unit tests --- coderd/workspaces_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 9244991078a33..8bb5d2364e260 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -624,7 +624,7 @@ func TestWorkspaceUpdateTTL(t *testing.T) { name: "update ttl", ttlMillis: ptr.Ref(12 * time.Hour.Milliseconds()), expectedError: "", - expectedDeadline: ptr.Ref(time.Now().Add(12 * time.Hour)), + expectedDeadline: ptr.Ref(time.Now().Add(12*time.Hour + time.Minute)), }, { name: "below minimum ttl", @@ -635,13 +635,13 @@ func TestWorkspaceUpdateTTL(t *testing.T) { name: "minimum ttl", ttlMillis: ptr.Ref(time.Minute.Milliseconds()), expectedError: "", - expectedDeadline: ptr.Ref(time.Now().Add(time.Minute)), + expectedDeadline: ptr.Ref(time.Now().Add(2 * time.Minute)), }, { name: "maximum ttl", ttlMillis: ptr.Ref((24 * 7 * time.Hour).Milliseconds()), expectedError: "", - expectedDeadline: ptr.Ref(time.Now().Add(24 * 7 * time.Hour)), + expectedDeadline: ptr.Ref(time.Now().Add(24*7*time.Hour + time.Minute)), }, { name: "above maximum ttl", @@ -676,6 +676,7 @@ func TestWorkspaceUpdateTTL(t *testing.T) { cwr.AutostartSchedule = nil cwr.TTLMillis = nil }) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) ) err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ From a4c07df7b76c996a4164c15b997ffcba10bb70b2 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 9 Jun 2022 14:30:14 +0100 Subject: [PATCH 09/17] Address PR comments --- cli/ttl.go | 2 +- coderd/workspaces.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/ttl.go b/cli/ttl.go index 1beadbb3d37f2..fb7e5c43ab564 100644 --- a/cli/ttl.go +++ b/cli/ttl.go @@ -107,7 +107,7 @@ func ttlset() *cobra.Command { Default: "yes", IsConfirm: true, }) - if errors.Is(err, cliui.Canceled) { + if errors.Is(err, cliui.Canceled) || err != nil { return nil } } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 589c8ce97391d..99c50e2bf89db 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -605,7 +605,6 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { ); err != nil { return xerrors.Errorf("update workspace deadline: %w", err) } - return nil }) From 9a7c623d8039b59da875a82de6099d7bb556924e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 9 Jun 2022 18:58:15 +0100 Subject: [PATCH 10/17] chore: WorkspaceScheduleForm: add unit tests --- .../WorkspaceScheduleForm.test.ts | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts index fae1fa4ff0546..da447bee511c7 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts @@ -1,4 +1,7 @@ -import { Language, validationSchema, WorkspaceScheduleFormValues } from "./WorkspaceScheduleForm" +import dayjs from "dayjs" +import { Workspace } from "../../api/typesGenerated" +import * as Mocks from "../../testHelpers/entities" +import { Language, ttlShutdownAt, validationSchema, WorkspaceScheduleFormValues } from "./WorkspaceScheduleForm" import { zones } from "./zones" const valid: WorkspaceScheduleFormValues = { @@ -155,3 +158,51 @@ describe("validationSchema", () => { expect(validate).toThrowError("ttl must be less than or equal to 168") }) }) + +describe("ttlShutdownAt", () => { + it.each<[dayjs.Dayjs, Workspace, string, number, string]>([ + [ + dayjs("2022-05-17T18:09:00Z"), + { + ...Mocks.MockWorkspace, + latest_build: { + ...Mocks.MockWorkspaceBuild, + transition: "stop", + }, + }, + "America/Chicago", + 1, + "Your workspace will automatically shut down after this amount of time has elapsed.", + ], + [ + dayjs("2022-05-17T18:09:00Z"), + Mocks.MockWorkspace, + "America/Chicago", + 0, + "Your workspace will not automatically shut down.", + ], + [ + dayjs("2022-05-17T18:09:00Z"), + Mocks.MockWorkspace, + "America/Chicago", + 1, + "Your workspace will shut down at 01:39 PM CDT.", + ], + [ + dayjs("2022-05-17T18:10:00Z"), + Mocks.MockWorkspace, + "America/Chicago", + 1, + "⚠️ Your workspace will shut down within 30 minutes. ⚠️", + ], + [ + dayjs("2022-05-17T18:40:00Z"), + Mocks.MockWorkspace, + "America/Chicago", + 1, + "⚠️ Your workspace will shut down immediately! ⚠️", + ], + ])("ttlShutdownAt(%p, %p, %p, %p) returns %p", (now, workspace, timezone, ttlHours, expected) => { + expect(ttlShutdownAt(now, workspace, timezone, ttlHours)).toEqual(expected) + }) +}) From 0278a93acb9c6d0816c813be284ebdd4d1c0a09e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 9 Jun 2022 19:09:55 +0100 Subject: [PATCH 11/17] WorkspaceScheduleForm: refactor ttlShutdownAt --- .../WorkspaceScheduleForm.test.ts | 8 +------ .../WorkspaceScheduleForm.tsx | 21 +++++++++---------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts index da447bee511c7..54e8fe20a0ade 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts @@ -163,13 +163,7 @@ describe("ttlShutdownAt", () => { it.each<[dayjs.Dayjs, Workspace, string, number, string]>([ [ dayjs("2022-05-17T18:09:00Z"), - { - ...Mocks.MockWorkspace, - latest_build: { - ...Mocks.MockWorkspaceBuild, - transition: "stop", - }, - }, + Mocks.MockStoppedWorkspace, "America/Chicago", 1, "Your workspace will automatically shut down after this amount of time has elapsed.", diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx index f23bba3e54927..2e2a6ea3e245c 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx @@ -17,6 +17,7 @@ import * as Yup from "yup" import { FieldErrors } from "../../api/errors" import { Workspace } from "../../api/typesGenerated" import { getFormHelpers } from "../../util/formUtils" +import { isWorkspaceOn } from "../../util/workspace" import { FormFooter } from "../FormFooter/FormFooter" import { FullPageForm } from "../FullPageForm/FullPageForm" import { Stack } from "../Stack/Stack" @@ -278,22 +279,20 @@ export const WorkspaceScheduleForm: FC = ({ ) } -const ttlShutdownAt = (now: dayjs.Dayjs, workspace: Workspace, tz: string, newTTL: number): string => { - if (workspace.latest_build.transition !== "start") { +export const ttlShutdownAt = (now: dayjs.Dayjs, workspace: Workspace, tz: string, newTTL: number): string => { + const newDeadline = dayjs(workspace.latest_build.updated_at).add(newTTL, "hour") + if (!isWorkspaceOn(workspace)) { return Language.ttlHelperText - } - if (newTTL === 0) { + } else if (newTTL === 0) { return Language.ttlCausesNoShutdownHelperText - } - const newDeadline = dayjs(workspace.latest_build.updated_at).add(newTTL, "hour") - if (newDeadline.isBefore(now)) { + } else if (newDeadline.isBefore(now)) { return `⚠️ ${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownImmediately} ⚠️` - } - if (newDeadline.isBefore(now.add(30, "minute"))) { + } else if (newDeadline.isBefore(now.add(30, "minute"))) { return `⚠️ ${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownSoon} ⚠️` + } else { + const newDeadlineString = newDeadline.tz(tz).format("hh:mm A z") + return `${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownAt} ${newDeadlineString}.` } - const newDeadlineString = newDeadline.tz(tz).format("hh:mm A z") - return `${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownAt} ${newDeadlineString}.` } const useStyles = makeStyles({ From a372dce38dcd284abb2c7c4504048b7c28824505 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 9 Jun 2022 19:10:12 +0100 Subject: [PATCH 12/17] apply suggestions from code review --- .../WorkspaceScheduleForm/WorkspaceScheduleForm.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx index 2e2a6ea3e245c..2d3b411840df3 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx @@ -59,7 +59,7 @@ export interface WorkspaceScheduleFormProps { fieldErrors?: FieldErrors initialValues?: WorkspaceScheduleFormValues isLoading: boolean - now: dayjs.Dayjs + now?: dayjs.Dayjs onCancel: () => void onSubmit: (values: WorkspaceScheduleFormValues) => void workspace: Workspace @@ -182,7 +182,7 @@ export const WorkspaceScheduleForm: FC = ({ ...WorkspaceScheduleFormInitialValues, }, isLoading, - now: now, + now = dayjs(), onCancel, onSubmit, workspace, @@ -265,7 +265,7 @@ export const WorkspaceScheduleForm: FC = ({ Date: Thu, 9 Jun 2022 19:10:25 +0100 Subject: [PATCH 13/17] fixup! apply suggestions from code review --- cli/ttl.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cli/ttl.go b/cli/ttl.go index fb7e5c43ab564..9bc19dc033bea 100644 --- a/cli/ttl.go +++ b/cli/ttl.go @@ -107,8 +107,11 @@ func ttlset() *cobra.Command { Default: "yes", IsConfirm: true, }) - if errors.Is(err, cliui.Canceled) || err != nil { - return nil + if err != nil { + if errors.Is(err, cliui.Canceled) { + return nil + } + return err } } From 23b474c6b6e7b6ace7a4be0c9d01f7e633a9a12b Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 9 Jun 2022 20:50:42 +0100 Subject: [PATCH 14/17] yarn lint; more pr comments --- .../WorkspaceScheduleForm/WorkspaceScheduleForm.tsx | 6 ++---- .../pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx index 2d3b411840df3..4da2bbcb20f0d 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx @@ -178,9 +178,7 @@ export const validationSchema = Yup.object({ export const WorkspaceScheduleForm: FC = ({ fieldErrors, - initialValues = { - ...WorkspaceScheduleFormInitialValues, - }, + initialValues = WorkspaceScheduleFormInitialValues, isLoading, now = dayjs(), onCancel, @@ -265,7 +263,7 @@ export const WorkspaceScheduleForm: FC = ({ { return ( Date: Thu, 9 Jun 2022 21:20:01 +0100 Subject: [PATCH 15/17] bump ci From fd86474ab618418dbdeedd68afdba9730211bc91 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 9 Jun 2022 21:33:49 +0100 Subject: [PATCH 16/17] mind your language --- .../WorkspaceScheduleForm.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts index 54e8fe20a0ade..708b424d09ba8 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts @@ -166,35 +166,35 @@ describe("ttlShutdownAt", () => { Mocks.MockStoppedWorkspace, "America/Chicago", 1, - "Your workspace will automatically shut down after this amount of time has elapsed.", + Language.ttlHelperText, ], [ dayjs("2022-05-17T18:09:00Z"), Mocks.MockWorkspace, "America/Chicago", 0, - "Your workspace will not automatically shut down.", + Language.ttlCausesNoShutdownHelperText, ], [ dayjs("2022-05-17T18:09:00Z"), Mocks.MockWorkspace, "America/Chicago", 1, - "Your workspace will shut down at 01:39 PM CDT.", + `${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownAt} 01:39 PM CDT.`, ], [ dayjs("2022-05-17T18:10:00Z"), Mocks.MockWorkspace, "America/Chicago", 1, - "⚠️ Your workspace will shut down within 30 minutes. ⚠️", + `⚠️ ${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownSoon} ⚠️`, ], [ dayjs("2022-05-17T18:40:00Z"), Mocks.MockWorkspace, "America/Chicago", 1, - "⚠️ Your workspace will shut down immediately! ⚠️", + `⚠️ ${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownImmediately} ⚠️`, ], ])("ttlShutdownAt(%p, %p, %p, %p) returns %p", (now, workspace, timezone, ttlHours, expected) => { expect(ttlShutdownAt(now, workspace, timezone, ttlHours)).toEqual(expected) From 2b9f93cb3b0900b1b5893417889d4c6938ad253c Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 9 Jun 2022 21:37:35 +0100 Subject: [PATCH 17/17] fumpt --- .../WorkspaceScheduleForm.stories.tsx | 6 +----- .../WorkspaceScheduleForm.test.ts | 16 ++-------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx index 741ba3f76c406..c9ea6eafa8b9d 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx @@ -5,11 +5,7 @@ import advancedFormat from "dayjs/plugin/advancedFormat" import timezone from "dayjs/plugin/timezone" import utc from "dayjs/plugin/utc" import * as Mocks from "../../testHelpers/entities" -import { - WorkspaceScheduleForm, - defaultWorkspaceSchedule, - WorkspaceScheduleFormProps, -} from "./WorkspaceScheduleForm" +import { defaultWorkspaceSchedule, WorkspaceScheduleForm, WorkspaceScheduleFormProps } from "./WorkspaceScheduleForm" dayjs.extend(advancedFormat) dayjs.extend(utc) diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts index 708b424d09ba8..8fa25cc66abd3 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts @@ -161,20 +161,8 @@ describe("validationSchema", () => { describe("ttlShutdownAt", () => { it.each<[dayjs.Dayjs, Workspace, string, number, string]>([ - [ - dayjs("2022-05-17T18:09:00Z"), - Mocks.MockStoppedWorkspace, - "America/Chicago", - 1, - Language.ttlHelperText, - ], - [ - dayjs("2022-05-17T18:09:00Z"), - Mocks.MockWorkspace, - "America/Chicago", - 0, - Language.ttlCausesNoShutdownHelperText, - ], + [dayjs("2022-05-17T18:09:00Z"), Mocks.MockStoppedWorkspace, "America/Chicago", 1, Language.ttlHelperText], + [dayjs("2022-05-17T18:09:00Z"), Mocks.MockWorkspace, "America/Chicago", 0, Language.ttlCausesNoShutdownHelperText], [ dayjs("2022-05-17T18:09:00Z"), Mocks.MockWorkspace,