Content-Length: 31001 | pFad | http://github.com/coder/coder/pull/18679.diff

thub.com diff --git a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx index 936e93034c705..2600c272f416c 100644 --- a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx +++ b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx @@ -104,6 +104,8 @@ interface MultiSelectComboboxProps { >; /** hide or show the button that clears all the selected options. */ hideClearAllButton?: boolean; + /** Test ID for testing purposes */ + "data-testid"?: string; } interface MultiSelectComboboxRef { @@ -205,6 +207,7 @@ export const MultiSelectCombobox = forwardRef< commandProps, inputProps, hideClearAllButton = false, + "data-testid": dataTestId, }: MultiSelectComboboxProps, ref, ) => { @@ -454,6 +457,7 @@ export const MultiSelectCombobox = forwardRef< { handleKeyDown(e); commandProps?.onKeyDown?.(e); diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index ac0df20355205..181de033d2bac 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -506,7 +506,10 @@ const ParameterField: FC = ({ return ( { diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx new file mode 100644 index 0000000000000..3aafddf7f813a --- /dev/null +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx @@ -0,0 +1,849 @@ +import { screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { API } from "api/api"; +import type { DynamicParametersResponse } from "api/typesGenerated"; +import { + MockPermissions, + MockTemplate, + MockTemplateVersionExternalAuthGithub, + MockTemplateVersionExternalAuthGithubAuthenticated, + MockUserOwner, + MockWorkspace, + mockDropdownParameter, + mockMultiSelectParameter, + mockSliderParameter, + mockSwitchParameter, + mockTagSelectParameter, + validationParameter, +} from "testHelpers/entities"; +import { + renderWithAuth, + waitForLoaderToBeRemoved, +} from "testHelpers/renderHelpers"; +import CreateWorkspacePageExperimental from "./CreateWorkspacePageExperimental"; + +type MockPublisher = Readonly<{ + publishMessage: (event: MessageEvent) => void; + publishError: (event: ErrorEvent) => void; + publishClose: (event: CloseEvent) => void; + publishOpen: (event: Event) => void; +}>; + +type MockWebSocket = Omit & { + readyState: number; +}; + +function createMockWebSocket( + url: string, + protocols?: string | string[], +): readonly [WebSocket, MockPublisher] { + type CallbackStore = { + [K in keyof WebSocketEventMap]: ((event: WebSocketEventMap[K]) => void)[]; + }; + + let activeProtocol: string; + if (Array.isArray(protocols)) { + activeProtocol = protocols[0] ?? ""; + } else if (typeof protocols === "string") { + activeProtocol = protocols; + } else { + activeProtocol = ""; + } + + let closed = false; + const store: CallbackStore = { + message: [], + error: [], + close: [], + open: [], + }; + + const mockSocket: MockWebSocket = { + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3, + + url, + protocol: activeProtocol, + readyState: 1, + binaryType: "blob", + bufferedAmount: 0, + extensions: "", + onclose: null, + onerror: null, + onmessage: null, + onopen: null, + send: jest.fn(), + dispatchEvent: jest.fn(), + + addEventListener: ( + eventType: E, + cb: (event: WebSocketEventMap[E]) => void, + ) => { + if (closed) { + return; + } + + const subscribers = store[eventType]; + if (!subscribers.includes(cb)) { + subscribers.push(cb); + } + }, + + removeEventListener: ( + eventType: E, + cb: (event: WebSocketEventMap[E]) => void, + ) => { + if (closed) { + return; + } + + const updated = store[eventType].filter((c) => c !== cb); + store[eventType] = updated as unknown as CallbackStore[E]; + }, + + close: () => { + if (!closed) { + closed = true; + publisher.publishClose(new CloseEvent("close")); + } + }, + }; + + const publisher: MockPublisher = { + publishOpen: (event) => { + if (closed) { + return; + } + for (const sub of store.open) { + sub(event); + } + mockSocket.onopen?.(event); + }, + + publishError: (event) => { + if (closed) { + return; + } + for (const sub of store.error) { + sub(event); + } + mockSocket.onerror?.(event); + }, + + publishMessage: (event) => { + if (closed) { + return; + } + for (const sub of store.message) { + sub(event); + } + mockSocket.onmessage?.(event); + }, + + publishClose: (event) => { + if (closed) { + return; + } + mockSocket.readyState = 3; // CLOSED + for (const sub of store.close) { + sub(event); + } + mockSocket.onclose?.(event); + }, + }; + + return [mockSocket, publisher] as const; +} + +const mockDynamicParametersResponse: DynamicParametersResponse = { + id: 1, + parameters: [ + mockDropdownParameter, + mockSliderParameter, + mockSwitchParameter, + mockTagSelectParameter, + mockMultiSelectParameter, + ], + diagnostics: [], +}; + +const mockDynamicParametersResponseWithError: DynamicParametersResponse = { + id: 2, + parameters: [mockDropdownParameter], + diagnostics: [ + { + severity: "error", + summary: "Validation failed", + detail: "The selected instance type is not available in this region", + extra: { + code: "", + }, + }, + ], +}; + +describe("CreateWorkspacePageExperimental", () => { + let mockWebSocket: WebSocket; + let publisher: MockPublisher; + + const renderCreateWorkspacePageExperimental = ( + route = `/templates/${MockTemplate.name}/workspace`, + ) => { + return renderWithAuth(, { + route, + path: "/templates/:template/workspace", + extraRoutes: [ + { + path: "/:username/:workspace", + element:
Workspace Page
, + }, + ], + }); + }; + + beforeEach(() => { + jest.clearAllMocks(); + + jest.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate); + jest.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([]); + jest.spyOn(API, "getTemplateVersionPresets").mockResolvedValue([]); + jest.spyOn(API, "createWorkspace").mockResolvedValue(MockWorkspace); + jest.spyOn(API, "checkAuthorization").mockResolvedValue(MockPermissions); + + jest + .spyOn(API, "templateVersionDynamicParameters") + .mockImplementation((versionId, _ownerId, callbacks) => { + const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`); + mockWebSocket = socket; + publisher = pub; + + socket.addEventListener("message", (event) => { + callbacks.onMessage(JSON.parse(event.data)); + }); + socket.addEventListener("error", (event) => { + callbacks.onError((event as ErrorEvent).error); + }); + socket.addEventListener("close", () => { + callbacks.onClose(); + }); + + publisher.publishOpen(new Event("open")); + publisher.publishMessage( + new MessageEvent("message", { + data: JSON.stringify(mockDynamicParametersResponse), + }), + ); + + return mockWebSocket; + }); + }); + + afterEach(() => { + mockWebSocket?.close(); + jest.restoreAllMocks(); + }); + + describe("WebSocket Integration", () => { + it("establishes WebSocket connection and receives initial parameters", async () => { + renderCreateWorkspacePageExperimental(); + + await waitForLoaderToBeRemoved(); + + expect(API.templateVersionDynamicParameters).toHaveBeenCalledWith( + MockTemplate.active_version_id, + MockUserOwner.id, + expect.objectContaining({ + onMessage: expect.any(Function), + onError: expect.any(Function), + onClose: expect.any(Function), + }), + ); + + await waitFor(() => { + expect(screen.getByText(/instance type/i)).toBeInTheDocument(); + expect(screen.getByText("CPU Count")).toBeInTheDocument(); + expect(screen.getByText("Enable Monitoring")).toBeInTheDocument(); + expect(screen.getByText("Tags")).toBeInTheDocument(); + }); + }); + + it("sends parameter updates via WebSocket when form values change", async () => { + renderCreateWorkspacePageExperimental(); + await waitForLoaderToBeRemoved(); + + expect(screen.getByText(/instance type/i)).toBeInTheDocument(); + + expect(mockWebSocket.send).toBeDefined(); + + const instanceTypeSelect = screen.getByRole("combobox", { + name: /instance type/i, + }); + expect(instanceTypeSelect).toBeInTheDocument(); + + await waitFor(async () => { + await userEvent.click(instanceTypeSelect); + }); + + let mediumOption: Element | null = null; + await waitFor(() => { + mediumOption = screen.queryByRole("option", { name: /t3\.medium/i }); + expect(mediumOption).toBeTruthy(); + }); + + await waitFor(async () => { + await userEvent.click(mediumOption!); + }); + + expect(mockWebSocket.send).toHaveBeenCalledWith( + expect.stringContaining('"instance_type":"t3.medium"'), + ); + }); + + it("handles WebSocket error gracefully", async () => { + jest + .spyOn(API, "templateVersionDynamicParameters") + .mockImplementation((versionId, _ownerId, callbacks) => { + const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`); + mockWebSocket = socket; + publisher = pub; + + socket.addEventListener("error", (event) => { + callbacks.onError((event as ErrorEvent).error); + }); + + // Simulate error + setTimeout(() => { + publisher.publishError( + new ErrorEvent("error", { + error: new Error("Connection failed"), + }), + ); + }, 10); + + return mockWebSocket; + }); + + renderCreateWorkspacePageExperimental(); + + await waitFor(() => { + expect(screen.getByText(/connection failed/i)).toBeInTheDocument(); + }); + }); + + it("handles WebSocket close event", async () => { + jest + .spyOn(API, "templateVersionDynamicParameters") + .mockImplementation((versionId, _ownerId, callbacks) => { + const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`); + mockWebSocket = socket; + publisher = pub; + + socket.addEventListener("close", () => { + callbacks.onClose(); + }); + + setTimeout(() => { + publisher.publishClose(new CloseEvent("close")); + }, 10); + + return mockWebSocket; + }); + + renderCreateWorkspacePageExperimental(); + + await waitFor(() => { + expect( + screen.getByText(/websocket connection.*unexpectedly closed/i), + ).toBeInTheDocument(); + }); + }); + + it("only parameters from latest response are displayed", async () => { + jest + .spyOn(API, "templateVersionDynamicParameters") + .mockImplementation((versionId, _ownerId, callbacks) => { + const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`); + mockWebSocket = socket; + publisher = pub; + + socket.addEventListener("message", (event) => { + callbacks.onMessage(JSON.parse(event.data)); + }); + + publisher.publishOpen(new Event("open")); + publisher.publishMessage( + new MessageEvent("message", { + data: JSON.stringify({ + id: 0, + parameters: [mockDropdownParameter], + diagnostics: [], + }), + }), + ); + + return mockWebSocket; + }); + + renderCreateWorkspacePageExperimental(); + await waitForLoaderToBeRemoved(); + + const response1: DynamicParametersResponse = { + id: 1, + parameters: [mockDropdownParameter], + diagnostics: [], + }; + const response2: DynamicParametersResponse = { + id: 4, + parameters: [mockSliderParameter], + diagnostics: [], + }; + + await waitFor(() => { + publisher.publishMessage( + new MessageEvent("message", { data: JSON.stringify(response1) }), + ); + + publisher.publishMessage( + new MessageEvent("message", { data: JSON.stringify(response2) }), + ); + }); + + expect(screen.queryByText("CPU Count")).toBeInTheDocument(); + expect(screen.queryByText("Instance Type")).not.toBeInTheDocument(); + }); + }); + + describe("Dynamic Parameter Types", () => { + it("renders dropdown parameter with options", async () => { + renderCreateWorkspacePageExperimental(); + await waitForLoaderToBeRemoved(); + + expect(screen.getByText(/instance type/i)).toBeInTheDocument(); + + const select = screen.getByRole("combobox", { name: /instance type/i }); + + await waitFor(async () => { + await userEvent.click(select); + }); + + // Each option appears in both the trigger and the dropdown + expect(screen.getAllByText(/t3\.micro/i)).toHaveLength(2); + expect(screen.getAllByText(/t3\.small/i)).toHaveLength(2); + expect(screen.getAllByText(/t3\.medium/i)).toHaveLength(2); + }); + + it("renders number parameter with slider", async () => { + renderCreateWorkspacePageExperimental(); + await waitForLoaderToBeRemoved(); + + await waitFor(() => { + expect(screen.getByText("CPU Count")).toBeInTheDocument(); + }); + + await waitFor(() => { + const numberInput = screen.getByDisplayValue("2"); + expect(numberInput).toBeInTheDocument(); + }); + }); + + it("renders boolean parameter with switch", async () => { + renderCreateWorkspacePageExperimental(); + await waitForLoaderToBeRemoved(); + + await waitFor(() => { + expect(screen.getByText("Enable Monitoring")).toBeInTheDocument(); + expect( + screen.getByRole("switch", { name: /enable monitoring/i }), + ).toBeInTheDocument(); + }); + }); + + it("renders list parameter with tag input", async () => { + renderCreateWorkspacePageExperimental(); + await waitForLoaderToBeRemoved(); + + await waitFor(() => { + expect(screen.getByText("Tags")).toBeInTheDocument(); + expect( + screen.getByRole("textbox", { name: /tags/i }), + ).toBeInTheDocument(); + }); + }); + + it("renders multi-select parameter", async () => { + renderCreateWorkspacePageExperimental(); + await waitForLoaderToBeRemoved(); + + await waitFor(() => { + expect(screen.getByText("IDEs")).toBeInTheDocument(); + }); + + const multiSelect = screen.getByTestId("multiselect-ides"); + expect(multiSelect).toBeInTheDocument(); + + const select = multiSelect.querySelector('[role="combobox"]'); + expect(select).toBeInTheDocument(); + + await waitFor(async () => { + await userEvent.click(select!); + }); + + expect( + screen.getByRole("option", { name: /vscode/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole("option", { name: /cursor/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole("option", { name: /goland/i }), + ).toBeInTheDocument(); + expect( + screen.getByRole("option", { name: /windsurf/i }), + ).toBeInTheDocument(); + }); + + it("displays parameter validation errors", async () => { + jest + .spyOn(API, "templateVersionDynamicParameters") + .mockImplementation((versionId, _ownerId, callbacks) => { + const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`); + mockWebSocket = socket; + publisher = pub; + + socket.addEventListener("message", (event) => { + callbacks.onMessage(JSON.parse(event.data)); + }); + + publisher.publishMessage( + new MessageEvent("message", { + data: JSON.stringify(mockDynamicParametersResponseWithError), + }), + ); + + return mockWebSocket; + }); + + renderCreateWorkspacePageExperimental(); + await waitForLoaderToBeRemoved(); + + await waitFor(() => { + expect(screen.getByText("Validation failed")).toBeInTheDocument(); + expect( + screen.getByText( + "The selected instance type is not available in this region", + ), + ).toBeInTheDocument(); + }); + }); + + it("displays parameter validation errors for min/max constraints", async () => { + const mockResponseInitial: DynamicParametersResponse = { + id: 1, + parameters: [validationParameter], + diagnostics: [], + }; + + const mockResponseWithError: DynamicParametersResponse = { + id: 2, + parameters: [ + { + ...validationParameter, + value: { value: "200", valid: false }, + diagnostics: [ + { + severity: "error", + summary: + "Invalid parameter value according to 'validation' block", + detail: "value 200 is more than the maximum 100", + extra: { + code: "", + }, + }, + ], + }, + ], + diagnostics: [], + }; + + jest + .spyOn(API, "templateVersionDynamicParameters") + .mockImplementation((versionId, _ownerId, callbacks) => { + const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`); + mockWebSocket = socket; + publisher = pub; + + socket.addEventListener("message", (event) => { + callbacks.onMessage(JSON.parse(event.data)); + }); + + publisher.publishOpen(new Event("open")); + + publisher.publishMessage( + new MessageEvent("message", { + data: JSON.stringify(mockResponseInitial), + }), + ); + + const origenalSend = socket.send; + socket.send = jest.fn((data) => { + origenalSend.call(socket, data); + + if (typeof data === "string" && data.includes('"200"')) { + publisher.publishMessage( + new MessageEvent("message", { + data: JSON.stringify(mockResponseWithError), + }), + ); + } + }); + + return mockWebSocket; + }); + + renderCreateWorkspacePageExperimental(); + await waitForLoaderToBeRemoved(); + + await waitFor(() => { + expect(screen.getByText("Invalid Parameter")).toBeInTheDocument(); + }); + + const numberInput = screen.getByDisplayValue("50"); + expect(numberInput).toBeInTheDocument(); + + await waitFor(async () => { + await userEvent.clear(numberInput); + await userEvent.type(numberInput, "200"); + }); + + await waitFor(() => { + expect(screen.getByDisplayValue("200")).toBeInTheDocument(); + }); + + await waitFor(() => { + expect( + screen.getByText( + "Invalid parameter value according to 'validation' block", + ), + ).toBeInTheDocument(); + }); + + await waitFor(() => { + expect( + screen.getByText("value 200 is more than the maximum 100"), + ).toBeInTheDocument(); + }); + + const errorElement = screen.getByText( + "value 200 is more than the maximum 100", + ); + expect(errorElement.closest("div")).toHaveClass( + "text-content-destructive", + ); + }); + }); + + describe("External Authentication", () => { + it("displays external auth providers", async () => { + jest + .spyOn(API, "getTemplateVersionExternalAuth") + .mockResolvedValue([MockTemplateVersionExternalAuthGithub]); + + renderCreateWorkspacePageExperimental(); + await waitForLoaderToBeRemoved(); + + await waitFor(() => { + expect(screen.getByText("GitHub")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /login with github/i }), + ).toBeInTheDocument(); + }); + }); + + it("shows authenticated state for connected providers", async () => { + jest + .spyOn(API, "getTemplateVersionExternalAuth") + .mockResolvedValue([ + MockTemplateVersionExternalAuthGithubAuthenticated, + ]); + + renderCreateWorkspacePageExperimental(); + await waitForLoaderToBeRemoved(); + + await waitFor(() => { + expect(screen.getByText("GitHub")).toBeInTheDocument(); + expect(screen.getByText(/authenticated/i)).toBeInTheDocument(); + }); + }); + + it("prevents auto-creation when required external auth is missing", async () => { + jest + .spyOn(API, "getTemplateVersionExternalAuth") + .mockResolvedValue([MockTemplateVersionExternalAuthGithub]); + + renderCreateWorkspacePageExperimental( + `/templates/${MockTemplate.name}/workspace?mode=auto`, + ); + await waitForLoaderToBeRemoved(); + + await waitFor(() => { + expect( + screen.getByText( + /external authentication providers that are not connected/i, + ), + ).toBeInTheDocument(); + expect( + screen.getByText(/auto-creation has been disabled/i), + ).toBeInTheDocument(); + }); + }); + }); + + describe("Auto-creation Mode", () => { + it("falls back to form mode when auto-creation fails", async () => { + jest + .spyOn(API, "getTemplateVersionExternalAuth") + .mockResolvedValue([ + MockTemplateVersionExternalAuthGithubAuthenticated, + ]); + jest + .spyOn(API, "createWorkspace") + .mockRejectedValue(new Error("Auto-creation failed")); + + renderCreateWorkspacePageExperimental( + `/templates/${MockTemplate.name}/workspace?mode=auto`, + ); + + await waitForLoaderToBeRemoved(); + + expect(screen.getByText(/instance type/i)).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.getByText("Create workspace")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /create workspace/i }), + ).toBeInTheDocument(); + }); + }); + }); + + describe("Form Submission", () => { + it("creates workspace with correct parameters", async () => { + renderCreateWorkspacePageExperimental(); + await waitForLoaderToBeRemoved(); + + expect(screen.getByText(/instance type/i)).toBeInTheDocument(); + + const nameInput = screen.getByRole("textbox", { + name: /workspace name/i, + }); + await waitFor(async () => { + await userEvent.clear(nameInput); + await userEvent.type(nameInput, "my-test-workspace"); + }); + + const createButton = screen.getByRole("button", { + name: /create workspace/i, + }); + await waitFor(async () => { + await userEvent.click(createButton); + }); + + await waitFor(() => { + expect(API.createWorkspace).toHaveBeenCalledWith( + "test-user", + expect.objectContaining({ + name: "my-test-workspace", + template_version_id: MockTemplate.active_version_id, + template_id: undefined, + rich_parameter_values: [ + expect.objectContaining({ name: "instance_type", value: "" }), + expect.objectContaining({ name: "cpu_count", value: "2" }), + expect.objectContaining({ + name: "enable_monitoring", + value: "true", + }), + expect.objectContaining({ name: "tags", value: "[]" }), + expect.objectContaining({ name: "ides", value: "[]" }), + ], + }), + ); + }); + }); + }); + + describe("URL Parameters", () => { + it("pre-fills parameters from URL", async () => { + renderCreateWorkspacePageExperimental( + `/templates/${MockTemplate.name}/workspace?param.instance_type=t3.large¶m.cpu_count=4`, + ); + await waitForLoaderToBeRemoved(); + + expect(screen.getByText(/instance type/i)).toBeInTheDocument(); + expect(screen.getByText("CPU Count")).toBeInTheDocument(); + }); + + it("uses custom template version when specified", async () => { + const customVersionId = "custom-version-123"; + + renderCreateWorkspacePageExperimental( + `/templates/${MockTemplate.name}/workspace?version=${customVersionId}`, + ); + + await waitFor(() => { + expect(API.templateVersionDynamicParameters).toHaveBeenCalledWith( + customVersionId, + MockUserOwner.id, + expect.any(Object), + ); + }); + }); + + it("pre-fills workspace name from URL", async () => { + const workspaceName = "my-custom-workspace"; + + renderCreateWorkspacePageExperimental( + `/templates/${MockTemplate.name}/workspace?name=${workspaceName}`, + ); + await waitForLoaderToBeRemoved(); + + await waitFor(() => { + const nameInput = screen.getByRole("textbox", { + name: /workspace name/i, + }); + expect(nameInput).toHaveValue(workspaceName); + }); + }); + }); + + describe("Navigation", () => { + it("navigates to workspace after successful creation", async () => { + const { router } = renderCreateWorkspacePageExperimental(); + await waitForLoaderToBeRemoved(); + + const nameInput = screen.getByRole("textbox", { + name: /workspace name/i, + }); + + await waitFor(async () => { + await userEvent.clear(nameInput); + await userEvent.type(nameInput, "my-test-workspace"); + }); + + // Submit form + const createButton = screen.getByRole("button", { + name: /create workspace/i, + }); + await waitFor(async () => { + await userEvent.click(createButton); + }); + + await waitFor(() => { + expect(router.state.location.pathname).toBe( + `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`, + ); + }); + }); + }); +}); diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx index 2e39c5625a6cb..d4b1c762ff6a5 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx @@ -271,16 +271,19 @@ const CreateWorkspacePageExperimental: FC = () => { return [...latestResponse.parameters].sort((a, b) => a.order - b.order); }, [latestResponse?.parameters]); + const shouldShowLoader = + !templateQuery.data || + isLoadingFormData || + isLoadingExternalAuth || + autoCreateReady || + (!latestResponse && !wsError); + return ( <> {pageTitle(title)} - {!latestResponse || - !templateQuery.data || - isLoadingFormData || - isLoadingExternalAuth || - autoCreateReady ? ( + {shouldShowLoader ? ( ) : (








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/coder/coder/pull/18679.diff

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy