diff --git a/coderd/coderd.go b/coderd/coderd.go index c3c1fb09cc6cc..f3a84416eb653 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1267,6 +1267,9 @@ func New(options *Options) *API { }) r.Get("/appearance", api.userAppearanceSettings) r.Put("/appearance", api.putUserAppearanceSettings) + r.Get("/proxy", api.userProxySettings) + r.Put("/proxy", api.putUserProxySettings) + r.Delete("/proxy", api.deleteUserProxySettings) r.Route("/password", func(r chi.Router) { r.Use(httpmw.RateLimit(options.LoginRateLimit, time.Minute)) r.Put("/", api.putUserPassword) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 9af6e50764dfd..00b7ea098bf50 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -4884,6 +4884,39 @@ func (q *querier) UpdateUserThemePreference(ctx context.Context, arg database.Up return q.db.UpdateUserThemePreference(ctx, arg) } +func (q *querier) GetUserPreferredProxy(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.ActionRead, u); err != nil { + return "", err + } + return q.db.GetUserPreferredProxy(ctx, userID) +} + +func (q *querier) UpdateUserPreferredProxy(ctx context.Context, arg database.UpdateUserPreferredProxyParams) (database.UserConfig, error) { + u, err := q.db.GetUserByID(ctx, arg.UserID) + if err != nil { + return database.UserConfig{}, err + } + if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { + return database.UserConfig{}, err + } + return q.db.UpdateUserPreferredProxy(ctx, arg) +} + +func (q *querier) DeleteUserPreferredProxy(ctx context.Context, userID uuid.UUID) error { + u, err := q.db.GetUserByID(ctx, userID) + if err != nil { + return err + } + if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { + return err + } + return q.db.DeleteUserPreferredProxy(ctx, userID) +} + func (q *querier) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index c153974394650..14918e4bca1fa 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4896,6 +4896,24 @@ func (s *MethodTestSuite) TestNotifications() { }).Asserts(rbac.ResourceNotificationPreference.WithOwner(user.ID.String()), policy.ActionUpdate) })) + s.Run("GetUserPreferredProxy", s.Subtest(func(db database.Store, check *expects) { + user := dbgen.User(s.T(), db, database.User{}) + check.Args(user.ID). + Asserts(rbac.ResourceUser.WithOwner(user.ID.String()), policy.ActionReadPersonal) + })) + s.Run("UpdateUserPreferredProxy", s.Subtest(func(db database.Store, check *expects) { + user := dbgen.User(s.T(), db, database.User{}) + check.Args(database.UpdateUserPreferredProxyParams{ + UserID: user.ID, + PreferredProxy: "proxy1", + }).Asserts(rbac.ResourceUser.WithOwner(user.ID.String()), policy.ActionUpdatePersonal) + })) + s.Run("DeleteUserPreferredProxy", s.Subtest(func(db database.Store, check *expects) { + user := dbgen.User(s.T(), db, database.User{}) + check.Args(user.ID). + Asserts(rbac.ResourceUser.WithOwner(user.ID.String()), policy.ActionUpdatePersonal) + })) + s.Run("GetInboxNotificationsByUserID", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 7a7c3cb2d41c6..4f0f48adf99b6 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -3001,6 +3001,27 @@ func (m queryMetricsStore) UpdateUserThemePreference(ctx context.Context, arg da return r0, r1 } +func (m queryMetricsStore) GetUserPreferredProxy(ctx context.Context, userID uuid.UUID) (string, error) { + start := time.Now() + r0, r1 := m.s.GetUserPreferredProxy(ctx, userID) + m.queryLatencies.WithLabelValues("GetUserPreferredProxy").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) UpdateUserPreferredProxy(ctx context.Context, arg database.UpdateUserPreferredProxyParams) (database.UserConfig, error) { + start := time.Now() + r0, r1 := m.s.UpdateUserPreferredProxy(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateUserPreferredProxy").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) DeleteUserPreferredProxy(ctx context.Context, userID uuid.UUID) error { + start := time.Now() + r0 := m.s.DeleteUserPreferredProxy(ctx, userID) + m.queryLatencies.WithLabelValues("DeleteUserPreferredProxy").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { start := time.Now() r0 := m.s.UpdateVolumeResourceMonitor(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index fba3deb45e4be..f261ed097cec7 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -6404,6 +6404,50 @@ func (mr *MockStoreMockRecorder) UpdateUserThemePreference(ctx, arg any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserThemePreference", reflect.TypeOf((*MockStore)(nil).UpdateUserThemePreference), ctx, arg) } +// GetUserPreferredProxy mocks base method. +func (m *MockStore) GetUserPreferredProxy(ctx context.Context, userID uuid.UUID) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserPreferredProxy", ctx, userID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserPreferredProxy indicates an expected call of GetUserPreferredProxy. +func (mr *MockStoreMockRecorder) GetUserPreferredProxy(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserPreferredProxy", reflect.TypeOf((*MockStore)(nil).GetUserPreferredProxy), ctx, userID) +} + +// UpdateUserPreferredProxy mocks base method. +func (m *MockStore) UpdateUserPreferredProxy(ctx context.Context, arg database.UpdateUserPreferredProxyParams) (database.UserConfig, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserPreferredProxy", ctx, arg) + ret0, _ := ret[0].(database.UserConfig) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateUserPreferredProxy indicates an expected call of UpdateUserPreferredProxy. +func (mr *MockStoreMockRecorder) UpdateUserPreferredProxy(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserPreferredProxy", reflect.TypeOf((*MockStore)(nil).UpdateUserPreferredProxy), ctx, arg) +} + +// DeleteUserPreferredProxy mocks base method. +func (m *MockStore) DeleteUserPreferredProxy(ctx context.Context, userID uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteUserPreferredProxy", ctx, userID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteUserPreferredProxy indicates an expected call of DeleteUserPreferredProxy. +func (mr *MockStoreMockRecorder) DeleteUserPreferredProxy(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserPreferredProxy", reflect.TypeOf((*MockStore)(nil).DeleteUserPreferredProxy), ctx, userID) +} + // UpdateVolumeResourceMonitor mocks base method. func (m *MockStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 24893a9197815..51eadf7a4f276 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -623,6 +623,9 @@ type sqlcQuerier interface { UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error) UpdateUserThemePreference(ctx context.Context, arg UpdateUserThemePreferenceParams) (UserConfig, error) + GetUserPreferredProxy(ctx context.Context, userID uuid.UUID) (string, error) + UpdateUserPreferredProxy(ctx context.Context, arg UpdateUserPreferredProxyParams) (UserConfig, error) + DeleteUserPreferredProxy(ctx context.Context, userID uuid.UUID) error UpdateVolumeResourceMonitor(ctx context.Context, arg UpdateVolumeResourceMonitorParams) error UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (WorkspaceTable, error) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 0ef4553149465..38eed4a44a099 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -21154,3 +21154,58 @@ func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg Insert } return items, nil } + +const getUserPreferredProxy = `-- name: GetUserPreferredProxy :one +SELECT + value as preferred_proxy +FROM + user_configs +WHERE + user_id = $1 + AND key = 'preferred_proxy' +` + +func (q *sqlQuerier) GetUserPreferredProxy(ctx context.Context, userID uuid.UUID) (string, error) { + row := q.db.QueryRowContext(ctx, getUserPreferredProxy, userID) + var preferredProxy string + err := row.Scan(&preferredProxy) + return preferredProxy, err +} + +const updateUserPreferredProxy = `-- name: UpdateUserPreferredProxy :one +INSERT INTO + user_configs (user_id, key, value) +VALUES + ($1, 'preferred_proxy', $2) +ON CONFLICT + ON CONSTRAINT user_configs_pkey +DO UPDATE +SET + value = $2 +WHERE user_configs.user_id = $1 + AND user_configs.key = 'preferred_proxy' +RETURNING user_id, key, value +` + +type UpdateUserPreferredProxyParams struct { + UserID uuid.UUID `db:"user_id" json:"user_id"` + PreferredProxy string `db:"preferred_proxy" json:"preferred_proxy"` +} + +func (q *sqlQuerier) UpdateUserPreferredProxy(ctx context.Context, arg UpdateUserPreferredProxyParams) (UserConfig, error) { + row := q.db.QueryRowContext(ctx, updateUserPreferredProxy, arg.UserID, arg.PreferredProxy) + var i UserConfig + err := row.Scan(&i.UserID, &i.Key, &i.Value) + return i, err +} + +const deleteUserPreferredProxy = `-- name: DeleteUserPreferredProxy :exec +DELETE FROM user_configs +WHERE user_id = $1 + AND key = 'preferred_proxy' +` + +func (q *sqlQuerier) DeleteUserPreferredProxy(ctx context.Context, userID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteUserPreferredProxy, userID) + return err +} diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index eece2f96512ea..d2476fa8a5e64 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -148,6 +148,34 @@ WHERE user_configs.user_id = @user_id AND user_configs.key = 'terminal_font' RETURNING *; +-- name: GetUserPreferredProxy :one +SELECT + value as preferred_proxy +FROM + user_configs +WHERE + user_id = @user_id + AND key = 'preferred_proxy'; + +-- name: UpdateUserPreferredProxy :one +INSERT INTO + user_configs (user_id, key, value) +VALUES + (@user_id, 'preferred_proxy', @preferred_proxy) +ON CONFLICT + ON CONSTRAINT user_configs_pkey +DO UPDATE +SET + value = @preferred_proxy +WHERE user_configs.user_id = @user_id + AND user_configs.key = 'preferred_proxy' +RETURNING *; + +-- name: DeleteUserPreferredProxy :exec +DELETE FROM user_configs +WHERE user_id = @user_id + AND key = 'preferred_proxy'; + -- name: UpdateUserRoles :one UPDATE users diff --git a/coderd/users.go b/coderd/users.go index 7fbb8e7d04cdf..ac04b1d3b4a39 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -1066,6 +1066,104 @@ func (api *API) putUserAppearanceSettings(rw http.ResponseWriter, r *http.Reques }) } +// @Summary Get user proxy settings +// @ID get-user-proxy-settings +// @Security CoderSessionToken +// @Produce json +// @Tags Users +// @Param user path string true "User ID, name, or me" +// @Success 200 {object} codersdk.UserProxySettings +// @Router /users/{user}/proxy [get] +func (api *API) userProxySettings(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + user = httpmw.UserParam(r) + ) + + preferredProxy, err := api.Database.GetUserPreferredProxy(ctx, user.ID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ + Message: "User proxy settings not found.", + }) + return + } + + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Error reading user proxy settings.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserProxySettings{ + PreferredProxy: preferredProxy, + }) +} + +// @Summary Update user proxy settings +// @ID update-user-proxy-settings +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Users +// @Param user path string true "User ID, name, or me" +// @Param request body codersdk.UpdateUserProxySettingsRequest true "New proxy settings" +// @Success 200 {object} codersdk.UserProxySettings +// @Router /users/{user}/proxy [put] +func (api *API) putUserProxySettings(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + user = httpmw.UserParam(r) + ) + + var params codersdk.UpdateUserProxySettingsRequest + if !httpapi.Read(ctx, rw, r, ¶ms) { + return + } + + updatedPreferredProxy, err := api.Database.UpdateUserPreferredProxy(ctx, database.UpdateUserPreferredProxyParams{ + UserID: user.ID, + PreferredProxy: params.PreferredProxy, + }) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error updating user proxy preference.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserProxySettings{ + PreferredProxy: updatedPreferredProxy.Value, + }) +} + +// @Summary Delete user proxy settings +// @ID delete-user-proxy-settings +// @Security CoderSessionToken +// @Tags Users +// @Param user path string true "User ID, name, or me" +// @Success 204 +// @Router /users/{user}/proxy [delete] +func (api *API) deleteUserProxySettings(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + user = httpmw.UserParam(r) + ) + + err := api.Database.DeleteUserPreferredProxy(ctx, user.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error deleting user proxy preference.", + Detail: err.Error(), + }) + return + } + + rw.WriteHeader(http.StatusNoContent) +} + func isValidFontName(font codersdk.TerminalFontName) bool { return slices.Contains(codersdk.TerminalFontNames, font) } diff --git a/coderd/users_test.go b/coderd/users_test.go index 9d695f37c9906..9baebfdebc15c 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -2630,3 +2630,54 @@ func BenchmarkUsersMe(b *testing.B) { require.NoError(b, err) } } + +func TestUserPreferredProxy(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Test getting preferred proxy when none is set + _, err := client.GetUserProxySettings(ctx, user.UserID.String()) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + + // Test setting preferred proxy + proxy := "proxy1" + _, err = client.UpdateUserProxySettings(ctx, user.UserID.String(), codersdk.UpdateUserProxySettingsRequest{ + PreferredProxy: proxy, + }) + require.NoError(t, err) + + // Test getting preferred proxy + resp, err := client.GetUserProxySettings(ctx, user.UserID.String()) + require.NoError(t, err) + require.Equal(t, proxy, resp.PreferredProxy) + + // Test updating preferred proxy + newProxy := "proxy2" + _, err = client.UpdateUserProxySettings(ctx, user.UserID.String(), codersdk.UpdateUserProxySettingsRequest{ + PreferredProxy: newProxy, + }) + require.NoError(t, err) + + // Verify the update + resp, err = client.GetUserProxySettings(ctx, user.UserID.String()) + require.NoError(t, err) + require.Equal(t, newProxy, resp.PreferredProxy) + + // Test deleting preferred proxy + err = client.DeleteUserProxySettings(ctx, user.UserID.String()) + require.NoError(t, err) + + // Verify deletion + _, err = client.GetUserProxySettings(ctx, user.UserID.String()) + require.Error(t, err) + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) +} diff --git a/codersdk/users.go b/codersdk/users.go index f65223a666a62..efa8411a368c3 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -216,6 +216,14 @@ type UpdateUserAppearanceSettingsRequest struct { TerminalFont TerminalFontName `json:"terminal_font" validate:"required"` } +type UserProxySettings struct { + PreferredProxy string `json:"preferred_proxy"` +} + +type UpdateUserProxySettingsRequest struct { + PreferredProxy string `json:"preferred_proxy" validate:"required"` +} + type UpdateUserPasswordRequest struct { OldPassword string `json:"old_password" validate:""` Password string `json:"password" validate:"required"` @@ -513,6 +521,47 @@ func (c *Client) UpdateUserAppearanceSettings(ctx context.Context, user string, return resp, json.NewDecoder(res.Body).Decode(&resp) } +// GetUserProxySettings fetches the proxy settings for a user. +func (c *Client) GetUserProxySettings(ctx context.Context, user string) (UserProxySettings, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/proxy", user), nil) + if err != nil { + return UserProxySettings{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return UserProxySettings{}, ReadBodyAsError(res) + } + var resp UserProxySettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// UpdateUserProxySettings updates the proxy settings for a user. +func (c *Client) UpdateUserProxySettings(ctx context.Context, user string, req UpdateUserProxySettingsRequest) (UserProxySettings, error) { + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/proxy", user), req) + if err != nil { + return UserProxySettings{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return UserProxySettings{}, ReadBodyAsError(res) + } + var resp UserProxySettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + +// DeleteUserProxySettings clears the proxy settings for a user. +func (c *Client) DeleteUserProxySettings(ctx context.Context, user string) error { + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/users/%s/proxy", user), nil) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusNoContent { + return ReadBodyAsError(res) + } + return nil +} + // UpdateUserPassword updates a user password. // It calls PUT /users/{user}/password func (c *Client) UpdateUserPassword(ctx context.Context, user string, req UpdateUserPasswordRequest) error { diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 013c018d5c656..6da84e73fb745 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1454,6 +1454,22 @@ class ApiMethods { return response.data; }; + getProxySettings = async (): Promise => { + const response = await this.axios.get("/api/v2/users/me/proxy"); + return response.data; + }; + + updateProxySettings = async ( + data: TypesGen.UpdateUserProxySettingsRequest, + ): Promise => { + const response = await this.axios.put("/api/v2/users/me/proxy", data); + return response.data; + }; + + deleteProxySettings = async (): Promise => { + await this.axios.delete("/api/v2/users/me/proxy"); + }; + getUserQuietHoursSchedule = async ( userId: TypesGen.User["id"], ): Promise => { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b4df5654824bc..d8074a8841a61 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3183,6 +3183,11 @@ export interface UpdateUserProfileRequest { readonly name: string; } +// From codersdk/users.go +export interface UpdateUserProxySettingsRequest { + readonly preferred_proxy: string; +} + // From codersdk/users.go export interface UpdateUserQuietHoursScheduleRequest { readonly schedule: string; @@ -3330,6 +3335,11 @@ export interface UserParameter { readonly value: string; } +// From codersdk/users.go +export interface UserProxySettings { + readonly preferred_proxy: string; +} + // From codersdk/deployment.go export interface UserQuietHoursScheduleConfig { readonly default_schedule: string; diff --git a/site/src/contexts/ProxyContext.tsx b/site/src/contexts/ProxyContext.tsx index c162c2c4952ff..df3d516e2e43d 100644 --- a/site/src/contexts/ProxyContext.tsx +++ b/site/src/contexts/ProxyContext.tsx @@ -12,7 +12,8 @@ import { useEffect, useState, } from "react"; -import { useQuery } from "react-query"; +import { useQuery, useQueryClient } from "react-query"; + import { type ProxyLatencyReport, useProxyLatency } from "./useProxyLatency"; export type Proxies = readonly Region[] | readonly WorkspaceProxy[]; @@ -20,7 +21,6 @@ export type ProxyLatencies = Record; export interface ProxyContextValue { // proxy is **always** the workspace proxy that should be used. // The 'proxy.selectedProxy' field is the proxy being used and comes from either: - // 1. The user manually selected this proxy. (saved to local storage) // 2. The default proxy auto selected because: // a. The user has not selected a proxy. // b. The user's selected proxy is not in the list of proxies. @@ -72,8 +72,7 @@ export interface ProxyContextValue { interface PreferredProxy { // proxy is the proxy being used. It is provided for // getting the fields such as "display_name" and "id" - // Do not use the fields 'path_app_url' or 'wildcard_hostname' from this - // object. Use the preferred fields. + // Do not use the fields 'path_app_url' or 'wildcard_hostname' from this // object. Use the preferred fields. proxy: Region | undefined; // PreferredPathAppURL is the URL of the proxy or it is the empty string // to indicate using relative paths. To add a path to this: @@ -91,13 +90,16 @@ export const ProxyContext = createContext( * ProxyProvider interacts with local storage to indicate the preferred workspace proxy. */ export const ProxyProvider: FC = ({ children }) => { + const queryClient = useQueryClient(); + // Using a useState so the caller always has the latest user saved // proxy. const [userSavedProxy, setUserSavedProxy] = useState(loadUserSelectedProxy()); - // Load the initial state from local storage. + // Load the initial state from localStorage only + // Let useEffect handle proper initialization when data is available const [proxy, setProxy] = useState( - computeUsableURLS(userSavedProxy), + computeUsableURLS(loadUserSelectedProxy()), ); const { permissions } = useAuthenticated(); @@ -134,8 +136,6 @@ export const ProxyProvider: FC = ({ children }) => { // updateProxy is a helper function that when called will // update the proxy being used. const updateProxy = useCallback(() => { - // Update the saved user proxy for the caller. - setUserSavedProxy(loadUserSelectedProxy()); setProxy( getPreferredProxy( proxiesResp ?? [], @@ -180,6 +180,7 @@ export const ProxyProvider: FC = ({ children }) => { if (best?.proxy) { saveUserSelectedProxy(best.proxy); + setUserSavedProxy(best.proxy); updateProxy(); } }, [latenciesLoaded, proxiesResp, proxyLatencies]); @@ -199,14 +200,16 @@ export const ProxyProvider: FC = ({ children }) => { // These functions are exposed to allow the user to select a proxy. setProxy: (proxy: Region) => { - // Save to local storage to persist the user's preference across reloads + // Save to localStorage saveUserSelectedProxy(proxy); + setUserSavedProxy(proxy); // Update the selected proxy updateProxy(); }, clearProxy: () => { - // Clear the user's selection from local storage. + // Clear from localStorage clearUserSelectedProxy(); + setUserSavedProxy(undefined); updateProxy(); }, }} pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy