Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ui/rspack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const config: Configuration = {
'./platformLibrary': './src/services/platformlibrary/k8s.ts',
'./AlertsNavbarUpdater': './src/components/AlertNavbarUpdaterComponent.tsx',
'./Metalk8sLocalVolumeProvider': './src/services/k8s/Metalk8sLocalVolumeProvider.ts',
'./PlatformGlobalHealthBarFederated': './src/components/PlatformGlobalHealthBarFederated.tsx',
},
remotes: !isProduction
? {
Expand Down
92 changes: 7 additions & 85 deletions ui/src/components/DashboardGlobalHealth.tsx
Original file line number Diff line number Diff line change
@@ -1,118 +1,40 @@
import styled from 'styled-components';
import DashboardAlerts from './DashboardAlerts';
import { Box, useMetricsTimeSpan } from '@scality/core-ui/dist/next';
import {
EmphaseText,
LargerText,
SmallerText,
StatusWrapper,
Loader,
AppContainer,
spacing,
Stack,
IconHelp,
} from '@scality/core-ui';
import { Alert, GlobalHealthBar as GlobalHealthBarRecharts } from '@scality/core-ui/dist/next';
import { AppContainer, LargerText, Stack, StatusWrapper } from '@scality/core-ui';
import { Box } from '@scality/core-ui/dist/next';
import { highestAlertToStatus, useAlertLibrary, useHighestSeverityAlerts } from '../containers/AlertProvider';
import { useIntl } from 'react-intl';
import { useStartingTimeStamp } from '../containers/StartTimeProvider';
import CircleStatus from './CircleStatus';
import DashboardAlerts from './DashboardAlerts';
import PlatformGlobalHealthBar from './PlatformGlobalHealthBar';
import StatusIcon from './StatusIcon';
import { getClusterAlertSegmentQuery } from '../services/platformlibrary/metrics';

import { useQuery } from 'react-query';

const HealthBarContainer = styled.div`
flex-direction: column;
width: 90%;
margin: 0 auto;
`;
const PlatformStatusIcon = styled.div`
margin: 0 1rem;
font-size: 2rem;
`;

const StyledEmphaseText = styled(EmphaseText)`
letter-spacing: ${spacing.r2};
`;

const DashboardGlobalHealth = () => {
const intl = useIntl();
const { startingTimeISO, currentTimeISO } = useStartingTimeStamp();
const alertsLibrary = useAlertLibrary();
const { duration } = useMetricsTimeSpan();
const { data: alerts, status: historyAlertStatus } = useQuery(getClusterAlertSegmentQuery(duration));
const platformHighestSeverityAlert = useHighestSeverityAlerts(alertsLibrary.getPlatformAlertSelectors());
const platformStatus = highestAlertToStatus(platformHighestSeverityAlert);

return (
<AppContainer.OverallSummary>
<Stack style={{ alignItems: 'center' }}>
<Box flex="1" display="flex">
<PlatformStatusIcon>
<StatusWrapper status={platformStatus}>
<StatusIcon status={platformStatus} name="Datacenter" entity='Platform' />
<StatusIcon status={platformStatus} name="Datacenter" entity="Platform" />
</StatusWrapper>
</PlatformStatusIcon>

<LargerText>
{intl.formatMessage({
id: 'platform',
})}
</LargerText>
</Box>
<Box flex="2">
<HealthBarContainer>
<Stack
style={{
display: 'flex',
alignItems: 'center',
}}
gap="r20"
>
<StyledEmphaseText>Global Health</StyledEmphaseText>

<IconHelp
placement="bottom"
tooltipMessage={
<Stack direction="vertical" gap="r4">
{intl
.formatMessage({
id: 'global_health_explanation',
})
.split('\n')
.map((line, key) => (
<SmallerText key={`globalheathexplanation-${key}`}>{line}</SmallerText>
))}
</Stack>
}
/>
<CircleStatus status={platformStatus} />
</Stack>

{historyAlertStatus === 'loading' ? (
<Box ml={8} height={50}>
<Loader size={'larger'} />
</Box>
) : (
<GlobalHealthBarRecharts
id={'platform_globalhealth'}
alerts={
historyAlertStatus === 'error'
? ([
{
startsAt: startingTimeISO,
endsAt: currentTimeISO,
severity: 'unavailable',
description: 'Failed to load alert history for the selected period',
},
] as Alert[])
: alerts || []
}
start={new Date(startingTimeISO)}
end={new Date(currentTimeISO)}
/>
)}
</HealthBarContainer>
<PlatformGlobalHealthBar />
</Box>
<Box flex="2" ml={24}>
<DashboardAlerts />
Expand Down
83 changes: 83 additions & 0 deletions ui/src/components/PlatformGlobalHealthBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { IconHelp, Loader, SmallerText, Stack, Text } from '@scality/core-ui';
import { Alert, Box, GlobalHealthBar as GlobalHealthBarRecharts, useMetricsTimeSpan } from '@scality/core-ui/dist/next';
import { useIntl } from 'react-intl';
import { useQuery } from 'react-query';
import styled from 'styled-components';
import { highestAlertToStatus, useAlertLibrary, useHighestSeverityAlerts } from '../containers/AlertProvider';
import { useStartingTimeStamp } from '../containers/StartTimeProvider';
import { getClusterAlertSegmentQuery } from '../services/platformlibrary/metrics';
import CircleStatus from './CircleStatus';

const HealthBarContainer = styled.div`
display: flex;
flex-direction: column;
`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original HealthBarContainer had width: 90%; margin: 0 auto; which is now removed. This changes the dashboard layout — the health bar will render at full parent width instead of centered at 90%. Intentional?

— Claude Code


const PlatformGlobalHealthBar = ({ title = 'Global Health' }: { title?: string }) => {
const intl = useIntl();
const { startingTimeISO, currentTimeISO } = useStartingTimeStamp();
const alertsLibrary = useAlertLibrary();
const { duration } = useMetricsTimeSpan();
const { data: alerts, status: historyAlertStatus } = useQuery({
...getClusterAlertSegmentQuery(duration),
keepPreviousData: true,
});
const platformHighestSeverityAlert = useHighestSeverityAlerts(alertsLibrary.getPlatformAlertSelectors());
const platformStatus = highestAlertToStatus(platformHighestSeverityAlert);

return (
<HealthBarContainer>
<Stack
style={{
display: 'flex',
alignItems: 'center',
}}
gap="r8"
>
<CircleStatus status={platformStatus} />
<Text isEmphazed>{title}</Text>
<IconHelp
placement="bottom"
tooltipMessage={
<Stack direction="vertical" gap="r4">
{intl
.formatMessage({
id: 'global_health_explanation',
})
.split('\n')
.map((line, key) => (
<SmallerText key={`globalheathexplanation-${key}`}>{line}</SmallerText>
))}
</Stack>
}
/>
</Stack>

{historyAlertStatus === 'loading' ? (
<Box ml={8} height={50}>
<Loader size={'larger'} />
</Box>
) : (
<GlobalHealthBarRecharts
id={'platform_globalhealth'}
alerts={
historyAlertStatus === 'error'
? ([
{
startsAt: startingTimeISO,
endsAt: currentTimeISO,
severity: 'unavailable',
description: 'Failed to load alert history for the selected period',
},
] as Alert[])
: alerts || []
}
start={new Date(startingTimeISO)}
end={new Date(currentTimeISO)}
/>
)}
</HealthBarContainer>
);
};

export default PlatformGlobalHealthBar;
66 changes: 66 additions & 0 deletions ui/src/components/PlatformGlobalHealthBarFederated.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { MetricsTimeSpanContext } from '@scality/core-ui/dist/components/charts/MetricsTimeSpanProvider';
import { useShellHooks } from '@scality/module-federation';
import { useEffect, useLayoutEffect, useMemo } from 'react';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import. useLayoutEffect is imported but never used.

```suggestion
import { useEffect, useMemo } from 'react';

import FederatedIntlProvider from '../containers/IntlProvider';
import StartTimeProvider from '../containers/StartTimeProvider';
import { initialize as initializePrometheus, setHeaders } from '../services/prometheus/api';
import PlatformGlobalHealthBar from './PlatformGlobalHealthBar';

// 7-day defaults — same constants as core-ui's SAMPLE_DURATION/FREQUENCY_LAST_SEVEN_DAYS
const DEFAULT_DURATION_SECONDS = 7 * 24 * 60 * 60;
const DEFAULT_FREQUENCY_SECONDS = 60 * 60;

type Props = {
prometheusUrl: string;
title?: string;
durationSeconds?: number;
frequencySeconds?: number;
};

export default function PlatformGlobalHealthBarFederated({
prometheusUrl,
title,
durationSeconds = DEFAULT_DURATION_SECONDS,
frequencySeconds = DEFAULT_FREQUENCY_SECONDS,
}: Props) {
const { useAuth } = useShellHooks();
const { userData } = useAuth();
const token = userData?.token;


/**
* Initialize the Prometheus client and set the authorization header if the token is available
* The initialization of Prometheus client is neeeded here as it is shared with Module Federation to another ui
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: "neeeded" should be "needed".

— Claude Code

* The prometheus client could not be initialized in the parent component rendering it.
*/
useEffect(() => {
if (token) {
initializePrometheus(prometheusUrl);
setHeaders({ Authorization: `Bearer ${token}` });
}
}, [prometheusUrl, token]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: initializePrometheus and setHeaders run in useEffect (after paint), but PlatformGlobalHealthBar fires its useQuery during the same render — before the effect executes. Since prometheusApiClient is still null, queryPrometheusRange returns undefined, React Query treats it as a successful response with no data, and the health bar stays empty until refetchInterval fires (60 s).

Fix by tracking initialization state so the child only renders once Prometheus is ready, and gate the render: if (!token || !isInitialized) return null;

— Claude Code

Comment on lines +36 to +41
Copy link
Copy Markdown

@damiengillesscality damiengillesscality Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Since this component displays null when no token is available we should probably make token a property of the component and do conditional rendering, that would allow us to dodge some ifs.
  • Why is it a useLayoutEffect and not a useEffect ?
  • I don't see initializePrometheus defined anywhere in the code, there is function that looks like it with a different name tho.
  • From where will prometheusUrl come from ? Does it need to initialise the api so deep into the code ? This part can probably be bubled up or we will forget it and reinitialise it at different places causing more redraws.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initializePrometheus is an alias for initialize from prometheus api:
import { initialize as initializePrometheus, setHeaders } from '../services/prometheus/api';

The component will be used in artesca, if the prometheus is not initialized the query will throw an error


const timeSpan = useMemo(
() => ({
query: '',
label: '',
duration: durationSeconds,
interval: frequencySeconds,
//TODO: remove this field when QueryTimeSpan type is updated
frequency: frequencySeconds, // required by QueryTimeSpan type (deprecated but not optional)
}),
[durationSeconds, frequencySeconds],
);

if (!token) return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: useEffect runs after paint, so on the first render where token is available, PlatformGlobalHealthBar mounts and useQuery fires before initializePrometheus executes. queryPrometheusRange finds prometheusApiClient still null and silently returns undefined.

Consider adding an initialization guard — e.g. an isInitialized state set to true inside the effect, and gate rendering on !token || !isInitialized.

(The unused useLayoutEffect import on line 3 suggests this was partially considered.)

— Claude Code


return (
<MetricsTimeSpanContext.Provider value={timeSpan}>
<StartTimeProvider>
<FederatedIntlProvider>
<PlatformGlobalHealthBar title={title} />
Comment thread
JeanMarcMilletScality marked this conversation as resolved.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PlatformGlobalHealthBar calls useHighestSeverityAlerts() and useAlertLibrary(), which both rely on useShellAlerts(). The other federated component (AlertNavbarUpdaterComponent) explicitly wraps its children with AlertProvider to ensure alert hooks work. This component omits it — is the host app guaranteed to have AlertsProvider in its tree above the mount point for this federated component? If not, these hooks may fail at runtime.

— Claude Code

Comment thread
JeanMarcMilletScality marked this conversation as resolved.
</FederatedIntlProvider>
</StartTimeProvider>
</MetricsTimeSpanContext.Provider>
);
}
18 changes: 9 additions & 9 deletions ui/src/containers/StartTimeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { createContext, useContext, useCallback, useState } from 'react';
import { useMetricsTimeSpan } from '@scality/core-ui/dist/next';
import type React from 'react';
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { REFRESH_METRICS_GRAPH } from '../constants';
import { useEffect } from 'react';
import { useMemo } from 'react';

type StartTimeContextType = {
startingTimeISO: string;
currentTimeISO: string;
Expand All @@ -22,23 +22,23 @@ export const useStartingTimeStamp = (): {
};

const StartTimeProvider = ({ children }: { children: React.ReactNode }) => {
const { duration, frequency } = useMetricsTimeSpan();
const { duration, interval } = useMetricsTimeSpan();
const [currentTime, setCurrentTime] = useState(() => {
const newCurrentDate = new Date().getTime();
return newCurrentDate - (newCurrentDate % (frequency * 1000));
const newCurrentDate = Date.now();
return newCurrentDate - (newCurrentDate % (interval * 1000));
});
const [startingTimeISO, setStartingTimeISO] = useState(
new Date((currentTime / 1000 - duration) * 1000).toISOString(),
);
const updateCurrentTime = useCallback(() => {
const newCurrentDate = new Date().getTime();
const newCurrentDate = Date.now();
//In order to always display the same data on the charts over refresh and new entroes comming
//we round start and end time to frequency factors. Hence for example for a 30 seconds fequency
//we will rount current time and start time to the previous 30 seconds factor (00, or 30 seconds for each minute)
const newCurrentTime = newCurrentDate - (newCurrentDate % (frequency * 1000));
const newCurrentTime = newCurrentDate - (newCurrentDate % (interval * 1000));
setCurrentTime(newCurrentTime);
setStartingTimeISO(new Date((newCurrentTime / 1000 - duration) * 1000).toISOString());
}, [duration, frequency]);
}, [duration, interval]);
useMemo(() => {
updateCurrentTime();
}, [updateCurrentTime]);
Expand Down
Loading