diff --git a/site/src/api/queries/appearance.ts b/site/src/api/queries/appearance.ts index aaab8290e5eba..1e891deb59619 100644 --- a/site/src/api/queries/appearance.ts +++ b/site/src/api/queries/appearance.ts @@ -1,21 +1,30 @@ -import { QueryClient } from "react-query"; +import { QueryClient, type UseQueryOptions } from "react-query"; import * as API from "api/api"; -import { AppearanceConfig } from "api/typesGenerated"; +import { type AppearanceConfig } from "api/typesGenerated"; import { getMetadataAsJSON } from "utils/metadata"; -export const appearance = () => { +const initialAppearanceData = getMetadataAsJSON("appearance"); +const appearanceConfigKey = ["appearance"] as const; + +export const appearance = (queryClient: QueryClient) => { return { - queryKey: ["appearance"], - queryFn: async () => - getMetadataAsJSON("appearance") ?? API.getAppearance(), - }; + queryKey: appearanceConfigKey, + queryFn: async () => { + const cachedData = queryClient.getQueryData(appearanceConfigKey); + if (cachedData === undefined && initialAppearanceData !== undefined) { + return initialAppearanceData; + } + + return API.getAppearance(); + }, + } satisfies UseQueryOptions; }; export const updateAppearance = (queryClient: QueryClient) => { return { mutationFn: API.updateAppearance, onSuccess: (newConfig: AppearanceConfig) => { - queryClient.setQueryData(["appearance"], newConfig); + queryClient.setQueryData(appearanceConfigKey, newConfig); }, }; }; diff --git a/site/src/api/queries/buildInfo.ts b/site/src/api/queries/buildInfo.ts index af90ffacef0be..c10b458a67fcf 100644 --- a/site/src/api/queries/buildInfo.ts +++ b/site/src/api/queries/buildInfo.ts @@ -1,11 +1,21 @@ +import { QueryClient, type UseQueryOptions } from "react-query"; +import { type BuildInfoResponse } from "api/typesGenerated"; import * as API from "api/api"; -import { BuildInfoResponse } from "api/typesGenerated"; import { getMetadataAsJSON } from "utils/metadata"; -export const buildInfo = () => { +const initialBuildInfoData = getMetadataAsJSON("build-info"); +const buildInfoKey = ["buildInfo"] as const; + +export const buildInfo = (queryClient: QueryClient) => { return { - queryKey: ["buildInfo"], - queryFn: async () => - getMetadataAsJSON("build-info") ?? API.getBuildInfo(), - }; + queryKey: buildInfoKey, + queryFn: async () => { + const cachedData = queryClient.getQueryData(buildInfoKey); + if (cachedData === undefined && initialBuildInfoData !== undefined) { + return initialBuildInfoData; + } + + return API.getBuildInfo(); + }, + } satisfies UseQueryOptions; }; diff --git a/site/src/api/queries/experiments.ts b/site/src/api/queries/experiments.ts index cc6a2a067fa1d..27126696a7313 100644 --- a/site/src/api/queries/experiments.ts +++ b/site/src/api/queries/experiments.ts @@ -1,11 +1,21 @@ import * as API from "api/api"; -import { Experiments } from "api/typesGenerated"; import { getMetadataAsJSON } from "utils/metadata"; +import { type Experiments } from "api/typesGenerated"; +import { QueryClient, type UseQueryOptions } from "react-query"; -export const experiments = () => { +const initialExperimentsData = getMetadataAsJSON("experiments"); +const experimentsKey = ["experiments"] as const; + +export const experiments = (queryClient: QueryClient) => { return { - queryKey: ["experiments"], - queryFn: async () => - getMetadataAsJSON("experiments") ?? API.getExperiments(), - }; + queryKey: experimentsKey, + queryFn: async () => { + const cachedData = queryClient.getQueryData(experimentsKey); + if (cachedData === undefined && initialExperimentsData !== undefined) { + return initialExperimentsData; + } + + return API.getExperiments(); + }, + } satisfies UseQueryOptions; }; diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts index f39b3629757cd..2d3f1dbc58c2e 100644 --- a/site/src/api/queries/users.ts +++ b/site/src/api/queries/users.ts @@ -1,4 +1,4 @@ -import { QueryClient, QueryOptions } from "react-query"; +import { QueryClient, type UseQueryOptions } from "react-query"; import * as API from "api/api"; import { AuthorizationRequest, @@ -11,7 +11,7 @@ import { import { getMetadataAsJSON } from "utils/metadata"; import { getAuthorizationKey } from "./authCheck"; -export const users = (req: UsersRequest): QueryOptions => { +export const users = (req: UsersRequest): UseQueryOptions => { return { queryKey: ["users", req], queryFn: ({ signal }) => API.getUsers(req, signal), @@ -89,12 +89,21 @@ export const authMethods = () => { }; }; -export const me = () => { +const initialMeData = getMetadataAsJSON("user"); +const meKey = ["me"] as const; + +export const me = (queryClient: QueryClient) => { return { - queryKey: ["me"], - queryFn: async () => - getMetadataAsJSON("user") ?? API.getAuthenticatedUser(), - }; + queryKey: meKey, + queryFn: async () => { + const cachedData = queryClient.getQueryData(meKey); + if (cachedData === undefined && initialMeData !== undefined) { + return initialMeData; + } + + return API.getAuthenticatedUser(); + }, + } satisfies UseQueryOptions; }; export const hasFirstUser = () => { diff --git a/site/src/components/AuthProvider/AuthProvider.tsx b/site/src/components/AuthProvider/AuthProvider.tsx index cdcba35660546..c62bd72d27a0e 100644 --- a/site/src/components/AuthProvider/AuthProvider.tsx +++ b/site/src/components/AuthProvider/AuthProvider.tsx @@ -45,7 +45,9 @@ type AuthContextValue = { const AuthContext = createContext(undefined); export const AuthProvider: FC = ({ children }) => { - const meOptions = me(); + const queryClient = useQueryClient(); + const meOptions = me(queryClient); + const userQuery = useQuery(meOptions); const authMethodsQuery = useQuery(authMethods()); const hasFirstUserQuery = useQuery(hasFirstUser()); @@ -54,7 +56,6 @@ export const AuthProvider: FC = ({ children }) => { enabled: userQuery.data !== undefined, }); - const queryClient = useQueryClient(); const loginMutation = useMutation( login({ checks: permissionsToCheck }, queryClient), ); diff --git a/site/src/components/Dashboard/DashboardProvider.tsx b/site/src/components/Dashboard/DashboardProvider.tsx index 9e5a4b0b80f81..a0bd176178a1a 100644 --- a/site/src/components/Dashboard/DashboardProvider.tsx +++ b/site/src/components/Dashboard/DashboardProvider.tsx @@ -1,4 +1,4 @@ -import { useQuery } from "react-query"; +import { useQuery, useQueryClient } from "react-query"; import { buildInfo } from "api/queries/buildInfo"; import { experiments } from "api/queries/experiments"; import { entitlements } from "api/queries/entitlements"; @@ -30,8 +30,8 @@ interface Appearance { interface DashboardProviderValue { buildInfo: BuildInfoResponse; entitlements: Entitlements; - appearance: Appearance; experiments: Experiments; + appearance: Appearance; } export const DashboardProviderContext = createContext< @@ -39,10 +39,12 @@ export const DashboardProviderContext = createContext< >(undefined); export const DashboardProvider: FC = ({ children }) => { - const buildInfoQuery = useQuery(buildInfo()); + const queryClient = useQueryClient(); + const buildInfoQuery = useQuery(buildInfo(queryClient)); const entitlementsQuery = useQuery(entitlements()); - const experimentsQuery = useQuery(experiments()); - const appearanceQuery = useQuery(appearance()); + const experimentsQuery = useQuery(experiments(queryClient)); + const appearanceQuery = useQuery(appearance(queryClient)); + const isLoading = !buildInfoQuery.data || !entitlementsQuery.data || diff --git a/site/src/theme/colors.ts b/site/src/theme/colors.ts index 139080edd556e..6f0e870ae1631 100644 --- a/site/src/theme/colors.ts +++ b/site/src/theme/colors.ts @@ -6,7 +6,8 @@ import { getMetadataAsJSON } from "utils/metadata"; // so you can just set this to true. export const experimentalTheme = typeof document !== "undefined" && - getMetadataAsJSON("experiments")?.includes("dashboard_theme"); + (getMetadataAsJSON("experiments")?.includes("dashboard_theme") ?? + false); export const colors = { white: "hsl(0, 0%, 100%)", diff --git a/site/src/utils/metadata.ts b/site/src/utils/metadata.ts index 56c2a8323839f..8b05747579746 100644 --- a/site/src/utils/metadata.ts +++ b/site/src/utils/metadata.ts @@ -1,18 +1,24 @@ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- It can be any -export const getMetadataAsJSON = >( +export const getMetadataAsJSON = >( property: string, ): T | undefined => { const appearance = document.querySelector(`meta[property=${property}]`); + if (appearance) { const rawContent = appearance.getAttribute("content"); - try { - return JSON.parse(rawContent as string); - } catch (ex) { - // In development the metadata is always going to be empty throwing this - // error - if (process.env.NODE_ENV === "production") { - console.warn(`Failed to parse ${property} metadata`); + + if (rawContent) { + try { + return JSON.parse(rawContent); + } catch (err) { + // In development, the metadata is always going to be empty; error is + // only a concern for production + if (process.env.NODE_ENV === "production") { + console.warn(`Failed to parse ${property} metadata. Error message:`); + console.warn(err); + } } } } + + return undefined; };