= (props) => {
+ const moesifAppUrl = insightsRuntimeConfig.moesifAppUrl;
+ if (!moesifAppUrl) {
+ return (
+
+
+
+ Insights
+
+
+
+
+ );
+ }
+
+ return ;
+};
+
+export default InsightsEmbed;
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/InsightsEmbed.unconfigured.test.tsx b/portals/cloud-plugins/apip-cloud-ui-insights/src/InsightsEmbed.unconfigured.test.tsx
new file mode 100644
index 0000000000..7e19fd1b7d
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/InsightsEmbed.unconfigured.test.tsx
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { render, screen } from '@testing-library/react';
+import type { ReactNode } from 'react';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+vi.mock('./api/analyticsApi', () => ({
+ fetchViewerToken: vi.fn(),
+}));
+
+vi.mock('./components/StateViews', () => ({
+ LoadingState: ({ label }: { label?: string }) => (
+ {label}
+ ),
+ ErrorState: ({ title, message }: { title: string; message: string }) => (
+
+ {title}: {message}
+
+ ),
+}));
+
+vi.mock('@wso2/oxygen-ui', () => ({
+ Box: ({
+ children,
+ ...props
+ }: {
+ children?: ReactNode;
+ sx?: unknown;
+ }) => {children}
,
+ PageTitle: Object.assign(
+ ({ children }: { children?: ReactNode }) => {children}
,
+ {
+ Header: ({ children }: { children?: ReactNode }) => (
+ {children}
+ ),
+ SubHeader: ({ children }: { children?: ReactNode }) => (
+ {children}
+ ),
+ }
+ ),
+}));
+
+describe('InsightsEmbed without Moesif runtime config', () => {
+ afterEach(() => {
+ delete window.config;
+ delete window.__RUNTIME_CONFIG__;
+ vi.resetModules();
+ vi.unstubAllEnvs();
+ });
+
+ beforeEach(() => {
+ delete window.config;
+ delete window.__RUNTIME_CONFIG__;
+ vi.unstubAllEnvs();
+ });
+
+ it('renders not-configured error and never mounts a Moesif iframe', async () => {
+ const { default: InsightsEmbed } = await import('./InsightsEmbed');
+
+ render( );
+
+ expect(screen.getByTestId('error-state')).toHaveTextContent(
+ 'Insights is not configured for this deployment.'
+ );
+ expect(screen.queryByTitle('Moesif Insights')).not.toBeInTheDocument();
+ });
+});
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/InsightsFeature.test.tsx b/portals/cloud-plugins/apip-cloud-ui-insights/src/InsightsFeature.test.tsx
new file mode 100644
index 0000000000..4b34a46fc5
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/InsightsFeature.test.tsx
@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { render, screen, waitFor } from '@testing-library/react';
+import type { ReactNode } from 'react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import type { InsightsEmbedScope } from './types';
+
+const { mockResolveProjectScope, embedScopes } = vi.hoisted(() => ({
+ mockResolveProjectScope: vi.fn(),
+ embedScopes: [] as InsightsEmbedScope[],
+}));
+
+vi.mock('./api/analyticsApi', () => ({
+ resolveProjectScope: (...args: unknown[]) => mockResolveProjectScope(...args),
+}));
+
+vi.mock('./components/StateViews', () => ({
+ LoadingState: ({ label }: { label?: string }) => (
+ {label}
+ ),
+ ErrorState: ({ title, message }: { title: string; message: string }) => (
+
+ {title}: {message}
+
+ ),
+}));
+
+vi.mock('@wso2/oxygen-ui', () => ({
+ PageContent: ({ children }: { children?: ReactNode }) => (
+ {children}
+ ),
+}));
+
+vi.mock('./InsightsEmbed', () => ({
+ default: ({ scope }: { scope: InsightsEmbedScope }) => {
+ embedScopes.push(scope);
+ return (
+
+ {scope.level}:{scope.projectId ?? 'none'}
+
+ );
+ },
+}));
+
+import InsightsFeature from './InsightsFeature';
+import type { InsightsHostPort } from './hostPort';
+
+const basePort: InsightsHostPort = {
+ orgHandle: 'acme',
+ navigate: vi.fn(),
+ notify: vi.fn(),
+};
+
+describe('InsightsFeature', () => {
+ beforeEach(() => {
+ embedScopes.length = 0;
+ mockResolveProjectScope.mockReset();
+ });
+
+ it('embeds project Insights when project scope resolves', async () => {
+ mockResolveProjectScope.mockResolvedValue({
+ projectId: 'id-a',
+ projectName: 'Project A',
+ });
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('insights-embed')).toHaveTextContent(
+ 'project:id-a'
+ );
+ });
+ expect(mockResolveProjectScope).toHaveBeenCalledWith('acme', 'project-a');
+ });
+
+ it('falls back to organization Insights when project scope resolve fails', async () => {
+ mockResolveProjectScope.mockRejectedValue(
+ new Error('Project "missing" was not found')
+ );
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('insights-embed')).toHaveTextContent(
+ 'organization:none'
+ );
+ });
+ expect(screen.queryByTestId('error-state')).not.toBeInTheDocument();
+ expect(embedScopes.at(-1)).toEqual({
+ level: 'organization',
+ projectId: null,
+ projectName: null,
+ });
+ });
+
+ it('uses ai-overview embed for AI Workspace without resolving project_id', async () => {
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('insights-embed')).toHaveTextContent(
+ 'organization:none'
+ );
+ });
+ expect(mockResolveProjectScope).not.toHaveBeenCalled();
+ });
+
+ it('shows loading instead of stale project metadata when switching projects', async () => {
+ let resolveProjectB: (value: {
+ projectId: string;
+ projectName: string;
+ }) => void = () => {};
+ const projectBPromise = new Promise<{
+ projectId: string;
+ projectName: string;
+ }>((resolve) => {
+ resolveProjectB = resolve;
+ });
+
+ mockResolveProjectScope.mockImplementation((_org, handle) => {
+ if (handle === 'project-a') {
+ return Promise.resolve({
+ projectId: 'id-a',
+ projectName: 'Project A',
+ });
+ }
+ if (handle === 'project-b') {
+ return projectBPromise;
+ }
+ return Promise.reject(new Error(`unexpected handle: ${handle}`));
+ });
+
+ const { rerender } = render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('insights-embed')).toHaveTextContent(
+ 'project:id-a'
+ );
+ });
+ const embedRenderCountAfterA = embedScopes.length;
+
+ rerender(
+
+ );
+
+ expect(screen.queryByTestId('insights-embed')).not.toBeInTheDocument();
+ expect(screen.getByTestId('loading-state')).toHaveTextContent(
+ 'Preparing Insights'
+ );
+ expect(embedScopes.length).toBe(embedRenderCountAfterA);
+
+ resolveProjectB({ projectId: 'id-b', projectName: 'Project B' });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('insights-embed')).toHaveTextContent(
+ 'project:id-b'
+ );
+ });
+ expect(embedScopes.at(-1)).toEqual({
+ level: 'project',
+ projectId: 'id-b',
+ projectName: 'Project B',
+ });
+ });
+
+ it('wraps AI Workspace Insights in PageContent for shell padding', async () => {
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('insights-embed')).toBeInTheDocument();
+ });
+ expect(screen.getByTestId('page-content')).toContainElement(
+ screen.getByTestId('insights-embed')
+ );
+ });
+
+ it('does not nest PageContent for API Control Plane', async () => {
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('insights-embed')).toBeInTheDocument();
+ });
+ expect(screen.queryByTestId('page-content')).not.toBeInTheDocument();
+ });
+});
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/InsightsFeature.tsx b/portals/cloud-plugins/apip-cloud-ui-insights/src/InsightsFeature.tsx
new file mode 100644
index 0000000000..996ebae68c
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/InsightsFeature.tsx
@@ -0,0 +1,206 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { PageContent } from '@wso2/oxygen-ui';
+import { useEffect, useMemo, useState, type FC, type ReactNode } from 'react';
+
+import { resolveProjectScope } from './api/analyticsApi';
+import { ErrorState, LoadingState } from './components/StateViews';
+import type { InsightsHostPort } from './hostPort';
+import InsightsEmbed from './InsightsEmbed';
+import type { InsightsEmbedProfile, InsightsScopeLevel } from './types';
+import { resolveInsightsScopeLevel } from './utils/moesifEmbed';
+import { parseInsightsRouteParams } from './utils/routeParams';
+
+/**
+ * AI Workspace pages own their PageContent (the shell does not wrap the outlet).
+ * ACP already wraps the outlet in PageContent — do not nest another here.
+ */
+function withHostPageChrome(
+ embedProfile: InsightsEmbedProfile,
+ children: ReactNode
+): ReactNode {
+ if (embedProfile === 'ai-workspace') {
+ return {children} ;
+ }
+ return children;
+}
+
+export type InsightsFeatureProps = {
+ port: InsightsHostPort;
+ /** When set, overrides URL-derived scope. */
+ forcedScopeLevel?: InsightsScopeLevel;
+ /**
+ * Host-chosen Moesif iframe path. AI Workspace uses the same ai-overview
+ * URL at org and project (no project_id filtering).
+ */
+ embedProfile?: InsightsEmbedProfile;
+};
+
+/**
+ * Resolves project scope when needed, then renders the wrap/basic Moesif embed.
+ *
+ * Until Moesif reliably supports project filtering, a failed project resolve
+ * falls back to the organization embed instead of blocking the page.
+ *
+ * Organization context for the viewer token comes from the BFF session — the
+ * embed does not need `idpOrganizationRefUuid` from the platform-api org list.
+ */
+const InsightsFeature: FC = ({
+ port,
+ forcedScopeLevel,
+ embedProfile = 'api-control-plane',
+}) => {
+ const routeParams = parseInsightsRouteParams(window.location.pathname);
+ const orgHandle = port.orgHandle || routeParams.orgHandle || '';
+ const projectHandle = port.projectHandle || routeParams.projectHandler;
+
+ const requestedScopeLevel =
+ forcedScopeLevel ??
+ resolveInsightsScopeLevel({
+ projectHandle,
+ });
+
+ // AI Workspace: same iframe at org and project — skip project_id resolve.
+ const needsProjectResolve =
+ embedProfile === 'api-control-plane' && requestedScopeLevel === 'project';
+
+ const scopeKey = useMemo(
+ () =>
+ [orgHandle, projectHandle ?? '', requestedScopeLevel, embedProfile].join(
+ '|'
+ ),
+ [embedProfile, orgHandle, projectHandle, requestedScopeLevel]
+ );
+
+ const [embedScopeLevel, setEmbedScopeLevel] =
+ useState(requestedScopeLevel);
+ const [projectId, setProjectId] = useState(null);
+ const [projectName, setProjectName] = useState(null);
+ const [scopeError, setScopeError] = useState(null);
+ const [scopeLoading, setScopeLoading] = useState(needsProjectResolve);
+ const [resolvedScopeKey, setResolvedScopeKey] = useState(() =>
+ needsProjectResolve ? null : scopeKey
+ );
+
+ const isScopeReady = resolvedScopeKey === scopeKey;
+
+ useEffect(() => {
+ if (!needsProjectResolve) {
+ setEmbedScopeLevel(
+ embedProfile === 'ai-workspace' ? 'organization' : requestedScopeLevel
+ );
+ setScopeLoading(false);
+ setScopeError(null);
+ setProjectId(null);
+ setProjectName(null);
+ setResolvedScopeKey(scopeKey);
+ return;
+ }
+
+ let cancelled = false;
+ setScopeLoading(true);
+ setScopeError(null);
+ setProjectId(null);
+ setProjectName(null);
+ setEmbedScopeLevel('project');
+ setResolvedScopeKey(null);
+
+ (async () => {
+ try {
+ if (!orgHandle) {
+ throw new Error('Organization context is unavailable.');
+ }
+ if (!projectHandle) {
+ if (!cancelled) {
+ setEmbedScopeLevel('organization');
+ setProjectId(null);
+ setProjectName(null);
+ setResolvedScopeKey(scopeKey);
+ }
+ return;
+ }
+ const project = await resolveProjectScope(orgHandle, projectHandle);
+ if (cancelled) return;
+ setEmbedScopeLevel('project');
+ setProjectId(project.projectId);
+ setProjectName(project.projectName);
+ setResolvedScopeKey(scopeKey);
+ } catch {
+ if (!cancelled) {
+ setEmbedScopeLevel('organization');
+ setProjectId(null);
+ setProjectName(null);
+ setScopeError(null);
+ setResolvedScopeKey(scopeKey);
+ }
+ } finally {
+ if (!cancelled) setScopeLoading(false);
+ }
+ })();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [
+ embedProfile,
+ needsProjectResolve,
+ orgHandle,
+ projectHandle,
+ requestedScopeLevel,
+ scopeKey,
+ ]);
+
+ if (!orgHandle) {
+ return withHostPageChrome(
+ embedProfile,
+
+ );
+ }
+
+ if (scopeError) {
+ return withHostPageChrome(
+ embedProfile,
+
+ );
+ }
+
+ if (scopeLoading || !isScopeReady) {
+ return withHostPageChrome(
+ embedProfile,
+
+ );
+ }
+
+ return withHostPageChrome(
+ embedProfile,
+
+ );
+};
+
+export default InsightsFeature;
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/api/analyticsApi.ts b/portals/cloud-plugins/apip-cloud-ui-insights/src/api/analyticsApi.ts
new file mode 100644
index 0000000000..c49e1d4985
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/api/analyticsApi.ts
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Client for WSO2 Cloud Moesif viewer-token endpoint (platform-api-service).
+ * See wso2cloud/backend/core/internal/moesifmapping/handler/handler.go.
+ *
+ * Calls go through the portal BFF same-origin proxy so session cookies and
+ * bearer injection stay server-side.
+ */
+
+import { insightsRuntimeConfig, platformApiRoot } from '../config/runtimeConfig';
+
+type ViewerTokenResponse = {
+ token: string;
+};
+
+const cloudApiBase = () =>
+ insightsRuntimeConfig.platformApiBaseUrl.replace(/\/$/, '');
+
+/** User-facing copy only. */
+const userFacingRequestError = (status: number): string => {
+ if (status === 401 || status === 403) {
+ return 'You do not have permission to view Insights for this organization.';
+ }
+ if (status === 404) {
+ return 'Insights are not available for this organization. Contact your administrator if you believe this is an error.';
+ }
+ if (status === 408 || status === 504) {
+ return 'Insights took too long to respond. Please try again.';
+ }
+ if (status >= 500) {
+ return 'Insights are temporarily unavailable. Please try again in a few minutes.';
+ }
+ return 'Unable to load Insights right now. Please try again.';
+};
+
+const readJson = async (response: Response): Promise => {
+ if (!response.ok) {
+ throw new Error(userFacingRequestError(response.status));
+ }
+ return (await response.json().catch(() => ({}))) as T;
+};
+
+const fetchJson = async (
+ url: string,
+ init?: RequestInit
+): Promise => {
+ const response = await fetch(url, {
+ ...init,
+ credentials: 'include',
+ headers: {
+ accept: 'application/json',
+ ...(init?.headers ?? {}),
+ },
+ });
+ return readJson(response);
+};
+
+/** GET /cloud/analytics/id-token — Moesif dashboard-viewer token for the caller org. */
+export async function fetchViewerToken(): Promise {
+ const response = await fetch(`${cloudApiBase()}/cloud/analytics/id-token`, {
+ credentials: 'include',
+ headers: { accept: 'application/json' },
+ });
+ if (!response.ok) {
+ throw new Error(userFacingRequestError(response.status));
+ }
+ const payload = (await response.json().catch(() => ({}))) as ViewerTokenResponse;
+ if (!payload.token?.trim()) {
+ throw new Error(
+ 'Unable to load Insights right now. Please try again.'
+ );
+ }
+ return payload.token;
+}
+
+type ProjectRecord = {
+ uuid?: string;
+ id?: string;
+ handler?: string;
+ handle?: string;
+ name?: string;
+ displayName?: string;
+};
+
+const asRecord = (value: unknown): Record =>
+ value && typeof value === 'object' ? (value as Record) : {};
+
+const pickProjectHandle = (project: ProjectRecord) =>
+ project.id?.trim() ||
+ project.handler?.trim() ||
+ project.handle?.trim() ||
+ '';
+
+const pickProjectId = (project: ProjectRecord) =>
+ project.uuid?.trim() || pickProjectHandle(project);
+
+const pickProjectName = (project: ProjectRecord, fallback: string) =>
+ project.displayName?.trim() || project.name?.trim() || fallback;
+
+/** Resolve a project id for Moesif `project_id` filtering. */
+export async function resolveProjectScope(
+ orgHandle: string,
+ projectHandle: string
+): Promise<{ projectId: string; projectName: string }> {
+ const trimmedHandle = projectHandle.trim();
+ if (!trimmedHandle) {
+ throw new Error('Project scope is required for project insights.');
+ }
+
+ const headers = { 'X-Org-Id': orgHandle };
+
+ try {
+ const project = await fetchJson(
+ `${platformApiRoot()}/projects/${encodeURIComponent(trimmedHandle)}`,
+ { headers }
+ );
+ if (pickProjectHandle(project) === trimmedHandle) {
+ const projectId = pickProjectId(project);
+ if (projectId) {
+ return {
+ projectId,
+ projectName: pickProjectName(project, trimmedHandle),
+ };
+ }
+ }
+ } catch {
+ // Fall back to list lookup below.
+ }
+
+ try {
+ const response = await fetchJson<{ list?: unknown[] }>(
+ `${platformApiRoot()}/projects`,
+ { headers }
+ );
+ for (const item of response.list ?? []) {
+ const project = asRecord(item) as ProjectRecord;
+ if (pickProjectHandle(project) === trimmedHandle) {
+ const projectId = pickProjectId(project);
+ if (!projectId) break;
+ return {
+ projectId,
+ projectName: pickProjectName(project, trimmedHandle),
+ };
+ }
+ }
+ } catch {
+ // Treat failed project resolution as not found — do not surface proxy/status noise.
+ }
+ throw new Error(`Project "${trimmedHandle}" was not found`);
+}
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/components/StateViews.tsx b/portals/cloud-plugins/apip-cloud-ui-insights/src/components/StateViews.tsx
new file mode 100644
index 0000000000..7bcb3a36cf
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/components/StateViews.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Alert, Box, Button, CircularProgress, Typography } from '@wso2/oxygen-ui';
+
+export function LoadingState({
+ label = 'Loading',
+}: {
+ label?: string;
+}) {
+ return (
+
+
+ {label}
+
+ );
+}
+
+export function ErrorState({
+ title,
+ message,
+ actionLabel,
+ onAction,
+}: {
+ title: string;
+ message: string;
+ actionLabel?: string;
+ onAction?: () => void;
+}) {
+ return (
+
+ {title}
+ {message}
+ {actionLabel && onAction && (
+
+ {actionLabel}
+
+ )}
+
+ );
+}
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/config/runtimeConfig.test.ts b/portals/cloud-plugins/apip-cloud-ui-insights/src/config/runtimeConfig.test.ts
new file mode 100644
index 0000000000..58fa1d6e47
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/config/runtimeConfig.test.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+const loadRuntimeConfig = async () => {
+ vi.resetModules();
+ return (await import('./runtimeConfig')).insightsRuntimeConfig;
+};
+
+afterEach(() => {
+ delete window.config;
+ delete window.__RUNTIME_CONFIG__;
+ vi.resetModules();
+ vi.unstubAllEnvs();
+});
+
+describe('insightsRuntimeConfig', () => {
+ it('leaves moesifAppUrl undefined when nothing is configured', async () => {
+ const config = await loadRuntimeConfig();
+
+ expect(config.moesifAppUrl).toBeUndefined();
+ });
+
+ it('accepts an allowlisted moesifAppUrl from window runtime config', async () => {
+ window.__RUNTIME_CONFIG__ = {
+ moesifAppUrl: 'https://www.moesif.com/wrap',
+ };
+
+ const config = await loadRuntimeConfig();
+
+ expect(config.moesifAppUrl).toBe('https://www.moesif.com');
+ });
+
+ it('rejects a non-allowlisted moesifAppUrl without falling back to web-dev', async () => {
+ window.__RUNTIME_CONFIG__ = {
+ moesifAppUrl: 'https://evil.example.com',
+ };
+
+ const config = await loadRuntimeConfig();
+
+ expect(config.moesifAppUrl).toBeUndefined();
+ });
+
+ it('reports configured when an allowlisted Moesif origin is present', async () => {
+ window.__RUNTIME_CONFIG__ = {
+ APIP_AIW_MOESIF_WEB_URL: 'https://web-dev.moesif.com',
+ };
+ vi.resetModules();
+ const { isInsightsMoesifConfigured } = await import('./runtimeConfig');
+ expect(isInsightsMoesifConfigured()).toBe(true);
+ });
+
+ it('reports unconfigured when Moesif origin is missing', async () => {
+ vi.resetModules();
+ const { isInsightsMoesifConfigured } = await import('./runtimeConfig');
+ expect(isInsightsMoesifConfigured()).toBe(false);
+ });
+});
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/config/runtimeConfig.ts b/portals/cloud-plugins/apip-cloud-ui-insights/src/config/runtimeConfig.ts
new file mode 100644
index 0000000000..232f86c89a
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/config/runtimeConfig.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+///
+
+import { pickAllowlistedMoesifAppUrl } from '../utils/moesifEmbed';
+
+export type InsightsRuntimeConfig = {
+ /** Same-origin BFF proxy prefix, e.g. "/proxy". */
+ platformApiBaseUrl: string;
+ platformApiVersion: string;
+ /** Moesif wrap host (iframe postMessage origin). Undefined when not configured. */
+ moesifAppUrl?: string;
+};
+
+type LegacyWindowConfig = Partial<{
+ PLATFORM_API_BASE_URL: string;
+ platformApiBaseUrl: string;
+ PLATFORM_API_VERSION: string;
+ platformApiVersion: string;
+ MOESIF_APP_URL: string;
+ MOESIF_BASIC_INSIGHTS_URL: string;
+ moesifAppUrl: string;
+ moesifBasicInsightsUrl: string;
+ /** AI Workspace BFF runtime key (`moesif_web_url` → APIP_AIW_MOESIF_WEB_URL). */
+ APIP_AIW_MOESIF_WEB_URL: string;
+}>;
+
+const fromWindow = (): LegacyWindowConfig => {
+ const globalWindow = window as Window & {
+ config?: LegacyWindowConfig;
+ __RUNTIME_CONFIG__?: LegacyWindowConfig;
+ };
+ return {
+ ...(globalWindow.__RUNTIME_CONFIG__ ?? {}),
+ ...(globalWindow.config ?? {}),
+ };
+};
+
+const configuredMoesifAppUrl = () =>
+ fromWindow().MOESIF_APP_URL ||
+ fromWindow().MOESIF_BASIC_INSIGHTS_URL ||
+ fromWindow().moesifAppUrl ||
+ fromWindow().moesifBasicInsightsUrl ||
+ fromWindow().APIP_AIW_MOESIF_WEB_URL ||
+ import.meta.env.VITE_MOESIF_APP_URL ||
+ import.meta.env.VITE_MOESIF_BASIC_INSIGHTS_URL ||
+ import.meta.env.APIP_AIW_MOESIF_WEB_URL ||
+ '';
+
+const resolveInsightsMoesifAppUrl = (): string | undefined => {
+ const configured = configuredMoesifAppUrl().trim();
+ if (!configured) return undefined;
+ return pickAllowlistedMoesifAppUrl(configured);
+};
+
+/** True when a trusted Moesif wrap origin is available (runtime or Vite env). */
+export const isInsightsMoesifConfigured = (): boolean =>
+ Boolean(resolveInsightsMoesifAppUrl());
+
+/** Same-origin BFF proxy prefix from Vite `base` (`/` → `/proxy`, `/ai-workspace/` → `/ai-workspace/proxy`). */
+export const defaultPlatformApiBaseUrl = (
+ viteBase = import.meta.env.BASE_URL
+): string => {
+ const base = String(viteBase ?? '/').replace(/\/$/, '');
+ return `${base}/proxy`;
+};
+
+export const insightsRuntimeConfig: InsightsRuntimeConfig = {
+ platformApiBaseUrl:
+ fromWindow().PLATFORM_API_BASE_URL ||
+ fromWindow().platformApiBaseUrl ||
+ import.meta.env.VITE_PLATFORM_API_BASE_URL ||
+ defaultPlatformApiBaseUrl(),
+ platformApiVersion:
+ fromWindow().PLATFORM_API_VERSION ||
+ fromWindow().platformApiVersion ||
+ import.meta.env.VITE_PLATFORM_API_VERSION ||
+ 'v0.9',
+ moesifAppUrl: resolveInsightsMoesifAppUrl(),
+};
+
+export const platformApiRoot = () => {
+ const base = insightsRuntimeConfig.platformApiBaseUrl.replace(/\/$/, '');
+ const version = insightsRuntimeConfig.platformApiVersion.replace(/^\//, '');
+ return `${base}/api/${version}`;
+};
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/config/vite-env.d.ts b/portals/cloud-plugins/apip-cloud-ui-insights/src/config/vite-env.d.ts
new file mode 100644
index 0000000000..292b123138
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/config/vite-env.d.ts
@@ -0,0 +1,15 @@
+///
+
+interface ImportMetaEnv {
+ readonly BASE_URL: string;
+ readonly VITE_PLATFORM_API_BASE_URL?: string;
+ readonly VITE_PLATFORM_API_VERSION?: string;
+ readonly VITE_MOESIF_APP_URL?: string;
+ readonly VITE_MOESIF_BASIC_INSIGHTS_URL?: string;
+ readonly APIP_AIW_MOESIF_WEB_URL?: string;
+ readonly VITE_ENVIRONMENT_NAME?: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/hostPort.ts b/portals/cloud-plugins/apip-cloud-ui-insights/src/hostPort.ts
new file mode 100644
index 0000000000..6dd3393dd9
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/hostPort.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Hand-mirrors the host Port types from api-control-plane and ai-workspace.
+ */
+export type NotifySeverity = 'success' | 'info' | 'warning' | 'error';
+
+export type InsightsHostPort = {
+ orgHandle: string;
+ projectHandle?: string;
+ navigate: (path: string) => void;
+ notify: (message: string, severity?: NotifySeverity) => void;
+};
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/index.ts b/portals/cloud-plugins/apip-cloud-ui-insights/src/index.ts
new file mode 100644
index 0000000000..4d2ec5fb8d
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/index.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { default as InsightsFeature } from './InsightsFeature';
+export type { InsightsFeatureProps } from './InsightsFeature';
+export type { InsightsHostPort, NotifySeverity } from './hostPort';
+export type {
+ InsightsEmbedProfile,
+ InsightsEmbedScope,
+ InsightsScopeLevel,
+} from './types';
+export { isInsightsMoesifConfigured } from './config/runtimeConfig';
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/types.ts b/portals/cloud-plugins/apip-cloud-ui-insights/src/types.ts
new file mode 100644
index 0000000000..92220ddef4
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/types.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export type InsightsScopeLevel = 'organization' | 'project';
+
+/**
+ * Host-specific Moesif iframe shape. Chosen by the host registry, not by
+ * runtime config — the Moesif origin is configured; the path is not.
+ *
+ * - `api-control-plane`: `/wrap/basic` (org) and `/wrap/basic?project_id=` (project)
+ * - `ai-workspace`: `/wrap/basic/ai-overview?...` for both org and project (no filter)
+ */
+export type InsightsEmbedProfile = 'api-control-plane' | 'ai-workspace';
+
+export type InsightsEmbedScope = {
+ level: InsightsScopeLevel;
+ projectId?: string | null;
+ projectName?: string | null;
+};
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/utils/moesifEmbed.test.ts b/portals/cloud-plugins/apip-cloud-ui-insights/src/utils/moesifEmbed.test.ts
new file mode 100644
index 0000000000..5440e69c54
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/utils/moesifEmbed.test.ts
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import {
+ buildAiWorkspaceIframeSrc,
+ buildBasicIframeSrc,
+ buildBasicProjectIframeSrc,
+ resolveInsightsScopeLevel,
+ resolveMoesifEmbeddingOrigin,
+ resolveTrustedMoesifAppUrl,
+} from './moesifEmbed';
+
+describe('moesifEmbed helpers', () => {
+ it('resolves insights scope level', () => {
+ expect(resolveInsightsScopeLevel({})).toBe('organization');
+ expect(resolveInsightsScopeLevel({ projectHandle: 'p' })).toBe('project');
+ });
+
+ it('builds wrap/basic iframe src like choreo-console main', () => {
+ expect(buildBasicIframeSrc('https://web-dev.moesif.com')).toBe(
+ 'https://web-dev.moesif.com/wrap/basic#auth=post'
+ );
+ });
+
+ it('adds project_id for project-level wrap/basic', () => {
+ expect(
+ buildBasicProjectIframeSrc('https://web-dev.moesif.com', 'proj-1')
+ ).toBe(
+ 'https://web-dev.moesif.com/wrap/basic?project_id=proj-1#auth=post'
+ );
+ });
+
+ it('builds the AI Workspace ai-overview iframe src', () => {
+ expect(buildAiWorkspaceIframeSrc('https://web-dev.moesif.com')).toBe(
+ 'https://web-dev.moesif.com/wrap/basic/ai-overview?embedded_ui=true&isolated_section=true#auth=post'
+ );
+ });
+
+ it('falls back to org iframe when project id is empty', () => {
+ expect(buildBasicProjectIframeSrc('https://web-dev.moesif.com', ' ')).toBe(
+ 'https://web-dev.moesif.com/wrap/basic#auth=post'
+ );
+ });
+
+ it('normalizes moesif app url to serialized origin for postMessage', () => {
+ expect(
+ resolveMoesifEmbeddingOrigin('https://www.moesif.com/')
+ ).toBe('https://www.moesif.com');
+ expect(
+ resolveMoesifEmbeddingOrigin('https://web-dev.moesif.com')
+ ).toBe('https://web-dev.moesif.com');
+ });
+
+ it('rejects untrusted moesif hosts and uses allowlisted fallback', () => {
+ expect(
+ resolveTrustedMoesifAppUrl(
+ 'https://evil.example.com',
+ 'https://web-dev.moesif.com'
+ )
+ ).toBe('https://web-dev.moesif.com');
+ });
+
+ it('returns undefined when no allowlisted host is configured', () => {
+ expect(
+ resolveTrustedMoesifAppUrl(
+ 'https://evil.example.com',
+ 'https://also-evil.example.com'
+ )
+ ).toBeUndefined();
+ });
+
+ it('rejects non-https moesif hosts', () => {
+ expect(
+ resolveTrustedMoesifAppUrl(
+ 'http://www.moesif.com',
+ 'https://www.moesif.com'
+ )
+ ).toBe('https://www.moesif.com');
+ });
+
+ it('accepts allowlisted https moesif hosts', () => {
+ expect(
+ resolveTrustedMoesifAppUrl(
+ 'https://www.moesif.com/wrap',
+ 'https://web-dev.moesif.com'
+ )
+ ).toBe('https://www.moesif.com');
+ });
+});
+
+describe('analyticsApi', () => {
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ it('fetchViewerToken reads token from cloud analytics endpoint', async () => {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn(async () => ({
+ ok: true,
+ json: async () => ({ token: 'viewer-token-123' }),
+ }))
+ );
+
+ const { fetchViewerToken } = await import('../api/analyticsApi');
+ await expect(fetchViewerToken()).resolves.toBe('viewer-token-123');
+ expect(fetch).toHaveBeenCalledWith(
+ '/proxy/cloud/analytics/id-token',
+ expect.objectContaining({ credentials: 'include' })
+ );
+ });
+
+ it('fetchViewerToken maps 404 to a user-facing org message', async () => {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn(async () => ({
+ ok: false,
+ status: 404,
+ json: async () => ({ error: 'not found' }),
+ }))
+ );
+
+ const { fetchViewerToken } = await import('../api/analyticsApi');
+ await expect(fetchViewerToken()).rejects.toThrow(
+ /not available for this organization/i
+ );
+ });
+
+ it('fetchViewerToken maps 502 without exposing the status code', async () => {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn(async () => ({
+ ok: false,
+ status: 502,
+ json: async () => ({ error: 'bad gateway' }),
+ }))
+ );
+
+ const { fetchViewerToken } = await import('../api/analyticsApi');
+ await expect(fetchViewerToken()).rejects.toThrow(
+ /temporarily unavailable/i
+ );
+ await expect(fetchViewerToken()).rejects.not.toThrow(/502/);
+ });
+
+ it('resolveProjectScope does not reuse the org-mapping 404 copy', async () => {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn(async () => ({
+ ok: false,
+ status: 404,
+ json: async () => ({ error: 'missing' }),
+ }))
+ );
+
+ const { resolveProjectScope } = await import('../api/analyticsApi');
+ await expect(resolveProjectScope('default', 'missing-proj')).rejects.toThrow(
+ /Project "missing-proj" was not found/
+ );
+ });
+
+ it('resolveProjectScope matches platform-api project handle in id field', async () => {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn(async (url: string) => ({
+ ok: true,
+ json: async () => {
+ if (url.includes('/projects/new-project')) {
+ return {
+ id: 'new-project',
+ displayName: 'New Project',
+ organizationId: 'default',
+ };
+ }
+ return { list: [] };
+ },
+ }))
+ );
+
+ const { resolveProjectScope } = await import('../api/analyticsApi');
+ await expect(resolveProjectScope('default', 'new-project')).resolves.toEqual({
+ projectId: 'new-project',
+ projectName: 'New Project',
+ });
+ });
+
+ it('resolveProjectScope prefers uuid when present', async () => {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn(async () => ({
+ ok: true,
+ json: async () => ({
+ id: 'new-project',
+ uuid: '019feb1e-63d9-71f7-a693-1956ac197303',
+ displayName: 'New Project',
+ }),
+ }))
+ );
+
+ const { resolveProjectScope } = await import('../api/analyticsApi');
+ await expect(resolveProjectScope('default', 'new-project')).resolves.toEqual({
+ projectId: '019feb1e-63d9-71f7-a693-1956ac197303',
+ projectName: 'New Project',
+ });
+ });
+
+ it('resolveProjectScope matches project id when handler differs', async () => {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn(async () => ({
+ ok: true,
+ json: async () => ({
+ id: 'proj-uuid',
+ handler: 'orders',
+ uuid: 'proj-uuid',
+ displayName: 'Orders',
+ }),
+ }))
+ );
+
+ const { resolveProjectScope } = await import('../api/analyticsApi');
+ await expect(resolveProjectScope('default', 'proj-uuid')).resolves.toEqual({
+ projectId: 'proj-uuid',
+ projectName: 'Orders',
+ });
+ });
+
+ it('resolveProjectScope sends Accept and X-Org-Id headers', async () => {
+ const fetchMock = vi.fn(async () => ({
+ ok: true,
+ json: async () => ({
+ id: 'proj-uuid',
+ displayName: 'Orders',
+ }),
+ }));
+ vi.stubGlobal('fetch', fetchMock);
+
+ const { resolveProjectScope } = await import('../api/analyticsApi');
+ await resolveProjectScope('default', 'proj-uuid');
+
+ expect(fetchMock).toHaveBeenCalledWith(
+ expect.stringContaining('/projects/proj-uuid'),
+ expect.objectContaining({
+ headers: expect.objectContaining({
+ accept: 'application/json',
+ 'X-Org-Id': 'default',
+ }),
+ })
+ );
+ });
+});
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/utils/moesifEmbed.ts b/portals/cloud-plugins/apip-cloud-ui-insights/src/utils/moesifEmbed.ts
new file mode 100644
index 0000000000..cd31e62dfc
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/utils/moesifEmbed.ts
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { InsightsScopeLevel } from '../types';
+
+/** postMessage types used by Moesif wrap/basic embed (`#auth=post`). */
+export const MOESIF_EMBEDDED_POST_MESSAGE_TYPES = {
+ SET_TOKEN: 'SET_TOKEN',
+ ORG_LOAD_FINISHED: 'ORG_LOAD_FINISHED',
+ SCHEMA_GEN_FINISHED: 'SCHEMA_GEN_FINISHED',
+ REFRESH_TOKEN: 'REFRESH_TOKEN',
+} as const;
+
+/** Known Moesif wrap/basic hosts — reject misconfigured runtime URLs. */
+export const ALLOWED_MOESIF_ORIGINS = new Set([
+ 'https://www.moesif.com',
+ 'https://web-dev.moesif.com',
+]);
+
+/** Serialized origin for postMessage targetOrigin and MessageEvent.origin checks. */
+export const resolveMoesifEmbeddingOrigin = (moesifAppUrl: string): string =>
+ new URL(moesifAppUrl).origin;
+
+/** Return configuredUrl when it is HTTPS and on the Moesif allowlist. */
+export const pickAllowlistedMoesifAppUrl = (
+ configuredUrl: string
+): string | undefined => {
+ try {
+ const parsed = new URL(configuredUrl);
+ if (parsed.protocol !== 'https:') return undefined;
+ if (!ALLOWED_MOESIF_ORIGINS.has(parsed.origin)) return undefined;
+ return parsed.origin;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Return a trusted Moesif app base URL (HTTPS + allowlisted origin).
+ * Returns undefined when neither URL is on the allowlist — callers must not
+ * silently fall back to web-dev in production.
+ */
+export const resolveTrustedMoesifAppUrl = (
+ configuredUrl: string,
+ fallbackUrl: string
+): string | undefined =>
+ pickAllowlistedMoesifAppUrl(configuredUrl) ??
+ pickAllowlistedMoesifAppUrl(fallbackUrl);
+
+/**
+ * Org-level wrap/basic iframe.
+ * Shape matches choreo-console: `{origin}/wrap/basic#auth=post`
+ */
+export const buildBasicIframeSrc = (embeddingOrigin: string) =>
+ `${embeddingOrigin.replace(/\/$/, '')}/wrap/basic#auth=post`;
+
+/**
+ * Project-level wrap/basic iframe with Moesif `project_id` filtering.
+ */
+export const buildBasicProjectIframeSrc = (
+ embeddingOrigin: string,
+ projectId: string
+) => {
+ const origin = embeddingOrigin.replace(/\/$/, '');
+ const cleaned = projectId.trim();
+ if (!cleaned) return buildBasicIframeSrc(embeddingOrigin);
+ return `${origin}/wrap/basic?project_id=${encodeURIComponent(
+ cleaned
+ )}#auth=post`;
+};
+
+/**
+ * AI Workspace Insights iframe — same URL at organization and project scope
+ * (no project_id filtering). Matches choreo / AIW Main.tsx:
+ * `{origin}/wrap/basic/ai-overview?embedded_ui=true&isolated_section=true#auth=post`
+ */
+export const buildAiWorkspaceIframeSrc = (embeddingOrigin: string) =>
+ `${embeddingOrigin.replace(/\/$/, '')}/wrap/basic/ai-overview?embedded_ui=true&isolated_section=true#auth=post`;
+
+export const resolveInsightsScopeLevel = (params: {
+ projectHandle?: string;
+}): InsightsScopeLevel =>
+ params.projectHandle ? 'project' : 'organization';
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/src/utils/routeParams.ts b/portals/cloud-plugins/apip-cloud-ui-insights/src/utils/routeParams.ts
new file mode 100644
index 0000000000..f4182b597b
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/src/utils/routeParams.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/** Parse org/project/api handles from the current browser pathname. */
+export const parseInsightsRouteParams = (pathname: string) => {
+ const orgMatch = pathname.match(/\/organizations\/([^/]+)/);
+ const projectMatch = pathname.match(/\/projects\/([^/]+)/);
+ const apiMatch = pathname.match(/\/apis\/([^/]+)/);
+
+ return {
+ orgHandle: orgMatch?.[1],
+ projectHandler: projectMatch?.[1],
+ apiHandler: apiMatch?.[1],
+ };
+};
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/tsconfig.json b/portals/cloud-plugins/apip-cloud-ui-insights/tsconfig.json
new file mode 100644
index 0000000000..facba9f607
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "baseUrl": ".",
+ "paths": {
+ "@wso2/oxygen-ui": ["../../ai-workspace/node_modules/@wso2/oxygen-ui"],
+ "@wso2/oxygen-ui-icons-react": [
+ "../../ai-workspace/node_modules/@wso2/oxygen-ui-icons-react"
+ ],
+ "react": ["../../ai-workspace/node_modules/@types/react/index.d.ts"],
+ "react/jsx-runtime": [
+ "../../ai-workspace/node_modules/@types/react/jsx-runtime.d.ts"
+ ]
+ },
+ "jsx": "react-jsx",
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "types": ["vitest/globals", "@testing-library/jest-dom"]
+ },
+ "include": ["src"]
+}
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/vitest.config.ts b/portals/cloud-plugins/apip-cloud-ui-insights/vitest.config.ts
new file mode 100644
index 0000000000..39c65369ca
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/vitest.config.ts
@@ -0,0 +1,11 @@
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ environment: 'jsdom',
+ globals: true,
+ setupFiles: ['./vitest.setup.ts'],
+ },
+});
diff --git a/portals/cloud-plugins/apip-cloud-ui-insights/vitest.setup.ts b/portals/cloud-plugins/apip-cloud-ui-insights/vitest.setup.ts
new file mode 100644
index 0000000000..bb02c60cd0
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui-insights/vitest.setup.ts
@@ -0,0 +1 @@
+import '@testing-library/jest-dom/vitest';
diff --git a/portals/cloud-plugins/apip-cloud-ui/package.json b/portals/cloud-plugins/apip-cloud-ui/package.json
index 0c47347927..ff88bfa654 100644
--- a/portals/cloud-plugins/apip-cloud-ui/package.json
+++ b/portals/cloud-plugins/apip-cloud-ui/package.json
@@ -14,6 +14,7 @@
"@wso2-enterprise/apip-cloud-ui-deploy": "file:../apip-cloud-ui-deploy",
"@wso2-enterprise/apip-cloud-ui-environments-new": "file:../apip-cloud-ui-environments-new",
"@wso2-enterprise/apip-cloud-ui-gateways": "file:../apip-cloud-ui-gateways",
+ "@wso2-enterprise/apip-cloud-ui-insights": "file:../apip-cloud-ui-insights",
"@wso2-enterprise/apip-cloud-ui-pipelines": "file:../apip-cloud-ui-pipelines",
"@wso2/oxygen-ui": "0.5.0",
"@wso2/oxygen-ui-icons-react": "0.5.0",
diff --git a/portals/cloud-plugins/apip-cloud-ui/src/hosts/ai-workspace.tsx b/portals/cloud-plugins/apip-cloud-ui/src/hosts/ai-workspace.tsx
index 9a4b734db4..66fa165eb2 100644
--- a/portals/cloud-plugins/apip-cloud-ui/src/hosts/ai-workspace.tsx
+++ b/portals/cloud-plugins/apip-cloud-ui/src/hosts/ai-workspace.tsx
@@ -11,10 +11,15 @@ import { Boxes, Network, Workflow } from '@wso2/oxygen-ui-icons-react';
import { EnvironmentsFeature } from '@wso2-enterprise/apip-cloud-ui-environments-new';
import { GatewaysFeature } from '@wso2-enterprise/apip-cloud-ui-gateways';
-import { PipelinesFeature, ProjectPipelinesFeature } from '@wso2-enterprise/apip-cloud-ui-pipelines';
+import { InsightsFeature } from '@wso2-enterprise/apip-cloud-ui-insights';
+import {
+ PipelinesFeature,
+ ProjectPipelinesFeature,
+} from '@wso2-enterprise/apip-cloud-ui-pipelines';
import {
AI_WORKSPACE_GATEWAYS_NAV_REGION,
AI_WORKSPACE_GATEWAYS_SLOT,
+ AI_WORKSPACE_INSIGHTS_SLOT,
type AIWorkspaceCloudEntry,
type AIWorkspaceExtension,
} from '../../../../ai-workspace/src/extensions';
@@ -35,6 +40,10 @@ import { defineCloudPlugin, getCloudExtensions, type CloudPluginFeature } from '
* It also carries nav placement so the entry sits between Environments and
* Pipelines, suppressing the built-in item via `hides`.
*
+ * `insights` registers against `AI_WORKSPACE_INSIGHTS_SLOT` the same way —
+ * see `InsightsRoute` in `ai-workspace/src/App.tsx` — so the built-in Insights
+ * nav stays and only the page body is replaced when Moesif is configured.
+ *
* The deploy feature is deliberately NOT registered here. Deploying is scoped to
* one API — the page reads and writes that API's deployments — and this host has
* no API-scoped placement, so its Port carries no `apiHandle`. Registered here
@@ -101,6 +110,21 @@ export const cloudPluginFeatures: CloudPluginFeature[] =
},
],
}),
+ defineCloudPlugin({
+ id: 'insights',
+ version: '0.1.0',
+ extensions: [
+ {
+ id: 'insights',
+ slot: AI_WORKSPACE_INSIGHTS_SLOT,
+ order: 0,
+ // Same Moesif ai-overview URL at org and project — no project_id filter.
+ render: (port) => (
+
+ ),
+ },
+ ],
+ }),
];
export const cloudExtensions = getCloudExtensions(cloudPluginFeatures);
diff --git a/portals/cloud-plugins/apip-cloud-ui/src/hosts/api-control-plane.tsx b/portals/cloud-plugins/apip-cloud-ui/src/hosts/api-control-plane.tsx
index 02cad50760..14e42d9238 100644
--- a/portals/cloud-plugins/apip-cloud-ui/src/hosts/api-control-plane.tsx
+++ b/portals/cloud-plugins/apip-cloud-ui/src/hosts/api-control-plane.tsx
@@ -7,11 +7,12 @@
* You may not alter or remove any copyright or other notice from copies of this content.
*/
-import { Layers, Workflow } from '@wso2/oxygen-ui-icons-react';
+import { BarChart3, Layers, Workflow } from '@wso2/oxygen-ui-icons-react';
import { DeployFeature } from '@wso2-enterprise/apip-cloud-ui-deploy';
import { EnvironmentsFeature } from '@wso2-enterprise/apip-cloud-ui-environments-new';
import { GatewaysFeature } from '@wso2-enterprise/apip-cloud-ui-gateways';
+import { InsightsFeature } from '@wso2-enterprise/apip-cloud-ui-insights';
import {
PipelinesFeature,
ProjectPipelinesFeature,
@@ -24,6 +25,7 @@ import {
import { routes } from '../../../../api-control-plane/src/routes/paths';
import { ScopeGate } from '../../../../api-control-plane/src/scope/ScopeGate';
import { defineCloudPlugin, getCloudExtensions, type CloudPluginFeature } from '../plugin';
+import { filterExtensionsForRuntime } from '../runtimeFlags';
/**
* Cloud features registered for the api-control-plane host. All live in this
@@ -61,6 +63,10 @@ import { defineCloudPlugin, getCloudExtensions, type CloudPluginFeature } from '
* organization- or project-level page (which the sidebar allows, and is a normal
* thing to do) left a dead end instead of the project/API picker that navigates
* to the scoped URL.
+ *
+ * `insights` registers org/project sidebar Moesif embeds and hides the
+ * built-in Insights parent outside API scope when loaded. Gated on
+ * `cloudProxyEnabled` via `filterExtensionsForRuntime`.
*/
export const cloudPluginFeatures: CloudPluginFeature[] = [
defineCloudPlugin({
@@ -164,7 +170,68 @@ export const cloudPluginFeatures: CloudPluginFeature[]
},
],
}),
+ defineCloudPlugin({
+ id: 'insights',
+ version: '0.1.0',
+ extensions: [
+ {
+ id: 'organization-insights',
+ slot: 'sidebar.organization',
+ order: 60,
+ routePath: 'insights',
+ label: 'Insights',
+ group: 'api',
+ level: 'organization',
+ icon: ,
+ isVisible: (scope) => {
+ const typed = scope as {
+ isOrganizationScope?: boolean;
+ isProjectScope?: boolean;
+ isApiScope?: boolean;
+ };
+ return (
+ Boolean(typed.isOrganizationScope) &&
+ !typed.isProjectScope &&
+ !typed.isApiScope
+ );
+ },
+ render: (port) => (
+
+ ),
+ },
+ {
+ id: 'project-insights',
+ slot: 'sidebar.project',
+ order: 60,
+ routePath: 'insights',
+ label: 'Insights',
+ group: 'api',
+ level: 'project',
+ icon: ,
+ isVisible: (scope) => {
+ const typed = scope as {
+ isProjectScope?: boolean;
+ isApiScope?: boolean;
+ };
+ return Boolean(typed.isProjectScope) && !typed.isApiScope;
+ },
+ render: (port) => (
+
+ ),
+ },
+ ],
+ }),
];
-export const cloudExtensions = getCloudExtensions(cloudPluginFeatures);
+export const cloudExtensions = filterExtensionsForRuntime(
+ getCloudExtensions(cloudPluginFeatures)
+);
export type { ApiControlPlaneExtension };
diff --git a/portals/cloud-plugins/apip-cloud-ui/src/runtimeFlags.test.ts b/portals/cloud-plugins/apip-cloud-ui/src/runtimeFlags.test.ts
new file mode 100644
index 0000000000..1baa9a7727
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui/src/runtimeFlags.test.ts
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC (http://www.wso2.com). All Rights Reserved.
+ *
+ * This software is the property of WSO2 LLC and its suppliers, if any.
+ * Dissemination of any information or reproduction of any material contained
+ * herein in any form is strictly forbidden, unless permitted by WSO2 expressly.
+ * You may not alter or remove any copyright or other notice from copies of this content.
+ */
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import {
+ CLOUD_INSIGHTS_EXTENSION_IDS,
+ filterExtensionsForRuntime,
+} from './runtimeFlags';
+
+beforeEach(() => {
+ vi.stubGlobal('window', {
+ __RUNTIME_CONFIG__: {},
+ config: {},
+ });
+});
+
+afterEach(() => {
+ vi.unstubAllGlobals();
+});
+
+describe('filterExtensionsForRuntime', () => {
+ const extensions = [
+ { id: 'organization-insights' },
+ { id: 'project-insights' },
+ { id: 'other-feature' },
+ ] as const;
+
+ it('drops cloud Insights entries when cloudProxyEnabled is absent', () => {
+ const filtered = filterExtensionsForRuntime([...extensions]);
+
+ expect(filtered.map((entry) => entry.id)).toEqual(['other-feature']);
+ for (const id of CLOUD_INSIGHTS_EXTENSION_IDS) {
+ expect(filtered.some((entry) => entry.id === id)).toBe(false);
+ }
+ });
+
+ it('keeps cloud Insights entries when cloudProxyEnabled is true', () => {
+ (window as Window & { __RUNTIME_CONFIG__?: Record })
+ .__RUNTIME_CONFIG__ = { cloudProxyEnabled: 'true' };
+
+ const filtered = filterExtensionsForRuntime([...extensions]);
+
+ expect(filtered.map((entry) => entry.id)).toEqual([
+ 'organization-insights',
+ 'project-insights',
+ 'other-feature',
+ ]);
+ });
+});
diff --git a/portals/cloud-plugins/apip-cloud-ui/src/runtimeFlags.ts b/portals/cloud-plugins/apip-cloud-ui/src/runtimeFlags.ts
new file mode 100644
index 0000000000..22e0c87b26
--- /dev/null
+++ b/portals/cloud-plugins/apip-cloud-ui/src/runtimeFlags.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC (http://www.wso2.com). All Rights Reserved.
+ *
+ * This software is the property of WSO2 LLC and its suppliers, if any.
+ * Dissemination of any information or reproduction of any material contained
+ * herein in any form is strictly forbidden, unless permitted by WSO2 expressly.
+ * You may not alter or remove any copyright or other notice from copies of this content.
+ */
+
+type RuntimeConfigWindow = Window & {
+ __RUNTIME_CONFIG__?: Record;
+ config?: Record;
+};
+
+export const readRuntimeBoolean = (key: string): boolean => {
+ const runtimeWindow =
+ typeof window === 'undefined' ? undefined : (window as RuntimeConfigWindow);
+ const value =
+ runtimeWindow?.__RUNTIME_CONFIG__?.[key] ??
+ runtimeWindow?.config?.[key];
+ return value === true || value === 'true';
+};
+
+/** Cloud Insights sidebar entries require the BFF "cloud" named upstream. */
+export const CLOUD_INSIGHTS_EXTENSION_IDS = new Set([
+ 'organization-insights',
+ 'project-insights',
+]);
+
+export const filterExtensionsForRuntime = <
+ TExtension extends { id: string },
+>(
+ extensions: readonly TExtension[]
+): TExtension[] =>
+ extensions.filter(
+ (extension) =>
+ !CLOUD_INSIGHTS_EXTENSION_IDS.has(extension.id) ||
+ readRuntimeBoolean('cloudProxyEnabled')
+ );
diff --git a/portals/cloud-plugins/apip-cloud-ui/tsconfig.json b/portals/cloud-plugins/apip-cloud-ui/tsconfig.json
index 75ced84105..e08ab494b1 100644
--- a/portals/cloud-plugins/apip-cloud-ui/tsconfig.json
+++ b/portals/cloud-plugins/apip-cloud-ui/tsconfig.json
@@ -7,6 +7,7 @@
"@wso2-enterprise/apip-cloud-ui-deploy": ["../apip-cloud-ui-deploy/src/index.ts"],
"@wso2-enterprise/apip-cloud-ui-environments-new": ["../apip-cloud-ui-environments-new/src/index.ts"],
"@wso2-enterprise/apip-cloud-ui-gateways": ["../apip-cloud-ui-gateways/src/index.ts"],
+ "@wso2-enterprise/apip-cloud-ui-insights": ["../apip-cloud-ui-insights/src/index.ts"],
"@wso2-enterprise/apip-cloud-ui-pipelines": ["../apip-cloud-ui-pipelines/src/index.ts"],
"@wso2/oxygen-ui": ["../../ai-workspace/node_modules/@wso2/oxygen-ui"],
"@wso2/oxygen-ui-icons-react": ["../../ai-workspace/node_modules/@wso2/oxygen-ui-icons-react"],