Skip to content

Commit

Permalink
[7.x] update copy styling (#79313) (#79593)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelolo24 committed Oct 6, 2020
1 parent c088772 commit a14c2ae
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history';

import copy from 'copy-to-clipboard';
import { noAncestorsTwoChildrenWithRelatedEventsOnOrigin } from '../data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin';
import { Simulator } from '../test_utilities/simulator';
// Extend jest with a custom matcher
Expand All @@ -14,6 +14,10 @@ import { urlSearch } from '../test_utilities/url_search';
// the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances
const resolverComponentInstanceID = 'resolverComponentInstanceID';

jest.mock('copy-to-clipboard', () => {
return jest.fn();
});

describe(`Resolver: when analyzing a tree with no ancestors and two children and two related registry event on the origin, and when the component instance ID is ${resolverComponentInstanceID}`, () => {
/**
* Get (or lazily create and get) the simulator.
Expand Down Expand Up @@ -112,6 +116,16 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and
wordBreaks: 2,
});
});
it('should allow all node details to be copied', async () => {
const copyableFields = await simulator().resolve('resolver:panel:copyable-field');

copyableFields?.map((copyableField) => {
copyableField.simulate('mouseenter');
simulator().testSubject('clipboard').last().simulate('click');
expect(copy).toHaveBeenLastCalledWith(copyableField.text(), expect.any(Object));
copyableField.simulate('mouseleave');
});
});
});

const queryStringWithFirstChildSelected = urlSearch(resolverComponentInstanceID, {
Expand Down Expand Up @@ -158,6 +172,19 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and
).toYieldEqualTo(3);
});

it('should be able to copy the timestamps for all 3 nodes', async () => {
const copyableFields = await simulator().resolve('resolver:panel:copyable-field');

expect(copyableFields?.length).toBe(3);

copyableFields?.map((copyableField) => {
copyableField.simulate('mouseenter');
simulator().testSubject('clipboard').last().simulate('click');
expect(copy).toHaveBeenLastCalledWith(copyableField.text(), expect.any(Object));
copyableField.simulate('mouseleave');
});
});

describe('when there is an item in the node list and its text has been clicked', () => {
beforeEach(async () => {
const nodeLinks = await simulator().resolve('resolver:node-list:node-link:title');
Expand Down Expand Up @@ -239,6 +266,34 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and
)
).toYieldEqualTo(2);
});
describe('and when the first event link is clicked', () => {
beforeEach(async () => {
const link = await simulator().resolve(
'resolver:panel:node-events-in-category:event-link'
);
const first = link?.first();
expect(first).toBeTruthy();

if (first) {
first.simulate('click', { button: 0 });
}
});
it('should show the event detail view', async () => {
await expect(
simulator().map(() => simulator().testSubject('resolver:panel:event-detail').length)
).toYieldEqualTo(1);
});
it('should allow all fields to be copied', async () => {
const copyableFields = await simulator().resolve('resolver:panel:copyable-field');

copyableFields?.map((copyableField) => {
copyableField.simulate('mouseenter');
simulator().testSubject('clipboard').last().simulate('click');
expect(copy).toHaveBeenLastCalledWith(copyableField.text(), expect.any(Object));
copyableField.simulate('mouseleave');
});
});
});
});
});
describe('and when the node list link has been clicked', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

/* eslint-disable react/display-name */

import { EuiToolTip, EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import React, { memo, useState, useContext } from 'react';
import { WithCopyToClipboard } from '../../../common/lib/clipboard/with_copy_to_clipboard';
import { useColors } from '../use_colors';
import { ResolverPanelContext } from './panel_context';

interface StyledCopyableField {
readonly backgroundColor: string;
readonly activeBackgroundColor: string;
}

const StyledCopyableField = styled.div<StyledCopyableField>`
background-color: ${(props) => props.backgroundColor};
border-radius: 3px;
padding: 4px;
transition: background 0.2s ease;
&:hover {
background-color: ${(props) => props.activeBackgroundColor};
color: #fff;
}
`;

export const CopyablePanelField = memo(
({ textToCopy, content }: { textToCopy: string; content: JSX.Element | string }) => {
const { linkColor, copyableBackground } = useColors();
const [isOpen, setIsOpen] = useState(false);
const panelContext = useContext(ResolverPanelContext);

const onMouseEnter = () => setIsOpen(true);

const ButtonContent = memo(() => (
<StyledCopyableField
backgroundColor={panelContext.isHoveringInPanel ? copyableBackground : 'transparent'}
data-test-subj="resolver:panel:copyable-field"
activeBackgroundColor={linkColor}
onMouseEnter={onMouseEnter}
>
{content}
</StyledCopyableField>
));

const onMouseLeave = () => setIsOpen(false);

return (
<div onMouseLeave={onMouseLeave}>
<EuiPopover
anchorPosition={'downCenter'}
button={<ButtonContent />}
closePopover={onMouseLeave}
hasArrow={false}
isOpen={isOpen}
panelPaddingSize="s"
>
<EuiToolTip
content={i18n.translate('xpack.securitySolution.resolver.panel.copyToClipboard', {
defaultMessage: 'Copy to Clipboard',
})}
>
<WithCopyToClipboard
data-test-subj="resolver:panel:copy-to-clipboard"
text={textToCopy}
titleSummary={textToCopy}
/>
</EuiToolTip>
</EuiPopover>
</div>
);
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
GeneratedText,
noTimestampRetrievedText,
} from './panel_content_utilities';
import { CopyablePanelField } from './copyable_panel_field';
import { Breadcrumbs } from './breadcrumbs';
import * as eventModel from '../../../../common/endpoint/models/event';
import * as selectors from '../../store/selectors';
Expand Down Expand Up @@ -156,7 +157,12 @@ function EventDetailFields({ event }: { event: SafeResolverEvent }) {
namespace: <GeneratedText>{key}</GeneratedText>,
descriptions: deepObjectEntries(value).map(([path, fieldValue]) => ({
title: <GeneratedText>{path.join('.')}</GeneratedText>,
description: <GeneratedText>{String(fieldValue)}</GeneratedText>,
description: (
<CopyablePanelField
textToCopy={String(fieldValue)}
content={<GeneratedText>{String(fieldValue)}</GeneratedText>}
/>
),
})),
};
returnValue.push(section);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

/* eslint-disable react/display-name */

import React, { memo } from 'react';
import React, { memo, useState } from 'react';
import { useSelector } from 'react-redux';
import * as selectors from '../../store/selectors';
import { NodeEventsInCategory } from './node_events_of_type';
Expand All @@ -15,33 +15,48 @@ import { NodeDetail } from './node_detail';
import { NodeList } from './node_list';
import { EventDetail } from './event_detail';
import { PanelViewAndParameters } from '../../types';
import { ResolverPanelContext } from './panel_context';

/**
* Show the panel that matches the `panelViewAndParameters` (derived from the browser's location.search)
*/

export const PanelRouter = memo(function () {
const params: PanelViewAndParameters = useSelector(selectors.panelViewAndParameters);
const [isHoveringInPanel, updateIsHoveringInPanel] = useState(false);

const triggerPanelHover = () => updateIsHoveringInPanel(true);
const stopPanelHover = () => updateIsHoveringInPanel(false);

/* The default 'Event List' / 'List of all processes' view */
let panelViewToRender = <NodeList />;

if (params.panelView === 'nodeDetail') {
return <NodeDetail nodeID={params.panelParameters.nodeID} />;
panelViewToRender = <NodeDetail nodeID={params.panelParameters.nodeID} />;
} else if (params.panelView === 'nodeEvents') {
return <NodeEvents nodeID={params.panelParameters.nodeID} />;
panelViewToRender = <NodeEvents nodeID={params.panelParameters.nodeID} />;
} else if (params.panelView === 'nodeEventsInCategory') {
return (
panelViewToRender = (
<NodeEventsInCategory
nodeID={params.panelParameters.nodeID}
eventCategory={params.panelParameters.eventCategory}
/>
);
} else if (params.panelView === 'eventDetail') {
return (
panelViewToRender = (
<EventDetail
nodeID={params.panelParameters.nodeID}
eventID={params.panelParameters.eventID}
eventCategory={params.panelParameters.eventCategory}
/>
);
} else {
/* The default 'Event List' / 'List of all processes' view */
return <NodeList />;
}

return (
<ResolverPanelContext.Provider value={{ isHoveringInPanel }}>
<div onMouseEnter={triggerPanelHover} onMouseLeave={stopPanelHover}>
{panelViewToRender}
</div>
</ResolverPanelContext.Provider>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { StyledDescriptionList, StyledTitle } from './styles';
import * as selectors from '../../store/selectors';
import * as eventModel from '../../../../common/endpoint/models/event';
import { GeneratedText } from './panel_content_utilities';
import { CopyablePanelField } from './copyable_panel_field';
import { Breadcrumbs } from './breadcrumbs';
import { processPath, processPID } from '../../models/process_event';
import { CubeForProcess } from './cube_for_process';
Expand Down Expand Up @@ -131,7 +132,12 @@ const NodeDetailView = memo(function ({
.map((entry) => {
return {
...entry,
description: <GeneratedText>{String(entry.description)}</GeneratedText>,
description: (
<CopyablePanelField
textToCopy={String(entry.description)}
content={<GeneratedText>{String(entry.description)}</GeneratedText>}
/>
),
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { SafeResolverEvent } from '../../../../common/endpoint/types';
import { ResolverAction } from '../../store/actions';
import { useFormattedDate } from './use_formatted_date';
import { getEmptyTagValue } from '../../../common/components/empty_value';
import { CopyablePanelField } from './copyable_panel_field';

interface ProcessTableView {
name?: string;
Expand Down Expand Up @@ -214,5 +215,9 @@ function NodeDetailLink({
const NodeDetailTimestamp = memo(({ eventDate }: { eventDate: Date | undefined }) => {
const formattedDate = useFormattedDate(eventDate);

return formattedDate ? <>{formattedDate}</> : getEmptyTagValue();
return formattedDate ? (
<CopyablePanelField textToCopy={formattedDate} content={formattedDate} />
) : (
getEmptyTagValue()
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';

export const ResolverPanelContext = React.createContext({ isHoveringInPanel: false });
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import { useMemo } from 'react';
import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public';

type ResolverColorNames =
| 'copyableBackground'
| 'descriptionText'
| 'full'
| 'graphControls'
| 'graphControlsBackground'
| 'linkColor'
| 'resolverBackground'
| 'resolverEdge'
| 'resolverEdgeText'
Expand All @@ -31,6 +33,7 @@ export function useColors(): ColorMap {
const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight;
return useMemo(() => {
return {
copyableBackground: theme.euiColorLightShade,
descriptionText: theme.euiTextColor,
full: theme.euiColorFullShade,
graphControls: theme.euiColorDarkestShade,
Expand All @@ -42,6 +45,7 @@ export function useColors(): ColorMap {
resolverEdgeText: isDarkMode ? theme.euiColorFullShade : theme.euiColorDarkShade,
triggerBackingFill: `${theme.euiColorDanger}${isDarkMode ? '1F' : '0F'}`,
pillStroke: theme.euiColorLightShade,
linkColor: theme.euiLinkColor,
};
}, [isDarkMode, theme]);
}

0 comments on commit a14c2ae

Please sign in to comment.