diff --git a/packages/playground/blueprints/src/index.ts b/packages/playground/blueprints/src/index.ts index 170e002a1c..3b774ac7e0 100644 --- a/packages/playground/blueprints/src/index.ts +++ b/packages/playground/blueprints/src/index.ts @@ -19,6 +19,8 @@ export { isBlueprintBundle, compileBlueprintV1, runBlueprintV1Steps, + InvalidBlueprintError, + BlueprintStepExecutionError, // BC: compileBlueprintV1 as compileBlueprint, @@ -49,6 +51,7 @@ export type { VFSReference, VFSResource, } from './lib/v1/resources'; +export { BlueprintFilesystemRequiredError } from './lib/v1/resources'; export * from './lib/steps'; export * from './lib/steps/handlers'; export type { @@ -61,7 +64,10 @@ export { getV2Runner } from './lib/v2/get-v2-runner'; export { runBlueprintV2 } from './lib/v2/run-blueprint-v2'; export type { BlueprintMessage } from './lib/v2/run-blueprint-v2'; -export { resolveRemoteBlueprint } from './lib/resolve-remote-blueprint'; +export { + resolveRemoteBlueprint, + BlueprintFetchError, +} from './lib/resolve-remote-blueprint'; export { wpContentFilesExcludedFromExport } from './lib/utils/wp-content-files-excluded-from-exports'; export { resolveRuntimeConfiguration } from './lib/resolve-runtime-configuration'; diff --git a/packages/playground/blueprints/src/lib/resolve-remote-blueprint.ts b/packages/playground/blueprints/src/lib/resolve-remote-blueprint.ts index b9e4a141dd..557c167518 100644 --- a/packages/playground/blueprints/src/lib/resolve-remote-blueprint.ts +++ b/packages/playground/blueprints/src/lib/resolve-remote-blueprint.ts @@ -6,6 +6,17 @@ import { } from '@wp-playground/storage'; import type { BlueprintBundle } from './types'; +export class BlueprintFetchError extends Error { + constructor( + message: string, + public readonly url: string, + options?: ErrorOptions + ) { + super(message, options); + this.name = 'BlueprintFetchError'; + } +} + /** * Resolves a remote blueprint from a URL. * @@ -15,34 +26,42 @@ import type { BlueprintBundle } from './types'; export async function resolveRemoteBlueprint( url: string ): Promise { - const response = await fetch(url, { - credentials: 'omit', - }); - if (!response.ok) { - throw new Error(`Failed to fetch blueprint from ${url}`); - } - const blueprintBytes = await response.arrayBuffer(); try { - const blueprintText = new TextDecoder().decode(blueprintBytes); - JSON.parse(blueprintText); + const response = await fetch(url, { + credentials: 'omit', + }); + if (!response.ok) { + throw new Error(`Failed to fetch blueprint from ${url}`); + } + const blueprintBytes = await response.arrayBuffer(); + try { + const blueprintText = new TextDecoder().decode(blueprintBytes); + JSON.parse(blueprintText); - // No exceptions, good! We're dealing with a JSON file. Let's - // resolve the "bundled" resources from the same remote URL. - return new OverlayFilesystem([ - new InMemoryFilesystem({ - 'blueprint.json': blueprintText, - }), - new FetchFilesystem({ - baseUrl: url, - }), - ]); - } catch { - // If the blueprint is not a JSON file, check if it's a ZIP file. - if (await looksLikeZipFile(blueprintBytes)) { - return ZipFilesystem.fromArrayBuffer(blueprintBytes); + // No exceptions, good! We're dealing with a JSON file. Let's + // resolve the "bundled" resources from the same remote URL. + return new OverlayFilesystem([ + new InMemoryFilesystem({ + 'blueprint.json': blueprintText, + }), + new FetchFilesystem({ + baseUrl: url, + }), + ]); + } catch { + // If the blueprint is not a JSON file, check if it's a ZIP file. + if (await looksLikeZipFile(blueprintBytes)) { + return ZipFilesystem.fromArrayBuffer(blueprintBytes); + } + throw new Error( + `Blueprint file at ${url} is neither a valid JSON nor a ZIP file.` + ); } - throw new Error( - `Blueprint file at ${url} is neither a valid JSON nor a ZIP file.` + } catch (error) { + throw new BlueprintFetchError( + `Blueprint file at ${url} is neither a valid JSON nor a ZIP file.`, + url, + { cause: error } ); } } diff --git a/packages/playground/blueprints/src/lib/steps/activate-plugin.ts b/packages/playground/blueprints/src/lib/steps/activate-plugin.ts index 300c34cfcb..4f7b0ca794 100644 --- a/packages/playground/blueprints/src/lib/steps/activate-plugin.ts +++ b/packages/playground/blueprints/src/lib/steps/activate-plugin.ts @@ -98,7 +98,7 @@ export const activatePlugin: StepHandler = async ( /** * Instead of trusting the activation response, check the active plugins list. * - * We try to discard any extra output via output buffering. The output of the script below + * We try to discard any extra output via output buffering. The output of the script below * end with `{"success": true}` or `{"success": false}`. Only `{"success": true}` is * treated as a successful plugin activation. */ @@ -152,9 +152,11 @@ export const activatePlugin: StepHandler = async ( } throw new Error( - `Plugin ${pluginPath} could not be activated – WordPress exited with no error. ` + - `Sometimes, when $_SERVER or site options are not configured correctly, ` + - `WordPress exits early with a 301 redirect. ` + - `Inspect the "debug" logs in the console for more details.` + `Plugin ${pluginPath} could not be activated - WordPress exited with exit code ${activatePluginResult.exitCode}. ` + + `Inspect the "debug" logs in the console for more details. Output headers: ${JSON.stringify( + activatePluginResult.headers, + null, + 2 + )}` ); }; diff --git a/packages/playground/blueprints/src/lib/steps/activate-theme.ts b/packages/playground/blueprints/src/lib/steps/activate-theme.ts index cf2e753c43..6343d1873a 100644 --- a/packages/playground/blueprints/src/lib/steps/activate-theme.ts +++ b/packages/playground/blueprints/src/lib/steps/activate-theme.ts @@ -67,10 +67,12 @@ export const activateTheme: StepHandler = async ( if (result.text !== 'Theme activated successfully') { logger.debug(result); throw new Error( - `Theme ${themeFolderName} could not be activated – WordPress exited with no error. ` + - `Sometimes, when $_SERVER or site options are not configured correctly, ` + - `WordPress exits early with a 301 redirect. ` + - `Inspect the "debug" logs in the console for more details` + `Theme ${themeFolderName} could not be activated - WordPress exited with exit code ${result.exitCode}. ` + + `Inspect the "debug" logs in the console for more details. Output headers: ${JSON.stringify( + result.headers, + null, + 2 + )}` ); } }; diff --git a/packages/playground/blueprints/src/lib/v1/compile.ts b/packages/playground/blueprints/src/lib/v1/compile.ts index 00e6c87d1f..7e8c9924a3 100644 --- a/packages/playground/blueprints/src/lib/v1/compile.ts +++ b/packages/playground/blueprints/src/lib/v1/compile.ts @@ -41,6 +41,50 @@ const keyedStepHandlers = { */ import blueprintValidator from '../../../public/blueprint-schema-validator'; import { defaultWpCliPath, defaultWpCliResource } from '../steps/wp-cli'; +import type { ErrorObject } from 'ajv'; + +export class InvalidBlueprintError extends Error { + constructor(message: string, public readonly validationErrors?: unknown) { + super(message); + this.name = 'InvalidBlueprintError'; + } +} + +/** + * Error thrown when a single Blueprint step fails during execution. + * + * This error carries structured information about the failing step so that + * consumers (e.g. the Playground UI) do not have to parse human‑readable + * error messages to understand what went wrong. + */ +export class BlueprintStepExecutionError extends Error { + public readonly stepNumber: number; + public readonly step: StepDefinition; + public readonly messages: string[]; + + constructor(options: { + stepNumber: number; + step: StepDefinition; + cause: unknown; + }) { + const { stepNumber, step, cause } = options; + const causeError = + cause instanceof Error ? cause : new Error(String(cause)); + const baseMessage = `Error when executing the blueprint step #${stepNumber}`; + const fullMessage = causeError.message + ? `${baseMessage}: ${causeError.message}` + : baseMessage; + + super(fullMessage, { cause: causeError }); + this.name = 'BlueprintStepExecutionError'; + this.stepNumber = stepNumber; + this.step = step; + this.messages = (causeError.message || '') + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + } +} export type CompiledV1Step = (php: UniversalPHP) => Promise | void; @@ -307,14 +351,17 @@ function compileBlueprintJson( const { valid, errors } = validateBlueprint(blueprint); if (!valid) { - const e = new Error( - `Invalid blueprint: ${errors![0].message} at ${ - errors![0].instancePath - }` + const formattedErrors = formatValidationErrors(blueprint, errors ?? []); + + throw new InvalidBlueprintError( + `Invalid Blueprint: The Blueprint does not conform to the schema.\n\n` + + `Found ${ + errors!.length + } validation error(s):\n\n${formattedErrors}\n\n` + + `Please review your Blueprint and fix these issues. ` + + `Learn more about the Blueprint format: https://wordpress.github.io/wordpress-playground/blueprints/data-format`, + errors ); - // Attach Ajv output to the thrown object for easier debugging - (e as any).errors = errors; - throw e; } onBlueprintValidated(blueprint); @@ -378,12 +425,12 @@ function compileBlueprintJson( const result = await run(playground); onStepCompleted(result, step); } catch (e) { - throw new Error( - `Error when executing the blueprint step #${i} (${JSON.stringify( - step - )}) ${e instanceof Error ? `: ${e.message}` : e}`, - { cause: e } - ); + const stepNumber = Number(i) + 1; + throw new BlueprintStepExecutionError({ + stepNumber, + step, + cause: e, + }); } } } finally { @@ -419,6 +466,80 @@ function compileBlueprintJson( }; } +function formatValidationErrors( + blueprint: BlueprintV1Declaration, + errors: ErrorObject[] +) { + return errors + .map((err, index) => { + const path = err.instancePath || '/'; + let message = err.message || 'validation failed'; + + // For "additional properties" errors, highlight the actual problematic key + let highlightedSnippet = ''; + if (message.includes('must NOT have additional properties')) { + // Extract the property name from the error params + const additionalProperty = (err.params as any) + ?.additionalProperty; + if (additionalProperty) { + message = `has unexpected property "${additionalProperty}"`; + + // Try to show the offending key highlighted + try { + const pathParts = path.split('/').filter(Boolean); + let currentValue: any = blueprint; + for (const part of pathParts) { + if ( + currentValue && + typeof currentValue === 'object' + ) { + currentValue = currentValue[part]; + } + } + + if (currentValue && typeof currentValue === 'object') { + const offendingValue = + currentValue[additionalProperty]; + const valueStr = JSON.stringify(offendingValue); + highlightedSnippet = `\n "${additionalProperty}": ${valueStr}\n ${'^'.repeat( + additionalProperty.length + 2 + )} This property is not recognized`; + } + } catch { + // If we can't extract context, that's okay + } + } + } else { + // For other errors, try to extract the offending value + try { + const pathParts = path.split('/').filter(Boolean); + let currentValue: any = blueprint; + for (const part of pathParts) { + if (currentValue && typeof currentValue === 'object') { + currentValue = currentValue[part]; + } + } + if (currentValue !== undefined) { + const valueStr = JSON.stringify(currentValue, null, 2); + // Limit snippet length + const snippet = + valueStr.length > 200 + ? valueStr.substring(0, 200) + '...' + : valueStr; + highlightedSnippet = `\n Value: ${snippet}`; + } + } catch { + // If we can't extract context, that's okay + } + } + + return `${ + index + 1 + }. At path "${path}": ${message}${highlightedSnippet}`; + }) + .join('\n\n'); +} + export function validateBlueprint(blueprintMaybe: object) { const valid = blueprintValidator(blueprintMaybe); if (valid) { diff --git a/packages/playground/blueprints/src/lib/v1/resources.ts b/packages/playground/blueprints/src/lib/v1/resources.ts index 93ad66957c..7702283dca 100644 --- a/packages/playground/blueprints/src/lib/v1/resources.ts +++ b/packages/playground/blueprints/src/lib/v1/resources.ts @@ -19,6 +19,25 @@ import { StreamedFile } from '@php-wasm/stream-compression'; import type { StreamBundledFile } from './types'; import { createDotGitDirectory } from '@wp-playground/storage'; +const BUNDLED_RESOURCE_ERROR_MESSAGE = + 'Blueprint resource of type "bundled" requires a filesystem.\n\n' + + 'This Blueprint refers to files that should be bundled with it (like images, plugins, or themes), ' + + 'but the filesystem needed to access these files is not available. This usually happens when:\n\n' + + "1. You're trying to load a Blueprint as a standalone JSON file that was meant to be part of a bundle\n" + + '2. The Blueprint was not packaged correctly as a blueprint.zip file\n\n' + + 'To fix this:\n' + + "• If you're loading from a URL, make sure all referenced files are accessible relative to the Blueprint file\n" + + "• If you're using a blueprint.zip file, ensure it contains all the files referenced in the Blueprint\n" + + '• Check that the "resource": "bundled" references in your Blueprint match actual files in your bundle\n\n' + + 'Learn more about Blueprint resources: https://wordpress.github.io/wordpress-playground/blueprints/data-format#resources'; + +export class BlueprintFilesystemRequiredError extends Error { + constructor(message = BUNDLED_RESOURCE_ERROR_MESSAGE) { + super(message); + this.name = 'BlueprintFilesystemRequiredError'; + } +} + export type { FileTree }; export const ResourceTypes = [ 'vfs', @@ -198,9 +217,7 @@ export abstract class Resource { break; case 'bundled': if (!streamBundledFile) { - throw new Error( - 'Filesystem is required for blueprint resources' - ); + throw new BlueprintFilesystemRequiredError(); } resource = new BundledResource( ref, @@ -369,31 +386,9 @@ export abstract class FetchResource extends Resource { return new File([await response.blob()], filename); } catch (e) { throw new Error( - `Could not download "${url}". - Check if the URL is correct and the server is reachable. - If it is reachable, the server might be blocking the request. - Check the browser console and network tabs for more information. - - ## Does the console show the error "No 'Access-Control-Allow-Origin' header"? - - This means the server that hosts your file does not allow requests from other sites - (cross-origin requests, or CORS). You need to move the asset to a server that allows - cross-origin file downloads. Learn more about CORS at - https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS. - - If your file is on GitHub, load it from "raw.githubusercontent.com". - Here's how to do that: - - 1. Start with the original GitHub URL of the file. For example: - https://github.com/username/repository/blob/branch/filename. - 2. Replace "github.com" with "raw.githubusercontent.com". - 3. Remove the "/blob/" part of the URL. - - The resulting URL should look like this: - https://raw.githubusercontent.com/username/repository/branch/filename - - Error: - ${e}` + `Could not download "${url}".\n\n` + + `Confirm that the URL is correct, the server is reachable, and the file is` + + `actually served at that URL. Original error: \n ${e}` ); } } diff --git a/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-is-selected.tsx b/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-is-selected.tsx index aea36cef8f..eb2fc991bb 100644 --- a/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-is-selected.tsx +++ b/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-is-selected.tsx @@ -16,8 +16,7 @@ import { import { redirectTo } from '../../lib/state/url/router'; import { logger } from '@php-wasm/logger'; import { usePrevious } from '../../lib/hooks/use-previous'; -import { modalSlugs } from '../layout'; -import { setActiveModal } from '../../lib/state/redux/slice-ui'; +import { modalSlugs, setActiveModal } from '../../lib/state/redux/slice-ui'; import { selectClientBySiteSlug } from '../../lib/state/redux/slice-clients'; import { randomSiteName } from '../../lib/state/redux/random-site-name'; diff --git a/packages/playground/website/src/components/error-report-modal/index.tsx b/packages/playground/website/src/components/error-report-modal/index.tsx deleted file mode 100644 index 4b56e7cef6..0000000000 --- a/packages/playground/website/src/components/error-report-modal/index.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { logger } from '@php-wasm/logger'; -import { TextareaControl, TextControl } from '@wordpress/components'; -import type { BlueprintV1Declaration } from '@wp-playground/blueprints'; -import { useDispatch } from 'react-redux'; -import type { - PlaygroundDispatch, - PlaygroundReduxState, -} from '../../lib/state/redux/store'; -import { useAppSelector } from '../../lib/state/redux/store'; -import { setActiveModal } from '../../lib/state/redux/slice-ui'; -import { Modal } from '../../components/modal'; -import ModalButtons from '../modal/modal-buttons'; - -export function ErrorReportModal(props: { blueprint: BlueprintV1Declaration }) { - const activeModal = useAppSelector( - (state: PlaygroundReduxState) => state.ui.activeModal - ); - const dispatch: PlaygroundDispatch = useDispatch(); - const [loading, setLoading] = useState(false); - const [text, setText] = useState(''); - const [logs, setLogs] = useState(''); - const [url, setUrl] = useState(''); - const [submitted, setSubmitted] = useState(false); - const [submitError, setSubmitError] = useState(''); - - useEffect(() => { - resetForm(); - setLogs(logger.getLogs().join('\n')); - setUrl(window.location.href); - }, [activeModal, logs, setLogs]); - - function resetForm() { - setText(''); - setLogs(''); - setUrl(''); - } - - function resetSubmission() { - setSubmitted(false); - setSubmitError(''); - } - - function onClose() { - dispatch(setActiveModal(null)); - resetForm(); - resetSubmission(); - } - - function getContext() { - return { - ...props.blueprint.preferredVersions, - userAgent: navigator.userAgent, - ...((window.performance as any)?.memory ?? {}), - window: { - width: window.innerWidth, - height: window.innerHeight, - }, - }; - } - - async function onSubmit() { - setLoading(true); - const formdata = new FormData(); - formdata.append('description', text); - if (logs) { - formdata.append('logs', logs); - } - if (url) { - formdata.append('url', url); - } - formdata.append('context', JSON.stringify(getContext())); - formdata.append('blueprint', JSON.stringify(props.blueprint)); - try { - const response = await fetch( - 'https://playground.wordpress.net/logger.php', - { - method: 'POST', - body: formdata, - } - ); - setSubmitted(true); - - const body = await response.json(); - if (!body.ok) { - throw new Error(body.error); - } - - setSubmitError(''); - resetForm(); - } catch (e) { - setSubmitError((e as Error).message); - } finally { - setLoading(false); - } - } - - function getTitle() { - if (!submitted) { - return 'Report error'; - } else if (submitError) { - return 'Failed to report the error'; - } else { - return 'Thank you for reporting the error'; - } - } - - function getContent() { - if (!submitted) { - return ( - <> - Playground crashed because of an error. You can help resolve - the issue by sharing the error details with us. - - ); - } else if (submitError) { - return ( - <> - We were unable to submit the error report. Please try again - or open an{' '} - - issue on GitHub. - - - ); - } else { - return ( - <> - Your report has been submitted to the{' '} - - Making WordPress #playground-logs Slack channel - {' '} - and will be reviewed by the team. - - ); - } - } - - /** - * Show the form if the error has not been submitted or if there was an error - * submitting it. - * - * @return {boolean} - */ - function showForm() { - return !submitted || submitError; - } - - return ( - -

{getContent()}

- {showForm() && ( - <> - - - - - - - - )} -
- ); -} diff --git a/packages/playground/website/src/components/layout/index.tsx b/packages/playground/website/src/components/layout/index.tsx index 4ef23807bd..0d9fa650c7 100644 --- a/packages/playground/website/src/components/layout/index.tsx +++ b/packages/playground/website/src/components/layout/index.tsx @@ -2,12 +2,8 @@ import css from './style.module.css'; import { SiteManager } from '../site-manager'; import { CSSTransition } from 'react-transition-group'; -import type { - PlaygroundDispatch, - PlaygroundReduxState, -} from '../../lib/state/redux/store'; -import { useAppSelector, useAppDispatch } from '../../lib/state/redux/store'; -import { addCrashListener, logger } from '@php-wasm/logger'; +import type { PlaygroundReduxState } from '../../lib/state/redux/store'; +import { useAppSelector } from '../../lib/state/redux/store'; import type { BlueprintV1Declaration } from '@wp-playground/blueprints'; import { useState, useEffect, useRef } from 'react'; import { acquireOAuthTokenIfNeeded } from '../../github/acquire-oauth-token-if-needed'; @@ -17,7 +13,6 @@ import { asPullRequestAction } from '../../github/github-export-form/form'; import { GithubImportModal } from '../../github/github-import-form'; import { GitHubOAuthGuardModal } from '../../github/github-oauth-guard'; import { asContentType } from '../../github/import-from-github'; -import { ErrorReportModal } from '../error-report-modal'; import { LogModal } from '../log-modal'; import { StartErrorModal } from '../start-error-modal'; import type { DisplayMode } from '../playground-viewport'; @@ -25,31 +20,15 @@ import { supportedDisplayModes, PlaygroundViewport, } from '../playground-viewport'; -import { setActiveModal } from '../../lib/state/redux/slice-ui'; import { ImportFormModal } from '../import-form-modal'; import { PreviewPRModal } from '../../github/preview-pr'; import { MissingSiteModal } from '../missing-site-modal'; import { RenameSiteModal } from '../rename-site-modal'; import { SaveSiteModal } from '../save-site-modal'; +import { modalSlugs } from '../../lib/state/redux/slice-ui'; import { GitHubPrivateRepoAuthModal } from '../github-private-repo-auth-modal'; acquireOAuthTokenIfNeeded(); - -export const modalSlugs = { - LOG: 'log', - ERROR_REPORT: 'error-report', - START_ERROR: 'start-error', - IMPORT_FORM: 'import-form', - GITHUB_IMPORT: 'github-import', - GITHUB_EXPORT: 'github-export', - GITHUB_PRIVATE_REPO_AUTH: 'github-private-repo-auth', - PREVIEW_PR_WP: 'preview-pr-wordpress', - PREVIEW_PR_GUTENBERG: 'preview-pr-gutenberg', - MISSING_SITE_PROMPT: 'missing-site-prompt', - RENAME_SITE: 'rename-site', - SAVE_SITE: 'save-site', -}; - const displayMode = getDisplayModeFromQuery(); function getDisplayModeFromQuery(): DisplayMode { const query = new URLSearchParams(document.location.search); @@ -102,8 +81,6 @@ export function Layout() { * top level, like here, or contextual to where the "Show modal" button is rendered. */ function Modals(blueprint: BlueprintV1Declaration) { - const dispatch: PlaygroundDispatch = useAppDispatch(); - const query = new URL(document.location.href).searchParams; const [githubExportFiles, setGithubExportFiles] = useState(); @@ -148,15 +125,8 @@ function Modals(blueprint: BlueprintV1Declaration) { return values; }); - useEffect(() => { - addCrashListener(logger, (e) => { - const error = e as CustomEvent; - if (error.detail?.source === 'php-wasm') { - dispatch(setActiveModal(modalSlugs.ERROR_REPORT)); - } - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => {}, []); const currentModal = useAppSelector( (state: PlaygroundReduxState) => state.ui.activeModal @@ -164,8 +134,6 @@ function Modals(blueprint: BlueprintV1Declaration) { if (currentModal === modalSlugs.LOG) { return ; - } else if (currentModal === modalSlugs.ERROR_REPORT) { - return ; } else if (currentModal === modalSlugs.START_ERROR) { return ; } else if (currentModal === modalSlugs.IMPORT_FORM) { diff --git a/packages/playground/website/src/components/playground-viewport/index.tsx b/packages/playground/website/src/components/playground-viewport/index.tsx index 8d3eabca10..570c3be60b 100644 --- a/packages/playground/website/src/components/playground-viewport/index.tsx +++ b/packages/playground/website/src/components/playground-viewport/index.tsx @@ -4,21 +4,21 @@ import css from './style.module.css'; import BrowserChrome from '../browser-chrome'; import { selectActiveSiteError, + selectActiveSiteErrorDetails, useActiveSite, useAppDispatch, useAppSelector, } from '../../lib/state/redux/store'; import { removeClientInfo } from '../../lib/state/redux/slice-clients'; import { bootSiteClient } from '../../lib/state/redux/boot-site-client'; -import type { SiteError } from '../../lib/state/redux/slice-ui'; -import { Button, Spinner } from '@wordpress/components'; +import { Spinner } from '@wordpress/components'; import { - removeSite, selectSiteBySlug, selectSitesLoaded, selectTemporarySites, } from '../../lib/state/redux/slice-sites'; import classNames from 'classnames'; +import { SiteErrorModal } from '../site-error-modal'; export const supportedDisplayModes = [ 'browser-full-screen', @@ -200,138 +200,26 @@ export const JustViewport = function JustViewport({ }, [siteSlug, iframeRef, runtimeConfigString]); const error = useAppSelector(selectActiveSiteError); - - if (error) { - return ( -
-
- -
-
- ); - } - - return ( -