-
{panel.title}
- {panel.description && (
-
{panel.description}
- )}
+
+ {loading && (
+
+
)}
+ {console.log('Rendering panel:', panel.id, 'with URL:', iframeUrl)}
-
);
diff --git a/src/components/dashboard/panel-preview.jsx b/src/components/dashboard/panel-preview.jsx
new file mode 100644
index 00000000..07a07785
--- /dev/null
+++ b/src/components/dashboard/panel-preview.jsx
@@ -0,0 +1,237 @@
+import { useState, useEffect, useRef } from 'react';
+import { DashboardPanel } from './dashboard-panel';
+import { dashboardsService } from '@/services/grafana/dashboards';
+import { Skeleton } from '@/components/ui/skeleton';
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
+import { AlertCircle } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+
+/**
+ * Component that creates a temporary dashboard and shows a real preview of the panel
+ * using the provided configuration in an actual Grafana panel.
+ */
+export function PanelPreview({
+ panelConfig,
+ height = 300,
+ baseDashboardUid = null,
+ onClose = () => {},
+ showCloseButton = false,
+ customTimeRange = null
+}) {
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [tempDashboard, setTempDashboard] = useState(null);
+ const [tempPanel, setTempPanel] = useState(null);
+
+ // Get timeRange from panelConfig first, use customTimeRange as fallback, or use a default value
+ const [timeRange, setTimeRange] = useState(() => {
+ if (panelConfig?.time) {
+ return panelConfig.time;
+ } else if (customTimeRange) {
+ return customTimeRange;
+ } else {
+ return { from: 'now-7d', to: 'now' }; // Wider time range to avoid "Zoom out of range" issues
+ }
+ });
+
+ const [dashboardUrl, setDashboardUrl] = useState(null);
+ const [dashboardCreated, setDashboardCreated] = useState(false);
+
+ // References to track dashboard creation across renders
+ const creationInProgressRef = useRef(false);
+ const dashboardUidRef = useRef(null);
+
+ // Create a temporary dashboard when the component mounts
+ useEffect(() => {
+ if (dashboardCreated || !panelConfig) {
+ setLoading(false);
+ return;
+ }
+
+ const createTempDashboard = async () => {
+ if (creationInProgressRef.current || dashboardCreated) {
+ return;
+ }
+
+ creationInProgressRef.current = true;
+ setLoading(true);
+ setError(null);
+
+ try {
+ // Ensure panelConfig has a timeRange
+ const configWithTimeRange = {
+ ...panelConfig,
+ time: panelConfig.time || timeRange
+ };
+
+ const options = {
+ timeRange: configWithTimeRange.time,
+ autoCleanup: true
+ };
+
+ const response = await dashboardsService.createTemporaryDashboard(configWithTimeRange, baseDashboardUid, options);
+
+ const responseData = response.data || response;
+
+ if (responseData && responseData.dashboard) {
+ setTempDashboard(responseData.dashboard);
+ dashboardUidRef.current = responseData.dashboard.uid;
+
+ setDashboardCreated(true);
+
+ const grafanaUrl = import.meta.env.VITE_GRAFANA_URL || 'http://localhost:3100';
+ const dashUrl = `${grafanaUrl}/d/${responseData.dashboard.uid}`;
+ setDashboardUrl(dashUrl);
+
+ // Create temporary panel data until we get real panel info
+ const tempPanelData = {
+ id: 1,
+ title: panelConfig.title || 'Panel Preview',
+ type: panelConfig.type || 'gauge',
+ description: panelConfig.description || '',
+ options: panelConfig.options || {},
+ rawSql: panelConfig.sql || '',
+ panelId: 1
+ };
+
+ setTempPanel(tempPanelData);
+
+ // Try to get panels after creating the dashboard
+ setTimeout(async () => {
+ try {
+ const panelsResponse = await dashboardsService.getPanels(dashboardUidRef.current);
+ const panels = Array.isArray(panelsResponse)
+ ? panelsResponse
+ : (panelsResponse.data || []);
+
+ if (panels && panels.length > 0) {
+ setTempPanel(panels[0]);
+ }
+ } catch (panelErr) {
+ console.warn('Could not get panels from dashboard:', panelErr);
+ }
+ }, 1000); // Wait for Grafana to process the dashboard
+ } else {
+ throw new Error('Temporary dashboard information was not received');
+ }
+ } catch (err) {
+ console.error('Error creating temporary dashboard:', err);
+ setError(err.message || 'Error creating temporary dashboard');
+ setDashboardCreated(false);
+ creationInProgressRef.current = false;
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (!creationInProgressRef.current) {
+ createTempDashboard();
+ }
+
+ // Clean up temporary dashboard when component unmounts
+ return () => {
+ if (dashboardUidRef.current) {
+ dashboardsService.delete(dashboardUidRef.current)
+ .catch(err => console.error('Error deleting temporary dashboard:', err));
+ dashboardUidRef.current = null;
+ creationInProgressRef.current = false;
+ }
+ };
+ //eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [panelConfig, baseDashboardUid]);
+
+ // Update timeRange when panelConfig or customTimeRange changes
+ useEffect(() => {
+ const newTimeRange = panelConfig?.time || customTimeRange || { from: 'now-7d', to: 'now' };
+ setTimeRange(newTimeRange);
+ }, [panelConfig, customTimeRange]);
+
+ // Loading state
+ if (loading) {
+ return (
+
+
+
+
Creating preview...
+
+
+ );
+ }
+
+ // Error state
+ if (error) {
+ return (
+
+
+
+ Preview Error
+
+ {typeof error === 'object' ? JSON.stringify(error) : error}
+
+ The temporary dashboard was created, but panel information could not be obtained.
+ Try again or check the panel configuration.
+
+ {dashboardUrl && (
+
+
+
+ )}
+
+
+
+ );
+ }
+
+ // No dashboard created
+ if (!tempDashboard) {
+ return (
+
+
Could not create temporary dashboard
+
+ );
+ }
+
+ // Successful state - show panel or dashboard
+ return (
+
+
+ {!tempPanel &&
Showing full dashboard due to missing panel information
}
+
+
+ {tempPanel ? (
+ // Show panel with timeRange
+
+ ) : (
+ // Show iframe with the complete dashboard
+
+
+
+ )}
+
+ {showCloseButton && (
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/layouts/sidebar.jsx b/src/components/layouts/sidebar.jsx
index fd234f1e..d32427d5 100644
--- a/src/components/layouts/sidebar.jsx
+++ b/src/components/layouts/sidebar.jsx
@@ -1,4 +1,4 @@
-import {Home, FolderOpen, Shapes, Workflow, FileSliders, ChartNoAxesCombined, ChevronRight, ChevronsUpDown, LogOut, SquareAsterisk } from 'lucide-react';
+import {Home, FolderOpen, Shapes, Workflow, FileSliders, ChartNoAxesCombined, ChevronRight, ChevronsUpDown, LogOut, SquareAsterisk, GitGraph, LucideGitGraph, GitGraphIcon, LucidePencil, LucideGlasses, LucideFileArchive, LucideFileCheck, LucideFile, LucideLockKeyhole, LucideLogOut, LucideHome, LucideEdit2, LucideEdit, LucideEdit3, LucideFileEdit, LucideActivity, LucideMap, LucideClipboard, LucideScreenShare, LucideChartArea, LucideChartNetwork, LucideChartSpline, LucideActivitySquare, LucideChartColumn, LucideChartColumnBig, LucideCakeSlice, LucideChartPie, LucideWorkflow } from 'lucide-react';
import { Link } from 'react-router';
import { MoreHorizontal } from 'lucide-react';
import {
@@ -42,32 +42,32 @@ const data = [
{
title: 'Home',
url: '/app',
- icon: Home,
+ icon: LucideHome,
},
{
title: 'Catalogs',
url: '/app/catalogs',
- icon: FolderOpen,
+ icon: LucideFile,
},
{
title: 'Dashboards',
url: '/app/dashboards',
- icon: ChartNoAxesCombined,
+ icon: LucideChartPie,
},
{
title: 'Scopes',
url: '/app/scopes',
- icon: Shapes,
+ icon: LucideGlasses,
},
{
title: 'Mashups',
url: '/app/mashups',
- icon: FileSliders,
+ icon: LucideWorkflow,
},
{
title: 'Editor',
url: '/app/editor',
- icon: Workflow,
+ icon: LucideScreenShare,
roles: ['admin', 'developer'],
},
]
@@ -80,13 +80,13 @@ const data = [
type: 'item',
title: 'Secrets',
url: '/app/secrets',
- icon: SquareAsterisk,
+ icon: LucideLockKeyhole,
},
{
type: 'item',
title: 'Logout',
url: '/logout',
- icon: LogOut,
+ icon: LucideLogOut,
},
]
}
diff --git a/src/components/mashups/PreviousTests.jsx b/src/components/mashups/PreviousTests.jsx
new file mode 100644
index 00000000..3c0b51ae
--- /dev/null
+++ b/src/components/mashups/PreviousTests.jsx
@@ -0,0 +1,102 @@
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Trash2, Eye, Calendar } from 'lucide-react';
+import { format } from 'date-fns';
+
+export function PreviousTests({
+ tests,
+ onDeleteTest,
+ onViewTest,
+ mashupName
+}) {
+ if (!tests || tests.length === 0) {
+ return (
+
+
+ Previous Tests
+
+
+
+ No previous tests found for this mashup.
+
+
+
+ );
+ }
+
+ const getTestStatus = (test) => {
+ if (test.results?.error) return 'error';
+ if (test.results?.computationResults?.code === 200) return 'success';
+ if (test.results?.mashupResponse) return 'partial';
+ return 'unknown';
+ };
+
+ const getStatusBadge = (status) => {
+ switch (status) {
+ case 'success':
+ return
Success;
+ case 'partial':
+ return
Partial;
+ case 'error':
+ return
Error;
+ default:
+ return
Unknown;
+ }
+ };
+
+ return (
+
+
+ Previous Tests
+
+
+ {tests.map((test) => {
+ const status = getTestStatus(test);
+ const computationsCount = test.results?.computationResults?.computations?.length || 0;
+
+ return (
+
+
+
+ {getStatusBadge(status)}
+
+
+
+
+ {format(new Date(test.timestamp), 'MMM dd, yyyy HH:mm')}
+
+ {computationsCount > 0 && (
+
{computationsCount} computations
+ )}
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/mashups/TestResults.jsx b/src/components/mashups/TestResults.jsx
new file mode 100644
index 00000000..419f12f0
--- /dev/null
+++ b/src/components/mashups/TestResults.jsx
@@ -0,0 +1,200 @@
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
+import { Loader2, ExternalLink } from 'lucide-react';
+
+function HierarchicalTestResults({ testResults }) {
+
+ const renderComputationSummary = (computations) => {
+ if (!computations || !Array.isArray(computations)) return null;
+
+ const totalComputations = computations.length;
+ const computationsWithEvidences = computations.reduce((acc, comp) => {
+ return acc + (comp.evidences ? comp.evidences.length : 0);
+ }, 0);
+
+ return (
+
+
Summary
+
+
+ Total Computations: {totalComputations}
+
+
+ Total Evidences: {computationsWithEvidences}
+
+
+
+ );
+ };
+
+ const renderEvidenceTable = (evidences) => {
+ if (!evidences || !Array.isArray(evidences)) return null;
+
+ return (
+
+
Evidences ({evidences.length})
+
+
+
+ Scope
+ Value
+ Document
+ Created
+
+
+
+ {evidences.map((evidence, index) => (
+
+
+ {evidence.scope ? (
+
+ {evidence.scope.member &&
Member: {evidence.scope.member}
}
+ {evidence.scope.project &&
Project: {evidence.scope.project}
}
+ {evidence.scope.class &&
Class: {evidence.scope.class}
}
+
+ ) : 'N/A'}
+
+
+ {evidence.value !== null && evidence.value !== undefined ? (
+
+ {String(evidence.value)}
+
+ ) : (
+ null
+ )}
+
+
+ {evidence.evidences?.[0]?.document || 'No document'}
+
+
+ {evidence.createdAt ? new Date(evidence.createdAt).toLocaleString() : 'N/A'}
+
+
+ ))}
+
+
+
+ );
+ };
+
+ return (
+
+ {/* Computation Results Section */}
+ {testResults.computationResults && (
+
+
+ {/* Computations Summary */}
+ {testResults.computationResults.computations &&
+ renderComputationSummary(testResults.computationResults.computations)}
+
+ {/* Detailed Computations */}
+ {testResults.computationResults.computations &&
+ testResults.computationResults.computations.map((computation, index) => (
+
+
+
+ Compliance Result:
+
+
+ {computation.value !== null && computation.value !== undefined && (
+
+ {String(computation.value)}
+
+ )}
+
+
+
+ {/* Computation Scope */}
+ {computation.scope && (
+
+
Scope
+
+ {computation.scope.project && (
+
Project: {computation.scope.project}
+ )}
+ {computation.scope.class && (
+
Class: {computation.scope.class}
+ )}
+
+
+ )}
+
+ {/* Evidences Table */}
+ {computation.evidences && renderEvidenceTable(computation.evidences)}
+
+ ))}
+
+
+ )}
+
+ {/* Error Section */}
+ {testResults.error && (
+
+
Error
+
{testResults.error}
+
+ )}
+
+ );
+}
+
+export function TestResults({
+ testResults,
+ loadingTestExecution,
+ mashupDetails,
+ onReset,
+ onClose
+}) {
+ if (loadingTestExecution) {
+ return (
+
+
+ Running Test...
+
+
+
+
+ Running mashup test...
+
+
+
+ );
+ }
+
+ if (testResults) {
+ return (
+
+
+
+
+ {mashupDetails?.id && (
+
+
+
+ )}
+
+
+ );
+ }
+
+ return (
+
+
+ Mashup Test
+
+
+
+ The test will run automatically after creating the control.
+
+
+
+ );
+}
diff --git a/src/components/ui/combobox.jsx b/src/components/ui/combobox.jsx
new file mode 100644
index 00000000..0a3c36fb
--- /dev/null
+++ b/src/components/ui/combobox.jsx
@@ -0,0 +1,85 @@
+// src/components/ui/combobox.jsx
+import * as React from "react"
+import { Check, ChevronsUpDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command"
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover"
+
+/**
+ * A reusable ComboBox component for selecting from a list of options with search functionality.
+ *
+ * @param {object} props - The component props.
+ * @param {Array