Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update copy styling #79313

Merged
merged 6 commits into from
Oct 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]);
}