From f76800c01ee2cfd2c2f5382fcf65268b30d2a3bd Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 5 Mar 2024 18:10:34 +0000 Subject: [PATCH 1/7] Create terminal storybook --- site/src/contexts/auth/AuthProvider.tsx | 1 - .../TerminalPage/TerminalPage.stories.tsx | 95 +++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 site/src/pages/TerminalPage/TerminalPage.stories.tsx diff --git a/site/src/contexts/auth/AuthProvider.tsx b/site/src/contexts/auth/AuthProvider.tsx index 573c5c20a2565..e772ce51bf1e2 100644 --- a/site/src/contexts/auth/AuthProvider.tsx +++ b/site/src/contexts/auth/AuthProvider.tsx @@ -48,7 +48,6 @@ export const AuthContext = createContext( export const AuthProvider: FC = ({ children }) => { const queryClient = useQueryClient(); const meOptions = me(); - const userQuery = useQuery(meOptions); const authMethodsQuery = useQuery(authMethods()); const hasFirstUserQuery = useQuery(hasFirstUser()); diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx new file mode 100644 index 0000000000000..a7b27d6b3ed52 --- /dev/null +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -0,0 +1,95 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import TerminalPage from "./TerminalPage"; +import { AuthProvider } from "contexts/auth/AuthProvider"; +import { + reactRouterOutlet, + reactRouterParameters, +} from "storybook-addon-react-router-v6"; +import { RequireAuth } from "contexts/auth/RequireAuth"; +import { + MockAppearanceConfig, + MockAuthMethodsAll, + MockBuildInfo, + MockEntitlements, + MockExperiments, + MockUser, + MockWorkspace, + MockWorkspaceAgent, +} from "testHelpers/entities"; +import { getAuthorizationKey } from "api/queries/authCheck"; +import { permissionsToCheck } from "contexts/auth/permissions"; +import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; +import { Workspace } from "api/typesGenerated"; +import { withWebSocket } from "testHelpers/storybook"; + +const meta: 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`, + }, + , + ), + }), + 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: workspaceByOwnerAndNameKey( + MockWorkspace.owner_name, + MockWorkspace.name, + ), + data: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { + ...MockWorkspace.latest_build.resources[0], + agents: [{ ...MockWorkspaceAgent, lifecycle_state: "ready" }], + }, + ], + }, + } satisfies Workspace, + }, + { + key: getAuthorizationKey({ checks: permissionsToCheck }), + data: { editWorkspaceProxies: true }, + }, + ], + webSocket: { + // Copied and pasted this from browser + messages: [ + `➜ codergit:(bq/refactor-web-term-notifications) ✗`, + ], + }, + }, + decorators: [ + (Story) => ( + + + + ), + withWebSocket, + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; From 47a88f7278f7c8006a7a17427833de521b4dd48d Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 5 Mar 2024 18:14:14 +0000 Subject: [PATCH 2/7] Refactor story to match connected --- .../TerminalPage/TerminalPage.stories.tsx | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index a7b27d6b3ed52..138d161346588 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -22,7 +22,7 @@ import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; import { Workspace } from "api/typesGenerated"; import { withWebSocket } from "testHelpers/storybook"; -const meta: Meta = { +const meta = { title: "pages/Terminal", component: RequireAuth, parameters: { @@ -49,6 +49,37 @@ const meta: Meta = { { key: ["entitlements"], data: MockEntitlements }, { key: ["experiments"], data: MockExperiments }, { key: ["appearance"], data: MockAppearanceConfig }, + + { + key: getAuthorizationKey({ checks: permissionsToCheck }), + data: { editWorkspaceProxies: true }, + }, + ], + }, + decorators: [ + (Story) => ( + + + + ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Connected: Story = { + decorators: [withWebSocket], + parameters: { + ...meta.parameters, + webSocket: { + // Copied and pasted this from browser + messages: [ + `➜ codergit:(bq/refactor-web-term-notifications) ✗`, + ], + }, + queries: [ + ...meta.parameters.queries, { key: workspaceByOwnerAndNameKey( MockWorkspace.owner_name, @@ -67,29 +98,6 @@ const meta: Meta = { }, } satisfies Workspace, }, - { - key: getAuthorizationKey({ checks: permissionsToCheck }), - data: { editWorkspaceProxies: true }, - }, ], - webSocket: { - // Copied and pasted this from browser - messages: [ - `➜ codergit:(bq/refactor-web-term-notifications) ✗`, - ], - }, }, - decorators: [ - (Story) => ( - - - - ), - withWebSocket, - ], }; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; From 7436c97fc1fd71706b86bfadd6b3bdf563094fcb Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 6 Mar 2024 13:35:07 +0000 Subject: [PATCH 3/7] Fix fmt --- site/src/pages/TerminalPage/TerminalPage.stories.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index 138d161346588..40cf2a4058a6a 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -1,10 +1,13 @@ import type { Meta, StoryObj } from "@storybook/react"; -import TerminalPage from "./TerminalPage"; -import { AuthProvider } from "contexts/auth/AuthProvider"; import { reactRouterOutlet, reactRouterParameters, } from "storybook-addon-react-router-v6"; +import { getAuthorizationKey } from "api/queries/authCheck"; +import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; +import type { Workspace } from "api/typesGenerated"; +import { AuthProvider } from "contexts/auth/AuthProvider"; +import { permissionsToCheck } from "contexts/auth/permissions"; import { RequireAuth } from "contexts/auth/RequireAuth"; import { MockAppearanceConfig, @@ -16,11 +19,8 @@ import { MockWorkspace, MockWorkspaceAgent, } from "testHelpers/entities"; -import { getAuthorizationKey } from "api/queries/authCheck"; -import { permissionsToCheck } from "contexts/auth/permissions"; -import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; -import { Workspace } from "api/typesGenerated"; import { withWebSocket } from "testHelpers/storybook"; +import TerminalPage from "./TerminalPage"; const meta = { title: "pages/Terminal", From 2a2f35a264ad3e0a5a5ec71a9ced6222fb1a12d2 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 6 Mar 2024 14:02:19 +0000 Subject: [PATCH 4/7] Support error --- site/src/@types/storybook.d.ts | 9 ++-- .../BuildLogsDrawer.stories.tsx | 1 + .../TerminalPage/TerminalPage.stories.tsx | 53 +++++++++++-------- site/src/testHelpers/storybook.tsx | 18 ++++--- 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/site/src/@types/storybook.d.ts b/site/src/@types/storybook.d.ts index 90155110411ae..a4655c3c27d74 100644 --- a/site/src/@types/storybook.d.ts +++ b/site/src/@types/storybook.d.ts @@ -7,8 +7,11 @@ declare module "@storybook/react" { features?: FeatureName[]; experiments?: Experiments; queries?: { key: QueryKey; data: unknown }[]; - webSocket?: { - messages: string[]; - }; + webSocket?: + | { + event: "message"; + messages: string[]; + } + | { event: "error" }; } } diff --git a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx index c2414cfcd3069..96f46ebee74b8 100644 --- a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx +++ b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx @@ -47,6 +47,7 @@ export const Logs: Story = { decorators: [withWebSocket], parameters: { webSocket: { + event: "message", messages: MockWorkspaceBuildLogs.map((log) => JSON.stringify(log)), }, }, diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index 40cf2a4058a6a..2e07ee0d46aa2 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -49,7 +49,6 @@ const meta = { { key: ["entitlements"], data: MockEntitlements }, { key: ["experiments"], data: MockExperiments }, { key: ["appearance"], data: MockAppearanceConfig }, - { key: getAuthorizationKey({ checks: permissionsToCheck }), data: { editWorkspaceProxies: true }, @@ -68,36 +67,44 @@ const meta = { export default meta; type Story = StoryObj; -export const Connected: Story = { +const readyWorkspaceQuery = { + key: workspaceByOwnerAndNameKey(MockWorkspace.owner_name, MockWorkspace.name), + data: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { + ...MockWorkspace.latest_build.resources[0], + agents: [{ ...MockWorkspaceAgent, lifecycle_state: "ready" }], + }, + ], + }, + } satisfies Workspace, +}; + +export const OnMessage: Story = { decorators: [withWebSocket], parameters: { ...meta.parameters, webSocket: { + event: "message", // Copied and pasted this from browser messages: [ `➜ codergit:(bq/refactor-web-term-notifications) ✗`, ], }, - queries: [ - ...meta.parameters.queries, - { - key: workspaceByOwnerAndNameKey( - MockWorkspace.owner_name, - MockWorkspace.name, - ), - data: { - ...MockWorkspace, - latest_build: { - ...MockWorkspace.latest_build, - resources: [ - { - ...MockWorkspace.latest_build.resources[0], - agents: [{ ...MockWorkspaceAgent, lifecycle_state: "ready" }], - }, - ], - }, - } satisfies Workspace, - }, - ], + queries: [...meta.parameters.queries, readyWorkspaceQuery], + }, +}; + +export const OnError: Story = { + decorators: [withWebSocket], + parameters: { + ...meta.parameters, + webSocket: { + event: "error", + }, + queries: [...meta.parameters.queries, readyWorkspaceQuery], }, }; diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index 232ffb82aa103..1078ffaa62b9b 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -46,10 +46,11 @@ export const withDashboardProvider = ( }; export const withWebSocket = (Story: FC, { parameters }: StoryContext) => { - if (!parameters.webSocket) { - console.warn( - "Looks like you forgot to add websocket messages to the story", - ); + const webSocketConfig = parameters.webSocket; + + if (!webSocketConfig) { + console.warn("Your forgot to add the `parameters.webSocket` to your story"); + return ; } // @ts-expect-error -- TS doesn't know about the global WebSocket @@ -57,13 +58,16 @@ export const withWebSocket = (Story: FC, { parameters }: StoryContext) => { return { addEventListener: ( type: string, - callback: (ev: Record<"data", string>) => void, + callback: (ev?: Record<"data", string>) => void, ) => { - if (type === "message") { - parameters.webSocket?.messages.forEach((message) => { + if (type === "message" && webSocketConfig.event === "message") { + webSocketConfig.messages.forEach((message) => { callback({ data: message }); }); } + if (type === "error" && webSocketConfig.event === "error") { + callback(); + } }, close: () => {}, }; From 116b8d76b4d1bc6f2463ecde956700cab758d02a Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 6 Mar 2024 14:24:46 +0000 Subject: [PATCH 5/7] Stack multiple web socket events --- site/src/@types/storybook.d.ts | 8 +--- .../BuildLogsDrawer.stories.tsx | 6 +-- .../TerminalPage/TerminalPage.stories.tsx | 22 ++++++----- site/src/testHelpers/storybook.tsx | 38 ++++++++++++------- 4 files changed, 41 insertions(+), 33 deletions(-) diff --git a/site/src/@types/storybook.d.ts b/site/src/@types/storybook.d.ts index a4655c3c27d74..995fbb2e53a9b 100644 --- a/site/src/@types/storybook.d.ts +++ b/site/src/@types/storybook.d.ts @@ -3,15 +3,11 @@ import type { QueryKey } from "react-query"; import type { Experiments, FeatureName } from "api/typesGenerated"; declare module "@storybook/react" { + type WebSocketEvent = { event: "message"; data: string } | { event: "error" }; interface Parameters { features?: FeatureName[]; experiments?: Experiments; queries?: { key: QueryKey; data: unknown }[]; - webSocket?: - | { - event: "message"; - messages: string[]; - } - | { event: "error" }; + webSocket?: WebSocketEvent[]; } } diff --git a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx index 96f46ebee74b8..281de5cf036f9 100644 --- a/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx +++ b/site/src/pages/CreateTemplatePage/BuildLogsDrawer.stories.tsx @@ -46,9 +46,9 @@ export const Logs: Story = { }, decorators: [withWebSocket], parameters: { - webSocket: { + webSocket: MockWorkspaceBuildLogs.map((log) => ({ event: "message", - messages: MockWorkspaceBuildLogs.map((log) => JSON.stringify(log)), - }, + data: JSON.stringify(log), + })), }, }; diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index 2e07ee0d46aa2..f1b5917c335ef 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -87,13 +87,13 @@ export const OnMessage: Story = { decorators: [withWebSocket], parameters: { ...meta.parameters, - webSocket: { - event: "message", - // Copied and pasted this from browser - messages: [ - `➜ codergit:(bq/refactor-web-term-notifications) ✗`, - ], - }, + webSocket: [ + { + event: "message", + // Copied and pasted this from browser + data: `➜ codergit:(bq/refactor-web-term-notifications) ✗`, + }, + ], queries: [...meta.parameters.queries, readyWorkspaceQuery], }, }; @@ -102,9 +102,11 @@ export const OnError: Story = { decorators: [withWebSocket], parameters: { ...meta.parameters, - webSocket: { - event: "error", - }, + webSocket: [ + { + event: "error", + }, + ], queries: [...meta.parameters.queries, readyWorkspaceQuery], }, }; diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index 1078ffaa62b9b..6743f895287a9 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -45,29 +45,39 @@ export const withDashboardProvider = ( ); }; +type MessageEvent = Record<"data", string>; +type CallbackFn = (ev?: MessageEvent) => void; + export const withWebSocket = (Story: FC, { parameters }: StoryContext) => { - const webSocketConfig = parameters.webSocket; + const events = parameters.webSocket; - if (!webSocketConfig) { + if (!events) { console.warn("Your forgot to add the `parameters.webSocket` to your story"); return ; } + const listeners = new Map(); + let callEventsDelay: number; + // @ts-expect-error -- TS doesn't know about the global WebSocket window.WebSocket = function () { return { - addEventListener: ( - type: string, - callback: (ev?: Record<"data", string>) => void, - ) => { - if (type === "message" && webSocketConfig.event === "message") { - webSocketConfig.messages.forEach((message) => { - callback({ data: message }); - }); - } - if (type === "error" && webSocketConfig.event === "error") { - callback(); - } + addEventListener: (type: string, callback: CallbackFn) => { + listeners.set(type, callback); + + // Runs when the last event listener is added + clearTimeout(callEventsDelay); + callEventsDelay = window.setTimeout(() => { + for (const entry of events) { + const callback = listeners.get(entry.event); + + if (callback) { + entry.event === "message" + ? callback({ data: entry.data }) + : callback(); + } + } + }, 0); }, close: () => {}, }; From 0255669e302721b530a0e86b4dc8b2a540d908a3 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 6 Mar 2024 14:35:51 +0000 Subject: [PATCH 6/7] Add terminal storybook for other states --- .../TerminalPage/TerminalPage.stories.tsx | 70 ++++++++++++++----- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index f1b5917c335ef..d3caf7c315248 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -5,7 +5,7 @@ import { } from "storybook-addon-react-router-v6"; import { getAuthorizationKey } from "api/queries/authCheck"; import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; -import type { Workspace } from "api/typesGenerated"; +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"; @@ -22,6 +22,27 @@ import { 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, @@ -67,23 +88,22 @@ const meta = { export default meta; type Story = StoryObj; -const readyWorkspaceQuery = { - key: workspaceByOwnerAndNameKey(MockWorkspace.owner_name, MockWorkspace.name), - data: { - ...MockWorkspace, - latest_build: { - ...MockWorkspace.latest_build, - resources: [ - { - ...MockWorkspace.latest_build.resources[0], - agents: [{ ...MockWorkspaceAgent, lifecycle_state: "ready" }], - }, - ], - }, - } satisfies Workspace, +export const Starting: Story = { + decorators: [withWebSocket], + parameters: { + ...meta.parameters, + webSocket: [ + { + event: "message", + // Copied and pasted this from browser + data: `➜ codergit:(bq/refactor-web-term-notifications) ✗`, + }, + ], + queries: [...meta.parameters.queries, createWorkspaceWithAgent("starting")], + }, }; -export const OnMessage: Story = { +export const Ready: Story = { decorators: [withWebSocket], parameters: { ...meta.parameters, @@ -94,11 +114,23 @@ export const OnMessage: Story = { data: `➜ codergit:(bq/refactor-web-term-notifications) ✗`, }, ], - queries: [...meta.parameters.queries, readyWorkspaceQuery], + 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 OnError: Story = { +export const ConnectionError: Story = { decorators: [withWebSocket], parameters: { ...meta.parameters, @@ -107,6 +139,6 @@ export const OnError: Story = { event: "error", }, ], - queries: [...meta.parameters.queries, readyWorkspaceQuery], + queries: [...meta.parameters.queries, createWorkspaceWithAgent("ready")], }, }; From 3798d2a139c7bddae6c2365cffb67101f4402455 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 7 Mar 2024 11:12:01 -0300 Subject: [PATCH 7/7] Update site/src/testHelpers/storybook.tsx Co-authored-by: Asher --- site/src/testHelpers/storybook.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index 6743f895287a9..2daff79e1184b 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -52,7 +52,7 @@ export const withWebSocket = (Story: FC, { parameters }: StoryContext) => { const events = parameters.webSocket; if (!events) { - console.warn("Your forgot to add the `parameters.webSocket` to your story"); + console.warn("You forgot to add `parameters.webSocket` to your story"); return ; }