diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index c93af6a64a41c..61deac3008a8b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -15595,6 +15595,19 @@ const docTemplate = `{ "TemplateVersionWarningUnsupportedWorkspaces" ] }, + "codersdk.TerminalFontName": { + "type": "string", + "enum": [ + "", + "ibm-plex-mono", + "fira-code" + ], + "x-enum-varnames": [ + "TerminalFontUnknown", + "TerminalFontIBMPlexMono", + "TerminalFontFiraCode" + ] + }, "codersdk.TimingStage": { "type": "string", "enum": [ @@ -15768,9 +15781,13 @@ const docTemplate = `{ "codersdk.UpdateUserAppearanceSettingsRequest": { "type": "object", "required": [ + "terminal_font", "theme_preference" ], "properties": { + "terminal_font": { + "$ref": "#/definitions/codersdk.TerminalFontName" + }, "theme_preference": { "type": "string" } @@ -16062,6 +16079,9 @@ const docTemplate = `{ "codersdk.UserAppearanceSettings": { "type": "object", "properties": { + "terminal_font": { + "$ref": "#/definitions/codersdk.TerminalFontName" + }, "theme_preference": { "type": "string" } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index da4d7a4fcf41c..0d2b63ef3131a 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -14180,6 +14180,15 @@ "enum": ["UNSUPPORTED_WORKSPACES"], "x-enum-varnames": ["TemplateVersionWarningUnsupportedWorkspaces"] }, + "codersdk.TerminalFontName": { + "type": "string", + "enum": ["", "ibm-plex-mono", "fira-code"], + "x-enum-varnames": [ + "TerminalFontUnknown", + "TerminalFontIBMPlexMono", + "TerminalFontFiraCode" + ] + }, "codersdk.TimingStage": { "type": "string", "enum": [ @@ -14350,8 +14359,11 @@ }, "codersdk.UpdateUserAppearanceSettingsRequest": { "type": "object", - "required": ["theme_preference"], + "required": ["terminal_font", "theme_preference"], "properties": { + "terminal_font": { + "$ref": "#/definitions/codersdk.TerminalFontName" + }, "theme_preference": { "type": "string" } @@ -14617,6 +14629,9 @@ "codersdk.UserAppearanceSettings": { "type": "object", "properties": { + "terminal_font": { + "$ref": "#/definitions/codersdk.TerminalFontName" + }, "theme_preference": { "type": "string" } diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 3815f713c0f4e..042e97f6569f6 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2711,17 +2711,6 @@ func (q *querier) GetUserActivityInsights(ctx context.Context, arg database.GetU return q.db.GetUserActivityInsights(ctx, arg) } -func (q *querier) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) { - u, err := q.db.GetUserByID(ctx, userID) - if err != nil { - return "", err - } - if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil { - return "", err - } - return q.db.GetUserAppearanceSettings(ctx, userID) -} - func (q *querier) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { return fetch(q.log, q.auth, q.db.GetUserByEmailOrUsername)(ctx, arg) } @@ -2794,6 +2783,28 @@ func (q *querier) GetUserStatusCounts(ctx context.Context, arg database.GetUserS return q.db.GetUserStatusCounts(ctx, arg) } +func (q *querier) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) { + u, err := q.db.GetUserByID(ctx, userID) + if err != nil { + return "", err + } + if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil { + return "", err + } + return q.db.GetUserTerminalFont(ctx, userID) +} + +func (q *querier) GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) { + u, err := q.db.GetUserByID(ctx, userID) + if err != nil { + return "", err + } + if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil { + return "", err + } + return q.db.GetUserThemePreference(ctx, userID) +} + func (q *querier) GetUserWorkspaceBuildParameters(ctx context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) { u, err := q.db.GetUserByID(ctx, params.OwnerID) if err != nil { @@ -4311,17 +4322,6 @@ func (q *querier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg da return fetchAndExec(q.log, q.auth, policy.ActionUpdate, fetch, q.db.UpdateTemplateWorkspacesLastUsedAt)(ctx, arg) } -func (q *querier) UpdateUserAppearanceSettings(ctx context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.UserConfig, error) { - u, err := q.db.GetUserByID(ctx, arg.UserID) - if err != nil { - return database.UserConfig{}, err - } - if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { - return database.UserConfig{}, err - } - return q.db.UpdateUserAppearanceSettings(ctx, arg) -} - func (q *querier) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error { return deleteQ(q.log, q.auth, q.db.GetUserByID, q.db.UpdateUserDeletedByID)(ctx, id) } @@ -4459,6 +4459,28 @@ func (q *querier) UpdateUserStatus(ctx context.Context, arg database.UpdateUserS return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateUserStatus)(ctx, arg) } +func (q *querier) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) { + u, err := q.db.GetUserByID(ctx, arg.UserID) + if err != nil { + return database.UserConfig{}, err + } + if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { + return database.UserConfig{}, err + } + return q.db.UpdateUserTerminalFont(ctx, arg) +} + +func (q *querier) UpdateUserThemePreference(ctx context.Context, arg database.UpdateUserThemePreferenceParams) (database.UserConfig, error) { + u, err := q.db.GetUserByID(ctx, arg.UserID) + if err != nil { + return database.UserConfig{}, err + } + if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { + return database.UserConfig{}, err + } + return q.db.UpdateUserThemePreference(ctx, arg) +} + func (q *querier) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 0fe17f886b1b2..bd6ef69528335 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1630,27 +1630,48 @@ func (s *MethodTestSuite) TestUser() { []database.GetUserWorkspaceBuildParametersRow{}, ) })) - s.Run("GetUserAppearanceSettings", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetUserThemePreference", s.Subtest(func(db database.Store, check *expects) { ctx := context.Background() u := dbgen.User(s.T(), db, database.User{}) - db.UpdateUserAppearanceSettings(ctx, database.UpdateUserAppearanceSettingsParams{ + db.UpdateUserThemePreference(ctx, database.UpdateUserThemePreferenceParams{ UserID: u.ID, ThemePreference: "light", }) check.Args(u.ID).Asserts(u, policy.ActionReadPersonal).Returns("light") })) - s.Run("UpdateUserAppearanceSettings", s.Subtest(func(db database.Store, check *expects) { + s.Run("UpdateUserThemePreference", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) uc := database.UserConfig{ UserID: u.ID, Key: "theme_preference", Value: "dark", } - check.Args(database.UpdateUserAppearanceSettingsParams{ + check.Args(database.UpdateUserThemePreferenceParams{ UserID: u.ID, ThemePreference: uc.Value, }).Asserts(u, policy.ActionUpdatePersonal).Returns(uc) })) + s.Run("GetUserTerminalFont", s.Subtest(func(db database.Store, check *expects) { + ctx := context.Background() + u := dbgen.User(s.T(), db, database.User{}) + db.UpdateUserTerminalFont(ctx, database.UpdateUserTerminalFontParams{ + UserID: u.ID, + TerminalFont: "ibm-plex-mono", + }) + check.Args(u.ID).Asserts(u, policy.ActionReadPersonal).Returns("ibm-plex-mono") + })) + s.Run("UpdateUserTerminalFont", s.Subtest(func(db database.Store, check *expects) { + u := dbgen.User(s.T(), db, database.User{}) + uc := database.UserConfig{ + UserID: u.ID, + Key: "terminal_font", + Value: "ibm-plex-mono", + } + check.Args(database.UpdateUserTerminalFontParams{ + UserID: u.ID, + TerminalFont: uc.Value, + }).Asserts(u, policy.ActionUpdatePersonal).Returns(uc) + })) s.Run("UpdateUserStatus", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) check.Args(database.UpdateUserStatusParams{ diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index bfae69fa68b98..a8351cbb8ca16 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -6434,20 +6434,6 @@ func (q *FakeQuerier) GetUserActivityInsights(_ context.Context, arg database.Ge return rows, nil } -func (q *FakeQuerier) GetUserAppearanceSettings(_ context.Context, userID uuid.UUID) (string, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - for _, uc := range q.userConfigs { - if uc.UserID != userID || uc.Key != "theme_preference" { - continue - } - return uc.Value, nil - } - - return "", sql.ErrNoRows -} - func (q *FakeQuerier) GetUserByEmailOrUsername(_ context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { if err := validateDatabaseType(arg); err != nil { return database.User{}, err @@ -6660,6 +6646,34 @@ func (q *FakeQuerier) GetUserStatusCounts(_ context.Context, arg database.GetUse return result, nil } +func (q *FakeQuerier) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, uc := range q.userConfigs { + if uc.UserID != userID || uc.Key != "terminal_font" { + continue + } + return uc.Value, nil + } + + return "", sql.ErrNoRows +} + +func (q *FakeQuerier) GetUserThemePreference(_ context.Context, userID uuid.UUID) (string, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, uc := range q.userConfigs { + if uc.UserID != userID || uc.Key != "theme_preference" { + continue + } + return uc.Value, nil + } + + return "", sql.ErrNoRows +} + func (q *FakeQuerier) GetUserWorkspaceBuildParameters(_ context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -10996,33 +11010,6 @@ func (q *FakeQuerier) UpdateTemplateWorkspacesLastUsedAt(_ context.Context, arg return nil } -func (q *FakeQuerier) UpdateUserAppearanceSettings(_ context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.UserConfig, error) { - err := validateDatabaseType(arg) - if err != nil { - return database.UserConfig{}, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - for i, uc := range q.userConfigs { - if uc.UserID != arg.UserID || uc.Key != "theme_preference" { - continue - } - uc.Value = arg.ThemePreference - q.userConfigs[i] = uc - return uc, nil - } - - uc := database.UserConfig{ - UserID: arg.UserID, - Key: "theme_preference", - Value: arg.ThemePreference, - } - q.userConfigs = append(q.userConfigs, uc) - return uc, nil -} - func (q *FakeQuerier) UpdateUserDeletedByID(_ context.Context, id uuid.UUID) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -11348,6 +11335,60 @@ func (q *FakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUse return database.User{}, sql.ErrNoRows } +func (q *FakeQuerier) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.UserConfig{}, err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, uc := range q.userConfigs { + if uc.UserID != arg.UserID || uc.Key != "terminal_font" { + continue + } + uc.Value = arg.TerminalFont + q.userConfigs[i] = uc + return uc, nil + } + + uc := database.UserConfig{ + UserID: arg.UserID, + Key: "terminal_font", + Value: arg.TerminalFont, + } + q.userConfigs = append(q.userConfigs, uc) + return uc, nil +} + +func (q *FakeQuerier) UpdateUserThemePreference(_ context.Context, arg database.UpdateUserThemePreferenceParams) (database.UserConfig, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.UserConfig{}, err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, uc := range q.userConfigs { + if uc.UserID != arg.UserID || uc.Key != "theme_preference" { + continue + } + uc.Value = arg.ThemePreference + q.userConfigs[i] = uc + return uc, nil + } + + uc := database.UserConfig{ + UserID: arg.UserID, + Key: "theme_preference", + Value: arg.ThemePreference, + } + q.userConfigs = append(q.userConfigs, uc) + return uc, nil +} + func (q *FakeQuerier) UpdateVolumeResourceMonitor(_ context.Context, arg database.UpdateVolumeResourceMonitorParams) error { err := validateDatabaseType(arg) if err != nil { diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index b29d95752d195..31ce37e096f1f 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1502,13 +1502,6 @@ func (m queryMetricsStore) GetUserActivityInsights(ctx context.Context, arg data return r0, r1 } -func (m queryMetricsStore) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) { - start := time.Now() - r0, r1 := m.s.GetUserAppearanceSettings(ctx, userID) - m.queryLatencies.WithLabelValues("GetUserAppearanceSettings").Observe(time.Since(start).Seconds()) - return r0, r1 -} - func (m queryMetricsStore) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { start := time.Now() user, err := m.s.GetUserByEmailOrUsername(ctx, arg) @@ -1572,6 +1565,20 @@ func (m queryMetricsStore) GetUserStatusCounts(ctx context.Context, arg database return r0, r1 } +func (m queryMetricsStore) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) { + start := time.Now() + r0, r1 := m.s.GetUserTerminalFont(ctx, userID) + m.queryLatencies.WithLabelValues("GetUserTerminalFont").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) { + start := time.Now() + r0, r1 := m.s.GetUserThemePreference(ctx, userID) + m.queryLatencies.WithLabelValues("GetUserThemePreference").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetUserWorkspaceBuildParameters(ctx context.Context, ownerID database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) { start := time.Now() r0, r1 := m.s.GetUserWorkspaceBuildParameters(ctx, ownerID) @@ -2727,13 +2734,6 @@ func (m queryMetricsStore) UpdateTemplateWorkspacesLastUsedAt(ctx context.Contex return r0 } -func (m queryMetricsStore) UpdateUserAppearanceSettings(ctx context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.UserConfig, error) { - start := time.Now() - r0, r1 := m.s.UpdateUserAppearanceSettings(ctx, arg) - m.queryLatencies.WithLabelValues("UpdateUserAppearanceSettings").Observe(time.Since(start).Seconds()) - return r0, r1 -} - func (m queryMetricsStore) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error { start := time.Now() r0 := m.s.UpdateUserDeletedByID(ctx, id) @@ -2825,6 +2825,20 @@ func (m queryMetricsStore) UpdateUserStatus(ctx context.Context, arg database.Up return user, err } +func (m queryMetricsStore) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) { + start := time.Now() + r0, r1 := m.s.UpdateUserTerminalFont(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateUserTerminalFont").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) UpdateUserThemePreference(ctx context.Context, arg database.UpdateUserThemePreferenceParams) (database.UserConfig, error) { + start := time.Now() + r0, r1 := m.s.UpdateUserThemePreference(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateUserThemePreference").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { start := time.Now() r0 := m.s.UpdateVolumeResourceMonitor(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index e30759c6bba42..4117cb71074be 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3139,21 +3139,6 @@ func (mr *MockStoreMockRecorder) GetUserActivityInsights(ctx, arg any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserActivityInsights", reflect.TypeOf((*MockStore)(nil).GetUserActivityInsights), ctx, arg) } -// GetUserAppearanceSettings mocks base method. -func (m *MockStore) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserAppearanceSettings", ctx, userID) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserAppearanceSettings indicates an expected call of GetUserAppearanceSettings. -func (mr *MockStoreMockRecorder) GetUserAppearanceSettings(ctx, userID any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserAppearanceSettings", reflect.TypeOf((*MockStore)(nil).GetUserAppearanceSettings), ctx, userID) -} - // GetUserByEmailOrUsername mocks base method. func (m *MockStore) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { m.ctrl.T.Helper() @@ -3289,6 +3274,36 @@ func (mr *MockStoreMockRecorder) GetUserStatusCounts(ctx, arg any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStatusCounts", reflect.TypeOf((*MockStore)(nil).GetUserStatusCounts), ctx, arg) } +// GetUserTerminalFont mocks base method. +func (m *MockStore) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserTerminalFont", ctx, userID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserTerminalFont indicates an expected call of GetUserTerminalFont. +func (mr *MockStoreMockRecorder) GetUserTerminalFont(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserTerminalFont", reflect.TypeOf((*MockStore)(nil).GetUserTerminalFont), ctx, userID) +} + +// GetUserThemePreference mocks base method. +func (m *MockStore) GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserThemePreference", ctx, userID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserThemePreference indicates an expected call of GetUserThemePreference. +func (mr *MockStoreMockRecorder) GetUserThemePreference(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserThemePreference", reflect.TypeOf((*MockStore)(nil).GetUserThemePreference), ctx, userID) +} + // GetUserWorkspaceBuildParameters mocks base method. func (m *MockStore) GetUserWorkspaceBuildParameters(ctx context.Context, arg database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) { m.ctrl.T.Helper() @@ -5768,21 +5783,6 @@ func (mr *MockStoreMockRecorder) UpdateTemplateWorkspacesLastUsedAt(ctx, arg any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateWorkspacesLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateTemplateWorkspacesLastUsedAt), ctx, arg) } -// UpdateUserAppearanceSettings mocks base method. -func (m *MockStore) UpdateUserAppearanceSettings(ctx context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.UserConfig, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUserAppearanceSettings", ctx, arg) - ret0, _ := ret[0].(database.UserConfig) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateUserAppearanceSettings indicates an expected call of UpdateUserAppearanceSettings. -func (mr *MockStoreMockRecorder) UpdateUserAppearanceSettings(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserAppearanceSettings", reflect.TypeOf((*MockStore)(nil).UpdateUserAppearanceSettings), ctx, arg) -} - // UpdateUserDeletedByID mocks base method. func (m *MockStore) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error { m.ctrl.T.Helper() @@ -5974,6 +5974,36 @@ func (mr *MockStoreMockRecorder) UpdateUserStatus(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatus", reflect.TypeOf((*MockStore)(nil).UpdateUserStatus), ctx, arg) } +// UpdateUserTerminalFont mocks base method. +func (m *MockStore) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserTerminalFont", ctx, arg) + ret0, _ := ret[0].(database.UserConfig) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateUserTerminalFont indicates an expected call of UpdateUserTerminalFont. +func (mr *MockStoreMockRecorder) UpdateUserTerminalFont(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserTerminalFont", reflect.TypeOf((*MockStore)(nil).UpdateUserTerminalFont), ctx, arg) +} + +// UpdateUserThemePreference mocks base method. +func (m *MockStore) UpdateUserThemePreference(ctx context.Context, arg database.UpdateUserThemePreferenceParams) (database.UserConfig, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserThemePreference", ctx, arg) + ret0, _ := ret[0].(database.UserConfig) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateUserThemePreference indicates an expected call of UpdateUserThemePreference. +func (mr *MockStoreMockRecorder) UpdateUserThemePreference(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserThemePreference", reflect.TypeOf((*MockStore)(nil).UpdateUserThemePreference), ctx, arg) +} + // UpdateVolumeResourceMonitor mocks base method. func (m *MockStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 54483c2176f4e..028c025dde921 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -343,7 +343,6 @@ type sqlcQuerier interface { // produces a bloated value if a user has used multiple templates // simultaneously. GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error) - GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) GetUserCount(ctx context.Context, includeSystem bool) (int64, error) @@ -369,6 +368,8 @@ type sqlcQuerier interface { // We do not start counting from 0 at the start_time. We check the last status change before the start_time for each user. As such, // the result shows the total number of users in each status on any particular day. GetUserStatusCounts(ctx context.Context, arg GetUserStatusCountsParams) ([]GetUserStatusCountsRow, error) + GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) + GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error) // This will never return deleted users. GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error) @@ -570,7 +571,6 @@ type sqlcQuerier interface { UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error UpdateTemplateVersionExternalAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionExternalAuthProvidersByJobIDParams) error UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error - UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (UserConfig, error) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error UpdateUserHashedOneTimePasscode(ctx context.Context, arg UpdateUserHashedOneTimePasscodeParams) error @@ -584,6 +584,8 @@ type sqlcQuerier interface { UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) + UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error) + UpdateUserThemePreference(ctx context.Context, arg UpdateUserThemePreferenceParams) (UserConfig, error) UpdateVolumeResourceMonitor(ctx context.Context, arg UpdateVolumeResourceMonitorParams) error UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (WorkspaceTable, error) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e1c7c3e65ab92..e9acf4bb5462b 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -12159,23 +12159,6 @@ func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid. return i, err } -const getUserAppearanceSettings = `-- name: GetUserAppearanceSettings :one -SELECT - value as theme_preference -FROM - user_configs -WHERE - user_id = $1 - AND key = 'theme_preference' -` - -func (q *sqlQuerier) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) { - row := q.db.QueryRowContext(ctx, getUserAppearanceSettings, userID) - var theme_preference string - err := row.Scan(&theme_preference) - return theme_preference, err -} - const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system @@ -12273,6 +12256,40 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context, includeSystem bool) (int6 return count, err } +const getUserTerminalFont = `-- name: GetUserTerminalFont :one +SELECT + value as terminal_font +FROM + user_configs +WHERE + user_id = $1 + AND key = 'terminal_font' +` + +func (q *sqlQuerier) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) { + row := q.db.QueryRowContext(ctx, getUserTerminalFont, userID) + var terminal_font string + err := row.Scan(&terminal_font) + return terminal_font, err +} + +const getUserThemePreference = `-- name: GetUserThemePreference :one +SELECT + value as theme_preference +FROM + user_configs +WHERE + user_id = $1 + AND key = 'theme_preference' +` + +func (q *sqlQuerier) GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) { + row := q.db.QueryRowContext(ctx, getUserThemePreference, userID) + var theme_preference string + err := row.Scan(&theme_preference) + return theme_preference, err +} + const getUsers = `-- name: GetUsers :many SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, COUNT(*) OVER() AS count @@ -12636,33 +12653,6 @@ func (q *sqlQuerier) UpdateInactiveUsersToDormant(ctx context.Context, arg Updat return items, nil } -const updateUserAppearanceSettings = `-- name: UpdateUserAppearanceSettings :one -INSERT INTO - user_configs (user_id, key, value) -VALUES - ($1, 'theme_preference', $2) -ON CONFLICT - ON CONSTRAINT user_configs_pkey -DO UPDATE -SET - value = $2 -WHERE user_configs.user_id = $1 - AND user_configs.key = 'theme_preference' -RETURNING user_id, key, value -` - -type UpdateUserAppearanceSettingsParams struct { - UserID uuid.UUID `db:"user_id" json:"user_id"` - ThemePreference string `db:"theme_preference" json:"theme_preference"` -} - -func (q *sqlQuerier) UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (UserConfig, error) { - row := q.db.QueryRowContext(ctx, updateUserAppearanceSettings, arg.UserID, arg.ThemePreference) - var i UserConfig - err := row.Scan(&i.UserID, &i.Key, &i.Value) - return i, err -} - const updateUserDeletedByID = `-- name: UpdateUserDeletedByID :exec UPDATE users @@ -13010,6 +13000,60 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP return i, err } +const updateUserTerminalFont = `-- name: UpdateUserTerminalFont :one +INSERT INTO + user_configs (user_id, key, value) +VALUES + ($1, 'terminal_font', $2) +ON CONFLICT + ON CONSTRAINT user_configs_pkey +DO UPDATE +SET + value = $2 +WHERE user_configs.user_id = $1 + AND user_configs.key = 'terminal_font' +RETURNING user_id, key, value +` + +type UpdateUserTerminalFontParams struct { + UserID uuid.UUID `db:"user_id" json:"user_id"` + TerminalFont string `db:"terminal_font" json:"terminal_font"` +} + +func (q *sqlQuerier) UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error) { + row := q.db.QueryRowContext(ctx, updateUserTerminalFont, arg.UserID, arg.TerminalFont) + var i UserConfig + err := row.Scan(&i.UserID, &i.Key, &i.Value) + return i, err +} + +const updateUserThemePreference = `-- name: UpdateUserThemePreference :one +INSERT INTO + user_configs (user_id, key, value) +VALUES + ($1, 'theme_preference', $2) +ON CONFLICT + ON CONSTRAINT user_configs_pkey +DO UPDATE +SET + value = $2 +WHERE user_configs.user_id = $1 + AND user_configs.key = 'theme_preference' +RETURNING user_id, key, value +` + +type UpdateUserThemePreferenceParams struct { + UserID uuid.UUID `db:"user_id" json:"user_id"` + ThemePreference string `db:"theme_preference" json:"theme_preference"` +} + +func (q *sqlQuerier) UpdateUserThemePreference(ctx context.Context, arg UpdateUserThemePreferenceParams) (UserConfig, error) { + row := q.db.QueryRowContext(ctx, updateUserThemePreference, arg.UserID, arg.ThemePreference) + var i UserConfig + err := row.Scan(&i.UserID, &i.Key, &i.Value) + return i, err +} + const getWorkspaceAgentDevcontainersByAgentID = `-- name: GetWorkspaceAgentDevcontainersByAgentID :many SELECT id, workspace_agent_id, created_at, workspace_folder, config_path, name diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index c4304cfc3e60e..0bac76c8df14a 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -102,7 +102,7 @@ SET WHERE id = $1; --- name: GetUserAppearanceSettings :one +-- name: GetUserThemePreference :one SELECT value as theme_preference FROM @@ -111,7 +111,7 @@ WHERE user_id = @user_id AND key = 'theme_preference'; --- name: UpdateUserAppearanceSettings :one +-- name: UpdateUserThemePreference :one INSERT INTO user_configs (user_id, key, value) VALUES @@ -125,6 +125,29 @@ WHERE user_configs.user_id = @user_id AND user_configs.key = 'theme_preference' RETURNING *; +-- name: GetUserTerminalFont :one +SELECT + value as terminal_font +FROM + user_configs +WHERE + user_id = @user_id + AND key = 'terminal_font'; + +-- name: UpdateUserTerminalFont :one +INSERT INTO + user_configs (user_id, key, value) +VALUES + (@user_id, 'terminal_font', @terminal_font) +ON CONFLICT + ON CONSTRAINT user_configs_pkey +DO UPDATE +SET + value = @terminal_font +WHERE user_configs.user_id = @user_id + AND user_configs.key = 'terminal_font' +RETURNING *; + -- name: UpdateUserRoles :one UPDATE users diff --git a/coderd/users.go b/coderd/users.go index 069e1fc240302..03f900c01ddeb 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "slices" "github.com/go-chi/chi/v5" "github.com/go-chi/render" @@ -976,7 +977,7 @@ func (api *API) userAppearanceSettings(rw http.ResponseWriter, r *http.Request) user = httpmw.UserParam(r) ) - themePreference, err := api.Database.GetUserAppearanceSettings(ctx, user.ID) + themePreference, err := api.Database.GetUserThemePreference(ctx, user.ID) if err != nil { if !errors.Is(err, sql.ErrNoRows) { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -989,8 +990,22 @@ func (api *API) userAppearanceSettings(rw http.ResponseWriter, r *http.Request) themePreference = "" } + terminalFont, err := api.Database.GetUserTerminalFont(ctx, user.ID) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Error reading user settings.", + Detail: err.Error(), + }) + return + } + + terminalFont = "" + } + httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserAppearanceSettings{ ThemePreference: themePreference, + TerminalFont: codersdk.TerminalFontName(terminalFont), }) } @@ -1015,23 +1030,47 @@ func (api *API) putUserAppearanceSettings(rw http.ResponseWriter, r *http.Reques return } - updatedSettings, err := api.Database.UpdateUserAppearanceSettings(ctx, database.UpdateUserAppearanceSettingsParams{ + if !isValidFontName(params.TerminalFont) { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Unsupported font family.", + }) + return + } + + updatedThemePreference, err := api.Database.UpdateUserThemePreference(ctx, database.UpdateUserThemePreferenceParams{ UserID: user.ID, ThemePreference: params.ThemePreference, }) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error updating user.", + Message: "Internal error updating user theme preference.", + Detail: err.Error(), + }) + return + } + + updatedTerminalFont, err := api.Database.UpdateUserTerminalFont(ctx, database.UpdateUserTerminalFontParams{ + UserID: user.ID, + TerminalFont: string(params.TerminalFont), + }) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error updating user terminal font.", Detail: err.Error(), }) return } httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserAppearanceSettings{ - ThemePreference: updatedSettings.Value, + ThemePreference: updatedThemePreference.Value, + TerminalFont: codersdk.TerminalFontName(updatedTerminalFont.Value), }) } +func isValidFontName(font codersdk.TerminalFontName) bool { + return slices.Contains(codersdk.TerminalFontNames, font) +} + // @Summary Update user password // @ID update-user-password // @Security CoderSessionToken diff --git a/coderd/users_test.go b/coderd/users_test.go index c21eca85a5ee7..fdaad21a826a9 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -1972,6 +1972,86 @@ func TestPostTokens(t *testing.T) { require.NoError(t, err) } +func TestUserTerminalFont(t *testing.T) { + t.Parallel() + + t.Run("valid font", func(t *testing.T) { + t.Parallel() + + adminClient := coderdtest.New(t, nil) + firstUser := coderdtest.CreateFirstUser(t, adminClient) + client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // given + initial, err := client.GetUserAppearanceSettings(ctx, "me") + require.NoError(t, err) + require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont) + + // when + updated, err := client.UpdateUserAppearanceSettings(ctx, "me", codersdk.UpdateUserAppearanceSettingsRequest{ + ThemePreference: "light", + TerminalFont: "fira-code", + }) + require.NoError(t, err) + + // then + require.Equal(t, codersdk.TerminalFontFiraCode, updated.TerminalFont) + }) + + t.Run("unsupported font", func(t *testing.T) { + t.Parallel() + + adminClient := coderdtest.New(t, nil) + firstUser := coderdtest.CreateFirstUser(t, adminClient) + client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // given + initial, err := client.GetUserAppearanceSettings(ctx, "me") + require.NoError(t, err) + require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont) + + // when + _, err = client.UpdateUserAppearanceSettings(ctx, "me", codersdk.UpdateUserAppearanceSettingsRequest{ + ThemePreference: "light", + TerminalFont: "foobar", + }) + + // then + require.Error(t, err) + }) + + t.Run("undefined font is not ok", func(t *testing.T) { + t.Parallel() + + adminClient := coderdtest.New(t, nil) + firstUser := coderdtest.CreateFirstUser(t, adminClient) + client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // given + initial, err := client.GetUserAppearanceSettings(ctx, "me") + require.NoError(t, err) + require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont) + + // when + _, err = client.UpdateUserAppearanceSettings(ctx, "me", codersdk.UpdateUserAppearanceSettingsRequest{ + ThemePreference: "light", + TerminalFont: "", + }) + + // then + require.Error(t, err) + }) +} + func TestWorkspacesByUser(t *testing.T) { t.Parallel() t.Run("Empty", func(t *testing.T) { diff --git a/codersdk/users.go b/codersdk/users.go index 31854731a0ae1..bdc9b521367f0 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -189,12 +189,25 @@ type ValidateUserPasswordResponse struct { Details string `json:"details"` } +// TerminalFontName is the name of supported terminal font +type TerminalFontName string + +var TerminalFontNames = []TerminalFontName{TerminalFontUnknown, TerminalFontIBMPlexMono, TerminalFontFiraCode} + +const ( + TerminalFontUnknown TerminalFontName = "" + TerminalFontIBMPlexMono TerminalFontName = "ibm-plex-mono" + TerminalFontFiraCode TerminalFontName = "fira-code" +) + type UserAppearanceSettings struct { - ThemePreference string `json:"theme_preference"` + ThemePreference string `json:"theme_preference"` + TerminalFont TerminalFontName `json:"terminal_font"` } type UpdateUserAppearanceSettingsRequest struct { - ThemePreference string `json:"theme_preference" validate:"required"` + ThemePreference string `json:"theme_preference" validate:"required"` + TerminalFont TerminalFontName `json:"terminal_font" validate:"required"` } type UpdateUserPasswordRequest struct { @@ -466,17 +479,31 @@ func (c *Client) UpdateUserStatus(ctx context.Context, user string, status UserS return resp, json.NewDecoder(res.Body).Decode(&resp) } +// GetUserAppearanceSettings fetches the appearance settings for a user. +func (c *Client) GetUserAppearanceSettings(ctx context.Context, user string) (UserAppearanceSettings, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/appearance", user), nil) + if err != nil { + return UserAppearanceSettings{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return UserAppearanceSettings{}, ReadBodyAsError(res) + } + var resp UserAppearanceSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + // UpdateUserAppearanceSettings updates the appearance settings for a user. -func (c *Client) UpdateUserAppearanceSettings(ctx context.Context, user string, req UpdateUserAppearanceSettingsRequest) (User, error) { +func (c *Client) UpdateUserAppearanceSettings(ctx context.Context, user string, req UpdateUserAppearanceSettingsRequest) (UserAppearanceSettings, error) { res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/appearance", user), req) if err != nil { - return User{}, err + return UserAppearanceSettings{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return User{}, ReadBodyAsError(res) + return UserAppearanceSettings{}, ReadBodyAsError(res) } - var resp User + var resp UserAppearanceSettings return resp, json.NewDecoder(res.Body).Decode(&resp) } diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 4791967b53c9e..1359c2feec9fb 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -6712,6 +6712,22 @@ Restarts will only happen on weekdays in this list on weeks which line up with W |--------------------------| | `UNSUPPORTED_WORKSPACES` | +## codersdk.TerminalFontName + +```json +"" +``` + +### Properties + +#### Enumerated Values + +| Value | +|-----------------| +| `` | +| `ibm-plex-mono` | +| `fira-code` | + ## codersdk.TimingStage ```json @@ -6909,15 +6925,17 @@ Restarts will only happen on weekdays in this list on weeks which line up with W ```json { + "terminal_font": "", "theme_preference": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|--------------------|--------|----------|--------------|-------------| -| `theme_preference` | string | true | | | +| Name | Type | Required | Restrictions | Description | +|--------------------|--------------------------------------------------------|----------|--------------|-------------| +| `terminal_font` | [codersdk.TerminalFontName](#codersdkterminalfontname) | true | | | +| `theme_preference` | string | true | | | ## codersdk.UpdateUserNotificationPreferences @@ -7260,15 +7278,17 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { + "terminal_font": "", "theme_preference": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|--------------------|--------|----------|--------------|-------------| -| `theme_preference` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|--------------------|--------------------------------------------------------|----------|--------------|-------------| +| `terminal_font` | [codersdk.TerminalFontName](#codersdkterminalfontname) | false | | | +| `theme_preference` | string | false | | | ## codersdk.UserLatency diff --git a/docs/reference/api/users.md b/docs/reference/api/users.md index 3f0c38571f7c4..43842fde6539b 100644 --- a/docs/reference/api/users.md +++ b/docs/reference/api/users.md @@ -501,6 +501,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/appearance \ ```json { + "terminal_font": "", "theme_preference": "string" } ``` @@ -531,6 +532,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/appearance \ ```json { + "terminal_font": "", "theme_preference": "string" } ``` @@ -548,6 +550,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/appearance \ ```json { + "terminal_font": "", "theme_preference": "string" } ``` diff --git a/site/package.json b/site/package.json index d9fd1fbec2b05..bc40e094df28c 100644 --- a/site/package.json +++ b/site/package.json @@ -42,6 +42,7 @@ "@emotion/styled": "11.14.0", "@fastly/performance-observer-polyfill": "2.0.0", "@fontsource-variable/inter": "5.1.1", + "@fontsource/fira-code": "5.2.5", "@fontsource/ibm-plex-mono": "5.1.1", "@monaco-editor/react": "4.6.0", "@mui/icons-material": "5.16.14", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 55c4c955da4d9..3b7ff150e36ef 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -40,6 +40,9 @@ importers: '@fontsource-variable/inter': specifier: 5.1.1 version: 5.1.1 + '@fontsource/fira-code': + specifier: 5.2.5 + version: 5.2.5 '@fontsource/ibm-plex-mono': specifier: 5.1.1 version: 5.1.1 @@ -1034,6 +1037,9 @@ packages: '@fontsource-variable/inter@5.1.1': resolution: {integrity: sha512-OpXFTmiH6tHkYijMvQTycFKBLK4X+SRV6tet1m4YOUH7SzIIlMqDja+ocDtiCA72UthBH/vF+3ZtlMr2rN/wIw==, tarball: https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.1.1.tgz} + '@fontsource/fira-code@5.2.5': + resolution: {integrity: sha512-Rn9PJoyfRr5D6ukEhZpzhpD+rbX2rtoz9QjkOuGxqFxrL69fQvhadMUBxQIOuTF4sTTkPRSKlAEpPjTKaI12QA==, tarball: https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.2.5.tgz} + '@fontsource/ibm-plex-mono@5.1.1': resolution: {integrity: sha512-1aayqPe/ZkD3MlvqpmOHecfA3f2B8g+fAEkgvcCd3lkPP0pS1T0xG5Zmn2EsJQqr1JURtugPUH+5NqvKyfFZMQ==, tarball: https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.1.1.tgz} @@ -6954,6 +6960,8 @@ snapshots: '@fontsource-variable/inter@5.1.1': {} + '@fontsource/fira-code@5.2.5': {} + '@fontsource/ibm-plex-mono@5.1.1': {} '@humanwhocodes/config-array@0.11.14': diff --git a/site/site.go b/site/site.go index f4d5509479db5..e47e15848cda0 100644 --- a/site/site.go +++ b/site/site.go @@ -428,6 +428,7 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht var eg errgroup.Group var user database.User var themePreference string + var terminalFont string orgIDs := []uuid.UUID{} eg.Go(func() error { var err error @@ -436,13 +437,22 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht }) eg.Go(func() error { var err error - themePreference, err = h.opts.Database.GetUserAppearanceSettings(ctx, apiKey.UserID) + themePreference, err = h.opts.Database.GetUserThemePreference(ctx, apiKey.UserID) if errors.Is(err, sql.ErrNoRows) { themePreference = "" return nil } return err }) + eg.Go(func() error { + var err error + terminalFont, err = h.opts.Database.GetUserTerminalFont(ctx, apiKey.UserID) + if errors.Is(err, sql.ErrNoRows) { + terminalFont = "" + return nil + } + return err + }) eg.Go(func() error { memberIDs, err := h.opts.Database.GetOrganizationIDsByMemberIDs(ctx, []uuid.UUID{apiKey.UserID}) if errors.Is(err, sql.ErrNoRows) || len(memberIDs) == 0 { @@ -471,6 +481,7 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht defer wg.Done() userAppearance, err := json.Marshal(codersdk.UserAppearanceSettings{ ThemePreference: themePreference, + TerminalFont: codersdk.TerminalFontName(terminalFont), }) if err == nil { state.UserAppearance = html.EscapeString(string(userAppearance)) diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts index 5de828b6eac22..82b10213b4409 100644 --- a/site/src/api/queries/users.ts +++ b/site/src/api/queries/users.ts @@ -251,6 +251,7 @@ export const updateAppearanceSettings = ( // more responsive. queryClient.setQueryData(myAppearanceKey, { theme_preference: patch.theme_preference, + terminal_font: patch.terminal_font, }); }, onSuccess: async () => diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 2df1c351d9db1..e205e8938e999 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2655,6 +2655,15 @@ export interface TemplateVersionsByTemplateRequest extends Pagination { readonly include_archived: boolean; } +// From codersdk/users.go +export type TerminalFontName = "fira-code" | "ibm-plex-mono" | ""; + +export const TerminalFontNames: TerminalFontName[] = [ + "fira-code", + "ibm-plex-mono", + "", +]; + // From codersdk/workspacebuilds.go export type TimingStage = | "apply" @@ -2788,6 +2797,7 @@ export interface UpdateTemplateMeta { // From codersdk/users.go export interface UpdateUserAppearanceSettingsRequest { readonly theme_preference: string; + readonly terminal_font: TerminalFontName; } // From codersdk/notifications.go @@ -2904,6 +2914,7 @@ export interface UserActivityInsightsResponse { // From codersdk/users.go export interface UserAppearanceSettings { readonly theme_preference: string; + readonly terminal_font: TerminalFontName; } // From codersdk/insights.go diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index 4cf052668bb06..aa24485353894 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -17,6 +17,7 @@ import { MockEntitlements, MockExperiments, MockUser, + MockUserAppearanceSettings, MockWorkspace, MockWorkspaceAgent, } from "testHelpers/entities"; @@ -76,6 +77,7 @@ const meta = { key: getAuthorizationKey({ checks: permissionChecks }), data: { editWorkspaceProxies: true }, }, + { key: ["me", "appearance"], data: MockUserAppearanceSettings }, ], chromatic: { delay: 300 }, }, @@ -106,6 +108,38 @@ export const Starting: Story = { }, }; +export const FontFiraCode: Story = { + decorators: [withWebSocket], + parameters: { + ...meta.parameters, + webSocket: [ + { + event: "message", + // Copied and pasted this from browser + data: "➜ codergit:(bq/refactor-web-term-notifications) ✗", + }, + ], + queries: [ + ...meta.parameters.queries.filter( + (q) => + !( + Array.isArray(q.key) && + q.key[0] === "me" && + q.key[1] === "appearance" + ), + ), + { + key: ["me", "appearance"], + data: { + ...MockUserAppearanceSettings, + terminal_font: "fira-code", + }, + }, + createWorkspaceWithAgent("ready"), + ], + }, +}; + export const Ready: Story = { decorators: [withWebSocket], parameters: { diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index c86a3f9ed5396..9740e239233a4 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -7,18 +7,20 @@ import { WebLinksAddon } from "@xterm/addon-web-links"; import { WebglAddon } from "@xterm/addon-webgl"; import { Terminal } from "@xterm/xterm"; import { deploymentConfig } from "api/queries/deployment"; +import { appearanceSettings } from "api/queries/users"; import { workspaceByOwnerAndName, workspaceUsage, } from "api/queries/workspaces"; import { useProxy } from "contexts/ProxyContext"; import { ThemeOverride } from "contexts/ThemeProvider"; +import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import { type FC, useCallback, useEffect, useRef, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import themes from "theme"; -import { MONOSPACE_FONT_FAMILY } from "theme/constants"; +import { DEFAULT_TERMINAL_FONT, terminalFonts } from "theme/constants"; import { pageTitle } from "utils/page"; import { openMaybePortForwardedURL } from "utils/portForward"; import { terminalWebsocketUrl } from "utils/terminal"; @@ -100,6 +102,13 @@ const TerminalPage: FC = () => { handleWebLinkRef.current = handleWebLink; }, [handleWebLink]); + const { metadata } = useEmbeddedMetadata(); + const appearanceSettingsQuery = useQuery( + appearanceSettings(metadata.userAppearance), + ); + const currentTerminalFont = + appearanceSettingsQuery.data?.terminal_font || DEFAULT_TERMINAL_FONT; + // Create the terminal! const fitAddonRef = useRef(); useEffect(() => { @@ -110,7 +119,7 @@ const TerminalPage: FC = () => { allowProposedApi: true, allowTransparency: true, disableStdin: false, - fontFamily: MONOSPACE_FONT_FAMILY, + fontFamily: terminalFonts[currentTerminalFont], fontSize: 16, theme: { background: theme.palette.background.default, @@ -150,7 +159,12 @@ const TerminalPage: FC = () => { window.removeEventListener("resize", listener); terminal.dispose(); }; - }, [config.isLoading, renderer, theme.palette.background.default]); + }, [ + config.isLoading, + renderer, + theme.palette.background.default, + currentTerminalFont, + ]); // Updates the reconnection token into the URL if necessary. useEffect(() => { diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.stories.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.stories.tsx index 4f2c5965dc957..436e2e7e38c2d 100644 --- a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.stories.tsx +++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.stories.tsx @@ -18,6 +18,6 @@ type Story = StoryObj; export const Example: Story = { args: { - initialValues: { theme_preference: "" }, + initialValues: { theme_preference: "", terminal_font: "" }, }, }; diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx index 3468685a246cb..9ecee2dfac83a 100644 --- a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx +++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx @@ -1,12 +1,23 @@ import type { Interpolation } from "@emotion/react"; +import CircularProgress from "@mui/material/CircularProgress"; +import FormControl from "@mui/material/FormControl"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Radio from "@mui/material/Radio"; +import RadioGroup from "@mui/material/RadioGroup"; import { visuallyHidden } from "@mui/utils"; -import type { UpdateUserAppearanceSettingsRequest } from "api/typesGenerated"; +import { + type TerminalFontName, + TerminalFontNames, + type UpdateUserAppearanceSettingsRequest, +} from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { PreviewBadge } from "components/Badges/Badges"; import { Stack } from "components/Stack/Stack"; import { ThemeOverride } from "contexts/ThemeProvider"; import type { FC } from "react"; import themes, { DEFAULT_THEME, type Theme } from "theme"; +import { DEFAULT_TERMINAL_FONT, terminalFontLabels } from "theme/constants"; +import { Section } from "../Section"; export interface AppearanceFormProps { isUpdating?: boolean; @@ -22,43 +33,107 @@ export const AppearanceForm: FC = ({ initialValues, }) => { const currentTheme = initialValues.theme_preference || DEFAULT_THEME; + const currentTerminalFont = + initialValues.terminal_font || DEFAULT_TERMINAL_FONT; const onChangeTheme = async (theme: string) => { if (isUpdating) { return; } + await onSubmit({ + theme_preference: theme, + terminal_font: currentTerminalFont, + }); + }; - await onSubmit({ theme_preference: theme }); + const onChangeTerminalFont = async (terminalFont: TerminalFontName) => { + if (isUpdating) { + return; + } + await onSubmit({ + theme_preference: currentTheme, + terminal_font: terminalFont, + }); }; return (
{Boolean(error) && } - - onChangeTheme("auto")} - /> - onChangeTheme("dark")} - /> - onChangeTheme("light")} - /> - +
+ Theme + {isUpdating && } + + } + layout="fluid" + > + + onChangeTheme("auto")} + /> + onChangeTheme("dark")} + /> + onChangeTheme("light")} + /> + +
+
+
+ Terminal Font + {isUpdating && } + + } + layout="fluid" + > + + + onChangeTerminalFont(toTerminalFontName(value)) + } + > + {TerminalFontNames.filter((name) => name !== "").map((name) => ( + } + label={ +
+ {terminalFontLabels[name]} +
+ } + /> + ))} +
+
+
); }; +export function toTerminalFontName(value: string): TerminalFontName { + return TerminalFontNames.includes(value as TerminalFontName) + ? (value as TerminalFontName) + : ""; +} + interface AutoThemePreviewButtonProps extends Omit { themes: [Theme, Theme]; onSelect?: () => void; diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx index c48c265460a4e..59dc62980b9f0 100644 --- a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx +++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx @@ -12,13 +12,14 @@ describe("appearance page", () => { jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({ ...MockUser, theme_preference: "dark", + terminal_font: "fira-code", }); const dark = await screen.findByText("Dark"); await userEvent.click(dark); // Check if the API was called correctly - expect(API.updateAppearanceSettings).toBeCalledTimes(0); + expect(API.updateAppearanceSettings).toHaveBeenCalledTimes(0); }); it("changes theme to light", async () => { @@ -26,6 +27,7 @@ describe("appearance page", () => { jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({ ...MockUser, + terminal_font: "ibm-plex-mono", theme_preference: "light", }); @@ -33,9 +35,30 @@ describe("appearance page", () => { await userEvent.click(light); // Check if the API was called correctly - expect(API.updateAppearanceSettings).toBeCalledTimes(1); + expect(API.updateAppearanceSettings).toHaveBeenCalledTimes(1); expect(API.updateAppearanceSettings).toHaveBeenCalledWith({ + terminal_font: "ibm-plex-mono", theme_preference: "light", }); }); + + it("changes font to fira code", async () => { + renderWithAuth(); + + jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({ + ...MockUser, + terminal_font: "fira-code", + theme_preference: "dark", + }); + + const ibmPlex = await screen.findByText("Fira Code"); + await userEvent.click(ibmPlex); + + // Check if the API was called correctly + expect(API.updateAppearanceSettings).toHaveBeenCalledTimes(1); + expect(API.updateAppearanceSettings).toHaveBeenCalledWith({ + terminal_font: "fira-code", + theme_preference: "dark", + }); + }); }); diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.tsx index 1379e42d0e909..679ad6aeef3bd 100644 --- a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.tsx +++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.tsx @@ -1,13 +1,10 @@ -import CircularProgress from "@mui/material/CircularProgress"; import { updateAppearanceSettings } from "api/queries/users"; import { appearanceSettings } from "api/queries/users"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; -import { Stack } from "components/Stack/Stack"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import type { FC } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; -import { Section } from "../Section"; import { AppearanceForm } from "./AppearanceForm"; export const AppearancePage: FC = () => { @@ -31,26 +28,15 @@ export const AppearancePage: FC = () => { return ( <> -
- Theme - {updateAppearanceSettingsMutation.isLoading && ( - - )} - - } - layout="fluid" - > - -
+ ); }; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index a298dea4ffd9d..02b08f03c5376 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -536,6 +536,7 @@ export const SuspendedMockUser: TypesGen.User = { export const MockUserAppearanceSettings: TypesGen.UserAppearanceSettings = { theme_preference: "dark", + terminal_font: "", }; export const MockOrganizationMember: TypesGen.OrganizationMemberWithUserData = { diff --git a/site/src/theme/constants.ts b/site/src/theme/constants.ts index b95998640efde..162e67310749c 100644 --- a/site/src/theme/constants.ts +++ b/site/src/theme/constants.ts @@ -1,7 +1,23 @@ +import type { TerminalFontName } from "api/typesGenerated"; + export const borderRadius = 8; export const MONOSPACE_FONT_FAMILY = "'IBM Plex Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'Liberation Mono', 'Monaco', 'Courier New', Courier, monospace"; export const BODY_FONT_FAMILY = `"Inter Variable", system-ui, sans-serif`; + +export const terminalFonts: Record = { + "fira-code": MONOSPACE_FONT_FAMILY.replace("IBM Plex Mono", "Fira Code"), + "ibm-plex-mono": MONOSPACE_FONT_FAMILY, + + "": MONOSPACE_FONT_FAMILY, +}; +export const terminalFontLabels: Record = { + "fira-code": "Fira Code", + "ibm-plex-mono": "IBM Plex Mono", + "": "", // needed for enum completeness, otherwise fails the build +}; +export const DEFAULT_TERMINAL_FONT = "ibm-plex-mono"; + export const navHeight = 62; export const containerWidth = 1380; export const containerWidthMedium = 1080; diff --git a/site/src/theme/globalFonts.ts b/site/src/theme/globalFonts.ts index 24371dd57568e..db8089f9db266 100644 --- a/site/src/theme/globalFonts.ts +++ b/site/src/theme/globalFonts.ts @@ -3,3 +3,6 @@ import "@fontsource/ibm-plex-mono/400.css"; import "@fontsource/ibm-plex-mono/600.css"; // Main body copy font import "@fontsource-variable/inter"; +// Alternative font for Terminal +import "@fontsource/fira-code/400.css"; +import "@fontsource/fira-code/600.css"; 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