Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
131 lines
4.1 KiB
TypeScript
131 lines
4.1 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
|
import { bootstrapIframe } from '@ee/lib/extensions/ui/iframeBridge';
|
|
import LoadingIndicator from '@alga-psa/ui/components/LoadingIndicator';
|
|
|
|
type Props = {
|
|
src: string;
|
|
extensionId?: string;
|
|
};
|
|
|
|
/**
|
|
* Extension iframe component for Docker backend mode.
|
|
* Uses same-origin path-based URLs instead of custom domains.
|
|
*/
|
|
export default function DockerExtensionIframe({ src, extensionId }: Props) {
|
|
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [hasError, setHasError] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const iframe = iframeRef.current;
|
|
if (!iframe || !src) return;
|
|
|
|
let allowedOrigin: string | undefined;
|
|
try {
|
|
allowedOrigin = new URL(iframe.src || src, window.location.href).origin;
|
|
} catch {
|
|
allowedOrigin = window.location.origin;
|
|
}
|
|
|
|
// Listen for the 'ready' message from the extension to hide loading state
|
|
const handleMessage = (ev: MessageEvent) => {
|
|
// Validate origin matches same origin (Docker mode)
|
|
if (allowedOrigin && ev.origin !== allowedOrigin) {
|
|
console.warn('DockerExtensionIframe: ignoring message from different origin', {
|
|
expected: allowedOrigin,
|
|
received: ev.origin
|
|
});
|
|
return;
|
|
}
|
|
|
|
const data = ev.data as any;
|
|
// Check for Alga envelope format with ready message
|
|
if (data?.alga === true && data?.version === '1' && data?.type === 'ready') {
|
|
console.log('DockerExtensionIframe: extension ready');
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', handleMessage);
|
|
|
|
// Bootstrap iframe communication
|
|
const cleanupBridge = bootstrapIframe({ iframe, allowedOrigin, extensionId });
|
|
|
|
return () => {
|
|
window.removeEventListener('message', handleMessage);
|
|
cleanupBridge();
|
|
};
|
|
}, [src, extensionId]);
|
|
|
|
// Ensure src includes parentOrigin
|
|
const finalSrc = React.useMemo(() => {
|
|
if (!src) return src;
|
|
try {
|
|
const url = new URL(src, window.location.href);
|
|
url.searchParams.set('parentOrigin', window.location.origin);
|
|
return url.toString();
|
|
} catch {
|
|
return src;
|
|
}
|
|
}, [src]);
|
|
|
|
useEffect(() => {
|
|
// Reset state whenever the src changes so we show the loading state again.
|
|
setIsLoading(true);
|
|
setHasError(false);
|
|
}, [finalSrc]);
|
|
|
|
useEffect(() => {
|
|
if (!isLoading) return;
|
|
const fallback = window.setTimeout(() => {
|
|
setIsLoading(false);
|
|
}, 1500);
|
|
return () => window.clearTimeout(fallback);
|
|
}, [isLoading]);
|
|
|
|
return (
|
|
<div className="relative flex-1 h-full w-full flex flex-col min-h-0" aria-busy={isLoading}>
|
|
{isLoading && !hasError && (
|
|
<div className="extension-loading-overlay" role="status">
|
|
<LoadingIndicator
|
|
layout="stacked"
|
|
className="extension-loading-indicator"
|
|
text="Starting extension"
|
|
textClassName="extension-loading-text"
|
|
spinnerProps={{ size: 'sm' }}
|
|
/>
|
|
<p className="extension-loading-subtext">Loading extension UI…</p>
|
|
</div>
|
|
)}
|
|
|
|
{hasError && (
|
|
<div className="extension-loading-overlay extension-loading-overlay--error" role="alert">
|
|
<p className="extension-loading-text">We couldn’t load this extension.</p>
|
|
<p className="extension-loading-subtext">Check the extension configuration and try again.</p>
|
|
</div>
|
|
)}
|
|
|
|
<iframe
|
|
ref={iframeRef}
|
|
key={finalSrc}
|
|
src={finalSrc}
|
|
title="Extension App"
|
|
className={`flex-1 h-full w-full border-0 transition-opacity duration-300 ${
|
|
isLoading ? 'opacity-0' : 'opacity-100'
|
|
}`}
|
|
sandbox="allow-scripts allow-forms allow-popups allow-same-origin"
|
|
onLoad={() => {
|
|
setIsLoading(false);
|
|
}}
|
|
onError={() => {
|
|
console.error('DockerExtensionIframe: iframe error');
|
|
setHasError(true);
|
|
setIsLoading(false);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|