mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: prevent metadata queries from short-circuiting (#10312)
* fix: prevent metadata queries from short-circuiting * fix: use correct type definitions
This commit is contained in:
@@ -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<AppearanceConfig>("appearance");
|
||||
const appearanceConfigKey = ["appearance"] as const;
|
||||
|
||||
export const appearance = (queryClient: QueryClient) => {
|
||||
return {
|
||||
queryKey: ["appearance"],
|
||||
queryFn: async () =>
|
||||
getMetadataAsJSON<AppearanceConfig>("appearance") ?? API.getAppearance(),
|
||||
};
|
||||
queryKey: appearanceConfigKey,
|
||||
queryFn: async () => {
|
||||
const cachedData = queryClient.getQueryData(appearanceConfigKey);
|
||||
if (cachedData === undefined && initialAppearanceData !== undefined) {
|
||||
return initialAppearanceData;
|
||||
}
|
||||
|
||||
return API.getAppearance();
|
||||
},
|
||||
} satisfies UseQueryOptions<AppearanceConfig>;
|
||||
};
|
||||
|
||||
export const updateAppearance = (queryClient: QueryClient) => {
|
||||
return {
|
||||
mutationFn: API.updateAppearance,
|
||||
onSuccess: (newConfig: AppearanceConfig) => {
|
||||
queryClient.setQueryData(["appearance"], newConfig);
|
||||
queryClient.setQueryData(appearanceConfigKey, newConfig);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<BuildInfoResponse>("build-info");
|
||||
const buildInfoKey = ["buildInfo"] as const;
|
||||
|
||||
export const buildInfo = (queryClient: QueryClient) => {
|
||||
return {
|
||||
queryKey: ["buildInfo"],
|
||||
queryFn: async () =>
|
||||
getMetadataAsJSON<BuildInfoResponse>("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<BuildInfoResponse>;
|
||||
};
|
||||
|
||||
@@ -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>("experiments");
|
||||
const experimentsKey = ["experiments"] as const;
|
||||
|
||||
export const experiments = (queryClient: QueryClient) => {
|
||||
return {
|
||||
queryKey: ["experiments"],
|
||||
queryFn: async () =>
|
||||
getMetadataAsJSON<Experiments>("experiments") ?? API.getExperiments(),
|
||||
};
|
||||
queryKey: experimentsKey,
|
||||
queryFn: async () => {
|
||||
const cachedData = queryClient.getQueryData(experimentsKey);
|
||||
if (cachedData === undefined && initialExperimentsData !== undefined) {
|
||||
return initialExperimentsData;
|
||||
}
|
||||
|
||||
return API.getExperiments();
|
||||
},
|
||||
} satisfies UseQueryOptions<Experiments>;
|
||||
};
|
||||
|
||||
@@ -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<GetUsersResponse> => {
|
||||
export const users = (req: UsersRequest): UseQueryOptions<GetUsersResponse> => {
|
||||
return {
|
||||
queryKey: ["users", req],
|
||||
queryFn: ({ signal }) => API.getUsers(req, signal),
|
||||
@@ -89,12 +89,21 @@ export const authMethods = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const me = () => {
|
||||
const initialMeData = getMetadataAsJSON<User>("user");
|
||||
const meKey = ["me"] as const;
|
||||
|
||||
export const me = (queryClient: QueryClient) => {
|
||||
return {
|
||||
queryKey: ["me"],
|
||||
queryFn: async () =>
|
||||
getMetadataAsJSON<User>("user") ?? API.getAuthenticatedUser(),
|
||||
};
|
||||
queryKey: meKey,
|
||||
queryFn: async () => {
|
||||
const cachedData = queryClient.getQueryData(meKey);
|
||||
if (cachedData === undefined && initialMeData !== undefined) {
|
||||
return initialMeData;
|
||||
}
|
||||
|
||||
return API.getAuthenticatedUser();
|
||||
},
|
||||
} satisfies UseQueryOptions<User>;
|
||||
};
|
||||
|
||||
export const hasFirstUser = () => {
|
||||
|
||||
@@ -45,7 +45,9 @@ type AuthContextValue = {
|
||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: FC<PropsWithChildren> = ({ 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<PropsWithChildren> = ({ children }) => {
|
||||
enabled: userQuery.data !== undefined,
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const loginMutation = useMutation(
|
||||
login({ checks: permissionsToCheck }, queryClient),
|
||||
);
|
||||
|
||||
@@ -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<PropsWithChildren> = ({ 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 ||
|
||||
|
||||
@@ -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<string[]>("experiments")?.includes("dashboard_theme") ??
|
||||
false);
|
||||
|
||||
export const colors = {
|
||||
white: "hsl(0, 0%, 100%)",
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- It can be any
|
||||
export const getMetadataAsJSON = <T extends Record<string, any>>(
|
||||
export const getMetadataAsJSON = <T extends NonNullable<unknown>>(
|
||||
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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user