Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion site/src/api/queries/buildInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { BuildInfoResponse } from "#/api/typesGenerated";
import type { MetadataState } from "#/hooks/useEmbeddedMetadata";
import { cachedQuery } from "./util";

const buildInfoKey = ["buildInfo"] as const;
export const buildInfoKey = ["buildInfo"];

export const buildInfo = (metadata: MetadataState<BuildInfoResponse>) => {
// The version of the app can't change without reloading the page.
Expand Down
4 changes: 3 additions & 1 deletion site/src/api/queries/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ export const deploymentDAUs = () => {
};
};

export const deploymentStatsQueryKey = ["deployment", "stats"];

export const deploymentStats = () => {
return {
queryKey: ["deployment", "stats"],
queryKey: deploymentStatsQueryKey,
queryFn: API.getDeploymentStats,
};
};
Expand Down
4 changes: 3 additions & 1 deletion site/src/api/queries/updateCheck.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { API } from "#/api/api";

export const updateCheckQueryKey = ["updateCheck"];

export const updateCheck = () => {
return {
queryKey: ["updateCheck"],
queryKey: updateCheckQueryKey,
queryFn: () => API.getUpdateCheck(),
};
};
125 changes: 125 additions & 0 deletions site/src/modules/dashboard/DashboardLayout.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, screen, userEvent, within } from "storybook/test";
import {
reactRouterOutlet,
reactRouterParameters,
} from "storybook-addon-remix-react-router";
import { buildInfoKey } from "#/api/queries/buildInfo";
import { deploymentStatsQueryKey } from "#/api/queries/deployment";
import { updateCheckQueryKey } from "#/api/queries/updateCheck";
import type { UpdateCheckResponse } from "#/api/typesGenerated";
import {
MockBuildInfo,
MockDeploymentStats,
MockNoPermissions,
MockPermissions,
MockUpdateCheck,
MockUserMember,
MockUserOwner,
} from "#/testHelpers/entities";
import { pixelWithTablet } from "#/testHelpers/pixel";
import {
withAuthProvider,
withDashboardProvider,
withProxyProvider,
} from "#/testHelpers/storybook";
import { DashboardFullPage, DashboardLayout } from "./DashboardLayout";

const outdatedUpdateCheck: UpdateCheckResponse = {
...MockUpdateCheck,
current: false,
version: "v0.12.9",
url: "https://github.com/coder/coder/releases/tag/v0.12.9",
};
Comment thread
jakehwll marked this conversation as resolved.

const pageContent = (
<DashboardFullPage className="p-6">
<h1>Workspaces</h1>
<p>Page content rendered in the dashboard outlet.</p>
</DashboardFullPage>
);

const meta: Meta<typeof DashboardLayout> = {
title: "modules/dashboard/DashboardLayout",
component: DashboardLayout,
decorators: [withAuthProvider, withDashboardProvider, withProxyProvider()],
parameters: {
layout: "fullscreen",
pixel: { matrix: pixelWithTablet },
user: MockUserOwner,
permissions: MockPermissions,
reactRouter: reactRouterParameters({
location: { path: "/" },
routing: reactRouterOutlet({ path: "/" }, pageContent),
}),
queries: [
{ key: buildInfoKey, data: MockBuildInfo },
{ key: updateCheckQueryKey, data: MockUpdateCheck },
{ key: deploymentStatsQueryKey, data: MockDeploymentStats },
],
},
};

export default meta;
type Story = StoryObj<typeof DashboardLayout>;

export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.getByRole("heading", { name: "Workspaces" }),
).toBeVisible();
await expect(
canvas.getByRole("link", { name: "Skip to main content" }),
).toBeInTheDocument();
},
};

export const ForMember: Story = {
parameters: {
user: MockUserMember,
permissions: MockNoPermissions,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.getByRole("heading", { name: "Workspaces" }),
).toBeVisible();
await expect(
screen.queryByTestId("update-check-notice"),
).not.toBeInTheDocument();
},
};

export const UpdateAvailable: Story = {
parameters: {
queries: [
{ key: buildInfoKey, data: MockBuildInfo },
{ key: updateCheckQueryKey, data: outdatedUpdateCheck },
{ key: deploymentStatsQueryKey, data: MockDeploymentStats },
],
},
beforeEach: () => {
localStorage.removeItem("dismissedVersion");
},
play: async () => {
const notice = await screen.findByTestId("update-check-notice");
await expect(notice).toBeVisible();
await expect(
screen.getByText(/Coder v0\.12\.9 is now available/),
).toBeVisible();
},
};

export const SkipToMainContent: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const skipLink = canvas.getByRole("link", {
name: "Skip to main content",
});
const main = canvas.getByRole("main");

await userEvent.click(skipLink);
await expect(main).toHaveFocus();
},
};
2 changes: 1 addition & 1 deletion site/src/modules/dashboard/DashboardLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ test("Show the new Coder version notification", async () => {
renderWithAuth(<DashboardLayout />, {
children: [{ element: <h1>Test page</h1> }],
});
await screen.findByTestId("update-check-snackbar");
await screen.findByTestId("update-check-notice");
});

