diff --git a/site/src/api/queries/appearance.ts b/site/src/api/queries/appearance.ts index 45a6c618c5dec..8178ed602abb7 100644 --- a/site/src/api/queries/appearance.ts +++ b/site/src/api/queries/appearance.ts @@ -8,13 +8,12 @@ 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), + // We either have our initial data or should immediately fetch and never again! + return cachedQuery({ + initialData: initialAppearanceData, queryKey: ["appearance"], queryFn: () => API.getAppearance(), - }; + }); }; export const updateAppearance = (queryClient: QueryClient) => { diff --git a/site/src/api/queries/buildInfo.ts b/site/src/api/queries/buildInfo.ts index aeed3ecd3d02b..504b59bd1d341 100644 --- a/site/src/api/queries/buildInfo.ts +++ b/site/src/api/queries/buildInfo.ts @@ -8,11 +8,10 @@ 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), + // The version of the app can't change without reloading the page. + return cachedQuery({ + initialData: initialBuildInfoData, queryKey: buildInfoKey, queryFn: () => API.getBuildInfo(), - }; + }); }; diff --git a/site/src/api/queries/entitlements.ts b/site/src/api/queries/entitlements.ts index 1a4c990fe2ad4..46b41133bd476 100644 --- a/site/src/api/queries/entitlements.ts +++ b/site/src/api/queries/entitlements.ts @@ -8,11 +8,11 @@ const initialEntitlementsData = getMetadataAsJSON("entitlements"); const entitlementsQueryKey = ["entitlements"] as const; export const entitlements = (): UseQueryOptions => { - return { - ...cachedQuery(initialEntitlementsData), + return cachedQuery({ + initialData: initialEntitlementsData, queryKey: entitlementsQueryKey, queryFn: () => API.getEntitlements(), - }; + }); }; export const refreshEntitlements = (queryClient: QueryClient) => { diff --git a/site/src/api/queries/experiments.ts b/site/src/api/queries/experiments.ts index bd7f436f5b9e6..934e44b863437 100644 --- a/site/src/api/queries/experiments.ts +++ b/site/src/api/queries/experiments.ts @@ -8,11 +8,11 @@ const initialExperimentsData = getMetadataAsJSON("experiments"); const experimentsKey = ["experiments"] as const; export const experiments = (): UseQueryOptions => { - return { - ...cachedQuery(initialExperimentsData), + return cachedQuery({ + initialData: initialExperimentsData, queryKey: experimentsKey, queryFn: () => API.getExperiments(), - } satisfies UseQueryOptions; + }); }; export const availableExperiments = () => { diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts index 34609b47f5a20..3a1806276146b 100644 --- a/site/src/api/queries/users.ts +++ b/site/src/api/queries/users.ts @@ -129,11 +129,11 @@ const meKey = ["me"]; export const me = (): UseQueryOptions & { queryKey: QueryKey; } => { - return { - ...cachedQuery(initialUserData), + return cachedQuery({ + initialData: initialUserData, queryKey: meKey, queryFn: API.getAuthenticatedUser, - }; + }); }; export function apiKey(): UseQueryOptions { @@ -144,12 +144,12 @@ export function apiKey(): UseQueryOptions { } export const hasFirstUser = (): UseQueryOptions => { - return { + return cachedQuery({ // This cannot be false otherwise it will not fetch! - ...cachedQuery(typeof initialUserData !== "undefined" ? true : undefined), + initialData: Boolean(initialUserData) || undefined, queryKey: ["hasFirstUser"], queryFn: API.hasFirstUser, - }; + }); }; export const login = ( diff --git a/site/src/api/queries/util.ts b/site/src/api/queries/util.ts index d5eb591c1ffd2..6dc02ac897482 100644 --- a/site/src/api/queries/util.ts +++ b/site/src/api/queries/util.ts @@ -1,23 +1,27 @@ 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, - }; +/** + * 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 = < + TQueryOptions extends UseQueryOptions, + TData, +>( + options: TQueryOptions, +): TQueryOptions => + // Only do this if there is initial data, otherwise it can conflict with tests. + ({ + ...(options.initialData + ? { + cacheTime: Infinity, + staleTime: Infinity, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + } + : {}), + ...options, + }); diff --git a/site/src/contexts/ProxyContext.tsx b/site/src/contexts/ProxyContext.tsx index ff99d7b03c41d..07e1486dd12e6 100644 --- a/site/src/contexts/ProxyContext.tsx +++ b/site/src/contexts/ProxyContext.tsx @@ -7,7 +7,7 @@ import { useEffect, useState, } from "react"; -import { type UseQueryOptions, useQuery } from "react-query"; +import { useQuery } from "react-query"; import { getWorkspaceProxies, getWorkspaceProxyRegions } from "api/api"; import { cachedQuery } from "api/queries/util"; import type { Region, WorkspaceProxy } from "api/typesGenerated"; @@ -131,11 +131,13 @@ export const ProxyProvider: FC = ({ children }) => { error: proxiesError, isLoading: proxiesLoading, isFetched: proxiesFetched, - } = useQuery({ - ...cachedQuery(initialData), - queryKey, - queryFn: query, - } as UseQueryOptions); + } = useQuery( + cachedQuery({ + initialData, + queryKey, + queryFn: query, + }), + ); // Every time we get a new proxiesResponse, update the latency check // to each workspace proxy. diff --git a/site/src/modules/dashboard/DashboardProvider.tsx b/site/src/modules/dashboard/DashboardProvider.tsx index f389311754807..7c1871946cf4c 100644 --- a/site/src/modules/dashboard/DashboardProvider.tsx +++ b/site/src/modules/dashboard/DashboardProvider.tsx @@ -7,12 +7,10 @@ import { } from "react"; import { useQuery } from "react-query"; import { appearance } from "api/queries/appearance"; -import { buildInfo } from "api/queries/buildInfo"; import { entitlements } from "api/queries/entitlements"; import { experiments } from "api/queries/experiments"; import type { AppearanceConfig, - BuildInfoResponse, Entitlements, Experiments, } from "api/typesGenerated"; @@ -27,7 +25,6 @@ interface Appearance { } export interface DashboardValue { - buildInfo: BuildInfoResponse; entitlements: Entitlements; experiments: Experiments; appearance: Appearance; @@ -38,16 +35,12 @@ export const DashboardContext = createContext( ); export const DashboardProvider: FC = ({ children }) => { - const buildInfoQuery = useQuery(buildInfo()); const entitlementsQuery = useQuery(entitlements()); const experimentsQuery = useQuery(experiments()); const appearanceQuery = useQuery(appearance()); const isLoading = - !buildInfoQuery.data || - !entitlementsQuery.data || - !appearanceQuery.data || - !experimentsQuery.data; + !entitlementsQuery.data || !appearanceQuery.data || !experimentsQuery.data; const [configPreview, setConfigPreview] = useState(); @@ -84,7 +77,6 @@ export const DashboardProvider: FC = ({ children }) => { return ( { - const { appearance, buildInfo } = useDashboard(); + const { appearance } = useDashboard(); + const buildInfoQuery = useQuery(buildInfo()); const { user: me, permissions, signOut } = useAuthenticated(); const featureVisibility = useFeatureVisibility(); const canViewAuditLog = @@ -19,7 +22,7 @@ export const Navbar: FC = () => { void; - children?: ReactNode; } export const UserDropdown: FC = ({ diff --git a/site/src/modules/resources/AgentVersion.tsx b/site/src/modules/resources/AgentVersion.tsx index bba87833f3999..1c62f68bbd519 100644 --- a/site/src/modules/resources/AgentVersion.tsx +++ b/site/src/modules/resources/AgentVersion.tsx @@ -24,7 +24,7 @@ export const AgentVersion: FC = ({ ); if (status === agentVersionStatus.Updated) { - return Updated; + return null; } return ( diff --git a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx b/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx index 572ac83aae577..a9c065c33b330 100644 --- a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx @@ -1,20 +1,20 @@ import type { Meta, StoryObj } from "@storybook/react"; import { DashboardContext } from "modules/dashboard/DashboardProvider"; import { + MockAppearanceConfig, + MockBuildInfo, MockCanceledWorkspace, MockCancelingWorkspace, MockDeletedWorkspace, MockDeletingWorkspace, + MockEntitlementsWithScheduling, + MockExperiments, MockFailedWorkspace, MockPendingWorkspace, MockStartingWorkspace, MockStoppedWorkspace, MockStoppingWorkspace, MockWorkspace, - MockBuildInfo, - MockEntitlementsWithScheduling, - MockExperiments, - MockAppearanceConfig, } from "testHelpers/entities"; import { WorkspaceStatusBadge } from "./WorkspaceStatusBadge"; @@ -27,11 +27,18 @@ const MockedAppearance = { const meta: Meta = { title: "modules/workspaces/WorkspaceStatusBadge", component: WorkspaceStatusBadge, + parameters: { + queries: [ + { + key: ["buildInfo"], + data: MockBuildInfo, + }, + ], + }, decorators: [ (Story) => ( { const redirectTo = retrieveRedirect(location.search); const applicationName = getApplicationName(); const navigate = useNavigate(); + const buildInfoQuery = useQuery(buildInfo()); if (isSignedIn) { // If the redirect is going to a workspace application, and we @@ -65,6 +67,7 @@ export const LoginPage: FC = () => { authMethods={authMethodsQuery.data} error={signInError} isLoading={isLoading || authMethodsQuery.isLoading} + buildInfo={buildInfoQuery.data} isSigningIn={isSigningIn} onSignIn={async ({ email, password }) => { await signIn(email, password); diff --git a/site/src/pages/LoginPage/LoginPageView.tsx b/site/src/pages/LoginPage/LoginPageView.tsx index e62e8be0d834e..cd585f3f1db65 100644 --- a/site/src/pages/LoginPage/LoginPageView.tsx +++ b/site/src/pages/LoginPage/LoginPageView.tsx @@ -1,7 +1,7 @@ import type { Interpolation, Theme } from "@emotion/react"; import type { FC } from "react"; import { useLocation } from "react-router-dom"; -import type { AuthMethods } from "api/typesGenerated"; +import type { AuthMethods, BuildInfoResponse } from "api/typesGenerated"; import { CoderIcon } from "components/Icons/CoderIcon"; import { Loader } from "components/Loader/Loader"; import { getApplicationName, getLogoURL } from "utils/appearance"; @@ -12,6 +12,7 @@ export interface LoginPageViewProps { authMethods: AuthMethods | undefined; error: unknown; isLoading: boolean; + buildInfo?: BuildInfoResponse; isSigningIn: boolean; onSignIn: (credentials: { email: string; password: string }) => void; } @@ -20,6 +21,7 @@ export const LoginPageView: FC = ({ authMethods, error, isLoading, + buildInfo, isSigningIn, onSignIn, }) => { @@ -64,7 +66,10 @@ export const LoginPageView: FC = ({ /> )}
- Copyright © {new Date().getFullYear()} Coder Technologies, Inc. +
+ Copyright © {new Date().getFullYear()} Coder Technologies, Inc. +
+
{buildInfo?.version}
diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index dd0ef043ac84d..7f4db0efb8888 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -27,6 +27,10 @@ const meta: Meta = { component: Workspace, parameters: { queries: [ + { + key: ["buildInfo"], + data: Mocks.MockBuildInfo, + }, { key: ["portForward", Mocks.MockWorkspaceAgent.id], data: Mocks.MockListeningPortsResponse, @@ -37,7 +41,6 @@ const meta: Meta = { (Story) => ( = ({ }) => { const navigate = useNavigate(); const queryClient = useQueryClient(); - const { buildInfo } = useDashboard(); + const buildInfoQuery = useQuery(buildInfo()); const featureVisibility = useFeatureVisibility(); if (workspace === undefined) { throw Error("Workspace is undefined"); @@ -248,7 +248,7 @@ export const WorkspaceReadyPage: FC = ({ canChangeVersions={canChangeVersions} hideSSHButton={featureVisibility["browser_only"]} hideVSCodeDesktopButton={featureVisibility["browser_only"]} - buildInfo={buildInfo} + buildInfo={buildInfoQuery.data} sshPrefix={sshPrefixQuery.data?.hostname_prefix} template={template} buildLogs={ diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index 965dfe4b6b006..1c7b61558a8cf 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -139,11 +139,18 @@ const meta: Meta = { count: 13, page: 1, }, + parameters: { + queries: [ + { + key: ["buildInfo"], + data: MockBuildInfo, + }, + ], + }, decorators: [ (Story) => ( { .querySelector(`meta[name=application-name]`) ?.getAttribute("content"); // Fallback to "Coder" if the application name is not available for some reason. - // We need to check if the content does not look like {{ .ApplicationName}} - // as it means that Coder is running in development mode (port :8080). + // We need to check if the content does not look like `{{ .ApplicationName }}` + // as it means that Coder is running in development mode. return c && !c.startsWith("{{ .") ? c : "Coder"; };