[8.8] Kubernetes dashboard fixes/improvements (#158605) (#158885)

# Backport

This will backport the following commits from `main` to `8.8`:
- [Kubernetes dashboard fixes/improvements
(#158605)](https://github.com/elastic/kibana/pull/158605)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Karl
Godard","email":"karl.godard@elastic.co"},"sourceCommit":{"committedDate":"2023-06-01T20:53:52Z","message":"Kubernetes
dashboard fixes/improvements (#158605)\n\n## Summary\r\n\r\nFixes a
number of issues on the Kubernetes dashboard in Security.\r\n\r\n**Bug
fixes:**\r\n- Fixed an issue where a default query was being added in a
useMemo\r\nwhich would override the global query causing all charts to
never be\r\nfiltered by the search bar above.\r\n- The empty state would
sometimes prevent new search requests from\r\nrunning which would force
the user to do a full page refresh\r\n- i18n formatting fix ups\r\n-
Includes an update to the cloud_defend regex validation patterns
for\r\ncontainerImageName and
containerImageFullName\r\n\r\n**Enhancements:**\r\n- Tree view selection
remembered (via useLocalStorage)\r\n\r\n**Deprecations**\r\n- Removed
\"responder console\" code from k8s dashboard. The k8smd service\r\nis
slated to be deprecated from Endpoint.\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] Any UI
touched in this PR is usable by keyboard only (learn more\r\nabout
[keyboard accessibility](https://webaim.org/techniques/keyboard/))\r\n-
[x] Any UI touched in this PR does not create any new axe
failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[x] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[x] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[x] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n---------\r\n\r\nCo-authored-by:
sec_cloudnative_integrations
<sec-cloudnative-integrations@elastic.co>","sha":"432a5d7734f127c321f75679be913a87bc3c71a6","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","backport:prev-minor","Team:
Cloud Native
Integrations","v8.9.0","v8.8.1"],"number":158605,"url":"https://github.com/elastic/kibana/pull/158605","mergeCommit":{"message":"Kubernetes
dashboard fixes/improvements (#158605)\n\n## Summary\r\n\r\nFixes a
number of issues on the Kubernetes dashboard in Security.\r\n\r\n**Bug
fixes:**\r\n- Fixed an issue where a default query was being added in a
useMemo\r\nwhich would override the global query causing all charts to
never be\r\nfiltered by the search bar above.\r\n- The empty state would
sometimes prevent new search requests from\r\nrunning which would force
the user to do a full page refresh\r\n- i18n formatting fix ups\r\n-
Includes an update to the cloud_defend regex validation patterns
for\r\ncontainerImageName and
containerImageFullName\r\n\r\n**Enhancements:**\r\n- Tree view selection
remembered (via useLocalStorage)\r\n\r\n**Deprecations**\r\n- Removed
\"responder console\" code from k8s dashboard. The k8smd service\r\nis
slated to be deprecated from Endpoint.\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] Any UI
touched in this PR is usable by keyboard only (learn more\r\nabout
[keyboard accessibility](https://webaim.org/techniques/keyboard/))\r\n-
[x] Any UI touched in this PR does not create any new axe
failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[x] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[x] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[x] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n---------\r\n\r\nCo-authored-by:
sec_cloudnative_integrations
<sec-cloudnative-integrations@elastic.co>","sha":"432a5d7734f127c321f75679be913a87bc3c71a6"}},"sourceBranch":"main","suggestedTargetBranches":["8.8"],"targetPullRequestStates":[{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/158605","number":158605,"mergeCommit":{"message":"Kubernetes
dashboard fixes/improvements (#158605)\n\n## Summary\r\n\r\nFixes a
number of issues on the Kubernetes dashboard in Security.\r\n\r\n**Bug
fixes:**\r\n- Fixed an issue where a default query was being added in a
useMemo\r\nwhich would override the global query causing all charts to
never be\r\nfiltered by the search bar above.\r\n- The empty state would
sometimes prevent new search requests from\r\nrunning which would force
the user to do a full page refresh\r\n- i18n formatting fix ups\r\n-
Includes an update to the cloud_defend regex validation patterns
for\r\ncontainerImageName and
containerImageFullName\r\n\r\n**Enhancements:**\r\n- Tree view selection
remembered (via useLocalStorage)\r\n\r\n**Deprecations**\r\n- Removed
\"responder console\" code from k8s dashboard. The k8smd service\r\nis
slated to be deprecated from Endpoint.\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] Any UI
touched in this PR is usable by keyboard only (learn more\r\nabout
[keyboard accessibility](https://webaim.org/techniques/keyboard/))\r\n-
[x] Any UI touched in this PR does not create any new axe
failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[x] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[x] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[x] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n---------\r\n\r\nCo-authored-by:
sec_cloudnative_integrations
<sec-cloudnative-integrations@elastic.co>","sha":"432a5d7734f127c321f75679be913a87bc3c71a6"}},{"branch":"8.8","label":"v8.8.1","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Karl Godard 2023-06-01 20:02:16 -07:00 committed by GitHub
parent 958823469a
commit d2aab8fc2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 128 additions and 519 deletions

View file

@ -399,6 +399,9 @@ describe('<ControlGeneralViewSelector />', () => {
if (el) {
userEvent.type(el, 'docker.io/nginx{enter}');
userEvent.type(el, 'docker.io/nginx-dev{enter}');
userEvent.type(el, 'docker.io/nginx.dev{enter}');
userEvent.type(el, '127.0.0.1:8080/nginx_dev{enter}');
} else {
throw new Error("Can't find input");
}
@ -409,7 +412,7 @@ describe('<ControlGeneralViewSelector />', () => {
expect(findByText(regexError)).toMatchObject({});
userEvent.type(el, 'nginx{enter}');
updatedSelector = onChange.mock.calls[2][0];
updatedSelector = onChange.mock.calls[5][0];
rerender(<WrappedComponent selector={updatedSelector} />);
expect(getByText(regexError)).toBeTruthy();
@ -506,7 +509,8 @@ describe('<ControlGeneralViewSelector />', () => {
throw new Error("Can't find input");
}
const expectedError = '"containerImageName" values must match the pattern: /^[a-z0-9]+$/';
const expectedError =
'"containerImageName" values must match the pattern: /^([a-z0-9]+(?:[._-][a-z0-9]+)*)$/';
expect(getByText(expectedError)).toBeTruthy();
});

View file

@ -118,7 +118,7 @@
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[a-z0-9]+$"
"pattern": "^([a-z0-9]+(?:[._-][a-z0-9]+)*)$"
}
},
"containerImageTag": {
@ -133,7 +133,7 @@
"minItems": 1,
"items": {
"type": "string",
"pattern": "^(?:\\[[a-fA-F0-9:]+\\]|(?:[a-zA-Z0-9-](?:\\.[a-z0-9]+)*)+)(?::[0-9]+)?(?:\\/[a-z0-9]+)+$"
"pattern": "^(?:\\[[a-fA-F0-9:]+\\]|(?:[a-zA-Z0-9-](?:\\.[a-z0-9]+)*)+)(?::[0-9]+)?(?:\\/[a-z0-9]+(?:[._-][a-z0-9]+)*)+$"
}
},
"kubernetesClusterId": {
@ -261,7 +261,7 @@
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[a-z0-9]+$"
"pattern": "^[a-z0-9]+(?:[._-][a-z0-9]+)*$"
}
},
"containerImageTag": {
@ -276,7 +276,7 @@
"minItems": 1,
"items": {
"type": "string",
"pattern": "^(?:\\[[a-fA-F0-9:]+\\]|(?:[a-zA-Z0-9-](?:\\.[a-z0-9]+)*)+)(?::[0-9]+)?(?:\\/[a-z0-9]+)+$"
"pattern": "^(?:\\[[a-fA-F0-9:]+\\]|(?:[a-zA-Z0-9-](?:\\.[a-z0-9]+)*)+)(?::[0-9]+)?(?:\\/[a-z0-9]+(?:[._-][a-z0-9]+)*)+$"
}
},
"kubernetesClusterId": {

View file

@ -77,7 +77,7 @@ describe('<Policies />', () => {
expect(screen.getByText(error.message)).toBeInTheDocument();
});
it('renders the benchmarks table', () => {
it('renders the policies table', () => {
renderPolicies(
createReactQueryResponse({
status: 'success',

View file

@ -104,13 +104,13 @@ export const SelectorConditionsMap: SelectorConditionsMapProps = {
containerImageFullName: {
type: 'stringArray',
pattern:
'^(?:\\[[a-fA-F0-9:]+\\]|(?:[a-zA-Z0-9-](?:\\.[a-z0-9]+)*)+)(?::[0-9]+)?(?:\\/[a-z0-9]+)+$',
'^(?:\\[[a-fA-F0-9:]+\\]|(?:[a-zA-Z0-9-](?:\\.[a-z0-9]+)*)+)(?::[0-9]+)?(?:\\/[a-z0-9]+(?:[._-][a-z0-9]+)*)+$',
patternError: i18n.errorInvalidFullContainerImageName,
not: ['containerImageName'],
},
containerImageName: {
type: 'stringArray',
pattern: '^[a-z0-9]+$',
pattern: '^([a-z0-9]+(?:[._-][a-z0-9]+)*)$',
not: ['containerImageFullName'],
},
containerImageTag: { type: 'stringArray' },

View file

@ -8,6 +8,7 @@
export const KUBERNETES_PATH = '/kubernetes' as const;
export const KUBERNETES_TITLE = 'Kubernetes';
export const LOCAL_STORAGE_HIDE_WIDGETS_KEY = 'kubernetesSecurity:shouldHideWidgets';
export const LOCAL_STORAGE_TREE_NAV_KEY = 'kubernetesSecurity:treeNavSelection';
export const AGGREGATE_ROUTE = '/internal/kubernetes_security/aggregate';
export const COUNT_ROUTE = '/internal/kubernetes_security/count';
@ -44,20 +45,24 @@ export const COUNT_WIDGET_KEY_NODES = 'CountNodesWidgets';
export const COUNT_WIDGET_KEY_PODS = 'CountPodsWidgets';
export const COUNT_WIDGET_KEY_CONTAINER_IMAGES = 'CountContainerImagesWidgets';
export const DEFAULT_QUERY = '{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}';
export const DEFAULT_KUBERNETES_FILTER_QUERY =
'{"bool":{"must":[],"filter":[{"bool": {"should": [{"exists": {"field": "orchestrator.cluster.id"}}]}}],"should":[],"must_not":[]}}';
export const DEFAULT_FILTER_QUERY =
'{"bool":{"must":[],"filter":[{"bool": {"should": [{"exists": {"field": "process.entry_leader.entity_id"}}]}}],"should":[],"must_not":[]}}';
export const DEFAULT_FILTER = {
bool: {
should: [
{
exists: {
field: ENTRY_LEADER_ENTITY_ID,
field: ORCHESTRATOR_CLUSTER_ID,
},
},
],
minimum_should_match: 1,
},
};
export const DEFAULT_FILTER_QUERY = JSON.stringify({
bool: {
must: [],
filter: [DEFAULT_FILTER],
should: [],
must_not: [],
},
});

View file

@ -62,14 +62,14 @@ export const TREE_NAVIGATION_SHOW_MORE = (name: string) =>
export const TREE_NAVIGATION_COLLAPSE = i18n.translate(
'xpack.kubernetesSecurity.treeNavigation.collapse',
{
defaultMessage: 'Collapse Tree Navigation',
defaultMessage: 'Collapse tree navigation',
}
);
export const TREE_NAVIGATION_EXPAND = i18n.translate(
'xpack.kubernetesSecurity.treeNavigation.expand',
{
defaultMessage: 'Expand Tree Navigation',
defaultMessage: 'Expand tree navigation',
}
);
@ -106,14 +106,14 @@ export const COUNT_WIDGET_PODS = i18n.translate('xpack.kubernetesSecurity.countW
export const COUNT_WIDGET_CONTAINER_IMAGES = i18n.translate(
'xpack.kubernetesSecurity.countWidget.containerImages',
{
defaultMessage: 'Container Images',
defaultMessage: 'Container images',
}
);
export const CONTAINER_NAME_SESSION = i18n.translate(
'xpack.kubernetesSecurity.containerNameWidget.containerImage',
{
defaultMessage: 'Container images',
defaultMessage: 'Container image',
}
);
@ -127,6 +127,6 @@ export const CONTAINER_NAME_SESSION_COUNT_COLUMN = i18n.translate(
export const CONTAINER_NAME_SESSION_ARIA_LABEL = i18n.translate(
'xpack.kubernetesSecurity.containerNameWidget.containerImageAriaLabel',
{
defaultMessage: 'Container Name Session Widget',
defaultMessage: 'Container name session widget',
}
);

View file

@ -21,7 +21,7 @@ import { ROW_TEST_ID } from './container_name_row';
const TABLE_SORT_BUTTON_ID = 'tableHeaderSortButton';
const TITLE = 'Container images';
const TITLE = 'Container image';
const GLOBAL_FILTER: GlobalFilter = {
endDate: '2022-06-15T14:15:25.777Z',
filterQuery: '{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}',

View file

@ -5,14 +5,14 @@
* 2.0.
*/
import { DEFAULT_QUERY } from '../../../common/constants';
import { DEFAULT_FILTER_QUERY } from '../../../common/constants';
import { QueryDslQueryContainerBool } from '../../types';
export const addResourceTypeToFilterQuery = (
filterQuery: string | undefined,
resourceType: 'node' | 'pod'
) => {
let validFilterQuery = DEFAULT_QUERY;
let validFilterQuery = DEFAULT_FILTER_QUERY;
try {
const parsedFilterQuery: QueryDslQueryContainerBool = JSON.parse(filterQuery || '{}');
@ -32,7 +32,7 @@ export const addResourceTypeToFilterQuery = (
});
validFilterQuery = JSON.stringify(parsedFilterQuery);
} catch {
// no-op since validFilterQuery is initialized to be DEFAULT_QUERY
// no-op since validFilterQuery is initialized to be DEFAULT_FILTER_QUERY
}
return validFilterQuery;

View file

@ -53,11 +53,6 @@ const renderWithRouter = (
},
};
});
const responseActionButtonProps = {
tooltip: { content: 'test' },
isDisabled: false,
canAccessResponseConsole: true,
};
const mockedContext = createAppRootMockRenderer();
return mockedContext.render(
<MemoryRouter initialEntries={initialEntries}>
@ -68,10 +63,7 @@ const renderWithRouter = (
startDate: '2022-03-08T18:52:15.532Z',
endDate: '2022-06-09T17:52:15.532Z',
}}
responseActionButtonProps={responseActionButtonProps}
responseActionClick={jest.fn()}
renderSessionsView={jest.fn()}
handleTreeNavSelection={jest.fn()}
/>
</MemoryRouter>
);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useMemo } from 'react';
import React, { useCallback } from 'react';
import { Switch } from 'react-router-dom';
import { Route } from '@kbn/shared-ux-router';
import useLocalStorage from 'react-use/lib/useLocalStorage';
@ -36,11 +36,10 @@ import {
COUNT_WIDGET_KEY_NAMESPACE,
COUNT_WIDGET_KEY_NODES,
COUNT_WIDGET_KEY_CONTAINER_IMAGES,
DEFAULT_KUBERNETES_FILTER_QUERY,
} from '../../../common/constants';
import { PercentWidget } from '../percent_widget';
import { CountWidget } from '../count_widget';
import { GlobalFilter, KubernetesSecurityDeps } from '../../types';
import { KubernetesSecurityDeps } from '../../types';
import { AggregateResult } from '../../../common/types/aggregate';
import { useLastUpdated } from '../../hooks';
import { useStyles } from './styles';
@ -61,9 +60,6 @@ const KubernetesSecurityRoutesComponent = ({
indexPattern,
globalFilter,
renderSessionsView,
responseActionClick,
handleTreeNavSelection,
responseActionButtonProps,
}: KubernetesSecurityDeps) => {
const [shouldHideCharts, setShouldHideCharts] = useLocalStorage(
LOCAL_STORAGE_HIDE_WIDGETS_KEY,
@ -71,17 +67,6 @@ const KubernetesSecurityRoutesComponent = ({
);
const styles = useStyles();
const lastUpdated = useLastUpdated(globalFilter);
const globalFilterForKubernetes: GlobalFilter = useMemo(() => {
return {
...globalFilter,
filterQuery: JSON.stringify({
...JSON.parse(globalFilter?.filterQuery ?? ''),
...JSON.parse(DEFAULT_KUBERNETES_FILTER_QUERY),
}),
};
}, [globalFilter]);
const onReduceInteractiveAggs = useCallback(
(result: AggregateResult): Record<string, number> =>
result.buckets.reduce((groupedByKeyValue, aggregate) => {
@ -140,7 +125,7 @@ const KubernetesSecurityRoutesComponent = ({
<CountWidget
title={COUNT_WIDGET_CLUSTERS}
indexPattern={indexPattern}
globalFilter={globalFilterForKubernetes}
globalFilter={globalFilter}
widgetKey={COUNT_WIDGET_KEY_CLUSTERS}
groupedBy={ORCHESTRATOR_CLUSTER_ID}
/>
@ -149,7 +134,7 @@ const KubernetesSecurityRoutesComponent = ({
<CountWidget
title={COUNT_WIDGET_NAMESPACE}
indexPattern={indexPattern}
globalFilter={globalFilterForKubernetes}
globalFilter={globalFilter}
widgetKey={COUNT_WIDGET_KEY_NAMESPACE}
groupedBy={ORCHESTRATOR_NAMESPACE}
/>
@ -158,7 +143,7 @@ const KubernetesSecurityRoutesComponent = ({
<CountWidget
title={COUNT_WIDGET_NODES}
indexPattern={indexPattern}
globalFilter={globalFilterForKubernetes}
globalFilter={globalFilter}
widgetKey={COUNT_WIDGET_KEY_NODES}
groupedBy={CLOUD_INSTANCE_NAME}
/>
@ -167,7 +152,7 @@ const KubernetesSecurityRoutesComponent = ({
<CountWidget
title={COUNT_WIDGET_PODS}
indexPattern={indexPattern}
globalFilter={globalFilterForKubernetes}
globalFilter={globalFilter}
widgetKey={COUNT_WIDGET_KEY_NODES}
groupedBy={ORCHESTRATOR_RESOURCE_ID}
/>
@ -176,7 +161,7 @@ const KubernetesSecurityRoutesComponent = ({
<CountWidget
title={COUNT_WIDGET_CONTAINER_IMAGES}
indexPattern={indexPattern}
globalFilter={globalFilterForKubernetes}
globalFilter={globalFilter}
widgetKey={COUNT_WIDGET_KEY_CONTAINER_IMAGES}
groupedBy={CONTAINER_IMAGE_NAME}
/>
@ -190,7 +175,7 @@ const KubernetesSecurityRoutesComponent = ({
<EuiText size="xs" css={styles.percentageChartTitle}>
<FormattedMessage
id="xpack.kubernetesSecurity.sessionChart.title"
defaultMessage="Session Interactivity"
defaultMessage="Session interactivity"
/>
</EuiText>
<EuiIconTip
@ -206,7 +191,7 @@ const KubernetesSecurityRoutesComponent = ({
}
widgetKey="sessionsPercentage"
indexPattern={indexPattern}
globalFilter={globalFilterForKubernetes}
globalFilter={globalFilter}
dataValueMap={{
true: {
name: i18n.translate(
@ -242,14 +227,14 @@ const KubernetesSecurityRoutesComponent = ({
<EuiText size="xs" css={styles.percentageChartTitle}>
<FormattedMessage
id="xpack.kubernetesSecurity.entryUserChart.title"
defaultMessage="Session Entry Users"
defaultMessage="Entry session users"
/>
</EuiText>
<EuiIconTip
content={
<FormattedMessage
id="xpack.kubernetesSecurity.entryUserChart.tooltip"
defaultMessage="The session user is the initial Linux user associated
defaultMessage="The entry session user is the initial Linux user associated
with the session. This user may be set from authentication of a remote
login or automatically for service sessions started by init."
/>
@ -259,7 +244,7 @@ const KubernetesSecurityRoutesComponent = ({
}
widgetKey="rootLoginPercentage"
indexPattern={indexPattern}
globalFilter={globalFilterForKubernetes}
globalFilter={globalFilter}
dataValueMap={{
'0': {
name: i18n.translate('xpack.kubernetesSecurity.entryUserChart.root', {
@ -288,7 +273,7 @@ const KubernetesSecurityRoutesComponent = ({
<ContainerNameWidget
widgetKey="containerNameSessions"
indexPattern={indexPattern}
globalFilter={globalFilterForKubernetes}
globalFilter={globalFilter}
groupedBy={CONTAINER_IMAGE_NAME}
countBy={ENTRY_LEADER_ENTITY_ID}
/>
@ -297,12 +282,9 @@ const KubernetesSecurityRoutesComponent = ({
</>
)}
<TreeViewContainer
globalFilter={globalFilterForKubernetes}
globalFilter={globalFilter}
renderSessionsView={renderSessionsView}
indexPattern={indexPattern}
responseActionButtonProps={responseActionButtonProps}
responseActionClick={responseActionClick}
handleTreeNavSelection={handleTreeNavSelection}
/>
</Route>
</Switch>

View file

@ -113,29 +113,6 @@ Object {
</button>
</span>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
class="euiButtonEmpty css-9t7nyf-empty-primary"
data-test-subj="kubernetesSecurity:kubernetesSecurityResponseAction"
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
Respond
</span>
</span>
</button>
</span>
</div>
</div>
</div>
</div>
@ -249,29 +226,6 @@ Object {
</button>
</span>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
class="euiButtonEmpty css-9t7nyf-empty-primary"
data-test-subj="kubernetesSecurity:kubernetesSecurityResponseAction"
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
Respond
</span>
</span>
</button>
</span>
</div>
</div>
</div>
</div>,
@ -442,29 +396,6 @@ Object {
</button>
</span>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
class="euiButtonEmpty css-9t7nyf-empty-primary"
data-test-subj="kubernetesSecurity:kubernetesSecurityResponseAction"
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
Respond
</span>
</span>
</button>
</span>
</div>
</div>
</div>
</div>
@ -578,29 +509,6 @@ Object {
</button>
</span>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
class="euiButtonEmpty css-9t7nyf-empty-primary"
data-test-subj="kubernetesSecurity:kubernetesSecurityResponseAction"
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
Respond
</span>
</span>
</button>
</span>
</div>
</div>
</div>
</div>,
@ -792,29 +700,6 @@ Object {
</button>
</span>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
class="euiButtonEmpty css-9t7nyf-empty-primary"
data-test-subj="kubernetesSecurity:kubernetesSecurityResponseAction"
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
Respond
</span>
</span>
</button>
</span>
</div>
</div>
</div>
</div>
@ -949,29 +834,6 @@ Object {
</button>
</span>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
class="euiButtonEmpty css-9t7nyf-empty-primary"
data-test-subj="kubernetesSecurity:kubernetesSecurityResponseAction"
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
Respond
</span>
</span>
</button>
</span>
</div>
</div>
</div>
</div>,
@ -1121,30 +983,6 @@ Object {
</button>
</span>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
class="euiButtonEmpty css-1at5a38-empty-disabled"
data-test-subj="kubernetesSecurity:kubernetesSecurityResponseAction"
disabled=""
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
Respond
</span>
</span>
</button>
</span>
</div>
</div>
</div>
</div>
@ -1237,30 +1075,6 @@ Object {
</button>
</span>
</div>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
class="euiButtonEmpty css-1at5a38-empty-disabled"
data-test-subj="kubernetesSecurity:kubernetesSecurityResponseAction"
disabled=""
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
Respond
</span>
</span>
</button>
</span>
</div>
</div>
</div>
</div>,

View file

@ -24,27 +24,17 @@ describe('Tree view Breadcrumb component', () => {
let renderResult: ReturnType<typeof render>;
let mockedContext: AppContextTestRender;
let onSelect: jest.Mock;
let onResponseActionButtonClick: jest.Mock;
const responseActionButtonProps = {
tooltip: { content: 'test' },
isDisabled: false,
canAccessResponseConsole: true,
};
beforeEach(() => {
mockedContext = createAppRootMockRenderer();
onSelect = jest.fn();
onResponseActionButtonClick = jest.fn();
});
describe('When Breadcrumb is mounted', () => {
it('renders Breadcrumb button content correctly', async () => {
renderResult = mockedContext.render(
<Breadcrumb
treeNavResponseActionDisabled={false}
treeNavSelection={{ ...MOCK_TREE_SELECTION, node: undefined }}
responseActionClick={onResponseActionButtonClick}
responseActionButtonProps={responseActionButtonProps}
onSelect={onSelect}
/>
);
@ -59,13 +49,7 @@ describe('Tree view Breadcrumb component', () => {
it('should render breadcrumb icons', async () => {
renderResult = mockedContext.render(
<Breadcrumb
responseActionClick={onResponseActionButtonClick}
treeNavResponseActionDisabled={false}
treeNavSelection={MOCK_TREE_SELECTION}
responseActionButtonProps={responseActionButtonProps}
onSelect={onSelect}
/>
<Breadcrumb treeNavSelection={MOCK_TREE_SELECTION} onSelect={onSelect} />
);
expect(
@ -79,29 +63,18 @@ describe('Tree view Breadcrumb component', () => {
expect(renderResult).toMatchSnapshot();
});
it('returns null when no selected collection', async () => {
renderResult = mockedContext.render(
<Breadcrumb
responseActionClick={onResponseActionButtonClick}
treeNavResponseActionDisabled={false}
treeNavSelection={{}}
responseActionButtonProps={responseActionButtonProps}
onSelect={onSelect}
/>
);
renderResult = mockedContext.render(<Breadcrumb treeNavSelection={{}} onSelect={onSelect} />);
expect(renderResult.container).toBeEmptyDOMElement();
});
it('should display cluster icon button when no cluster name is provided', async () => {
renderResult = mockedContext.render(
<Breadcrumb
treeNavResponseActionDisabled={false}
responseActionClick={onResponseActionButtonClick}
treeNavSelection={{
...MOCK_TREE_SELECTION,
clusterName: undefined,
node: undefined,
}}
responseActionButtonProps={responseActionButtonProps}
onSelect={onSelect}
/>
);
@ -117,10 +90,7 @@ describe('Tree view Breadcrumb component', () => {
it('should return null when no cluster in selection', async () => {
renderResult = mockedContext.render(
<Breadcrumb
treeNavResponseActionDisabled={false}
responseActionClick={onResponseActionButtonClick}
treeNavSelection={{ ...MOCK_TREE_SELECTION, clusterId: undefined }}
responseActionButtonProps={responseActionButtonProps}
onSelect={onSelect}
/>
);
@ -137,13 +107,7 @@ describe('Tree view Breadcrumb component', () => {
pod: 'selected pod',
};
renderResult = mockedContext.render(
<Breadcrumb
responseActionClick={onResponseActionButtonClick}
treeNavResponseActionDisabled={false}
treeNavSelection={mockPodNavSelection}
onSelect={onSelect}
responseActionButtonProps={responseActionButtonProps}
/>
<Breadcrumb treeNavSelection={mockPodNavSelection} onSelect={onSelect} />
);
expect(renderResult.queryByText(mockPodNavSelection.pod)).toBeVisible();
renderResult.getByText(mockPodNavSelection.pod).click();
@ -153,8 +117,6 @@ describe('Tree view Breadcrumb component', () => {
it('should render last breadcrumb content only', async () => {
renderResult = mockedContext.render(
<Breadcrumb
treeNavResponseActionDisabled={true}
responseActionClick={onResponseActionButtonClick}
treeNavSelection={{
clusterId: MOCK_TREE_SELECTION.clusterId,
clusterName: MOCK_TREE_SELECTION.clusterName,
@ -162,7 +124,6 @@ describe('Tree view Breadcrumb component', () => {
containerImage: MOCK_TREE_SELECTION.containerImage,
}}
onSelect={onSelect}
responseActionButtonProps={responseActionButtonProps}
/>
);

View file

@ -20,28 +20,17 @@ import {
KubernetesCollection,
TreeViewIconProps,
KubernetesTreeViewLevels,
ResponseActionButtonProps,
} from '../../../types';
import { useStyles } from './styles';
import { KUBERNETES_COLLECTION_ICONS_PROPS } from '../helpers';
import { RESPONSE_ACTION_BUTTON } from './translations';
import { showBreadcrumbDisplayText } from './helper';
import { BREADCRUMBS_CLUSTER_TREE_VIEW_LEVELS } from '../translations';
interface BreadcrumbDeps {
treeNavSelection: Partial<KubernetesCollectionMap>;
onSelect: (selection: Partial<KubernetesCollectionMap>) => void;
responseActionClick: () => void;
treeNavResponseActionDisabled: boolean;
responseActionButtonProps: ResponseActionButtonProps;
}
export const Breadcrumb = ({
treeNavSelection,
onSelect,
treeNavResponseActionDisabled,
responseActionButtonProps,
responseActionClick,
}: BreadcrumbDeps) => {
export const Breadcrumb = ({ treeNavSelection, onSelect }: BreadcrumbDeps) => {
const styles = useStyles();
const { euiVars } = useEuiTheme();
const onBreadCrumbClick = useCallback(
@ -73,8 +62,6 @@ export const Breadcrumb = ({
},
[onSelect, treeNavSelection]
);
const isResponseActionDisabled =
(responseActionButtonProps?.isDisabled ?? false) || treeNavResponseActionDisabled;
const renderBreadcrumbLink = useCallback(
(
@ -161,19 +148,6 @@ export const Breadcrumb = ({
true
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
{responseActionButtonProps?.canAccessResponseConsole && (
<EuiToolTip content={responseActionButtonProps?.tooltip ?? null}>
<EuiButtonEmpty
data-test-subj={`kubernetesSecurity:kubernetesSecurityResponseAction`}
isDisabled={isResponseActionDisabled}
onClick={responseActionClick}
>
{RESPONSE_ACTION_BUTTON}
</EuiButtonEmpty>
</EuiToolTip>
)}
</EuiFlexItem>
</EuiFlexGroup>
</div>
);

View file

@ -1,14 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const RESPONSE_ACTION_BUTTON = i18n.translate(
'xpack.kubernetesSecurity.breadcrumb.responseActionButton',
{
defaultMessage: 'Respond',
}
);

View file

@ -38,11 +38,13 @@ const focusPreviousButton = (event: KeyboardEvent) => {
};
const DynamicTreeViewExpander = ({
defaultExpanded = false,
children,
}: {
defaultExpanded: boolean;
children: (childrenProps: { isExpanded: boolean; onToggleExpand: () => void }) => JSX.Element;
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const onToggleExpand = () => {
setIsExpanded((e) => !e);
@ -66,7 +68,7 @@ export const DynamicTreeView = ({
}: DynamicTreeViewProps) => {
const styles = useStyles(depth);
const { indexPattern, setNoResults } = useTreeViewContext();
const { indexPattern, setNoResults, setTreeNavSelection } = useTreeViewContext();
const { data, fetchNextPage, isFetchingNextPage, hasNextPage, isLoading } =
useFetchDynamicTreeView(query, tree[depth].key, indexPattern, expanded);
@ -91,10 +93,15 @@ export const DynamicTreeView = ({
};
useEffect(() => {
if (depth === 0 && data && data.pages?.[0].buckets.length === 0) {
setNoResults(true);
if (depth === 0 && data) {
const noData = data.pages?.[0].buckets.length === 0;
setNoResults(noData);
if (noData) {
setTreeNavSelection({});
}
}
}, [data, depth, setNoResults]);
}, [data, depth, setNoResults, setTreeNavSelection]);
useEffect(() => {
if (expanded) {
@ -165,8 +172,10 @@ export const DynamicTreeView = ({
},
};
const defaultExpanded = selected.indexOf('' + aggData.key) > 0;
return (
<DynamicTreeViewExpander key={aggData.key}>
<DynamicTreeViewExpander key={aggData.key} defaultExpanded={defaultExpanded}>
{({ isExpanded, onToggleExpand }) => (
<DynamicTreeViewItem
aggData={aggData}

View file

@ -8,7 +8,7 @@
import {
CLOUD_INSTANCE_NAME,
CONTAINER_IMAGE_NAME,
DEFAULT_QUERY,
DEFAULT_FILTER_QUERY,
ORCHESTRATOR_CLUSTER_ID,
ORCHESTRATOR_CLUSTER_NAME,
ORCHESTRATOR_NAMESPACE,
@ -43,7 +43,7 @@ export const addTreeNavSelectionToFilterQuery = (
filterQuery: string | undefined,
treeNavSelection: Partial<KubernetesCollectionMap>
) => {
let validFilterQuery = DEFAULT_QUERY;
let validFilterQuery = DEFAULT_FILTER_QUERY;
try {
const parsedFilterQuery: QueryDslQueryContainerBool = JSON.parse(filterQuery || '{}');
@ -73,7 +73,7 @@ export const addTreeNavSelectionToFilterQuery = (
validFilterQuery = JSON.stringify(parsedFilterQuery);
} catch {
// no-op since validFilterQuery is initialized to be DEFAULT_QUERY
// no-op since validFilterQuery is initialized to be DEFAULT_FILTER_QUERY
}
return validFilterQuery;

View file

@ -4,17 +4,13 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { CoreStart } from '@kbn/core/public';
import type { KubernetesCollectionMap, QueryDslQueryContainerBool } from '../../types';
import { useCallback, useMemo, useState } from 'react';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import type { KubernetesCollectionMap } from '../../types';
import { LOCAL_STORAGE_TREE_NAV_KEY } from '../../../common/constants';
import { addTimerangeAndDefaultFilterToQuery } from '../../utils/add_timerange_and_default_filter_to_query';
import { addTreeNavSelectionToFilterQuery } from './helpers';
import { IndexPattern, GlobalFilter } from '../../types';
import { QUERY_KEY_AGENT_ID, AGENT_ID_ROUTE } from '../../../common/constants';
import { AgentIdResult } from './tree_nav/types';
export type UseTreeViewProps = {
globalFilter: GlobalFilter;
@ -23,9 +19,9 @@ export type UseTreeViewProps = {
export const useTreeView = ({ globalFilter, indexPattern }: UseTreeViewProps) => {
const [noResults, setNoResults] = useState(false);
const [treeNavSelection, setTreeNavSelection] = useState<Partial<KubernetesCollectionMap>>({});
const [hasSelection, setHasSelection] = useState(false);
const [treeNavResponseActionDisabled, setTreeNavResponseActionDisabled] = useState(false);
const [treeNavSelection = {}, setTreeNavSelection] = useLocalStorage<
Partial<KubernetesCollectionMap>
>(LOCAL_STORAGE_TREE_NAV_KEY, {});
const filterQueryWithTimeRange = useMemo(() => {
return JSON.parse(
addTimerangeAndDefaultFilterToQuery(
@ -36,61 +32,26 @@ export const useTreeView = ({ globalFilter, indexPattern }: UseTreeViewProps) =>
);
}, [globalFilter.filterQuery, globalFilter.startDate, globalFilter.endDate]);
const onTreeNavSelect = useCallback((selection: Partial<KubernetesCollectionMap>) => {
const isResponseActionEnabled =
!!selection?.node || !!selection?.pod || !!selection?.containerImage;
setTreeNavResponseActionDisabled(isResponseActionEnabled ? false : true);
setHasSelection(false);
setTreeNavSelection(selection);
}, []);
const onTreeNavSelect = useCallback(
(selection: Partial<KubernetesCollectionMap>) => {
setTreeNavSelection(selection);
},
[setTreeNavSelection]
);
const sessionViewFilter = useMemo(
() => addTreeNavSelectionToFilterQuery(globalFilter.filterQuery, treeNavSelection),
[globalFilter.filterQuery, treeNavSelection]
);
// Resetting defaults whenever filter changes
useEffect(() => {
setNoResults(false);
setTreeNavSelection({});
}, [filterQueryWithTimeRange]);
useEffect(() => {
if (!!treeNavSelection.clusterId) {
setHasSelection(true);
setTreeNavSelection(treeNavSelection);
}
}, [treeNavSelection]);
return {
noResults,
setNoResults,
filterQueryWithTimeRange,
indexPattern: indexPattern?.title || '',
onTreeNavSelect,
hasSelection,
treeNavSelection,
treeNavResponseActionDisabled,
setTreeNavSelection,
sessionViewFilter,
};
};
export const useFetchAgentIdForResponder = (
filterQuery: QueryDslQueryContainerBool,
index?: string
) => {
const { http } = useKibana<CoreStart>().services;
const cachingKeys = [QUERY_KEY_AGENT_ID, filterQuery, index];
const query = useQuery(cachingKeys, async (): Promise<AgentIdResult> => {
const res = await http.get<AgentIdResult>(AGENT_ID_ROUTE, {
query: {
query: JSON.stringify(filterQuery),
index,
},
});
return res;
});
return query;
};

View file

@ -7,6 +7,7 @@
import React from 'react';
import { TreeViewContainer } from '.';
import { DEFAULT_FILTER_QUERY } from '../../../common/constants';
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
import * as context from './contexts';
@ -18,10 +19,11 @@ describe('TreeNav component', () => {
const defaultProps = {
globalFilter: {
filterQuery: DEFAULT_FILTER_QUERY,
startDate: Date.now().toString(),
endDate: (Date.now() + 1).toString(),
},
renderSessionsView: <div>Session View</div>,
renderSessionsView: () => <div>Session View</div>,
} as any;
beforeEach(() => {
@ -31,13 +33,14 @@ describe('TreeNav component', () => {
spy.mockRestore();
});
it('shows empty message when there is no results', async () => {
it('shows empty message when there is no results', () => {
spy.mockImplementation(() => ({
...jest.requireActual('./contexts').useTreeViewContext,
noResults: true,
treeNavSelection: {},
}));
renderResult = mockedContext.render(<TreeViewContainer {...defaultProps} />);
expect(await renderResult.getByText(/no results/i)).toBeInTheDocument();
expect(renderResult.getByText(/no results/i)).toBeInTheDocument();
});
});

View file

@ -8,18 +8,13 @@
import React from 'react';
import { EuiSplitPanel, EuiText } from '@elastic/eui';
import { useStyles } from './styles';
import { IndexPattern, GlobalFilter, ResponseActionButtonProps } from '../../types';
import { IndexPattern, GlobalFilter } from '../../types';
import { TreeNav } from './tree_nav';
import { Breadcrumb } from './breadcrumb';
import { TreeViewContextProvider, useTreeViewContext } from './contexts';
import { EmptyState } from './empty_state';
import { addTreeNavSelectionToFilterQuery } from './helpers';
import { useFetchAgentIdForResponder } from './hooks';
export interface TreeViewContainerComponentDeps {
responseActionButtonProps: ResponseActionButtonProps;
responseActionClick: () => void;
handleTreeNavSelection: (agentId: string) => void;
renderSessionsView: (sessionsFilterQuery: string | undefined) => JSX.Element;
}
export interface TreeViewContainerDeps extends TreeViewContainerComponentDeps {
@ -31,69 +26,31 @@ export const TreeViewContainer = ({
globalFilter,
renderSessionsView,
indexPattern,
responseActionClick,
handleTreeNavSelection,
responseActionButtonProps,
}: TreeViewContainerDeps) => {
return (
<TreeViewContextProvider indexPattern={indexPattern} globalFilter={globalFilter}>
<TreeViewContainerComponent
responseActionButtonProps={responseActionButtonProps}
renderSessionsView={renderSessionsView}
responseActionClick={responseActionClick}
handleTreeNavSelection={handleTreeNavSelection}
/>
<TreeViewContainerComponent renderSessionsView={renderSessionsView} />
</TreeViewContextProvider>
);
};
const TreeViewContainerComponent = ({
renderSessionsView,
responseActionButtonProps,
responseActionClick,
handleTreeNavSelection,
}: TreeViewContainerComponentDeps) => {
const TreeViewContainerComponent = ({ renderSessionsView }: TreeViewContainerComponentDeps) => {
const styles = useStyles();
const {
hasSelection,
treeNavSelection,
sessionViewFilter,
indexPattern,
onTreeNavSelect,
noResults,
treeNavResponseActionDisabled,
} = useTreeViewContext();
const query = JSON.parse(addTreeNavSelectionToFilterQuery(sessionViewFilter, treeNavSelection));
const { data } = useFetchAgentIdForResponder(query, indexPattern);
if (data?.agentId) {
handleTreeNavSelection(data.agentId);
}
const { treeNavSelection, sessionViewFilter, onTreeNavSelect, noResults } = useTreeViewContext();
return (
<EuiSplitPanel.Outer direction="row" hasBorder borderRadius="m" css={styles.outerPanel}>
{noResults ? (
<EmptyState />
) : (
<>
<EuiSplitPanel.Inner color="subdued" grow={false} css={styles.navPanel}>
<EuiText>
<TreeNav />
</EuiText>
</EuiSplitPanel.Inner>
<EuiSplitPanel.Inner css={styles.sessionsPanel}>
<Breadcrumb
treeNavSelection={treeNavSelection}
treeNavResponseActionDisabled={treeNavResponseActionDisabled}
responseActionClick={responseActionClick}
responseActionButtonProps={responseActionButtonProps}
onSelect={onTreeNavSelect}
/>
{hasSelection && renderSessionsView(sessionViewFilter)}
</EuiSplitPanel.Inner>
</>
)}
{noResults && <EmptyState />}
<EuiSplitPanel.Inner hidden={noResults} color="subdued" grow={false} css={styles.navPanel}>
<EuiText>
<TreeNav />
</EuiText>
</EuiSplitPanel.Inner>
<EuiSplitPanel.Inner hidden={noResults} css={styles.sessionsPanel}>
<Breadcrumb treeNavSelection={treeNavSelection} onSelect={onTreeNavSelect} />
{renderSessionsView(sessionViewFilter)}
</EuiSplitPanel.Inner>
</EuiSplitPanel.Outer>
);
};

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useCallback } from 'react';
import {
EuiButtonGroup,
useGeneratedHtmlId,
@ -31,7 +31,8 @@ import { useTreeViewContext } from '../contexts';
export const TreeNav = () => {
const styles = useStyles();
const [tree, setTree] = useState(TREE_VIEW.logical);
const [selected, setSelected] = useState('');
const { filterQueryWithTimeRange, onTreeNavSelect, treeNavSelection, setTreeNavSelection } =
useTreeViewContext();
const [isCollapsed, setIsCollapsed] = useState(false);
const treeNavTypePrefix = useGeneratedHtmlId({
prefix: 'treeNavType',
@ -39,7 +40,11 @@ export const TreeNav = () => {
const logicalTreeViewPrefix = `${treeNavTypePrefix}${LOGICAL}`;
const [toggleIdSelected, setToggleIdSelected] = useState(logicalTreeViewPrefix);
const { filterQueryWithTimeRange, onTreeNavSelect } = useTreeViewContext();
const selected = useMemo(() => {
return Object.entries(treeNavSelection)
.map(([k, v]) => `${k}.${v}`)
.join();
}, [treeNavSelection]);
const handleToggleCollapse = () => {
setIsCollapsed(!isCollapsed);
@ -65,10 +70,14 @@ export const TreeNav = () => {
return options.find((opt) => opt.id === toggleIdSelected)!.label;
}, [options, toggleIdSelected]);
const handleTreeViewSwitch = (id: string, value: TreeViewKind) => {
setToggleIdSelected(id);
setTree(TREE_VIEW[value]);
};
const handleTreeViewSwitch = useCallback(
(id: string, value: TreeViewKind) => {
setToggleIdSelected(id);
setTree(TREE_VIEW[value]);
setTreeNavSelection({});
},
[setTreeNavSelection]
);
return (
<>
@ -123,11 +132,6 @@ export const TreeNav = () => {
[type]: key,
...(clusterName && { clusterName }),
};
setSelected(
Object.entries(newSelectionDepth)
.map(([k, v]) => `${k}.${v}`)
.join()
);
onTreeNavSelect(newSelectionDepth);
}}
/>

View file

@ -12,7 +12,3 @@ export interface TreeViewOptionsGroup {
label: string;
value: TreeViewKind;
}
export interface AgentIdResult {
agentId: string | null;
}

View file

@ -33,19 +33,11 @@ export interface GlobalFilter {
endDate: string;
}
export interface ResponseActionButtonProps {
tooltip: React.ReactNode;
canAccessResponseConsole: boolean;
isDisabled: boolean;
}
export interface KubernetesSecurityDeps {
filter: React.ReactNode;
renderSessionsView: (sessionsFilterQuery: string | undefined) => JSX.Element;
indexPattern?: IndexPattern;
globalFilter: GlobalFilter;
responseActionClick: () => void;
handleTreeNavSelection: (agentId: string) => void;
responseActionButtonProps: ResponseActionButtonProps;
}
export interface KubernetesSecurityStart {

View file

@ -14,7 +14,7 @@ const TEST_INVALID_QUERY = '{"bool":{"must":[';
const TEST_EMPTY_STRING = '';
const TEST_DATE = '2022-06-09T22:36:46.628Z';
const VALID_RESULT =
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"exists":{"field":"process.entry_leader.entity_id"}}],"minimum_should_match":1}},{"bool":{"should":[{"match":{"process.entry_leader.same_as_process":true}}],"minimum_should_match":1}},{"range":{"@timestamp":{"gte":"2022-06-09T22:36:46.628Z","lte":"2022-06-09T22:36:46.628Z"}}}],"should":[],"must_not":[]}}';
'{"bool":{"must":[],"filter":[{"bool":{"should":[{"exists":{"field":"orchestrator.cluster.id"}}],"minimum_should_match":1}},{"bool":{"should":[{"match":{"process.entry_leader.same_as_process":true}}],"minimum_should_match":1}},{"range":{"@timestamp":{"gte":"2022-06-09T22:36:46.628Z","lte":"2022-06-09T22:36:46.628Z"}}}],"should":[],"must_not":[]}}';
describe('addTimerangeAndDefaultFilterToQuery(query, startDate, endDate)', () => {
it('works for valid query, startDate, and endDate', () => {

View file

@ -67,6 +67,11 @@ export const doSearch = async (
field: countBy,
},
},
count_alerts: {
cardinality: {
field: countBy,
},
},
}
: undefined;

View file

@ -5,9 +5,8 @@
* 2.0.
*/
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { getEsQueryConfig } from '@kbn/data-plugin/common';
import type { ResponseActionButtonProps } from '@kbn/kubernetes-security-plugin/public/types';
import { TableId } from '@kbn/securitysolution-data-table';
import { InputsModelId } from '../../common/store/inputs/constants';
import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
@ -26,18 +25,12 @@ import { convertToBuildEsQuery } from '../../common/lib/kuery';
import { useInvalidFilterQuery } from '../../common/hooks/use_invalid_filter_query';
import { SessionsView } from '../../common/components/sessions_viewer';
import { kubernetesSessionsHeaders } from './constants';
import { useResponderActionData } from '../../detections/components/endpoint_responder/use_responder_action_data';
import { useUserPrivileges } from '../../common/components/user_privileges';
export const KubernetesContainer = React.memo(() => {
const { kubernetesSecurity, uiSettings } = useKibana().services;
const { globalFullScreen } = useGlobalFullScreen();
const {
indexPattern,
// runtimeMappings,
// loading: isLoadingIndexPattern,
} = useSourcererDataView();
const { indexPattern } = useSourcererDataView();
const { from, to } = useGlobalTime();
const getGlobalFiltersQuerySelector = useMemo(
@ -47,9 +40,7 @@ export const KubernetesContainer = React.memo(() => {
const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []);
const query = useDeepEqualSelector(getGlobalQuerySelector);
const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector);
const canAccessResponseConsole = useUserPrivileges().endpointPrivileges.canAccessResponseConsole;
const [agentIdForResponder, setAgentIdForResponder] = useState<string>('');
const [filterQuery, kqlError] = useMemo(
() =>
convertToBuildEsQuery({
@ -85,24 +76,6 @@ export const KubernetesContainer = React.memo(() => {
[from, to]
);
const { handleResponseActionsClick, isDisabled, tooltip } = useResponderActionData({
endpointId: agentIdForResponder,
});
const handleTreeNavSelection = useCallback((agentId: string) => {
setAgentIdForResponder(agentId);
}, []);
const responseActionClick = useCallback(() => {
handleResponseActionsClick();
}, [handleResponseActionsClick]);
const responseActionButtonProps: ResponseActionButtonProps = {
isDisabled,
canAccessResponseConsole,
tooltip,
};
return (
<SecuritySolutionPageWrapper noPadding>
{kubernetesSecurity.getKubernetesPage({
@ -118,9 +91,6 @@ export const KubernetesContainer = React.memo(() => {
endDate: to,
},
renderSessionsView,
responseActionClick,
handleTreeNavSelection,
responseActionButtonProps,
})}
<SpyRoute pageName={SecurityPageName.kubernetes} />
</SecuritySolutionPageWrapper>

View file

@ -7,13 +7,13 @@
import { render } from '@testing-library/react';
import React from 'react';
import { BETA } from '@kbn/kubernetes-security-plugin/common/translations';
import { SecurityPageName } from '../../app/types';
import type { NavLinkItem } from '../../common/components/navigation/types';
import { TestProviders } from '../../common/mock';
import { LandingLinksImages, LandingImageCards } from './landing_links_images';
import * as telemetry from '../../common/lib/telemetry';
const BETA = 'Beta';
const DEFAULT_NAV_ITEM: NavLinkItem = {
id: SecurityPageName.overview,
title: 'TEST LABEL',

View file

@ -20257,8 +20257,6 @@
"xpack.ingestPipelines.testPipelineFlyout.successNotificationText": "Pipeline exécuté",
"xpack.ingestPipelines.testPipelineFlyout.title": "Pipeline de test",
"xpack.kubernetesSecurity.treeNavigation.loadMore": "Afficher plus {name}",
"xpack.kubernetesSecurity.beta": "Bêta",
"xpack.kubernetesSecurity.breadcrumb.responseActionButton": "Répondre",
"xpack.kubernetesSecurity.chartsToggle.hide": "Masquer les graphiques",
"xpack.kubernetesSecurity.chartsToggle.show": "Afficher les graphiques",
"xpack.kubernetesSecurity.containerNameWidget.containerImage": "Images de conteneurs",

View file

@ -20256,8 +20256,6 @@
"xpack.ingestPipelines.testPipelineFlyout.successNotificationText": "パイプラインが実行されました",
"xpack.ingestPipelines.testPipelineFlyout.title": "パイプラインをテスト",
"xpack.kubernetesSecurity.treeNavigation.loadMore": "{name}詳細表示",
"xpack.kubernetesSecurity.beta": "ベータ",
"xpack.kubernetesSecurity.breadcrumb.responseActionButton": "対応",
"xpack.kubernetesSecurity.chartsToggle.hide": "グラフを非表示",
"xpack.kubernetesSecurity.chartsToggle.show": "チャートを表示",
"xpack.kubernetesSecurity.containerNameWidget.containerImage": "コンテナーイメージ",

View file

@ -20256,8 +20256,6 @@
"xpack.ingestPipelines.testPipelineFlyout.successNotificationText": "管道已执行",
"xpack.ingestPipelines.testPipelineFlyout.title": "测试管道",
"xpack.kubernetesSecurity.treeNavigation.loadMore": "显示更多 {name}",
"xpack.kubernetesSecurity.beta": "公测版",
"xpack.kubernetesSecurity.breadcrumb.responseActionButton": "响应",
"xpack.kubernetesSecurity.chartsToggle.hide": "隐藏图表",
"xpack.kubernetesSecurity.chartsToggle.show": "显示图表",
"xpack.kubernetesSecurity.containerNameWidget.containerImage": "容器映像",