mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 16:02:59 +00:00
The registry's item described pickers the breadcrumb no longer has: nine fields — the editability flags, the change callbacks, the navigation hook — that nothing read, while the flow editor passed eight of them and quietly lost its inline rename. The type now says what the bar does: a path, and either a summary or the page's own control for one. The flow editor renames through that control, as the detail page already did. `barPlacement` went the same way: no page registered 'inline', so the layout's guard never fired. The sessions page registered an inset of 0 whenever its preview pane never mounted — the "no session selected" and opted-out arms — and 0 reads as "a page owns the right edge", floating the band over the page's first line. It registers an inset only once the pane has a width. The band's sidebar handle docks through `setDetached` like the card's own toggle, so docking still suppresses the rail's entry animation and closes the card; a summary without its breadcrumb no longer leads with a stray separator in an embed; a page's hint sits with its name whichever way the page named itself; and `Menu.closeInstantly` and `EditorHeader`'s `inline`/`hideSummary` go, their callers having been replaced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
171 lines
6.2 KiB
Svelte
171 lines
6.2 KiB
Svelte
<script lang="ts">
|
|
/*
|
|
* WIN-2006: shared in-workspace app viewer (low-code AND raw). Renders the app
|
|
* through the same PublicAppFrame -> PublicApp machinery the public viewer uses,
|
|
* so the sandbox behavior is identical on every page. PublicAppFrame picks the
|
|
* rendering: an opaque /app_embed iframe for sandboxed low-code, or inline (with
|
|
* the bundle isolated in RawAppPreview's own opaque iframe) for raw and for
|
|
* unsandboxed apps. When the publisher opts an app into sandbox isolation, its
|
|
* untrusted markup/JS must not run with the member's full session — hence the
|
|
* scoped embed token / opaque isolation.
|
|
*/
|
|
import { base } from '$lib/base'
|
|
import PublicApp from '$lib/components/apps/editor/PublicApp.svelte'
|
|
import PublicAppFrame from '$lib/components/apps/editor/PublicAppFrame.svelte'
|
|
import PageHeaderContent from '$lib/components/PageHeaderContent.svelte'
|
|
import { Button } from '$lib/components/common'
|
|
import { AppService, OpenAPI } from '$lib/gen'
|
|
import { userStore } from '$lib/stores'
|
|
import { canWrite } from '$lib/utils'
|
|
import { getUserExt } from '$lib/user'
|
|
import { Pen } from 'lucide-svelte'
|
|
import { page } from '$app/state'
|
|
|
|
let {
|
|
workspace,
|
|
path
|
|
}: {
|
|
workspace: string
|
|
path: string
|
|
} = $props()
|
|
|
|
let app: any = $state(undefined)
|
|
let notExists = $state(false)
|
|
let noPermission = $state(false)
|
|
let canWriteApp = $state(false)
|
|
/** Raw vs low-code, read from the app itself rather than from the route:
|
|
* both kinds render here and either route serves either kind (links to a raw
|
|
* app point at /apps/get all over the app), so only the app can say which
|
|
* editor the Edit button must open. */
|
|
let isRawApp = $state(false)
|
|
let editHref = $derived(`${base}/${isRawApp ? 'apps_raw' : 'apps'}/edit/${path}?nodraft=true`)
|
|
let refresh: (() => void) | undefined
|
|
|
|
// The opaque iframe loads the dedicated cookieless, chrome-less viewer route.
|
|
// The page's query/hash are forwarded so the app sees the same `ctx.query` /
|
|
// `ctx.hash` as the pre-sandbox viewer did. Captured ONCE (not reactively):
|
|
// the embedder later mirrors the app's own hash/query changes back onto this
|
|
// page's URL (wm_embed_hash relay), and re-deriving the iframe src from them
|
|
// would reload the app on its every navigation.
|
|
const initialSearchHash = page.url.search + page.url.hash
|
|
let viewerUrl = $derived(`${base}/app_embed/${workspace}/${path}${initialSearchHash}`)
|
|
|
|
const hideEditBtn = page.url.searchParams.get('hideEditBtn') === 'true'
|
|
const hideRefreshBar = page.url.searchParams.get('hideRefreshBar') === 'true'
|
|
|
|
// Embedder side: mint a scoped embed token (by path) from the member's session.
|
|
async function fetchEmbedToken(opts?: { sdkConsent?: boolean }): Promise<{ token?: string }> {
|
|
const headers: Record<string, string> = {}
|
|
if (typeof OpenAPI.TOKEN === 'string' && OpenAPI.TOKEN) {
|
|
headers['Authorization'] = `Bearer ${OpenAPI.TOKEN}`
|
|
}
|
|
const consent = opts?.sdkConsent ? '?sdk_consent=true' : ''
|
|
const res = await fetch(`${OpenAPI.BASE}/w/${workspace}/apps/embed_token/p/${path}${consent}`, {
|
|
headers
|
|
})
|
|
if (!res.ok) {
|
|
const err: any = new Error('Failed to fetch embed token')
|
|
err.status = res.status
|
|
throw err
|
|
}
|
|
return await res.json()
|
|
}
|
|
|
|
// Viewer side — used for the inline renderings (raw, and unsandboxed low-code);
|
|
// the sandboxed low-code case loads inside the opaque /app_embed iframe instead.
|
|
// getAppByPath returns bundle_secret + runnables for raw apps, which
|
|
// PublicApp -> RawAppPreview needs.
|
|
async function loadApp() {
|
|
try {
|
|
userStore.set(await getUserExt(workspace))
|
|
} catch (e) {
|
|
console.warn('Anonymous user')
|
|
}
|
|
try {
|
|
const loaded: any = await AppService.getAppByPath({ workspace, path })
|
|
// Raw apps need the bundle secret to load their bundle. getAppByPath
|
|
// doesn't compute it (unlike the public handlers), so fetch it here — the
|
|
// same call the previous raw viewer used — and hand it to PublicApp ->
|
|
// RawAppPreview via bundle_secret.
|
|
if (loaded?.raw_app && !loaded.bundle_secret) {
|
|
try {
|
|
loaded.bundle_secret = await AppService.getPublicSecretOfLatestVersionOfApp({
|
|
workspace,
|
|
path
|
|
})
|
|
} catch (e) {
|
|
console.error('Failed to load raw app bundle secret', e)
|
|
}
|
|
}
|
|
app = loaded
|
|
noPermission = false
|
|
notExists = false
|
|
} catch (e: any) {
|
|
if (e.status == 401) refresh?.()
|
|
else if (e.status == 403) noPermission = true
|
|
else notExists = true
|
|
}
|
|
}
|
|
|
|
// Edit button: determine write access and which editor to open on this
|
|
// real-origin page (cookie). The sandboxed low-code app never loads on this
|
|
// page (it loads inside the opaque iframe), so `app` can't be the source.
|
|
async function loadPerms() {
|
|
try {
|
|
const lite: any = await AppService.getAppLiteByPath({ workspace, path })
|
|
canWriteApp = canWrite(lite?.path, lite?.extra_perms ?? {}, $userStore)
|
|
isRawApp = !!lite?.raw_app
|
|
} catch (_) {
|
|
canWriteApp = false
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
if (workspace && path) loadPerms()
|
|
})
|
|
</script>
|
|
|
|
<!-- The band draws no edge of its own, so the separation from it is this page's first line. It
|
|
goes on the app's wrapper rather than on the frame, which the header-less public page shares —
|
|
and as an inset shadow rather than a border, which would take a pixel off an app sized to fill
|
|
the page exactly and leave it scrolling. -->
|
|
<div class="h-full shadow-[inset_0_1px_0_0_rgb(var(--color-border-light))]">
|
|
<PublicAppFrame
|
|
{fetchEmbedToken}
|
|
{viewerUrl}
|
|
onViewerReady={(_token, requestTokenRefresh) => {
|
|
refresh = requestTokenRefresh
|
|
loadApp()
|
|
}}
|
|
>
|
|
{#snippet viewer()}
|
|
<PublicApp
|
|
{app}
|
|
{workspace}
|
|
{notExists}
|
|
{noPermission}
|
|
jwtError={false}
|
|
inWorkspace
|
|
{hideRefreshBar}
|
|
onLoginSuccess={() => loadApp()}
|
|
></PublicApp>
|
|
{/snippet}
|
|
</PublicAppFrame>
|
|
</div>
|
|
|
|
{#if canWriteApp && !hideEditBtn}
|
|
<!-- The page header carries it, with the app's name: a floating button over the app's own
|
|
canvas lands on whatever the app draws there. -->
|
|
<PageHeaderContent item={{ kind: 'app', path }} actions={editAction} />
|
|
{/if}
|
|
|
|
{#snippet editAction()}
|
|
<Button
|
|
unifiedSize="sm"
|
|
startIcon={{ icon: Pen }}
|
|
variant="subtle"
|
|
href={editHref}
|
|
id="app-edit-btn">Edit</Button
|
|
>
|
|
{/snippet}
|