-
Notifications
You must be signed in to change notification settings - Fork 84
feat: add export button for service map connections #1033
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import React, { useCallback } from 'react'; | ||
| import { observer } from 'mobx-react'; | ||
|
|
||
| import { Icon, Button } from '@blueprintjs/core'; | ||
|
|
||
| import css from './styles.scss'; | ||
|
|
||
| export interface Props { | ||
| onExport?: () => void; | ||
| } | ||
|
|
||
| export const ExportButton = observer(function ExportButton(props: Props) { | ||
| const handleClick = useCallback(() => { | ||
| props.onExport?.(); | ||
| }, [props.onExport]); | ||
|
|
||
| return ( | ||
| <Button | ||
| minimal | ||
| small | ||
| icon={<Icon icon="export" iconSize={16} />} | ||
| onClick={handleClick} | ||
| title="Export Service Map Connections" | ||
| > | ||
| Export | ||
| </Button> | ||
| ); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { ServiceMapExporter } from './serviceMapExporter'; | ||
| export type { ExportedConnection, ExportedServiceMap } from './types'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| import { ServiceCard } from '~/domain/service-map/card'; | ||
| import { Link } from '~/domain/link'; | ||
| import { ExportedConnection, ExportedServiceMap } from './types'; | ||
|
|
||
| export class ServiceMapExporter { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please, use pure functions and unit tests them. (No class, and especially no static method) |
||
| /** | ||
| * Exports service map connections to JSON format | ||
| */ | ||
| public static exportToJSON( | ||
| services: Map<string, ServiceCard>, | ||
| links: Link[], | ||
| ): ExportedServiceMap { | ||
| const connections: ExportedConnection[] = []; | ||
| let skippedLinks = 0; | ||
|
|
||
| console.log('[ServiceMapExporter] Starting export...', { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is all the console logs really necessary for this feature? Hubble UI console is already filled with some many messages, which usually are useless for most users, I think we should use them with care. Hopefully in the future we can find a more sustainable way. I would recommend to only console log the summary, especially remove all the console logs that you have used for developing (e.g. sample flow) |
||
| totalServices: services.size, | ||
| totalLinks: links.length, | ||
| }); | ||
|
|
||
| for (const link of links) { | ||
| const sourceService = services.get(link.sourceId); | ||
| const destService = services.get(link.destinationId); | ||
|
|
||
| // Skip if we can't find either service | ||
| if (!sourceService || !destService) { | ||
| skippedLinks++; | ||
| console.warn('[ServiceMapExporter] Skipping link - service not found:', { | ||
| sourceId: link.sourceId, | ||
| destId: link.destinationId, | ||
| sourceFound: !!sourceService, | ||
| destFound: !!destService, | ||
| }); | ||
| continue; | ||
| } | ||
|
|
||
| const latency = link.throughput.latency; | ||
| connections.push({ | ||
| source: { | ||
| id: link.sourceId, | ||
| name: sourceService.caption, | ||
| namespace: sourceService.namespace, | ||
| }, | ||
| destination: { | ||
| id: link.destinationId, | ||
| name: destService.caption, | ||
| namespace: destService.namespace, | ||
| }, | ||
| destinationPort: link.destinationPort, | ||
| ipProtocol: link.ipProtocol.toString(), | ||
| verdicts: Array.from(link.verdicts).map(v => v.toString()), | ||
| authTypes: Array.from(link.authTypes).map(a => a.toString()), | ||
| isEncrypted: link.isEncrypted, | ||
| throughput: { | ||
| flowAmount: link.throughput.flowAmount, | ||
| latency: latency | ||
| ? { | ||
| min: Number(latency.min.seconds), | ||
| max: Number(latency.max.seconds), | ||
| avg: Number(latency.avg.seconds), | ||
| } | ||
| : null, | ||
| bytesTransfered: link.throughput.bytesTransfered, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| // Calculate unique services involved in connections | ||
| const uniqueServiceIds = new Set<string>(); | ||
| connections.forEach(conn => { | ||
| uniqueServiceIds.add(conn.source.id); | ||
| uniqueServiceIds.add(conn.destination.id); | ||
| }); | ||
|
|
||
| const result = { | ||
| exportedAt: new Date().toISOString(), | ||
| connections, | ||
| summary: { | ||
| totalConnections: connections.length, | ||
| totalServices: uniqueServiceIds.size, | ||
| }, | ||
| }; | ||
|
|
||
| console.log('[ServiceMapExporter] Export complete:', { | ||
| exportedConnections: result.summary.totalConnections, | ||
| uniqueServices: result.summary.totalServices, | ||
| skippedLinks, | ||
| timestamp: result.exportedAt, | ||
| }); | ||
|
|
||
| // Log sample connections for verification | ||
| if (connections.length > 0) { | ||
| console.log('[ServiceMapExporter] Sample connections (first 3):', | ||
| connections.slice(0, 3).map(c => ({ | ||
| from: `${c.source.name} (${c.source.namespace})`, | ||
| to: `${c.destination.name} (${c.destination.namespace})`, | ||
| port: c.destinationPort, | ||
| protocol: c.ipProtocol, | ||
| encrypted: c.isEncrypted, | ||
| })) | ||
| ); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * Downloads the exported data as a JSON file | ||
| */ | ||
| public static downloadJSON(data: ExportedServiceMap, filename?: string): void { | ||
| const jsonString = JSON.stringify(data, null, 2); | ||
| const blob = new Blob([jsonString], { type: 'application/json' }); | ||
| const url = URL.createObjectURL(blob); | ||
|
|
||
| const link = document.createElement('a'); | ||
| link.href = url; | ||
| link.download = filename || `hubble-service-map-${Date.now()}.json`; | ||
| document.body.appendChild(link); | ||
| link.click(); | ||
| document.body.removeChild(link); | ||
|
|
||
| // Clean up the URL object | ||
| URL.revokeObjectURL(url); | ||
| } | ||
|
|
||
| /** | ||
| * Exports and downloads service map connections in one operation | ||
| */ | ||
| public static export(services: Map<string, ServiceCard>, links: Link[]): void { | ||
| const data = this.exportToJSON(services, links); | ||
| this.downloadJSON(data); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| export interface ExportedConnection { | ||
| source: { | ||
| id: string; | ||
| name: string; | ||
| namespace: string | null; | ||
| }; | ||
| destination: { | ||
| id: string; | ||
| name: string; | ||
| namespace: string | null; | ||
| }; | ||
| destinationPort: number; | ||
| ipProtocol: string; | ||
| verdicts: string[]; | ||
| authTypes: string[]; | ||
| isEncrypted: boolean; | ||
| throughput: { | ||
| flowAmount: number; | ||
| latency: { | ||
| min: number; | ||
| max: number; | ||
| avg: number; | ||
| } | null; | ||
| bytesTransfered: number; | ||
| }; | ||
| } | ||
|
|
||
| export interface ExportedServiceMap { | ||
| exportedAt: string; | ||
| connections: ExportedConnection[]; | ||
| summary: { | ||
| totalConnections: number; | ||
| totalServices: number; | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are not using any Mobx elements in this component, so no need to wrap it in
observer.