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, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy