diff --git a/frontend/src/components/integration/CodeBlock.tsx b/frontend/src/components/integration/CodeBlock.tsx new file mode 100644 index 0000000..8ad7f84 --- /dev/null +++ b/frontend/src/components/integration/CodeBlock.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Button, message, theme } from 'antd'; +import { CopyOutlined } from '@ant-design/icons'; + +const { useToken } = theme; + +interface CodeBlockProps { + code: string; + label: string; + maxHeight?: number; +} + +const CodeBlock: React.FC = ({ code, label, maxHeight }) => { + const { token } = useToken(); + + const copy = async () => { + try { + await navigator.clipboard.writeText(code); + message.success(`${label} copied`); + } catch { + message.error('Clipboard unavailable'); + } + }; + + if (!code) { + return ( +
+        Configure search parameters in the previous step to generate this snippet.
+      
+ ); + } + + return ( +
+ +
+        {code}
+      
+
+ ); +}; + +export default CodeBlock; diff --git a/frontend/src/components/integration/DependencyCategoryGroup.tsx b/frontend/src/components/integration/DependencyCategoryGroup.tsx new file mode 100644 index 0000000..316c945 --- /dev/null +++ b/frontend/src/components/integration/DependencyCategoryGroup.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { Card, Table, Tag, Row, Col, Statistic, Tooltip, Empty, Typography, theme } from 'antd'; +import type { DependencySummaryService, DependencySummaryGroup } from '../../store/api/communicationApi'; + +const { Text } = Typography; + +const CATEGORY_COLORS: Record = { + database: 'blue', + cache: 'green', + message_broker: 'purple', +}; + +interface DependencyCategoryGroupProps { + group: DependencySummaryGroup; + title: string; +} + +const DependencyCategoryGroup: React.FC = ({ group, title }) => { + const { token } = theme.useToken(); + if (!group || group.total === 0) { + return ; + } + + return ( +
+ + + {(group.critical_count ?? 0) > 0 && ( + + )} + + + {Object.entries(group.by_category || {}).map(([cat, services]: [string, DependencySummaryService[]]) => ( + + {cat} + ({services.length}) + + } + style={{ marginBottom: 8 }} + > + `${r.namespace}/${r.name}`} + size="small" + pagination={false} + columns={[ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + render: (v: string, r: DependencySummaryService) => ( + <> + {v} + {r.is_critical && critical} + + ), + }, + { title: 'Namespace', dataIndex: 'namespace', key: 'ns' }, + { title: 'Kind', dataIndex: 'kind', key: 'kind', render: (v: string) => v ? {v} : '-' }, + { title: 'Port', dataIndex: 'port', key: 'port', render: (v: number) => v ?? '-' }, + { + title: 'Annotations', + key: 'ann', + render: (_: unknown, r: DependencySummaryService) => { + const entries = Object.entries(r.annotations || {}); + if (!entries.length) return -; + const gitRepo = r.annotations['git-repo'] || r.annotations['gitRepo'] || r.annotations['source-repo']; + if (gitRepo) return git-repo; + return ( + `${k}=${v}`).join(', ')}> + {entries.length} annotations + + ); + }, + }, + ]} + /> + + ))} + + ); +}; + +export default DependencyCategoryGroup; diff --git a/frontend/src/pages/AIIntegrationHub.tsx b/frontend/src/pages/AIIntegrationHub.tsx index bc72ee1..d4a489b 100644 --- a/frontend/src/pages/AIIntegrationHub.tsx +++ b/frontend/src/pages/AIIntegrationHub.tsx @@ -14,7 +14,6 @@ import { Typography, Alert, Divider, - Empty, Tooltip, message, Spin, @@ -23,13 +22,11 @@ import { Descriptions, Statistic, Radio, + theme, } from 'antd'; import { RobotOutlined, - ApartmentOutlined, CodeOutlined, - CopyOutlined, - ApiOutlined, CheckCircleOutlined, ArrowLeftOutlined, ArrowRightOutlined, @@ -37,11 +34,10 @@ import { ArrowUpOutlined, RocketOutlined, EyeOutlined, - AlertOutlined, - MessageOutlined, - AuditOutlined, ExperimentOutlined, KeyOutlined, + ThunderboltOutlined, + InfoCircleOutlined, } from '@ant-design/icons'; import { Link, useSearchParams } from 'react-router-dom'; import { useGetClustersQuery } from '../store/api/clusterApi'; @@ -49,152 +45,45 @@ import { useGetAnalysesQuery } from '../store/api/analysisApi'; import { useLazyGetDependencySummaryQuery, DependencySummaryParams, - DependencySummaryService, - DependencySummaryGroup, MatchedService, } from '../store/api/communicationApi'; +import CodeBlock from '../components/integration/CodeBlock'; +import DependencyCategoryGroup from '../components/integration/DependencyCategoryGroup'; +import { + PIPELINE_PLATFORMS, + ID_METHODS, + buildCurlSnippet, + buildPythonSnippet, + buildJsSnippet, + buildPipelineSnippet, + buildBlastRadiusCurlSnippet, + buildBlastRadiusPipelineSnippet, +} from '../utils/snippetBuilders'; const { Text, Title, Paragraph } = Typography; const { Option } = Select; -const API_BASE = typeof window !== 'undefined' ? `${window.location.origin}/api/v1` : '/api/v1'; - -type IntegrationType = 'cicd' | 'agent' | 'explorer' | null; - -const PIPELINE_PLATFORMS = [ - { value: 'azure_devops', label: 'Azure DevOps' }, - { value: 'github_actions', label: 'GitHub Actions' }, - { value: 'gitlab_ci', label: 'GitLab CI' }, - { value: 'jenkins', label: 'Jenkins' }, - { value: 'tekton', label: 'Tekton' }, - { value: 'argocd', label: 'ArgoCD' }, - { value: 'other', label: 'Other' }, -]; - -const AGENT_TYPES = [ - { value: 'code_review', label: 'Code Review' }, - { value: 'security_scan', label: 'Security Scan' }, - { value: 'architecture', label: 'Architecture Compliance' }, - { value: 'migration', label: 'Migration Impact' }, - { value: 'custom', label: 'Custom' }, -]; - -const ID_METHODS = [ - { value: 'annotation', label: 'Annotation (e.g. git-repo URL)' }, - { value: 'label', label: 'Label (e.g. app name)' }, - { value: 'namespace_deployment', label: 'Namespace + Deployment' }, - { value: 'pod_name', label: 'Pod Name' }, - { value: 'advanced', label: 'Advanced (any combination)' }, -]; - -const CODE_BLOCK_STYLE: React.CSSProperties = { - background: '#0d1117', - color: '#e6edf3', - padding: 16, - borderRadius: 8, - overflow: 'auto', - fontSize: 12, - margin: 0, - whiteSpace: 'pre-wrap', - wordBreak: 'break-all', -}; - -function CodeBlockWithCopy({ code, label }: { code: string; label: string }) { - const copy = async () => { - try { - await navigator.clipboard.writeText(code); - message.success(`${label} copied`); - } catch { - message.error('Clipboard unavailable'); - } - }; - return ( -
- -
{code}
-
- ); -} - -function CategoryGroup({ group, title }: { group: DependencySummaryGroup; title: string }) { - if (!group || group.total === 0) { - return ; - } - - return ( -
- -
- {(group.critical_count ?? 0) > 0 && ( - - )} - - - {Object.entries(group.by_category || {}).map(([cat, services]) => ( - {cat} ({services.length})} - style={{ marginBottom: 8 }} - > -
`${r.namespace}/${r.name}`} - size="small" - pagination={false} - columns={[ - { title: 'Name', dataIndex: 'name', key: 'name', render: (v: string, r: DependencySummaryService) => <>{v}{r.is_critical && critical} }, - { title: 'Namespace', dataIndex: 'namespace', key: 'ns' }, - { title: 'Kind', dataIndex: 'kind', key: 'kind', render: (v: string) => v ? {v} : '-' }, - { title: 'Port', dataIndex: 'port', key: 'port', render: (v: number) => v ?? '-' }, - { - title: 'Annotations', - key: 'ann', - render: (_: unknown, r: DependencySummaryService) => { - const entries = Object.entries(r.annotations || {}); - if (!entries.length) return -; - const gitRepo = r.annotations['git-repo'] || r.annotations['gitRepo'] || r.annotations['source-repo']; - if (gitRepo) return git-repo; - return `${k}=${v}`).join(', ')}>{entries.length} annotations; - }, - }, - ]} - /> - - ))} - - ); -} - const AIIntegrationHub: React.FC = () => { + const { token } = theme.useToken(); const [currentStep, setCurrentStep] = useState(0); - const [integrationType, setIntegrationType] = useState(null); const [form] = Form.useForm(); const [selectedAnalysisIds, setSelectedAnalysisIds] = useState([]); const [platform, setPlatform] = useState('azure_devops'); - const [agentType, setAgentType] = useState('code_review'); const [idMethod, setIdMethod] = useState('annotation'); const [depth, setDepth] = useState(1); - // Lazy query: triggered manually, always fresh. Use isFetching (not isLoading) - // so the spinner shows even during forced refetch of cached params. const [triggerSummary, { data: rawSummaryData, isFetching: summaryLoading, error: rawSummaryError }] = useLazyGetDependencySummaryQuery(); const [summaryParams, setSummaryParams] = useState(null); const [summaryCleared, setSummaryCleared] = useState(false); const summaryData = summaryCleared ? undefined : rawSummaryData; const summaryError = summaryCleared ? undefined : rawSummaryError; - const resetSummary = useCallback(() => setSummaryCleared(true), []); + const resetSummary = useCallback(() => { + setSummaryCleared(true); + setSummaryParams(null); + }, []); - // Pre-fill from URL params (e.g. when navigating from Map page) const [searchParams] = useSearchParams(); useEffect(() => { const urlOwner = searchParams.get('owner_name'); @@ -202,11 +91,8 @@ const AIIntegrationHub: React.FC = () => { const urlAnnotationKey = searchParams.get('annotation_key'); const urlAnnotationValue = searchParams.get('annotation_value'); if (urlOwner || urlNs || urlAnnotationKey) { - setIntegrationType('explorer'); - setCurrentStep(1); if (urlAnnotationKey) setIdMethod('annotation'); - else if (urlOwner && urlNs) setIdMethod('namespace_deployment'); - else if (urlOwner) setIdMethod('pod_name'); + else if (urlOwner || urlNs) setIdMethod('namespace_deployment'); setTimeout(() => { const fields: Record = {}; if (urlOwner) fields.owner_name = urlOwner; @@ -218,7 +104,6 @@ const AIIntegrationHub: React.FC = () => { } }, []); // eslint-disable-line react-hooks/exhaustive-deps - // Load ALL analyses (no cluster filter) and cluster names for display const { data: clustersData } = useGetClustersQuery(); const clusters: any[] = (clustersData as any)?.clusters || []; const clusterNameMap = useMemo(() => { @@ -247,7 +132,7 @@ const AIIntegrationHub: React.FC = () => { return raw; }, [summaryError, summaryData]); - const canProceedStep1 = selectedAnalysisIds.length > 0; + const canProceedStep0 = selectedAnalysisIds.length > 0; const buildParamsFromForm = useCallback((): DependencySummaryParams | null => { const values = form.getFieldsValue(); @@ -295,261 +180,16 @@ const AIIntegrationHub: React.FC = () => { return; } setSummaryParams(params); - setCurrentStep(3); + setCurrentStep(2); }, [selectedAnalysisIds, buildParamsFromForm]); - const buildQueryString = useCallback(() => { - if (!summaryParams) return ''; - const qs = new URLSearchParams(); - summaryParams.analysis_ids.forEach(id => qs.append('analysis_ids', String(id))); - if (summaryParams.annotation_key) qs.set('annotation_key', summaryParams.annotation_key); - if (summaryParams.annotation_value) qs.set('annotation_value', summaryParams.annotation_value); - if (summaryParams.label_key) qs.set('label_key', summaryParams.label_key); - if (summaryParams.label_value) qs.set('label_value', summaryParams.label_value); - if (summaryParams.namespace) qs.set('namespace', summaryParams.namespace); - if (summaryParams.owner_name) qs.set('owner_name', summaryParams.owner_name); - if (summaryParams.pod_name) qs.set('pod_name', summaryParams.pod_name); - if (summaryParams.ip) qs.set('ip', summaryParams.ip); - if (summaryParams.depth && summaryParams.depth > 1) qs.set('depth', String(summaryParams.depth)); - return qs.toString(); - }, [summaryParams]); - - const buildCurlSnippet = useCallback(() => { - const qsStr = buildQueryString(); - if (!qsStr) return ''; - return `# Get your API key from Settings > API Keys\nFLOWFISH_API_KEY='**********'\n\ncurl -sf -H "X-API-Key: $FLOWFISH_API_KEY" \\\n "${API_BASE}/communications/dependencies/summary?${qsStr}"`; - }, [buildQueryString]); - - const buildPipelineSnippet = useCallback(() => { - const qsStr = buildQueryString(); - if (!qsStr) return ''; - - const baseUrl = typeof window !== 'undefined' ? window.location.origin : 'https://your-flowfish-instance'; - - if (platform === 'azure_devops') { - return `# Azure DevOps Pipeline - Flowfish Integration -# Set FLOWFISH_API_KEY as a secret variable in Pipeline Settings > Variables -variables: - FLOWFISH_URL: '${baseUrl}' - FLOWFISH_QUERY: '${qsStr}' - -steps: - - script: | - DEPS=$(curl -sf -H "X-API-Key: $(FLOWFISH_API_KEY)" \\ - "$(FLOWFISH_URL)/api/v1/communications/dependencies/summary?$(FLOWFISH_QUERY)") - echo "$DEPS" > flowfish-deps.json - - CRITICAL=$(echo "$DEPS" | python3 -c " -import json,sys -d=json.load(sys.stdin) -c=d.get('downstream',{}).get('critical_count',0) -print(c) -") - echo "##vso[task.setvariable variable=CRITICAL_DEPS]$CRITICAL" - displayName: 'Flowfish: Get Cross-Project Dependencies' - env: - FLOWFISH_API_KEY: $(FLOWFISH_API_KEY) - FLOWFISH_URL: $(FLOWFISH_URL) - - - script: | - python ai-agent/analyze.py \\ - --pr-diff $(System.PullRequest.PullRequestId) \\ - --deps flowfish-deps.json - displayName: 'AI Impact Analysis (Cross-Project)' - condition: succeededOrFailed()`; - } - - if (platform === 'github_actions') { - return `# GitHub Actions - Flowfish Integration -# Store your API key in repository secrets as FLOWFISH_API_KEY -# Set FLOWFISH_URL in repository variables (Settings > Secrets and variables > Actions) -env: - FLOWFISH_QUERY: '${qsStr}' - -jobs: - flowfish: - steps: - - name: Get Flowfish Dependencies - id: flowfish - run: | - curl -sf -H "X-API-Key: \${{ secrets.FLOWFISH_API_KEY }}" \\ - "\${{ vars.FLOWFISH_URL }}/api/v1/communications/dependencies/summary?\${FLOWFISH_QUERY}" \\ - > flowfish-deps.json - - CRITICAL=$(python3 -c " -import json -d=json.load(open('flowfish-deps.json')) -print(d.get('downstream',{}).get('critical_count',0)) -") - echo "critical_deps=$CRITICAL" >> $GITHUB_OUTPUT - - - name: AI Impact Analysis - run: | - python ai-agent/analyze.py \\ - --pr-diff \${{ github.event.pull_request.number }} \\ - --deps flowfish-deps.json`; - } - - if (platform === 'gitlab_ci') { - return `# GitLab CI - Flowfish Integration -# Store FLOWFISH_API_KEY and FLOWFISH_URL as CI/CD variables -variables: - FLOWFISH_URL: '${baseUrl}' - FLOWFISH_QUERY: '${qsStr}' - -flowfish_dependencies: - stage: test - script: - - | - curl -sf -H "X-API-Key: $FLOWFISH_API_KEY" \\ - "$FLOWFISH_URL/api/v1/communications/dependencies/summary?$FLOWFISH_QUERY" \\ - > flowfish-deps.json - - python ai-agent/analyze.py --deps flowfish-deps.json - artifacts: - paths: - - flowfish-deps.json`; - } - - if (platform === 'jenkins') { - return `// Jenkins Pipeline - Flowfish Integration -// Store API key as a Secret Text credential named 'flowfish-api-key' -def FLOWFISH_URL = '${baseUrl}' -def FLOWFISH_QUERY = '${qsStr}' - -stage('Flowfish Dependencies') { - steps { - withCredentials([string(credentialsId: 'flowfish-api-key', variable: 'FLOWFISH_API_KEY')]) { - script { - def deps = sh(returnStdout: true, script: """ - curl -sf -H "X-API-Key: \${FLOWFISH_API_KEY}" \\ - "\${FLOWFISH_URL}/api/v1/communications/dependencies/summary?\${FLOWFISH_QUERY}" - """).trim() - writeFile file: 'flowfish-deps.json', text: deps - } - } - } -}`; - } - - return `# Generic CI/CD - Flowfish Integration -# Get your API key from Flowfish Settings > API Keys -FLOWFISH_API_KEY='**********' -FLOWFISH_URL='${baseUrl}' -FLOWFISH_QUERY='${qsStr}' - -curl -sf -H "X-API-Key: $FLOWFISH_API_KEY" \\ - "$FLOWFISH_URL/api/v1/communications/dependencies/summary?$FLOWFISH_QUERY" \\ - > flowfish-deps.json`; - }, [buildQueryString, platform]); - - const buildPythonSnippet = useCallback(() => { - if (!summaryParams) return ''; - const paramLines: string[] = []; - summaryParams.analysis_ids.forEach(id => paramLines.push(` ("analysis_ids", "${id}"),`)); - if (summaryParams.annotation_key) paramLines.push(` ("annotation_key", "${summaryParams.annotation_key}"),`); - if (summaryParams.annotation_value) paramLines.push(` ("annotation_value", "${summaryParams.annotation_value}"),`); - if (summaryParams.label_key) paramLines.push(` ("label_key", "${summaryParams.label_key}"),`); - if (summaryParams.label_value) paramLines.push(` ("label_value", "${summaryParams.label_value}"),`); - if (summaryParams.namespace) paramLines.push(` ("namespace", "${summaryParams.namespace}"),`); - if (summaryParams.owner_name) paramLines.push(` ("owner_name", "${summaryParams.owner_name}"),`); - if (summaryParams.pod_name) paramLines.push(` ("pod_name", "${summaryParams.pod_name}"),`); - if (summaryParams.ip) paramLines.push(` ("ip", "${summaryParams.ip}"),`); - if (summaryParams.depth && summaryParams.depth > 1) paramLines.push(` ("depth", "${summaryParams.depth}"),`); - - return `import requests - -FLOWFISH_URL = "${API_BASE}" -FLOWFISH_API_KEY = "**********" # Get from Settings > API Keys - -resp = requests.get( - f"{FLOWFISH_URL}/communications/dependencies/summary", - params=[ -${paramLines.join('\n')} - ], - headers={"X-API-Key": FLOWFISH_API_KEY}, -) -resp.raise_for_status() -deps = resp.json() - -# Extract affected git repos from downstream annotations -affected_repos = [] -for category, services in deps.get("downstream", {}).get("by_category", {}).items(): - for svc in services: - repo = svc.get("annotations", {}).get("git-repo") - if repo: - affected_repos.append({ - "repo": repo, - "service": svc["name"], - "namespace": svc["namespace"], - "category": category, - "critical": svc.get("is_critical", False), - }) - -print(f"Found {len(affected_repos)} affected repositories") -for r in affected_repos: - flag = " [CRITICAL]" if r["critical"] else "" - print(f" {r['service']} ({r['category']}){flag} -> {r['repo']}")`; - }, [summaryParams]); - - const buildJsSnippet = useCallback(() => { - const qsStr = buildQueryString(); - if (!qsStr) return ''; - return `// Get your API key from Flowfish Settings > API Keys -const FLOWFISH_API_KEY = "**********"; -const FLOWFISH_URL = "${API_BASE}"; - -const resp = await fetch( - \`\${FLOWFISH_URL}/communications/dependencies/summary?${qsStr}\`, - { headers: { "X-API-Key": FLOWFISH_API_KEY } } -); -if (!resp.ok) throw new Error(\`HTTP \${resp.status}: \${await resp.text()}\`); -const deps = await resp.json(); - -// Extract affected repos -const affectedRepos = Object.entries(deps.downstream?.by_category ?? {}) - .flatMap(([category, services]) => - services - .filter(svc => svc.annotations?.["git-repo"]) - .map(svc => ({ - repo: svc.annotations["git-repo"], - service: svc.name, - category, - critical: svc.is_critical, - })) - ); - -console.log(\`Found \${affectedRepos.length} affected repos\`);`; - }, [buildQueryString]); - const responseSize = useMemo(() => { if (!summaryData) return 0; return Math.round(JSON.stringify(summaryData).length / 1024 * 10) / 10; }, [summaryData]); - // Step navigation helper - function StepNav({ disableNext, nextLabel }: { disableNext?: boolean; nextLabel?: string }) { - return ( -
- - -
- ); - } - - // ───────────────────────── RENDER ───────────────────────── + const contextNamespace = summaryParams?.namespace; + const contextOwnerName = summaryParams?.owner_name; return (
@@ -559,7 +199,7 @@ console.log(\`Found \${affectedRepos.length} affected repos\`);`; AI Integration Hub - Set up CI/CD pipeline and AI agent integrations with Flowfish dependency data. + Set up CI/CD pipeline and AI agent integrations with Flowfish dependency and impact data.
@@ -572,74 +212,19 @@ console.log(\`Found \${affectedRepos.length} affected repos\`);`; current={currentStep} onChange={(n) => { if (n < currentStep) setCurrentStep(n); - else if (n === 1 && integrationType) setCurrentStep(n); - else if (n === 2 && summaryData?.success) setCurrentStep(n); - else if (n === 3 && (summaryData?.success || summaryParams)) setCurrentStep(n); + else if (n === 1 && summaryData?.success) setCurrentStep(n); + else if (n === 2 && (summaryData?.success || summaryParams)) setCurrentStep(n); }} items={[ - { title: 'Integration Type', icon: }, { title: 'Configure', icon: }, { title: 'Preview', icon: }, - { title: 'Integration Setup', icon: }, + { title: 'Integration Code', icon: }, ]} /> - {/* ─── Step 0: Integration Type ─── */} + {/* ─── Step 0: Configure ─── */} {currentStep === 0 && ( -
- - {[ - { key: 'cicd' as const, icon: , title: 'CI/CD Pipeline', desc: 'PR validation, deployment gates, build job dependency and impact analysis', tags: ['Azure DevOps', 'GitHub Actions', 'GitLab CI', 'Jenkins'] }, - { key: 'agent' as const, icon: , title: 'AI Agent', desc: 'Code review agents, security scan agents, architecture compliance', tags: ['Code Review', 'Security', 'Compliance'] }, - { key: 'explorer' as const, icon: , title: 'Analysis Explorer', desc: 'Browse analysis dependencies, export data, generate reports', tags: ['Browse', 'Export', 'Audit'] }, - ].map(item => ( -
- { setIntegrationType(item.key); form.resetFields(); resetSummary(); setCurrentStep(1); }} - style={{ - borderColor: integrationType === item.key ? '#1677ff' : undefined, - borderWidth: integrationType === item.key ? 2 : 1, - height: '100%', - }} - > - - {item.icon} - {item.title} - {item.desc} -
{item.tags.map(t => {t})}
-
-
- - ))} - - - Coming Soon - - - {[ - { icon: , title: 'Monitoring & Alerting', desc: 'Prometheus/Grafana dashboards, PagerDuty/OpsGenie' }, - { icon: , title: 'Change Management', desc: 'ServiceNow/Jira change request enrichment' }, - { icon: , title: 'ChatOps', desc: 'Slack/Teams bot dependency queries' }, - ].map(item => ( - - - - {item.icon} - {item.title} - {item.desc} - - - - - ))} - - - )} - - {/* ─── Step 1: Configure ─── */} - {currentStep === 1 && ( - {PIPELINE_PLATFORMS.map(p => )} - - - )} + + { + const next = e.target.value; + if (next === 'advanced') { + setIdMethod(next); + resetSummary(); + return; + } + const keep: Record = {}; + const current = form.getFieldsValue(); + const fieldsForMethod: Record = { + annotation: ['annotation_key', 'annotation_value'], + label: ['label_key', 'label_value'], + namespace_deployment: ['namespace', 'owner_name'], + pod_name: ['pod_name'], + }; + const nextFields = fieldsForMethod[next] || []; + nextFields.forEach((f) => { if (current[f]) keep[f] = current[f]; }); + form.resetFields(); + if (Object.keys(keep).length) { + setTimeout(() => form.setFieldsValue(keep), 0); + } + setIdMethod(next); + resetSummary(); + }} + > + {ID_METHODS.map(m => {m.label})} + + - {integrationType === 'agent' && ( - - - - )} - - {(integrationType === 'cicd' || integrationType === 'agent') && ( - <> - - { setIdMethod(e.target.value); form.resetFields(); resetSummary(); }}> - {ID_METHODS.map(m => {m.label})} - - - -
- - {(idMethod === 'annotation' || idMethod === 'advanced') && ( - <> -
- - - - - - - - - - - )} - {(idMethod === 'label' || idMethod === 'advanced') && ( - <> - - - - - - - - - - - - )} - {(idMethod === 'namespace_deployment' || idMethod === 'advanced') && ( - <> - - - - - - - - - - - - )} - {(idMethod === 'pod_name' || idMethod === 'advanced') && ( - - - - - - )} - {idMethod === 'advanced' && ( - - - - - - )} - - - - )} - - {integrationType === 'explorer' && ( - - + + + {(idMethod === 'annotation' || idMethod === 'advanced') && ( + <> + + + + + + + + + + + + )} + {(idMethod === 'label' || idMethod === 'advanced') && ( + <> + + + + + + + + + + + + )} + {(idMethod === 'namespace_deployment' || idMethod === 'advanced') && ( + <> + + + + + + + + + + + + )} + {(idMethod === 'pod_name' || idMethod === 'advanced') && ( - - + + + )} + {idMethod === 'advanced' && ( - - + + - - - )} + )} + + + {PIPELINE_PLATFORMS.map(p => )} + + (affects pipeline snippet format) + + + {PIPELINE_PLATFORMS.find(p => p.value === platform)?.label || 'Pipeline'}, - children: , - }] : []), + children: , + }, { key: 'curl', label: curl, - children: , + children: , }, { key: 'python', label: Python, - children: , + children: , }, { key: 'js', label: JavaScript, - children: , + children: , + }, + { + key: 'blast-radius', + label: Blast Radius, + children: ( +
+ } + message="Pre-deployment risk assessment" + description="Use this endpoint to assess the impact of deploying changes to a service. Returns a risk score (0-100), affected services count, and actionable recommendations." + style={{ marginBottom: 16 }} + /> + , + }, + { + key: 'br-pipeline', + label: PIPELINE_PLATFORMS.find(p => p.value === platform)?.label || 'Pipeline', + children: , + }, + ]} + /> +
+ + + +
+
+ ), }, ]} /> @@ -1015,26 +669,24 @@ console.log(\`Found \${affectedRepos.length} affected repos\`);`; /> - {(integrationType === 'cicd' || integrationType === 'agent') && ( - - - The /dependencies/summary response groups all dependencies by service category (database, cache, api, message_broker, etc.). - Each dependency includes its Kubernetes annotations and labels. - - - Your AI agent should: - -
    -
  1. Extract annotations["git-repo"] from each downstream service to identify affected repositories
  2. -
  3. Check is_critical flag to prioritize critical dependency changes
  4. -
  5. Use service_category grouping to understand the type of each dependency (database, cache, API, etc.)
  6. -
  7. Examine callers to understand which services call the changed service
  8. -
-
- )} + + + The /dependencies/summary response groups all dependencies by service category (database, cache, api, message_broker, etc.). + Each dependency includes its Kubernetes annotations and labels. + + + Your AI agent or pipeline should: + +
    +
  1. Extract annotations["git-repo"] from each downstream service to identify affected repositories
  2. +
  3. Check is_critical flag to prioritize critical dependency changes
  4. +
  5. Use service_category grouping to understand the type of each dependency (database, cache, API, etc.)
  6. +
  7. Examine callers to understand which services call the changed service
  8. +
+
-
diff --git a/frontend/src/pages/BlastRadiusOracle.tsx b/frontend/src/pages/BlastRadiusOracle.tsx index a5f48ab..0190908 100644 --- a/frontend/src/pages/BlastRadiusOracle.tsx +++ b/frontend/src/pages/BlastRadiusOracle.tsx @@ -57,7 +57,9 @@ import { InfoCircleOutlined, FileTextOutlined, SettingOutlined, + RobotOutlined, } from '@ant-design/icons'; +import { Link } from 'react-router-dom'; import { useGetClustersQuery } from '../store/api/clusterApi'; import { useGetAnalysesQuery } from '../store/api/analysisApi'; import { colors } from '../styles/colors'; @@ -429,10 +431,13 @@ const BlastRadiusOracle: React.FC = () => { } }; - // Copy code to clipboard - const copyCode = (code: string) => { - navigator.clipboard.writeText(code); - message.success('Code copied to clipboard!'); + const copyCode = async (code: string) => { + try { + await navigator.clipboard.writeText(code); + message.success('Code copied to clipboard!'); + } catch { + message.error('Clipboard unavailable'); + } }; // View assessment detail @@ -747,6 +752,21 @@ const BlastRadiusOracle: React.FC = () => { )} /> + + + + +
+ Need dependency data for AI agents? +
+ + + AI Integration Hub — generate integration snippets for dependency analysis + + +
+
+
@@ -801,14 +821,15 @@ const BlastRadiusOracle: React.FC = () => { } >
                 {codeSnippets[selectedPlatform as keyof typeof codeSnippets]}
               
