From 45e923e01a85d1b69c6faa1f983c4abc517343a6 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 2 Aug 2023 06:57:38 +0000 Subject: [PATCH 1/6] feat: add activity bumping to template scheduling --- coderd/database/dbauthz/dbauthz.go | 14 +- coderd/database/dbfake/dbfake.go | 34 +++- coderd/database/dbmetrics/dbmetrics.go | 13 +- coderd/database/dbmock/dbmock.go | 26 ++- coderd/database/querier.go | 3 +- coderd/database/queries.sql.go | 41 ++++- coderd/database/queries/workspaces.sql | 23 ++- coderd/schedule/template.go | 12 ++ coderd/templates.go | 8 +- codersdk/templates.go | 9 + enterprise/coderd/schedule/template.go | 25 ++- enterprise/coderd/templates_test.go | 122 +++++++++++++- site/src/api/typesGenerated.ts | 2 + .../Dialogs/ConfirmDialog/ConfirmDialog.tsx | 155 +++++++++++++++++- .../ImpendingDeletionBanner.tsx | 6 +- .../TemplateSettingsForm.tsx | 2 + .../TemplateSettingsPage.test.tsx | 2 + .../TemplateScheduleForm.tsx | 77 ++++++--- .../useWorkspacesToBeDeleted.ts | 29 ++-- .../TemplateSchedulePage.test.tsx | 6 +- 20 files changed, 522 insertions(+), 87 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 7ae81c3b041d5..1fc1c9782235e 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2385,6 +2385,14 @@ func (q *querier) UpdateTemplateVersionGitAuthProvidersByJobID(ctx context.Conte return q.db.UpdateTemplateVersionGitAuthProvidersByJobID(ctx, arg) } +func (q *querier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg database.UpdateTemplateWorkspacesLastUsedAtParams) error { + fetch := func(ctx context.Context, arg database.UpdateTemplateWorkspacesLastUsedAtParams) (database.Template, error) { + return q.db.GetTemplateByID(ctx, arg.TemplateID) + } + + return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateTemplateWorkspacesLastUsedAt)(ctx, arg) +} + // UpdateUserDeletedByID // Deprecated: Delete this function in favor of 'SoftDeleteUserByID'. Deletes are // irreversible. @@ -2663,12 +2671,12 @@ func (q *querier) UpdateWorkspaceTTL(ctx context.Context, arg database.UpdateWor return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceTTL)(ctx, arg) } -func (q *querier) UpdateWorkspacesDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesDeletingAtByTemplateIDParams) error { - fetch := func(ctx context.Context, arg database.UpdateWorkspacesDeletingAtByTemplateIDParams) (database.Template, error) { +func (q *querier) UpdateWorkspacesLockedDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error { + fetch := func(ctx context.Context, arg database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams) (database.Template, error) { return q.db.GetTemplateByID(ctx, arg.TemplateID) } - return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateWorkspacesDeletingAtByTemplateID)(ctx, arg) + return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateWorkspacesLockedDeletingAtByTemplateID)(ctx, arg) } func (q *querier) UpsertAppSecurityKey(ctx context.Context, data string) error { diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 589c17efcae0a..d583a9b545d73 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -5199,6 +5199,26 @@ func (q *FakeQuerier) UpdateTemplateVersionGitAuthProvidersByJobID(_ context.Con return sql.ErrNoRows } +func (q *FakeQuerier) UpdateTemplateWorkspacesLastUsedAt(_ context.Context, arg database.UpdateTemplateWorkspacesLastUsedAtParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, ws := range q.workspaces { + if ws.TemplateID != arg.TemplateID { + continue + } + ws.LastUsedAt = arg.LastUsedAt + q.workspaces[i] = ws + } + + return nil +} + func (q *FakeQuerier) UpdateUserDeletedByID(_ context.Context, params database.UpdateUserDeletedByIDParams) error { if err := validateDatabaseType(params); err != nil { return err @@ -5796,7 +5816,7 @@ func (q *FakeQuerier) UpdateWorkspaceTTL(_ context.Context, arg database.UpdateW return sql.ErrNoRows } -func (q *FakeQuerier) UpdateWorkspacesDeletingAtByTemplateID(_ context.Context, arg database.UpdateWorkspacesDeletingAtByTemplateIDParams) error { +func (q *FakeQuerier) UpdateWorkspacesLockedDeletingAtByTemplateID(_ context.Context, arg database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -5806,9 +5826,21 @@ func (q *FakeQuerier) UpdateWorkspacesDeletingAtByTemplateID(_ context.Context, } for i, ws := range q.workspaces { + if ws.TemplateID != arg.TemplateID { + continue + } + if ws.LockedAt.Time.IsZero() { continue } + + if !arg.LockedAt.IsZero() { + ws.LockedAt = sql.NullTime{ + Valid: true, + Time: arg.LockedAt, + } + } + deletingAt := sql.NullTime{ Valid: arg.LockedTtlMs > 0, } diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 991be6b64ac6b..498ca57b504e4 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1453,6 +1453,13 @@ func (m metricsStore) UpdateTemplateVersionGitAuthProvidersByJobID(ctx context.C return err } +func (m metricsStore) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg database.UpdateTemplateWorkspacesLastUsedAtParams) error { + start := time.Now() + r0 := m.s.UpdateTemplateWorkspacesLastUsedAt(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateTemplateWorkspacesLastUsedAt").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) UpdateUserDeletedByID(ctx context.Context, arg database.UpdateUserDeletedByIDParams) error { start := time.Now() err := m.s.UpdateUserDeletedByID(ctx, arg) @@ -1635,10 +1642,10 @@ func (m metricsStore) UpdateWorkspaceTTL(ctx context.Context, arg database.Updat return r0 } -func (m metricsStore) UpdateWorkspacesDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesDeletingAtByTemplateIDParams) error { +func (m metricsStore) UpdateWorkspacesLockedDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error { start := time.Now() - r0 := m.s.UpdateWorkspacesDeletingAtByTemplateID(ctx, arg) - m.queryLatencies.WithLabelValues("UpdateWorkspacesDeletingAtByTemplateID").Observe(time.Since(start).Seconds()) + r0 := m.s.UpdateWorkspacesLockedDeletingAtByTemplateID(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateWorkspacesLockedDeletingAtByTemplateID").Observe(time.Since(start).Seconds()) return r0 } diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 6f07fb72790ce..ac1e782e7d398 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3062,6 +3062,20 @@ func (mr *MockStoreMockRecorder) UpdateTemplateVersionGitAuthProvidersByJobID(ar return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateVersionGitAuthProvidersByJobID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateVersionGitAuthProvidersByJobID), arg0, arg1) } +// UpdateTemplateWorkspacesLastUsedAt mocks base method. +func (m *MockStore) UpdateTemplateWorkspacesLastUsedAt(arg0 context.Context, arg1 database.UpdateTemplateWorkspacesLastUsedAtParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateTemplateWorkspacesLastUsedAt", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateTemplateWorkspacesLastUsedAt indicates an expected call of UpdateTemplateWorkspacesLastUsedAt. +func (mr *MockStoreMockRecorder) UpdateTemplateWorkspacesLastUsedAt(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateWorkspacesLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateTemplateWorkspacesLastUsedAt), arg0, arg1) +} + // UpdateUserDeletedByID mocks base method. func (m *MockStore) UpdateUserDeletedByID(arg0 context.Context, arg1 database.UpdateUserDeletedByIDParams) error { m.ctrl.T.Helper() @@ -3437,18 +3451,18 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceTTL(arg0, arg1 interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceTTL", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceTTL), arg0, arg1) } -// UpdateWorkspacesDeletingAtByTemplateID mocks base method. -func (m *MockStore) UpdateWorkspacesDeletingAtByTemplateID(arg0 context.Context, arg1 database.UpdateWorkspacesDeletingAtByTemplateIDParams) error { +// UpdateWorkspacesLockedDeletingAtByTemplateID mocks base method. +func (m *MockStore) UpdateWorkspacesLockedDeletingAtByTemplateID(arg0 context.Context, arg1 database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateWorkspacesDeletingAtByTemplateID", arg0, arg1) + ret := m.ctrl.Call(m, "UpdateWorkspacesLockedDeletingAtByTemplateID", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// UpdateWorkspacesDeletingAtByTemplateID indicates an expected call of UpdateWorkspacesDeletingAtByTemplateID. -func (mr *MockStoreMockRecorder) UpdateWorkspacesDeletingAtByTemplateID(arg0, arg1 interface{}) *gomock.Call { +// UpdateWorkspacesLockedDeletingAtByTemplateID indicates an expected call of UpdateWorkspacesLockedDeletingAtByTemplateID. +func (mr *MockStoreMockRecorder) UpdateWorkspacesLockedDeletingAtByTemplateID(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspacesDeletingAtByTemplateID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspacesDeletingAtByTemplateID), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspacesLockedDeletingAtByTemplateID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspacesLockedDeletingAtByTemplateID), arg0, arg1) } // UpsertAppSecurityKey mocks base method. diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 2c0cc90277f9a..a217648035e90 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -270,6 +270,7 @@ type sqlcQuerier interface { UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error UpdateTemplateVersionGitAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionGitAuthProvidersByJobIDParams) error + UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error UpdateUserDeletedByID(ctx context.Context, arg UpdateUserDeletedByIDParams) error UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error) @@ -297,7 +298,7 @@ type sqlcQuerier interface { UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error) UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error - UpdateWorkspacesDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDeletingAtByTemplateIDParams) error + UpdateWorkspacesLockedDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error UpsertAppSecurityKey(ctx context.Context, value string) error // The default proxy is implied and not actually stored in the database. // So we need to store it's configuration here for display purposes. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 86c953643bdeb..292f22ce74e05 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -9771,6 +9771,24 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar return i, err } +const updateTemplateWorkspacesLastUsedAt = `-- name: UpdateTemplateWorkspacesLastUsedAt :exec +UPDATE workspaces +SET + last_used_at = $1::timestamptz +WHERE + template_id = $2 +` + +type UpdateTemplateWorkspacesLastUsedAtParams struct { + LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"` + TemplateID uuid.UUID `db:"template_id" json:"template_id"` +} + +func (q *sqlQuerier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error { + _, err := q.db.ExecContext(ctx, updateTemplateWorkspacesLastUsedAt, arg.LastUsedAt, arg.TemplateID) + return err +} + const updateWorkspace = `-- name: UpdateWorkspace :one UPDATE workspaces @@ -9930,23 +9948,28 @@ func (q *sqlQuerier) UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspace return err } -const updateWorkspacesDeletingAtByTemplateID = `-- name: UpdateWorkspacesDeletingAtByTemplateID :exec -UPDATE - workspaces +const updateWorkspacesLockedDeletingAtByTemplateID = `-- name: UpdateWorkspacesLockedDeletingAtByTemplateID :exec +UPDATE workspaces SET - deleting_at = CASE WHEN $1::bigint = 0 THEN NULL ELSE locked_at + interval '1 milliseconds' * $1::bigint END + deleting_at = CASE + WHEN $1::bigint = 0 THEN NULL + WHEN $2::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN ($2::timestamptz) + interval '1 milliseconds' * $1::bigint + ELSE locked_at + interval '1 milliseconds' * $1::bigint + END, + locked_at = CASE WHEN $2::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN $2::timestamptz ELSE locked_at END WHERE - template_id = $2 + template_id = $3 AND - locked_at IS NOT NULL + locked_at IS NOT NULL ` -type UpdateWorkspacesDeletingAtByTemplateIDParams struct { +type UpdateWorkspacesLockedDeletingAtByTemplateIDParams struct { LockedTtlMs int64 `db:"locked_ttl_ms" json:"locked_ttl_ms"` + LockedAt time.Time `db:"locked_at" json:"locked_at"` TemplateID uuid.UUID `db:"template_id" json:"template_id"` } -func (q *sqlQuerier) UpdateWorkspacesDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDeletingAtByTemplateIDParams) error { - _, err := q.db.ExecContext(ctx, updateWorkspacesDeletingAtByTemplateID, arg.LockedTtlMs, arg.TemplateID) +func (q *sqlQuerier) UpdateWorkspacesLockedDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesLockedDeletingAtByTemplateIDParams) error { + _, err := q.db.ExecContext(ctx, updateWorkspacesLockedDeletingAtByTemplateID, arg.LockedTtlMs, arg.LockedAt, arg.TemplateID) return err } diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 9dd8aa00b5f55..503d0a6b52bd8 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -501,12 +501,23 @@ AND workspaces.id = $1 RETURNING workspaces.*; --- name: UpdateWorkspacesDeletingAtByTemplateID :exec -UPDATE - workspaces +-- name: UpdateWorkspacesLockedDeletingAtByTemplateID :exec +UPDATE workspaces SET - deleting_at = CASE WHEN @locked_ttl_ms::bigint = 0 THEN NULL ELSE locked_at + interval '1 milliseconds' * @locked_ttl_ms::bigint END + deleting_at = CASE + WHEN @locked_ttl_ms::bigint = 0 THEN NULL + WHEN @locked_at::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN (@locked_at::timestamptz) + interval '1 milliseconds' * @locked_ttl_ms::bigint + ELSE locked_at + interval '1 milliseconds' * @locked_ttl_ms::bigint + END, + locked_at = CASE WHEN @locked_at::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN @locked_at::timestamptz ELSE locked_at END WHERE - template_id = @template_id + template_id = @template_id AND - locked_at IS NOT NULL; + locked_at IS NOT NULL; + +-- name: UpdateTemplateWorkspacesLastUsedAt :exec +UPDATE workspaces +SET + last_used_at = @last_used_at::timestamptz +WHERE + template_id = @template_id; diff --git a/coderd/schedule/template.go b/coderd/schedule/template.go index 7c6a906c6b62c..0e3774b798358 100644 --- a/coderd/schedule/template.go +++ b/coderd/schedule/template.go @@ -105,6 +105,18 @@ type TemplateScheduleOptions struct { // LockedTTL dictates the duration after which locked workspaces will be // permanently deleted. LockedTTL time.Duration `json:"locked_ttl"` + // UpdateWorkspaceLastUsedAt updates the template's workspaces' + // last_used_at field. This is useful for preventing updates to the + // templates inactivity_ttl immediately triggering a lock action against + // workspaces whose last_used_at field violates the new template + // inactivity_ttl threshold. + UpdateWorkspaceLastUsedAt bool `json:"update_workspace_last_used_at"` + // UpdateWorkspaceLockedAt updates the template's workspaces' + // locked_at field. This is useful for preventing updates to the + // templates locked_ttl immediately triggering a delete action against + // workspaces whose locked_at field violates the new template locked_ttl + // threshold. + UpdateWorkspaceLockedAt bool `json:"update_workspace_locked_at"` } // TemplateScheduleStore provides an interface for retrieving template diff --git a/coderd/templates.go b/coderd/templates.go index 75c18402d7ca5..f51f42668e1a1 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -622,9 +622,11 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { DaysOfWeek: restartRequirementDaysOfWeekParsed, Weeks: req.RestartRequirement.Weeks, }, - FailureTTL: failureTTL, - InactivityTTL: inactivityTTL, - LockedTTL: lockedTTL, + FailureTTL: failureTTL, + InactivityTTL: inactivityTTL, + LockedTTL: lockedTTL, + UpdateWorkspaceLastUsedAt: req.UpdateWorkspaceLastUsedAt, + UpdateWorkspaceLockedAt: req.UpdateWorkspaceLockedAt, }) if err != nil { return xerrors.Errorf("set template schedule options: %w", err) diff --git a/codersdk/templates.go b/codersdk/templates.go index 2022c876db360..f566ac4f2ce32 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -192,6 +192,15 @@ type UpdateTemplateMeta struct { FailureTTLMillis int64 `json:"failure_ttl_ms,omitempty"` InactivityTTLMillis int64 `json:"inactivity_ttl_ms,omitempty"` LockedTTLMillis int64 `json:"locked_ttl_ms,omitempty"` + // UpdateWorkspaceLastUsedAt updates the last_used_at field of workspaces + // spawned from the template. This is useful for preventing workspaces being + // immediately locked when updating the inactivity_ttl field to a new, shorter + // value. + UpdateWorkspaceLastUsedAt bool `json:"update_workspace_last_used_at"` + // UpdateWorkspaceLockedAt updates the locked_at field of workspaces spawned + // from the template. This is useful for preventing locked workspaces being immediately + // deleted when updating the locked_ttl field to a new, shorter value. + UpdateWorkspaceLockedAt bool `json:"update_workspace_locked_at"` } type TemplateExample struct { diff --git a/enterprise/coderd/schedule/template.go b/enterprise/coderd/schedule/template.go index 19309454aacde..08408e50d44b4 100644 --- a/enterprise/coderd/schedule/template.go +++ b/enterprise/coderd/schedule/template.go @@ -113,11 +113,11 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S } var template database.Template - err = db.InTx(func(db database.Store) error { + err = db.InTx(func(tx database.Store) error { ctx, span := tracing.StartSpanWithName(ctx, "(*schedule.EnterpriseTemplateScheduleStore).Set()-InTx()") defer span.End() - err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{ + err := tx.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{ ID: tpl.ID, UpdatedAt: s.now(), AllowUserAutostart: opts.UserAutostartEnabled, @@ -134,19 +134,36 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S return xerrors.Errorf("update template schedule: %w", err) } + var lockedAt time.Time + if opts.UpdateWorkspaceLockedAt { + lockedAt = database.Now() + } + // If we updated the locked_ttl we need to update all the workspaces deleting_at // to ensure workspaces are being cleaned up correctly. Similarly if we are // disabling it (by passing 0), then we want to delete nullify the deleting_at // fields of all the template workspaces. - err = db.UpdateWorkspacesDeletingAtByTemplateID(ctx, database.UpdateWorkspacesDeletingAtByTemplateIDParams{ + err = tx.UpdateWorkspacesLockedDeletingAtByTemplateID(ctx, database.UpdateWorkspacesLockedDeletingAtByTemplateIDParams{ TemplateID: tpl.ID, LockedTtlMs: opts.LockedTTL.Milliseconds(), + LockedAt: lockedAt, }) if err != nil { return xerrors.Errorf("update deleting_at of all workspaces for new locked_ttl %q: %w", opts.LockedTTL, err) } - template, err = db.GetTemplateByID(ctx, tpl.ID) + if opts.UpdateWorkspaceLastUsedAt { + err = tx.UpdateTemplateWorkspacesLastUsedAt(ctx, database.UpdateTemplateWorkspacesLastUsedAtParams{ + TemplateID: tpl.ID, + LastUsedAt: database.Now(), + }) + if err != nil { + return xerrors.Errorf("update template workspaces last_used_at: %w", err) + } + } + + // TODO: update all workspace max_deadlines to be within new bounds + template, err = tx.GetTemplateByID(ctx, tpl.ID) if err != nil { return xerrors.Errorf("get updated template schedule: %w", err) } diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index 4ac4a0e35d518..af364d3578b1c 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -283,10 +283,11 @@ func TestTemplates(t *testing.T) { require.Nil(t, unlockedWorkspace.LockedAt) require.Nil(t, unlockedWorkspace.DeletingAt) - lockedWorkspace = coderdtest.MustWorkspace(t, client, lockedWorkspace.ID) - require.NotNil(t, lockedWorkspace.LockedAt) - require.NotNil(t, lockedWorkspace.DeletingAt) - require.Equal(t, lockedWorkspace.LockedAt.Add(lockedTTL), *lockedWorkspace.DeletingAt) + updatedLockedWorkspace := coderdtest.MustWorkspace(t, client, lockedWorkspace.ID) + require.NotNil(t, updatedLockedWorkspace.LockedAt) + require.NotNil(t, updatedLockedWorkspace.DeletingAt) + require.Equal(t, updatedLockedWorkspace.LockedAt.Add(lockedTTL), *updatedLockedWorkspace.DeletingAt) + require.Equal(t, updatedLockedWorkspace.LockedAt, lockedWorkspace.LockedAt) // Disable the locked_ttl on the template, then we can assert that the workspaces // no longer have a deleting_at field. @@ -307,6 +308,119 @@ func TestTemplates(t *testing.T) { require.NotNil(t, lockedWorkspace.LockedAt) require.Nil(t, lockedWorkspace.DeletingAt) }) + + t.Run("UpdateLockedAt", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + client, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAdvancedTemplateScheduling: 1, + }, + }, + }) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + unlockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + lockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + require.Nil(t, unlockedWorkspace.DeletingAt) + require.Nil(t, lockedWorkspace.DeletingAt) + + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, unlockedWorkspace.LatestBuild.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID) + + err := client.UpdateWorkspaceLock(ctx, lockedWorkspace.ID, codersdk.UpdateWorkspaceLock{ + Lock: true, + }) + require.NoError(t, err) + + lockedWorkspace = coderdtest.MustWorkspace(t, client, lockedWorkspace.ID) + require.NotNil(t, lockedWorkspace.LockedAt) + // The deleting_at field should be nil since there is no template locked_ttl set. + require.Nil(t, lockedWorkspace.DeletingAt) + + lockedTTL := time.Minute + updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + LockedTTLMillis: lockedTTL.Milliseconds(), + UpdateWorkspaceLockedAt: true, + }) + require.NoError(t, err) + require.Equal(t, lockedTTL.Milliseconds(), updated.LockedTTLMillis) + + unlockedWorkspace = coderdtest.MustWorkspace(t, client, unlockedWorkspace.ID) + require.Nil(t, unlockedWorkspace.LockedAt) + require.Nil(t, unlockedWorkspace.DeletingAt) + + updatedLockedWorkspace := coderdtest.MustWorkspace(t, client, lockedWorkspace.ID) + require.NotNil(t, updatedLockedWorkspace.LockedAt) + require.NotNil(t, updatedLockedWorkspace.DeletingAt) + // Validate that the workspace locked_at value is updated. + require.True(t, updatedLockedWorkspace.LockedAt.After(*lockedWorkspace.LockedAt)) + require.Equal(t, updatedLockedWorkspace.LockedAt.Add(lockedTTL), *updatedLockedWorkspace.DeletingAt) + }) + + t.Run("UpdateLastUsedAt", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + client, user := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAdvancedTemplateScheduling: 1, + }, + }, + }) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + unlockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + lockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + require.Nil(t, unlockedWorkspace.DeletingAt) + require.Nil(t, lockedWorkspace.DeletingAt) + + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, unlockedWorkspace.LatestBuild.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID) + + err := client.UpdateWorkspaceLock(ctx, lockedWorkspace.ID, codersdk.UpdateWorkspaceLock{ + Lock: true, + }) + require.NoError(t, err) + + lockedWorkspace = coderdtest.MustWorkspace(t, client, lockedWorkspace.ID) + require.NotNil(t, lockedWorkspace.LockedAt) + // The deleting_at field should be nil since there is no template locked_ttl set. + require.Nil(t, lockedWorkspace.DeletingAt) + + inactivityTTL := time.Minute + updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + InactivityTTLMillis: inactivityTTL.Milliseconds(), + UpdateWorkspaceLastUsedAt: true, + }) + require.NoError(t, err) + require.Equal(t, inactivityTTL.Milliseconds(), updated.InactivityTTLMillis) + + updatedUnlockedWS := coderdtest.MustWorkspace(t, client, unlockedWorkspace.ID) + require.Nil(t, updatedUnlockedWS.LockedAt) + require.Nil(t, updatedUnlockedWS.DeletingAt) + require.True(t, updatedUnlockedWS.LastUsedAt.After(unlockedWorkspace.LastUsedAt)) + + updatedLockedWorkspace := coderdtest.MustWorkspace(t, client, lockedWorkspace.ID) + require.NotNil(t, updatedLockedWorkspace.LockedAt) + require.Nil(t, updatedLockedWorkspace.DeletingAt) + // Validate that the workspace locked_at value is updated. + require.Equal(t, updatedLockedWorkspace.LockedAt, lockedWorkspace.LockedAt) + require.True(t, updatedLockedWorkspace.LastUsedAt.After(lockedWorkspace.LastUsedAt)) + }) } func TestTemplateACL(t *testing.T) { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index e0592b966bca4..b37f8fcf9dc8e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1152,6 +1152,8 @@ export interface UpdateTemplateMeta { readonly failure_ttl_ms?: number readonly inactivity_ttl_ms?: number readonly locked_ttl_ms?: number + readonly update_workspace_last_used_at: boolean + readonly update_workspace_locked_at: boolean } // From codersdk/users.go diff --git a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx index 5d77d34b28906..556f20989b7cf 100644 --- a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx +++ b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx @@ -7,6 +7,9 @@ import { DialogActionButtonsProps, } from "../Dialog" import { ConfirmDialogType } from "../types" +import Checkbox from "@mui/material/Checkbox" +import FormControlLabel from "@mui/material/FormControlLabel" +import { Stack } from "@mui/system" interface ConfirmDialogTypeConfig { confirmText: ReactNode @@ -61,7 +64,7 @@ const useStyles = makeStyles((theme) => ({ background: theme.palette.background.paper, border: `1px solid ${theme.palette.divider}`, width: "100%", - maxWidth: theme.spacing(55), + maxWidth: theme.spacing(50), }, "& .MuiDialogActions-spacing": { padding: `0 ${theme.spacing(5)} ${theme.spacing(5)}`, @@ -151,3 +154,153 @@ export const ConfirmDialog: FC> = ({ ) } + +export interface ScheduleDialogProps extends ConfirmDialogProps { + readonly inactiveWorkspaceToBeLocked: number + readonly lockedWorkspacesToBeDeleted: number + readonly updateLockedWorkspaces: (confirm: boolean) => void + readonly updateInactiveWorkspaces: (confirm: boolean) => void +} + +export const ScheduleDialog: FC> = ({ + cancelText, + confirmLoading, + disabled = false, + hideCancel, + onClose, + onConfirm, + type, + open = false, + title, + inactiveWorkspaceToBeLocked, + lockedWorkspacesToBeDeleted, + updateLockedWorkspaces, + updateInactiveWorkspaces, +}) => { + const styles = useScheduleStyles({ type }) + + const defaults = CONFIRM_DIALOG_DEFAULTS["delete"] + + if (typeof hideCancel === "undefined") { + hideCancel = defaults.hideCancel + } + + return ( + +
+

