[Kubernetes Security][Bug Fix] Container Images widget bug fix (#136696)

* added fix for issue 136575

* padding and font updates

* added copy to clipboard button, pr comments

* fix check type

* pr comments

* PR comments

* fix check fail

* Add more media queries to adjust flex item margins on smaller screens

Co-authored-by: Jack <zizhou.wang@elastic.co>
This commit is contained in:
Rickyanto Ang 2022-07-25 09:10:51 -07:00 committed by GitHub
parent d80890467f
commit 170fa429ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 112 additions and 28 deletions

View file

@ -95,14 +95,14 @@ export const COUNT_WIDGET_CONTAINER_IMAGES = i18n.translate(
export const CONTAINER_NAME_SESSION = i18n.translate(
'xpack.kubernetesSecurity.containerNameWidget.containerImage',
{
defaultMessage: 'Container Images Session',
defaultMessage: 'Container images',
}
);
export const CONTAINER_NAME_SESSION_COUNT_COLUMN = i18n.translate(
'xpack.kubernetesSecurity.containerNameWidget.containerImageCountColumn',
{
defaultMessage: 'Count',
defaultMessage: 'Session count',
}
);

View file

@ -13,6 +13,7 @@ import { fireEvent } from '@testing-library/react';
const TEST_NAME = 'TEST ROW';
const TEST_BUTTON_FILTER = <div>Filter In</div>;
const TEST_BUTTON_FILTER_OUT = <div>Filter Out</div>;
const TEST_BUTTON_COPY = <div>Copy</div>;
describe('ContainerNameRow component with valid row', () => {
let renderResult: ReturnType<typeof render>;
@ -23,6 +24,7 @@ describe('ContainerNameRow component with valid row', () => {
name={TEST_NAME}
filterButtonIn={TEST_BUTTON_FILTER}
filterButtonOut={TEST_BUTTON_FILTER_OUT}
copyToClipboardButton={TEST_BUTTON_COPY}
/>
));
@ -32,6 +34,7 @@ describe('ContainerNameRow component with valid row', () => {
fireEvent.mouseOver(renderResult.queryByText(TEST_NAME)!);
expect(renderResult.getByText('Filter In')).toBeVisible();
expect(renderResult.getByText('Filter Out')).toBeVisible();
expect(renderResult.getByText('Copy')).toBeVisible();
});
it('should show the row element but not the pop up filter button outside mouse hover', async () => {
@ -39,5 +42,6 @@ describe('ContainerNameRow component with valid row', () => {
expect(renderResult.getByText(TEST_NAME)).toBeVisible();
expect(renderResult.queryByText('Filter In')).toBeFalsy();
expect(renderResult.queryByText('Filter Out')).toBeFalsy();
expect(renderResult.queryByText('Copy')).toBeFalsy();
});
});

View file

@ -13,6 +13,7 @@ export interface ContainerNameRowDeps {
name: string;
filterButtonIn?: ReactNode;
filterButtonOut?: ReactNode;
copyToClipboardButton?: ReactNode;
}
export const ROW_TEST_ID = 'kubernetesSecurity:containerNameSessionRow';
@ -21,6 +22,7 @@ export const ContainerNameRow = ({
name,
filterButtonIn,
filterButtonOut,
copyToClipboardButton,
}: ContainerNameRowDeps) => {
const [isHover, setIsHover] = useState<boolean>(false);
@ -31,13 +33,15 @@ export const ContainerNameRow = ({
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
data-test-subj={ROW_TEST_ID}
css={styles.flexWidth}
>
<EuiText size="xs" css={styles.dataInfo}>
{name}
<div css={styles.truncate}>{name}</div>
{isHover && (
<div css={styles.filters}>
{filterButtonIn}
{filterButtonOut}
{copyToClipboardButton}
</div>
)}
</EuiText>

View file

@ -21,7 +21,7 @@ import { ROW_TEST_ID } from './container_name_row';
const TABLE_SORT_BUTTON_ID = 'tableHeaderSortButton';
const TITLE = 'Container Images Session';
const TITLE = 'Container images';
const GLOBAL_FILTER: GlobalFilter = {
endDate: '2022-06-15T14:15:25.777Z',
filterQuery: '{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}',
@ -64,6 +64,7 @@ jest.mock('../../hooks/use_filter', () => ({
useSetFilter: () => ({
getFilterForValueButton: jest.fn(),
getFilterOutValueButton: jest.fn(),
getCopyButton: jest.fn(),
filterManager: {},
}),
}));

View file

@ -19,6 +19,7 @@ import {
CONTAINER_NAME_SESSION_COUNT_COLUMN,
CONTAINER_NAME_SESSION_ARIA_LABEL,
} from '../../../common/translations';
import { addCommasToNumber } from '../../utils/add_commas_to_number';
export const LOADING_TEST_ID = 'kubernetesSecurity:containerNameWidgetLoading';
export const NAME_COLUMN_TEST_ID = 'kubernetesSecurity:containerImageNameSessionNameColumn';
@ -35,7 +36,7 @@ export interface ContainerNameWidgetDataValueMap {
export interface ContainerNameArrayDataValue {
name: string;
count: number;
count: string;
}
export interface ContainerNameWidgetDeps {
@ -51,6 +52,10 @@ interface FilterButtons {
filterOutButtons: ReactNode[];
}
interface CopyButtons {
copyButtons: ReactNode[];
}
export const ContainerNameWidget = ({
widgetKey,
indexPattern,
@ -95,7 +100,8 @@ export const ContainerNameWidget = ({
enableAllColumns: true,
};
const { getFilterForValueButton, getFilterOutValueButton, filterManager } = useSetFilter();
const { getFilterForValueButton, getFilterOutValueButton, getCopyButton, filterManager } =
useSetFilter();
const filterButtons = useMemo((): FilterButtons => {
const result: FilterButtons = {
filterForButtons:
@ -137,6 +143,27 @@ export const ContainerNameWidget = ({
return result;
}, [data, getFilterForValueButton, getFilterOutValueButton, filterManager]);
const copyToClipboardButtons = useMemo((): CopyButtons => {
const result: CopyButtons = {
copyButtons:
data?.pages
?.map((aggsData) => {
return aggsData?.buckets.map((aggData) => {
return getCopyButton({
field: CONTAINER_IMAGE_NAME,
size: 'xs',
onClick: () => {},
ownFocus: false,
showTooltip: true,
value: aggData.key as string,
});
});
})
.flat() || [],
};
return result;
}, [data, getCopyButton]);
const containerNameArray = useMemo((): ContainerNameArrayDataValue[] => {
return data
? data?.pages
@ -144,7 +171,7 @@ export const ContainerNameWidget = ({
return aggsData?.buckets.map((aggData) => {
return {
name: aggData.key as string,
count: aggData.count_by_aggs.value,
count: addCommasToNumber(aggData.count_by_aggs.value),
};
});
})
@ -162,23 +189,23 @@ export const ContainerNameWidget = ({
const indexHelper = containerNameArray.findIndex((obj) => {
return obj.name === name;
});
return (
<ContainerNameRow
name={name}
filterButtonIn={filterButtons.filterForButtons[indexHelper]}
filterButtonOut={filterButtons.filterOutButtons[indexHelper]}
copyToClipboardButton={copyToClipboardButtons.copyButtons[indexHelper]}
/>
);
},
align: 'left',
width: '74%',
width: '67%',
sortable: false,
},
{
field: 'count',
name: CONTAINER_NAME_SESSION_COUNT_COLUMN,
width: '26%',
width: '33%',
'data-test-subj': COUNT_COLUMN_TEST_ID,
render: (count: number) => {
return <span css={styles.countValue}>{count}</span>;
@ -187,7 +214,13 @@ export const ContainerNameWidget = ({
align: 'right',
},
];
}, [filterButtons.filterForButtons, filterButtons.filterOutButtons, containerNameArray, styles]);
}, [
filterButtons.filterForButtons,
filterButtons.filterOutButtons,
copyToClipboardButtons.copyButtons,
containerNameArray,
styles,
]);
const scrollerRef = useRef<HTMLDivElement>(null);
useScroll({
@ -199,6 +232,12 @@ export const ContainerNameWidget = ({
},
});
const cellProps = () => {
return {
css: styles.cellPad,
};
};
return (
<div
data-test-subj={CONTAINER_NAME_TABLE_TEST_ID}
@ -220,6 +259,7 @@ export const ContainerNameWidget = ({
columns={columns}
sorting={sorting}
onChange={onTableChange}
cellProps={cellProps}
/>
</div>
);

View file

@ -14,24 +14,25 @@ export const useStyles = () => {
const { euiTheme } = useEuiTheme();
const cached = useMemo(() => {
const { size, font, colors } = euiTheme;
const { size, colors } = euiTheme;
const container: CSSObject = {
padding: size.base,
paddingTop: size.s,
paddingBottom: size.s,
paddingRight: size.base,
paddingLeft: size.base,
border: euiTheme.border.thin,
borderRadius: euiTheme.border.radius.medium,
overflow: 'auto',
height: '100%',
minHeight: '250px',
height: '239px',
position: 'relative',
marginBottom: size.l,
};
const dataInfo: CSSObject = {
marginBottom: size.xs,
display: 'flex',
alignItems: 'center',
height: size.l,
height: size.base,
position: 'relative',
};
@ -44,10 +45,28 @@ export const useStyles = () => {
border: euiTheme.border.thin,
bottom: '-25px',
boxShadow: `0 ${size.xs} ${size.xs} ${transparentize(euiTheme.colors.shadow, 0.04)}`,
display: 'flex',
zIndex: 1,
};
const countValue: CSSObject = {
fontWeight: font.weight.semiBold,
fontSize: size.m,
};
const truncate: CSSObject = {
width: '100%',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
};
const flexWidth: CSSObject = {
width: '100%',
};
const cellPad: CSSObject = {
paddingBottom: '5px',
paddingTop: '5px',
};
return {
@ -55,6 +74,9 @@ export const useStyles = () => {
dataInfo,
filters,
countValue,
truncate,
flexWidth,
cellPad,
};
}, [euiTheme]);

View file

@ -28,6 +28,7 @@ export const useStyles = () => {
marginBottom: size.m,
fontSize: size.m,
fontWeight: font.weight.bold,
whiteSpace: 'nowrap',
};
const dataInfo: CSSObject = {

View file

@ -107,7 +107,7 @@ const KubernetesSecurityRoutesComponent = ({
</EuiFlexGroup>
{!shouldHideCharts && (
<>
<EuiFlexGroup>
<EuiFlexGroup css={styles.widgetsGroup}>
<EuiFlexItem css={styles.leftWidgetsGroup}>
<EuiFlexGroup css={styles.countWidgetsGroup}>
<EuiFlexItem>
@ -157,7 +157,7 @@ const KubernetesSecurityRoutesComponent = ({
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup css={styles.widgetsBottomSpacing}>
<EuiFlexItem css={styles.noBottomSpacing}>
<EuiFlexItem>
<PercentWidget
title={
<>
@ -209,7 +209,7 @@ const KubernetesSecurityRoutesComponent = ({
onReduce={onReduceInteractiveAggs}
/>
</EuiFlexItem>
<EuiFlexItem css={styles.noBottomSpacing}>
<EuiFlexItem>
<PercentWidget
title={
<>

View file

@ -52,21 +52,25 @@ export const useStyles = () => {
marginBottom: size.m,
};
const noBottomSpacing: CSSObject = {
marginBottom: 0,
};
const countWidgetsGroup: CSSObject = {
...widgetsBottomSpacing,
flexWrap: 'wrap',
[`@media (max-width:${euiTheme.breakpoint.xl}px)`]: {
flexDirection: 'column',
},
};
const leftWidgetsGroup: CSSObject = {
...noBottomSpacing,
[`@media (max-width:${euiTheme.breakpoint.xl}px)`]: {
marginBottom: '0 !important',
},
minWidth: `calc(70% - ${size.xxxl})`,
};
const rightWidgetsGroup: CSSObject = {
[`@media (max-width:${euiTheme.breakpoint.xl}px)`]: {
marginTop: '0 !important',
},
minWidth: '30%',
};
@ -86,6 +90,12 @@ export const useStyles = () => {
lineHeight: size.base,
};
const widgetsGroup: CSSObject = {
[`@media (max-width:${euiTheme.breakpoint.xl}px)`]: {
flexDirection: 'column',
},
};
return {
titleSection,
titleActions,
@ -97,8 +107,8 @@ export const useStyles = () => {
rightWidgetsGroup,
widgetsBottomSpacing,
percentageChartTitle,
noBottomSpacing,
widgetHolder,
widgetsGroup,
};
}, [euiTheme]);

View file

@ -12,13 +12,15 @@ import type { StartPlugins } from '../types';
export const useSetFilter = () => {
const { data, timelines } = useKibana<CoreStart & StartPlugins>().services;
const { getFilterForValueButton, getFilterOutValueButton } = timelines.getHoverActions();
const { getFilterForValueButton, getFilterOutValueButton, getCopyButton } =
timelines.getHoverActions();
const filterManager = useMemo(() => data.query.filterManager, [data.query.filterManager]);
return {
getFilterForValueButton,
getFilterOutValueButton,
getCopyButton,
filterManager,
};
};