Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
85 changes: 85 additions & 0 deletions ui/src/components/PlatformGlobalHealthBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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`
flex-direction: column;
margin: 0 auto;
`;
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 variant="Large" 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;
56 changes: 56 additions & 0 deletions ui/src/components/PlatformGlobalHealthBarFederated.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { MetricsTimeSpanContext } from '@scality/core-ui/dist/components/charts/MetricsTimeSpanProvider';
import { useShellHooks } from '@scality/module-federation';
import { useLayoutEffect } 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;

useLayoutEffect(() => {
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


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


const timeSpan = {
query: '',
label: '',
duration: durationSeconds,
interval: frequencySeconds,
frequency: frequencySeconds, // StartTimeProvider reads this deprecated field
};
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 timeSpan object is recreated on every render, which gives MetricsTimeSpanContext.Provider a new reference each time and forces all context consumers (including the charts inside PlatformGlobalHealthBar) to re-render unnecessarily.

Memoize it:

```suggestion
const timeSpan = useMemo(() => ({
query: '',
label: '',
duration: durationSeconds,
interval: frequencySeconds,
frequency: frequencySeconds, // StartTimeProvider reads this deprecated field
}), [durationSeconds, frequencySeconds]);

<br>And add `useMemo` to the React import on line 3.<br><br>— Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

timeSpan is recreated on every render, producing a new object reference each time. Since it is passed as the value to MetricsTimeSpanContext.Provider, every parent re-render will push a new context value and trigger re-renders in all consumers (StartTimeProvider, PlatformGlobalHealthBar, useMetricsTimeSpan).

Wrap it in useMemo:

suggestion const timeSpan = useMemo(() => ({ query: '', label: '', duration: durationSeconds, interval: frequencySeconds, frequency: frequencySeconds, // StartTimeProvider reads this deprecated field }), [durationSeconds, frequencySeconds]);
and add useMemo to the import on line 3.

— 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>
);
}
Loading