/**
* Label Studio Wrapper Component for Things Factory
*
* This component embeds Label Studio within Things Factory using an iframe.
* It uses subdomain-based cookie sharing for SSO authentication, eliminating
* the need for proxying.
*
* ## Key Features:
* - Direct Label Studio access (no proxy needed)
* - Automatic SSO cookie setup via shared domain
* - Hidden header for seamless integration
* - Floating Action Buttons (FAB) for Reload and Fullscreen
*
* ## Architecture:
* 1. Client calls /label-studio/sso/setup
* 2. Backend sets cookie with shared domain (e.g., .nubison.localhost)
* 3. Browser → Label Studio (direct, with cookie)
*
* ## Configuration Requirements:
* - Label Studio and Things Factory must use subdomain pattern (e.g., label.example.com, app.example.com)
* - Label Studio .env must include:
* - CSRF_TRUSTED_ORIGINS: Include Things Factory frontend domain
* - ALLOWED_HOSTS: Include subdomain patterns
* - JWT_SSO_* settings for SSO authentication
* - Things Factory config must include:
* - labelStudio.serverUrl: Label Studio URL
* - labelStudio.cookieDomain: Shared cookie domain (e.g., .example.com)
*
* ## Usage:
* ```html
*
* ```
*
* The component automatically:
* 1. Calls SSO setup endpoint to establish authentication
* 2. Builds iframe URL pointing directly to Label Studio
* 3. Adds ?hideHeader=true to hide Label Studio's header
* 4. Provides FAB buttons for reload and fullscreen
*/
import { css, html, LitElement } from 'lit'
import { customElement, state, query } from 'lit/decorators.js'
@customElement('label-studio-wrapper')
export class LabelStudioWrapper extends LitElement {
static styles = css`
:host {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.iframe-container {
flex: 1;
position: relative;
overflow: hidden;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
.fab-container {
position: absolute;
bottom: 24px;
left: 24px;
display: flex;
flex-direction: column;
gap: 12px;
z-index: 1000;
}
.fab-button {
width: 56px;
height: 56px;
border-radius: 16px;
background-color: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: all 0.2s ease;
}
.fab-button:hover {
background-color: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
transform: translateY(-2px);
}
.fab-button md-icon {
font-size: 24px;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: 18px;
color: var(--md-sys-color-on-surface-variant);
}
.error {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: var(--md-sys-color-error);
flex-direction: column;
gap: 10px;
}
md-filled-button {
--md-filled-button-container-color: var(--md-sys-color-primary);
}
`
@state() private loading: boolean = true
@state() private error: string | null = null
@state() private iframeUrl: string = ''
@state() private path: string = ''
@state() private iframeLoaded: boolean = false
@state() private labelStudioUrl: string = ''
@query('iframe') private iframe!: HTMLIFrameElement
async updated(changes: any) {
if (changes.has('path') && this.path) {
await this.buildIframeUrl()
setTimeout(() => {
if (!this.iframeLoaded && !this.error && !this.loading) {
this.error = 'Failed to load Label Studio. The server may be unavailable.'
}
}, 15000)
}
}
async buildIframeUrl() {
try {
this.loading = true
this.error = null
// Step 1: Get Label Studio configuration
console.log('[Label Studio Wrapper] Fetching Label Studio configuration...')
const configResponse = await fetch('/label-studio/sso/config', {
method: 'GET',
credentials: 'include'
})
if (!configResponse.ok) {
throw new Error(`Failed to fetch Label Studio config: ${configResponse.status}`)
}
const configData = await configResponse.json()
this.labelStudioUrl = configData.labelStudioUrl
if (!this.labelStudioUrl || this.labelStudioUrl === 'not configured') {
throw new Error('Label Studio URL is not configured. Please check your settings.')
}
console.log('[Label Studio Wrapper] Label Studio URL:', this.labelStudioUrl)
console.log('[Label Studio Wrapper] Cookie domain:', configData.cookieDomain)
// Step 2: Setup SSO token before loading iframe
console.log('[Label Studio Wrapper] Setting up SSO token...')
const ssoResponse = await fetch('/label-studio/sso/setup', {
method: 'GET',
credentials: 'include' // Include cookies in request
})
if (!ssoResponse.ok) {
const errorData = await ssoResponse.json().catch(() => ({}))
throw new Error(errorData.message || `SSO setup failed: ${ssoResponse.status}`)
}
const ssoData = await ssoResponse.json()
console.log('[Label Studio Wrapper] SSO setup complete:', ssoData.message)
// Step 3: Build iframe URL pointing directly to Label Studio
// No proxy needed - cookie is shared via domain
let url = this.labelStudioUrl + this.path
// Build query parameters
const params = new URLSearchParams()
// Hide header for seamless iframe embedding
params.set('hideHeader', 'true')
// Combine URL with parameters
const queryString = params.toString()
this.iframeUrl = queryString ? `${url}?${queryString}` : url
console.log('[Label Studio Wrapper] iframe URL:', this.iframeUrl)
} catch (err: any) {
console.error('[Label Studio Wrapper] Failed to build iframe URL:', err)
this.error = `Failed to initialize: ${err.message}`
} finally {
this.loading = false
}
}
handleIframeLoad() {
console.log('Label Studio iframe loaded successfully')
this.iframeLoaded = true
this.loading = false
}
handleIframeError() {
console.error('Label Studio iframe failed to load')
this.error = 'Failed to load Label Studio iframe. Please check the server connection.'
this.loading = false
}
handleReload() {
if (this.iframe) {
this.iframe.src = this.iframeUrl
}
}
handleFullscreen() {
if (this.iframe) {
if (this.iframe.requestFullscreen) {
this.iframe.requestFullscreen()
}
}
}
render() {
if (this.loading) {
return html`
Loading Label Studio...
`
}
if (this.error) {
return html`
error
${this.error}
refresh
Retry
`
}
return html`
`
}
}