mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[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:
parent
d80890467f
commit
170fa429ae
10 changed files with 112 additions and 28 deletions
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {},
|
||||
}),
|
||||
}));
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ export const useStyles = () => {
|
|||
marginBottom: size.m,
|
||||
fontSize: size.m,
|
||||
fontWeight: font.weight.bold,
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
|
||||
const dataInfo: CSSObject = {
|
||||
|
|
|
@ -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={
|
||||
<>
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue