/** * 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`
` } }