| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- import dagre from '@dagrejs/dagre'
- import { Edge, Node, Position } from '@xyflow/react'
- import { groupBy } from 'lodash'
- import { AWS_REGIONS, AWS_REGIONS_KEYS } from 'shared-data'
- import {
- AVAILABLE_REPLICA_REGIONS,
- AWS_REGIONS_COORDINATES,
- NODE_HEIGHT_FALLBACKS,
- NODE_SEP,
- NODE_WIDTH,
- ReplicaNodeData,
- } from './InstanceConfiguration.constants'
- import type { LoadBalancer } from '@/data/read-replicas/load-balancers-query'
- import type { Database } from '@/data/read-replicas/replicas-query'
- // [Joshen] Just FYI the nodes generation assumes each project only has one load balancer
- // Will need to change if this eventually becomes otherwise
- export const generateNodes = ({
- primary,
- replicas,
- loadBalancers,
- onSelectRestartReplica,
- onSelectDropReplica,
- }: {
- primary: Database
- replicas: Database[]
- loadBalancers: LoadBalancer[]
- onSelectRestartReplica: (database: Database) => void
- onSelectDropReplica: (database: Database) => void
- }): Node[] => {
- const position = { x: 0, y: 0 }
- const regions = groupBy(replicas, (d) => {
- const region = AVAILABLE_REPLICA_REGIONS.find((region) => d.region.includes(region.region))
- return region?.key
- })
- const loadBalancer = loadBalancers.find((x) =>
- x.databases.some((db) => db.identifier === primary.identifier)
- )
- const loadBalancerNode: Node | undefined =
- loadBalancer !== undefined
- ? {
- position,
- id: 'load-balancer',
- type: 'LOAD_BALANCER',
- data: {
- numDatabases: loadBalancer.databases.length,
- },
- }
- : undefined
- // [Joshen] We should be finding from AVAILABLE_REPLICA_REGIONS instead
- // but because the new regions (zurich, stockholm, ohio, paris) dont have
- // coordinates yet in AWS_REGIONS_COORDINATES - we'll need to add them in once
- // they are ready to spin up coordinates for
- const primaryRegion = Object.keys(AWS_REGIONS)
- .map((key) => {
- return {
- key: key as AWS_REGIONS_KEYS,
- name: AWS_REGIONS?.[key as AWS_REGIONS_KEYS].displayName,
- region: AWS_REGIONS?.[key as AWS_REGIONS_KEYS].code,
- coordinates: AWS_REGIONS_COORDINATES[key],
- }
- })
- .find((region) => primary.region.includes(region.region))
- // [Joshen] Once we have the coordinates for Zurich and Stockholm, we can remove the above
- // and uncomment below for better simplicity
- // const primaryRegion = AVAILABLE_REPLICA_REGIONS.find((region) =>
- // primary.region.includes(region.region)
- // )
- const primaryNode: Node = {
- position,
- id: primary.identifier,
- type: 'PRIMARY',
- data: {
- id: primary.identifier,
- region:
- primary.cloud_provider === 'FLY'
- ? { name: 'Singapore (sin)', key: 'SOUTHEAST_ASIA' }
- : (primaryRegion ?? { name: primary.region }),
- provider: primary.cloud_provider,
- inserted_at: primary.inserted_at,
- computeSize: primary.size,
- status: primary.status,
- numReplicas: replicas.length,
- numRegions: Object.keys(regions).length,
- hasLoadBalancer: loadBalancer !== undefined,
- },
- }
- const replicaNodes: Node[] = replicas
- .sort((a, b) => (a.region > b.region ? 1 : -1))
- .map((database) => {
- const region = AVAILABLE_REPLICA_REGIONS.find((region) =>
- database.region.includes(region.region)
- )
- return {
- position,
- id: database.identifier,
- type: 'READ_REPLICA',
- data: {
- id: database.identifier,
- region,
- provider: database.cloud_provider,
- inserted_at: database.inserted_at,
- computeSize: database.size,
- status: database.status,
- onSelectRestartReplica: () => onSelectRestartReplica(database),
- onSelectDropReplica: () => onSelectDropReplica(database),
- },
- }
- })
- return [
- ...(loadBalancerNode !== undefined ? [loadBalancerNode] : []),
- primaryNode,
- ...replicaNodes,
- ]
- }
- const getDagreNodeHeight = (node: Node) => {
- if (node.measured?.height) return node.measured.height
- return NODE_HEIGHT_FALLBACKS[node.type ?? ''] ?? 100
- }
- export const getDagreGraphLayout = (nodes: Node[], edges: Edge[]) => {
- const dagreGraph = new dagre.graphlib.Graph()
- dagreGraph.setDefaultEdgeLabel(() => ({}))
- dagreGraph.setGraph({ rankdir: 'TB', ranksep: 60, nodesep: NODE_SEP })
- nodes.forEach((node) => {
- dagreGraph.setNode(node.id, {
- width: NODE_WIDTH / 2,
- height: getDagreNodeHeight(node),
- })
- })
- edges.forEach((edge) => dagreGraph.setEdge(edge.source, edge.target))
- dagre.layout(dagreGraph)
- nodes.forEach((node) => {
- const nodeWithPosition = dagreGraph.node(node.id)
- node.targetPosition = Position.Top
- node.sourcePosition = Position.Bottom
- // We are shifting the dagre node position (anchor=center center) to the top left
- // so it matches the React Flow node anchor point (top left).
- node.position = {
- x: nodeWithPosition.x - nodeWithPosition.width / 2,
- y: nodeWithPosition.y - nodeWithPosition.height / 2,
- }
- return node
- })
- return { nodes, edges }
- }
- /**
- * [Joshen] This is some customized logic to add region nodes as "subflow" as dagre doesn't support
- * subflows, and I didn't want to go down a rabbit hole with the other layout libraries that react-flow
- * supports. Definitely some things to improve in the future
- * - Allow setting max number of nodes per row, so that the chart does not become too horizontally sparse
- * when many many replicas created
- * - Nodes are a bit too spaced out between each other within a region
- */
- export const addRegionNodes = (nodes: Node[], edges: Edge[]) => {
- const regionNodes: Node[] = []
- const replicaNodes = nodes.filter(
- (node) => node.type === 'READ_REPLICA'
- ) as Node<ReplicaNodeData>[]
- const nodesByRegion = groupBy(replicaNodes, (node) => node.data.region.key)
- Object.entries(nodesByRegion).map(([key, value]) => {
- const region = AVAILABLE_REPLICA_REGIONS.find((r) => r.key === key)
- const nodeXPositions = value.map((x) => x.position.x)
- const nodeYPositions = value.map((x) => x.position.y)
- const minX = Math.min(...nodeXPositions)
- const maxX = Math.max(...nodeXPositions)
- const minY = Math.max(...nodeYPositions)
- const regionNode: Node = {
- id: key,
- position: { x: minX - 10, y: minY - 10 },
- width: maxX - minX + NODE_WIDTH / 2,
- type: 'REGION',
- data: { region, numReplicas: value.length },
- }
- regionNodes.push(regionNode)
- })
- return { nodes: [...regionNodes, ...nodes], edges }
- }
- export const formatSeconds = (value: number) => {
- const hours = ~~(value / 3600)
- const minutes = Math.floor((value % 3600) / 60)
- const seconds = Math.floor(value % 60)
- return `${hours > 0 ? `${hours}h` : ''} ${minutes > 0 ? `${minutes}m` : ''} ${seconds > 0 ? `${seconds}s` : ''}`.trim()
- }
|