{title}

+ <> + {inactiveWorkspaceToBeLocked > 0 && ( + <> +

{"Inactivity TTL"}

+ +
{`The current value will result in ${inactiveWorkspaceToBeLocked}+ workspaces being automatically soft deleted. To prevent this, do you want to reset the inactivity period for all template workspaces?`}
+ { + updateInactiveWorkspaces(e.target.checked) + }} + /> + } + label="Reset" + /> +
+ + )} + + {lockedWorkspacesToBeDeleted > 0 && ( + <> +

{"Deletion Grace Period"}

+ +
{`The current value will result in ${lockedWorkspacesToBeDeleted}+ workspaces being permanently deleted. To prevent this, do you want to reset the soft-deletion period for all template workspaces?`}
+ { + updateLockedWorkspaces(e.target.checked) + }} + /> + } + label="Reset" + /> +
+ + )} + +
+ + + + +
+ ) +} + +const useScheduleStyles = makeStyles((theme) => ({ + dialogWrapper: { + "& .MuiPaper-root": { + background: theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, + width: "100%", + maxWidth: theme.spacing(125), + }, + "& .MuiDialogActions-spacing": { + padding: `0 ${theme.spacing(5)} ${theme.spacing(5)}`, + }, + }, + dialogContent: { + color: theme.palette.text.secondary, + padding: theme.spacing(5), + }, + dialogTitle: { + margin: 0, + marginBottom: theme.spacing(2), + color: theme.palette.text.primary, + fontWeight: 400, + fontSize: theme.spacing(2.5), + }, + dialogDescription: { + color: theme.palette.text.secondary, + lineHeight: "160%", + fontSize: 16, + + "& strong": { + color: theme.palette.text.primary, + }, + + "& p:not(.MuiFormHelperText-root)": { + margin: 0, + }, + + "& > p": { + margin: theme.spacing(1, 0), + }, + }, +})) diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index 501dd50dfa95f..4e80250411352 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -61,13 +61,13 @@ export const LockedWorkspaceBanner = ({ hasDeletionScheduledWorkspaces.deleting_at && hasDeletionScheduledWorkspaces.locked_at ) { - return `This workspace has been locked since ${formatDistanceToNow( + return `This workspace has been locked for ${formatDistanceToNow( Date.parse(hasDeletionScheduledWorkspaces.locked_at), - )} and is scheduled to be deleted at ${formatDate( + )} and is scheduled to be deleted on ${formatDate( hasDeletionScheduledWorkspaces.deleting_at, )} . To keep it you must unlock the workspace.` } else if (hasLockedWorkspaces && hasLockedWorkspaces.locked_at) { - return `This workspace has been locked since ${formatDate( + return `This workspace has been locked for ${formatDate( hasLockedWorkspaces.locked_at, )} and cannot be interacted diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index 0844a3e5f3959..4ae79d145baaa 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -72,6 +72,8 @@ export const TemplateSettingsForm: FC = ({ icon: template.icon, allow_user_cancel_workspace_jobs: template.allow_user_cancel_workspace_jobs, + update_workspace_last_used_at: false, + update_workspace_locked_at: false, }, validationSchema, onSubmit, diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx index 597ad0a15387e..9f9f0d231f9c4 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx @@ -33,6 +33,8 @@ const validFormValues: FormValues = { failure_ttl_ms: 0, inactivity_ttl_ms: 0, locked_ttl_ms: 0, + update_workspace_last_used_at: false, + update_workspace_locked_at: false, } const renderTemplateSettingsPage = async () => { diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx index 1efdcec885cfb..13c524d6b5d51 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx @@ -16,7 +16,7 @@ import Link from "@mui/material/Link" import Checkbox from "@mui/material/Checkbox" import FormControlLabel from "@mui/material/FormControlLabel" import Switch from "@mui/material/Switch" -import { DeleteLockedDialog, InactivityDialog } from "./InactivityDialog" +import { InactivityDialog } from "./InactivityDialog" import { useWorkspacesToBeLocked, useWorkspacesToBeDeleted, @@ -24,6 +24,7 @@ import { import { TemplateScheduleFormValues, getValidationSchema } from "./formHelpers" import { TTLHelperText } from "./TTLHelperText" import { docs } from "utils/docs" +import { ScheduleDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" const MS_HOUR_CONVERSION = 3600000 const MS_DAY_CONVERSION = 86400000 @@ -87,21 +88,27 @@ export const TemplateScheduleForm: FC = ({ allowAdvancedScheduling && Boolean(template.inactivity_ttl_ms), locked_cleanup_enabled: allowAdvancedScheduling && Boolean(template.locked_ttl_ms), + update_workspace_last_used_at: false, + update_workspace_locked_at: false, }, validationSchema, onSubmit: () => { - if ( + // Determine if this form will automatically + // lock workspaces upon submission. + const updateWillLockWorkspaces = form.values.inactivity_cleanup_enabled && workspacesToBeLockedToday && workspacesToBeLockedToday.length > 0 - ) { - setIsInactivityDialogOpen(true) - } else if ( + + // Determine if this form will automatically + // delete locked workspaces upon submission. + const updateWillDeleteWorkspaces = form.values.locked_cleanup_enabled && workspacesToBeDeletedToday && workspacesToBeDeletedToday.length > 0 - ) { - setIsLockedDialogOpen(true) + + if (updateWillLockWorkspaces || updateWillDeleteWorkspaces) { + setIsScheduleDialogOpen(true) } else { submitValues() } @@ -124,9 +131,14 @@ export const TemplateScheduleForm: FC = ({ form.values, ) - const [isInactivityDialogOpen, setIsInactivityDialogOpen] = + const showScheduleDialog = + workspacesToBeLockedToday && + workspacesToBeDeletedToday && + (workspacesToBeLockedToday.length > 0 || + workspacesToBeDeletedToday.length > 0) + + const [isScheduleDialogOpen, setIsScheduleDialogOpen] = useState(false) - const [isLockedDialogOpen, setIsLockedDialogOpen] = useState(false) const submitValues = () => { // on submit, convert from hours => ms @@ -149,6 +161,8 @@ export const TemplateScheduleForm: FC = ({ allow_user_autostart: form.values.allow_user_autostart, allow_user_autostop: form.values.allow_user_autostop, + update_workspace_last_used_at: form.values.update_workspace_last_used_at, + update_workspace_locked_at: form.values.update_workspace_locked_at, }) } @@ -345,8 +359,8 @@ export const TemplateScheduleForm: FC = ({ = ({ onChange={handleToggleInactivityCleanup} /> } - label="Enable Inactivity TTL" + label="Enable Inactivity Soft Deletion" /> = ({ = ({ onChange={handleToggleLockedCleanup} /> } - label="Enable Locked TTL" + label="Enable Deletion Retention" /> = ({ {workspacesToBeLockedToday && workspacesToBeLockedToday.length > 0 && ( )} - {workspacesToBeDeletedToday && workspacesToBeDeletedToday.length > 0 && ( - { + submitValues() + setIsScheduleDialogOpen(false) + // These fields are request-scoped so they should be reset + // after every submission. + form.setFieldValue("update_workspace_locked_at", false) + form.setFieldValue("update_workspace_last_used_at", false) + }} + inactiveWorkspaceToBeLocked={workspacesToBeLockedToday.length} + lockedWorkspacesToBeDeleted={workspacesToBeDeletedToday.length} + open={isScheduleDialogOpen} + onClose={() => { + setIsScheduleDialogOpen(false) + }} + title="Workspace Scheduling" + updateLockedWorkspaces={(update: boolean) => + form.setFieldValue("update_workspace_locked_at", update) + } + updateInactiveWorkspaces={(update: boolean) => + form.setFieldValue("update_workspace_last_used_at", update) + } /> )} diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts index 9836c5b273b3f..feff04eeecee7 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts @@ -1,23 +1,19 @@ -import { useQuery } from "@tanstack/react-query" -import { getWorkspaces } from "api/api" import { compareAsc } from "date-fns" import { Workspace, Template } from "api/typesGenerated" import { TemplateScheduleFormValues } from "./formHelpers" +import { useWorkspacesData } from "pages/WorkspacesPage/data" export const useWorkspacesToBeLocked = ( template: Template, formValues: TemplateScheduleFormValues, ) => { - const { data: workspacesData } = useQuery({ - queryKey: ["workspaces"], - queryFn: () => - getWorkspaces({ - q: "template:" + template.name, - }), - enabled: formValues.inactivity_cleanup_enabled, + const { data } = useWorkspacesData({ + page: 0, + limit: 0, + query: "template:" + template.name, }) - return workspacesData?.workspaces?.filter((workspace: Workspace) => { + return data?.workspaces?.filter((workspace: Workspace) => { if (!formValues.inactivity_ttl_ms) { return } @@ -41,15 +37,12 @@ export const useWorkspacesToBeDeleted = ( template: Template, formValues: TemplateScheduleFormValues, ) => { - const { data: workspacesData } = useQuery({ - queryKey: ["workspaces"], - queryFn: () => - getWorkspaces({ - q: "template:" + template.name, - }), - enabled: formValues.locked_cleanup_enabled, + const { data } = useWorkspacesData({ + page: 0, + limit: 0, + query: "template:" + template.name + " locked_at:1970-01-01", }) - return workspacesData?.workspaces?.filter((workspace: Workspace) => { + return data?.workspaces?.filter((workspace: Workspace) => { if (!workspace.locked_at || !formValues.locked_ttl_ms) { return false } diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx index 15612a544d89e..97dedbe454c84 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -23,6 +23,8 @@ const validFormValues = { failure_ttl_ms: 7, inactivity_ttl_ms: 180, locked_ttl_ms: 30, + update_workspace_last_used_at: false, + update_workspace_locked_at: false, } const renderTemplateSchedulePage = async () => { @@ -63,12 +65,12 @@ const fillAndSubmitForm = async ({ await user.type(failureTtlField, failure_ttl_ms.toString()) const inactivityTtlField = screen.getByRole("checkbox", { - name: /Inactivity TTL/i, + name: /Inactivity Soft Deletion/i, }) await user.type(inactivityTtlField, inactivity_ttl_ms.toString()) const lockedTtlField = screen.getByRole("checkbox", { - name: /Locked TTL/i, + name: /Deletion Retention/i, }) await user.type(lockedTtlField, locked_ttl_ms.toString()) From 4b989fbdda25d6c9622aff9df6fcfe4e83cb1245 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 14 Aug 2023 14:04:05 -0500 Subject: [PATCH 2/6] chore: reword some workspace actions stuff (#9061) --- site/src/i18n/en/templateSettingsPage.json | 12 ++++++------ .../TemplateScheduleForm/TemplateScheduleForm.tsx | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/site/src/i18n/en/templateSettingsPage.json b/site/src/i18n/en/templateSettingsPage.json index d1b0927b5a78a..bf624ae736938 100644 --- a/site/src/i18n/en/templateSettingsPage.json +++ b/site/src/i18n/en/templateSettingsPage.json @@ -22,12 +22,12 @@ "failureTTLHelperText_zero": "Coder will not automatically stop failed workspaces", "failureTTLHelperText_one": "Coder will attempt to stop failed workspaces after {{count}} day.", "failureTTLHelperText_other": "Coder will attempt to stop failed workspaces after {{count}} days.", - "inactivityTTLHelperText_zero": "Coder will not automatically lock inactive workspaces", - "inactivityTTLHelperText_one": "Coder will automatically lock inactive workspaces after {{count}} day.", - "inactivityTTLHelperText_other": "Coder will automatically lock inactive workspaces after {{count}} days.", - "lockedTTLHelperText_zero": "Coder will not automatically delete locked workspaces", - "lockedTTLHelperText_one": "Coder will automatically delete locked workspaces after {{count}} day.", - "lockedTTLHelperText_other": "Coder will automatically delete locked workspaces after {{count}} days.", + "inactivityTTLHelperText_zero": "Coder will not mark workspaces as inactive", + "inactivityTTLHelperText_one": "Coder will will mark workspaces as inactive after {{count}} day without user connections.", + "inactivityTTLHelperText_other": "Coder will will mark workspaces as inactive after {{count}} days without user connections", + "lockedTTLHelperText_zero": "Coder will not automatically delete inactive workspaces", + "lockedTTLHelperText_one": "Coder will automatically delete inactive workspaces after {{count}} day.", + "lockedTTLHelperText_other": "Coder will automatically delete inactive workspaces after {{count}} days.", "allowUserCancelWorkspaceJobsLabel": "Allow users to cancel in-progress workspace jobs.", "allowUserCancelWorkspaceJobsNotice": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases.", "allowUsersCancelHelperText": "If checked, users may be able to corrupt their workspace.", diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx index 13c524d6b5d51..7751c91d343ee 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx @@ -359,8 +359,8 @@ export const TemplateScheduleForm: FC = ({ = ({ onChange={handleToggleInactivityCleanup} /> } - label="Enable Inactivity Soft Deletion" + label="Enable Inactivity Threshold" /> = ({ = ({ onChange={handleToggleLockedCleanup} /> } - label="Enable Deletion Retention" + label="Enable Inactivity Deletion" /> Date: Mon, 14 Aug 2023 19:25:04 +0000 Subject: [PATCH 3/6] lint --- .../TemplateScheduleForm/TemplateScheduleForm.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx index 7751c91d343ee..dcf4610f5f3b5 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx @@ -439,8 +439,16 @@ export const TemplateScheduleForm: FC = ({ setIsScheduleDialogOpen(false) // These fields are request-scoped so they should be reset // after every submission. - form.setFieldValue("update_workspace_locked_at", false) - form.setFieldValue("update_workspace_last_used_at", false) + form + .setFieldValue("update_workspace_locked_at", false) + .catch((error) => { + throw error + }) + form + .setFieldValue("update_workspace_last_used_at", false) + .catch((error) => { + throw error + }) }} inactiveWorkspaceToBeLocked={workspacesToBeLockedToday.length} lockedWorkspacesToBeDeleted={workspacesToBeDeletedToday.length} From 32f9f4b150b7570755d0c25243d831e9aebb2cf0 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 22 Aug 2023 01:11:07 +0000 Subject: [PATCH 4/6] update verbiage from locked to dormant --- .../Dialogs/ConfirmDialog/ConfirmDialog.tsx | 40 +++++-- site/src/components/Workspace/Workspace.tsx | 33 +++--- .../components/WorkspaceActions/Buttons.tsx | 10 +- .../WorkspaceActions/WorkspaceActions.tsx | 14 ++- .../components/WorkspaceActions/constants.ts | 6 +- .../ImpendingDeletionBanner.tsx | 18 +-- .../WorkspaceStatusBadge.tsx | 4 - site/src/i18n/en/templateSettingsPage.json | 12 +- .../TemplateScheduleForm.tsx | 111 +++++++++++------- .../useWorkspacesToBeDeleted.ts | 12 +- .../WorkspacePage/WorkspaceReadyPage.tsx | 2 +- .../WorkspacesPage/WorkspacesPageView.tsx | 4 +- .../xServices/workspace/workspaceXService.ts | 28 ++--- 13 files changed, 168 insertions(+), 126 deletions(-) diff --git a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx index 556f20989b7cf..c03c67686a0a2 100644 --- a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx +++ b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx @@ -10,6 +10,7 @@ import { ConfirmDialogType } from "../types" import Checkbox from "@mui/material/Checkbox" import FormControlLabel from "@mui/material/FormControlLabel" import { Stack } from "@mui/system" +import { getInsightsUserLatency } from "api/api" interface ConfirmDialogTypeConfig { confirmText: ReactNode @@ -156,10 +157,14 @@ export const ConfirmDialog: FC> = ({ } export interface ScheduleDialogProps extends ConfirmDialogProps { - readonly inactiveWorkspaceToBeLocked: number - readonly lockedWorkspacesToBeDeleted: number + readonly inactiveWorkspacesToGoDormant: number + readonly inactiveWorkspacesToGoDormantInWeek: number + readonly dormantWorkspacesToBeDeleted: number + readonly dormantWorkspacesToBeDeletedInWeek: number readonly updateLockedWorkspaces: (confirm: boolean) => void readonly updateInactiveWorkspaces: (confirm: boolean) => void + readonly dormantValueChanged: boolean + readonly deletionValueChanged: boolean } export const ScheduleDialog: FC> = ({ @@ -172,10 +177,14 @@ export const ScheduleDialog: FC> = ({ type, open = false, title, - inactiveWorkspaceToBeLocked, - lockedWorkspacesToBeDeleted, + inactiveWorkspacesToGoDormant, + inactiveWorkspacesToGoDormantInWeek, + dormantWorkspacesToBeDeleted, + dormantWorkspacesToBeDeletedInWeek, updateLockedWorkspaces, updateInactiveWorkspaces, + dormantValueChanged, + deletionValueChanged, }) => { const styles = useScheduleStyles({ type }) @@ -185,6 +194,14 @@ export const ScheduleDialog: FC> = ({ hideCancel = defaults.hideCancel } + const showDormancyWarning = + dormantValueChanged && + (inactiveWorkspacesToGoDormant > 0 || + inactiveWorkspacesToGoDormantInWeek > 0) + const showDeletionWarning = + deletionValueChanged && + (dormantWorkspacesToBeDeleted > 0 || dormantWorkspacesToBeDeletedInWeek > 0) + return ( > = ({

{title}

<> - {inactiveWorkspaceToBeLocked > 0 && ( + {showDormancyWarning && ( <> -

{"Inactivity TTL"}

+

{"Dormancy Threshold"}

-
{`The current value will result in ${inactiveWorkspaceToBeLocked}+ workspaces being automatically soft deleted. To prevent this, do you want to reset the inactivity period for all template workspaces?`}
+
{` + This change will result in ${inactiveWorkspacesToGoDormant} workspaces being immediately transitioned to the dormant state and ${inactiveWorkspacesToGoDormantInWeek} over the next seven days. To prevent this, do you want to reset the inactivity period for all template workspaces?`}
> = ({ )} - {lockedWorkspacesToBeDeleted > 0 && ( + {showDeletionWarning && ( <> -

{"Deletion Grace Period"}

+

{"Dormancy Auto-Deletion"}

{`The current value will result in ${lockedWorkspacesToBeDeleted}+ workspaces being permanently deleted. To prevent this, do you want to reset the soft-deletion period for all template workspaces?`}
+ >{`This change will result in ${dormantWorkspacesToBeDeleted} workspaces being immediately deleted and ${dormantWorkspacesToBeDeletedInWeek} over the next 7 days. To prevent this, do you want to reset the dormancy period for all template workspaces?`}
void handleSettings: () => void handleChangeVersion: () => void - handleUnlock: () => void + handleDormantActivate: () => void isUpdating: boolean isRestarting: boolean workspace: TypesGen.Workspace @@ -88,7 +88,7 @@ export const Workspace: FC> = ({ handleCancel, handleSettings, handleChangeVersion, - handleUnlock, + handleDormantActivate: handleDormantActivate, workspace, isUpdating, isRestarting, @@ -170,19 +170,14 @@ export const Workspace: FC> = ({ <> - {workspace.locked_at ? ( - - ) : ( - - {workspace.name} - - )} - + + {workspace.name} +
{workspace.name} {workspace.owner_name} @@ -211,7 +206,7 @@ export const Workspace: FC> = ({ handleCancel={handleCancel} handleSettings={handleSettings} handleChangeVersion={handleChangeVersion} - handleUnlock={handleUnlock} + handleDormantActivate={handleDormantActivate} canChangeVersions={canChangeVersions} isUpdating={isUpdating} isRestarting={isRestarting} @@ -262,7 +257,7 @@ export const Workspace: FC> = ({ {/* determines its own visibility */} - = ({ ) } -export const UnlockButton: FC = ({ +export const ActivateButton: FC = ({ handleAction, loading, }) => { return ( } + startIcon={} onClick={handleAction} > - Unlock + Activate ) } diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 409c77e673eab..23c2398372f40 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -12,7 +12,7 @@ import { StopButton, RestartButton, UpdateButton, - UnlockButton, + ActivateButton, } from "./Buttons" import { ButtonMapping, @@ -34,7 +34,7 @@ export interface WorkspaceActionsProps { handleCancel: () => void handleSettings: () => void handleChangeVersion: () => void - handleUnlock: () => void + handleDormantActivate: () => void isUpdating: boolean isRestarting: boolean children?: ReactNode @@ -51,7 +51,7 @@ export const WorkspaceActions: FC = ({ handleCancel, handleSettings, handleChangeVersion, - handleUnlock, + handleDormantActivate: handleDormantActivate, isUpdating, isRestarting, canChangeVersions, @@ -96,9 +96,11 @@ export const WorkspaceActions: FC = ({ [ButtonTypesEnum.canceling]: , [ButtonTypesEnum.deleted]: , [ButtonTypesEnum.pending]: , - [ButtonTypesEnum.unlock]: , - [ButtonTypesEnum.unlocking]: ( - + [ButtonTypesEnum.activate]: ( + + ), + [ButtonTypesEnum.activating]: ( + ), } diff --git a/site/src/components/WorkspaceActions/constants.ts b/site/src/components/WorkspaceActions/constants.ts index c21fdedc98249..1d2eeb9d4811e 100644 --- a/site/src/components/WorkspaceActions/constants.ts +++ b/site/src/components/WorkspaceActions/constants.ts @@ -12,8 +12,8 @@ export enum ButtonTypesEnum { deleting = "deleting", update = "update", updating = "updating", - unlock = "lock", - unlocking = "unlocking", + activate = "activate", + activating = "activating", // disabled buttons canceling = "canceling", deleted = "deleted", @@ -36,7 +36,7 @@ export const actionsByWorkspaceStatus = ( ): WorkspaceAbilities => { if (workspace.locked_at) { return { - actions: [ButtonTypesEnum.unlock], + actions: [ButtonTypesEnum.activate], canCancel: false, canAcceptJobs: false, } diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index 4e80250411352..b65f3a5cdc28b 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -10,7 +10,7 @@ export enum Count { Multiple, } -export const LockedWorkspaceBanner = ({ +export const DormantWorkspaceBanner = ({ workspaces, onDismiss, shouldRedisplayBanner, @@ -61,18 +61,18 @@ export const LockedWorkspaceBanner = ({ hasDeletionScheduledWorkspaces.deleting_at && hasDeletionScheduledWorkspaces.locked_at ) { - return `This workspace has been locked for ${formatDistanceToNow( + return `This workspace has been dormant for ${formatDistanceToNow( Date.parse(hasDeletionScheduledWorkspaces.locked_at), )} and is scheduled to be deleted on ${formatDate( hasDeletionScheduledWorkspaces.deleting_at, - )} . To keep it you must unlock the workspace.` + )} . To keep it you must activate the workspace.` } else if (hasLockedWorkspaces && hasLockedWorkspaces.locked_at) { - return `This workspace has been locked for ${formatDate( - hasLockedWorkspaces.locked_at, + return `This workspace has been dormant for ${formatDistanceToNow( + Date.parse(hasLockedWorkspaces.locked_at), )} and cannot be interacted - with. Locked workspaces are eligible for - permanent deletion. To prevent deletion, unlock + with. Dormant workspaces are eligible for + permanent deletion. To prevent deletion, activate the workspace.` } } @@ -92,8 +92,8 @@ export const LockedWorkspaceBanner = ({ > workspaces {" "} - that may be deleted soon due to inactivity. Unlock the workspaces you - wish to retain. + that may be deleted soon due to inactivity. Activate the workspaces + you wish to retain. )} diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx index 75a35f6839657..ac99d91522839 100644 --- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx @@ -28,10 +28,6 @@ export const WorkspaceStatusBadge: FC< ) return ( - {/* determines its own visibility */} - - - = ({ }, validationSchema, onSubmit: () => { - // Determine if this form will automatically - // lock workspaces upon submission. - const updateWillLockWorkspaces = + const dormancyChanged = + form.initialValues.inactivity_ttl_ms != form.values.inactivity_ttl_ms + const deletionChanged = + form.initialValues.locked_ttl_ms != form.values.locked_ttl_ms + + const dormancy = form.values.inactivity_cleanup_enabled && - workspacesToBeLockedToday && - workspacesToBeLockedToday.length > 0 + dormancyChanged && + workspacesToDormancyInWeek && + workspacesToDormancyInWeek.length > 0 - // Determine if this form will automatically - // delete locked workspaces upon submission. - const updateWillDeleteWorkspaces = - form.values.locked_cleanup_enabled && - workspacesToBeDeletedToday && - workspacesToBeDeletedToday.length > 0 + const deletion = + form.values.inactivity_cleanup_enabled && + deletionChanged && + workspacesToBeDeletedInWeek && + workspacesToBeDeletedInWeek.length > 0 - if (updateWillLockWorkspaces || updateWillDeleteWorkspaces) { + if (dormancy || deletion) { setIsScheduleDialogOpen(true) } else { submitValues() @@ -122,20 +124,42 @@ export const TemplateScheduleForm: FC = ({ const { t } = useTranslation("templateSettingsPage") const styles = useStyles() - const workspacesToBeLockedToday = useWorkspacesToBeLocked( + const now = new Date() + const weekFromNow = new Date(now) + weekFromNow.setDate(now.getDate() + 7) + + const workspacesToDormancyNow = useWorkspacesToBeLocked( + template, + form.values, + now, + ) + + const workspacesToDormancyInWeek = useWorkspacesToBeLocked( + template, + form.values, + weekFromNow, + ) + + const workspacesToBeDeletedNow = useWorkspacesToBeDeleted( template, form.values, + now, ) - const workspacesToBeDeletedToday = useWorkspacesToBeDeleted( + + const workspacesToBeDeletedInWeek = useWorkspacesToBeDeleted( template, form.values, + weekFromNow, ) const showScheduleDialog = - workspacesToBeLockedToday && - workspacesToBeDeletedToday && - (workspacesToBeLockedToday.length > 0 || - workspacesToBeDeletedToday.length > 0) + workspacesToDormancyNow && + workspacesToBeDeletedNow && + workspacesToDormancyInWeek && + workspacesToBeDeletedInWeek && + (workspacesToDormancyInWeek.length > 0 || + workspacesToBeDeletedInWeek.length > 0) + console.log("show dialog: ", showScheduleDialog) const [isScheduleDialogOpen, setIsScheduleDialogOpen] = useState(false) @@ -359,25 +383,25 @@ export const TemplateScheduleForm: FC = ({ } - label="Enable Inactivity Threshold" + label="Enable Dormancy Threshold" /> , )} @@ -386,52 +410,44 @@ export const TemplateScheduleForm: FC = ({ } fullWidth inputProps={{ min: 0, step: "any" }} - label="Time until cleanup (days)" + label="Time until dormant (days)" type="number" /> } - label="Enable Inactivity Deletion" + label="Enable Dormancy Auto-Deletion" /> , )} disabled={isSubmitting || !form.values.locked_cleanup_enabled} fullWidth inputProps={{ min: 0, step: "any" }} - label="Time until cleanup (days)" + label="Time until deletion (days)" type="number" /> )} - {workspacesToBeLockedToday && workspacesToBeLockedToday.length > 0 && ( - - )} {showScheduleDialog && ( { @@ -450,8 +466,14 @@ export const TemplateScheduleForm: FC = ({ throw error }) }} - inactiveWorkspaceToBeLocked={workspacesToBeLockedToday.length} - lockedWorkspacesToBeDeleted={workspacesToBeDeletedToday.length} + inactiveWorkspacesToGoDormant={workspacesToDormancyNow.length} + inactiveWorkspacesToGoDormantInWeek={ + workspacesToDormancyInWeek.length - workspacesToDormancyNow.length + } + dormantWorkspacesToBeDeleted={workspacesToBeDeletedNow.length} + dormantWorkspacesToBeDeletedInWeek={ + workspacesToBeDeletedInWeek.length - workspacesToBeDeletedNow.length + } open={isScheduleDialogOpen} onClose={() => { setIsScheduleDialogOpen(false) @@ -463,6 +485,13 @@ export const TemplateScheduleForm: FC = ({ updateInactiveWorkspaces={(update: boolean) => form.setFieldValue("update_workspace_last_used_at", update) } + dormantValueChanged={ + form.initialValues.inactivity_ttl_ms != + form.values.inactivity_ttl_ms + } + deletionValueChanged={ + form.initialValues.locked_ttl_ms != form.values.locked_ttl_ms + } /> )} diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts index feff04eeecee7..346d371f02951 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts @@ -6,6 +6,7 @@ import { useWorkspacesData } from "pages/WorkspacesPage/data" export const useWorkspacesToBeLocked = ( template: Template, formValues: TemplateScheduleFormValues, + fromDate: Date, ) => { const { data } = useWorkspacesData({ page: 0, @@ -24,18 +25,21 @@ export const useWorkspacesToBeLocked = ( const proposedLocking = new Date( new Date(workspace.last_used_at).getTime() + - formValues.inactivity_ttl_ms * 86400000, + formValues.inactivity_ttl_ms * DayInMS, ) - if (compareAsc(proposedLocking, new Date()) < 1) { + if (compareAsc(proposedLocking, fromDate) < 1) { return workspace } }) } +const DayInMS = 86400000 + export const useWorkspacesToBeDeleted = ( template: Template, formValues: TemplateScheduleFormValues, + fromDate: Date, ) => { const { data } = useWorkspacesData({ page: 0, @@ -49,10 +53,10 @@ export const useWorkspacesToBeDeleted = ( const proposedLocking = new Date( new Date(workspace.locked_at).getTime() + - formValues.locked_ttl_ms * 86400000, + formValues.locked_ttl_ms * DayInMS, ) - if (compareAsc(proposedLocking, new Date()) < 1) { + if (compareAsc(proposedLocking, fromDate) < 1) { return workspace } }) diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 4dc714960e57e..e7a92458e63bd 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -176,7 +176,7 @@ export const WorkspaceReadyPage = ({ handleChangeVersion={() => { setChangeVersionDialogOpen(true) }} - handleUnlock={() => workspaceSend({ type: "UNLOCK" })} + handleDormantActivate={() => workspaceSend({ type: "ACTIVATE" })} resources={workspace.latest_build.resources} builds={builds} canUpdateWorkspace={canUpdateWorkspace} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 78e61d7be39b4..9190db98c9575 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -14,7 +14,7 @@ import { Stack } from "components/Stack/Stack" import { WorkspaceHelpTooltip } from "components/Tooltips" import { WorkspacesTable } from "pages/WorkspacesPage/WorkspacesTable" import { useLocalStorage } from "hooks" -import { LockedWorkspaceBanner, Count } from "components/WorkspaceDeletion" +import { DormantWorkspaceBanner, Count } from "components/WorkspaceDeletion" import { ErrorAlert } from "components/Alert/ErrorAlert" import { WorkspacesFilter } from "./filter/filter" import { hasError, isApiValidationError } from "api/errors" @@ -101,7 +101,7 @@ export const WorkspacesPageView: FC< {/* determines its own visibility */} - diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index ead81dcf5a3e4..d3e9f7204e25c 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -96,7 +96,7 @@ export type WorkspaceEvent = | { type: "INCREASE_DEADLINE"; hours: number } | { type: "DECREASE_DEADLINE"; hours: number } | { type: "RETRY_BUILD" } - | { type: "UNLOCK" } + | { type: "ACTIVATE" } export const checks = { readWorkspace: "readWorkspace", @@ -171,7 +171,7 @@ export const workspaceMachine = createMachine( cancelWorkspace: { data: Types.Message } - unlockWorkspace: { + activateWorkspace: { data: Types.Message } listening: { @@ -264,7 +264,7 @@ export const workspaceMachine = createMachine( actions: ["enableDebugMode"], }, ], - UNLOCK: "requestingUnlock", + ACTIVATE: "requestingActivate", }, }, askingDelete: { @@ -410,15 +410,15 @@ export const workspaceMachine = createMachine( ], }, }, - requestingUnlock: { + requestingActivate: { entry: ["clearBuildError"], invoke: { - src: "unlockWorkspace", - id: "unlockWorkspace", + src: "activateWorkspace", + id: "activateWorkspace", onDone: "idle", onError: { target: "idle", - actions: ["displayUnlockError"], + actions: ["displayActivateError"], }, }, }, @@ -576,8 +576,8 @@ export const workspaceMachine = createMachine( ) displayError(message) }, - displayUnlockError: (_, { data }) => { - const message = getErrorMessage(data, "Error unlocking workspace.") + displayActivateError: (_, { data }) => { + const message = getErrorMessage(data, "Error activate workspace.") displayError(message) }, assignMissedParameters: assign({ @@ -695,16 +695,16 @@ export const workspaceMachine = createMachine( throw Error("Cannot cancel workspace without build id") } }, - unlockWorkspace: (context) => async (send) => { + activateWorkspace: (context) => async (send) => { if (context.workspace) { - const unlockWorkspacePromise = await API.updateWorkspaceLock( + const activateWorkspacePromise = await API.updateWorkspaceLock( context.workspace.id, false, ) - send({ type: "REFRESH_WORKSPACE", data: unlockWorkspacePromise }) - return unlockWorkspacePromise + send({ type: "REFRESH_WORKSPACE", data: activateWorkspacePromise }) + return activateWorkspacePromise } else { - throw Error("Cannot unlock workspace without workspace id") + throw Error("Cannot activate workspace without workspace id") } }, listening: (context) => (send) => { From 260bb6e0f5f905655ae9918a31179fe7169d7185 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 22 Aug 2023 19:28:30 +0000 Subject: [PATCH 5/6] linting --- .../Dialogs/ConfirmDialog/ConfirmDialog.tsx | 1 - site/src/components/Workspace/Workspace.tsx | 1 - .../WorkspaceStatusBadge/WorkspaceStatusBadge.tsx | 5 +---- .../TemplateScheduleForm/TemplateScheduleForm.tsx | 15 +++++++-------- .../TemplateScheduleForm/formHelpers.tsx | 8 ++++---- .../TemplateSchedulePage.test.tsx | 14 +++++++------- 6 files changed, 19 insertions(+), 25 deletions(-) diff --git a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx index c03c67686a0a2..13f76574fcdd7 100644 --- a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx +++ b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx @@ -10,7 +10,6 @@ import { ConfirmDialogType } from "../types" import Checkbox from "@mui/material/Checkbox" import FormControlLabel from "@mui/material/FormControlLabel" import { Stack } from "@mui/system" -import { getInsightsUserLatency } from "api/api" interface ConfirmDialogTypeConfig { confirmText: ReactNode diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 99896161b1fd3..d64e25dd89979 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -1,6 +1,5 @@ import Button from "@mui/material/Button" import { makeStyles } from "@mui/styles" -import BedtimeIcon from "@mui/icons-material/Bedtime" import { Avatar } from "components/Avatar/Avatar" import { AgentRow } from "components/Resources/AgentRow" import { diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx index ac99d91522839..5618ba056d7d8 100644 --- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx @@ -4,10 +4,7 @@ import { FC, PropsWithChildren } from "react" import { makeStyles } from "@mui/styles" import { combineClasses } from "utils/combineClasses" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { - LockedBadge, - ImpendingDeletionText, -} from "components/WorkspaceDeletion" +import { ImpendingDeletionText } from "components/WorkspaceDeletion" import { getDisplayWorkspaceStatus } from "utils/workspace" import Tooltip, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip" import { styled } from "@mui/material/styles" diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx index 6a37e69d3a150..180d81df978b8 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx @@ -93,23 +93,23 @@ export const TemplateScheduleForm: FC = ({ validationSchema, onSubmit: () => { const dormancyChanged = - form.initialValues.inactivity_ttl_ms != form.values.inactivity_ttl_ms + form.initialValues.inactivity_ttl_ms !== form.values.inactivity_ttl_ms const deletionChanged = - form.initialValues.locked_ttl_ms != form.values.locked_ttl_ms + form.initialValues.locked_ttl_ms !== form.values.locked_ttl_ms - const dormancy = + const dormancyScheduleChanged = form.values.inactivity_cleanup_enabled && dormancyChanged && workspacesToDormancyInWeek && workspacesToDormancyInWeek.length > 0 - const deletion = + const deletionScheduleChanged = form.values.inactivity_cleanup_enabled && deletionChanged && workspacesToBeDeletedInWeek && workspacesToBeDeletedInWeek.length > 0 - if (dormancy || deletion) { + if (dormancyScheduleChanged || deletionScheduleChanged) { setIsScheduleDialogOpen(true) } else { submitValues() @@ -159,7 +159,6 @@ export const TemplateScheduleForm: FC = ({ workspacesToBeDeletedInWeek && (workspacesToDormancyInWeek.length > 0 || workspacesToBeDeletedInWeek.length > 0) - console.log("show dialog: ", showScheduleDialog) const [isScheduleDialogOpen, setIsScheduleDialogOpen] = useState(false) @@ -486,11 +485,11 @@ export const TemplateScheduleForm: FC = ({ form.setFieldValue("update_workspace_last_used_at", update) } dormantValueChanged={ - form.initialValues.inactivity_ttl_ms != + form.initialValues.inactivity_ttl_ms !== form.values.inactivity_ttl_ms } deletionValueChanged={ - form.initialValues.locked_ttl_ms != form.values.locked_ttl_ms + form.initialValues.locked_ttl_ms !== form.values.locked_ttl_ms } /> )} diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/formHelpers.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/formHelpers.tsx index ee1fe7b13d302..d36fcd85b021c 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/formHelpers.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/formHelpers.tsx @@ -51,10 +51,10 @@ export const getValidationSchema = (): Yup.AnyObjectSchema => }, ), inactivity_ttl_ms: Yup.number() - .min(0, "Inactivity cleanup days must not be less than 0.") + .min(0, "Dormancy threshold days must not be less than 0.") .test( "positive-if-enabled", - "Inactivity cleanup days must be greater than zero when enabled.", + "Dormancy threshold days must be greater than zero when enabled.", function (value) { const parent = this.parent as TemplateScheduleFormValues if (parent.inactivity_cleanup_enabled) { @@ -65,10 +65,10 @@ export const getValidationSchema = (): Yup.AnyObjectSchema => }, ), locked_ttl_ms: Yup.number() - .min(0, "Locked cleanup days must not be less than 0.") + .min(0, "Dormancy auto-deletion days must not be less than 0.") .test( "positive-if-enabled", - "Locked cleanup days must be greater than zero when enabled.", + "Dormancy auto-deletion days must be greater than zero when enabled.", function (value) { const parent = this.parent as TemplateScheduleFormValues if (parent.locked_cleanup_enabled) { diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx index 97dedbe454c84..dd03610717829 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -65,12 +65,12 @@ const fillAndSubmitForm = async ({ await user.type(failureTtlField, failure_ttl_ms.toString()) const inactivityTtlField = screen.getByRole("checkbox", { - name: /Inactivity Soft Deletion/i, + name: /Dormancy Threshold/i, }) await user.type(inactivityTtlField, inactivity_ttl_ms.toString()) const lockedTtlField = screen.getByRole("checkbox", { - name: /Deletion Retention/i, + name: /Dormancy Auto-Deletion/i, }) await user.type(lockedTtlField, locked_ttl_ms.toString()) @@ -125,7 +125,7 @@ describe("TemplateSchedulePage", () => { ) }) - test("failure, inactivity, and locked ttl converted to and from days", async () => { + test("failure, dormancy, and dormancy auto-deletion converted to and from days", async () => { await renderTemplateSchedulePage() jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ @@ -239,11 +239,11 @@ describe("TemplateSchedulePage", () => { } const validate = () => getValidationSchema().validateSync(values) expect(validate).toThrowError( - "Inactivity cleanup days must not be less than 0.", + "Dormancy threshold days must not be less than 0.", ) }) - it("allows a locked ttl of 7 days", () => { + it("allows a dormancy ttl of 7 days", () => { const values: UpdateTemplateMeta = { ...validFormValues, locked_ttl_ms: 86400000 * 7, @@ -252,7 +252,7 @@ describe("TemplateSchedulePage", () => { expect(validate).not.toThrowError() }) - it("allows a locked ttl of 0", () => { + it("allows a dormancy ttl of 0", () => { const values: UpdateTemplateMeta = { ...validFormValues, locked_ttl_ms: 0, @@ -268,7 +268,7 @@ describe("TemplateSchedulePage", () => { } const validate = () => getValidationSchema().validateSync(values) expect(validate).toThrowError( - "Locked cleanup days must not be less than 0.", + "Dormancy auto-deletion days must not be less than 0.", ) }) }) From f1dbf7d30f463aa7b6a4d8846c7872c31780c205 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 22 Aug 2023 19:58:06 +0000 Subject: [PATCH 6/6] fix tests --- enterprise/coderd/schedule/template.go | 2 +- site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/enterprise/coderd/schedule/template.go b/enterprise/coderd/schedule/template.go index 08408e50d44b4..c5613c44e7880 100644 --- a/enterprise/coderd/schedule/template.go +++ b/enterprise/coderd/schedule/template.go @@ -171,7 +171,7 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S // Recalculate max_deadline and deadline for all running workspace // builds on this template. if s.UseRestartRequirement.Load() { - err = s.updateWorkspaceBuilds(ctx, db, template) + err = s.updateWorkspaceBuilds(ctx, tx, template) if err != nil { return xerrors.Errorf("update workspace builds: %w", err) } diff --git a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx index 13f76574fcdd7..5a1cfcc80060e 100644 --- a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx +++ b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx @@ -64,7 +64,7 @@ const useStyles = makeStyles((theme) => ({ background: theme.palette.background.paper, border: `1px solid ${theme.palette.divider}`, width: "100%", - maxWidth: theme.spacing(50), + maxWidth: theme.spacing(55), }, "& .MuiDialogActions-spacing": { padding: `0 ${theme.spacing(5)} ${theme.spacing(5)}`,