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, useLayoutEffect } 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;

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