mirror of https://github.com/coder/coder.git
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:
parent
8d1220e0c8
commit
d3f3ace220
|
@ -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(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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.
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue