Skip to content
Open
19 changes: 12 additions & 7 deletions shell-ui/src/alerts/AlertProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { useQuery } from 'react-query';
import { Loader } from '@scality/core-ui/dist/components/loader/Loader.component';
import { useAuth } from '../auth/AuthProvider';
import { getAlerts } from './services/alertManager';
import { AlertContext } from './alertContext';
/**
Expand All @@ -18,13 +19,17 @@ export default function AlertProvider({
alertManagerUrl: string;
children: React.ReactNode;
}) {
const query = useQuery('activeAlerts', () => getAlerts(alertManagerUrl), {
// refetch the alerts every 10 seconds
refetchInterval: 30000,
// TODO manage this refresh interval globally
// avoid stucking at the hard loading state before alertmanager is ready
initialData: [],
});
const { getToken } = useAuth();
const query = useQuery(
['activeAlerts'],
async () => getAlerts(alertManagerUrl, await getToken()),
{
refetchInterval: 30000,
// TODO manage this refresh interval globally
// avoid stucking at the hard loading state before alertmanager is ready
initialData: [],
},
);
return (
<AlertContext.Provider value={{ ...query }}>
{query.status === 'loading' && (
Expand Down
10 changes: 10 additions & 0 deletions shell-ui/src/alerts/alertHooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ import AlertProvider from './AlertProvider';
import { useHighestSeverityAlerts } from './alertHooks';
import { afterAll, beforeAll, jest } from '@jest/globals';
import { QueryClientProvider } from '../QueryClientProvider';

jest.mock('../auth/AuthProvider', () => ({
useAuth: () => ({
userData: {
token: 'test-token',
},
getToken: () => Promise.resolve('test-token'),
}),
}));

const testService = 'http://10.0.0.1/api/alertmanager';

const VOLUME_DEGRADED_ALERT = {
Expand Down
21 changes: 4 additions & 17 deletions shell-ui/src/alerts/services/alertManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import {
removeWarningAlerts,
formatActiveAlerts,
sortAlerts,
STATUS_CRITICAL,
STATUS_HEALTH,
} from './alertUtils';
export type PrometheusAlert = {
annotations: Record<string, string>;
Expand All @@ -29,8 +27,10 @@ export type AlertLabels = {
selectors?: string[];
[labelName: string]: string;
};
export function getAlerts(alertManagerUrl: string) {
return fetch(alertManagerUrl + '/api/v2/alerts')
export function getAlerts(alertManagerUrl: string, token?: string) {
return fetch(alertManagerUrl + '/api/v2/alerts', {
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
.then((r) => {
if (r.ok) {
return r.json();
Expand All @@ -50,16 +50,3 @@ export function getAlerts(alertManagerUrl: string) {
return sortAlerts(removeWarningAlerts(formatActiveAlerts(result)));
});
}
export const checkActiveAlertProvider = (): Promise<{
status: 'healthy' | 'critical';
}> => {
// depends on Watchdog to see the if Alertmanager is up
// @ts-expect-error - FIXME when you are working on it
return getAlerts().then((result) => {
const watchdog = result.find(
(alert) => alert.labels.alertname === 'Watchdog',
);
if (watchdog) return STATUS_HEALTH;
else return STATUS_CRITICAL;
});
};
13 changes: 8 additions & 5 deletions ui/src/FederableApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { applyMiddleware, compose, createStore, Store } from 'redux';
import createSagaMiddleware from 'redux-saga';
import 'regenerator-runtime/runtime';
import App from './containers/App';
import PrometheusAuthProvider from './containers/PrometheusAuthProvider';
import { authErrorAction } from './ducks/app/authError';
import { setApiConfigAction } from './ducks/config';
import { setHistory as setReduxHistory } from './ducks/history';
Expand Down Expand Up @@ -125,11 +126,13 @@ export default function FederableApp(props: FederatedAppProps) {
>
<Provider store={store}>
<AppConfigProvider>
<ToastProvider>
<RouterWithBaseName>
<App />
</RouterWithBaseName>
</ToastProvider>
<PrometheusAuthProvider>
<ToastProvider>
<RouterWithBaseName>
<App />
</RouterWithBaseName>
</ToastProvider>
</PrometheusAuthProvider>
</AppConfigProvider>
</Provider>
</ShellHooksProvider>
Expand Down
18 changes: 18 additions & 0 deletions ui/src/components/DashboardAlerts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,24 @@ describe('the dashboard alerts sub-panel', () => {
expect(queryByTestId('critical-alert-badge')).not.toBeInTheDocument();
expect(queryByTestId('view-all-link')).not.toBeInTheDocument();
});
test('should display a warning banner when alerts fail to fetch', () => {
(useAlertLibrary as any).mockImplementation(() => ({
getPlatformAlertSelectors: () => ({
alertname: ['ClusterAtRisk', 'ClusterDegraded'],
}),
}));
(useAlerts as any).mockImplementation(() => ({
alerts: [],
error: new Error('Alert manager responded with 401'),
}));
const { getByText } = render(<DashboardAlerts />);
expect(
getByText('Monitoring information unavailable'),
).toBeInTheDocument();
expect(
getByText('Some data can not be well retrieved'),
).toBeInTheDocument();
});
test('should redirect to alert page with View All link', () => {
(useAlerts as any).mockImplementation(() => ({
alerts: alerts,
Expand Down
60 changes: 40 additions & 20 deletions ui/src/components/DashboardAlerts.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { spacing, Text, TextBadge } from '@scality/core-ui';
import { Banner, Icon, spacing, Text, TextBadge } from '@scality/core-ui';
import { Box } from '@scality/core-ui/dist/next';
import { useMemo } from 'react';
import { useIntl } from 'react-intl';
Expand Down Expand Up @@ -61,25 +61,45 @@ const DashboardAlerts = () => {
const totalAlerts = criticalAlerts.length + warningAlerts.length;
return (
<AlertsContainer>
<div>
<Text isEmphazed>
{intl.formatMessage({
id: 'platform_active_alerts',
})}
</Text>
<TextBadge
variant="infoPrimary"
data-testid="all-alert-badge"
text={totalAlerts}
/>
</div>
{totalAlerts === 0 ? (
<Text variant="Smaller" color="textSecondary">
{intl.formatMessage({
id: 'no_active_alerts',
})}
</Text>
) : (
<Box display="flex" alignItems="center" gap={spacing.r8}>
<div>
<Text isEmphazed>
{intl.formatMessage({
id: 'platform_active_alerts',
})}
</Text>
<TextBadge
variant="infoPrimary"
data-testid="all-alert-badge"
text={totalAlerts}
/>
{totalAlerts === 0 && (
<div>
<Text variant="Smaller" color="textSecondary">
{intl.formatMessage({
id: 'no_active_alerts',
})}
</Text>
</div>
)}
</div>
{alerts.error && (
<div style={{ flex: 1 }}>
<Banner
variant="warning"
icon={<Icon name="Exclamation-circle" />}
title={intl.formatMessage({
id: 'monitoring_information_unavailable',
})}
>
{intl.formatMessage({
id: 'some_data_not_retrieved',
})}
</Banner>
</div>
)}
</Box>
{totalAlerts === 0 ? null : (
<Box pr={24}>
<BadgesContainer>
<div>
Expand Down
3 changes: 0 additions & 3 deletions ui/src/components/NodePartitionTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { rest } from 'msw';
import NodePartitionTable from './NodePartitionTable';
import { render, FAKE_CONTROL_PLANE_IP } from './__TEST__/util';
import { initialize as initializeProm } from '../services/prometheus/api';
import { initialize as initializeAM } from '../services/alertmanager/api';
import { initialize as initializeLoki } from '../services/loki/api';
import { mockOffsetSize } from '../tests/mocks/util';
import { useAlerts } from '../containers/AlertProvider';
Expand Down Expand Up @@ -338,7 +337,6 @@ describe('the system partition table', () => {
});
// Setup
initializeProm(`http://${FAKE_CONTROL_PLANE_IP}:8443/api/prometheus`);
initializeAM(`http://${FAKE_CONTROL_PLANE_IP}:8443/api/alertmanager`);
initializeLoki(`http://${FAKE_CONTROL_PLANE_IP}:8443/api/loki`);
render(<NodePartitionTable instanceIP={'192.168.1.29'} />);
expect(
Expand All @@ -361,7 +359,6 @@ describe('the system partition table', () => {
test('handles server error', async () => {
// S
initializeProm(`http://${FAKE_CONTROL_PLANE_IP}:8443/api/prometheus`);
initializeAM(`http://${FAKE_CONTROL_PLANE_IP}:8443/api/alertmanager`);
initializeLoki(`http://${FAKE_CONTROL_PLANE_IP}:8443/api/loki`);
// override the default route with error status
server.use(
Expand Down
35 changes: 24 additions & 11 deletions ui/src/containers/AlertPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { STATUS_CRITICAL, STATUS_HEALTH, STATUS_WARNING } from '../constants';
import { useUserAccessRight } from '../hooks';
import { compareHealth } from '../services/utils';
import { useAlerts } from './AlertProvider';
import type { Alert } from '../services/alertUtils';
import type { QueryStatus } from 'react-query';
import { useBasenameRelativeNavigate } from '@scality/module-federation';

const AlertPageHeaderContainer = styled.div`
Expand Down Expand Up @@ -94,8 +96,8 @@ const getAlertStatus = (numbersOfCritical, numbersOfWarning) =>
numbersOfCritical > 0
? STATUS_CRITICAL
: numbersOfWarning > 0
? STATUS_WARNING
: STATUS_HEALTH;
? STATUS_WARNING
: STATUS_HEALTH;

function AlertPageHeader({
activeAlerts,
Expand All @@ -118,7 +120,7 @@ function AlertPageHeader({
<Title>
<AlertStatusIcon>
<StatusWrapper status={alertStatus}>
<StatusIcon status={alertStatus} name="Alert" entity='Alerts' />
<StatusIcon status={alertStatus} name="Alert" entity="Alerts" />
</StatusWrapper>
</AlertStatusIcon>
<>
Expand Down Expand Up @@ -163,9 +165,14 @@ function AlertPageHeader({
);
}

type ActiveAlertTabProps = {
columns: Record<string, unknown>[];
data: Alert[];
status: QueryStatus;
};

const ActiveAlertTab = React.memo(
// @ts-expect-error - FIXME when you are working on it
({ columns, data }) => {
({ columns, data, status }: ActiveAlertTabProps) => {
const sortTypes = React.useMemo(() => {
return {
severity: (row1, row2) => {
Expand Down Expand Up @@ -197,6 +204,7 @@ const ActiveAlertTab = React.memo(
data={data}
defaultSortingKey={DEFAULT_SORTING_KEY}
sortTypes={sortTypes}
status={status}
entityName={{
en: {
singular: 'active alert',
Expand All @@ -219,10 +227,12 @@ const ActiveAlertTab = React.memo(
</Table>
);
},
(a, b) => {
// compare the alert only on id and severity
// @ts-expect-error - FIXME when you are working on it
return isEqual(a.columns, b.columns) && isEqualAlert(a.data, b.data);
(prevProps: ActiveAlertTabProps, nextProps: ActiveAlertTabProps) => {
return (
isEqual(prevProps.columns, nextProps.columns) &&
isEqualAlert(prevProps.data, nextProps.data) &&
prevProps.status === nextProps.status
);
},
);
export default function AlertPage() {
Expand Down Expand Up @@ -295,8 +305,11 @@ export default function AlertPage() {
/>
</AppContainer.OverallSummary>
<AppContainer.MainContent>
{/* @ts-expect-error - FIXME when you are working on it */}
<ActiveAlertTab data={leafAlerts} columns={columns} />
<ActiveAlertTab
data={leafAlerts}
columns={columns}
status={alerts.status}
/>
</AppContainer.MainContent>
</AppContainer>
);
Expand Down
28 changes: 28 additions & 0 deletions ui/src/containers/PrometheusAuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ReactNode, useEffect } from 'react';
import { useAuth } from './PrivateRoute';
import { setHeaders } from '../services/prometheus/api';

// Temporary solution: The Prometheus API client is initialized with the URL in the saga
// (setApiConfig), but the auth token is not available at that point. Rather than modifying
// the saga (which will be removed soon), this provider sets the auth header on the shared
// Prometheus client once the token is available, and blocks rendering until it's ready.
// TODO: Remove this provider once the saga is removed.

export default function PrometheusAuthProvider({
children,
}: {
children: ReactNode;
}) {
const { userData } = useAuth();
const token = userData?.token;

useEffect(() => {
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 render, so when token first becomes available, children render (and can trigger Prometheus API calls) before setHeaders has executed. Use useLayoutEffect instead to ensure headers are set before children mount.

— Claude Code

if (token) {
setHeaders({ Authorization: `Bearer ${token}` });
}
}, [token]);

if (!token) return null;

return <>{children}</>;
}
2 changes: 1 addition & 1 deletion ui/src/containers/VolumePageRSP.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const VolumePageRSP = (props) => {
const alertsVolume = useAlerts({
persistentvolumeclaim: PVCName,
});
const alertlist = alertsVolume && alertsVolume.alerts;
const alertlist = alertsVolume?.alerts ?? [];
const criticalAlerts = alertlist.filter(
(alert) => alert.severity === 'critical',
);
Expand Down
2 changes: 0 additions & 2 deletions ui/src/ducks/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import * as Api from '../services/api';
import * as ApiK8s from '../services/k8s/api';
import * as ApiSalt from '../services/salt/api';
import * as ApiPrometheus from '../services/prometheus/api';
import * as ApiAlertmanager from '../services/alertmanager/api';
import * as ApiLoki from '../services/loki/api';
import { EN_LANG } from '../constants';
import { authenticateSaltApi } from './login';
Expand Down Expand Up @@ -165,7 +164,6 @@ function* setApiConfig({
}): Generator<Effect, void, Result<Config>> {
yield call(ApiSalt.initialize, config.url_salt);
yield call(ApiPrometheus.initialize, config.url_prometheus);
yield call(ApiAlertmanager.initialize, config.url_alertmanager);
yield call(ApiLoki.initialize, config.url_loki);
yield put(setConfigStatusAction('success'));
}
Expand Down
6 changes: 1 addition & 5 deletions ui/src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,9 @@ export const useVolumesWithAlerts = (nodeName?: string) => {
// @ts-expect-error - FIXME when you are working on it
getVolumeListData(state, null, nodeName),
);
//This forces alerts to have been fetched at least once (watchdog alert should be present)
// before rendering volume list
// TODO enhance this using useAlerts status
if (!alerts || alerts.length === 0) return [];
// @ts-expect-error - FIXME when you are working on it
const volumeListWithStatus = volumeListData.map((volume) => {
const volumeAlerts = filterAlerts(alerts, {
const volumeAlerts = filterAlerts(alerts || [], {
persistentvolumeclaim: volume.persistentvolumeclaim,
});
// For the unbound volume, the health status should be none.
Expand Down
Loading
Loading