mirror of https://github.com/coder/coder.git
feat: show version on login page (#13033)
This commit is contained in:
parent
a69fc657f2
commit
215dd7b152
|
@ -8,13 +8,12 @@ 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),
|
||||
// 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) => {
|
||||
|
|
|
@ -8,11 +8,10 @@ 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),
|
||||
// The version of the app can't change without reloading the page.
|
||||
return cachedQuery({
|
||||
initialData: initialBuildInfoData,
|
||||
queryKey: buildInfoKey,
|
||||
queryFn: () => API.getBuildInfo(),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
@ -8,11 +8,11 @@ const initialEntitlementsData = getMetadataAsJSON<Entitlements>("entitlements");
|
|||
const entitlementsQueryKey = ["entitlements"] as const;
|
||||
|
||||
export const entitlements = (): UseQueryOptions<Entitlements> => {
|
||||
return {
|
||||
...cachedQuery(initialEntitlementsData),
|
||||
return cachedQuery({
|
||||
initialData: initialEntitlementsData,
|
||||
queryKey: entitlementsQueryKey,
|
||||
queryFn: () => API.getEntitlements(),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const refreshEntitlements = (queryClient: QueryClient) => {
|
||||
|
|
|
@ -8,11 +8,11 @@ const initialExperimentsData = getMetadataAsJSON<Experiments>("experiments");
|
|||
const experimentsKey = ["experiments"] as const;
|
||||
|
||||
export const experiments = (): UseQueryOptions<Experiments> => {
|
||||
return {
|
||||
...cachedQuery(initialExperimentsData),
|
||||
return cachedQuery({
|
||||
initialData: initialExperimentsData,
|
||||
queryKey: experimentsKey,
|
||||
queryFn: () => API.getExperiments(),
|
||||
} satisfies UseQueryOptions<Experiments>;
|
||||
});
|
||||
};
|
||||
|
||||
export const availableExperiments = () => {
|
||||
|
|
|
@ -129,11 +129,11 @@ const meKey = ["me"];
|
|||
export const me = (): UseQueryOptions<User> & {
|
||||
queryKey: QueryKey;
|
||||
} => {
|
||||
return {
|
||||
...cachedQuery(initialUserData),
|
||||
return cachedQuery({
|
||||
initialData: initialUserData,
|
||||
queryKey: meKey,
|
||||
queryFn: API.getAuthenticatedUser,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export function apiKey(): UseQueryOptions<GenerateAPIKeyResponse> {
|
||||
|
@ -144,12 +144,12 @@ export function apiKey(): UseQueryOptions<GenerateAPIKeyResponse> {
|
|||
}
|
||||
|
||||
export const hasFirstUser = (): UseQueryOptions<boolean> => {
|
||||
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 = (
|
||||
|
|
|
@ -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 = <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,
|
||||
};
|
||||
/**
|
||||
* 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>,
|
||||
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,
|
||||
});
|
||||
|
|
|
@ -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<PropsWithChildren> = ({ children }) => {
|
|||
error: proxiesError,
|
||||
isLoading: proxiesLoading,
|
||||
isFetched: proxiesFetched,
|
||||
} = useQuery({
|
||||
...cachedQuery(initialData),
|
||||
queryKey,
|
||||
queryFn: query,
|
||||
} as UseQueryOptions<readonly Region[]>);
|
||||
} = useQuery(
|
||||
cachedQuery({
|
||||
initialData,
|
||||
queryKey,
|
||||
queryFn: query,
|
||||
}),
|
||||
);
|
||||
|
||||
// Every time we get a new proxiesResponse, update the latency check
|
||||
// to each workspace proxy.
|
||||
|
|
|
@ -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<DashboardValue | undefined>(
|
|||
);
|
||||
|
||||
export const DashboardProvider: FC<PropsWithChildren> = ({ 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<AppearanceConfig>();
|
||||
|
||||
|
@ -84,7 +77,6 @@ export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
|
|||
return (
|
||||
<DashboardContext.Provider
|
||||
value={{
|
||||
buildInfo: buildInfoQuery.data,
|
||||
entitlements: entitlementsQuery.data,
|
||||
experiments: experimentsQuery.data,
|
||||
appearance: {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import type { FC } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { buildInfo } from "api/queries/buildInfo";
|
||||
import { useAuthenticated } from "contexts/auth/RequireAuth";
|
||||
import { useProxy } from "contexts/ProxyContext";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
|
@ -6,7 +8,8 @@ import { useFeatureVisibility } from "../useFeatureVisibility";
|
|||
import { NavbarView } from "./NavbarView";
|
||||
|
||||
export const Navbar: FC = () => {
|
||||
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 = () => {
|
|||
<NavbarView
|
||||
user={me}
|
||||
logo_url={appearance.config.logo_url}
|
||||
buildInfo={buildInfo}
|
||||
buildInfo={buildInfoQuery.data}
|
||||
supportLinks={appearance.config.support_links}
|
||||
onSignOut={signOut}
|
||||
canViewAuditLog={canViewAuditLog}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { css, type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import Badge from "@mui/material/Badge";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import type { FC } from "react";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
|
||||
import {
|
||||
|
@ -17,7 +17,6 @@ export interface UserDropdownProps {
|
|||
buildInfo?: TypesGen.BuildInfoResponse;
|
||||
supportLinks?: readonly TypesGen.LinkConfig[];
|
||||
onSignOut: () => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const UserDropdown: FC<UserDropdownProps> = ({
|
||||
|
|
|
@ -24,7 +24,7 @@ export const AgentVersion: FC<AgentVersionProps> = ({
|
|||
);
|
||||
|
||||
if (status === agentVersionStatus.Updated) {
|
||||
return <span>Updated</span>;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -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<typeof WorkspaceStatusBadge> = {
|
||||
title: "modules/workspaces/WorkspaceStatusBadge",
|
||||
component: WorkspaceStatusBadge,
|
||||
parameters: {
|
||||
queries: [
|
||||
{
|
||||
key: ["buildInfo"],
|
||||
data: MockBuildInfo,
|
||||
},
|
||||
],
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<DashboardContext.Provider
|
||||
value={{
|
||||
buildInfo: MockBuildInfo,
|
||||
entitlements: MockEntitlementsWithScheduling,
|
||||
experiments: MockExperiments,
|
||||
appearance: MockedAppearance,
|
||||
|
|
|
@ -2,6 +2,7 @@ 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 { buildInfo } from "api/queries/buildInfo";
|
||||
import { authMethods } from "api/queries/users";
|
||||
import { useAuthContext } from "contexts/auth/AuthProvider";
|
||||
import { getApplicationName } from "utils/appearance";
|
||||
|
@ -22,6 +23,7 @@ export const LoginPage: FC = () => {
|
|||
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);
|
||||
|
|
|
@ -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<LoginPageViewProps> = ({
|
|||
authMethods,
|
||||
error,
|
||||
isLoading,
|
||||
buildInfo,
|
||||
isSigningIn,
|
||||
onSignIn,
|
||||
}) => {
|
||||
|
@ -64,7 +66,10 @@ export const LoginPageView: FC<LoginPageViewProps> = ({
|
|||
/>
|
||||
)}
|
||||
<footer css={styles.footer}>
|
||||
Copyright © {new Date().getFullYear()} Coder Technologies, Inc.
|
||||
<div>
|
||||
Copyright © {new Date().getFullYear()} Coder Technologies, Inc.
|
||||
</div>
|
||||
<div>{buildInfo?.version}</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -27,6 +27,10 @@ const meta: Meta<typeof Workspace> = {
|
|||
component: Workspace,
|
||||
parameters: {
|
||||
queries: [
|
||||
{
|
||||
key: ["buildInfo"],
|
||||
data: Mocks.MockBuildInfo,
|
||||
},
|
||||
{
|
||||
key: ["portForward", Mocks.MockWorkspaceAgent.id],
|
||||
data: Mocks.MockListeningPortsResponse,
|
||||
|
@ -37,7 +41,6 @@ const meta: Meta<typeof Workspace> = {
|
|||
(Story) => (
|
||||
<DashboardContext.Provider
|
||||
value={{
|
||||
buildInfo: Mocks.MockBuildInfo,
|
||||
entitlements: Mocks.MockEntitlementsWithScheduling,
|
||||
experiments: Mocks.MockExperiments,
|
||||
appearance: MockedAppearance,
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { MissingBuildParameters, restartWorkspace } from "api/api";
|
||||
import { getErrorMessage } from "api/errors";
|
||||
import { buildInfo } from "api/queries/buildInfo";
|
||||
import { deploymentConfig, deploymentSSHConfig } from "api/queries/deployment";
|
||||
import { templateVersion, templateVersions } from "api/queries/templates";
|
||||
import {
|
||||
|
@ -27,7 +28,6 @@ import { MemoizedInlineMarkdown } from "components/Markdown/Markdown";
|
|||
import { Stack } from "components/Stack/Stack";
|
||||
import { useAuthenticated } from "contexts/auth/RequireAuth";
|
||||
import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { ChangeVersionDialog } from "./ChangeVersionDialog";
|
||||
|
@ -50,7 +50,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
|||
}) => {
|
||||
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<WorkspaceReadyPageProps> = ({
|
|||
canChangeVersions={canChangeVersions}
|
||||
hideSSHButton={featureVisibility["browser_only"]}
|
||||
hideVSCodeDesktopButton={featureVisibility["browser_only"]}
|
||||
buildInfo={buildInfo}
|
||||
buildInfo={buildInfoQuery.data}
|
||||
sshPrefix={sshPrefixQuery.data?.hostname_prefix}
|
||||
template={template}
|
||||
buildLogs={
|
||||
|
|
|
@ -139,11 +139,18 @@ const meta: Meta<typeof WorkspacesPageView> = {
|
|||
count: 13,
|
||||
page: 1,
|
||||
},
|
||||
parameters: {
|
||||
queries: [
|
||||
{
|
||||
key: ["buildInfo"],
|
||||
data: MockBuildInfo,
|
||||
},
|
||||
],
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<DashboardContext.Provider
|
||||
value={{
|
||||
buildInfo: MockBuildInfo,
|
||||
entitlements: MockEntitlementsWithScheduling,
|
||||
experiments: MockExperiments,
|
||||
appearance: MockedAppearance,
|
||||
|
|
|
@ -3,11 +3,7 @@ import type { FC } from "react";
|
|||
import { withDefaultFeatures } from "api/api";
|
||||
import type { Entitlements } from "api/typesGenerated";
|
||||
import { DashboardContext } from "modules/dashboard/DashboardProvider";
|
||||
import {
|
||||
MockAppearanceConfig,
|
||||
MockBuildInfo,
|
||||
MockEntitlements,
|
||||
} from "./entities";
|
||||
import { MockAppearanceConfig, MockEntitlements } from "./entities";
|
||||
|
||||
export const withDashboardProvider = (
|
||||
Story: FC,
|
||||
|
@ -30,7 +26,6 @@ export const withDashboardProvider = (
|
|||
return (
|
||||
<DashboardContext.Provider
|
||||
value={{
|
||||
buildInfo: MockBuildInfo,
|
||||
entitlements,
|
||||
experiments,
|
||||
appearance: {
|
||||
|
|
|
@ -3,8 +3,8 @@ export const getApplicationName = (): string => {
|
|||
.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";
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue