diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/sync_rules/edit_sync_rules_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/sync_rules/edit_sync_rules_flyout.tsx
index 47278a569a184..e675b7a149d3b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/sync_rules/edit_sync_rules_flyout.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/sync_rules/edit_sync_rules_flyout.tsx
@@ -24,6 +24,7 @@ import {
import { i18n } from '@kbn/i18n';
import { FilteringValidation } from '../../../../../../../common/types/connectors';
+import { BetaCallOut } from '../../../../../shared/beta/beta_callout';
import { AdvancedSyncRules } from './advanced_sync_rules';
import { EditSyncRulesTab } from './edit_sync_rules_tab';
@@ -104,6 +105,16 @@ export const EditSyncRulesFlyout: React.FC = ({
+
+
{i18n.translate(
'xpack.enterpriseSearch.content.index.connector.syncRules.flyout.description',
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_job_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_job_flyout.tsx
index b3257fa6455e8..0893b6e99d934 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_job_flyout.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_job_flyout.tsx
@@ -75,7 +75,7 @@ export const SyncJobFlyout: React.FC = ({ onClose, syncJob }
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs_view_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs_view_logic.test.ts
index 1b965632194b8..41ac136437ef5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs_view_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs_view_logic.test.ts
@@ -80,6 +80,7 @@ describe('SyncJobsViewLogic', () => {
metadata: {},
started_at: '2022-09-05T14:59:39.816+00:00',
status: SyncStatus.COMPLETED,
+ total_document_count: null,
trigger_method: TriggerMethod.ON_DEMAND,
worker_hostname: 'hostname_fake',
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/beta/beta_badge.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/beta/beta_badge.tsx
new file mode 100644
index 0000000000000..2b3b4a8a24d38
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/beta/beta_badge.tsx
@@ -0,0 +1,16 @@
+/*
+ * 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 React from 'react';
+
+import { EuiBadge } from '@elastic/eui';
+
+import { BETA_LABEL } from '../constants';
+
+export const BetaBadge: React.FC = () => {
+ return {BETA_LABEL};
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/beta/beta_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/beta/beta_callout.tsx
new file mode 100644
index 0000000000000..ffda9fad5475f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/beta/beta_callout.tsx
@@ -0,0 +1,29 @@
+/*
+ * 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 React from 'react';
+
+import { EuiCallOut } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+interface BetaCallOutProps {
+ description: string;
+}
+
+export const BetaCallOut: React.FC = ({ description }) => {
+ return (
+
+ {description}
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts
index f9dedfe069170..43c7a81ac527e 100644
--- a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts
@@ -81,6 +81,7 @@ describe('startSync lib function', () => {
metadata: {},
started_at: null,
status: SyncStatus.PENDING,
+ total_document_count: null,
trigger_method: TriggerMethod.ON_DEMAND,
worker_hostname: null,
},
@@ -143,6 +144,7 @@ describe('startSync lib function', () => {
metadata: {},
started_at: null,
status: SyncStatus.PENDING,
+ total_document_count: null,
trigger_method: TriggerMethod.ON_DEMAND,
worker_hostname: null,
},
diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.ts
index 0028b7185e867..c2c6e0a9995f3 100644
--- a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.ts
@@ -76,6 +76,7 @@ export const startConnectorSync = async (
metadata: {},
started_at: null,
status: SyncStatus.PENDING,
+ total_document_count: null,
trigger_method: TriggerMethod.ON_DEMAND,
worker_hostname: null,
},
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.test.tsx
new file mode 100644
index 0000000000000..4b19e86703f1b
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.test.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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 React from 'react';
+
+import { createFleetTestRendererMock } from '../../../../../../mock';
+
+import { AgentStatusBar } from './status_bar';
+
+describe('AgentStatusBar', () => {
+ it('should render the status bar if there is some agent displayed', () => {
+ const renderer = createFleetTestRendererMock();
+ const res = renderer.render(
+
+ );
+ expect(res.queryByTestId('agentStatusBar')).not.toBeNull();
+ });
+
+ it('should not render the status bar if there is no agent displayed', () => {
+ const renderer = createFleetTestRendererMock();
+ const res = renderer.render(
+
+ );
+ expect(res.queryByTestId('agentStatusBar')).toBeNull();
+ });
+});
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx
index 728134768b799..f4fc01204267b 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx
@@ -6,7 +6,7 @@
*/
import styled from 'styled-components';
-import { EuiColorPaletteDisplay } from '@elastic/eui';
+import { EuiColorPaletteDisplay, EuiSpacer } from '@elastic/eui';
import React, { useMemo } from 'react';
import { AGENT_STATUSES, getColorForAgentStatus } from '../../services/agent_status';
@@ -35,13 +35,19 @@ export const AgentStatusBar: React.FC<{
return acc;
}, [] as Array<{ stop: number; color: string }>);
}, [agentStatus]);
+
+ const hasNoAgent = palette[palette.length - 1].stop === 0;
+
+ if (hasNoAgent) {
+ return ;
+ }
+
return (
- <>
-
- >
+
);
};
diff --git a/x-pack/plugins/security_solution/public/common/components/filter_group/filter_group.test.tsx b/x-pack/plugins/security_solution/public/common/components/filter_group/filter_group.test.tsx
index 5da07f8add864..5600d231df19a 100644
--- a/x-pack/plugins/security_solution/public/common/components/filter_group/filter_group.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/filter_group/filter_group.test.tsx
@@ -95,7 +95,11 @@ const getStoreWithCustomState = (newState: typeof state = state) => {
return createStore(newState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
};
-const TestComponent: FC> = (props) => (
+const TestComponent: FC<
+ ComponentProps & {
+ filterGroupProps?: Partial>;
+ }
+> = (props) => (
> = (props) => (
chainingSystem="HIERARCHICAL"
onFilterChange={onFilterChangeMock}
onInit={onInitMock}
+ {...props.filterGroupProps}
/>
);
@@ -522,6 +527,36 @@ describe(' Filter Group Component ', () => {
expect(screen.queryByTestId(TEST_IDS.SAVE_CHANGE_POPOVER)).toBeVisible();
});
});
+ it('should update controlGroup with new filters and queries when valid query is supplied', async () => {
+ const validQuery = { query: { language: 'kuery', query: '' } };
+ // pass an invalid query
+ render();
+
+ await waitFor(() => {
+ expect(controlGroupMock.updateInput).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ filters: undefined,
+ query: validQuery.query,
+ })
+ );
+ });
+ });
+
+ it('should not update controlGroup with new filters and queries when invalid query is supplied', async () => {
+ const invalidQuery = { query: { language: 'kuery', query: '\\' } };
+ // pass an invalid query
+ render();
+
+ await waitFor(() => {
+ expect(controlGroupMock.updateInput).toHaveBeenCalledWith(
+ expect.objectContaining({
+ filters: [],
+ query: undefined,
+ })
+ );
+ });
+ });
});
describe('Filter Changed Banner', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/filter_group/index.tsx b/x-pack/plugins/security_solution/public/common/components/filter_group/index.tsx
index a4c61fb9c98a6..b821ffc6ca227 100644
--- a/x-pack/plugins/security_solution/public/common/components/filter_group/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/filter_group/index.tsx
@@ -46,6 +46,7 @@ import { FilterGroupContext } from './filter_group_context';
import { NUM_OF_CONTROLS } from './config';
import { TEST_IDS } from './constants';
import { URL_PARAM_ARRAY_EXCEPTION_MSG } from './translations';
+import { convertToBuildEsQuery } from '../../lib/kuery';
const FilterWrapper = styled.div.attrs((props) => ({
className: props.className,
@@ -149,14 +150,41 @@ const FilterGroupComponent = (props: PropsWithChildren) => {
return cleanup;
}, []);
+ const { filters: validatedFilters, query: validatedQuery } = useMemo(() => {
+ const [_, kqlError] = convertToBuildEsQuery({
+ config: {},
+ queries: query ? [query] : [],
+ filters: filters ?? [],
+ indexPattern: { fields: [], title: '' },
+ });
+
+ // we only need to handle kqlError because control group can handle Lucene error
+ if (kqlError) {
+ /*
+ * Based on the behaviour from other components,
+ * ignore all filters and queries if there is some error
+ * in the input filters and queries
+ *
+ * */
+ return {
+ filters: [],
+ query: undefined,
+ };
+ }
+ return {
+ filters,
+ query,
+ };
+ }, [filters, query]);
+
useEffect(() => {
controlGroup?.updateInput({
+ filters: validatedFilters,
+ query: validatedQuery,
timeRange,
- filters,
- query,
chainingSystem,
});
- }, [timeRange, filters, query, chainingSystem, controlGroup]);
+ }, [timeRange, chainingSystem, controlGroup, validatedQuery, validatedFilters]);
const handleInputUpdates = useCallback(
(newInput: ControlGroupInput) => {
@@ -171,7 +199,7 @@ const FilterGroupComponent = (props: PropsWithChildren) => {
[setControlGroupInputUpdates, getStoredControlInput, isViewMode, setHasPendingChanges]
);
- const handleFilterUpdates = useCallback(
+ const handleOutputFilterUpdates = useCallback(
({ filters: newFilters }: ControlGroupOutput) => {
if (isEqual(currentFiltersRef.current, newFilters)) return;
if (onFilterChange) onFilterChange(newFilters ?? []);
@@ -181,8 +209,8 @@ const FilterGroupComponent = (props: PropsWithChildren) => {
);
const debouncedFilterUpdates = useMemo(
- () => debounce(handleFilterUpdates, 500),
- [handleFilterUpdates]
+ () => debounce(handleOutputFilterUpdates, 500),
+ [handleOutputFilterUpdates]
);
useEffect(() => {
diff --git a/x-pack/plugins/security_solution/public/dashboards/components/status_prompt.test.tsx b/x-pack/plugins/security_solution/public/dashboards/components/status_prompt.test.tsx
index c173aa836af55..1677e78622c56 100644
--- a/x-pack/plugins/security_solution/public/dashboards/components/status_prompt.test.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/components/status_prompt.test.tsx
@@ -7,17 +7,17 @@
import { render } from '@testing-library/react';
import React from 'react';
import { DashboardViewPromptState } from '../hooks/use_dashboard_view_prompt_state';
-import { StatusPropmpt } from './status_prompt';
+import { StatusPrompt } from './status_prompt';
-describe('StatusPropmpt', () => {
+describe('StatusPrompt', () => {
it('hides by default', () => {
- const { queryByTestId } = render();
+ const { queryByTestId } = render();
expect(queryByTestId(`dashboardViewEmptyDefault`)).not.toBeInTheDocument();
});
it('shows when No Read Permission', () => {
const { queryByTestId } = render(
-
+
);
expect(
diff --git a/x-pack/plugins/security_solution/public/dashboards/components/status_prompt.tsx b/x-pack/plugins/security_solution/public/dashboards/components/status_prompt.tsx
index 7f0584ec0e882..6e4bb16374268 100644
--- a/x-pack/plugins/security_solution/public/dashboards/components/status_prompt.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/components/status_prompt.tsx
@@ -9,7 +9,7 @@ import { EuiPageTemplate } from '@elastic/eui';
import type { DashboardViewPromptState } from '../hooks/use_dashboard_view_prompt_state';
import { useDashboardViewPromptState } from '../hooks/use_dashboard_view_prompt_state';
-const StatusPropmptComponent = ({
+const StatusPromptComponent = ({
currentState,
}: {
currentState: DashboardViewPromptState | null;
@@ -21,5 +21,5 @@ const StatusPropmptComponent = ({
) : null;
};
-StatusPropmptComponent.displayName = 'StatusPropmptComponent';
-export const StatusPropmpt = React.memo(StatusPropmptComponent);
+StatusPromptComponent.displayName = 'StatusPromptComponent';
+export const StatusPrompt = React.memo(StatusPromptComponent);
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/details/index.test.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/details/index.test.tsx
new file mode 100644
index 0000000000000..9eed2b4a7f5c2
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/details/index.test.tsx
@@ -0,0 +1,107 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import React from 'react';
+import { Router } from 'react-router-dom';
+import { DashboardView } from '.';
+import { useCapabilities } from '../../../common/lib/kibana';
+import { TestProviders } from '../../../common/mock';
+
+jest.mock('react-router-dom', () => {
+ const actual = jest.requireActual('react-router-dom');
+ return {
+ ...actual,
+ useParams: jest.fn().mockReturnValue({ detailName: 'mockSavedObjectId' }),
+ };
+});
+
+jest.mock('../../../common/lib/kibana', () => {
+ const actual = jest.requireActual('../../../common/lib/kibana');
+ return {
+ ...actual,
+ useCapabilities: jest.fn().mockReturnValue({ show: true, showWriteControls: true }),
+ };
+});
+
+jest.mock('../../../common/components/dashboards/dashboard_renderer', () => ({
+ DashboardRenderer: jest
+ .fn()
+ .mockImplementation((props) => (
+
+ )),
+}));
+
+type Action = 'PUSH' | 'POP' | 'REPLACE';
+const pop: Action = 'POP';
+const location = {
+ pathname: '/network',
+ search: '',
+ state: '',
+ hash: '',
+};
+const mockHistory = {
+ length: 2,
+ location,
+ action: pop,
+ push: jest.fn(),
+ replace: jest.fn(),
+ go: jest.fn(),
+ goBack: jest.fn(),
+ goForward: jest.fn(),
+ block: jest.fn(),
+ createHref: jest.fn(),
+ listen: jest.fn(),
+};
+
+describe('DashboardView', () => {
+ beforeEach(() => {
+ (useCapabilities as unknown as jest.Mock).mockReturnValue({
+ show: true,
+ showWriteControls: true,
+ });
+ });
+ test('render when no error state', () => {
+ const { queryByTestId } = render(
+
+
+ ,
+ { wrapper: TestProviders }
+ );
+
+ expect(queryByTestId(`dashboard-view-mockSavedObjectId`)).toBeInTheDocument();
+ });
+
+ test('render a prompt when error state exists', () => {
+ (useCapabilities as unknown as jest.Mock).mockReturnValue({
+ show: false,
+ showWriteControls: true,
+ });
+ const { queryByTestId } = render(
+
+
+ ,
+ { wrapper: TestProviders }
+ );
+
+ expect(queryByTestId(`dashboard-view-mockSavedObjectId`)).not.toBeInTheDocument();
+ expect(queryByTestId(`dashboard-view-error-prompt-wrapper`)).toBeInTheDocument();
+ });
+
+ test('render dashboard view with height', () => {
+ const { queryByTestId } = render(
+
+
+ ,
+ { wrapper: TestProviders }
+ );
+
+ expect(queryByTestId(`dashboard-view-wrapper`)).toHaveStyle({
+ 'min-height': `calc(100vh - 140px)`,
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/details/index.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/details/index.tsx
index 410906b29ae73..7fa8671004552 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/details/index.tsx
@@ -13,13 +13,13 @@ import type { DashboardCapabilities } from '@kbn/dashboard-plugin/common/types';
import { useParams } from 'react-router-dom';
import { pick } from 'lodash/fp';
-import { EuiLoadingSpinner } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { SecurityPageName } from '../../../../common/constants';
import { SpyRoute } from '../../../common/utils/route/spy_routes';
import { useCapabilities } from '../../../common/lib/kibana';
import { DashboardViewPromptState } from '../../hooks/use_dashboard_view_prompt_state';
import { DashboardRenderer } from '../../../common/components/dashboards/dashboard_renderer';
-import { StatusPropmpt } from '../../components/status_prompt';
+import { StatusPrompt } from '../../components/status_prompt';
import { SiemSearchBar } from '../../../common/components/search_bar';
import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper';
import { FiltersGlobal } from '../../../common/components/filters_global';
@@ -33,6 +33,8 @@ import { EditDashboardButton } from '../../components/edit_dashboard_button';
type DashboardDetails = Record;
+const dashboardViewFlexGroupStyle = { minHeight: `calc(100vh - 140px)` };
+
const DashboardViewComponent: React.FC = () => {
const { fromStr, toStr, from, to } = useDeepEqualSelector((state) =>
pick(['fromStr', 'toStr', 'from', 'to'], inputsSelectors.globalTimeRangeSelector(state))
@@ -76,34 +78,47 @@ const DashboardViewComponent: React.FC = () => {
)}
- }>
- {showWriteControls && dashboardExists && (
-
+
+
+ }>
+ {showWriteControls && dashboardExists && (
+
+ )}
+
+
+ {!errorState && (
+
+
+
)}
-
-
- {!errorState && (
-
+
+
+ )}
+
- )}
-
-
-
+