diff --git a/src/components/ArchDiagramSection/index.js b/src/components/ArchDiagramSection/index.js index 7976a4c..008b793 100644 --- a/src/components/ArchDiagramSection/index.js +++ b/src/components/ArchDiagramSection/index.js @@ -1,95 +1,479 @@ -import React from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import Link from '@docusaurus/Link'; import styles from './styles.module.css'; -function ArrowDown() { - return ( - - ); +const clusterCount = 32; +const initialReplicaCount = 2; +const maxReplicaCount = 4; + +const initialFleetStage = { + visibleCount: 1, + onlineCount: '0001', + message: 'CLUSTER_001 ONLINE', + service: null, +}; + +const settledActivities = [ + { + message: 'SCALE REPLICA_013', + service: { type: 'scaling', index: 12 }, + }, + { + message: 'HEAL PRIMARY_009', + service: { type: 'healing', index: 8 }, + }, + { + message: 'MAINTAIN CLUSTER_016', + service: { type: 'maintaining', index: 15 }, + }, +]; + +function formatOnlineCount(count) { + return String(count).padStart(4, '0'); } -function ClusterConnectors() { - return ( - - ); +function randomBetween(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; } -function StorageConnectors() { - return ( - +function createReplicaTargets() { + const targetRange = maxReplicaCount - initialReplicaCount + 1; + const targets = Array.from( + { length: clusterCount }, + (_, index) => initialReplicaCount + (index % targetRange), ); + + for (let index = targets.length - 1; index > 0; index -= 1) { + const swapIndex = randomBetween(0, index); + [targets[index], targets[swapIndex]] = [targets[swapIndex], targets[index]]; + } + + return targets; +} + +function formatServiceMessage(service) { + const number = String(service.index + 1).padStart(3, '0'); + + switch (service.type) { + case 'scaling': + return `SCALE REPLICA_${number}`; + case 'healing': + return `HEAL PRIMARY_${number}`; + case 'maintaining': + return `MAINTAIN CLUSTER_${number}`; + default: + return `CLUSTER_${number} ONLINE`; + } +} + +function useFleetSequence() { + const fleetRef = useRef(null); + const [hasStarted, setHasStarted] = useState(false); + const [stage, setStage] = useState(initialFleetStage); + const [isSettled, setIsSettled] = useState(false); + const [settledActivity, setSettledActivity] = useState(settledActivities[2]); + const [reduceMotion, setReduceMotion] = useState(false); + const replicaCountsRef = useRef(Array(clusterCount).fill(initialReplicaCount)); + const replicaTargetsRef = useRef(null); + const lastServiceIndexRef = useRef(-1); + const [replicaCounts, setReplicaCounts] = useState(replicaCountsRef.current); + + if (replicaTargetsRef.current === null) { + replicaTargetsRef.current = createReplicaTargets(); + } + + const activateService = useCallback((requestedType, onlineClusterCount) => { + const availableCount = Math.max(1, Math.min(clusterCount, onlineClusterCount)); + const allCandidates = Array.from({ length: availableCount }, (_, index) => index); + let type = requestedType; + let candidates = requestedType === 'scaling' + ? allCandidates.filter((index) => ( + replicaCountsRef.current[index] < replicaTargetsRef.current[index] + )) + : allCandidates; + + if (candidates.length === 0) { + type = 'maintaining'; + candidates = allCandidates; + } + + if (candidates.length > 1) { + candidates = candidates.filter((index) => index !== lastServiceIndexRef.current); + } + + const index = candidates[randomBetween(0, candidates.length - 1)]; + lastServiceIndexRef.current = index; + + if (type === 'scaling') { + const nextCounts = [...replicaCountsRef.current]; + nextCounts[index] += 1; + replicaCountsRef.current = nextCounts; + setReplicaCounts(nextCounts); + } + + return { type, index }; + }, []); + + useEffect(() => { + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + if (prefersReducedMotion) { + replicaCountsRef.current = [...replicaTargetsRef.current]; + setReplicaCounts(replicaCountsRef.current); + setReduceMotion(true); + setStage({ + visibleCount: clusterCount, + onlineCount: '1000+', + ...settledActivities[2], + }); + setHasStarted(true); + setIsSettled(true); + return undefined; + } + + const node = fleetRef.current; + if (!node) return undefined; + + const observer = new IntersectionObserver(([entry]) => { + if (entry.isIntersecting) { + setHasStarted(true); + observer.disconnect(); + } + }, { threshold: 0.3 }); + + observer.observe(node); + return () => observer.disconnect(); + }, []); + + useEffect(() => { + if (!hasStarted || reduceMotion || isSettled) return undefined; + + const timers = new Set(); + let visibleCount = 1; + let onlineCountValue = clusterCount; + let growthActivityIndex = 0; + let counterActivityIndex = 0; + + const schedule = (callback, minDelay, maxDelay) => { + const timer = window.setTimeout(() => { + timers.delete(timer); + callback(); + }, randomBetween(minDelay, maxDelay)); + timers.add(timer); + }; + + const growCounter = () => { + if (onlineCountValue >= 1000) { + schedule(() => setIsSettled(true), 1400, 1900); + return; + } + + const batchSize = Math.min(randomBetween(1, 4), 1000 - onlineCountValue); + const firstCluster = onlineCountValue + 1; + onlineCountValue += batchSize; + counterActivityIndex += 1; + const activity = settledActivities[onlineCountValue % settledActivities.length]; + const showProvisioning = counterActivityIndex % 9 === 0; + const service = onlineCountValue === 1000 || showProvisioning + ? null + : activateService(activity.service.type, clusterCount); + const range = firstCluster === onlineCountValue + ? `CLUSTER_${String(firstCluster).padStart(4, '0')}` + : `CLUSTERS_${String(firstCluster).padStart(4, '0')}-${String(onlineCountValue).padStart(4, '0')}`; + + setStage({ + visibleCount: clusterCount, + onlineCount: onlineCountValue === 1000 ? '1000+' : formatOnlineCount(onlineCountValue), + message: onlineCountValue === 1000 + ? 'FLEET EXPANDED TO 1000+' + : showProvisioning ? `PROVISION ${range}` : formatServiceMessage(service), + service, + }); + + if (onlineCountValue < 1000) { + schedule(growCounter, 650, 1300); + } else { + schedule(() => setIsSettled(true), 1600, 2200); + } + }; + + const growVisibleFleet = () => { + const previouslyVisibleCount = visibleCount; + const batchSize = Math.min(randomBetween(1, 4), clusterCount - visibleCount); + const firstCluster = visibleCount + 1; + visibleCount += batchSize; + const lastCluster = visibleCount; + const range = firstCluster === lastCluster + ? `CLUSTER_${String(firstCluster).padStart(3, '0')}` + : `CLUSTERS_${String(firstCluster).padStart(3, '0')}-${String(lastCluster).padStart(3, '0')}`; + const cyclePosition = growthActivityIndex % (settledActivities.length + 1); + const showProvisioning = cyclePosition === settledActivities.length; + const serviceTemplate = settledActivities[cyclePosition % settledActivities.length].service; + const service = showProvisioning + ? null + : activateService(serviceTemplate.type, previouslyVisibleCount); + growthActivityIndex += 1; + + setStage({ + visibleCount, + onlineCount: formatOnlineCount(visibleCount), + message: showProvisioning ? `PROVISION ${range}` : formatServiceMessage(service), + service, + }); + + if (visibleCount < clusterCount) { + schedule(growVisibleFleet, 950, 1650); + } else { + schedule(growCounter, 1500, 2200); + } + }; + + schedule(growVisibleFleet, 900, 1400); + + return () => { + timers.forEach(window.clearTimeout); + }; + }, [activateService, hasStarted, isSettled, reduceMotion]); + + useEffect(() => { + if (!isSettled || reduceMotion) return undefined; + + let activityIndex = 0; + const rotateActivity = () => { + const template = settledActivities[activityIndex % settledActivities.length]; + const service = activateService(template.service.type, clusterCount); + activityIndex += 1; + + setSettledActivity({ + message: formatServiceMessage(service), + service, + }); + }; + + rotateActivity(); + const interval = window.setInterval(rotateActivity, 3200); + + return () => window.clearInterval(interval); + }, [activateService, isSettled, reduceMotion]); + + const currentStage = isSettled + ? { + visibleCount: clusterCount, + onlineCount: '1000+', + ...settledActivity, + } + : stage; + + return { + fleetRef, + hasStarted, + replicaCounts, + stage: currentStage, + }; } -function DatabaseIcon() { +function ControlBus({ active }) { return ( - + ); } -function ClusterNodeIcon() { +function FleetCluster({ + activity, + index, + online, + onPreviewClose, + onPreviewToggle, + previewOpen, + replicaCount, +}) { + const number = String(index + 1).padStart(3, '0'); + const isScaling = activity?.type === 'scaling' && activity.index === index; + const activityClass = activity?.index === index + ? styles[`cluster${activity.type[0].toUpperCase()}${activity.type.slice(1)}`] + : ''; + const handleKeyDown = (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + onPreviewToggle(); + } else if (event.key === 'Escape' && previewOpen) { + event.preventDefault(); + onPreviewClose(); + } + }; + return ( - +
+ {number} + + + {Array.from({ length: replicaCount }, (_, replicaIndex) => { + const isAddedReplica = isScaling && replicaIndex === replicaCount - 1; + + return ( + + ); + })} + +
); } -function ClusterTopology() { +function ClusterFleet({ fleetRef, replicaCounts, stage }) { + const [openPreviewIndex, setOpenPreviewIndex] = useState(null); + + useEffect(() => { + if (openPreviewIndex === null) return undefined; + + const closeOutside = (event) => { + if (!(event.target instanceof Element) || !event.target.closest('[data-cluster-tile]')) { + setOpenPreviewIndex(null); + } + }; + const closeOnEscape = (event) => { + if (event.key === 'Escape') setOpenPreviewIndex(null); + }; + + document.addEventListener('pointerdown', closeOutside); + document.addEventListener('keydown', closeOnEscape); + + return () => { + document.removeEventListener('pointerdown', closeOutside); + document.removeEventListener('keydown', closeOnEscape); + }; + }, [openPreviewIndex]); + return ( -