mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore(site): add storybook for terminal page (#12441)
This commit is contained in:
Vendored
+2
-3
@@ -3,12 +3,11 @@ import type { QueryKey } from "react-query";
|
|||||||
import type { Experiments, FeatureName } from "api/typesGenerated";
|
import type { Experiments, FeatureName } from "api/typesGenerated";
|
||||||
|
|
||||||
declare module "@storybook/react" {
|
declare module "@storybook/react" {
|
||||||
|
type WebSocketEvent = { event: "message"; data: string } | { event: "error" };
|
||||||
interface Parameters {
|
interface Parameters {
|
||||||
features?: FeatureName[];
|
features?: FeatureName[];
|
||||||
experiments?: Experiments;
|
experiments?: Experiments;
|
||||||
queries?: { key: QueryKey; data: unknown }[];
|
queries?: { key: QueryKey; data: unknown }[];
|
||||||
webSocket?: {
|
webSocket?: WebSocketEvent[];
|
||||||
messages: string[];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export const AuthContext = createContext<AuthContextValue | undefined>(
|
|||||||
export const AuthProvider: FC<PropsWithChildren> = ({ children }) => {
|
export const AuthProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const meOptions = me();
|
const meOptions = me();
|
||||||
|
|
||||||
const userQuery = useQuery(meOptions);
|
const userQuery = useQuery(meOptions);
|
||||||
const authMethodsQuery = useQuery(authMethods());
|
const authMethodsQuery = useQuery(authMethods());
|
||||||
const hasFirstUserQuery = useQuery(hasFirstUser());
|
const hasFirstUserQuery = useQuery(hasFirstUser());
|
||||||
|
|||||||
@@ -46,8 +46,9 @@ export const Logs: Story = {
|
|||||||
},
|
},
|
||||||
decorators: [withWebSocket],
|
decorators: [withWebSocket],
|
||||||
parameters: {
|
parameters: {
|
||||||
webSocket: {
|
webSocket: MockWorkspaceBuildLogs.map((log) => ({
|
||||||
messages: MockWorkspaceBuildLogs.map((log) => JSON.stringify(log)),
|
event: "message",
|
||||||
},
|
data: JSON.stringify(log),
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import {
|
||||||
|
reactRouterOutlet,
|
||||||
|
reactRouterParameters,
|
||||||
|
} from "storybook-addon-react-router-v6";
|
||||||
|
import { getAuthorizationKey } from "api/queries/authCheck";
|
||||||
|
import { workspaceByOwnerAndNameKey } from "api/queries/workspaces";
|
||||||
|
import type { Workspace, WorkspaceAgentLifecycle } from "api/typesGenerated";
|
||||||
|
import { AuthProvider } from "contexts/auth/AuthProvider";
|
||||||
|
import { permissionsToCheck } from "contexts/auth/permissions";
|
||||||
|
import { RequireAuth } from "contexts/auth/RequireAuth";
|
||||||
|
import {
|
||||||
|
MockAppearanceConfig,
|
||||||
|
MockAuthMethodsAll,
|
||||||
|
MockBuildInfo,
|
||||||
|
MockEntitlements,
|
||||||
|
MockExperiments,
|
||||||
|
MockUser,
|
||||||
|
MockWorkspace,
|
||||||
|
MockWorkspaceAgent,
|
||||||
|
} from "testHelpers/entities";
|
||||||
|
import { withWebSocket } from "testHelpers/storybook";
|
||||||
|
import TerminalPage from "./TerminalPage";
|
||||||
|
|
||||||
|
const createWorkspaceWithAgent = (lifecycle: WorkspaceAgentLifecycle) => {
|
||||||
|
return {
|
||||||
|
key: workspaceByOwnerAndNameKey(
|
||||||
|
MockWorkspace.owner_name,
|
||||||
|
MockWorkspace.name,
|
||||||
|
),
|
||||||
|
data: {
|
||||||
|
...MockWorkspace,
|
||||||
|
latest_build: {
|
||||||
|
...MockWorkspace.latest_build,
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
...MockWorkspace.latest_build.resources[0],
|
||||||
|
agents: [{ ...MockWorkspaceAgent, lifecycle_state: lifecycle }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} satisfies Workspace,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "pages/Terminal",
|
||||||
|
component: RequireAuth,
|
||||||
|
parameters: {
|
||||||
|
layout: "fullscreen",
|
||||||
|
reactRouter: reactRouterParameters({
|
||||||
|
location: {
|
||||||
|
pathParams: {
|
||||||
|
username: `@${MockWorkspace.owner_name}`,
|
||||||
|
workspace: MockWorkspace.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routing: reactRouterOutlet(
|
||||||
|
{
|
||||||
|
path: `/:username/:workspace/terminal`,
|
||||||
|
},
|
||||||
|
<TerminalPage />,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
queries: [
|
||||||
|
{ key: ["me"], data: MockUser },
|
||||||
|
{ key: ["authMethods"], data: MockAuthMethodsAll },
|
||||||
|
{ key: ["hasFirstUser"], data: true },
|
||||||
|
{ key: ["buildInfo"], data: MockBuildInfo },
|
||||||
|
{ key: ["entitlements"], data: MockEntitlements },
|
||||||
|
{ key: ["experiments"], data: MockExperiments },
|
||||||
|
{ key: ["appearance"], data: MockAppearanceConfig },
|
||||||
|
{
|
||||||
|
key: getAuthorizationKey({ checks: permissionsToCheck }),
|
||||||
|
data: { editWorkspaceProxies: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<AuthProvider>
|
||||||
|
<Story />
|
||||||
|
</AuthProvider>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
} satisfies Meta<typeof TerminalPage>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof TerminalPage>;
|
||||||
|
|
||||||
|
export const Starting: Story = {
|
||||||
|
decorators: [withWebSocket],
|
||||||
|
parameters: {
|
||||||
|
...meta.parameters,
|
||||||
|
webSocket: [
|
||||||
|
{
|
||||||
|
event: "message",
|
||||||
|
// Copied and pasted this from browser
|
||||||
|
data: `[H[2J[1m[32m➜ [36mcoder[C[34mgit:([31mbq/refactor-web-term-notifications[34m) [33m✗`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queries: [...meta.parameters.queries, createWorkspaceWithAgent("starting")],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Ready: Story = {
|
||||||
|
decorators: [withWebSocket],
|
||||||
|
parameters: {
|
||||||
|
...meta.parameters,
|
||||||
|
webSocket: [
|
||||||
|
{
|
||||||
|
event: "message",
|
||||||
|
// Copied and pasted this from browser
|
||||||
|
data: `[H[2J[1m[32m➜ [36mcoder[C[34mgit:([31mbq/refactor-web-term-notifications[34m) [33m✗`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queries: [...meta.parameters.queries, createWorkspaceWithAgent("ready")],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StartError: Story = {
|
||||||
|
decorators: [withWebSocket],
|
||||||
|
parameters: {
|
||||||
|
...meta.parameters,
|
||||||
|
webSocket: [],
|
||||||
|
queries: [
|
||||||
|
...meta.parameters.queries,
|
||||||
|
createWorkspaceWithAgent("start_error"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConnectionError: Story = {
|
||||||
|
decorators: [withWebSocket],
|
||||||
|
parameters: {
|
||||||
|
...meta.parameters,
|
||||||
|
webSocket: [
|
||||||
|
{
|
||||||
|
event: "error",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queries: [...meta.parameters.queries, createWorkspaceWithAgent("ready")],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -45,25 +45,39 @@ export const withDashboardProvider = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MessageEvent = Record<"data", string>;
|
||||||
|
type CallbackFn = (ev?: MessageEvent) => void;
|
||||||
|
|
||||||
export const withWebSocket = (Story: FC, { parameters }: StoryContext) => {
|
export const withWebSocket = (Story: FC, { parameters }: StoryContext) => {
|
||||||
if (!parameters.webSocket) {
|
const events = parameters.webSocket;
|
||||||
console.warn(
|
|
||||||
"Looks like you forgot to add websocket messages to the story",
|
if (!events) {
|
||||||
);
|
console.warn("You forgot to add `parameters.webSocket` to your story");
|
||||||
|
return <Story />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const listeners = new Map<string, CallbackFn>();
|
||||||
|
let callEventsDelay: number;
|
||||||
|
|
||||||
// @ts-expect-error -- TS doesn't know about the global WebSocket
|
// @ts-expect-error -- TS doesn't know about the global WebSocket
|
||||||
window.WebSocket = function () {
|
window.WebSocket = function () {
|
||||||
return {
|
return {
|
||||||
addEventListener: (
|
addEventListener: (type: string, callback: CallbackFn) => {
|
||||||
type: string,
|
listeners.set(type, callback);
|
||||||
callback: (ev: Record<"data", string>) => void,
|
|
||||||
) => {
|
// Runs when the last event listener is added
|
||||||
if (type === "message") {
|
clearTimeout(callEventsDelay);
|
||||||
parameters.webSocket?.messages.forEach((message) => {
|
callEventsDelay = window.setTimeout(() => {
|
||||||
callback({ data: message });
|
for (const entry of events) {
|
||||||
});
|
const callback = listeners.get(entry.event);
|
||||||
}
|
|
||||||
|
if (callback) {
|
||||||
|
entry.event === "message"
|
||||||
|
? callback({ data: entry.data })
|
||||||
|
: callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
},
|
},
|
||||||
close: () => {},
|
close: () => {},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user