diff --git a/frontend/src/utils/snippetBuilders.ts b/frontend/src/utils/snippetBuilders.ts new file mode 100644 index 0000000..fe01045 --- /dev/null +++ b/frontend/src/utils/snippetBuilders.ts @@ -0,0 +1,426 @@ +import type { DependencySummaryParams } from '../store/api/communicationApi'; + +const getApiBase = () => + typeof window !== 'undefined' ? `${window.location.origin}/api/v1` : '/api/v1'; + +const getBaseUrl = () => + typeof window !== 'undefined' ? window.location.origin : 'https://your-flowfish-instance'; + +export function buildQueryString(params: DependencySummaryParams | null): string { + if (!params) return ''; + const qs = new URLSearchParams(); + params.analysis_ids.forEach(id => qs.append('analysis_ids', String(id))); + if (params.annotation_key) qs.set('annotation_key', params.annotation_key); + if (params.annotation_value) qs.set('annotation_value', params.annotation_value); + if (params.label_key) qs.set('label_key', params.label_key); + if (params.label_value) qs.set('label_value', params.label_value); + if (params.namespace) qs.set('namespace', params.namespace); + if (params.owner_name) qs.set('owner_name', params.owner_name); + if (params.pod_name) qs.set('pod_name', params.pod_name); + if (params.ip) qs.set('ip', params.ip); + if (params.depth && params.depth > 1) qs.set('depth', String(params.depth)); + return qs.toString(); +} + +export function buildCurlSnippet(params: DependencySummaryParams | null): string { + const qsStr = buildQueryString(params); + if (!qsStr) return ''; + const API_BASE = getApiBase(); + return `# Get your API key from Settings > API Keys +FLOWFISH_API_KEY='**********' + +curl -sf -H "X-API-Key: $FLOWFISH_API_KEY" \\ + "${API_BASE}/communications/dependencies/summary?${qsStr}"`; +} + +export function buildPythonSnippet(params: DependencySummaryParams | null): string { + if (!params) return ''; + const API_BASE = getApiBase(); + const paramLines: string[] = []; + params.analysis_ids.forEach(id => paramLines.push(` ("analysis_ids", "${id}"),`)); + if (params.annotation_key) paramLines.push(` ("annotation_key", "${params.annotation_key}"),`); + if (params.annotation_value) paramLines.push(` ("annotation_value", "${params.annotation_value}"),`); + if (params.label_key) paramLines.push(` ("label_key", "${params.label_key}"),`); + if (params.label_value) paramLines.push(` ("label_value", "${params.label_value}"),`); + if (params.namespace) paramLines.push(` ("namespace", "${params.namespace}"),`); + if (params.owner_name) paramLines.push(` ("owner_name", "${params.owner_name}"),`); + if (params.pod_name) paramLines.push(` ("pod_name", "${params.pod_name}"),`); + if (params.ip) paramLines.push(` ("ip", "${params.ip}"),`); + if (params.depth && params.depth > 1) paramLines.push(` ("depth", "${params.depth}"),`); + + return `import requests + +FLOWFISH_URL = "${API_BASE}" +FLOWFISH_API_KEY = "**********" # Get from Settings > API Keys + +resp = requests.get( + f"{FLOWFISH_URL}/communications/dependencies/summary", + params=[ +${paramLines.join('\n')} + ], + headers={"X-API-Key": FLOWFISH_API_KEY}, +) +resp.raise_for_status() +deps = resp.json() + +# Extract affected git repos from downstream annotations +affected_repos = [] +for category, services in deps.get("downstream", {}).get("by_category", {}).items(): + for svc in services: + repo = svc.get("annotations", {}).get("git-repo") + if repo: + affected_repos.append({ + "repo": repo, + "service": svc["name"], + "namespace": svc["namespace"], + "category": category, + "critical": svc.get("is_critical", False), + }) + +print(f"Found {len(affected_repos)} affected repositories") +for r in affected_repos: + flag = " [CRITICAL]" if r["critical"] else "" + print(f" {r['service']} ({r['category']}){flag} -> {r['repo']}")`; +} + +export function buildJsSnippet(params: DependencySummaryParams | null): string { + const qsStr = buildQueryString(params); + if (!qsStr) return ''; + const API_BASE = getApiBase(); + return `// Get your API key from Flowfish Settings > API Keys +const FLOWFISH_API_KEY = "**********"; +const FLOWFISH_URL = "${API_BASE}"; + +const resp = await fetch( + \`\${FLOWFISH_URL}/communications/dependencies/summary?${qsStr}\`, + { headers: { "X-API-Key": FLOWFISH_API_KEY } } +); +if (!resp.ok) throw new Error(\`HTTP \${resp.status}: \${await resp.text()}\`); +const deps = await resp.json(); + +// Extract affected repos +const affectedRepos = Object.entries(deps.downstream?.by_category ?? {}) + .flatMap(([category, services]) => + services + .filter(svc => svc.annotations?.["git-repo"]) + .map(svc => ({ + repo: svc.annotations["git-repo"], + service: svc.name, + category, + critical: svc.is_critical, + })) + ); + +console.log(\`Found \${affectedRepos.length} affected repos\`);`; +} + +export function buildPipelineSnippet( + params: DependencySummaryParams | null, + platform: string, +): string { + const qsStr = buildQueryString(params); + if (!qsStr) return ''; + const baseUrl = getBaseUrl(); + + if (platform === 'azure_devops') { + return `# Azure DevOps Pipeline - Flowfish Integration +# Set FLOWFISH_API_KEY as a secret variable in Pipeline Settings > Variables +variables: + FLOWFISH_URL: '${baseUrl}' + FLOWFISH_QUERY: '${qsStr}' + +steps: + - script: | + DEPS=$(curl -sf -H "X-API-Key: $(FLOWFISH_API_KEY)" \\ + "$(FLOWFISH_URL)/api/v1/communications/dependencies/summary?$(FLOWFISH_QUERY)") + echo "$DEPS" > flowfish-deps.json + + CRITICAL=$(echo "$DEPS" | python3 -c " +import json,sys +d=json.load(sys.stdin) +c=d.get('downstream',{}).get('critical_count',0) +print(c) +") + echo "##vso[task.setvariable variable=CRITICAL_DEPS]$CRITICAL" + displayName: 'Flowfish: Get Cross-Project Dependencies' + env: + FLOWFISH_API_KEY: $(FLOWFISH_API_KEY) + FLOWFISH_URL: $(FLOWFISH_URL) + + - script: | + python ai-agent/analyze.py \\ + --pr-diff $(System.PullRequest.PullRequestId) \\ + --deps flowfish-deps.json + displayName: 'AI Impact Analysis (Cross-Project)' + condition: succeededOrFailed()`; + } + + if (platform === 'github_actions') { + return `# GitHub Actions - Flowfish Integration +# Store your API key in repository secrets as FLOWFISH_API_KEY +# Set FLOWFISH_URL in repository variables (Settings > Secrets and variables > Actions) +env: + FLOWFISH_QUERY: '${qsStr}' + +jobs: + flowfish: + steps: + - name: Get Flowfish Dependencies + id: flowfish + run: | + curl -sf -H "X-API-Key: \${{ secrets.FLOWFISH_API_KEY }}" \\ + "\${{ vars.FLOWFISH_URL }}/api/v1/communications/dependencies/summary?\${FLOWFISH_QUERY}" \\ + > flowfish-deps.json + + CRITICAL=$(python3 -c " +import json +d=json.load(open('flowfish-deps.json')) +print(d.get('downstream',{}).get('critical_count',0)) +") + echo "critical_deps=$CRITICAL" >> $GITHUB_OUTPUT + + - name: AI Impact Analysis + run: | + python ai-agent/analyze.py \\ + --pr-diff \${{ github.event.pull_request.number }} \\ + --deps flowfish-deps.json`; + } + + if (platform === 'gitlab_ci') { + return `# GitLab CI - Flowfish Integration +# Store FLOWFISH_API_KEY and FLOWFISH_URL as CI/CD variables +variables: + FLOWFISH_URL: '${baseUrl}' + FLOWFISH_QUERY: '${qsStr}' + +flowfish_dependencies: + stage: test + script: + - | + curl -sf -H "X-API-Key: $FLOWFISH_API_KEY" \\ + "$FLOWFISH_URL/api/v1/communications/dependencies/summary?$FLOWFISH_QUERY" \\ + > flowfish-deps.json + - python ai-agent/analyze.py --deps flowfish-deps.json + artifacts: + paths: + - flowfish-deps.json`; + } + + if (platform === 'jenkins') { + return `// Jenkins Pipeline - Flowfish Integration +// Store API key as a Secret Text credential named 'flowfish-api-key' +def FLOWFISH_URL = '${baseUrl}' +def FLOWFISH_QUERY = '${qsStr}' + +stage('Flowfish Dependencies') { + steps { + withCredentials([string(credentialsId: 'flowfish-api-key', variable: 'FLOWFISH_API_KEY')]) { + script { + def deps = sh(returnStdout: true, script: """ + curl -sf -H "X-API-Key: \${FLOWFISH_API_KEY}" \\ + "\${FLOWFISH_URL}/api/v1/communications/dependencies/summary?\${FLOWFISH_QUERY}" + """).trim() + writeFile file: 'flowfish-deps.json', text: deps + } + } + } +}`; + } + + return `# Generic CI/CD - Flowfish Integration +# Get your API key from Flowfish Settings > API Keys +FLOWFISH_API_KEY='**********' +FLOWFISH_URL='${baseUrl}' +FLOWFISH_QUERY='${qsStr}' + +curl -sf -H "X-API-Key: $FLOWFISH_API_KEY" \\ + "$FLOWFISH_URL/api/v1/communications/dependencies/summary?$FLOWFISH_QUERY" \\ + > flowfish-deps.json`; +} + +export function buildBlastRadiusCurlSnippet( + namespace?: string, + ownerName?: string, +): string { + const baseUrl = getBaseUrl(); + const target = ownerName || 'your-service-name'; + const ns = namespace || 'default'; + + return `# Blast Radius - Pre-deployment Impact Assessment +# Returns risk score, affected services, and recommendations +FLOWFISH_API_KEY='**********' # Get from Settings > API Keys + +curl -s -X POST "${baseUrl}/api/v1/blast-radius/assess" \\ + -H "X-API-Key: $FLOWFISH_API_KEY" \\ + -H "Content-Type: application/json" \\ + -d '{ + "cluster_id": 1, + "change": { + "type": "image_update", + "target": "${target}", + "namespace": "${ns}", + "triggered_by": "ci-pipeline", + "pipeline": "main-deploy" + } + }' + +# Response includes: +# risk_score (0-100), risk_level (low/medium/high/critical), +# blast_radius.total_affected, recommendation, suggested_actions[] +# advisory_only: true (Flowfish never blocks deployments)`; +} + +export function buildBlastRadiusPipelineSnippet( + platform: string, + namespace?: string, + ownerName?: string, +): string { + const baseUrl = getBaseUrl(); + const target = ownerName || 'your-service-name'; + const ns = namespace || 'default'; + + if (platform === 'azure_devops') { + return `# Azure DevOps - Flowfish Blast Radius Check +# Set FLOWFISH_API_KEY as a secret variable +variables: + FLOWFISH_URL: '${baseUrl}' + +steps: + - script: | + RESPONSE=$(curl -s -X POST "$(FLOWFISH_URL)/api/v1/blast-radius/assess" \\ + -H "X-API-Key: $(FLOWFISH_API_KEY)" \\ + -H "Content-Type: application/json" \\ + -d '{ + "cluster_id": $(CLUSTER_ID), + "change": { + "type": "image_update", + "target": "${target}", + "namespace": "${ns}", + "triggered_by": "$(Build.RequestedFor)", + "pipeline": "$(Build.DefinitionName)" + } + }') + + RISK_SCORE=$(echo "$RESPONSE" | python3 -c "import json,sys; print(json.load(sys.stdin).get('risk_score',0))") + echo "##vso[task.setvariable variable=RISK_SCORE]$RISK_SCORE" + echo "Risk Score: $RISK_SCORE/100" + displayName: 'Flowfish: Blast Radius Check' + continueOnError: true + env: + FLOWFISH_API_KEY: $(FLOWFISH_API_KEY)`; + } + + if (platform === 'github_actions') { + return `# GitHub Actions - Flowfish Blast Radius Check +- name: Flowfish Blast Radius Check + id: blast-radius + continue-on-error: true + run: | + RESPONSE=$(curl -s -X POST \\ + "\${{ secrets.FLOWFISH_URL }}/api/v1/blast-radius/assess" \\ + -H "X-API-Key: \${{ secrets.FLOWFISH_API_KEY }}" \\ + -H "Content-Type: application/json" \\ + -d '{ + "cluster_id": \${{ vars.CLUSTER_ID }}, + "change": { + "type": "image_update", + "target": "${target}", + "namespace": "${ns}", + "triggered_by": "\${{ github.actor }}", + "pipeline": "\${{ github.workflow }}" + } + }') + + RISK_SCORE=$(echo "$RESPONSE" | jq -r '.risk_score // 0') + echo "risk_score=$RISK_SCORE" >> $GITHUB_OUTPUT + echo "Risk Score: $RISK_SCORE/100"`; + } + + if (platform === 'gitlab_ci') { + return `# GitLab CI - Flowfish Blast Radius Check +flowfish_blast_radius: + stage: test + allow_failure: true + script: + - | + RESPONSE=$(curl -s -X POST "$FLOWFISH_URL/api/v1/blast-radius/assess" \\ + -H "X-API-Key: $FLOWFISH_API_KEY" \\ + -H "Content-Type: application/json" \\ + -d '{ + "cluster_id": '$CLUSTER_ID', + "change": { + "type": "image_update", + "target": "${target}", + "namespace": "${ns}", + "triggered_by": "'$GITLAB_USER_LOGIN'", + "pipeline": "'$CI_PIPELINE_NAME'" + } + }') + echo "Risk Score: $(echo $RESPONSE | jq -r '.risk_score')/100"`; + } + + if (platform === 'jenkins') { + return `// Jenkins - Flowfish Blast Radius Check +stage('Blast Radius Check') { + steps { + script { + def response = httpRequest( + url: "\${FLOWFISH_URL}/api/v1/blast-radius/assess", + httpMode: 'POST', + contentType: 'APPLICATION_JSON', + customHeaders: [[name: 'X-API-Key', value: "\${FLOWFISH_API_KEY}"]], + requestBody: """{ + "cluster_id": \${CLUSTER_ID}, + "change": { + "type": "image_update", + "target": "${target}", + "namespace": "${ns}", + "triggered_by": "\${BUILD_USER}", + "pipeline": "\${JOB_NAME}" + } + }""", + validResponseCodes: '200:500' + ) + if (response.status == 200) { + def result = readJSON(text: response.content) + echo "Risk Score: \${result.risk_score}/100 (\${result.risk_level})" + } + } + } +}`; + } + + return `# Generic CI/CD - Flowfish Blast Radius Check +FLOWFISH_API_KEY='**********' +FLOWFISH_URL='${baseUrl}' + +curl -s -X POST "$FLOWFISH_URL/api/v1/blast-radius/assess" \\ + -H "X-API-Key: $FLOWFISH_API_KEY" \\ + -H "Content-Type: application/json" \\ + -d '{ + "cluster_id": 1, + "change": { + "type": "image_update", + "target": "${target}", + "namespace": "${ns}", + "triggered_by": "ci-pipeline", + "pipeline": "main-deploy" + } + }'`; +} + +export const PIPELINE_PLATFORMS = [ + { value: 'azure_devops', label: 'Azure DevOps' }, + { value: 'github_actions', label: 'GitHub Actions' }, + { value: 'gitlab_ci', label: 'GitLab CI' }, + { value: 'jenkins', label: 'Jenkins' }, + { value: 'other', label: 'Other (Generic)' }, +]; + +export const ID_METHODS = [ + { value: 'annotation', label: 'Annotation (e.g. git-repo URL)' }, + { value: 'label', label: 'Label (e.g. app name)' }, + { value: 'namespace_deployment', label: 'Namespace + Deployment' }, + { value: 'pod_name', label: 'Pod Name' }, + { value: 'advanced', label: 'Advanced (any combination)' }, +];