diff --git a/shell-ui/src/alerts/AlertProvider.tsx b/shell-ui/src/alerts/AlertProvider.tsx index fb05e85c5b..1f013f635e 100644 --- a/shell-ui/src/alerts/AlertProvider.tsx +++ b/shell-ui/src/alerts/AlertProvider.tsx @@ -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'; /** @@ -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 ( {query.status === 'loading' && ( diff --git a/shell-ui/src/alerts/alertHooks.test.tsx b/shell-ui/src/alerts/alertHooks.test.tsx index 45f81611a4..e5ac68fd23 100644 --- a/shell-ui/src/alerts/alertHooks.test.tsx +++ b/shell-ui/src/alerts/alertHooks.test.tsx @@ -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 = { diff --git a/shell-ui/src/alerts/services/alertManager.ts b/shell-ui/src/alerts/services/alertManager.ts index 2f1d928b70..01e80ce157 100644 --- a/shell-ui/src/alerts/services/alertManager.ts +++ b/shell-ui/src/alerts/services/alertManager.ts @@ -2,8 +2,6 @@ import { removeWarningAlerts, formatActiveAlerts, sortAlerts, - STATUS_CRITICAL, - STATUS_HEALTH, } from './alertUtils'; export type PrometheusAlert = { annotations: Record; @@ -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(); @@ -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; - }); -}; diff --git a/ui/src/FederableApp.tsx b/ui/src/FederableApp.tsx index 8f898033c2..85556010e8 100644 --- a/ui/src/FederableApp.tsx +++ b/ui/src/FederableApp.tsx @@ -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'; @@ -125,11 +126,13 @@ export default function FederableApp(props: FederatedAppProps) { > - - - - - + + + + + + + diff --git a/ui/src/components/DashboardAlerts.test.tsx b/ui/src/components/DashboardAlerts.test.tsx index fca56d9fd1..f7f38ee4fe 100644 --- a/ui/src/components/DashboardAlerts.test.tsx +++ b/ui/src/components/DashboardAlerts.test.tsx @@ -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(); + 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, diff --git a/ui/src/components/DashboardAlerts.tsx b/ui/src/components/DashboardAlerts.tsx index 534d3d2562..50d82e3fcb 100644 --- a/ui/src/components/DashboardAlerts.tsx +++ b/ui/src/components/DashboardAlerts.tsx @@ -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'; @@ -61,25 +61,45 @@ const DashboardAlerts = () => { const totalAlerts = criticalAlerts.length + warningAlerts.length; return ( -
- - {intl.formatMessage({ - id: 'platform_active_alerts', - })} - - -
- {totalAlerts === 0 ? ( - - {intl.formatMessage({ - id: 'no_active_alerts', - })} - - ) : ( + +
+ + {intl.formatMessage({ + id: 'platform_active_alerts', + })} + + + {totalAlerts === 0 && ( +
+ + {intl.formatMessage({ + id: 'no_active_alerts', + })} + +
+ )} +
+ {alerts.error && ( +
+ } + title={intl.formatMessage({ + id: 'monitoring_information_unavailable', + })} + > + {intl.formatMessage({ + id: 'some_data_not_retrieved', + })} + +
+ )} +
+ {totalAlerts === 0 ? null : (
diff --git a/ui/src/components/NodePartitionTable.test.tsx b/ui/src/components/NodePartitionTable.test.tsx index b6a3d2c4b0..34cf32a70a 100644 --- a/ui/src/components/NodePartitionTable.test.tsx +++ b/ui/src/components/NodePartitionTable.test.tsx @@ -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'; @@ -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(); expect( @@ -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( diff --git a/ui/src/containers/AlertPage.tsx b/ui/src/containers/AlertPage.tsx index 8c44391280..a3b811e14a 100644 --- a/ui/src/containers/AlertPage.tsx +++ b/ui/src/containers/AlertPage.tsx @@ -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` @@ -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, @@ -118,7 +120,7 @@ function AlertPageHeader({ <AlertStatusIcon> <StatusWrapper status={alertStatus}> - <StatusIcon status={alertStatus} name="Alert" entity='Alerts' /> + <StatusIcon status={alertStatus} name="Alert" entity="Alerts" /> </StatusWrapper> </AlertStatusIcon> <> @@ -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) => { @@ -197,6 +204,7 @@ const ActiveAlertTab = React.memo( data={data} defaultSortingKey={DEFAULT_SORTING_KEY} sortTypes={sortTypes} + status={status} entityName={{ en: { singular: 'active alert', @@ -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() { @@ -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> ); diff --git a/ui/src/containers/PrometheusAuthProvider.tsx b/ui/src/containers/PrometheusAuthProvider.tsx new file mode 100644 index 0000000000..4203e777b3 --- /dev/null +++ b/ui/src/containers/PrometheusAuthProvider.tsx @@ -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}</>; +} diff --git a/ui/src/containers/VolumePageRSP.tsx b/ui/src/containers/VolumePageRSP.tsx index 2af2e2be97..3d60a7692a 100644 --- a/ui/src/containers/VolumePageRSP.tsx +++ b/ui/src/containers/VolumePageRSP.tsx @@ -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', ); diff --git a/ui/src/ducks/config.ts b/ui/src/ducks/config.ts index 7444463ec0..1482caf032 100644 --- a/ui/src/ducks/config.ts +++ b/ui/src/ducks/config.ts @@ -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'; @@ -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')); } diff --git a/ui/src/hooks.tsx b/ui/src/hooks.tsx index 4800688c14..1e0683cdf6 100644 --- a/ui/src/hooks.tsx +++ b/ui/src/hooks.tsx @@ -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. diff --git a/ui/src/services/ApiClient.ts b/ui/src/services/ApiClient.ts index ba3c317ced..069aa5861a 100644 --- a/ui/src/services/ApiClient.ts +++ b/ui/src/services/ApiClient.ts @@ -1,18 +1,24 @@ import axios from 'axios'; class ApiClient { - constructor({ apiUrl, headers = {} }) { - // @ts-expect-error - FIXME when you are working on it + headers: Record<string, string>; + settings: { baseURL: string }; + + constructor({ + apiUrl, + headers = {}, + }: { + apiUrl: string; + headers?: Record<string, string>; + }) { this.headers = headers; - // @ts-expect-error - FIXME when you are working on it this.settings = { baseURL: apiUrl, }; } - setHeaders = (headers) => { - // @ts-expect-error - FIXME when you are working on it - this.headers = headers; + setHeaders = (headers: Record<string, string>) => { + this.headers = { ...this.headers, ...headers }; }; async get(endpoint, params = {}, opts = {}) { @@ -72,12 +78,10 @@ class ApiClient { try { const response = await axios({ method, - // @ts-expect-error - FIXME when you are working on it headers: { ...this.headers, ...headers }, params, url: endpoint, data: payload, - // @ts-expect-error - FIXME when you are working on it ...this.settings, }); return response.data; diff --git a/ui/src/services/alertmanager/api.ts b/ui/src/services/alertmanager/api.ts index 6538f35dc5..8308e9e5ea 100644 --- a/ui/src/services/alertmanager/api.ts +++ b/ui/src/services/alertmanager/api.ts @@ -1,16 +1,3 @@ -import ApiClient from '../ApiClient'; -import { STATUS_CRITICAL, STATUS_HEALTH } from '../../constants'; -import { - removeWarningAlerts, - formatActiveAlerts, - sortAlerts, -} from '../alertUtils'; -let alertmanagerApiClient: ApiClient | null | undefined = null; -export function initialize(apiUrl: string) { - alertmanagerApiClient = new ApiClient({ - apiUrl, - }); -} export type PrometheusAlert = { annotations: Record<string, string>; receivers: { @@ -33,35 +20,3 @@ export type AlertLabels = { selectors?: string[]; [labelName: string]: string; }; -export function getAlerts() { - if (!alertmanagerApiClient) { - throw new Error('alertmanagerApiClient should be defined'); - } - - return alertmanagerApiClient - .get('/api/v2/alerts') - .then((resolve) => { - if (resolve.error) { - throw resolve.error; - } - - return resolve; - }) - .then((result) => { - // format the alerts then remove the warning and finally sort the alerts. - 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; - }); -}; diff --git a/ui/src/services/prometheus/api.ts b/ui/src/services/prometheus/api.ts index 21bb237fd2..d998a861e3 100644 --- a/ui/src/services/prometheus/api.ts +++ b/ui/src/services/prometheus/api.ts @@ -51,6 +51,16 @@ export function initialize(apiUrl: string) { prometheusApiClient = new ApiClient({ apiUrl }); } +export function setHeaders(headers: Record<string, string>) { + if (prometheusApiClient) { + prometheusApiClient.setHeaders(headers); + } else { + console.warn( + 'setHeaders called before prometheusApiClient was initialized', + ); + } +} + export function getAlerts() { if (prometheusApiClient) { return prometheusApiClient.get('/api/v1/alerts');