diff --git a/site/src/api/queries/appearance.ts b/site/src/api/queries/appearance.ts index c81e01dbb9337..45a6c618c5dec 100644 --- a/site/src/api/queries/appearance.ts +++ b/site/src/api/queries/appearance.ts @@ -2,14 +2,17 @@ import type { QueryClient, UseQueryOptions } from "react-query"; import * as API from "api/api"; import type { AppearanceConfig } from "api/typesGenerated"; import { getMetadataAsJSON } from "utils/metadata"; +import { cachedQuery } from "./util"; const initialAppearanceData = getMetadataAsJSON("appearance"); const appearanceConfigKey = ["appearance"] as const; export const appearance = (): UseQueryOptions => { return { + // We either have our initial data or should immediately + // fetch and never again! + ...cachedQuery(initialAppearanceData), queryKey: ["appearance"], - initialData: initialAppearanceData, queryFn: () => API.getAppearance(), }; }; diff --git a/site/src/api/queries/buildInfo.ts b/site/src/api/queries/buildInfo.ts index b0761f001d6d6..aeed3ecd3d02b 100644 --- a/site/src/api/queries/buildInfo.ts +++ b/site/src/api/queries/buildInfo.ts @@ -2,14 +2,17 @@ import type { UseQueryOptions } from "react-query"; import * as API from "api/api"; import type { BuildInfoResponse } from "api/typesGenerated"; import { getMetadataAsJSON } from "utils/metadata"; +import { cachedQuery } from "./util"; const initialBuildInfoData = getMetadataAsJSON("build-info"); const buildInfoKey = ["buildInfo"] as const; export const buildInfo = (): UseQueryOptions => { return { + // We either have our initial data or should immediately + // fetch and never again! + ...cachedQuery(initialBuildInfoData), queryKey: buildInfoKey, - initialData: initialBuildInfoData, queryFn: () => API.getBuildInfo(), }; }; diff --git a/site/src/api/queries/entitlements.ts b/site/src/api/queries/entitlements.ts index d92b81cec6095..1a4c990fe2ad4 100644 --- a/site/src/api/queries/entitlements.ts +++ b/site/src/api/queries/entitlements.ts @@ -2,15 +2,16 @@ import type { QueryClient, UseQueryOptions } from "react-query"; import * as API from "api/api"; import type { Entitlements } from "api/typesGenerated"; import { getMetadataAsJSON } from "utils/metadata"; +import { cachedQuery } from "./util"; const initialEntitlementsData = getMetadataAsJSON("entitlements"); -const ENTITLEMENTS_QUERY_KEY = ["entitlements"] as const; +const entitlementsQueryKey = ["entitlements"] as const; export const entitlements = (): UseQueryOptions => { return { - queryKey: ENTITLEMENTS_QUERY_KEY, + ...cachedQuery(initialEntitlementsData), + queryKey: entitlementsQueryKey, queryFn: () => API.getEntitlements(), - initialData: initialEntitlementsData, }; }; @@ -19,7 +20,7 @@ export const refreshEntitlements = (queryClient: QueryClient) => { mutationFn: API.refreshEntitlements, onSuccess: async () => { await queryClient.invalidateQueries({ - queryKey: ENTITLEMENTS_QUERY_KEY, + queryKey: entitlementsQueryKey, }); }, }; diff --git a/site/src/api/queries/experiments.ts b/site/src/api/queries/experiments.ts index 1f7e04901ae59..bd7f436f5b9e6 100644 --- a/site/src/api/queries/experiments.ts +++ b/site/src/api/queries/experiments.ts @@ -2,14 +2,15 @@ import type { UseQueryOptions } from "react-query"; import * as API from "api/api"; import type { Experiments } from "api/typesGenerated"; import { getMetadataAsJSON } from "utils/metadata"; +import { cachedQuery } from "./util"; const initialExperimentsData = getMetadataAsJSON("experiments"); const experimentsKey = ["experiments"] as const; export const experiments = (): UseQueryOptions => { return { + ...cachedQuery(initialExperimentsData), queryKey: experimentsKey, - initialData: initialExperimentsData, queryFn: () => API.getExperiments(), } satisfies UseQueryOptions; }; diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts index 3aa0b009a2527..34609b47f5a20 100644 --- a/site/src/api/queries/users.ts +++ b/site/src/api/queries/users.ts @@ -19,6 +19,7 @@ import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery"; import { prepareQuery } from "utils/filters"; import { getMetadataAsJSON } from "utils/metadata"; import { getAuthorizationKey } from "./authCheck"; +import { cachedQuery } from "./util"; export function usersKey(req: UsersRequest) { return ["users", req] as const; @@ -112,6 +113,8 @@ export const updateRoles = (queryClient: QueryClient) => { }; }; +const initialUserData = getMetadataAsJSON("user"); + export const authMethods = () => { return { // Even the endpoint being /users/authmethods we don't want to revalidate it @@ -121,16 +124,14 @@ export const authMethods = () => { }; }; -const initialUserData = getMetadataAsJSON("user"); - const meKey = ["me"]; export const me = (): UseQueryOptions & { queryKey: QueryKey; } => { return { + ...cachedQuery(initialUserData), queryKey: meKey, - initialData: initialUserData, queryFn: API.getAuthenticatedUser, }; }; @@ -142,8 +143,10 @@ export function apiKey(): UseQueryOptions { }; } -export const hasFirstUser = () => { +export const hasFirstUser = (): UseQueryOptions => { return { + // This cannot be false otherwise it will not fetch! + ...cachedQuery(typeof initialUserData !== "undefined" ? true : undefined), queryKey: ["hasFirstUser"], queryFn: API.hasFirstUser, }; diff --git a/site/src/api/queries/util.ts b/site/src/api/queries/util.ts new file mode 100644 index 0000000000000..d5eb591c1ffd2 --- /dev/null +++ b/site/src/api/queries/util.ts @@ -0,0 +1,23 @@ +import type { UseQueryOptions } from "react-query"; + +// cachedQuery allows the caller to only make a request +// a single time, and use `initialData` if it is provided. +// +// This is particularly helpful for passing values injected +// via metadata. We do this for the initial user fetch, buildinfo, +// and a few others to reduce page load time. +export const cachedQuery = (initialData?: T): Partial> => + // Only do this if there is initial data, + // otherwise it can conflict with tests. + initialData + ? { + cacheTime: Infinity, + staleTime: Infinity, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + initialData, + } + : { + initialData, + }; diff --git a/site/src/contexts/ProxyContext.tsx b/site/src/contexts/ProxyContext.tsx index ae6637238e8b9..ff99d7b03c41d 100644 --- a/site/src/contexts/ProxyContext.tsx +++ b/site/src/contexts/ProxyContext.tsx @@ -7,8 +7,9 @@ import { useEffect, useState, } from "react"; -import { useQuery } from "react-query"; +import { type UseQueryOptions, useQuery } from "react-query"; import { getWorkspaceProxies, getWorkspaceProxyRegions } from "api/api"; +import { cachedQuery } from "api/queries/util"; import type { Region, WorkspaceProxy } from "api/typesGenerated"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { type ProxyLatencyReport, useProxyLatency } from "./useProxyLatency"; @@ -131,11 +132,10 @@ export const ProxyProvider: FC = ({ children }) => { isLoading: proxiesLoading, isFetched: proxiesFetched, } = useQuery({ + ...cachedQuery(initialData), queryKey, queryFn: query, - staleTime: initialData ? Infinity : undefined, - initialData, - }); + } as UseQueryOptions); // Every time we get a new proxiesResponse, update the latency check // to each workspace proxy. diff --git a/site/src/contexts/auth/AuthProvider.tsx b/site/src/contexts/auth/AuthProvider.tsx index 567c48e8fce38..f581971b1175a 100644 --- a/site/src/contexts/auth/AuthProvider.tsx +++ b/site/src/contexts/auth/AuthProvider.tsx @@ -9,18 +9,13 @@ import { useMutation, useQuery, useQueryClient } from "react-query"; import { isApiError } from "api/errors"; import { checkAuthorization } from "api/queries/authCheck"; import { - authMethods, hasFirstUser, login, logout, me, updateProfile as updateProfileOptions, } from "api/queries/users"; -import type { - AuthMethods, - UpdateUserProfileRequest, - User, -} from "api/typesGenerated"; +import type { UpdateUserProfileRequest, User } from "api/typesGenerated"; import { displaySuccess } from "components/GlobalSnackbar/utils"; import { permissionsToCheck, type Permissions } from "./permissions"; @@ -34,7 +29,6 @@ export type AuthContextValue = { isUpdatingProfile: boolean; user: User | undefined; permissions: Permissions | undefined; - authMethods: AuthMethods | undefined; organizationId: string | undefined; signInError: unknown; updateProfileError: unknown; @@ -51,7 +45,6 @@ export const AuthProvider: FC = ({ children }) => { const queryClient = useQueryClient(); const meOptions = me(); const userQuery = useQuery(meOptions); - const authMethodsQuery = useQuery(authMethods()); const hasFirstUserQuery = useQuery(hasFirstUser()); const permissionsQuery = useQuery({ ...checkAuthorization({ checks: permissionsToCheck }), @@ -77,7 +70,6 @@ export const AuthProvider: FC = ({ children }) => { userQuery.error.response.status === 401; const isSigningOut = logoutMutation.isLoading; const isLoading = - authMethodsQuery.isLoading || userQuery.isLoading || hasFirstUserQuery.isLoading || (userQuery.isSuccess && permissionsQuery.isLoading); @@ -120,7 +112,6 @@ export const AuthProvider: FC = ({ children }) => { updateProfile, user: userQuery.data, permissions: permissionsQuery.data as Permissions | undefined, - authMethods: authMethodsQuery.data, signInError: loginMutation.error, updateProfileError: updateProfileMutation.error, organizationId: userQuery.data?.organization_ids[0], diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 65f14e7c1273f..440bdd44f249d 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -235,6 +235,13 @@ const ProxyMenu: FC = ({ proxyContextValue }) => { return proxy.healthy && latency !== undefined && latency.at < refetchDate; }; + // This endpoint returns a 404 when not using enterprise. + // If we don't return null, then it looks like this is + // loading forever! + if (proxyContextValue.error) { + return null; + } + if (isLoading) { return ( { isConfiguringTheFirstUser, signIn, isSigningIn, - authMethods, signInError, } = useAuthContext(); + const authMethodsQuery = useQuery(authMethods()); const redirectTo = retrieveRedirect(location.search); const applicationName = getApplicationName(); const navigate = useNavigate(); @@ -60,9 +62,9 @@ export const LoginPage: FC = () => { Codestin Search App { await signIn(email, password); diff --git a/site/src/utils/metadata.ts b/site/src/utils/metadata.ts index 8b05747579746..723b2e508395e 100644 --- a/site/src/utils/metadata.ts +++ b/site/src/utils/metadata.ts @@ -1,10 +1,10 @@ export const getMetadataAsJSON = >( property: string, ): T | undefined => { - const appearance = document.querySelector(`meta[property=${property}]`); + const metadata = document.querySelector(`meta[property=${property}]`); - if (appearance) { - const rawContent = appearance.getAttribute("content"); + if (metadata) { + const rawContent = metadata.getAttribute("content"); if (rawContent) { try {