diff --git a/shell-ui/src/alerts/alertHooks.js b/shell-ui/src/alerts/alertHooks.js index 85bb3528a8..aa53690ce7 100644 --- a/shell-ui/src/alerts/alertHooks.js +++ b/shell-ui/src/alerts/alertHooks.js @@ -19,7 +19,7 @@ export const getVolumesAlertSelectors = (): FilterLabels => { export const getNetworksAlertSelectors = (): FilterLabels => { return { - alertname: ['ControlPlaneNetworkDegraded', 'WorkloadPlaneNetworkDegraded'], + alertname: ['NetworkDegraded'], }; }; diff --git a/ui/src/components/DashboardNetwork.js b/ui/src/components/DashboardNetwork.js new file mode 100644 index 0000000000..038afff647 --- /dev/null +++ b/ui/src/components/DashboardNetwork.js @@ -0,0 +1,37 @@ +import React from 'react'; +import styled from 'styled-components'; +import { useIntl } from 'react-intl'; + +import { spacing } from '@scality/core-ui/dist/style/theme'; +import { PageSubtitle } from '../components/style/CommonLayoutStyle'; + +import DashboardPlane from './DashboardPlane'; + +export const NetworkContainer = styled.div` + padding: ${spacing.sp2} ${spacing.sp4}; + display: flex; + flex-direction: column; + flex-grow: 1; +`; + +export const PanelActions = styled.div` + display: flex; + padding: ${spacing.sp4}; + align-items: center; + justify-content: space-between; +`; + +const DashboardNetwork = () => { + const intl = useIntl(); + + return ( + + + {intl.formatMessage({ id: 'network' })} + + + + ); +}; + +export default DashboardNetwork; diff --git a/ui/src/components/DashboardPlane.js b/ui/src/components/DashboardPlane.js new file mode 100644 index 0000000000..993dc03565 --- /dev/null +++ b/ui/src/components/DashboardPlane.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { useIntl } from 'react-intl'; +import styled from 'styled-components'; +import { + useAlertLibrary, + useHighestSeverityAlerts, + highestAlertToStatus, +} from '../containers/AlertProvider'; +import { PageSubtitle } from './style/CommonLayoutStyle'; +import { PanelActions, NetworkContainer } from './DashboardNetwork'; +import HealthItem from './HealthItem.js'; +import { spacing } from '@scality/core-ui/dist/style/theme'; + +const PlanesContainer = styled.div` + padding-left: ${spacing.sp8}; + display: flex; + flex-direction: row; +`; + +const PlaneContainer = styled.div` + display: flex; + flex-direction: row; + margin-right: ${spacing.sp40}; +`; + +const DashboardPlane = () => { + const intl = useIntl(); + const alertsLibrary = useAlertLibrary(); + + const planesHighestSecurityAlert = useHighestSeverityAlerts( + alertsLibrary.getNetworksAlertSelectors(), + ); + const planesStatus = highestAlertToStatus(planesHighestSecurityAlert); + + return ( + + + Planes + + + + + + + + + + + ); +}; + +export default DashboardPlane; diff --git a/ui/src/components/DashboardPlane.test.js b/ui/src/components/DashboardPlane.test.js new file mode 100644 index 0000000000..8189e21693 --- /dev/null +++ b/ui/src/components/DashboardPlane.test.js @@ -0,0 +1,102 @@ +//@flow +import React from 'react'; +import { screen } from '@testing-library/react'; +import DashboardPlane from './DashboardPlane'; +import { render } from './__TEST__/util'; +import type { Alert } from '../services/alertUtils'; +import { useHighestSeverityAlerts } from '../containers/AlertProvider'; +import { + STATUS_WARNING, + STATUS_CRITICAL, + STATUS_HEALTH, +} from '../constants.js'; + +const alertsCritical = [ + { + id: 'alert1', + severity: STATUS_CRITICAL, + startsAt: '2021-07-28T10:36:24.293Z', + }, +]; +const alertsWarning = [ + { + id: 'alert2', + severity: STATUS_WARNING, + startsAt: '2021-07-28T10:36:24.293Z', + }, +]; +const noAlerts = []; + +jest.mock('../containers/AlertProvider', () => ({ + __esModule: true, + default: ({ children }) => <>{children}, + useHighestSeverityAlerts: jest.fn(), + useAlertLibrary: () => ({ + getNetworksAlertSelectors: () => {}, + }), + highestAlertToStatus: (alerts?: Alert[]): string => { + return (alerts?.[0] && ((alerts[0].severity: any): string)) || 'healthy'; + }, +})); + +jest.mock('../containers/ConfigProvider', () => ({ + __esModule: true, + default: ({ children }) => <>{children}, + useLinkOpener: () => ({ + openLink: jest.fn(), + }), + useDiscoveredViews: () => [ + { + app: { + kind: '', + name: '', + version: '', + url: '', + appHistoryBasePath: '', + }, + isFederated: true, + view: { path: '/alerts' }, + }, + ], +})); + +const NB_ITEMS = 2; + +describe("the dashboard network's plane panel", () => { + test("displays the network's plane panel and display 2 green statuses when no alerts are present", async () => { + // Have to any type jest.fn function to avoid Flow warning for mockImplementation() + (useHighestSeverityAlerts: any).mockImplementation(() => noAlerts); + render(); + expect(screen.getAllByLabelText(`status ${STATUS_HEALTH}`)).toHaveLength( + NB_ITEMS, + ); + }); + + test('displays 2 warning statuses when warning alerts are present as well as link to the alerts page', async () => { + // Have to any type jest.fn function to avoid Flow warning for mockImplementation() + (useHighestSeverityAlerts: any).mockImplementation(() => alertsWarning); + + // Render + render(); + + // Verify + expect(screen.getAllByLabelText(`status ${STATUS_WARNING}`)).toHaveLength( + NB_ITEMS, + ); + expect(screen.getAllByTestId('alert-link')).toHaveLength(NB_ITEMS); + }); + + test('displays 2 critical statuses when warning alerts are present as well as link to the alerts page', async () => { + // Have to any type jest.fn function to avoid Flow warning for mockImplementation() + (useHighestSeverityAlerts: any).mockImplementation(() => alertsCritical); + + // Render + render(); + + // Verify + expect(screen.getAllByLabelText(`status ${STATUS_CRITICAL}`)).toHaveLength( + NB_ITEMS, + ); + expect(screen.getAllByTestId('alert-link')).toHaveLength(NB_ITEMS); + }); +}); diff --git a/ui/src/components/DashboardServices.js b/ui/src/components/DashboardServices.js index b2d718b66b..889decf0c0 100644 --- a/ui/src/components/DashboardServices.js +++ b/ui/src/components/DashboardServices.js @@ -1,26 +1,16 @@ // @flow import React from 'react'; import styled from 'styled-components'; -import { Link } from 'react-router-dom'; import { useIntl } from 'react-intl'; -import { Tooltip, StatusText } from '@scality/core-ui'; -import { - spacing, - fontSize, - fontWeight, -} from '@scality/core-ui/dist/style/theme'; +import { spacing } from '@scality/core-ui/dist/style/theme'; -import type { Alert } from '../services/alertUtils'; -import type { Status } from '../containers/AlertProvider'; import { PageSubtitle } from '../components/style/CommonLayoutStyle'; import { useAlertLibrary, useHighestSeverityAlerts, highestAlertToStatus, } from '../containers/AlertProvider'; -import CircleStatus from './CircleStatus'; -import { STATUS_HEALTH } from '../constants.js'; -import { formatDateToMid1 } from '../services/utils'; +import HealthItem from './HealthItem.js'; const ServiceItems = styled.div` display: flex; @@ -28,137 +18,6 @@ const ServiceItems = styled.div` padding: ${spacing.sp4}; `; -const ServiceItemLabelWrapper = styled.div` - display: flex; - align-items: baseline; -`; - -const ServiceItemLabel = styled.div` - margin-left: ${spacing.sp8}; -`; - -const ServiceItemElement = styled.div` - padding: ${spacing.sp4}; -`; - -const ClickableServiceItemElement = styled(ServiceItemElement)` - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; - - :hover { - background-color: ${(props) => props.theme.highlight}; - } -`; - -const NonHealthyServiceItemElement = styled.div` - cursor: pointer; - width: 100%; - display: flex; - flex-direction: column; - - a { - text-decoration: none; - color: inherit; - width: 100%; - display: flex; - align-items: center; - } -`; - -const NonHealthyPopUp = styled.div` - display: flex; - flex-direction: column; - font-size: ${fontSize.base}; - - label { - width: 25%; - margin-right: ${spacing.sp8}; - color: ${(props) => props.theme.textSecondary}; - text-align: right; - } -`; - -const NonHealthyPopUpTitle = styled.div` - font-weight: ${fontWeight.bold} - text-align: center; -`; - -const NonHealthyPopUpItem = styled.div` - width: 100%; - display: flex; - margin: ${spacing.sp4} ${spacing.sp14}; - align-items: center; -`; - -const ClickableIcon = styled.i` - self-align: flex-end; -`; - -const ServiceItem = ({ - label, - status, - alerts, -}: { - label: string, - status: Status, - alerts: Alert[], -}) => { - const intl = useIntl(); - - if (!alerts.length && status === STATUS_HEALTH) - return ( - - - - {label} - - - ); - else - return ( - - - - {intl.formatMessage({ id: 'view_details' })} - - - - {status} - - {alerts[0] && alerts[0].startsAt && ( - - -
{formatDateToMid1(alerts[0].startsAt)}
-
- )} - - } - > - - - - - {label} - - - - -
-
- ); -}; - const DashboardServices = () => { const intl = useIntl(); const alertsLibrary = useAlertLibrary(); @@ -225,12 +84,12 @@ const DashboardServices = () => { {intl.formatMessage({ id: 'core' })} - - { {intl.formatMessage({ id: 'observability' })} - - - - { {intl.formatMessage({ id: 'access' })} - - ({ }, })); +jest.mock('../containers/ConfigProvider', () => ({ + __esModule: true, + default: ({ children }) => <>{children}, + useLinkOpener: () => ({ + openLink: jest.fn(), + }), + useDiscoveredViews: () => [ + { + app: { + kind: '', + name: '', + version: '', + url: '', + appHistoryBasePath: '', + }, + isFederated: true, + view: { path: '/alerts' }, + }, + ], +})); + describe('the dashboard inventory panel', () => { test('displays the services panel and display all 8 green statuses when no alerts are present', async () => { // Have to any type jest.fn function to avoid Flow warning for mockImplementation() diff --git a/ui/src/components/HealthItem.js b/ui/src/components/HealthItem.js new file mode 100644 index 0000000000..e16a8005cd --- /dev/null +++ b/ui/src/components/HealthItem.js @@ -0,0 +1,165 @@ +import React from 'react'; +import styled from 'styled-components'; +import { useIntl } from 'react-intl'; +import { Tooltip, StatusText } from '@scality/core-ui'; +import { + spacing, + fontSize, + fontWeight, +} from '@scality/core-ui/dist/style/theme'; + +import type { Alert } from '../services/alertUtils'; +import type { Status } from '../containers/AlertProvider'; +import CircleStatus from './CircleStatus'; +import { STATUS_HEALTH } from '../constants.js'; +import { formatDateToMid1 } from '../services/utils'; +import { + useDiscoveredViews, + useLinkOpener, +} from '../containers/ConfigProvider'; +import { useHistory } from 'react-router'; + +const ServiceItemLabelWrapper = styled.div` + display: flex; + align-items: baseline; +`; + +const ServiceItemLabel = styled.div` + margin-left: ${spacing.sp8}; +`; + +const ServiceItemElement = styled.div` + padding: ${spacing.sp4}; +`; + +const ClickableServiceItemElement = styled(ServiceItemElement)` + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + + :hover { + background-color: ${(props) => props.theme.highlight}; + } +`; + +const NonHealthyServiceItemElement = styled.div` + cursor: pointer; + width: 100%; + display: flex; + flex-direction: column; + + a { + text-decoration: none; + color: inherit; + width: 100%; + display: flex; + align-items: center; + } +`; + +const NonHealthyPopUp = styled.div` + display: flex; + flex-direction: column; + font-size: ${fontSize.base}; + + label { + width: 25%; + margin-right: ${spacing.sp8}; + color: ${(props) => props.theme.textSecondary}; + text-align: right; + } +`; + +const NonHealthyPopUpTitle = styled.div` + font-weight: ${fontWeight.bold} + text-align: center; +`; + +const NonHealthyPopUpItem = styled.div` + width: 100%; + display: flex; + margin: ${spacing.sp4} ${spacing.sp14}; + align-items: center; +`; + +const ClickableIcon = styled.i` + self-align: flex-end; +`; + +const HealthItem = ({ + label, + status, + alerts, + showArrow = true, +}: { + label: string, + status: Status, + alerts: Alert[], +}) => { + const intl = useIntl(); + const { openLink } = useLinkOpener(); + const history = useHistory(); + const discoveredViews = useDiscoveredViews(); + const alertView = discoveredViews.find( + (view) => view.view.path === '/alerts', + ); + + if (!alerts.length && status === STATUS_HEALTH) + return ( + + + + {label} + + + ); + else + return ( + + + + {intl.formatMessage({ id: 'view_details' })} + + + + {status} + + {alerts[0] && alerts[0].startsAt && ( + + +
{formatDateToMid1(alerts[0].startsAt)}
+
+ )} + + } + > +
{ + openLink(alertView); + history.replace('/alerts'); + }} + data-testid="alert-link" + > + + + + {label} + + {showArrow && } + +
+
+
+ ); +}; + +export default HealthItem; diff --git a/ui/src/containers/DashboardPage.js b/ui/src/containers/DashboardPage.js index a046fefe75..f39a840020 100644 --- a/ui/src/containers/DashboardPage.js +++ b/ui/src/containers/DashboardPage.js @@ -7,6 +7,7 @@ import DashboardMetrics from '../components/DashboardMetrics'; import DashboardInventory from '../components/DashboardInventory'; import DashboardServices from '../components/DashboardServices'; import DashboardGlobalHealth from '../components/DashboardGlobalHealth'; +import DashboardNetwork from '../components/DashboardNetwork'; import { padding, spacing } from '@scality/core-ui/dist/style/theme'; import { Dropdown } from '@scality/core-ui'; @@ -57,6 +58,7 @@ const DashboardGrid = styled.div` } .network { grid-area: network; + display: flex; } .header { grid-area: header; @@ -163,7 +165,9 @@ const DashboardPage = (props: {}) => { -
Network
+
+ +
diff --git a/ui/src/translations/en.json b/ui/src/translations/en.json index 27dbf7ff4c..c60ba01fe8 100644 --- a/ui/src/translations/en.json +++ b/ui/src/translations/en.json @@ -137,6 +137,7 @@ "used_by": "Used By", "backend_disk": "Backend Disk", "metrics": "Metrics", + "network": "Network", "advanced_metrics": "Advanced Metrics", "volume_is_not_bound": "The volume is not bound yet", "no_active_alerts": "No active alerts", diff --git a/ui/src/translations/fr.json b/ui/src/translations/fr.json index 5026c70baa..751d006841 100644 --- a/ui/src/translations/fr.json +++ b/ui/src/translations/fr.json @@ -137,6 +137,7 @@ "used_by": "Utilisé Par", "backend_disk": "Backend Disk", "metrics": "Métrique", + "network": "Réseau", "advanced_metrics": "Métrique avancée", "volume_is_not_bound": "Le volume n'est pas encore lié", "no_active_alerts": "Aucune alerte active",