Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 60 additions & 42 deletions frontend/src/components/layout/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useWorkflowUiStore } from '@/store/workflowUiStore';
import { useAuthStore, DEFAULT_ORG_ID } from '@/store/authStore';
import { cn } from '@/lib/utils';
import { env } from '@/config/env';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';

interface TopBarProps {
workflowId?: string;
Expand All @@ -45,6 +46,7 @@ interface TopBarProps {
onRedo?: () => void;
canUndo?: boolean;
canRedo?: boolean;
hasAnalyticsSink?: boolean;
}

const DEFAULT_WORKFLOW_NAME = 'Untitled Workflow';
Expand All @@ -63,6 +65,7 @@ export function TopBar({
onRedo,
canUndo,
canRedo,
hasAnalyticsSink = false,
}: TopBarProps) {
const navigate = useNavigate();
const [isSaving, setIsSaving] = useState(false);
Expand Down Expand Up @@ -469,48 +472,63 @@ export function TopBar({
{env.VITE_OPENSEARCH_DASHBOARDS_URL &&
workflowId &&
(!selectedRunId || (selectedRunStatus && selectedRunStatus !== 'RUNNING')) && (
<Button
variant="outline"
size="sm"
className="gap-1.5 md:gap-2 min-w-0"
disabled={!isOrgReady}
onClick={() => {
if (!isOrgReady) return;
const baseUrl = env.VITE_OPENSEARCH_DASHBOARDS_URL.replace(/\/+$/, '');
// Filter by run_id if a specific run is selected, otherwise by workflow_id
const filterQuery = selectedRunId
? `shipsec.run_id.keyword:"${selectedRunId}"`
: `shipsec.workflow_id.keyword:"${workflowId}"`;
// Use the run's backend-resolved org ID when available (matches indexed data),
// fall back to auth store org ID for workflow-level queries
const effectiveOrgId = (selectedRunOrgId || organizationId).toLowerCase();
const orgScopedPattern = `security-findings-${effectiveOrgId}-*`;
// OpenSearch Data Explorer URL format
// Use .keyword fields for exact match filtering
// Use 'all time' range (1 year) since run_id is unique - no need to filter by time
const aParam = encodeURIComponent(
`(discover:(columns:!(_source),interval:auto,sort:!()),metadata:(indexPattern:'${orgScopedPattern}',view:discover))`,
);
const qParam = encodeURIComponent(
`(query:(language:kuery,query:'${filterQuery}'))`,
);
const gParam = encodeURIComponent(
'(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-1y,to:now))',
);
const url = `${baseUrl}/app/data-explorer/discover/#?_a=${aParam}&_q=${qParam}&_g=${gParam}`;
window.open(url, '_blank', 'noopener,noreferrer');
}}
title={
!isOrgReady
? 'Loading organization context...'
: selectedRunId
? 'View analytics for this run in OpenSearch Dashboards'
: 'View analytics for this workflow in OpenSearch Dashboards'
}
>
<ExternalLink className="h-4 w-4" />
<span className="hidden lg:inline">View Analytics</span>
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex">
<Button
variant="outline"
size="sm"
className="gap-1.5 md:gap-2 min-w-0"
disabled={!isOrgReady || !hasAnalyticsSink}
onClick={() => {
if (!isOrgReady || !hasAnalyticsSink) return;
const baseUrl = env.VITE_OPENSEARCH_DASHBOARDS_URL.replace(
/\/+$/,
'',
);
// Filter by run_id if a specific run is selected, otherwise by workflow_id
const filterQuery = selectedRunId
? `shipsec.run_id.keyword:"${selectedRunId}"`
: `shipsec.workflow_id.keyword:"${workflowId}"`;
// Use the run's backend-resolved org ID when available (matches indexed data),
// fall back to auth store org ID for workflow-level queries
const effectiveOrgId = (
selectedRunOrgId || organizationId
).toLowerCase();
const orgScopedPattern = `security-findings-${effectiveOrgId}-*`;
// OpenSearch Data Explorer URL format
// Use .keyword fields for exact match filtering
// Use 'all time' range (1 year) since run_id is unique - no need to filter by time
const aParam = encodeURIComponent(
`(discover:(columns:!(_source),interval:auto,sort:!()),metadata:(indexPattern:'${orgScopedPattern}',view:discover))`,
);
const qParam = encodeURIComponent(
`(query:(language:kuery,query:'${filterQuery}'))`,
);
const gParam = encodeURIComponent(
'(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-1y,to:now))',
);
const url = `${baseUrl}/app/data-explorer/discover/#?_a=${aParam}&_q=${qParam}&_g=${gParam}`;
window.open(url, '_blank', 'noopener,noreferrer');
}}
>
<ExternalLink className="h-4 w-4" />
<span className="hidden lg:inline">View Analytics</span>
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{!hasAnalyticsSink
? 'Connect analytics sink to view analytics'
: !isOrgReady
? 'Loading organization context...'
: selectedRunId
? 'View analytics for this run in OpenSearch Dashboards'
: 'View analytics for this workflow in OpenSearch Dashboards'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}

<Button
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/features/workflow-builder/WorkflowBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,15 @@ function WorkflowBuilderContent() {
// This allows smooth transition without forcing mode change
const isInspectorVisible = mode === 'execution' || (selectedRunId !== null && mode !== 'design');

const hasAnalyticsSink = useMemo(() => {
// When viewing a specific run, bypass the sink check — the run may have
// indexed results even if the current design no longer contains a sink.
if (selectedRunId) return true;
return designNodes.some((node) => {
return (node.data?.componentId ?? node.data?.componentSlug) === 'core.analytics.sink';
});
}, [designNodes, selectedRunId]);

const shouldShowInitialLoader =
isLoading && designNodes.length === 0 && executionNodes.length === 0 && !isNewWorkflow;

Expand Down Expand Up @@ -948,6 +957,7 @@ function WorkflowBuilderContent() {
onRedo={redo}
canUndo={canUndo}
canRedo={canRedo}
hasAnalyticsSink={hasAnalyticsSink}
/>
);

Expand Down