chore: reduce dashboard requests from seeded data (#13034)

* chore: reduce requests the dashboard makes from seeded data

We already inject all of this content in `index.html`.

There was also a bug with displaying a loading indicator when
the workspace proxies endpoint 404s.

* Fix first user fetch

* Add util

* Add cached query for entitlements and experiments

* Fix authmethods unnecessary request

* Fix unnecessary region request

* Fix fmt

* Debug

* Fix test
This commit is contained in:
Kyle Carberry 2024-04-22 16:07:56 -04:00 committed by GitHub
parent 8d1220e0c8
commit d3f3ace220
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 65 additions and 31 deletions

View File

@ -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<AppearanceConfig>("appearance");
const appearanceConfigKey = ["appearance"] as const;
export const appearance = (): UseQueryOptions<AppearanceConfig> => {
return {
// We either have our initial data or should immediately
// fetch and never again!
...cachedQuery(initialAppearanceData),
queryKey: ["appearance"],
initialData: initialAppearanceData,
queryFn: () => API.getAppearance(),
};
};

View File

@ -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<BuildInfoResponse>("build-info");
const buildInfoKey = ["buildInfo"] as const;
export const buildInfo = (): UseQueryOptions<BuildInfoResponse> => {
return {
// We either have our initial data or should immediately
// fetch and never again!
...cachedQuery(initialBuildInfoData),
queryKey: buildInfoKey,
initialData: initialBuildInfoData,
queryFn: () => API.getBuildInfo(),
};
};

View File

@ -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>("entitlements");
const ENTITLEMENTS_QUERY_KEY = ["entitlements"] as const;
const entitlementsQueryKey = ["entitlements"] as const;
export const entitlements = (): UseQueryOptions<Entitlements> => {
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,
});
},
};

View File

@ -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>("experiments");
const experimentsKey = ["experiments"] as const;
export const experiments = (): UseQueryOptions<Experiments> => {
return {
...cachedQuery(initialExperimentsData),
queryKey: experimentsKey,
initialData: initialExperimentsData,
queryFn: () => API.getExperiments(),
} satisfies UseQueryOptions<Experiments>;
};

View File

@ -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>("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>("user");
const meKey = ["me"];
export const me = (): UseQueryOptions<User> & {
queryKey: QueryKey;
} => {
return {
...cachedQuery(initialUserData),
queryKey: meKey,
initialData: initialUserData,
queryFn: API.getAuthenticatedUser,
};
};
@ -142,8 +143,10 @@ export function apiKey(): UseQueryOptions<GenerateAPIKeyResponse> {
};
}
export const hasFirstUser = () => {
export const hasFirstUser = (): UseQueryOptions<boolean> => {
return {
// This cannot be false otherwise it will not fetch!
...cachedQuery(typeof initialUserData !== "undefined" ? true : undefined),
queryKey: ["hasFirstUser"],
queryFn: API.hasFirstUser,
};

View File

@ -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 = <T>(initialData?: T): Partial<UseQueryOptions<T>> =>
// 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,
};

View File

@ -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<PropsWithChildren> = ({ children }) => {
isLoading: proxiesLoading,
isFetched: proxiesFetched,
} = useQuery({
...cachedQuery(initialData),
queryKey,
queryFn: query,
staleTime: initialData ? Infinity : undefined,
initialData,
});
} as UseQueryOptions<readonly Region[]>);
// Every time we get a new proxiesResponse, update the latency check
// to each workspace proxy.

View File

@ -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<PropsWithChildren> = ({ 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<PropsWithChildren> = ({ 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<PropsWithChildren> = ({ 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],

View File

@ -235,6 +235,13 @@ const ProxyMenu: FC<ProxyMenuProps> = ({ 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 (
<Skeleton

View File

@ -1,6 +1,8 @@
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useQuery } from "react-query";
import { Navigate, useLocation, useNavigate } from "react-router-dom";
import { authMethods } from "api/queries/users";
import { useAuthContext } from "contexts/auth/AuthProvider";
import { getApplicationName } from "utils/appearance";
import { retrieveRedirect } from "utils/redirect";
@ -14,9 +16,9 @@ export const LoginPage: FC = () => {
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 = () => {
<title>Sign in to {applicationName}</title>
</Helmet>
<LoginPageView
authMethods={authMethods}
authMethods={authMethodsQuery.data}
error={signInError}
isLoading={isLoading}
isLoading={isLoading || authMethodsQuery.isLoading}
isSigningIn={isSigningIn}
onSignIn={async ({ email, password }) => {
await signIn(email, password);

View File

@ -1,10 +1,10 @@
export const getMetadataAsJSON = <T extends NonNullable<unknown>>(
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 {