From 7a53d863a54455c045115b8777bdea2363cc2f3d Mon Sep 17 00:00:00 2001 From: Bruno d'Auria Date: Wed, 8 Apr 2020 18:50:00 +0200 Subject: [PATCH] enh(ui): Fixes to details panel (#8564) --- package-lock.json | 43 +++++++++++ package.json | 1 + .../Body/tabs/Details/DetailsCard/cards.tsx | 13 +++- .../Body/tabs/Details/DetailsCard/index.tsx | 22 +++--- .../Body/tabs/Details/ExpandableCard.tsx | 4 +- .../Details/Body/tabs/Details/index.tsx | 68 +++++++++++++++-- .../src/Resources/Details/index.test.tsx | 75 +++++++++++++------ www/front_src/src/Resources/Details/models.ts | 4 +- .../src/Resources/translatedLabels.ts | 3 + 9 files changed, 183 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index 13c7f0154d2..1070c91735b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3249,6 +3249,12 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, + "arch": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz", + "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==", + "dev": true + }, "are-we-there-yet": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", @@ -4320,6 +4326,15 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -5030,6 +5045,25 @@ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" }, + "clipboardy": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz", + "integrity": "sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==", + "dev": true, + "requires": { + "arch": "^2.1.1", + "execa": "^1.0.0", + "is-wsl": "^2.1.1" + }, + "dependencies": { + "is-wsl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz", + "integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==", + "dev": true + } + } + }, "cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", @@ -7438,6 +7472,12 @@ } } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, "filesize": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", @@ -11081,6 +11121,7 @@ "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1", "node-pre-gyp": "*" }, @@ -19122,6 +19163,7 @@ "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1", "node-pre-gyp": "*" }, @@ -20296,6 +20338,7 @@ "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1", "node-pre-gyp": "*" }, diff --git a/package.json b/package.json index 72b192df5c9..7f02961eb97 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "babel-merge": "^3.0.0", "cache-loader": "^4.1.0", "clean-webpack-plugin": "^3.0.0", + "clipboardy": "^2.3.0", "css-loader": "^3.4.2", "eslint-config-airbnb": "^18.1.0", "eslint-config-prettier": "^6.10.1", diff --git a/www/front_src/src/Resources/Details/Body/tabs/Details/DetailsCard/cards.tsx b/www/front_src/src/Resources/Details/Body/tabs/Details/DetailsCard/cards.tsx index 1e72f5f7634..10aa32a4e85 100644 --- a/www/front_src/src/Resources/Details/Body/tabs/Details/DetailsCard/cards.tsx +++ b/www/front_src/src/Resources/Details/Body/tabs/Details/DetailsCard/cards.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { Typography, Grid, makeStyles } from '@material-ui/core'; +import { Typography, Grid, makeStyles, Box } from '@material-ui/core'; import IconCheck from '@material-ui/icons/Check'; import { @@ -18,6 +18,7 @@ import { labelPercentStateChange, labelLastNotification, labelCurrentNotificationNumber, + labelNo, } from '../../../../../translatedLabels'; import { getFormattedDate, getFormattedTime } from '../../../../../dateTime'; import { ResourceDetails } from '../../../../models'; @@ -31,7 +32,13 @@ interface DetailCardLines { } const DetailsLine = ({ line }: { line?: string }): JSX.Element => { - return {line}; + return ( + + + {line} + + + ); }; const useStyles = makeStyles((theme) => ({ @@ -148,7 +155,7 @@ const getDetailCardLines = ( getLines: (): Lines => [ { key: 'flapping', - line: , + line: , }, ], }, diff --git a/www/front_src/src/Resources/Details/Body/tabs/Details/DetailsCard/index.tsx b/www/front_src/src/Resources/Details/Body/tabs/Details/DetailsCard/index.tsx index abec5540d73..f56e90c7f00 100644 --- a/www/front_src/src/Resources/Details/Body/tabs/Details/DetailsCard/index.tsx +++ b/www/front_src/src/Resources/Details/Body/tabs/Details/DetailsCard/index.tsx @@ -11,19 +11,15 @@ const DetailsCard = ({ title, lines }: Props): JSX.Element => { return ( - - - - {title} - - - - {lines.map(({ key, line }) => ( - - {line} - - ))} - + + {title} + + + {lines.map(({ key, line }) => ( + + {line} + + ))} diff --git a/www/front_src/src/Resources/Details/Body/tabs/Details/ExpandableCard.tsx b/www/front_src/src/Resources/Details/Body/tabs/Details/ExpandableCard.tsx index 64710647141..3a4dcbe3a82 100644 --- a/www/front_src/src/Resources/Details/Body/tabs/Details/ExpandableCard.tsx +++ b/www/front_src/src/Resources/Details/Body/tabs/Details/ExpandableCard.tsx @@ -32,7 +32,6 @@ const useStyles = makeStyles((theme) => { }), }), title: ({ severityCode }): CreateCSSProperties => ({ - marginBottom: 5, ...(severityCode && { color: getStatusBackgroundColor(severityCode) }), }), }; @@ -53,7 +52,7 @@ const ExpandableCard = ({ const [outputExpanded, setOutputExpanded] = React.useState(false); - const lines = content.split('\n'); + const lines = content.split(/\n|\\n/); const threeFirstlines = lines.slice(0, 3); const lastlines = lines.slice(2, lines.length); @@ -74,6 +73,7 @@ const ExpandableCard = ({ className={classes.title} variant="subtitle2" color="textSecondary" + gutterBottom > {title} diff --git a/www/front_src/src/Resources/Details/Body/tabs/Details/index.tsx b/www/front_src/src/Resources/Details/Body/tabs/Details/index.tsx index 886e6c88279..0071d304826 100644 --- a/www/front_src/src/Resources/Details/Body/tabs/Details/index.tsx +++ b/www/front_src/src/Resources/Details/Body/tabs/Details/index.tsx @@ -1,12 +1,26 @@ import * as React from 'react'; import { isNil } from 'ramda'; +import { write as writeToClipboard } from 'clipboardy'; -import { Grid, Card, CardContent, Typography, styled } from '@material-ui/core'; +import { + Grid, + Card, + CardContent, + Typography, + styled, + Tooltip, + IconButton, +} from '@material-ui/core'; import { Skeleton } from '@material-ui/lab'; +import IconCopyFile from '@material-ui/icons/FileCopy'; + +import { useSnackbar, Severity } from '@centreon/ui'; import ExpandableCard from './ExpandableCard'; import { + labelCopy, + labelCommand, labelStatusInformation, labelDowntimeDuration, labelFrom, @@ -14,6 +28,8 @@ import { labelAcknowledgedBy, labelAt, labelPerformanceData, + labelCommandCopied, + labelSomethingWentWrong, } from '../../../../translatedLabels'; import StateCard from './StateCard'; import { getFormattedDateTime } from '../../../../dateTime'; @@ -46,10 +62,28 @@ interface Props { } const DetailsTab = ({ details }: Props): JSX.Element => { + const { showMessage } = useSnackbar(); + if (details === undefined) { return ; } + const copyCommandLine = (): void => { + writeToClipboard(details.command_line as string) + .then(() => { + showMessage({ + message: labelCommandCopied, + severity: Severity.success, + }); + }) + .catch(() => { + showMessage({ + message: labelSomethingWentWrong, + severity: Severity.error, + }); + }); + }; + return ( @@ -108,13 +142,31 @@ const DetailsTab = ({ details }: Props): JSX.Element => { /> )} - - - - {details.check_command} - - - + {details.command_line && ( + + + + + + {labelCommand} + + + + + + + + + + {details.command_line} + + + + )} ); }; diff --git a/www/front_src/src/Resources/Details/index.test.tsx b/www/front_src/src/Resources/Details/index.test.tsx index e9282325fe7..ff7f5632062 100644 --- a/www/front_src/src/Resources/Details/index.test.tsx +++ b/www/front_src/src/Resources/Details/index.test.tsx @@ -1,8 +1,15 @@ import React from 'react'; import axios from 'axios'; +import clipboardy from 'clipboardy'; + +import { + render, + waitFor, + fireEvent, + RenderResult, +} from '@testing-library/react'; -import { render, waitFor, fireEvent } from '@testing-library/react'; import Details from '.'; import { labelMore, @@ -28,6 +35,9 @@ import { labelLast7Days, labelLast24h, labelLast31Days, + labelCopy, + labelCommand, + labelResourceFlapping, } from '../translatedLabels'; import { selectOption } from '../test'; @@ -35,6 +45,10 @@ const mockedAxios = axios as jest.Mocked; jest.mock('../icons/Downtime'); +jest.mock('clipboardy'); + +const mockedClipboardy = clipboardy as jest.Mocked; + const onClose = jest.fn(); const detailsEndpoint = '/resource'; @@ -59,7 +73,7 @@ const retrievedDetails = { timezone: 'Europe/Paris', criticality: 10, active_checks: true, - check_command: 'base_host_alive', + command_line: 'base_host_alive', last_notification: '2020-07-18T19:30', latency: 0.005, next_check: '2020-06-18T19:15', @@ -95,6 +109,19 @@ const statusGraphData = { warning: [], ok: [], critical: [], unknown: [] }; const RealDate = Date; const currentDateIsoString = '2020-06-20T20:00:00.000Z'; +const renderDetails = (): RenderResult => + render( +
, + ); + describe(Details, () => { beforeEach(() => { global.Date.now = jest.fn(() => Date.parse(currentDateIsoString)); @@ -113,15 +140,9 @@ describe(Details, () => { it('displays resource details information', async () => { mockedAxios.get.mockResolvedValueOnce({ data: retrievedDetails }); - const { getByText, queryByText, getAllByText } = render( -
, - ); + const { getByText, queryByText, getAllByText } = renderDetails(); - await waitFor(() => expect(getByText('Central')).toBeInTheDocument()); + await waitFor(() => expect(mockedAxios.get).toHaveBeenCalled()); expect(getByText('10')).toBeInTheDocument(); expect(getByText('CRITICAL')).toBeInTheDocument(); @@ -175,6 +196,9 @@ describe(Details, () => { expect(getByText(labelLatency)).toBeInTheDocument(); expect(getByText('0.005 s')).toBeInTheDocument(); + expect(getByText(labelResourceFlapping)).toBeInTheDocument(); + expect(getByText('N/A')).toBeInTheDocument(); + expect(getByText(labelPercentStateChange)).toBeInTheDocument(); expect(getByText('3.5%')).toBeInTheDocument(); @@ -192,6 +216,7 @@ describe(Details, () => { ), ).toBeInTheDocument(); + expect(getByText(labelCommand)).toBeInTheDocument(); expect(getByText('base_host_alive')).toBeInTheDocument(); }); @@ -205,19 +230,9 @@ describe(Details, () => { .mockResolvedValueOnce({ data: performanceGraphData }) .mockResolvedValueOnce({ data: statusGraphData }); - const { getByText } = render( -
, - ); + const { getByText } = renderDetails(); - await waitFor(() => expect(getByText('Central')).toBeInTheDocument()); + await waitFor(() => expect(mockedAxios.get).toHaveBeenCalled()); fireEvent.click(getByText(labelGraph)); @@ -233,4 +248,20 @@ describe(Details, () => { ); }), ); + + it('copies the command line to clipboard when the copy button is clicked', async () => { + const { getByTitle } = renderDetails(); + + mockedClipboardy.write.mockResolvedValue(); + + await waitFor(() => expect(mockedAxios.get).toHaveBeenCalled()); + + fireEvent.click(getByTitle(labelCopy)); + + await waitFor(() => + expect(mockedClipboardy.write).toHaveBeenCalledWith( + retrievedDetails.command_line, + ), + ); + }); }); diff --git a/www/front_src/src/Resources/Details/models.ts b/www/front_src/src/Resources/Details/models.ts index 19af307b106..25117e96508 100644 --- a/www/front_src/src/Resources/Details/models.ts +++ b/www/front_src/src/Resources/Details/models.ts @@ -19,10 +19,10 @@ export interface ResourceDetails { active_checks: boolean; execution_time: number; latency: number; - flapping: boolean; + flapping?: boolean; percent_state_change: number; last_notification: string; notification_number: number; performance_data?: string; - check_command: string; + command_line?: string; } diff --git a/www/front_src/src/Resources/translatedLabels.ts b/www/front_src/src/Resources/translatedLabels.ts index 099a52be49d..eeac0a2d8d4 100644 --- a/www/front_src/src/Resources/translatedLabels.ts +++ b/www/front_src/src/Resources/translatedLabels.ts @@ -16,6 +16,9 @@ export const labelClearAll = I18n.t('Clear all'); export const labelCriterias = I18n.t('Criterias'); export const labelCritical = I18n.t('Critical'); export const labelCheckDuration = I18n.t('Check duration'); +export const labelCommand = I18n.t('Command'); +export const labelCopy = I18n.t('Copy'); +export const labelCommandCopied = I18n.t('Command copied to clipboard'); export const labelCurrentNotificationNumber = I18n.t( 'Current notification number', );