test("hides AI Governance seat warnings for non-admin users", async () => {
Expand Down
64 changes: 9 additions & 55 deletions site/src/modules/dashboard/DashboardLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import Link from "@mui/material/Link";
import Snackbar from "@mui/material/Snackbar";
import { InfoIcon } from "lucide-react";
import { type FC, type HTMLAttributes, Suspense } from "react";
import { Outlet } from "react-router";
import { Button } from "#/components/Button/Button";
import { Loader } from "#/components/Loader/Loader";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { AnnouncementBanners } from "#/modules/dashboard/AnnouncementBanners/AnnouncementBanners";
import { LicenseBanner } from "#/modules/dashboard/LicenseBanner/LicenseBanner";
import { cn } from "#/utils/cn";
import { docs } from "#/utils/docs";
import { DeploymentBanner } from "./DeploymentBanner/DeploymentBanner";
import { Navbar } from "./Navbar/Navbar";
import { UpdateCheckNotice } from "./UpdateCheckNotice/UpdateCheckNotice";
import { useUpdateCheck } from "./useUpdateCheck";

export const DashboardLayout: FC = () => {
Expand Down Expand Up @@ -54,56 +50,14 @@ export const DashboardLayout: FC = () => {

<DeploymentBanner />

<Snackbar
data-testid="update-check-snackbar"
open={updateCheck.isVisible}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
ContentProps={{
sx: (theme) => ({
background: theme.palette.background.paper,
color: theme.palette.text.primary,
maxWidth: 440,
flexDirection: "row",
borderColor: theme.palette.info.light,

"& .MuiSnackbarContent-message": {
flex: 1,
},

"& .MuiSnackbarContent-action": {
marginRight: 0,
},
}),
}}
message={
<div className="flex gap-4">
<InfoIcon
className="size-icon-xs"
css={(theme) => ({
fontSize: 16,
height: 20, // 20 is the height of the text line so we can align them
color: theme.palette.info.light,
})}
/>
<p>
Coder {updateCheck.data?.version} is now available. View the{" "}
<Link href={updateCheck.data?.url}>release notes</Link> and{" "}
<Link href={docs("/install/upgrade")}>
upgrade instructions
</Link>{" "}
for more information.
</p>
</div>
}
action={
<Button variant="subtle" size="sm" onClick={updateCheck.dismiss}>
Dismiss
</Button>
}
/>
{updateCheck.isVisible && updateCheck.data && (
<UpdateCheckNotice
version={updateCheck.data.version}
releaseNotesUrl={updateCheck.data.url}
onDismiss={updateCheck.dismiss}
aboveDeploymentBanner={Boolean(permissions.viewDeploymentStats)}
/>
)}
</div>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, within } from "storybook/test";
import { UpdateCheckNotice } from "./UpdateCheckNotice";

const meta: Meta<typeof UpdateCheckNotice> = {
title: "modules/dashboard/UpdateCheckNotice",
component: UpdateCheckNotice,
args: {
version: "v0.12.9",
releaseNotesUrl: "https://github.com/coder/coder/releases/tag/v0.12.9",
onDismiss: fn(),
aboveDeploymentBanner: true,
},
parameters: {
layout: "fullscreen",
},
};

export default meta;
type Story = StoryObj<typeof UpdateCheckNotice>;

export const AboveDeploymentBanner: Story = {
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
await expect(
canvas.getByText(/Coder v0\.12\.9 is now available/),
).toBeVisible();
await expect(
canvas.getByRole("link", { name: "release notes" }),
).toHaveAttribute(
"href",
"https://github.com/coder/coder/releases/tag/v0.12.9",
);
await expect(
canvas.getByRole("link", { name: "upgrade instructions" }),
).toBeVisible();

await userEvent.click(canvas.getByRole("button", { name: "Dismiss" }));
await expect(args.onDismiss).toHaveBeenCalled();
},
};

export const WithoutDeploymentBanner: Story = {
args: {
aboveDeploymentBanner: false,
},
};
66 changes: 66 additions & 0 deletions site/src/modules/dashboard/UpdateCheckNotice/UpdateCheckNotice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { InfoIcon, XIcon } from "lucide-react";
import type { FC } from "react";
import { Button } from "#/components/Button/Button";
import { cn } from "#/utils/cn";
import { docs } from "#/utils/docs";

type UpdateCheckNoticeProps = {
version: string;
releaseNotesUrl: string;
onDismiss: () => void;
aboveDeploymentBanner?: boolean;
};

export const UpdateCheckNotice: FC<UpdateCheckNoticeProps> = ({
version,
releaseNotesUrl,
onDismiss,
aboveDeploymentBanner = false,
}) => {
return (
<div
data-testid="update-check-notice"
role="status"
className={cn(
"fixed right-6 z-50 flex max-w-[420px] items-start gap-4 rounded border border-solid border-highlight-sky bg-surface-primary p-4 text-sm text-content-primary shadow",
// 60px keeps a 24px gap above the 36px deployment banner.
aboveDeploymentBanner ? "bottom-[60px]" : "bottom-6",
)}
>
<InfoIcon className="mt-0.5 size-icon-sm shrink-0 text-highlight-sky" />
<div className="flex flex-col gap-1">
<p className="m-0 font-semibold">Coder {version} is now available. </p>
<p className="m-0 flex-1 leading-5">
View the{" "}
<a
href={releaseNotesUrl}
target="_blank"
rel="noreferrer"
className="text-content-link underline hover:no-underline"
>
release notes
</a>{" "}
and{" "}
<a
href={docs("/install/upgrade")}
target="_blank"
rel="noreferrer"
className="text-content-link underline hover:no-underline"
>
upgrade instructions
</a>{" "}
for more information.
</p>
</div>
<Button
aria-label="Dismiss"
size="icon"
variant="subtle"
onClick={onDismiss}
className="-m-2"
>
<XIcon />
</Button>
</div>
);
};
Loading