From da4ae27ad58dfae6d0eb2f0fd56f792c84817a89 Mon Sep 17 00:00:00 2001 From: zivglik Date: Thu, 5 Feb 2026 11:20:00 +0200 Subject: [PATCH 1/3] feat: add ServicesStatus component with service monitoring features --- src/Routes/Base/Header/HelpBar.react.jsx | 3 + src/Routes/Base/Header/InactiveMode.jsx | 8 +- src/components/ServicesStatus/ServiceRow.jsx | 55 +++++++++ .../ServicesStatus/ServicesStatus.jsx | 104 ++++++++++++++++++ src/components/ServicesStatus/StatusLamp.jsx | 24 ++++ .../ServicesStatus/SubServicesPopover.jsx | 57 ++++++++++ src/components/ServicesStatus/index.js | 1 + src/components/ServicesStatus/mockData.js | 82 ++++++++++++++ src/components/index.js | 1 + 9 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 src/components/ServicesStatus/ServiceRow.jsx create mode 100644 src/components/ServicesStatus/ServicesStatus.jsx create mode 100644 src/components/ServicesStatus/StatusLamp.jsx create mode 100644 src/components/ServicesStatus/SubServicesPopover.jsx create mode 100644 src/components/ServicesStatus/index.js create mode 100644 src/components/ServicesStatus/mockData.js diff --git a/src/Routes/Base/Header/HelpBar.react.jsx b/src/Routes/Base/Header/HelpBar.react.jsx index 170f3b82f..3cb03a5ee 100644 --- a/src/Routes/Base/Header/HelpBar.react.jsx +++ b/src/Routes/Base/Header/HelpBar.react.jsx @@ -12,6 +12,7 @@ import SettingsUser from './Settings/SettingsUser'; import Settings from './Settings/Settings.react'; import InactiveModeTag from './InactiveMode'; import ExperimentPicker from './ExperimentPicker.react'; +import ServicesStatus from '../../../components/ServicesStatus/ServicesStatus'; const Container = styled(FlexBox.Auto)` position: relative; @@ -21,6 +22,8 @@ const HelpBar = () => { const { keycloakEnable } = useSelector(selectors.connection); return ( + + diff --git a/src/Routes/Base/Header/InactiveMode.jsx b/src/Routes/Base/Header/InactiveMode.jsx index c876a807f..4e5d6565a 100644 --- a/src/Routes/Base/Header/InactiveMode.jsx +++ b/src/Routes/Base/Header/InactiveMode.jsx @@ -6,10 +6,10 @@ import React from 'react'; import styled from 'styled-components'; const Tag = styled(AntTag)` - position: absolute; - right: 1em; - top: 50%; - transform: translateY(-50%); + + + + `; const Content = styled.span` diff --git a/src/components/ServicesStatus/ServiceRow.jsx b/src/components/ServicesStatus/ServiceRow.jsx new file mode 100644 index 000000000..acbbc0c64 --- /dev/null +++ b/src/components/ServicesStatus/ServiceRow.jsx @@ -0,0 +1,55 @@ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { Popover, Typography } from 'antd'; +import styled from 'styled-components'; + +import StatusLamp from './StatusLamp'; +import SubServicesPopover from './SubServicesPopover'; + +const Row = styled.div` + display: inline-flex; + align-items: center; + padding: 6px 8px; + border-radius: 6px; + cursor: default; + user-select: none; + + &:hover { + background: rgba(0, 0, 0, 0.04); + } +`; + +const ServiceRow = ({ service }) => { + const isOk = useMemo(() => { + const subServices = service.services || []; + return Boolean(service.status) && subServices.every(item => item.status); + }, [service]); + + return ( + } + placement="bottom" + trigger="hover"> + + + {service.serviceName} + + + ); +}; + +ServiceRow.propTypes = { + service: PropTypes.shape({ + serviceName: PropTypes.string.isRequired, + status: PropTypes.bool.isRequired, + services: PropTypes.arrayOf( + PropTypes.shape({ + subServiceName: PropTypes.string.isRequired, + status: PropTypes.bool.isRequired, + number: PropTypes.number.isRequired, + }) + ), + }).isRequired, +}; + +export default ServiceRow; diff --git a/src/components/ServicesStatus/ServicesStatus.jsx b/src/components/ServicesStatus/ServicesStatus.jsx new file mode 100644 index 000000000..32eb1af1d --- /dev/null +++ b/src/components/ServicesStatus/ServicesStatus.jsx @@ -0,0 +1,104 @@ +import React, { useMemo, useState } from 'react'; +import PropTypes from 'prop-types'; +import { Typography } from 'antd'; +import styled from 'styled-components'; + +import StatusLamp from './StatusLamp'; +import ServiceRow from './ServiceRow'; +import servicesStatusMock from './mockData'; + +const Container = styled.div` + position: relative; + display: inline-flex; + flex-direction: row-reverse; + gap: 8px; + border-radius: 6px; + border: 1px solid rgba(0, 0, 0, 0.12); + +`; + +const HeaderRow = styled.div` + display: inline-flex; + align-items: center; + padding: 6px 8px; + border-radius: 6px; + cursor: pointer; + user-select: none; + + &:hover { + background: rgba(0, 0, 0, 0.04); + } +`; + +const ServicesList = styled.div` + width: max-content; + position: ${props => (props.$isOpen ? 'static' : 'absolute')}; + right: calc(100% + 12px); + top: 0; + display: flex; + flex-direction: row; + + + + align-items: flex-start; + gap: 8px; + opacity: ${props => (props.$isOpen ? 1 : 0)}; + transform: ${props => + props.$isOpen ? 'translateX(0)' : 'translateX(8px)'}; + pointer-events: ${props => (props.$isOpen ? 'auto' : 'none')}; + transition: opacity 180ms ease, transform 180ms ease; + transform-origin: right top; +`; + +const ServicesStatus = ({ services = servicesStatusMock, label = 'Service metrics' }) => { + const [isOpen, setIsOpen] = useState(false); + + const isGlobalOk = useMemo(() => { + return services.every(service => { + const subServices = service.services || []; + return service.status && subServices.every(item => item.status); + }); + }, [services]); + + const handleToggle = () => setIsOpen(prev => !prev); + + return ( + + { + if (event.key === 'Enter' || event.key === ' ') handleToggle(); + }}> + + {label} + + + + {services.map(service => ( + + ))} + + + ); +}; + +ServicesStatus.propTypes = { + services: PropTypes.arrayOf( + PropTypes.shape({ + serviceName: PropTypes.string.isRequired, + status: PropTypes.bool.isRequired, + services: PropTypes.arrayOf( + PropTypes.shape({ + subServiceName: PropTypes.string.isRequired, + status: PropTypes.bool.isRequired, + number: PropTypes.number.isRequired, + }) + ), + }) + ), + label: PropTypes.string, +}; + +export default ServicesStatus; diff --git a/src/components/ServicesStatus/StatusLamp.jsx b/src/components/ServicesStatus/StatusLamp.jsx new file mode 100644 index 000000000..29ec3eb30 --- /dev/null +++ b/src/components/ServicesStatus/StatusLamp.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; + +const Lamp = styled.span` + width: ${props => props.$size}px; + height: ${props => props.$size}px; + border-radius: 50%; + display: inline-block; + background-color: ${props => (props.$isOk ? '#52c41a' : '#ff4d4f')}; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05) inset; + margin-right: 8px; +`; + +const StatusLamp = ({ isOk, size = 10 }) => ( + +); + +StatusLamp.propTypes = { + isOk: PropTypes.bool.isRequired, + size: PropTypes.number, +}; + +export default StatusLamp; diff --git a/src/components/ServicesStatus/SubServicesPopover.jsx b/src/components/ServicesStatus/SubServicesPopover.jsx new file mode 100644 index 000000000..7c2b2212f --- /dev/null +++ b/src/components/ServicesStatus/SubServicesPopover.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Typography } from 'antd'; +import styled from 'styled-components'; + +import StatusLamp from './StatusLamp'; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + min-width: 220px; + gap: 6px; +`; + +const SubRow = styled.div` + display: flex; + align-items: center; +`; + +const EmptyText = styled(Typography.Text)` + color: rgba(0, 0, 0, 0.45); +`; + +const SubServicesPopover = ({ subServices = [] }) => { + if (!subServices.length) { + return ( + + No sub services + + ); + } + + return ( + + {subServices.map(item => ( + + + + {String(item.status)}, {item.subServiceName} : {item.number} + + + ))} + + ); +}; + +SubServicesPopover.propTypes = { + subServices: PropTypes.arrayOf( + PropTypes.shape({ + subServiceName: PropTypes.string.isRequired, + status: PropTypes.bool.isRequired, + number: PropTypes.number.isRequired, + }) + ), +}; + +export default SubServicesPopover; diff --git a/src/components/ServicesStatus/index.js b/src/components/ServicesStatus/index.js new file mode 100644 index 000000000..f873af5e2 --- /dev/null +++ b/src/components/ServicesStatus/index.js @@ -0,0 +1 @@ +export { default } from './ServicesStatus'; diff --git a/src/components/ServicesStatus/mockData.js b/src/components/ServicesStatus/mockData.js new file mode 100644 index 000000000..528f23059 --- /dev/null +++ b/src/components/ServicesStatus/mockData.js @@ -0,0 +1,82 @@ +const servicesStatusMock = [ + { + serviceName: 'auth-service', + status: 'OK', // OK | DEGRADED | DOWN + uptime: 99.98, + latencyMsP95: 120, + errorRate: 0.2, + requestsPerSecond: 85, + services: [ + { + subServiceName: 'login-api', + status: 'OK', + latencyMsP95: 95, + errorRate: 0.1, + rps: 40, + lastCheck: '2026-02-05T08:42:10Z', + }, + { + subServiceName: 'token-refresh', + status: 'DEGRADED', + latencyMsP95: 280, + errorRate: 2.8, + rps: 12, + lastCheck: '2026-02-05T08:42:10Z', + }, + ], + }, + { + serviceName: 'payments-service', + status: 'DEGRADED', + uptime: 99.2, + latencyMsP95: 420, + errorRate: 3.5, + requestsPerSecond: 22, + services: [ + { + subServiceName: 'credit-card-processor', + status: 'OK', + latencyMsP95: 180, + errorRate: 0.6, + rps: 18, + lastCheck: '2026-02-05T08:42:10Z', + }, + { + subServiceName: 'invoice-generator', + status: 'DOWN', + latencyMsP95: null, + errorRate: 100, + rps: 0, + lastCheck: '2026-02-05T08:41:02Z', + }, + ], + }, + { + serviceName: 'notifications-service', + status: 'DOWN', + uptime: 97.6, + latencyMsP95: null, + errorRate: 100, + requestsPerSecond: 0, + services: [ + { + subServiceName: 'email-sender', + status: 'OK', + latencyMsP95: 210, + errorRate: 0.4, + rps: 55, + lastCheck: '2026-02-05T08:42:10Z', + }, + { + subServiceName: 'sms-gateway', + status: 'DOWN', + latencyMsP95: null, + errorRate: 100, + rps: 0, + lastCheck: '2026-02-05T08:40:31Z', + }, + ], + }, +]; + +export default servicesStatusMock; diff --git a/src/components/index.js b/src/components/index.js index 7d501719b..5f382ff67 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -5,3 +5,4 @@ export { default as GrafanaLink } from './GrafanaLink'; export { default as ParticleAnimation } from './ParticleAnimation'; export { default as VersionsTable } from './TableVersions/VersionsTable.react'; export { default as AuditTrailTable } from './TableVersions/AuditTrailTable'; +export { default as ServicesStatus } from './ServicesStatus'; From 36bb0c6e71f0c4d22bc9415c87dd83cb066346bf Mon Sep 17 00:00:00 2001 From: zivglik Date: Mon, 9 Feb 2026 10:33:12 +0200 Subject: [PATCH 2/3] fix: add missing 'number' fields for sub-services in mock data --- src/components/ServicesStatus/mockData.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/ServicesStatus/mockData.js b/src/components/ServicesStatus/mockData.js index 528f23059..79913ea4f 100644 --- a/src/components/ServicesStatus/mockData.js +++ b/src/components/ServicesStatus/mockData.js @@ -14,6 +14,7 @@ const servicesStatusMock = [ errorRate: 0.1, rps: 40, lastCheck: '2026-02-05T08:42:10Z', + number: 1 }, { subServiceName: 'token-refresh', @@ -22,7 +23,8 @@ const servicesStatusMock = [ errorRate: 2.8, rps: 12, lastCheck: '2026-02-05T08:42:10Z', - }, + number: 2 + } ], }, { @@ -40,6 +42,7 @@ const servicesStatusMock = [ errorRate: 0.6, rps: 18, lastCheck: '2026-02-05T08:42:10Z', + number: 1 }, { subServiceName: 'invoice-generator', @@ -48,6 +51,7 @@ const servicesStatusMock = [ errorRate: 100, rps: 0, lastCheck: '2026-02-05T08:41:02Z', + number: 1 }, ], }, @@ -66,6 +70,7 @@ const servicesStatusMock = [ errorRate: 0.4, rps: 55, lastCheck: '2026-02-05T08:42:10Z', + number: 1, }, { subServiceName: 'sms-gateway', @@ -74,6 +79,7 @@ const servicesStatusMock = [ errorRate: 100, rps: 0, lastCheck: '2026-02-05T08:40:31Z', + number: 2, }, ], }, From 65f6d0ab4a7f9fc8f39e1679e83f9c8b767e04a4 Mon Sep 17 00:00:00 2001 From: zivglik Date: Mon, 9 Feb 2026 14:29:47 +0200 Subject: [PATCH 3/3] fix: update service status values to boolean in mock data --- src/components/ServicesStatus/mockData.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ServicesStatus/mockData.js b/src/components/ServicesStatus/mockData.js index 79913ea4f..1d954ac0b 100644 --- a/src/components/ServicesStatus/mockData.js +++ b/src/components/ServicesStatus/mockData.js @@ -1,7 +1,7 @@ const servicesStatusMock = [ { serviceName: 'auth-service', - status: 'OK', // OK | DEGRADED | DOWN + status: false, // OK | DEGRADED | DOWN uptime: 99.98, latencyMsP95: 120, errorRate: 0.2, @@ -9,7 +9,7 @@ const servicesStatusMock = [ services: [ { subServiceName: 'login-api', - status: 'OK', + status: false, latencyMsP95: 95, errorRate: 0.1, rps: 40,