mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-19 00:03:19 +00:00
feat: give full-code app previews the deployed app's window.ctx (#11763)
* feat: give full-code app previews the deployed app's window.ctx * chore: pin the UI Builder build that sets window.ctx in the preview * fix: hold the editor preview until the viewer belongs to the app's workspace * fix: let Rebuild retry a failed viewer lookup in the editor preview
This commit is contained in:
@@ -67,7 +67,7 @@ const DEFAULT_PORT = 4000;
|
||||
const DEFAULT_HOST = "localhost";
|
||||
|
||||
// HTML template with live reload and SQL migration modal
|
||||
const createHTML = (jsPath: string, cssPath: string) => `
|
||||
const createHTML = (jsPath: string, cssPath: string, appCtx: unknown) => `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -228,6 +228,7 @@ const createHTML = (jsPath: string, cssPath: string) => `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>window.ctx = ${JSON.stringify(appCtx).replace(/</g, "\\u003c")};</script>
|
||||
<script src="${jsPath}"></script>
|
||||
<script>
|
||||
// Live reload via EventSource
|
||||
@@ -406,6 +407,18 @@ async function dev(opts: DevOptions, appFolder?: string) {
|
||||
await requireLogin(opts);
|
||||
const workspaceId = workspace.workspaceId;
|
||||
|
||||
// What the deployed app receives as `window.ctx`: the viewer's membership in this
|
||||
// workspace, shaped as the frontend's UserExt, or no viewer when it can't be looked up.
|
||||
const viewer = await wmill.whoami({ workspace: workspaceId }).catch(() => undefined);
|
||||
const appCtx = {
|
||||
ctx: viewer && {
|
||||
...viewer,
|
||||
workspace_id: workspaceId,
|
||||
pgroups: (viewer.groups ?? []).map((g) => `g/${g}`),
|
||||
},
|
||||
workspace: workspaceId,
|
||||
};
|
||||
|
||||
// Resolve relative imports in app inline scripts from local (not-yet-deployed)
|
||||
// content so previews use locally-edited workspace libs instead of deployed.
|
||||
// Computed here as a startup snapshot; re-run `wmill app dev` to pick up
|
||||
@@ -966,7 +979,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
|
||||
|
||||
// Serve injected HTML for root and any other path
|
||||
res.writeHead(200, { "Content-Type": "text/html" });
|
||||
res.end(createHTML("/dist/bundle.js", "/dist/bundle.css"));
|
||||
res.end(createHTML("/dist/bundle.js", "/dist/bundle.css", appCtx));
|
||||
});
|
||||
|
||||
// Create WebSocket server on the same HTTP server
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"baseUrl": "https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev",
|
||||
"version": "d3fdb70",
|
||||
"sha256": "8c894b3f02d5d1872a966c37d43314f05ce42900f7330bdcf45a18a772deb2b9"
|
||||
"version": "6880a7f",
|
||||
"sha256": "05d49a6ed4790b444058574ccb650c8d3882161bbf004c0f8c0370964e853367"
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
import { captureScale, MAX_IMAGE_EDGE } from '../copilot/chat/imageUtils'
|
||||
import { rawAppLintStore } from './lintStore'
|
||||
import { dbSchemas, userStore } from '$lib/stores'
|
||||
import { useActingUser } from '$lib/actingUser.svelte'
|
||||
import RawAppSdkConsent from './RawAppSdkConsent.svelte'
|
||||
import {
|
||||
hasStoredSdkConsent,
|
||||
@@ -258,6 +259,7 @@
|
||||
const opWorkspace = $derived(autosaveWorkspace ?? $operatingWorkspace)
|
||||
// Everything under the editor acts on it too (see operatingWorkspace.svelte.ts).
|
||||
setOperatingWorkspace(() => opWorkspace)
|
||||
const operatingUser = useActingUser(() => opWorkspace)
|
||||
|
||||
// The path autosaves land on, which is what the session preview loads the app by.
|
||||
const draftStoragePath = $derived(autosavePath ?? liveEditorDraftStoragePath)
|
||||
@@ -508,6 +510,8 @@ addEventListener('message', function (e) {
|
||||
const SDK_CONSENT_PENDING_TEXT =
|
||||
'The preview is waiting for the user to approve the permissions the app declares in policy.frontend_sdk_scopes, so the app code is not running and the page is empty. This is not a bug in the app: do not change the code. Ask the user to approve the permissions in the preview, then retry.'
|
||||
const SDK_STARTING_TEXT =
|
||||
'The preview is still loading the permissions the app declares in policy.frontend_sdk_scopes, so the app code has not started yet. Retry in a moment.'
|
||||
const VIEWER_PENDING_TEXT =
|
||||
'The preview is still looking up the viewer the app receives in window.ctx, so the app code has not started yet. This is not a bug in the app: do not change the code. Retry in a moment.'
|
||||
const PREVIEW_SETTLE_MS = 1000
|
||||
let editorDestroyed = false
|
||||
@@ -1813,12 +1817,13 @@ addEventListener('message', function (e) {
|
||||
}
|
||||
}
|
||||
|
||||
function syncExternalPreview() {
|
||||
function syncExternalPreview() {
|
||||
if (previewSdkPending || previewUserPending || !externalPreviewReady) return
|
||||
if (lastBuild) {
|
||||
postToExternalPreview({
|
||||
type: 'preview',
|
||||
css: lastBuild.css,
|
||||
js: lastBuild.js,
|
||||
ctx: previewCtx(),
|
||||
sdk: previewSdk,
|
||||
storage: previewStorage()
|
||||
@@ -1832,18 +1837,41 @@ addEventListener('message', function (e) {
|
||||
function feedPreviewIframe(build: { css: string; js: string }) {
|
||||
// Between dropping a credential and settling its replacement the shell stays
|
||||
// blank; whichever settles last — the mint or the shell's own `load` — starts
|
||||
// the app. Same for a shell still reloading: it replays once ready.
|
||||
// the app. Same for a shell still reloading: it replays once ready.
|
||||
if (previewSdkPending || previewUserPending || !previewIframeLoaded) return
|
||||
runtimeError = undefined
|
||||
emptyRender = false
|
||||
postToPreview({
|
||||
type: 'preview',
|
||||
css: build.css,
|
||||
js: build.js,
|
||||
ctx: previewCtx(),
|
||||
sdk: previewSdk,
|
||||
storage: previewStorage()
|
||||
})
|
||||
}
|
||||
|
||||
// The deployed app's `window.ctx`: the viewer's membership in the app's workspace.
|
||||
function previewCtx() {
|
||||
return $state.snapshot({ ctx: operatingUser.current, workspace: opWorkspace })
|
||||
}
|
||||
|
||||
// A build fed before the viewer is known keeps that viewer until the next one. Outside
|
||||
// the navigation workspace the viewer is looked up; in it, `$userStore` still holds the
|
||||
// previous workspace's membership right after a switch. A failed lookup also settles
|
||||
// this: the app then runs with no viewer, as we don't know who it is.
|
||||
const previewUserPending = $derived(
|
||||
!!opWorkspace &&
|
||||
(!operatingUser.resolved(opWorkspace) ||
|
||||
(!!operatingUser.current && operatingUser.current.workspace_id !== opWorkspace))
|
||||
)
|
||||
$effect(() => {
|
||||
if (previewUserPending) return
|
||||
untrack(() => {
|
||||
if (lastBuild) feedPreviewIframe(lastBuild)
|
||||
syncExternalPreview()
|
||||
})
|
||||
})
|
||||
|
||||
// The shell seeds a fresh document's storage shim from this; unsandboxed
|
||||
// previews use real storage and get none.
|
||||
@@ -2005,6 +2033,7 @@ addEventListener('message', function (e) {
|
||||
if (editorDestroyed) return undefined
|
||||
if (buildTracker.pending) return { text: BUILD_PENDING_TEXT }
|
||||
if (sdkPrompt) return { text: SDK_CONSENT_PENDING_TEXT }
|
||||
if (previewSdkPending) return { text: SDK_STARTING_TEXT }
|
||||
if (previewUserPending) return { text: VIEWER_PENDING_TEXT }
|
||||
const res = await readPreviewDom(query)
|
||||
return res && buildError !== undefined
|
||||
@@ -2183,6 +2212,7 @@ addEventListener('message', function (e) {
|
||||
if (editorDestroyed) throw new Error('The app preview was closed')
|
||||
if (buildTracker.pending) throw new Error(BUILD_PENDING_TEXT)
|
||||
if (sdkPrompt) throw new Error(SDK_CONSENT_PENDING_TEXT)
|
||||
if (previewSdkPending) throw new Error(SDK_STARTING_TEXT)
|
||||
if (previewUserPending) throw new Error(VIEWER_PENDING_TEXT)
|
||||
if (buildError !== undefined) throw new Error(buildFailedText(buildError))
|
||||
const body = await requestFromPreview('elementRect', { selector: 'body' })
|
||||
@@ -2860,6 +2890,8 @@ addEventListener('message', function (e) {
|
||||
startIcon={{ icon: RefreshCw }}
|
||||
title="Replay the last build into the preview"
|
||||
aria-label="Rebuild"
|
||||
onClick={() => {
|
||||
// Retries a viewer lookup that failed; the hold replays the build once it answers.
|
||||
operatingUser.forgetFailures()
|
||||
if (lastBuild) {
|
||||
feedPreviewIframe(lastBuild)
|
||||
|
||||
@@ -1074,20 +1074,14 @@ const App = () => {
|
||||
description="Windmill passes the workspace and the viewer to the app."
|
||||
code={${JSON.stringify(BUILDER_CONTEXT_CODE)}}
|
||||
>
|
||||
{ctx.workspace ? (
|
||||
<>
|
||||
<div>
|
||||
<span className="muted">Workspace </span>
|
||||
<strong>{ctx.workspace}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className="muted">Viewer </span>
|
||||
<strong>{viewer ? viewer.name || viewer.username : 'Anonymous visitor'}</strong>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<span className="muted">The editor preview has no viewer. Deploy the app to see yours here.</span>
|
||||
)}
|
||||
<div>
|
||||
<span className="muted">Workspace </span>
|
||||
<strong>{ctx.workspace}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className="muted">Viewer </span>
|
||||
<strong>{viewer ? viewer.name || viewer.username : 'Anonymous visitor'}</strong>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
@@ -1166,15 +1160,11 @@ const builderAppSvelte = `<script lang="ts">
|
||||
<section class="card">
|
||||
{@render header(steps.length + 1, 'Know who is viewing', 'Windmill passes the workspace and the viewer to the app.')}
|
||||
<div class="card-body">
|
||||
{#if ctx.workspace}
|
||||
<div><span class="muted">Workspace </span><strong>{ctx.workspace}</strong></div>
|
||||
<div>
|
||||
<span class="muted">Viewer </span>
|
||||
<strong>{viewer ? viewer.name || viewer.username : 'Anonymous visitor'}</strong>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="muted">The editor preview has no viewer. Deploy the app to see yours here.</span>
|
||||
{/if}
|
||||
<div><span class="muted">Workspace </span><strong>{ctx.workspace}</strong></div>
|
||||
<div>
|
||||
<span class="muted">Viewer </span>
|
||||
<strong>{viewer ? viewer.name || viewer.username : 'Anonymous visitor'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="snippet"><code>{contextCode}</code></pre>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user