Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Use selected site settings #1707

Closed
wants to merge 2 commits into from
Closed
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
18 changes: 11 additions & 7 deletions packages/playground/website/src/components/site-manager/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { SiteManagerSidebar } from './site-manager-sidebar';
import { __experimentalUseNavigator as useNavigator } from '@wordpress/components';
import store, { selectSite } from '../../lib/redux-store';
import store, { selectSite, type AppState } from '../../lib/redux-store';
import { useSelector } from 'react-redux';
import { siteInfoToUrl } from '../../lib/query-api';

import css from './style.module.css';

Expand All @@ -14,6 +16,7 @@ export function SiteManager({
siteViewRef: React.RefObject<HTMLDivElement>;
}) {
const { goTo } = useNavigator();
const sites = useSelector((state: AppState) => state.siteListing.sites);

const shouldHideSiteManagerOnSiteChange = () => {
/**
Expand All @@ -28,13 +31,14 @@ export function SiteManager({
};

const onSiteClick = async (siteSlug: string) => {
onSiteChange(siteSlug);
const url = new URL(window.location.href);
if (siteSlug) {
url.searchParams.set('site-slug', siteSlug);
} else {
url.searchParams.delete('site-slug');
const selectedSite = sites.find((site) => site.slug === siteSlug);
if (!selectedSite) {
return;
}

onSiteChange(siteSlug);

const url = siteInfoToUrl(new URL(window.location.href), selectedSite);
window.history.pushState({}, '', url.toString());

await store.dispatch(selectSite(siteSlug));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ export function SiteView({
iframeRef,
siteViewRef,
}: {
blueprint: Blueprint;
currentConfiguration: PlaygroundConfiguration;
blueprint?: Blueprint;
currentConfiguration?: PlaygroundConfiguration;
storage: StorageType;
playground?: PlaygroundClient;
url?: string;
Expand Down Expand Up @@ -173,6 +173,10 @@ export function SiteView({
}
};

if (blueprint === undefined || currentConfiguration === undefined) {
return null;
}

return (
<PlaygroundContext.Provider
value={{
Expand Down
110 changes: 110 additions & 0 deletions packages/playground/website/src/lib/query-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Blueprint } from '@wp-playground/blueprints';
import { makeBlueprint } from './make-blueprint';
import { type SiteInfo } from './site-storage';

const defaultPhpVersion = '8.0';

/**
* Get a URL with query params representing the specified site settings.
*
* @param baseUrl The current Playground URL
* @param siteInfo The site info to convert to Playground query params
* @returns URL with query params representing the site info
*/
export function siteInfoToUrl(baseUrl: URL, siteInfo: SiteInfo): URL {
Copy link
Collaborator

@adamziel adamziel Aug 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha! We've hit the runtime configuration vs site configuration problem (see "Runtime setup options"). This is an important one. Let's not rush a solution but take it slow and methodically. We're effectively building a v1 of the runtime configuration interface here.

Let's treat things like languages, themes, plugins, multisite etc. as build-time features that are only relevant when the site is first created. If we tried re-applying them to an existing site, we'd end up with, say, two copies of the same theme – that's not what the user expects.

That leaves us with settings that don't trigger any filesystem– or database–level updates in /wordpress. For sure that's networking, PHP version, and landing page. Probably also login, even though it causes some database writes. What else?

We don't yet have the plumbing to hotswap the WordPress version so let's remove the "WP Version" from the "edit site" form for now. Same for the language and the storage option. That's tangential but relevant.

Here's another question: When user changes the runtime configuration of an existing site, how will we save that information? We can either alter the original Blueprint, or store runtime configuration in another file. What would you do here?

Tangentially related: Down the road we will provide a way of applying a Blueprint to an existing site. At that time, we will want to combine the new Blueprint with the original one used to boot that site. There's nothing immediately actionable today. Regardless of the direction this PR takes, I'm just noticing the problem of rewriting/merging Blueprints will come back on more than one occasion.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder what would be the ripple effect of decoupling the runtime options across JS API, Blueprints API, Query API, if any.

For example, imagine a version of the Query API that distinguishes between "site build options" and "runtime options":

https://playground.wordpress.net/?build[plugin]=gutenberg&runtime[networking]=yes

These long names would be difficult to type and to remember so our current short parameters (?plugin=gutenberg&networking=yes) seem to be a more useful choice.

However, Blueprints and JS API are quite verbose already so maybe there may be an opportunity for an API refresh in there. If not, perhaps the documentation could distinguish between the two types of configuration options 🤔

Copy link
Collaborator

@adamziel adamziel Aug 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, notice this translates Blueprint->Query args only for Playground to do Query args->Blueprint. What if we passed the Blueprint directly? Or even just the site slug, since we know the config for each slug?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be highly useful to have stable site URLs. Say I go to playground.wordpress.net/my–base–theme and it always brings up the same site. Also, the site itself wouldn't use a random numeric scope, but the slug

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we could pair that with github autocommit and offer public site urls, e.g. public.playground.wordpress.net/adamziel/blog

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And that could later form v1 of collaboration and site sync. Cc @akirk @griffbrad @dawidurbanski @dmsnell

const newUrl = new URL(baseUrl);
newUrl.search = '';

const query = newUrl.searchParams;
query.set('php', siteInfo.phpVersion || defaultPhpVersion);
query.set('wp', siteInfo.wpVersion || 'latest');
query.set('site-slug', siteInfo.slug);

const preservedQueryParams = ['language', 'networking', 'storage'];
for (const key of preservedQueryParams) {
if (baseUrl.searchParams.has(key)) {
query.set(key, baseUrl.searchParams.get(key)!);
}
}

// TODO as playgroundFeatures
// if (blueprint.features?.networking) {
// query.set('networking', 'yes');
// }

// TODO
// if (blueprint.language) {
// query.set('language', blueprint.language);
// }

// TODO
// if (blueprint.theme) {
// query.set('theme', blueprint.theme);
// }

// TODO
// query.set('login', blueprint.login ? 'yes' : 'no');
// query.set('multisite', blueprint.multisite ? 'yes' : 'no');

// TODO
// if (blueprint.plugins && blueprint.plugins.length > 0) {
// blueprint.plugins.forEach(plugin => query.append('plugin', plugin));
//}

// TODO
// if (blueprint.landingPage) {
// query.set('url', blueprint.landingPage);
// }

// TODO as playgroundFeatures
// if (blueprint.phpExtensionBundles && blueprint.phpExtensionBundles.length > 0) {
// blueprint.phpExtensionBundles.forEach(bundle => query.append('php-extension-bundle', bundle));
// }

// TODO: Maybe
// if (blueprint.importSite) {
// query.set('import-site', blueprint.importSite);
// }

// TODO: Maybe
// if (blueprint.importWxr) {
// query.set('import-wxr', blueprint.importWxr);
// }

return newUrl;
}

/**
* Create a Blueprint based on Playground query params.
*
* @param query Query params to convert to a Blueprint
* @returns A Blueprint reflecting the settings specified by query params
*/
export function queryParamsToBlueprint(query: URLSearchParams): Blueprint {
const features: Blueprint['features'] = {};

/**
* Networking is disabled by default, so we only need to enable it
* if the query param is explicitly set to "yes".
*/
if (query.get('networking') === 'yes') {
features['networking'] = true;
}
const blueprint = makeBlueprint({
php: query.get('php') || defaultPhpVersion,
wp: query.get('wp') || 'latest',
theme: query.get('theme') || undefined,
login: !query.has('login') || query.get('login') === 'yes',
multisite: query.get('multisite') === 'yes',
features,
plugins: query.getAll('plugin'),
landingPage: query.get('url') || undefined,
phpExtensionBundles: query.getAll('php-extension-bundle') || [],
importSite: query.get('import-site') || undefined,
importWxr:
query.get('import-wxr') || query.get('import-content') || undefined,
language: query.get('language') || undefined,
});

return blueprint;
}
2 changes: 1 addition & 1 deletion packages/playground/website/src/lib/redux-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export type SiteListing = {
};

// Define the state types
interface AppState {
export interface AppState {
activeModal: string | null;
offline: boolean;
siteListing: SiteListing;
Expand Down
28 changes: 2 additions & 26 deletions packages/playground/website/src/lib/resolve-blueprint.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Blueprint } from '@wp-playground/client';
import { makeBlueprint } from './make-blueprint';
import { queryParamsToBlueprint } from './query-api';

const query = new URL(document.location.href).searchParams;
const fragment = decodeURI(document.location.hash || '#').substring(1);
Expand Down Expand Up @@ -50,31 +50,7 @@ export async function resolveBlueprint() {
// If no blueprint was passed, prepare one based on the query params.
// @ts-ignore
if (typeof blueprint === 'undefined') {
const features: Blueprint['features'] = {};
/**
* Networking is disabled by default, so we only need to enable it
* if the query param is explicitly set to "yes".
*/
if (query.get('networking') === 'yes') {
features['networking'] = true;
}
blueprint = makeBlueprint({
php: query.get('php') || '8.0',
wp: query.get('wp') || 'latest',
theme: query.get('theme') || undefined,
login: !query.has('login') || query.get('login') === 'yes',
multisite: query.get('multisite') === 'yes',
features,
plugins: query.getAll('plugin'),
landingPage: query.get('url') || undefined,
phpExtensionBundles: query.getAll('php-extension-bundle') || [],
importSite: query.get('import-site') || undefined,
importWxr:
query.get('import-wxr') ||
query.get('import-content') ||
undefined,
language: query.get('language') || undefined,
});
blueprint = queryParamsToBlueprint(query);
}

return blueprint;
Expand Down
29 changes: 13 additions & 16 deletions packages/playground/website/src/lib/use-boot-playground.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
import { useEffect, useRef, useState } from 'react';
import { Blueprint, startPlaygroundWeb } from '@wp-playground/client';
import type { PlaygroundClient } from '@wp-playground/client';
import type { MountDescriptor, PlaygroundClient } from '@wp-playground/client';
import { getRemoteUrl } from './config';
import { logger } from '@php-wasm/logger';
import {
PlaygroundDispatch,
PlaygroundReduxState,
setActiveModal,
} from './redux-store';
import { useDispatch, useSelector } from 'react-redux';
import { PlaygroundDispatch, setActiveModal } from './redux-store';
import { useDispatch } from 'react-redux';
import { playgroundAvailableInOpfs } from '../components/playground-configuration-group/playground-available-in-opfs';
import { directoryHandleFromMountDevice } from '@wp-playground/storage';

interface UsePlaygroundOptions {
blueprint?: Blueprint;
mountDescriptor?: Omit<MountDescriptor, 'initialSyncDirection'>;
}
export function useBootPlayground({ blueprint }: UsePlaygroundOptions) {
export function useBootPlayground({
blueprint,
mountDescriptor,
}: UsePlaygroundOptions) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const iframe = iframeRef.current;
const started = useRef<string | undefined>(undefined);
const [url, setUrl] = useState<string>();
const mountDescriptor = useSelector(
(state: PlaygroundReduxState) => state.opfsMountDescriptor
);
const [playground, setPlayground] = useState<PlaygroundClient>();
const [awaitedIframe, setAwaitedIframe] = useState(false);
const dispatch: PlaygroundDispatch = useDispatch();

useEffect(() => {
const remoteUrl = getRemoteUrl();
if (!blueprint) {
return;
}

if (!iframe) {
// Iframe ref is likely not set on the initial render.
// Re-render the current component to start the playground.
Expand All @@ -39,8 +38,6 @@ export function useBootPlayground({ blueprint }: UsePlaygroundOptions) {
}

async function doRun() {
started.current = remoteUrl.toString();

let isWordPressInstalled = false;
if (mountDescriptor) {
isWordPressInstalled = await playgroundAvailableInOpfs(
Expand Down Expand Up @@ -84,7 +81,7 @@ export function useBootPlayground({ blueprint }: UsePlaygroundOptions) {
}
doRun();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [iframe, awaitedIframe, mountDescriptor]);
}, [iframe, awaitedIframe, blueprint]);

return { playground, url, iframeRef };
}
Loading
Loading