Skip to content

Commit ca8ddbd

Browse files
antfubotantfu
authored andcommitted
feat(hub-ui): render json-render via the iframe view provider
Replaces hub-ui's inline Vue json-render renderer with the hub's iframe view-provider mechanism: - New ViewProvider.vue renders a provider-backed dock (json-render, and any future provider type) in a chromeless pooled iframe pointed at the registered provider SPA, passing the dock's view as ?view=<stateKey> (inline specs are materialized into an ephemeral shared-state key). One pooled iframe per view. - Shows a built-in empty state when no provider is registered for the type. - state/view-providers.ts reads the hub's devframe:view-providers shared state. - Deletes the inline src/client/json-render/ (registry + 17 components) and ViewJsonRender.vue; drops the now-moot --jr-primary bridge (json-render themes itself in its own SPA) and the unused @json-render/core|vue deps. json-render now composes with the hub UI as host + injected iframe provider, framework-agnostically.
1 parent 7f6fbee commit ca8ddbd

30 files changed

Lines changed: 150 additions & 1820 deletions

packages/hub-ui/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@
4949
"@devframes/hub": "workspace:*",
5050
"@devframes/json-render": "workspace:*",
5151
"@iconify-json/ph": "catalog:frontend",
52-
"@json-render/core": "catalog:deps",
53-
"@json-render/vue": "catalog:frontend",
5452
"@storybook/addon-docs": "catalog:storybook",
5553
"@storybook/vue3-vite": "catalog:storybook",
5654
"@types/node": "catalog:types",

packages/hub-ui/src/client/.generated/css.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/hub-ui/src/client/components/views/ViewEntry.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ defineProps<{
1818
divStyle?: CSSProperties
1919
}>()
2020
21-
// Lazy load some less frequently used builtin views
22-
const ViewJsonRender = defineAsyncComponent(() => import('./ViewJsonRender.vue'))
21+
// Provider-backed view types (json-render, …) render in a swappable iframe.
22+
const ViewProvider = defineAsyncComponent(() => import('./ViewProvider.vue'))
2323
</script>
2424

2525
<template>
@@ -62,10 +62,12 @@ const ViewJsonRender = defineAsyncComponent(() => import('./ViewJsonRender.vue')
6262
:context
6363
:entry
6464
/>
65-
<ViewJsonRender
65+
<ViewProvider
6666
v-else-if="entry.type === 'json-render'"
6767
:context
6868
:entry
69+
:panes="panes"
70+
:iframe-style="iframeStyle"
6971
/>
7072
<div v-else>
7173
Unknown entry: {{ entry }}

packages/hub-ui/src/client/components/views/ViewJsonRender.vue

Lines changed: 0 additions & 113 deletions
This file was deleted.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<script setup lang="ts">
2+
import type { DevframeDockEntry } from '@devframes/hub'
3+
import type { DocksContext } from '@devframes/hub/client'
4+
import type { JsonRenderViewRef } from '@devframes/json-render'
5+
import type { IframePanes } from 'iframe-pane'
6+
import type { CSSProperties } from 'vue'
7+
import { computed, onMounted, onUnmounted, shallowRef, useTemplateRef, watch, watchEffect } from 'vue'
8+
import { useViewProviders } from '../../state/view-providers'
9+
10+
// Renders a provider-backed dock view (e.g. `json-render`) in a chromeless
11+
// iframe pointed at the registered provider SPA — decoupling the renderer from
12+
// this UI's framework. The dock's view travels as `?view=<stateKey>`; an inline
13+
// spec is first materialized into an ephemeral shared-state key. When no
14+
// provider is registered for the type, a placeholder explains how to add one.
15+
const props = defineProps<{
16+
context: DocksContext
17+
entry: DevframeDockEntry
18+
panes: IframePanes
19+
iframeStyle?: CSSProperties
20+
}>()
21+
22+
const providers = useViewProviders(props.context.rpc)
23+
const providerBase = computed(() => providers.value[props.entry.type]?.base)
24+
25+
// The stateKey the provider SPA renders. A json-render entry carries a
26+
// serializable `view` ref: a live shared-state key, or an inline spec we
27+
// materialize into an ephemeral key the iframe can read over the shared socket.
28+
const viewKey = shallowRef<string | undefined>()
29+
30+
async function resolveViewKey(): Promise<void> {
31+
const view = (props.entry as { view?: JsonRenderViewRef }).view
32+
if (!view) {
33+
viewKey.value = undefined
34+
return
35+
}
36+
if ('stateKey' in view) {
37+
viewKey.value = view.stateKey
38+
return
39+
}
40+
const key = `devframe:json-render:inline:${props.entry.id}`
41+
const state = await props.context.rpc.sharedState.get(key, { initialValue: null })
42+
state.mutate(() => view.spec)
43+
viewKey.value = key
44+
}
45+
46+
const src = computed(() => {
47+
const base = providerBase.value
48+
if (!base)
49+
return undefined
50+
return viewKey.value ? `${base}?view=${encodeURIComponent(viewKey.value)}` : base
51+
})
52+
53+
// One pooled iframe per (provider type + view): preserved across tab switches,
54+
// reused when reselected.
55+
const paneKey = computed(() => `viewprovider:${props.entry.type}:${viewKey.value ?? props.entry.id}`)
56+
const viewFrame = useTemplateRef<HTMLDivElement>('viewFrame')
57+
58+
let mountedKey: string | undefined
59+
function mountPane(): void {
60+
const source = src.value
61+
const target = viewFrame.value
62+
if (!source || !target)
63+
return
64+
const key = paneKey.value
65+
const pane = props.panes.ensure(key, {
66+
src: source,
67+
style: { boxShadow: 'none', outline: 'none' },
68+
})
69+
Object.assign(pane.iframe.style, props.iframeStyle)
70+
const entryState = props.context.docks.getStateById(props.entry.id)
71+
if (entryState)
72+
entryState.domElements.iframe = pane.iframe
73+
pane.mount(target)
74+
pane.update()
75+
mountedKey = key
76+
}
77+
78+
onMounted(() => void resolveViewKey())
79+
watch(() => (props.entry as { view?: JsonRenderViewRef }).view, () => void resolveViewKey())
80+
// Mount (or re-point) once the provider base + view are resolved.
81+
watchEffect(() => {
82+
if (src.value && viewFrame.value)
83+
mountPane()
84+
})
85+
86+
onUnmounted(() => {
87+
if (!mountedKey)
88+
return
89+
const pane = props.panes.get(mountedKey)
90+
// Only unmount if this view still owns the pane (guards the shared handoff).
91+
if (pane && pane.target === viewFrame.value)
92+
pane.unmount()
93+
})
94+
</script>
95+
96+
<template>
97+
<div
98+
v-if="providerBase"
99+
ref="viewFrame"
100+
class="devframes-view-provider w-full h-full"
101+
/>
102+
<div
103+
v-else
104+
class="w-full h-full flex flex-col items-center justify-center gap-2 p6 text-center color-muted select-none"
105+
>
106+
<div class="i-ph:puzzle-piece-duotone text-3xl op40" />
107+
<div class="text-sm color-base">
108+
No <span class="font-mono">{{ entry.type }}</span> view provider registered
109+
</div>
110+
<p class="text-xs op-fade max-w-80 leading-relaxed">
111+
This host renders <span class="font-mono">{{ entry.type }}</span> views through a provider.
112+
Register one (e.g. <span class="font-mono">@devframes/json-render-ui</span>) via
113+
<span class="font-mono">initHub({ viewProviders })</span>.
114+
</p>
115+
</div>
116+
</template>

0 commit comments

Comments
 (0)