mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: add "user_configs" db table (#16564)
This commit is contained in:
@@ -10,7 +10,6 @@
|
||||
"last_seen_at": "====[timestamp]=====",
|
||||
"status": "active",
|
||||
"login_type": "password",
|
||||
"theme_preference": "",
|
||||
"organization_ids": [
|
||||
"===========[first org ID]==========="
|
||||
],
|
||||
@@ -32,7 +31,6 @@
|
||||
"last_seen_at": "====[timestamp]=====",
|
||||
"status": "dormant",
|
||||
"login_type": "password",
|
||||
"theme_preference": "",
|
||||
"organization_ids": [
|
||||
"===========[first org ID]==========="
|
||||
],
|
||||
|
||||
Generated
+44
-1
@@ -6395,6 +6395,38 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"/users/{user}/appearance": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Get user appearance settings",
|
||||
"operationId": "get-user-appearance-settings",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID, name, or me",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UserAppearanceSettings"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
@@ -6434,7 +6466,7 @@ const docTemplate = `{
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.User"
|
||||
"$ref": "#/definitions/codersdk.UserAppearanceSettings"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13857,6 +13889,7 @@ const docTemplate = `{
|
||||
]
|
||||
},
|
||||
"theme_preference": {
|
||||
"description": "Deprecated: this value should be retrieved from\n` + "`" + `codersdk.UserPreferenceSettings` + "`" + ` instead.",
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
@@ -14724,6 +14757,7 @@ const docTemplate = `{
|
||||
]
|
||||
},
|
||||
"theme_preference": {
|
||||
"description": "Deprecated: this value should be retrieved from\n` + "`" + `codersdk.UserPreferenceSettings` + "`" + ` instead.",
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
@@ -15334,6 +15368,7 @@ const docTemplate = `{
|
||||
]
|
||||
},
|
||||
"theme_preference": {
|
||||
"description": "Deprecated: this value should be retrieved from\n` + "`" + `codersdk.UserPreferenceSettings` + "`" + ` instead.",
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
@@ -15406,6 +15441,14 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UserAppearanceSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"theme_preference": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UserLatency": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+40
-1
@@ -5647,6 +5647,34 @@
|
||||
}
|
||||
},
|
||||
"/users/{user}/appearance": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Users"],
|
||||
"summary": "Get user appearance settings",
|
||||
"operationId": "get-user-appearance-settings",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID, name, or me",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UserAppearanceSettings"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
@@ -5680,7 +5708,7 @@
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.User"
|
||||
"$ref": "#/definitions/codersdk.UserAppearanceSettings"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12538,6 +12566,7 @@
|
||||
]
|
||||
},
|
||||
"theme_preference": {
|
||||
"description": "Deprecated: this value should be retrieved from\n`codersdk.UserPreferenceSettings` instead.",
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
@@ -13380,6 +13409,7 @@
|
||||
]
|
||||
},
|
||||
"theme_preference": {
|
||||
"description": "Deprecated: this value should be retrieved from\n`codersdk.UserPreferenceSettings` instead.",
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
@@ -13942,6 +13972,7 @@
|
||||
]
|
||||
},
|
||||
"theme_preference": {
|
||||
"description": "Deprecated: this value should be retrieved from\n`codersdk.UserPreferenceSettings` instead.",
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
@@ -14014,6 +14045,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UserAppearanceSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"theme_preference": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UserLatency": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -204,7 +204,6 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
|
||||
Deleted: dblog.UserDeleted.Bool,
|
||||
LastSeenAt: dblog.UserLastSeenAt.Time,
|
||||
QuietHoursSchedule: dblog.UserQuietHoursSchedule.String,
|
||||
ThemePreference: dblog.UserThemePreference.String,
|
||||
Name: dblog.UserName.String,
|
||||
}, []uuid.UUID{})
|
||||
user = &sdkUser
|
||||
|
||||
@@ -1145,6 +1145,7 @@ func New(options *Options) *API {
|
||||
r.Put("/suspend", api.putSuspendUserAccount())
|
||||
r.Put("/activate", api.putActivateUserAccount())
|
||||
})
|
||||
r.Get("/appearance", api.userAppearanceSettings)
|
||||
r.Put("/appearance", api.putUserAppearanceSettings)
|
||||
r.Route("/password", func(r chi.Router) {
|
||||
r.Use(httpmw.RateLimit(options.LoginRateLimit, time.Minute))
|
||||
|
||||
@@ -150,14 +150,13 @@ func ReducedUser(user database.User) codersdk.ReducedUser {
|
||||
Username: user.Username,
|
||||
AvatarURL: user.AvatarURL,
|
||||
},
|
||||
Email: user.Email,
|
||||
Name: user.Name,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
LastSeenAt: user.LastSeenAt,
|
||||
Status: codersdk.UserStatus(user.Status),
|
||||
LoginType: codersdk.LoginType(user.LoginType),
|
||||
ThemePreference: user.ThemePreference,
|
||||
Email: user.Email,
|
||||
Name: user.Name,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
LastSeenAt: user.LastSeenAt,
|
||||
Status: codersdk.UserStatus(user.Status),
|
||||
LoginType: codersdk.LoginType(user.LoginType),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +175,6 @@ func UserFromGroupMember(member database.GroupMember) database.User {
|
||||
Deleted: member.UserDeleted,
|
||||
LastSeenAt: member.UserLastSeenAt,
|
||||
QuietHoursSchedule: member.UserQuietHoursSchedule,
|
||||
ThemePreference: member.UserThemePreference,
|
||||
Name: member.UserName,
|
||||
GithubComUserID: member.UserGithubComUserID,
|
||||
}
|
||||
|
||||
@@ -2510,6 +2510,17 @@ 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)
|
||||
}
|
||||
@@ -4021,13 +4032,13 @@ 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.User, error) {
|
||||
u, err := q.db.GetUserByID(ctx, arg.ID)
|
||||
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.User{}, err
|
||||
return database.UserConfig{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil {
|
||||
return database.User{}, err
|
||||
return database.UserConfig{}, err
|
||||
}
|
||||
return q.db.UpdateUserAppearanceSettings(ctx, arg)
|
||||
}
|
||||
|
||||
@@ -1522,13 +1522,26 @@ func (s *MethodTestSuite) TestUser() {
|
||||
[]database.GetUserWorkspaceBuildParametersRow{},
|
||||
)
|
||||
}))
|
||||
s.Run("GetUserAppearanceSettings", s.Subtest(func(db database.Store, check *expects) {
|
||||
ctx := context.Background()
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
db.UpdateUserAppearanceSettings(ctx, database.UpdateUserAppearanceSettingsParams{
|
||||
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) {
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
uc := database.UserConfig{
|
||||
UserID: u.ID,
|
||||
Key: "theme_preference",
|
||||
Value: "dark",
|
||||
}
|
||||
check.Args(database.UpdateUserAppearanceSettingsParams{
|
||||
ID: u.ID,
|
||||
ThemePreference: u.ThemePreference,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
}).Asserts(u, policy.ActionUpdatePersonal).Returns(u)
|
||||
UserID: u.ID,
|
||||
ThemePreference: 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{})
|
||||
|
||||
@@ -528,7 +528,6 @@ func GroupMember(t testing.TB, db database.Store, member database.GroupMemberTab
|
||||
UserDeleted: user.Deleted,
|
||||
UserLastSeenAt: user.LastSeenAt,
|
||||
UserQuietHoursSchedule: user.QuietHoursSchedule,
|
||||
UserThemePreference: user.ThemePreference,
|
||||
UserName: user.Name,
|
||||
UserGithubComUserID: user.GithubComUserID,
|
||||
OrganizationID: group.OrganizationID,
|
||||
|
||||
@@ -55,44 +55,45 @@ func New() database.Store {
|
||||
mutex: &sync.RWMutex{},
|
||||
data: &data{
|
||||
apiKeys: make([]database.APIKey, 0),
|
||||
organizationMembers: make([]database.OrganizationMember, 0),
|
||||
organizations: make([]database.Organization, 0),
|
||||
users: make([]database.User, 0),
|
||||
auditLogs: make([]database.AuditLog, 0),
|
||||
customRoles: make([]database.CustomRole, 0),
|
||||
dbcryptKeys: make([]database.DBCryptKey, 0),
|
||||
externalAuthLinks: make([]database.ExternalAuthLink, 0),
|
||||
groups: make([]database.Group, 0),
|
||||
groupMembers: make([]database.GroupMemberTable, 0),
|
||||
auditLogs: make([]database.AuditLog, 0),
|
||||
files: make([]database.File, 0),
|
||||
gitSSHKey: make([]database.GitSSHKey, 0),
|
||||
groups: make([]database.Group, 0),
|
||||
groupMembers: make([]database.GroupMemberTable, 0),
|
||||
licenses: make([]database.License, 0),
|
||||
locks: map[int64]struct{}{},
|
||||
notificationMessages: make([]database.NotificationMessage, 0),
|
||||
notificationPreferences: make([]database.NotificationPreference, 0),
|
||||
InboxNotification: make([]database.InboxNotification, 0),
|
||||
organizationMembers: make([]database.OrganizationMember, 0),
|
||||
organizations: make([]database.Organization, 0),
|
||||
inboxNotifications: make([]database.InboxNotification, 0),
|
||||
parameterSchemas: make([]database.ParameterSchema, 0),
|
||||
presets: make([]database.TemplateVersionPreset, 0),
|
||||
presetParameters: make([]database.TemplateVersionPresetParameter, 0),
|
||||
provisionerDaemons: make([]database.ProvisionerDaemon, 0),
|
||||
provisionerKeys: make([]database.ProvisionerKey, 0),
|
||||
workspaceAgents: make([]database.WorkspaceAgent, 0),
|
||||
provisionerJobs: make([]database.ProvisionerJob, 0),
|
||||
provisionerJobLogs: make([]database.ProvisionerJobLog, 0),
|
||||
provisionerKeys: make([]database.ProvisionerKey, 0),
|
||||
runtimeConfig: map[string]string{},
|
||||
telemetryItems: make([]database.TelemetryItem, 0),
|
||||
templateVersions: make([]database.TemplateVersionTable, 0),
|
||||
templates: make([]database.TemplateTable, 0),
|
||||
users: make([]database.User, 0),
|
||||
userConfigs: make([]database.UserConfig, 0),
|
||||
userStatusChanges: make([]database.UserStatusChange, 0),
|
||||
workspaceAgents: make([]database.WorkspaceAgent, 0),
|
||||
workspaceResources: make([]database.WorkspaceResource, 0),
|
||||
workspaceModules: make([]database.WorkspaceModule, 0),
|
||||
workspaceResourceMetadata: make([]database.WorkspaceResourceMetadatum, 0),
|
||||
provisionerJobs: make([]database.ProvisionerJob, 0),
|
||||
templateVersions: make([]database.TemplateVersionTable, 0),
|
||||
templates: make([]database.TemplateTable, 0),
|
||||
workspaceAgentStats: make([]database.WorkspaceAgentStat, 0),
|
||||
workspaceAgentLogs: make([]database.WorkspaceAgentLog, 0),
|
||||
workspaceBuilds: make([]database.WorkspaceBuild, 0),
|
||||
workspaceApps: make([]database.WorkspaceApp, 0),
|
||||
workspaces: make([]database.WorkspaceTable, 0),
|
||||
licenses: make([]database.License, 0),
|
||||
workspaceProxies: make([]database.WorkspaceProxy, 0),
|
||||
customRoles: make([]database.CustomRole, 0),
|
||||
locks: map[int64]struct{}{},
|
||||
runtimeConfig: map[string]string{},
|
||||
userStatusChanges: make([]database.UserStatusChange, 0),
|
||||
telemetryItems: make([]database.TelemetryItem, 0),
|
||||
presets: make([]database.TemplateVersionPreset, 0),
|
||||
presetParameters: make([]database.TemplateVersionPresetParameter, 0),
|
||||
},
|
||||
}
|
||||
// Always start with a default org. Matching migration 198.
|
||||
@@ -207,7 +208,7 @@ type data struct {
|
||||
notificationMessages []database.NotificationMessage
|
||||
notificationPreferences []database.NotificationPreference
|
||||
notificationReportGeneratorLogs []database.NotificationReportGeneratorLog
|
||||
InboxNotification []database.InboxNotification
|
||||
inboxNotifications []database.InboxNotification
|
||||
oauth2ProviderApps []database.OAuth2ProviderApp
|
||||
oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret
|
||||
oauth2ProviderAppCodes []database.OAuth2ProviderAppCode
|
||||
@@ -224,6 +225,7 @@ type data struct {
|
||||
templateVersionWorkspaceTags []database.TemplateVersionWorkspaceTag
|
||||
templates []database.TemplateTable
|
||||
templateUsageStats []database.TemplateUsageStat
|
||||
userConfigs []database.UserConfig
|
||||
workspaceAgents []database.WorkspaceAgent
|
||||
workspaceAgentMetadata []database.WorkspaceAgentMetadatum
|
||||
workspaceAgentLogs []database.WorkspaceAgentLog
|
||||
@@ -899,7 +901,6 @@ func (q *FakeQuerier) getGroupMemberNoLock(ctx context.Context, userID, groupID
|
||||
UserDeleted: user.Deleted,
|
||||
UserLastSeenAt: user.LastSeenAt,
|
||||
UserQuietHoursSchedule: user.QuietHoursSchedule,
|
||||
UserThemePreference: user.ThemePreference,
|
||||
UserName: user.Name,
|
||||
UserGithubComUserID: user.GithubComUserID,
|
||||
OrganizationID: orgID,
|
||||
@@ -1725,7 +1726,7 @@ func (q *FakeQuerier) CountUnreadInboxNotificationsByUserID(_ context.Context, u
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
var count int64
|
||||
for _, notification := range q.InboxNotification {
|
||||
for _, notification := range q.inboxNotifications {
|
||||
if notification.UserID != userID {
|
||||
continue
|
||||
}
|
||||
@@ -3295,7 +3296,7 @@ func (q *FakeQuerier) GetFilteredInboxNotificationsByUserID(_ context.Context, a
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
notifications := make([]database.InboxNotification, 0)
|
||||
for _, notification := range q.InboxNotification {
|
||||
for _, notification := range q.inboxNotifications {
|
||||
if notification.UserID == arg.UserID {
|
||||
for _, template := range arg.Templates {
|
||||
templateFound := false
|
||||
@@ -3531,7 +3532,7 @@ func (q *FakeQuerier) GetInboxNotificationByID(_ context.Context, id uuid.UUID)
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, notification := range q.InboxNotification {
|
||||
for _, notification := range q.inboxNotifications {
|
||||
if notification.ID == id {
|
||||
return notification, nil
|
||||
}
|
||||
@@ -3545,7 +3546,7 @@ func (q *FakeQuerier) GetInboxNotificationsByUserID(_ context.Context, params da
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
notifications := make([]database.InboxNotification, 0)
|
||||
for _, notification := range q.InboxNotification {
|
||||
for _, notification := range q.inboxNotifications {
|
||||
if notification.UserID == params.UserID {
|
||||
notifications = append(notifications, notification)
|
||||
}
|
||||
@@ -6162,6 +6163,20 @@ 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
|
||||
@@ -8211,7 +8226,7 @@ func (q *FakeQuerier) InsertInboxNotification(_ context.Context, arg database.In
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
q.InboxNotification = append(q.InboxNotification, notification)
|
||||
q.inboxNotifications = append(q.inboxNotifications, notification)
|
||||
return notification, nil
|
||||
}
|
||||
|
||||
@@ -9938,9 +9953,9 @@ func (q *FakeQuerier) UpdateInboxNotificationReadStatus(_ context.Context, arg d
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for i := range q.InboxNotification {
|
||||
if q.InboxNotification[i].ID == arg.ID {
|
||||
q.InboxNotification[i].ReadAt = arg.ReadAt
|
||||
for i := range q.inboxNotifications {
|
||||
if q.inboxNotifications[i].ID == arg.ID {
|
||||
q.inboxNotifications[i].ReadAt = arg.ReadAt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10454,24 +10469,31 @@ func (q *FakeQuerier) UpdateTemplateWorkspacesLastUsedAt(_ context.Context, arg
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateUserAppearanceSettings(_ context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.User, error) {
|
||||
func (q *FakeQuerier) UpdateUserAppearanceSettings(_ context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.UserConfig, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return database.User{}, err
|
||||
return database.UserConfig{}, err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, user := range q.users {
|
||||
if user.ID != arg.ID {
|
||||
for i, uc := range q.userConfigs {
|
||||
if uc.UserID != arg.UserID || uc.Key != "theme_preference" {
|
||||
continue
|
||||
}
|
||||
user.ThemePreference = arg.ThemePreference
|
||||
q.users[index] = user
|
||||
return user, nil
|
||||
uc.Value = arg.ThemePreference
|
||||
q.userConfigs[i] = uc
|
||||
return uc, nil
|
||||
}
|
||||
return database.User{}, sql.ErrNoRows
|
||||
|
||||
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 {
|
||||
@@ -12862,7 +12884,6 @@ func (q *FakeQuerier) GetAuthorizedAuditLogsOffset(ctx context.Context, arg data
|
||||
UserLastSeenAt: sql.NullTime{Time: user.LastSeenAt, Valid: userValid},
|
||||
UserLoginType: database.NullLoginType{LoginType: user.LoginType, Valid: userValid},
|
||||
UserDeleted: sql.NullBool{Bool: user.Deleted, Valid: userValid},
|
||||
UserThemePreference: sql.NullString{String: user.ThemePreference, Valid: userValid},
|
||||
UserQuietHoursSchedule: sql.NullString{String: user.QuietHoursSchedule, Valid: userValid},
|
||||
UserStatus: database.NullUserStatus{UserStatus: user.Status, Valid: userValid},
|
||||
UserRoles: user.RBACRoles,
|
||||
|
||||
@@ -1403,6 +1403,13 @@ 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)
|
||||
@@ -2551,7 +2558,7 @@ func (m queryMetricsStore) UpdateTemplateWorkspacesLastUsedAt(ctx context.Contex
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateUserAppearanceSettings(ctx context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.User, error) {
|
||||
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())
|
||||
|
||||
@@ -2932,6 +2932,21 @@ 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()
|
||||
@@ -5399,10 +5414,10 @@ func (mr *MockStoreMockRecorder) UpdateTemplateWorkspacesLastUsedAt(ctx, arg any
|
||||
}
|
||||
|
||||
// UpdateUserAppearanceSettings mocks base method.
|
||||
func (m *MockStore) UpdateUserAppearanceSettings(ctx context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.User, error) {
|
||||
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.User)
|
||||
ret0, _ := ret[0].(database.UserConfig)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
Generated
+12
-4
@@ -849,7 +849,6 @@ CREATE TABLE users (
|
||||
deleted boolean DEFAULT false NOT NULL,
|
||||
last_seen_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL,
|
||||
quiet_hours_schedule text DEFAULT ''::text NOT NULL,
|
||||
theme_preference text DEFAULT ''::text NOT NULL,
|
||||
name text DEFAULT ''::text NOT NULL,
|
||||
github_com_user_id bigint,
|
||||
hashed_one_time_passcode bytea,
|
||||
@@ -859,8 +858,6 @@ CREATE TABLE users (
|
||||
|
||||
COMMENT ON COLUMN users.quiet_hours_schedule IS 'Daily (!) cron schedule (with optional CRON_TZ) signifying the start of the user''s quiet hours. If empty, the default quiet hours on the instance is used instead.';
|
||||
|
||||
COMMENT ON COLUMN users.theme_preference IS '"" can be interpreted as "the user does not care", falling back to the default theme';
|
||||
|
||||
COMMENT ON COLUMN users.name IS 'Name of the Coder user';
|
||||
|
||||
COMMENT ON COLUMN users.github_com_user_id IS 'The GitHub.com numerical user ID. At time of implementation, this is used to check if the user has starred the Coder repository.';
|
||||
@@ -892,7 +889,6 @@ CREATE VIEW group_members_expanded AS
|
||||
users.deleted AS user_deleted,
|
||||
users.last_seen_at AS user_last_seen_at,
|
||||
users.quiet_hours_schedule AS user_quiet_hours_schedule,
|
||||
users.theme_preference AS user_theme_preference,
|
||||
users.name AS user_name,
|
||||
users.github_com_user_id AS user_github_com_user_id,
|
||||
groups.organization_id,
|
||||
@@ -1547,6 +1543,12 @@ CREATE VIEW template_with_names AS
|
||||
|
||||
COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.';
|
||||
|
||||
CREATE TABLE user_configs (
|
||||
user_id uuid NOT NULL,
|
||||
key character varying(256) NOT NULL,
|
||||
value text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE user_deleted (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
@@ -2199,6 +2201,9 @@ ALTER TABLE ONLY template_versions
|
||||
ALTER TABLE ONLY templates
|
||||
ADD CONSTRAINT templates_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY user_configs
|
||||
ADD CONSTRAINT user_configs_pkey PRIMARY KEY (user_id, key);
|
||||
|
||||
ALTER TABLE ONLY user_deleted
|
||||
ADD CONSTRAINT user_deleted_pkey PRIMARY KEY (id);
|
||||
|
||||
@@ -2613,6 +2618,9 @@ ALTER TABLE ONLY templates
|
||||
ALTER TABLE ONLY templates
|
||||
ADD CONSTRAINT templates_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY user_configs
|
||||
ADD CONSTRAINT user_configs_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY user_deleted
|
||||
ADD CONSTRAINT user_deleted_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ const (
|
||||
ForeignKeyTemplateVersionsTemplateID ForeignKeyConstraint = "template_versions_template_id_fkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_fkey FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE CASCADE;
|
||||
ForeignKeyTemplatesCreatedBy ForeignKeyConstraint = "templates_created_by_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT;
|
||||
ForeignKeyTemplatesOrganizationID ForeignKeyConstraint = "templates_organization_id_fkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
ForeignKeyUserConfigsUserID ForeignKeyConstraint = "user_configs_user_id_fkey" // ALTER TABLE ONLY user_configs ADD CONSTRAINT user_configs_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyUserDeletedUserID ForeignKeyConstraint = "user_deleted_user_id_fkey" // ALTER TABLE ONLY user_deleted ADD CONSTRAINT user_deleted_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
||||
ForeignKeyUserLinksOauthAccessTokenKeyID ForeignKeyConstraint = "user_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
ForeignKeyUserLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "user_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
-- Put back "theme_preference" column
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS
|
||||
theme_preference text DEFAULT ''::text NOT NULL;
|
||||
|
||||
-- Copy "theme_preference" back to "users"
|
||||
UPDATE users
|
||||
SET theme_preference = (SELECT value
|
||||
FROM user_configs
|
||||
WHERE user_configs.user_id = users.id
|
||||
AND user_configs.key = 'theme_preference');
|
||||
|
||||
-- Drop the "user_configs" table.
|
||||
DROP TABLE user_configs;
|
||||
|
||||
-- Replace "group_members_expanded", and bring back with "theme_preference"
|
||||
DROP VIEW group_members_expanded;
|
||||
-- Taken from 000242_group_members_view.up.sql
|
||||
CREATE VIEW
|
||||
group_members_expanded
|
||||
AS
|
||||
-- If the group is a user made group, then we need to check the group_members table.
|
||||
-- If it is the "Everyone" group, then we need to check the organization_members table.
|
||||
WITH all_members AS (
|
||||
SELECT user_id, group_id FROM group_members
|
||||
UNION
|
||||
SELECT user_id, organization_id AS group_id FROM organization_members
|
||||
)
|
||||
SELECT
|
||||
users.id AS user_id,
|
||||
users.email AS user_email,
|
||||
users.username AS user_username,
|
||||
users.hashed_password AS user_hashed_password,
|
||||
users.created_at AS user_created_at,
|
||||
users.updated_at AS user_updated_at,
|
||||
users.status AS user_status,
|
||||
users.rbac_roles AS user_rbac_roles,
|
||||
users.login_type AS user_login_type,
|
||||
users.avatar_url AS user_avatar_url,
|
||||
users.deleted AS user_deleted,
|
||||
users.last_seen_at AS user_last_seen_at,
|
||||
users.quiet_hours_schedule AS user_quiet_hours_schedule,
|
||||
users.theme_preference AS user_theme_preference,
|
||||
users.name AS user_name,
|
||||
users.github_com_user_id AS user_github_com_user_id,
|
||||
groups.organization_id AS organization_id,
|
||||
groups.name AS group_name,
|
||||
all_members.group_id AS group_id
|
||||
FROM
|
||||
all_members
|
||||
JOIN
|
||||
users ON users.id = all_members.user_id
|
||||
JOIN
|
||||
groups ON groups.id = all_members.group_id
|
||||
WHERE
|
||||
users.deleted = 'false';
|
||||
|
||||
COMMENT ON VIEW group_members_expanded IS 'Joins group members with user information, organization ID, group name. Includes both regular group members and organization members (as part of the "Everyone" group).';
|
||||
@@ -0,0 +1,62 @@
|
||||
CREATE TABLE IF NOT EXISTS user_configs (
|
||||
user_id uuid NOT NULL,
|
||||
key varchar(256) NOT NULL,
|
||||
value text NOT NULL,
|
||||
|
||||
PRIMARY KEY (user_id, key),
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
-- Copy "theme_preference" from "users" table
|
||||
INSERT INTO user_configs (user_id, key, value)
|
||||
SELECT id, 'theme_preference', theme_preference
|
||||
FROM users
|
||||
WHERE users.theme_preference IS NOT NULL;
|
||||
|
||||
|
||||
-- Replace "group_members_expanded" without "theme_preference"
|
||||
DROP VIEW group_members_expanded;
|
||||
-- Taken from 000242_group_members_view.up.sql
|
||||
CREATE VIEW
|
||||
group_members_expanded
|
||||
AS
|
||||
-- If the group is a user made group, then we need to check the group_members table.
|
||||
-- If it is the "Everyone" group, then we need to check the organization_members table.
|
||||
WITH all_members AS (
|
||||
SELECT user_id, group_id FROM group_members
|
||||
UNION
|
||||
SELECT user_id, organization_id AS group_id FROM organization_members
|
||||
)
|
||||
SELECT
|
||||
users.id AS user_id,
|
||||
users.email AS user_email,
|
||||
users.username AS user_username,
|
||||
users.hashed_password AS user_hashed_password,
|
||||
users.created_at AS user_created_at,
|
||||
users.updated_at AS user_updated_at,
|
||||
users.status AS user_status,
|
||||
users.rbac_roles AS user_rbac_roles,
|
||||
users.login_type AS user_login_type,
|
||||
users.avatar_url AS user_avatar_url,
|
||||
users.deleted AS user_deleted,
|
||||
users.last_seen_at AS user_last_seen_at,
|
||||
users.quiet_hours_schedule AS user_quiet_hours_schedule,
|
||||
users.name AS user_name,
|
||||
users.github_com_user_id AS user_github_com_user_id,
|
||||
groups.organization_id AS organization_id,
|
||||
groups.name AS group_name,
|
||||
all_members.group_id AS group_id
|
||||
FROM
|
||||
all_members
|
||||
JOIN
|
||||
users ON users.id = all_members.user_id
|
||||
JOIN
|
||||
groups ON groups.id = all_members.group_id
|
||||
WHERE
|
||||
users.deleted = 'false';
|
||||
|
||||
COMMENT ON VIEW group_members_expanded IS 'Joins group members with user information, organization ID, group name. Includes both regular group members and organization members (as part of the "Everyone" group).';
|
||||
|
||||
-- Drop the "theme_preference" column now that the view no longer depends on it.
|
||||
ALTER TABLE users DROP COLUMN theme_preference;
|
||||
@@ -406,20 +406,19 @@ func ConvertUserRows(rows []GetUsersRow) []User {
|
||||
users := make([]User, len(rows))
|
||||
for i, r := range rows {
|
||||
users[i] = User{
|
||||
ID: r.ID,
|
||||
Email: r.Email,
|
||||
Username: r.Username,
|
||||
Name: r.Name,
|
||||
HashedPassword: r.HashedPassword,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
Status: r.Status,
|
||||
RBACRoles: r.RBACRoles,
|
||||
LoginType: r.LoginType,
|
||||
AvatarURL: r.AvatarURL,
|
||||
Deleted: r.Deleted,
|
||||
LastSeenAt: r.LastSeenAt,
|
||||
ThemePreference: r.ThemePreference,
|
||||
ID: r.ID,
|
||||
Email: r.Email,
|
||||
Username: r.Username,
|
||||
Name: r.Name,
|
||||
HashedPassword: r.HashedPassword,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
Status: r.Status,
|
||||
RBACRoles: r.RBACRoles,
|
||||
LoginType: r.LoginType,
|
||||
AvatarURL: r.AvatarURL,
|
||||
Deleted: r.Deleted,
|
||||
LastSeenAt: r.LastSeenAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -417,7 +417,6 @@ func (q *sqlQuerier) GetAuthorizedUsers(ctx context.Context, arg GetUsersParams,
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.QuietHoursSchedule,
|
||||
&i.ThemePreference,
|
||||
&i.Name,
|
||||
&i.GithubComUserID,
|
||||
&i.HashedOneTimePasscode,
|
||||
@@ -505,7 +504,6 @@ func (q *sqlQuerier) GetAuthorizedAuditLogsOffset(ctx context.Context, arg GetAu
|
||||
&i.UserRoles,
|
||||
&i.UserAvatarUrl,
|
||||
&i.UserDeleted,
|
||||
&i.UserThemePreference,
|
||||
&i.UserQuietHoursSchedule,
|
||||
&i.OrganizationName,
|
||||
&i.OrganizationDisplayName,
|
||||
|
||||
@@ -2605,7 +2605,6 @@ type GroupMember struct {
|
||||
UserDeleted bool `db:"user_deleted" json:"user_deleted"`
|
||||
UserLastSeenAt time.Time `db:"user_last_seen_at" json:"user_last_seen_at"`
|
||||
UserQuietHoursSchedule string `db:"user_quiet_hours_schedule" json:"user_quiet_hours_schedule"`
|
||||
UserThemePreference string `db:"user_theme_preference" json:"user_theme_preference"`
|
||||
UserName string `db:"user_name" json:"user_name"`
|
||||
UserGithubComUserID sql.NullInt64 `db:"user_github_com_user_id" json:"user_github_com_user_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
@@ -3176,8 +3175,6 @@ type User struct {
|
||||
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
|
||||
// Daily (!) cron schedule (with optional CRON_TZ) signifying the start of the user's quiet hours. If empty, the default quiet hours on the instance is used instead.
|
||||
QuietHoursSchedule string `db:"quiet_hours_schedule" json:"quiet_hours_schedule"`
|
||||
// "" can be interpreted as "the user does not care", falling back to the default theme
|
||||
ThemePreference string `db:"theme_preference" json:"theme_preference"`
|
||||
// Name of the Coder user
|
||||
Name string `db:"name" json:"name"`
|
||||
// The GitHub.com numerical user ID. At time of implementation, this is used to check if the user has starred the Coder repository.
|
||||
@@ -3188,6 +3185,12 @@ type User struct {
|
||||
OneTimePasscodeExpiresAt sql.NullTime `db:"one_time_passcode_expires_at" json:"one_time_passcode_expires_at"`
|
||||
}
|
||||
|
||||
type UserConfig struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
Key string `db:"key" json:"key"`
|
||||
Value string `db:"value" json:"value"`
|
||||
}
|
||||
|
||||
// Tracks when users were deleted
|
||||
type UserDeleted struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
|
||||
@@ -306,6 +306,7 @@ 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) (int64, error)
|
||||
@@ -522,7 +523,7 @@ 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) (User, 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
|
||||
|
||||
@@ -457,7 +457,6 @@ SELECT
|
||||
users.rbac_roles AS user_roles,
|
||||
users.avatar_url AS user_avatar_url,
|
||||
users.deleted AS user_deleted,
|
||||
users.theme_preference AS user_theme_preference,
|
||||
users.quiet_hours_schedule AS user_quiet_hours_schedule,
|
||||
COALESCE(organizations.name, '') AS organization_name,
|
||||
COALESCE(organizations.display_name, '') AS organization_display_name,
|
||||
@@ -608,7 +607,6 @@ type GetAuditLogsOffsetRow struct {
|
||||
UserRoles pq.StringArray `db:"user_roles" json:"user_roles"`
|
||||
UserAvatarUrl sql.NullString `db:"user_avatar_url" json:"user_avatar_url"`
|
||||
UserDeleted sql.NullBool `db:"user_deleted" json:"user_deleted"`
|
||||
UserThemePreference sql.NullString `db:"user_theme_preference" json:"user_theme_preference"`
|
||||
UserQuietHoursSchedule sql.NullString `db:"user_quiet_hours_schedule" json:"user_quiet_hours_schedule"`
|
||||
OrganizationName string `db:"organization_name" json:"organization_name"`
|
||||
OrganizationDisplayName string `db:"organization_display_name" json:"organization_display_name"`
|
||||
@@ -669,7 +667,6 @@ func (q *sqlQuerier) GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOff
|
||||
&i.UserRoles,
|
||||
&i.UserAvatarUrl,
|
||||
&i.UserDeleted,
|
||||
&i.UserThemePreference,
|
||||
&i.UserQuietHoursSchedule,
|
||||
&i.OrganizationName,
|
||||
&i.OrganizationDisplayName,
|
||||
@@ -1582,7 +1579,7 @@ func (q *sqlQuerier) DeleteGroupMemberFromGroup(ctx context.Context, arg DeleteG
|
||||
}
|
||||
|
||||
const getGroupMembers = `-- name: GetGroupMembers :many
|
||||
SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_theme_preference, user_name, user_github_com_user_id, organization_id, group_name, group_id FROM group_members_expanded
|
||||
SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, organization_id, group_name, group_id FROM group_members_expanded
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetGroupMembers(ctx context.Context) ([]GroupMember, error) {
|
||||
@@ -1608,7 +1605,6 @@ func (q *sqlQuerier) GetGroupMembers(ctx context.Context) ([]GroupMember, error)
|
||||
&i.UserDeleted,
|
||||
&i.UserLastSeenAt,
|
||||
&i.UserQuietHoursSchedule,
|
||||
&i.UserThemePreference,
|
||||
&i.UserName,
|
||||
&i.UserGithubComUserID,
|
||||
&i.OrganizationID,
|
||||
@@ -1629,7 +1625,7 @@ func (q *sqlQuerier) GetGroupMembers(ctx context.Context) ([]GroupMember, error)
|
||||
}
|
||||
|
||||
const getGroupMembersByGroupID = `-- name: GetGroupMembersByGroupID :many
|
||||
SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_theme_preference, user_name, user_github_com_user_id, organization_id, group_name, group_id FROM group_members_expanded WHERE group_id = $1
|
||||
SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, organization_id, group_name, group_id FROM group_members_expanded WHERE group_id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetGroupMembersByGroupID(ctx context.Context, groupID uuid.UUID) ([]GroupMember, error) {
|
||||
@@ -1655,7 +1651,6 @@ func (q *sqlQuerier) GetGroupMembersByGroupID(ctx context.Context, groupID uuid.
|
||||
&i.UserDeleted,
|
||||
&i.UserLastSeenAt,
|
||||
&i.UserQuietHoursSchedule,
|
||||
&i.UserThemePreference,
|
||||
&i.UserName,
|
||||
&i.UserGithubComUserID,
|
||||
&i.OrganizationID,
|
||||
@@ -7777,7 +7772,7 @@ FROM
|
||||
(
|
||||
-- Select all groups this user is a member of. This will also include
|
||||
-- the "Everyone" group for organizations the user is a member of.
|
||||
SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_theme_preference, user_name, user_github_com_user_id, organization_id, group_name, group_id FROM group_members_expanded
|
||||
SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, organization_id, group_name, group_id FROM group_members_expanded
|
||||
WHERE
|
||||
$1 = user_id AND
|
||||
$2 = group_members_expanded.organization_id
|
||||
@@ -11359,9 +11354,26 @@ 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, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at
|
||||
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
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
@@ -11393,7 +11405,6 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.QuietHoursSchedule,
|
||||
&i.ThemePreference,
|
||||
&i.Name,
|
||||
&i.GithubComUserID,
|
||||
&i.HashedOneTimePasscode,
|
||||
@@ -11404,7 +11415,7 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :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, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at
|
||||
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
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
@@ -11430,7 +11441,6 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.QuietHoursSchedule,
|
||||
&i.ThemePreference,
|
||||
&i.Name,
|
||||
&i.GithubComUserID,
|
||||
&i.HashedOneTimePasscode,
|
||||
@@ -11457,7 +11467,7 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) {
|
||||
|
||||
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, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, COUNT(*) OVER() AS count
|
||||
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, COUNT(*) OVER() AS count
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
@@ -11567,7 +11577,6 @@ type GetUsersRow struct {
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
|
||||
QuietHoursSchedule string `db:"quiet_hours_schedule" json:"quiet_hours_schedule"`
|
||||
ThemePreference string `db:"theme_preference" json:"theme_preference"`
|
||||
Name string `db:"name" json:"name"`
|
||||
GithubComUserID sql.NullInt64 `db:"github_com_user_id" json:"github_com_user_id"`
|
||||
HashedOneTimePasscode []byte `db:"hashed_one_time_passcode" json:"hashed_one_time_passcode"`
|
||||
@@ -11610,7 +11619,6 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.QuietHoursSchedule,
|
||||
&i.ThemePreference,
|
||||
&i.Name,
|
||||
&i.GithubComUserID,
|
||||
&i.HashedOneTimePasscode,
|
||||
@@ -11631,7 +11639,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse
|
||||
}
|
||||
|
||||
const getUsersByIDs = `-- name: GetUsersByIDs :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, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at FROM users WHERE id = ANY($1 :: uuid [ ])
|
||||
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 FROM users WHERE id = ANY($1 :: uuid [ ])
|
||||
`
|
||||
|
||||
// This shouldn't check for deleted, because it's frequently used
|
||||
@@ -11660,7 +11668,6 @@ func (q *sqlQuerier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.QuietHoursSchedule,
|
||||
&i.ThemePreference,
|
||||
&i.Name,
|
||||
&i.GithubComUserID,
|
||||
&i.HashedOneTimePasscode,
|
||||
@@ -11698,7 +11705,7 @@ VALUES
|
||||
-- if the status passed in is empty, fallback to dormant, which is what
|
||||
-- we were doing before.
|
||||
COALESCE(NULLIF($10::text, '')::user_status, 'dormant'::user_status)
|
||||
) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at
|
||||
) RETURNING 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
|
||||
`
|
||||
|
||||
type InsertUserParams struct {
|
||||
@@ -11742,7 +11749,6 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.QuietHoursSchedule,
|
||||
&i.ThemePreference,
|
||||
&i.Name,
|
||||
&i.GithubComUserID,
|
||||
&i.HashedOneTimePasscode,
|
||||
@@ -11804,45 +11810,29 @@ func (q *sqlQuerier) UpdateInactiveUsersToDormant(ctx context.Context, arg Updat
|
||||
}
|
||||
|
||||
const updateUserAppearanceSettings = `-- name: UpdateUserAppearanceSettings :one
|
||||
UPDATE
|
||||
users
|
||||
INSERT INTO
|
||||
user_configs (user_id, key, value)
|
||||
VALUES
|
||||
($1, 'theme_preference', $2)
|
||||
ON CONFLICT
|
||||
ON CONSTRAINT user_configs_pkey
|
||||
DO UPDATE
|
||||
SET
|
||||
theme_preference = $2,
|
||||
updated_at = $3
|
||||
WHERE
|
||||
id = $1
|
||||
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at
|
||||
value = $2
|
||||
WHERE user_configs.user_id = $1
|
||||
AND user_configs.key = 'theme_preference'
|
||||
RETURNING user_id, key, value
|
||||
`
|
||||
|
||||
type UpdateUserAppearanceSettingsParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
ThemePreference string `db:"theme_preference" json:"theme_preference"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateUserAppearanceSettings, arg.ID, arg.ThemePreference, arg.UpdatedAt)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.HashedPassword,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
&i.RBACRoles,
|
||||
&i.LoginType,
|
||||
&i.AvatarURL,
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.QuietHoursSchedule,
|
||||
&i.ThemePreference,
|
||||
&i.Name,
|
||||
&i.GithubComUserID,
|
||||
&i.HashedOneTimePasscode,
|
||||
&i.OneTimePasscodeExpiresAt,
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -11928,7 +11918,7 @@ SET
|
||||
last_seen_at = $2,
|
||||
updated_at = $3
|
||||
WHERE
|
||||
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at
|
||||
id = $1 RETURNING 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
|
||||
`
|
||||
|
||||
type UpdateUserLastSeenAtParams struct {
|
||||
@@ -11954,7 +11944,6 @@ func (q *sqlQuerier) UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLas
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.QuietHoursSchedule,
|
||||
&i.ThemePreference,
|
||||
&i.Name,
|
||||
&i.GithubComUserID,
|
||||
&i.HashedOneTimePasscode,
|
||||
@@ -11976,7 +11965,7 @@ SET
|
||||
'':: bytea
|
||||
END
|
||||
WHERE
|
||||
id = $2 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at
|
||||
id = $2 RETURNING 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
|
||||
`
|
||||
|
||||
type UpdateUserLoginTypeParams struct {
|
||||
@@ -12001,7 +11990,6 @@ func (q *sqlQuerier) UpdateUserLoginType(ctx context.Context, arg UpdateUserLogi
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.QuietHoursSchedule,
|
||||
&i.ThemePreference,
|
||||
&i.Name,
|
||||
&i.GithubComUserID,
|
||||
&i.HashedOneTimePasscode,
|
||||
@@ -12021,7 +12009,7 @@ SET
|
||||
name = $6
|
||||
WHERE
|
||||
id = $1
|
||||
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at
|
||||
RETURNING 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
|
||||
`
|
||||
|
||||
type UpdateUserProfileParams struct {
|
||||
@@ -12057,7 +12045,6 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.QuietHoursSchedule,
|
||||
&i.ThemePreference,
|
||||
&i.Name,
|
||||
&i.GithubComUserID,
|
||||
&i.HashedOneTimePasscode,
|
||||
@@ -12073,7 +12060,7 @@ SET
|
||||
quiet_hours_schedule = $2
|
||||
WHERE
|
||||
id = $1
|
||||
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at
|
||||
RETURNING 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
|
||||
`
|
||||
|
||||
type UpdateUserQuietHoursScheduleParams struct {
|
||||
@@ -12098,7 +12085,6 @@ func (q *sqlQuerier) UpdateUserQuietHoursSchedule(ctx context.Context, arg Updat
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.QuietHoursSchedule,
|
||||
&i.ThemePreference,
|
||||
&i.Name,
|
||||
&i.GithubComUserID,
|
||||
&i.HashedOneTimePasscode,
|
||||
@@ -12115,7 +12101,7 @@ SET
|
||||
rbac_roles = ARRAY(SELECT DISTINCT UNNEST($1 :: text[]))
|
||||
WHERE
|
||||
id = $2
|
||||
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at
|
||||
RETURNING 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
|
||||
`
|
||||
|
||||
type UpdateUserRolesParams struct {
|
||||
@@ -12140,7 +12126,6 @@ func (q *sqlQuerier) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesPar
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.QuietHoursSchedule,
|
||||
&i.ThemePreference,
|
||||
&i.Name,
|
||||
&i.GithubComUserID,
|
||||
&i.HashedOneTimePasscode,
|
||||
@@ -12156,7 +12141,7 @@ SET
|
||||
status = $2,
|
||||
updated_at = $3
|
||||
WHERE
|
||||
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at
|
||||
id = $1 RETURNING 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
|
||||
`
|
||||
|
||||
type UpdateUserStatusParams struct {
|
||||
@@ -12182,7 +12167,6 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
|
||||
&i.Deleted,
|
||||
&i.LastSeenAt,
|
||||
&i.QuietHoursSchedule,
|
||||
&i.ThemePreference,
|
||||
&i.Name,
|
||||
&i.GithubComUserID,
|
||||
&i.HashedOneTimePasscode,
|
||||
|
||||
@@ -16,7 +16,6 @@ SELECT
|
||||
users.rbac_roles AS user_roles,
|
||||
users.avatar_url AS user_avatar_url,
|
||||
users.deleted AS user_deleted,
|
||||
users.theme_preference AS user_theme_preference,
|
||||
users.quiet_hours_schedule AS user_quiet_hours_schedule,
|
||||
COALESCE(organizations.name, '') AS organization_name,
|
||||
COALESCE(organizations.display_name, '') AS organization_display_name,
|
||||
|
||||
@@ -98,14 +98,27 @@ SET
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: UpdateUserAppearanceSettings :one
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
theme_preference = $2,
|
||||
updated_at = $3
|
||||
-- name: GetUserAppearanceSettings :one
|
||||
SELECT
|
||||
value as theme_preference
|
||||
FROM
|
||||
user_configs
|
||||
WHERE
|
||||
id = $1
|
||||
user_id = @user_id
|
||||
AND key = 'theme_preference';
|
||||
|
||||
-- name: UpdateUserAppearanceSettings :one
|
||||
INSERT INTO
|
||||
user_configs (user_id, key, value)
|
||||
VALUES
|
||||
(@user_id, 'theme_preference', @theme_preference)
|
||||
ON CONFLICT
|
||||
ON CONSTRAINT user_configs_pkey
|
||||
DO UPDATE
|
||||
SET
|
||||
value = @theme_preference
|
||||
WHERE user_configs.user_id = @user_id
|
||||
AND user_configs.key = 'theme_preference'
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserRoles :one
|
||||
|
||||
@@ -65,6 +65,7 @@ const (
|
||||
UniqueTemplateVersionsPkey UniqueConstraint = "template_versions_pkey" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_pkey PRIMARY KEY (id);
|
||||
UniqueTemplateVersionsTemplateIDNameKey UniqueConstraint = "template_versions_template_id_name_key" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_name_key UNIQUE (template_id, name);
|
||||
UniqueTemplatesPkey UniqueConstraint = "templates_pkey" // ALTER TABLE ONLY templates ADD CONSTRAINT templates_pkey PRIMARY KEY (id);
|
||||
UniqueUserConfigsPkey UniqueConstraint = "user_configs_pkey" // ALTER TABLE ONLY user_configs ADD CONSTRAINT user_configs_pkey PRIMARY KEY (user_id, key);
|
||||
UniqueUserDeletedPkey UniqueConstraint = "user_deleted_pkey" // ALTER TABLE ONLY user_deleted ADD CONSTRAINT user_deleted_pkey PRIMARY KEY (id);
|
||||
UniqueUserLinksPkey UniqueConstraint = "user_links_pkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_pkey PRIMARY KEY (user_id, login_type);
|
||||
UniqueUserStatusChangesPkey UniqueConstraint = "user_status_changes_pkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_pkey PRIMARY KEY (id);
|
||||
|
||||
+38
-14
@@ -959,6 +959,38 @@ func (api *API) notifyUserStatusChanged(ctx context.Context, actingUserName stri
|
||||
return nil
|
||||
}
|
||||
|
||||
// @Summary Get user appearance settings
|
||||
// @ID get-user-appearance-settings
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Users
|
||||
// @Param user path string true "User ID, name, or me"
|
||||
// @Success 200 {object} codersdk.UserAppearanceSettings
|
||||
// @Router /users/{user}/appearance [get]
|
||||
func (api *API) userAppearanceSettings(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
user = httpmw.UserParam(r)
|
||||
)
|
||||
|
||||
themePreference, err := api.Database.GetUserAppearanceSettings(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
|
||||
}
|
||||
|
||||
themePreference = ""
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserAppearanceSettings{
|
||||
ThemePreference: themePreference,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Update user appearance settings
|
||||
// @ID update-user-appearance-settings
|
||||
// @Security CoderSessionToken
|
||||
@@ -967,7 +999,7 @@ func (api *API) notifyUserStatusChanged(ctx context.Context, actingUserName stri
|
||||
// @Tags Users
|
||||
// @Param user path string true "User ID, name, or me"
|
||||
// @Param request body codersdk.UpdateUserAppearanceSettingsRequest true "New appearance settings"
|
||||
// @Success 200 {object} codersdk.User
|
||||
// @Success 200 {object} codersdk.UserAppearanceSettings
|
||||
// @Router /users/{user}/appearance [put]
|
||||
func (api *API) putUserAppearanceSettings(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
@@ -980,10 +1012,9 @@ func (api *API) putUserAppearanceSettings(rw http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
updatedUser, err := api.Database.UpdateUserAppearanceSettings(ctx, database.UpdateUserAppearanceSettingsParams{
|
||||
ID: user.ID,
|
||||
updatedSettings, err := api.Database.UpdateUserAppearanceSettings(ctx, database.UpdateUserAppearanceSettingsParams{
|
||||
UserID: user.ID,
|
||||
ThemePreference: params.ThemePreference,
|
||||
UpdatedAt: dbtime.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -993,16 +1024,9 @@ func (api *API) putUserAppearanceSettings(rw http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
organizationIDs, err := userOrganizationIDs(ctx, api, user)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching user's organizations.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(updatedUser, organizationIDs))
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserAppearanceSettings{
|
||||
ThemePreference: updatedSettings.Value,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Update user password
|
||||
|
||||
+9
-3
@@ -54,9 +54,11 @@ type ReducedUser struct {
|
||||
UpdatedAt time.Time `json:"updated_at" table:"updated at" format:"date-time"`
|
||||
LastSeenAt time.Time `json:"last_seen_at" format:"date-time"`
|
||||
|
||||
Status UserStatus `json:"status" table:"status" enums:"active,suspended"`
|
||||
LoginType LoginType `json:"login_type"`
|
||||
ThemePreference string `json:"theme_preference"`
|
||||
Status UserStatus `json:"status" table:"status" enums:"active,suspended"`
|
||||
LoginType LoginType `json:"login_type"`
|
||||
// Deprecated: this value should be retrieved from
|
||||
// `codersdk.UserPreferenceSettings` instead.
|
||||
ThemePreference string `json:"theme_preference,omitempty"`
|
||||
}
|
||||
|
||||
// User represents a user in Coder.
|
||||
@@ -187,6 +189,10 @@ type ValidateUserPasswordResponse struct {
|
||||
Details string `json:"details"`
|
||||
}
|
||||
|
||||
type UserAppearanceSettings struct {
|
||||
ThemePreference string `json:"theme_preference"`
|
||||
}
|
||||
|
||||
type UpdateUserAppearanceSettingsRequest struct {
|
||||
ThemePreference string `json:"theme_preference" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ We track the following resources:
|
||||
| RoleSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
|
||||
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>source_example_id</td><td>false</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
| WorkspaceAgent<br><i>connect, disconnect</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>api_version</td><td>false</td></tr><tr><td>architecture</td><td>false</td></tr><tr><td>auth_instance_id</td><td>false</td></tr><tr><td>auth_token</td><td>false</td></tr><tr><td>connection_timeout_seconds</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>directory</td><td>false</td></tr><tr><td>disconnected_at</td><td>false</td></tr><tr><td>display_apps</td><td>false</td></tr><tr><td>display_order</td><td>false</td></tr><tr><td>environment_variables</td><td>false</td></tr><tr><td>expanded_directory</td><td>false</td></tr><tr><td>first_connected_at</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>instance_metadata</td><td>false</td></tr><tr><td>last_connected_at</td><td>false</td></tr><tr><td>last_connected_replica_id</td><td>false</td></tr><tr><td>lifecycle_state</td><td>false</td></tr><tr><td>logs_length</td><td>false</td></tr><tr><td>logs_overflowed</td><td>false</td></tr><tr><td>motd_file</td><td>false</td></tr><tr><td>name</td><td>false</td></tr><tr><td>operating_system</td><td>false</td></tr><tr><td>ready_at</td><td>false</td></tr><tr><td>resource_id</td><td>false</td></tr><tr><td>resource_metadata</td><td>false</td></tr><tr><td>started_at</td><td>false</td></tr><tr><td>subsystems</td><td>false</td></tr><tr><td>troubleshooting_url</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>version</td><td>false</td></tr></tbody></table> |
|
||||
| WorkspaceApp<br><i>open, close</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>agent_id</td><td>false</td></tr><tr><td>command</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>display_name</td><td>false</td></tr><tr><td>display_order</td><td>false</td></tr><tr><td>external</td><td>false</td></tr><tr><td>health</td><td>false</td></tr><tr><td>healthcheck_interval</td><td>false</td></tr><tr><td>healthcheck_threshold</td><td>false</td></tr><tr><td>healthcheck_url</td><td>false</td></tr><tr><td>hidden</td><td>false</td></tr><tr><td>icon</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>open_in</td><td>false</td></tr><tr><td>sharing_level</td><td>false</td></tr><tr><td>slug</td><td>false</td></tr><tr><td>subdomain</td><td>false</td></tr><tr><td>url</td><td>false</td></tr></tbody></table> |
|
||||
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>template_version_preset_id</td><td>false</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
||||
|
||||
Generated
+23
-23
@@ -260,7 +260,7 @@ Status Code **200**
|
||||
| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
|
||||
| `»» name` | string | false | | |
|
||||
| `»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
|
||||
| `»» theme_preference` | string | false | | |
|
||||
| `»» theme_preference` | string | false | | Deprecated: this value should be retrieved from `codersdk.UserPreferenceSettings` instead. |
|
||||
| `»» updated_at` | string(date-time) | false | | |
|
||||
| `»» username` | string | true | | |
|
||||
| `» name` | string | false | | |
|
||||
@@ -1271,7 +1271,7 @@ Status Code **200**
|
||||
| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
|
||||
| `»» name` | string | false | | |
|
||||
| `»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
|
||||
| `»» theme_preference` | string | false | | |
|
||||
| `»» theme_preference` | string | false | | Deprecated: this value should be retrieved from `codersdk.UserPreferenceSettings` instead. |
|
||||
| `»» updated_at` | string(date-time) | false | | |
|
||||
| `»» username` | string | true | | |
|
||||
| `» name` | string | false | | |
|
||||
@@ -3126,26 +3126,26 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl \
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|----------------------|----------------------------------------------------------|----------|--------------|-------------|
|
||||
| `[array item]` | array | false | | |
|
||||
| `» avatar_url` | string(uri) | false | | |
|
||||
| `» created_at` | string(date-time) | true | | |
|
||||
| `» email` | string(email) | true | | |
|
||||
| `» id` | string(uuid) | true | | |
|
||||
| `» last_seen_at` | string(date-time) | false | | |
|
||||
| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
|
||||
| `» name` | string | false | | |
|
||||
| `» organization_ids` | array | false | | |
|
||||
| `» role` | [codersdk.TemplateRole](schemas.md#codersdktemplaterole) | false | | |
|
||||
| `» roles` | array | false | | |
|
||||
| `»» display_name` | string | false | | |
|
||||
| `»» name` | string | false | | |
|
||||
| `»» organization_id` | string | false | | |
|
||||
| `» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
|
||||
| `» theme_preference` | string | false | | |
|
||||
| `» updated_at` | string(date-time) | false | | |
|
||||
| `» username` | string | true | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|----------------------|----------------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------|
|
||||
| `[array item]` | array | false | | |
|
||||
| `» avatar_url` | string(uri) | false | | |
|
||||
| `» created_at` | string(date-time) | true | | |
|
||||
| `» email` | string(email) | true | | |
|
||||
| `» id` | string(uuid) | true | | |
|
||||
| `» last_seen_at` | string(date-time) | false | | |
|
||||
| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
|
||||
| `» name` | string | false | | |
|
||||
| `» organization_ids` | array | false | | |
|
||||
| `» role` | [codersdk.TemplateRole](schemas.md#codersdktemplaterole) | false | | |
|
||||
| `» roles` | array | false | | |
|
||||
| `»» display_name` | string | false | | |
|
||||
| `»» name` | string | false | | |
|
||||
| `»» organization_id` | string | false | | |
|
||||
| `» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
|
||||
| `» theme_preference` | string | false | | Deprecated: this value should be retrieved from `codersdk.UserPreferenceSettings` instead. |
|
||||
| `» updated_at` | string(date-time) | false | | |
|
||||
| `» username` | string | true | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
@@ -3325,7 +3325,7 @@ Status Code **200**
|
||||
| `»»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
|
||||
| `»»» name` | string | false | | |
|
||||
| `»»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
|
||||
| `»»» theme_preference` | string | false | | |
|
||||
| `»»» theme_preference` | string | false | | Deprecated: this value should be retrieved from `codersdk.UserPreferenceSettings` instead. |
|
||||
| `»»» updated_at` | string(date-time) | false | | |
|
||||
| `»»» username` | string | true | | |
|
||||
| `»» name` | string | false | | |
|
||||
|
||||
Generated
+58
-44
@@ -5195,19 +5195,19 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|--------------------------------------------|----------|--------------|-------------|
|
||||
| `avatar_url` | string | false | | |
|
||||
| `created_at` | string | true | | |
|
||||
| `email` | string | true | | |
|
||||
| `id` | string | true | | |
|
||||
| `last_seen_at` | string | false | | |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
|
||||
| `theme_preference` | string | false | | |
|
||||
| `updated_at` | string | false | | |
|
||||
| `username` | string | true | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|--------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------|
|
||||
| `avatar_url` | string | false | | |
|
||||
| `created_at` | string | true | | |
|
||||
| `email` | string | true | | |
|
||||
| `id` | string | true | | |
|
||||
| `last_seen_at` | string | false | | |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
|
||||
| `theme_preference` | string | false | | Deprecated: this value should be retrieved from `codersdk.UserPreferenceSettings` instead. |
|
||||
| `updated_at` | string | false | | |
|
||||
| `username` | string | true | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
@@ -6180,22 +6180,22 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|-------------------------------------------------|----------|--------------|-------------|
|
||||
| `avatar_url` | string | false | | |
|
||||
| `created_at` | string | true | | |
|
||||
| `email` | string | true | | |
|
||||
| `id` | string | true | | |
|
||||
| `last_seen_at` | string | false | | |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `organization_ids` | array of string | false | | |
|
||||
| `role` | [codersdk.TemplateRole](#codersdktemplaterole) | false | | |
|
||||
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
|
||||
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
|
||||
| `theme_preference` | string | false | | |
|
||||
| `updated_at` | string | false | | |
|
||||
| `username` | string | true | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|-------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------|
|
||||
| `avatar_url` | string | false | | |
|
||||
| `created_at` | string | true | | |
|
||||
| `email` | string | true | | |
|
||||
| `id` | string | true | | |
|
||||
| `last_seen_at` | string | false | | |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `organization_ids` | array of string | false | | |
|
||||
| `role` | [codersdk.TemplateRole](#codersdktemplaterole) | false | | |
|
||||
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
|
||||
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
|
||||
| `theme_preference` | string | false | | Deprecated: this value should be retrieved from `codersdk.UserPreferenceSettings` instead. |
|
||||
| `updated_at` | string | false | | |
|
||||
| `username` | string | true | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
@@ -6880,21 +6880,21 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|-------------------------------------------------|----------|--------------|-------------|
|
||||
| `avatar_url` | string | false | | |
|
||||
| `created_at` | string | true | | |
|
||||
| `email` | string | true | | |
|
||||
| `id` | string | true | | |
|
||||
| `last_seen_at` | string | false | | |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `organization_ids` | array of string | false | | |
|
||||
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
|
||||
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
|
||||
| `theme_preference` | string | false | | |
|
||||
| `updated_at` | string | false | | |
|
||||
| `username` | string | true | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|-------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------|
|
||||
| `avatar_url` | string | false | | |
|
||||
| `created_at` | string | true | | |
|
||||
| `email` | string | true | | |
|
||||
| `id` | string | true | | |
|
||||
| `last_seen_at` | string | false | | |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `organization_ids` | array of string | false | | |
|
||||
| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | |
|
||||
| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | |
|
||||
| `theme_preference` | string | false | | Deprecated: this value should be retrieved from `codersdk.UserPreferenceSettings` instead. |
|
||||
| `updated_at` | string | false | | |
|
||||
| `username` | string | true | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
@@ -6990,6 +6990,20 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
|----------|----------------------------------------------------------------------------|----------|--------------|-------------|
|
||||
| `report` | [codersdk.UserActivityInsightsReport](#codersdkuseractivityinsightsreport) | false | | |
|
||||
|
||||
## codersdk.UserAppearanceSettings
|
||||
|
||||
```json
|
||||
{
|
||||
"theme_preference": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|--------|----------|--------------|-------------|
|
||||
| `theme_preference` | string | false | | |
|
||||
|
||||
## codersdk.UserLatency
|
||||
|
||||
```json
|
||||
|
||||
Generated
+41
-24
@@ -476,6 +476,43 @@ curl -X DELETE http://coder-server:8080/api/v2/users/{user} \
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get user appearance settings
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/users/{user}/appearance \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /users/{user}/appearance`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|--------|----------|----------------------|
|
||||
| `user` | path | string | true | User ID, name, or me |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"theme_preference": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------------------|
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.UserAppearanceSettings](schemas.md#codersdkuserappearancesettings) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Update user appearance settings
|
||||
|
||||
### Code samples
|
||||
@@ -511,35 +548,15 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/appearance \
|
||||
|
||||
```json
|
||||
{
|
||||
"avatar_url": "http://example.com",
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "",
|
||||
"name": "string",
|
||||
"organization_ids": [
|
||||
"497f6eca-6276-4993-bfeb-53cbbbba6f08"
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"display_name": "string",
|
||||
"name": "string",
|
||||
"organization_id": "string"
|
||||
}
|
||||
],
|
||||
"status": "active",
|
||||
"theme_preference": "string",
|
||||
"updated_at": "2019-08-24T14:15:22Z",
|
||||
"username": "string"
|
||||
"theme_preference": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|---------------------------------------------------------|-------------|------------------------------------------|
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.User](schemas.md#codersdkuser) |
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------------------|
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.UserAppearanceSettings](schemas.md#codersdkuserappearancesettings) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
|
||||
@@ -147,7 +147,6 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
||||
"last_seen_at": ActionIgnore,
|
||||
"deleted": ActionTrack,
|
||||
"quiet_hours_schedule": ActionTrack,
|
||||
"theme_preference": ActionIgnore,
|
||||
"name": ActionTrack,
|
||||
"github_com_user_id": ActionIgnore,
|
||||
"hashed_one_time_passcode": ActionIgnore,
|
||||
|
||||
+47
-46
@@ -9,53 +9,54 @@
|
||||
-->
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Coder</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#17172E" />
|
||||
<meta name="application-name" content="{{ .ApplicationName }}" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="csrf-token" content="{{ .CSRF.Token }}" />
|
||||
<meta property="build-info" content="{{ .BuildInfo }}" />
|
||||
<meta property="user" content="{{ .User }}" />
|
||||
<meta property="entitlements" content="{{ .Entitlements }}" />
|
||||
<meta property="appearance" content="{{ .Appearance }}" />
|
||||
<meta property="experiments" content="{{ .Experiments }}" />
|
||||
<meta property="regions" content="{{ .Regions }}" />
|
||||
<meta property="docs-url" content="{{ .DocsURL }}" />
|
||||
<meta property="logo-url" content="{{ .LogoURL }}" />
|
||||
<!-- We need to set data-react-helmet to be able to override it in the workspace page -->
|
||||
<link
|
||||
rel="alternate icon"
|
||||
type="image/png"
|
||||
href="/favicons/favicon-light.png"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/favicons/favicon-light.svg"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<link
|
||||
rel="alternate icon"
|
||||
type="image/png"
|
||||
href="/favicons/favicon-dark.png"
|
||||
media="(prefers-color-scheme: light)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/favicons/favicon-dark.svg"
|
||||
media="(prefers-color-scheme: light)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<meta charset="utf-8" />
|
||||
<title>Coder</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#17172E" />
|
||||
<meta name="application-name" content="{{ .ApplicationName }}" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="csrf-token" content="{{ .CSRF.Token }}" />
|
||||
<meta property="build-info" content="{{ .BuildInfo }}" />
|
||||
<meta property="user" content="{{ .User }}" />
|
||||
<meta property="entitlements" content="{{ .Entitlements }}" />
|
||||
<meta property="appearance" content="{{ .Appearance }}" />
|
||||
<meta property="userAppearance" content="{{ .UserAppearance }}" />
|
||||
<meta property="experiments" content="{{ .Experiments }}" />
|
||||
<meta property="regions" content="{{ .Regions }}" />
|
||||
<meta property="docs-url" content="{{ .DocsURL }}" />
|
||||
<meta property="logo-url" content="{{ .LogoURL }}" />
|
||||
<!-- We need to set data-react-helmet to be able to override it in the workspace page -->
|
||||
<link
|
||||
rel="alternate icon"
|
||||
type="image/png"
|
||||
href="/favicons/favicon-light.png"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/favicons/favicon-light.svg"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<link
|
||||
rel="alternate icon"
|
||||
type="image/png"
|
||||
href="/favicons/favicon-dark.png"
|
||||
media="(prefers-color-scheme: light)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/favicons/favicon-dark.svg"
|
||||
media="(prefers-color-scheme: light)"
|
||||
data-react-helmet="true"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./src/index.tsx"></script>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./src/index.tsx"></script>
|
||||
</body>
|
||||
|
||||
+29
-7
@@ -292,13 +292,14 @@ type htmlState struct {
|
||||
ApplicationName string
|
||||
LogoURL string
|
||||
|
||||
BuildInfo string
|
||||
User string
|
||||
Entitlements string
|
||||
Appearance string
|
||||
Experiments string
|
||||
Regions string
|
||||
DocsURL string
|
||||
BuildInfo string
|
||||
User string
|
||||
Entitlements string
|
||||
Appearance string
|
||||
UserAppearance string
|
||||
Experiments string
|
||||
Regions string
|
||||
DocsURL string
|
||||
}
|
||||
|
||||
type csrfState struct {
|
||||
@@ -426,12 +427,22 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht
|
||||
|
||||
var eg errgroup.Group
|
||||
var user database.User
|
||||
var themePreference string
|
||||
orgIDs := []uuid.UUID{}
|
||||
eg.Go(func() error {
|
||||
var err error
|
||||
user, err = h.opts.Database.GetUserByID(ctx, apiKey.UserID)
|
||||
return err
|
||||
})
|
||||
eg.Go(func() error {
|
||||
var err error
|
||||
themePreference, err = h.opts.Database.GetUserAppearanceSettings(ctx, apiKey.UserID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
themePreference = ""
|
||||
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 {
|
||||
@@ -455,6 +466,17 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
userAppearance, err := json.Marshal(codersdk.UserAppearanceSettings{
|
||||
ThemePreference: themePreference,
|
||||
})
|
||||
if err == nil {
|
||||
state.UserAppearance = html.EscapeString(string(userAppearance))
|
||||
}
|
||||
}()
|
||||
|
||||
if h.Entitlements != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
|
||||
+8
-6
@@ -1340,14 +1340,16 @@ class ApiMethods {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getAppearanceSettings =
|
||||
async (): Promise<TypesGen.UserAppearanceSettings> => {
|
||||
const response = await this.axios.get("/api/v2/users/me/appearance");
|
||||
return response.data;
|
||||
};
|
||||
|
||||
updateAppearanceSettings = async (
|
||||
userId: string,
|
||||
data: TypesGen.UpdateUserAppearanceSettingsRequest,
|
||||
): Promise<TypesGen.User> => {
|
||||
const response = await this.axios.put(
|
||||
`/api/v2/users/${userId}/appearance`,
|
||||
data,
|
||||
);
|
||||
): Promise<TypesGen.UserAppearanceSettings> => {
|
||||
const response = await this.axios.put("/api/v2/users/me/appearance", data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import type {
|
||||
UpdateUserPasswordRequest,
|
||||
UpdateUserProfileRequest,
|
||||
User,
|
||||
UserAppearanceSettings,
|
||||
UsersRequest,
|
||||
ValidateUserPasswordRequest,
|
||||
} from "api/typesGenerated";
|
||||
import {
|
||||
type MetadataState,
|
||||
@@ -224,35 +224,39 @@ export const updateProfile = (userId: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
const myAppearanceKey = ["me", "appearance"];
|
||||
|
||||
export const appearanceSettings = (
|
||||
metadata: MetadataState<UserAppearanceSettings>,
|
||||
) => {
|
||||
return cachedQuery({
|
||||
metadata,
|
||||
queryKey: myAppearanceKey,
|
||||
queryFn: API.getAppearanceSettings,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAppearanceSettings = (
|
||||
userId: string,
|
||||
queryClient: QueryClient,
|
||||
): UseMutationOptions<
|
||||
User,
|
||||
UserAppearanceSettings,
|
||||
unknown,
|
||||
UpdateUserAppearanceSettingsRequest,
|
||||
unknown
|
||||
> => {
|
||||
return {
|
||||
mutationFn: (req) => API.updateAppearanceSettings(userId, req),
|
||||
mutationFn: (req) => API.updateAppearanceSettings(req),
|
||||
onMutate: async (patch) => {
|
||||
// Mutate the `queryClient` optimistically to make the theme switcher
|
||||
// more responsive.
|
||||
const me: User | undefined = queryClient.getQueryData(meKey);
|
||||
if (userId === "me" && me) {
|
||||
queryClient.setQueryData(meKey, {
|
||||
...me,
|
||||
theme_preference: patch.theme_preference,
|
||||
});
|
||||
}
|
||||
queryClient.setQueryData(myAppearanceKey, {
|
||||
theme_preference: patch.theme_preference,
|
||||
});
|
||||
},
|
||||
onSuccess: async () => {
|
||||
onSuccess: async () =>
|
||||
// Could technically invalidate more, but we only ever care about the
|
||||
// `theme_preference` for the `me` query.
|
||||
if (userId === "me") {
|
||||
await queryClient.invalidateQueries(meKey);
|
||||
}
|
||||
},
|
||||
await queryClient.invalidateQueries(myAppearanceKey),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Generated
+6
-1
@@ -1970,7 +1970,7 @@ export interface ReducedUser extends MinimalUser {
|
||||
readonly last_seen_at: string;
|
||||
readonly status: UserStatus;
|
||||
readonly login_type: LoginType;
|
||||
readonly theme_preference: string;
|
||||
readonly theme_preference?: string;
|
||||
}
|
||||
|
||||
// From codersdk/workspaceproxy.go
|
||||
@@ -2805,6 +2805,11 @@ export interface UserActivityInsightsResponse {
|
||||
readonly report: UserActivityInsightsReport;
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
export interface UserAppearanceSettings {
|
||||
readonly theme_preference: string;
|
||||
}
|
||||
|
||||
// From codersdk/insights.go
|
||||
export interface UserLatency {
|
||||
readonly template_ids: readonly string[];
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||
import { fireEvent, screen } from "@testing-library/react";
|
||||
import { renderComponent } from "testHelpers/renderHelpers";
|
||||
import { FileUpload } from "./FileUpload";
|
||||
|
||||
test("accepts files with the correct extension", async () => {
|
||||
const onUpload = jest.fn();
|
||||
|
||||
render(
|
||||
<ThemeProvider>
|
||||
<FileUpload
|
||||
isUploading={false}
|
||||
onUpload={onUpload}
|
||||
removeLabel="Remove file"
|
||||
title="Upload file"
|
||||
extensions={["tar", "zip"]}
|
||||
/>
|
||||
</ThemeProvider>,
|
||||
renderComponent(
|
||||
<FileUpload
|
||||
isUploading={false}
|
||||
onUpload={onUpload}
|
||||
removeLabel="Remove file"
|
||||
title="Upload file"
|
||||
extensions={["tar", "zip"]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dropZone = screen.getByTestId("drop-zone");
|
||||
|
||||
@@ -7,26 +7,27 @@ import {
|
||||
StyledEngineProvider,
|
||||
// biome-ignore lint/nursery/noRestrictedImports: we extend the MUI theme
|
||||
} from "@mui/material/styles";
|
||||
import { appearanceSettings } from "api/queries/users";
|
||||
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
|
||||
import {
|
||||
type FC,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import themes, { DEFAULT_THEME, type Theme } from "theme";
|
||||
import { AuthContext } from "./auth/AuthProvider";
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
// We need to use the `AuthContext` directly, rather than the `useAuth` hook,
|
||||
// because Storybook and many tests depend on this component, but do not provide
|
||||
// an `AuthProvider`, and `useAuth` will throw in that case.
|
||||
const user = useContext(AuthContext)?.user;
|
||||
const { metadata } = useEmbeddedMetadata();
|
||||
const appearanceSettingsQuery = useQuery(
|
||||
appearanceSettings(metadata.userAppearance),
|
||||
);
|
||||
const themeQuery = useMemo(
|
||||
() => window.matchMedia?.("(prefers-color-scheme: light)"),
|
||||
[],
|
||||
@@ -53,7 +54,8 @@ export const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
}, [themeQuery]);
|
||||
|
||||
// We might not be logged in yet, or the `theme_preference` could be an empty string.
|
||||
const themePreference = user?.theme_preference || DEFAULT_THEME;
|
||||
const themePreference =
|
||||
appearanceSettingsQuery.data?.theme_preference || DEFAULT_THEME;
|
||||
// The janky casting here is find because of the much more type safe fallback
|
||||
// We need to support `themePreference` being wrong anyway because the database
|
||||
// value could be anything, like an empty string.
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
*/
|
||||
import { act, renderHook, screen } from "@testing-library/react";
|
||||
import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar";
|
||||
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||
import { ThemeOverride } from "contexts/ThemeProvider";
|
||||
import themes, { DEFAULT_THEME } from "theme";
|
||||
import {
|
||||
COPY_FAILED_MESSAGE,
|
||||
HTTP_FALLBACK_DATA_ID,
|
||||
@@ -121,10 +122,10 @@ function renderUseClipboard<TInput extends UseClipboardInput>(inputs: TInput) {
|
||||
initialProps: inputs,
|
||||
wrapper: ({ children }) => (
|
||||
// Need ThemeProvider because GlobalSnackbar uses theme
|
||||
<ThemeProvider>
|
||||
<ThemeOverride theme={themes[DEFAULT_THEME]}>
|
||||
{children}
|
||||
<GlobalSnackbar />
|
||||
</ThemeProvider>
|
||||
</ThemeOverride>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MockEntitlements,
|
||||
MockExperiments,
|
||||
MockUser,
|
||||
MockUserAppearanceSettings,
|
||||
} from "testHelpers/entities";
|
||||
import {
|
||||
DEFAULT_METADATA_KEY,
|
||||
@@ -38,6 +39,7 @@ const mockDataForTags = {
|
||||
entitlements: MockEntitlements,
|
||||
experiments: MockExperiments,
|
||||
user: MockUser,
|
||||
userAppearance: MockUserAppearanceSettings,
|
||||
regions: MockRegions,
|
||||
} as const satisfies Record<MetadataKey, MetadataValue>;
|
||||
|
||||
@@ -66,6 +68,10 @@ const emptyMetadata: RuntimeHtmlMetadata = {
|
||||
available: false,
|
||||
value: undefined,
|
||||
},
|
||||
userAppearance: {
|
||||
available: false,
|
||||
value: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const populatedMetadata: RuntimeHtmlMetadata = {
|
||||
@@ -93,6 +99,10 @@ const populatedMetadata: RuntimeHtmlMetadata = {
|
||||
available: true,
|
||||
value: MockUser,
|
||||
},
|
||||
userAppearance: {
|
||||
available: true,
|
||||
value: MockUserAppearanceSettings,
|
||||
},
|
||||
};
|
||||
|
||||
function seedInitialMetadata(metadataKey: string): () => void {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
Experiments,
|
||||
Region,
|
||||
User,
|
||||
UserAppearanceSettings,
|
||||
} from "api/typesGenerated";
|
||||
import { useMemo, useSyncExternalStore } from "react";
|
||||
|
||||
@@ -25,6 +26,7 @@ type AvailableMetadata = Readonly<{
|
||||
user: User;
|
||||
experiments: Experiments;
|
||||
appearance: AppearanceConfig;
|
||||
userAppearance: UserAppearanceSettings;
|
||||
entitlements: Entitlements;
|
||||
regions: readonly Region[];
|
||||
"build-info": BuildInfoResponse;
|
||||
@@ -83,6 +85,8 @@ export class MetadataManager implements MetadataManagerApi {
|
||||
this.metadata = {
|
||||
user: this.registerValue<User>("user"),
|
||||
appearance: this.registerValue<AppearanceConfig>("appearance"),
|
||||
userAppearance:
|
||||
this.registerValue<UserAppearanceSettings>("userAppearance"),
|
||||
entitlements: this.registerValue<Entitlements>("entitlements"),
|
||||
experiments: this.registerValue<Experiments>("experiments"),
|
||||
"build-info": this.registerValue<BuildInfoResponse>("build-info"),
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("appearance page", () => {
|
||||
|
||||
// Check if the API was called correctly
|
||||
expect(API.updateAppearanceSettings).toBeCalledTimes(1);
|
||||
expect(API.updateAppearanceSettings).toHaveBeenCalledWith("me", {
|
||||
expect(API.updateAppearanceSettings).toHaveBeenCalledWith({
|
||||
theme_preference: "light",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
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 { useAuthenticated } from "contexts/auth/RequireAuth";
|
||||
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
|
||||
import type { FC } from "react";
|
||||
import { useMutation, useQueryClient } from "react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { Section } from "../Section";
|
||||
import { AppearanceForm } from "./AppearanceForm";
|
||||
|
||||
export const AppearancePage: FC = () => {
|
||||
const { user: me } = useAuthenticated();
|
||||
const queryClient = useQueryClient();
|
||||
const updateAppearanceSettingsMutation = useMutation(
|
||||
updateAppearanceSettings("me", queryClient),
|
||||
updateAppearanceSettings(queryClient),
|
||||
);
|
||||
|
||||
const { metadata } = useEmbeddedMetadata();
|
||||
const appearanceSettingsQuery = useQuery(
|
||||
appearanceSettings(metadata.userAppearance),
|
||||
);
|
||||
|
||||
if (appearanceSettingsQuery.isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (!appearanceSettingsQuery.data) {
|
||||
return <ErrorAlert error={appearanceSettingsQuery.error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section
|
||||
@@ -30,7 +45,9 @@ export const AppearancePage: FC = () => {
|
||||
<AppearanceForm
|
||||
isUpdating={updateAppearanceSettingsMutation.isLoading}
|
||||
error={updateAppearanceSettingsMutation.error}
|
||||
initialValues={{ theme_preference: me.theme_preference }}
|
||||
initialValues={{
|
||||
theme_preference: appearanceSettingsQuery.data.theme_preference,
|
||||
}}
|
||||
onSubmit={updateAppearanceSettingsMutation.mutateAsync}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { API } from "api/api";
|
||||
import { workspaceByOwnerAndName } from "api/queries/workspaces";
|
||||
import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar";
|
||||
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||
import dayjs from "dayjs";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import type { FC } from "react";
|
||||
import { QueryClient, QueryClientProvider, useQuery } from "react-query";
|
||||
import { RouterProvider, createMemoryRouter } from "react-router-dom";
|
||||
import { useQuery } from "react-query";
|
||||
import { MockTemplate, MockWorkspace } from "testHelpers/entities";
|
||||
import { render } from "testHelpers/renderHelpers";
|
||||
import { server } from "testHelpers/server";
|
||||
import { WorkspaceScheduleControls } from "./WorkspaceScheduleControls";
|
||||
|
||||
@@ -45,16 +43,7 @@ const renderScheduleControls = async () => {
|
||||
});
|
||||
}),
|
||||
);
|
||||
render(
|
||||
<ThemeProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<RouterProvider
|
||||
router={createMemoryRouter([{ path: "/", element: <Wrapper /> }])}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
<GlobalSnackbar />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
render(<Wrapper />);
|
||||
await screen.findByTestId("schedule-controls");
|
||||
expect(screen.getByText("Stop in 3 hours")).toBeInTheDocument();
|
||||
};
|
||||
|
||||
@@ -495,7 +495,6 @@ export const MockUser: TypesGen.User = {
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
|
||||
last_seen_at: "",
|
||||
login_type: "password",
|
||||
theme_preference: "",
|
||||
name: "",
|
||||
};
|
||||
|
||||
@@ -516,7 +515,6 @@ export const MockUser2: TypesGen.User = {
|
||||
avatar_url: "",
|
||||
last_seen_at: "2022-09-14T19:12:21Z",
|
||||
login_type: "oidc",
|
||||
theme_preference: "",
|
||||
name: "Mock User The Second",
|
||||
};
|
||||
|
||||
@@ -532,10 +530,13 @@ export const SuspendedMockUser: TypesGen.User = {
|
||||
avatar_url: "",
|
||||
last_seen_at: "",
|
||||
login_type: "password",
|
||||
theme_preference: "",
|
||||
name: "",
|
||||
};
|
||||
|
||||
export const MockUserAppearanceSettings: TypesGen.UserAppearanceSettings = {
|
||||
theme_preference: "dark",
|
||||
};
|
||||
|
||||
export const MockOrganizationMember: TypesGen.OrganizationMemberWithUserData = {
|
||||
organization_id: MockOrganization.id,
|
||||
user_id: MockUser.id,
|
||||
|
||||
@@ -162,6 +162,9 @@ export const handlers = [
|
||||
http.get("/api/v2/users/me", () => {
|
||||
return HttpResponse.json(M.MockUser);
|
||||
}),
|
||||
http.get("/api/v2/users/me/appearance", () => {
|
||||
return HttpResponse.json(M.MockUserAppearanceSettings);
|
||||
}),
|
||||
http.get("/api/v2/users/me/keys", () => {
|
||||
return HttpResponse.json(M.MockAPIKey);
|
||||
}),
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@testing-library/react";
|
||||
import { AppProviders } from "App";
|
||||
import type { ProxyProvider } from "contexts/ProxyContext";
|
||||
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||
import { ThemeOverride } from "contexts/ThemeProvider";
|
||||
import { RequireAuth } from "contexts/auth/RequireAuth";
|
||||
import { DashboardLayout } from "modules/dashboard/DashboardLayout";
|
||||
import type { DashboardProvider } from "modules/dashboard/DashboardProvider";
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
RouterProvider,
|
||||
createMemoryRouter,
|
||||
} from "react-router-dom";
|
||||
import themes, { DEFAULT_THEME } from "theme";
|
||||
import { MockUser } from "./entities";
|
||||
|
||||
export function createTestQueryClient() {
|
||||
@@ -245,6 +246,8 @@ export const waitForLoaderToBeRemoved = async (): Promise<void> => {
|
||||
|
||||
export const renderComponent = (component: React.ReactElement) => {
|
||||
return testingLibraryRender(component, {
|
||||
wrapper: ({ children }) => <ThemeProvider>{children}</ThemeProvider>,
|
||||
wrapper: ({ children }) => (
|
||||
<ThemeOverride theme={themes[DEFAULT_THEME]}>{children}</ThemeOverride>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user