From 29e90f56027e90c2392db8f24f7afa6b20091c73 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 21 Jun 2021 12:14:29 +0100
Subject: [PATCH 001/163] skip flaky suite (#102183)
---
x-pack/test/functional/apps/lens/formula.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/formula.ts
index db7c680ac20af..d1376731886eb 100644
--- a/x-pack/test/functional/apps/lens/formula.ts
+++ b/x-pack/test/functional/apps/lens/formula.ts
@@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const testSubjects = getService('testSubjects');
- describe('lens formula', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/102183
+ describe.skip('lens formula', () => {
it('should transition from count to formula', async () => {
await PageObjects.visualize.gotoVisualizationLandingPage();
await listingTable.searchForItemWithName('lnsXYvis');
From fa8ab2a0207488d8b5528ac9529780fa075e1b80 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 21 Jun 2021 12:25:12 +0100
Subject: [PATCH 002/163] skip flaky suite (#102722)
---
x-pack/test/functional/apps/reporting/reporting.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/functional/apps/reporting/reporting.ts b/x-pack/test/functional/apps/reporting/reporting.ts
index bd1626ec32380..9896e3371a282 100644
--- a/x-pack/test/functional/apps/reporting/reporting.ts
+++ b/x-pack/test/functional/apps/reporting/reporting.ts
@@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const es = getService('es');
const esArchiver = getService('esArchiver');
- describe('Reporting', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/102722
+ describe.skip('Reporting', function () {
this.tags(['smoke', 'ciGroup2']);
before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/packaging');
From d7f2325e09953e5c8239a7afd919300c567eac7f Mon Sep 17 00:00:00 2001
From: Jorge Sanz
Date: Mon, 21 Jun 2021 13:35:50 +0200
Subject: [PATCH 003/163] [Maps] [Docs] Added emsUrl to the main kibana config
doc (#102718)
---
docs/setup/settings.asciidoc | 3 +++
1 file changed, 3 insertions(+)
diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc
index ddb906f390a2d..c3c29adcea18f 100644
--- a/docs/setup/settings.asciidoc
+++ b/docs/setup/settings.asciidoc
@@ -325,6 +325,9 @@ The time interval policy will rotate the log file every given interval of time.
When `includeElasticMapsService` is turned off, only the vector layers configured by <>
and the tile layer configured by <> are available in <>. *Default: `true`*
+| `map.emsUrl:`
+ | Specifies the URL of a self hosted <>
+
| `map.proxyElasticMapsServiceInMaps:`
| Set to `true` to proxy all <> Elastic Maps Service
requests through the {kib} server. *Default: `false`*
From 307a5b2e93d59aefd3bb18d9b421d850a63b16e0 Mon Sep 17 00:00:00 2001
From: Nicolas Chaulet
Date: Mon, 21 Jun 2021 09:46:56 -0400
Subject: [PATCH 004/163] [Fleet] Use docLinks service to link to the doc
(#102660)
---
...-plugin-core-public.doclinksstart.links.md | 13 +++++++++
...kibana-plugin-core-public.doclinksstart.md | 2 +-
.../public/doc_links/doc_links_service.ts | 27 +++++++++++++++++
src/core/public/public.api.md | 13 +++++++++
.../components/agent_policy_form.tsx | 7 ++---
.../step_define_package_policy.tsx | 7 ++---
.../es_requirements_page.tsx | 13 ++++-----
.../fleet_server_requirement_page.tsx | 29 ++++++++++++++-----
.../components/fleet_server_upgrade_modal.tsx | 8 +++--
.../missing_fleet_server_host_callout.tsx | 10 +++----
.../fleet/public/components/alpha_flyout.tsx | 10 +++----
.../enrollment_instructions/manual/index.tsx | 15 +++-------
.../components/settings_flyout/index.tsx | 4 ++-
x-pack/plugins/fleet/public/hooks/use_core.ts | 1 +
14 files changed, 110 insertions(+), 49 deletions(-)
diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
index 9930ab7319f65..d3d76079cdc2a 100644
--- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
+++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
@@ -185,5 +185,18 @@ readonly links: {
readonly plugins: Record;
readonly snapshotRestore: Record;
readonly ingest: Record;
+ readonly fleet: Readonly<{
+ guide: string;
+ fleetServer: string;
+ fleetServerAddFleetServer: string;
+ settings: string;
+ settingsFleetServerHostSettings: string;
+ troubleshooting: string;
+ elasticAgent: string;
+ datastreams: string;
+ datastreamsNamingScheme: string;
+ upgradeElasticAgent: string;
+ upgradeElasticAgent712lower: string;
+ }>;
};
```
diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
index ab8cdea5e4d86..34279cef198bf 100644
--- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
+++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
@@ -17,5 +17,5 @@ export interface DocLinksStart
| --- | --- | --- |
| [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string
| |
| [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string
| |
-| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
}
| |
+| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
}
| |
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 06277d9351922..95091a761639b 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -22,6 +22,7 @@ export class DocLinksService {
const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/';
const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`;
const KIBANA_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/`;
+ const FLEET_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/fleet/${DOC_LINK_VERSION}/`;
const PLUGIN_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`;
return deepFreeze({
@@ -400,6 +401,19 @@ export class DocLinksService {
urlDecode: `${ELASTICSEARCH_DOCS}urldecode-processor.html`,
userAgent: `${ELASTICSEARCH_DOCS}user-agent-processor.html`,
},
+ fleet: {
+ guide: `${FLEET_DOCS}index.html`,
+ fleetServer: `${FLEET_DOCS}fleet-server.html`,
+ fleetServerAddFleetServer: `${FLEET_DOCS}fleet-server.html#add-fleet-server`,
+ settings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`,
+ settingsFleetServerHostSettings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`,
+ troubleshooting: `${FLEET_DOCS}fleet-troubleshooting.html`,
+ elasticAgent: `${FLEET_DOCS}elastic-agent-installation-configuration.html`,
+ datastreams: `${FLEET_DOCS}data-streams.html`,
+ datastreamsNamingScheme: `${FLEET_DOCS}data-streams.html#data-streams-naming-scheme`,
+ upgradeElasticAgent: `${FLEET_DOCS}upgrade-elastic-agent.html`,
+ upgradeElasticAgent712lower: `${FLEET_DOCS}upgrade-elastic-agent.html#upgrade-7.12-lower`,
+ },
},
});
}
@@ -587,5 +601,18 @@ export interface DocLinksStart {
readonly plugins: Record;
readonly snapshotRestore: Record;
readonly ingest: Record;
+ readonly fleet: Readonly<{
+ guide: string;
+ fleetServer: string;
+ fleetServerAddFleetServer: string;
+ settings: string;
+ settingsFleetServerHostSettings: string;
+ troubleshooting: string;
+ elasticAgent: string;
+ datastreams: string;
+ datastreamsNamingScheme: string;
+ upgradeElasticAgent: string;
+ upgradeElasticAgent712lower: string;
+ }>;
};
}
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index d3426b50f7614..6cc2b3f321fb7 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -664,6 +664,19 @@ export interface DocLinksStart {
readonly plugins: Record;
readonly snapshotRestore: Record;
readonly ingest: Record;
+ readonly fleet: Readonly<{
+ guide: string;
+ fleetServer: string;
+ fleetServerAddFleetServer: string;
+ settings: string;
+ settingsFleetServerHostSettings: string;
+ troubleshooting: string;
+ elasticAgent: string;
+ datastreams: string;
+ datastreamsNamingScheme: string;
+ upgradeElasticAgent: string;
+ upgradeElasticAgent712lower: string;
+ }>;
};
}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
index a44edb5ce9a42..25a0993242822 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
@@ -29,6 +29,7 @@ import styled from 'styled-components';
import { dataTypes } from '../../../../../../common';
import type { NewAgentPolicy, AgentPolicy } from '../../../types';
import { isValidNamespace } from '../../../services';
+import { useStartServices } from '../../../hooks';
import { AgentPolicyDeleteProvider } from './agent_policy_delete_provider';
@@ -83,6 +84,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({
isEditing = false,
onDelete = () => {},
}) => {
+ const { docLinks } = useStartServices();
const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({});
const fields: Array<{
name: 'name' | 'description' | 'namespace';
@@ -174,10 +176,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({
defaultMessage="Namespaces are a user-configurable arbitrary grouping that makes it easier to search for data and manage user permissions. A policy namespace is used to name its integration's data streams. {fleetUserGuide}."
values={{
fleetUserGuide: (
-
+
{i18n.translate(
'xpack.fleet.agentPolicyForm.nameSpaceFieldDescription.fleetUserGuideLabel',
{ defaultMessage: 'Learn more' }
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx
index 7444bed6ed3fd..c276e67cabbff 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx
@@ -29,6 +29,7 @@ import type {
} from '../../../types';
import { packageToPackagePolicy, pkgKeyFromPackageInfo } from '../../../services';
import { Loading } from '../../../components';
+import { useStartServices } from '../../../hooks';
import { isAdvancedVar } from './services';
import type { PackagePolicyValidationResults } from './services';
@@ -52,6 +53,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
validationResults,
submitAttempted,
}) => {
+ const { docLinks } = useStartServices();
// Form show/hide states
const [isShowingAdvanced, setIsShowingAdvanced] = useState(false);
@@ -167,10 +169,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
defaultMessage="Change the default namespace inherited from the selected Agent policy. This setting changes the name of the integration's data stream. {learnMore}."
values={{
learnMore: (
-
+
{i18n.translate(
'xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNamespaceHelpLearnMoreLabel',
{ defaultMessage: 'Learn more' }
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx
index 5335432a13613..b4e6f1007536f 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx
@@ -24,6 +24,7 @@ import {
import { WithoutHeaderLayout } from '../../../layouts';
import type { GetFleetStatusResponse } from '../../../types';
+import { useStartServices } from '../../../hooks';
export const RequirementItem: React.FunctionComponent<{ isMissing: boolean }> = ({
isMissing,
@@ -50,6 +51,8 @@ export const RequirementItem: React.FunctionComponent<{ isMissing: boolean }> =
export const MissingESRequirementsPage: React.FunctionComponent<{
missingRequirements: GetFleetStatusResponse['missing_requirements'];
}> = ({ missingRequirements }) => {
+ const { docLinks } = useStartServices();
+
return (
@@ -79,7 +82,7 @@ export const MissingESRequirementsPage: React.FunctionComponent<{
values={{
esSecurityLink: (
@@ -104,7 +107,7 @@ export const MissingESRequirementsPage: React.FunctionComponent<{
true: true ,
apiKeyLink: (
@@ -128,11 +131,7 @@ xpack.security.authc.api_key.enabled: true`}
defaultMessage="For more information, read our {link} guide."
values={{
link: (
-
+
void;
}): EuiStepProps => {
+ const { docLinks } = useStartServices();
+
return {
title: i18n.translate('xpack.fleet.fleetServerSetup.stepInstallAgentTitle', {
defaultMessage: 'Start Fleet Server',
@@ -147,7 +149,11 @@ export const FleetServerCommandStep = ({
defaultMessage="From the agent directory, copy and run the appropriate quick start command to start an Elastic Agent as a Fleet Server using the generated token and a self-signed certificate. See the {userGuideLink} for instructions on using your own certificates for production deployment. All commands require administrator privileges."
values={{
userGuideLink: (
-
+
+
{
platform,
setPlatform,
} = useFleetServerInstructions();
+ const { docLinks } = useStartServices();
return (
@@ -304,7 +307,11 @@ const OnPremInstructions: React.FC = () => {
defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. See the {userGuideLink} for more information."
values={{
userGuideLink: (
-
+
{
};
const CloudInstructions: React.FC<{ deploymentUrl: string }> = ({ deploymentUrl }) => {
+ const { docLinks } = useStartServices();
+
return (
= ({ deploymentUrl
defaultMessage="A Fleet Server is required before you can enroll agents with Fleet. You can add one to your deployment by enabling APM & Fleet. For more information see the {link}"
values={{
link: (
-
+
= ({ onClose }) => {
const { getAssetsPath } = useLink();
- const { notifications, cloud } = useStartServices();
+ const { notifications, cloud, docLinks } = useStartServices();
const isCloud = !!cloud?.cloudId;
@@ -163,7 +163,11 @@ export const FleetServerUpgradeModal: React.FunctionComponent = ({ onClos
),
link: (
-
+
{
const { setModal } = useUrlModal();
+ const { docLinks } = useStartServices();
+
return (
{
defaultMessage="A URL for your Fleet Server host is required to enroll agents with Fleet. You can add this information in Fleet Settings. For more information, see the {link}."
values={{
link: (
-
+
void;
}
export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => {
+ const { docLinks } = useStartServices();
+
return (
@@ -49,11 +53,7 @@ export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => {
defaultMessage="Read our {docsLink} or go to our {forumLink} for questions or feedback."
values={{
docsLink: (
-
+
= ({
fleetServerHosts,
}) => {
const { platform, setPlatform } = usePlatform();
+ const { docLinks } = useStartServices();
const enrollArgs = getfleetServerHostsEnrollArgs(apiKey, fleetServerHosts);
@@ -85,11 +86,7 @@ export const ManualInstructions: React.FunctionComponent = ({
defaultMessage="See the {link} for RPM / DEB deploy instructions."
values={{
link: (
-
+
= ({
defaultMessage="If you are having trouble connecting, see our {link}."
values={{
link: (
-
+
void) {
}
export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
+ const { docLinks } = useStartServices();
+
const settingsRequest = useGetSettings();
const settings = settingsRequest?.data?.item;
const outputsRequest = useGetOutputs();
@@ -302,7 +304,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
values={{
link: (
diff --git a/x-pack/plugins/fleet/public/hooks/use_core.ts b/x-pack/plugins/fleet/public/hooks/use_core.ts
index be4a21a094bd4..2c817bfc938f8 100644
--- a/x-pack/plugins/fleet/public/hooks/use_core.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_core.ts
@@ -13,5 +13,6 @@ export function useStartServices(): FleetStartServices {
if (services === null) {
throw new Error('KibanaContextProvider not initialized');
}
+
return services;
}
From f3ec948bee73b848e10270d03677606a98de89bc Mon Sep 17 00:00:00 2001
From: Mikhail Shustov
Date: Mon, 21 Jun 2021 16:03:00 +0200
Subject: [PATCH 005/163] Cleanup outdated @elastic/elasticsearch client type
errors (#101741)
* fix errors and update comments in Core
* fix errors or update comments in Security plugin
* update spaces test
* update task_manager files
* update comments in monitoring plugin
* fix errors in update comments in security_solutions
* fix errors and update comments in data_enhanced
* update fleet code
* update infra code
* update comment in trigger_actions_ui
* update comment in lens
* update comments in ES-UI code
* update typings for search
* update monitoring
* remove outdated export
---
.../core_usage_data/core_usage_data_service.ts | 14 ++++++--------
.../migrations/core/index_migrator.ts | 8 ++------
.../saved_objects/service/lib/repository.ts | 4 ++--
src/core/types/elasticsearch/search.ts | 2 ++
.../get_saved_object_counts.ts | 2 +-
.../server/check_cluster_data.test.ts | 18 +++++++-----------
.../data_enhanced/server/collectors/fetch.ts | 10 +++++++---
.../server/search/session/get_search_status.ts | 2 +-
.../server/routes/data_streams/handlers.ts | 12 +++++++-----
.../services/api_keys/enrollment_api_key.ts | 2 +-
.../evaluate_condition.ts | 2 +-
.../log_entries/log_entry_search_strategy.ts | 2 +-
.../lens/server/usage/visualization_counts.ts | 2 +-
.../lib/alerts/fetch_ccr_read_exceptions.ts | 2 +-
.../alerts/fetch_cpu_usage_node_stats.test.ts | 6 +++---
.../alerts/fetch_disk_usage_node_stats.test.ts | 2 +-
.../lib/alerts/fetch_disk_usage_node_stats.ts | 2 +-
.../lib/alerts/fetch_index_shard_size.ts | 2 +-
.../lib/alerts/fetch_kibana_versions.test.ts | 2 +-
.../lib/alerts/fetch_logstash_versions.test.ts | 2 +-
.../alerts/fetch_memory_usage_node_stats.ts | 2 +-
.../fetch_missing_monitoring_data.test.ts | 4 ++--
.../alerts/fetch_nodes_from_cluster_stats.ts | 2 +-
.../fetch_thread_pool_rejections_stats.ts | 2 +-
x-pack/plugins/security/common/model/user.ts | 4 ++--
.../management/users/edit_user/user_form.tsx | 4 ++--
.../server/authentication/api_keys/api_keys.ts | 2 +-
.../server/authentication/providers/base.ts | 2 +-
.../server/authentication/providers/token.ts | 2 +-
.../security/server/authentication/tokens.ts | 2 +-
.../server/routes/authorization/roles/get.ts | 2 +-
.../routes/authorization/roles/get_all.ts | 2 +-
.../security/server/routes/role_mapping/get.ts | 2 +-
.../routes/resolver/tree/queries/stats.ts | 2 +-
.../migrations/get_signal_versions_by_index.ts | 5 ++---
.../migrations/get_signals_indices_in_range.ts | 5 ++---
.../threshold/bulk_create_threshold_signals.ts | 1 -
.../factory/hosts/overview/index.ts | 2 +-
.../factory/network/overview/index.ts | 2 +-
.../server/routes/api/snapshots.ts | 6 ++----
.../monitoring/workload_statistics.test.ts | 10 +++++-----
.../plugins/task_manager/server/task_store.ts | 10 ++++------
.../transform/server/routes/api/transforms.ts | 2 +-
.../server/data/lib/time_series_query.ts | 2 +-
.../common/suites/copy_to_space.ts | 9 ++++++---
45 files changed, 90 insertions(+), 96 deletions(-)
diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts
index dc24f889cd8dd..afe1b45175f86 100644
--- a/src/core/server/core_usage_data/core_usage_data_service.ts
+++ b/src/core/server/core_usage_data/core_usage_data_service.ts
@@ -126,14 +126,12 @@ export class CoreUsageDataService implements CoreService): SavedObjectsFindResult => ({
- // @ts-expect-error @elastic/elasticsearch declared Id as string | number
+ // @ts-expect-error @elastic/elasticsearch _source is optional
...this._rawToSavedObject(hit),
score: hit._score!,
- // @ts-expect-error @elastic/elasticsearch declared sort as string | number
+ // @ts-expect-error @elastic/elasticsearch _source is optional
sort: hit.sort,
})
),
diff --git a/src/core/types/elasticsearch/search.ts b/src/core/types/elasticsearch/search.ts
index 36a684fb097a5..0960fb189a341 100644
--- a/src/core/types/elasticsearch/search.ts
+++ b/src/core/types/elasticsearch/search.ts
@@ -417,7 +417,9 @@ export type AggregateOf<
{
key: string;
from?: number;
+ from_as_string?: string;
to?: number;
+ to_as_string?: string;
doc_count: number;
},
TAggregationContainer extends { range: { ranges: Array } }
diff --git a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts
index 9927b27da6c8f..eeaeed67e753f 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts
@@ -26,6 +26,6 @@ export async function getSavedObjectsCounts(
},
};
const { body } = await esClient.search(savedObjectCountSearchParams);
- // @ts-expect-error @elastic/elasticsearch Aggregate does not include `buckets`
+ // @ts-expect-error declare type for aggregations explicitly
return body.aggregations?.types?.buckets || [];
}
diff --git a/src/plugins/security_oss/server/check_cluster_data.test.ts b/src/plugins/security_oss/server/check_cluster_data.test.ts
index 9e9459a68754c..6aa1cc9a28c39 100644
--- a/src/plugins/security_oss/server/check_cluster_data.test.ts
+++ b/src/plugins/security_oss/server/check_cluster_data.test.ts
@@ -27,20 +27,19 @@ describe('checkClusterForUserData', () => {
it('returns false if data only exists in system indices', async () => {
const esClient = elasticsearchServiceMock.createElasticsearchClient();
esClient.cat.indices.mockResolvedValue(
- // @ts-expect-error @elastic/elasticsearch ES types don't support array response format
elasticsearchServiceMock.createApiResponse({
body: [
{
index: '.kibana',
- 'docs.count': 500,
+ 'docs.count': '500',
},
{
index: 'kibana_sample_ecommerce_data',
- 'docs.count': 20,
+ 'docs.count': '20',
},
{
index: '.somethingElse',
- 'docs.count': 20,
+ 'docs.count': '20',
},
],
})
@@ -56,16 +55,15 @@ describe('checkClusterForUserData', () => {
it('returns true if data exists in non-system indices', async () => {
const esClient = elasticsearchServiceMock.createElasticsearchClient();
esClient.cat.indices.mockResolvedValue(
- // @ts-expect-error @elastic/elasticsearch ES types don't support array response format
elasticsearchServiceMock.createApiResponse({
body: [
{
index: '.kibana',
- 'docs.count': 500,
+ 'docs.count': '500',
},
{
index: 'some_real_index',
- 'docs.count': 20,
+ 'docs.count': '20',
},
],
})
@@ -87,23 +85,21 @@ describe('checkClusterForUserData', () => {
)
.mockRejectedValueOnce(new Error('something terrible happened'))
.mockResolvedValueOnce(
- // @ts-expect-error @elastic/elasticsearch ES types don't support array response format
elasticsearchServiceMock.createApiResponse({
body: [
{
index: '.kibana',
- 'docs.count': 500,
+ 'docs.count': '500',
},
],
})
)
.mockResolvedValueOnce(
- // @ts-expect-error @elastic/elasticsearch ES types don't support array response format
elasticsearchServiceMock.createApiResponse({
body: [
{
index: 'some_real_index',
- 'docs.count': 20,
+ 'docs.count': '20',
},
],
})
diff --git a/x-pack/plugins/data_enhanced/server/collectors/fetch.ts b/x-pack/plugins/data_enhanced/server/collectors/fetch.ts
index 6feb13432d07b..bfd5ee745cd75 100644
--- a/x-pack/plugins/data_enhanced/server/collectors/fetch.ts
+++ b/x-pack/plugins/data_enhanced/server/collectors/fetch.ts
@@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
+import type { estypes } from '@elastic/elasticsearch';
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { SearchResponse } from 'elasticsearch';
@@ -36,8 +36,12 @@ export function fetchProvider(config$: Observable, logger: L
},
});
- // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregations
- const buckets: SessionPersistedTermsBucket[] = esResponse.aggregations!.persisted.buckets;
+ const aggs = esResponse.aggregations as Record<
+ string,
+ estypes.AggregationsMultiBucketAggregate
+ >;
+
+ const buckets = aggs.persisted.buckets;
if (!buckets.length) {
return { transientCount: 0, persistedCount: 0, totalCount: 0 };
}
diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts
index e228ba725489c..461c41b46491c 100644
--- a/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts
+++ b/x-pack/plugins/data_enhanced/server/search/session/get_search_status.ts
@@ -18,7 +18,7 @@ export async function getSearchStatus(
): Promise> {
// TODO: Handle strategies other than the default one
try {
- // @ts-expect-error @elastic/elasticsearch status method is not defined
+ // @ts-expect-error start_time_in_millis: EpochMillis is string | number
const apiResponse: ApiResponse = await client.asyncSearch.status({
id: asyncId,
});
diff --git a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts
index b80ddfe8e7c9b..073ff7806d9fe 100644
--- a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts
+++ b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts
@@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
+import type { estypes } from '@elastic/elasticsearch';
import { keyBy, keys, merge } from 'lodash';
import type { RequestHandler, SavedObjectsBulkGetObject } from 'src/core/server';
@@ -140,10 +140,7 @@ export const getListHandler: RequestHandler = async (context, request, response)
// Query backing indices to extract data stream dataset, namespace, and type values
const {
- body: {
- // @ts-expect-error @elastic/elasticsearch aggregations are not typed
- aggregations: { dataset, namespace, type },
- },
+ body: { aggregations: dataStreamAggs },
} = await esClient.search({
index: dataStream.indices.map((index) => index.index_name),
body: {
@@ -187,6 +184,11 @@ export const getListHandler: RequestHandler = async (context, request, response)
},
});
+ const { dataset, namespace, type } = dataStreamAggs as Record<
+ string,
+ estypes.AggregationsMultiBucketAggregate<{ key?: string }>
+ >;
+
// Set values from backing indices query
dataStreamResponse.dataset = dataset.buckets[0]?.key || '';
dataStreamResponse.namespace = namespace.buckets[0]?.key || '';
diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts
index 8f575f4969cf4..14d43e6e219db 100644
--- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts
+++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts
@@ -47,7 +47,7 @@ export async function listEnrollmentApiKeys(
body: query ? { query } : undefined,
});
- // @ts-expect-error @elastic/elasticsearch
+ // @ts-expect-error @elastic/elasticsearch _source is optional
const items = res.body.hits.hits.map(esDocToEnrollmentApiKey);
return {
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
index cf3d8a15b7b65..922b10e8bd2b0 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
@@ -123,7 +123,7 @@ const getData = async (
const client = async (
options: CallWithRequestParams
): Promise> =>
- // @ts-expect-error @elastic/elasticsearch SearchResponse.body.timeout is not required
+ // @ts-expect-error SearchResponse.body.timeout is optional
(await esClient.search(options)).body as InfraDatabaseSearchResponse;
const metrics = [
diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts
index aa34204b9fb44..1f0f13eeb6ca9 100644
--- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts
+++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts
@@ -84,7 +84,7 @@ export const logEntrySearchStrategyProvider = ({
tiebreakerField,
runtimeMappings,
}): IEsSearchRequest => ({
- // @ts-expect-error @elastic/elasticsearch declares indices_boost as Record
+ // @ts-expect-error `Field` is not assignable to `SearchRequest.docvalue_fields`
params: createGetLogEntryQuery(
indices,
params.logEntryId,
diff --git a/x-pack/plugins/lens/server/usage/visualization_counts.ts b/x-pack/plugins/lens/server/usage/visualization_counts.ts
index f0c48fb1152e8..6e79d5f342377 100644
--- a/x-pack/plugins/lens/server/usage/visualization_counts.ts
+++ b/x-pack/plugins/lens/server/usage/visualization_counts.ts
@@ -75,7 +75,7 @@ export async function getVisualizationCounts(
},
});
- // @ts-expect-error @elastic/elasticsearch no way to declare aggregations for search response
+ // @ts-expect-error specify aggregations type explicitly
const buckets = results.aggregations.groups.buckets;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts
index 213e73a4b9534..a96a7454ea744 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts
@@ -95,7 +95,7 @@ export async function fetchCCRReadExceptions(
const { body: response } = await esClient.search(params);
const stats: CCRReadExceptionsStats[] = [];
- // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets
+ // @ts-expect-error declare aggegations type explicitly
const { buckets: remoteClusterBuckets = [] } = response.aggregations?.remote_clusters;
if (!remoteClusterBuckets?.length) {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts
index 0fb9dd5298e9e..9cb773c81923b 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts
@@ -25,7 +25,7 @@ describe('fetchCpuUsageNodeStats', () => {
it('fetch normal stats', async () => {
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
clusters: {
@@ -79,7 +79,7 @@ describe('fetchCpuUsageNodeStats', () => {
it('fetch container stats', async () => {
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
clusters: {
@@ -146,7 +146,7 @@ describe('fetchCpuUsageNodeStats', () => {
it('fetch properly return ccs', async () => {
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
clusters: {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts
index 8faf79fc4b59c..4766400891af5 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts
@@ -25,7 +25,7 @@ describe('fetchDiskUsageNodeStats', () => {
it('fetch normal stats', async () => {
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
clusters: {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts
index a51dccd727966..2e8b5c7478e15 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts
@@ -101,7 +101,7 @@ export async function fetchDiskUsageNodeStats(
const { body: response } = await esClient.search(params);
const stats: AlertDiskUsageNodeStats[] = [];
- // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets
+ // @ts-expect-error declare type for aggregations explicitly
const { buckets: clusterBuckets } = response.aggregations?.clusters;
if (!clusterBuckets?.length) {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts
index aab3f0101ef83..117894c0d823b 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts
@@ -105,7 +105,7 @@ export async function fetchIndexShardSize(
};
const { body: response } = await esClient.search(params);
- // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets
+ // @ts-expect-error declare aggegations type explicitly
const { buckets: clusterBuckets } = response.aggregations?.clusters;
const stats: IndexShardSizeStats[] = [];
if (!clusterBuckets?.length) {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts
index 2b966b16f2f5c..f9a03bb73d5fc 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts
@@ -23,7 +23,7 @@ describe('fetchKibanaVersions', () => {
it('fetch as expected', async () => {
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
index: {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts
index d7d4e6531f58e..5732fc00f009b 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts
@@ -23,7 +23,7 @@ describe('fetchLogstashVersions', () => {
it('fetch as expected', async () => {
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
index: {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts
index 245838541d435..46bb9c794a6a6 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts
@@ -94,7 +94,7 @@ export async function fetchMemoryUsageNodeStats(
const { body: response } = await esClient.search(params);
const stats: AlertMemoryUsageNodeStats[] = [];
- // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets
+ // @ts-expect-error declare type for aggregations explicitly
const { buckets: clusterBuckets } = response.aggregations?.clusters;
if (!clusterBuckets?.length) {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts
index c8d15acf8ff73..980adb009ff8f 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts
@@ -56,7 +56,7 @@ describe('fetchMissingMonitoringData', () => {
];
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
clusters: {
@@ -115,7 +115,7 @@ describe('fetchMissingMonitoringData', () => {
},
];
esClient.search.mockReturnValue(
- // @ts-expect-error @elastic/elasticsearch Aggregate only allows unknown values
+ // @ts-expect-error not full response interface
elasticsearchClientMock.createSuccessTransportRequestPromise({
aggregations: {
clusters: {
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts
index d1a343b9b3eef..5f867ca5b6edf 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts
@@ -90,7 +90,7 @@ export async function fetchNodesFromClusterStats(
const { body: response } = await esClient.search(params);
const nodes: AlertClusterStatsNodes[] = [];
- // @ts-expect-error @elastic/elasticsearch Aggregate does not define buckets
+ // @ts-expect-error declare type for aggregations explicitly
const clusterBuckets = response.aggregations?.clusters?.buckets;
if (!clusterBuckets?.length) {
return nodes;
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts
index db5943ca67031..954ec3877144f 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts
@@ -96,7 +96,7 @@ export async function fetchThreadPoolRejectionStats(
const { body: response } = await esClient.search(params);
const stats: AlertThreadPoolRejectionsStats[] = [];
- // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets
+ // @ts-expect-error declare type for aggregations explicitly
const { buckets: clusterBuckets } = response.aggregations?.clusters;
if (!clusterBuckets?.length) {
diff --git a/x-pack/plugins/security/common/model/user.ts b/x-pack/plugins/security/common/model/user.ts
index 3f4787b207f88..2bcea659699cb 100644
--- a/x-pack/plugins/security/common/model/user.ts
+++ b/x-pack/plugins/security/common/model/user.ts
@@ -7,8 +7,8 @@
export interface User {
username: string;
- email: string;
- full_name: string;
+ email?: string;
+ full_name?: string;
roles: readonly string[];
enabled: boolean;
metadata?: {
diff --git a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx
index 29d87e31797cc..8101c09d64907 100644
--- a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx
+++ b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx
@@ -41,8 +41,8 @@ export const THROTTLE_USERS_WAIT = 10000;
export interface UserFormValues {
username?: string;
- full_name: string;
- email: string;
+ full_name?: string;
+ email?: string;
password?: string;
confirm_password?: string;
roles: readonly string[];
diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts
index 4cbca1c70f507..1707ca710aaf8 100644
--- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts
+++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts
@@ -224,7 +224,7 @@ export class APIKeys {
try {
result = (
await this.clusterClient.asInternalUser.security.grantApiKey({
- // @ts-expect-error @elastic/elasticsearch api_key.role_descriptors
+ // @ts-expect-error @elastic/elasticsearch api_key.role_descriptors doesn't support `Record`
body: params,
})
).body;
diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts
index c7c0edcf1e9e1..f6d9af24ee1ad 100644
--- a/x-pack/plugins/security/server/authentication/providers/base.ts
+++ b/x-pack/plugins/security/server/authentication/providers/base.ts
@@ -117,7 +117,7 @@ export abstract class BaseAuthenticationProvider {
*/
protected async getUser(request: KibanaRequest, authHeaders: Headers = {}) {
return this.authenticationInfoToAuthenticatedUser(
- // @ts-expect-error @elastic/elasticsearch `AuthenticateResponse` type doesn't define `authentication_type` and `enabled`.
+ // @ts-expect-error Metadata is defined as Record
(
await this.options.client
.asScoped({ headers: { ...request.headers, ...authHeaders } })
diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts
index 43338a8f6400f..fae0d7ca69038 100644
--- a/x-pack/plugins/security/server/authentication/providers/token.ts
+++ b/x-pack/plugins/security/server/authentication/providers/token.ts
@@ -84,7 +84,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider {
this.logger.debug('Get token API request to Elasticsearch successful');
return AuthenticationResult.succeeded(
this.authenticationInfoToAuthenticatedUser(
- // @ts-expect-error @elastic/elasticsearch GetUserAccessTokenResponse declares authentication: string, but expected AuthenticatedUser
+ // @ts-expect-error @elastic/elasticsearch metadata defined as Record;
authenticationInfo as AuthenticationInfo
),
{
diff --git a/x-pack/plugins/security/server/authentication/tokens.ts b/x-pack/plugins/security/server/authentication/tokens.ts
index 1adbb2dc66533..47051cc08da23 100644
--- a/x-pack/plugins/security/server/authentication/tokens.ts
+++ b/x-pack/plugins/security/server/authentication/tokens.ts
@@ -73,7 +73,7 @@ export class Tokens {
return {
accessToken,
refreshToken,
- // @ts-expect-error @elastic/elasticsearch declared GetUserAccessTokenResponse.authentication: string
+ // @ts-expect-error @elastic/elasticsearch user metadata defined as Record
authenticationInfo: authenticationInfo as AuthenticationInfo,
};
} catch (err) {
diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts
index 01d32f7fb8233..075a8d133f1e6 100644
--- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts
+++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts
@@ -32,7 +32,7 @@ export function defineGetRolesRoutes({ router, authz }: RouteDefinitionParams) {
if (elasticsearchRole) {
return response.ok({
body: transformElasticsearchRoleToRole(
- // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`.
+ // @ts-expect-error `SecurityIndicesPrivileges.names` expected to be `string[]`
elasticsearchRole,
request.params.name,
authz.applicationName
diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts
index 4d458be4e332f..be0880a06d59d 100644
--- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts
+++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts
@@ -27,7 +27,7 @@ export function defineGetAllRolesRoutes({ router, authz }: RouteDefinitionParams
body: Object.entries(elasticsearchRoles)
.map(([roleName, elasticsearchRole]) =>
transformElasticsearchRoleToRole(
- // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`.
+ // @ts-expect-error @elastic/elasticsearch SecurityIndicesPrivileges.names expected to be string[]
elasticsearchRole,
roleName,
authz.applicationName
diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts
index 67cd8975b76eb..257b4210b13f7 100644
--- a/x-pack/plugins/security/server/routes/role_mapping/get.ts
+++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts
@@ -36,7 +36,7 @@ export function defineRoleMappingGetRoutes(params: RouteDefinitionParams) {
return {
name,
...mapping,
- // @ts-expect-error @elastic/elasticsearch `XPackRoleMapping` type doesn't define `role_templates` property.
+ // @ts-expect-error @elastic/elasticsearch `SecurityRoleMapping` doeesn't contain `role_templates`
role_templates: (mapping.role_templates || []).map((entry: RoleTemplate) => {
return {
...entry,
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts
index f21259980d464..b64390f4e382f 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts
@@ -128,7 +128,7 @@ export class StatsQuery {
index: this.indexPatterns,
});
- // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response
+ // @ts-expect-error declare aggegations type explicitly
return response.body.aggregations?.ids?.buckets.reduce(
(cummulative: Record, bucket: CategoriesAgg) => ({
...cummulative,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts
index 784164e430ff0..decde16d77a38 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signal_versions_by_index.ts
@@ -72,9 +72,8 @@ export const getSignalVersionsByIndex = async ({
},
});
- // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response
- const body = response.body as SignalVersionsAggResponse;
- const indexBuckets = body.aggregations.signals_indices.buckets;
+ const aggs = response.body.aggregations as SignalVersionsAggResponse['aggregations'];
+ const indexBuckets = aggs.signals_indices.buckets;
return index.reduce((agg, _index) => {
const bucket = indexBuckets.find((ib) => ib.key === _index);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts
index 3c9132fc81279..af236b10d0795 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_signals_indices_in_range.ts
@@ -72,7 +72,6 @@ export const getSignalsIndicesInRange = async ({
},
});
- // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response
- const body = response.body as IndexesResponse;
- return body.aggregations.indexes.buckets.map((bucket) => bucket.key);
+ const aggs = response.body.aggregations as IndexesResponse['aggregations'];
+ return aggs.indexes.buckets.map((bucket) => bucket.key);
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts
index 08fa2f14a0fd5..f56ed3a5e9eb4 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts
@@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import { get } from 'lodash/fp';
import set from 'set-value';
import {
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts
index c58e450806849..5866695ab1641 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts
@@ -24,7 +24,7 @@ export const hostOverview: SecuritySolutionFactory = {
options: HostOverviewRequestOptions,
response: IEsSearchResponse
): Promise => {
- // @ts-expect-error @elastic/elasticsearch no way to declare type for aggregations
+ // @ts-expect-error specify aggregations type explicitly
const aggregations: OverviewHostHit = get('aggregations', response.rawResponse) || {};
const inspect = {
dsl: [inspectStringifyObject(buildOverviewHostQuery(options))],
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts
index 1f85a119f3c8e..069125c6700eb 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/overview/index.ts
@@ -24,7 +24,7 @@ export const networkOverview: SecuritySolutionFactory =
options: NetworkOverviewRequestOptions,
response: IEsSearchResponse
): Promise => {
- // @ts-expect-error @elastic/elasticsearch no way to declare type for aggregations
+ // @ts-expect-error specify aggregations type explicitly
const aggregations: OverviewNetworkHit = get('aggregations', response.rawResponse) || {};
const inspect = {
dsl: [inspectStringifyObject(buildOverviewNetworkQuery(options))],
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts
index 8ce33cb8cd05b..af9c08f76f6f7 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts
@@ -73,12 +73,10 @@ export function registerSnapshotsRoutes({
ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable.
});
- const { responses: fetchedResponses } = response.body;
+ const { responses: fetchedResponses = [] } = response.body;
// Decorate each snapshot with the repository with which it's associated.
- // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client
- fetchedResponses.forEach(({ snapshots: fetchedSnapshots }) => {
- // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client
+ fetchedResponses.forEach(({ snapshots: fetchedSnapshots = [] }) => {
fetchedSnapshots.forEach((snapshot) => {
snapshots.push(
deserializeSnapshotDetails(
diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts
index e88144f2b4a35..3fe003ebc6591 100644
--- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts
+++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts
@@ -788,11 +788,11 @@ describe('padBuckets', () => {
padBuckets(10, 3000, {
key: '2020-10-02T19:47:28.128Z-2020-10-02T19:48:28.128Z',
from: 1601668048128,
- // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property
from_as_string: '2020-10-02T19:47:28.128Z',
to: 1601668108128,
to_as_string: '2020-10-02T19:48:28.128Z',
doc_count: 0,
+ // @ts-expect-error result type doesn't define histogram
histogram: {
buckets: [],
},
@@ -805,11 +805,11 @@ describe('padBuckets', () => {
padBuckets(10, 3000, {
key: '2020-10-02T19:47:28.128Z-2020-10-02T19:48:28.128Z',
from: 1601668046000,
- // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property
from_as_string: '2020-10-02T19:47:26.000Z',
to: 1601668076000,
to_as_string: '2020-10-02T19:47:56.000Z',
doc_count: 3,
+ // @ts-expect-error result type doesn't define histogram
histogram: {
buckets: [
{
@@ -883,11 +883,11 @@ describe('padBuckets', () => {
padBuckets(10, 3000, {
key: '2020-10-02T20:39:45.793Z-2020-10-02T20:40:14.793Z',
from: 1601671183000,
- // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property
from_as_string: '2020-10-02T20:39:43.000Z',
to: 1601671213000,
to_as_string: '2020-10-02T20:40:13.000Z',
doc_count: 2,
+ // @ts-expect-error result type doesn't define histogram
histogram: {
buckets: [
{
@@ -913,11 +913,11 @@ describe('padBuckets', () => {
padBuckets(20, 3000, {
key: '2020-10-02T20:39:45.793Z-2020-10-02T20:40:14.793Z',
from: 1601671185793,
- // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property
from_as_string: '2020-10-02T20:39:45.793Z',
to: 1601671245793,
to_as_string: '2020-10-02T20:40:45.793Z',
doc_count: 2,
+ // @ts-expect-error result type doesn't define histogram
histogram: {
buckets: [
{
@@ -943,11 +943,11 @@ describe('padBuckets', () => {
padBuckets(20, 3000, {
key: '2021-02-02T10:08:32.161Z-2021-02-02T10:09:32.161Z',
from: 1612260512161,
- // @ts-expect-error @elastic/elasticsearch doesn't decalre from_as_string property
from_as_string: '2021-02-02T10:08:32.161Z',
to: 1612260572161,
to_as_string: '2021-02-02T10:09:32.161Z',
doc_count: 2,
+ // @ts-expect-error result type doesn't define histogram
histogram: {
buckets: [
{
diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts
index ce01660134683..0a8335ebe98f3 100644
--- a/x-pack/plugins/task_manager/server/task_store.ts
+++ b/x-pack/plugins/task_manager/server/task_store.ts
@@ -319,9 +319,9 @@ export class TaskStore {
return {
docs: tasks
- // @ts-expect-error @elastic/elasticsearch `Hid._id` expected to be `string`
+ // @ts-expect-error @elastic/elasticsearch _source is optional
.filter((doc) => this.serializer.isRawSavedObject(doc))
- // @ts-expect-error @elastic/elasticsearch `Hid._id` expected to be `string`
+ // @ts-expect-error @elastic/elasticsearch _source is optional
.map((doc) => this.serializer.rawToSavedObject(doc))
.map((doc) => omit(doc, 'namespace') as SavedObject)
.map(savedObjectToConcreteTaskInstance),
@@ -379,10 +379,8 @@ export class TaskStore {
);
return {
- // @ts-expect-error @elastic/elasticsearch declares UpdateByQueryResponse.total as optional
- total,
- // @ts-expect-error @elastic/elasticsearch declares UpdateByQueryResponse.total as optional
- updated,
+ total: total || 0,
+ updated: updated || 0,
version_conflicts: conflictsCorrectedForContinuation,
};
} catch (e) {
diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts
index ea2bb28776ac2..aa30a60b3421c 100644
--- a/x-pack/plugins/transform/server/routes/api/transforms.ts
+++ b/x-pack/plugins/transform/server/routes/api/transforms.ts
@@ -208,7 +208,7 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) {
await ctx.core.elasticsearch.client.asCurrentUser.transform
.putTransform({
- // @ts-expect-error @elastic/elasticsearch max_page_search_size is required in TransformPivot
+ // @ts-expect-error @elastic/elasticsearch group_by is expected to be optional in TransformPivot
body: req.body,
transform_id: transformId,
})
diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts
index a2ba8d43c9c60..6b2849b7b9670 100644
--- a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts
+++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts
@@ -165,7 +165,7 @@ export function getResultFromEs(
delete aggregations.dateAgg;
}
- // @ts-expect-error @elastic/elasticsearch Aggregate does not specify buckets
+ // @ts-expect-error specify aggregations type explicitly
const groupBuckets = aggregations.groupAgg?.buckets || [];
const result: TimeSeriesResult = {
results: [],
diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
index ead34f6be8850..d187228a83b17 100644
--- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
+++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
@@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
+import type { estypes } from '@elastic/elasticsearch';
import expect from '@kbn/expect';
import { SuperTest } from 'supertest';
import { EsArchiver } from '@kbn/es-archiver';
@@ -93,9 +93,12 @@ export function copyToSpaceTestSuiteFactory(
},
});
+ const aggs = response.aggregations as Record<
+ string,
+ estypes.AggregationsMultiBucketAggregate
+ >;
return {
- // @ts-expect-error @elastic/elasticsearch doesn't defined `count.buckets`.
- buckets: response.aggregations?.count.buckets as SpaceBucket[],
+ buckets: aggs.count.buckets,
};
};
From e97cfad38546651efb42b69070d4c621bd1c7e38 Mon Sep 17 00:00:00 2001
From: Robert Oskamp
Date: Mon, 21 Jun 2021 16:11:28 +0200
Subject: [PATCH 006/163] [ML] Functional tests - stabilize module tests
(#102708)
This PR stabilizes the `setupModule` API tests by increasing the timeout for the jobs to finish.
---
.../apis/ml/modules/setup_module.ts | 4 +--
x-pack/test/functional/services/ml/api.ts | 32 +++++++++++--------
2 files changed, 20 insertions(+), 16 deletions(-)
diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
index 30dc31ef460c6..6011c38255cdc 100644
--- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
+++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
@@ -1146,8 +1146,8 @@ export default ({ getService }: FtrProviderContext) => {
if (testData.requestBody.startDatafeed === true) {
await ml.api.waitForADJobRecordCountToBePositive(job.jobId);
}
- await ml.api.waitForJobState(job.jobId, job.jobState);
- await ml.api.waitForDatafeedState(datafeedId, job.datafeedState);
+ await ml.api.waitForDatafeedState(datafeedId, job.datafeedState, 4 * 60 * 1000);
+ await ml.api.waitForJobState(job.jobId, job.jobState, 4 * 60 * 1000);
// model memory limit should be <= 99mb
const {
diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts
index 317f2dfe60514..728e3ff8fc8e6 100644
--- a/x-pack/test/functional/services/ml/api.ts
+++ b/x-pack/test/functional/services/ml/api.ts
@@ -183,19 +183,19 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) {
return jobStats;
},
- async waitForJobState(jobId: string, expectedJobState: JOB_STATE) {
- await retry.waitForWithTimeout(
- `job state to be ${expectedJobState}`,
- 2 * 60 * 1000,
- async () => {
- const state = await this.getJobState(jobId);
- if (state === expectedJobState) {
- return true;
- } else {
- throw new Error(`expected job state to be ${expectedJobState} but got ${state}`);
- }
+ async waitForJobState(
+ jobId: string,
+ expectedJobState: JOB_STATE,
+ timeout: number = 2 * 60 * 1000
+ ) {
+ await retry.waitForWithTimeout(`job state to be ${expectedJobState}`, timeout, async () => {
+ const state = await this.getJobState(jobId);
+ if (state === expectedJobState) {
+ return true;
+ } else {
+ throw new Error(`expected job state to be ${expectedJobState} but got ${state}`);
}
- );
+ });
},
async getDatafeedState(datafeedId: string): Promise {
@@ -214,10 +214,14 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) {
return state;
},
- async waitForDatafeedState(datafeedId: string, expectedDatafeedState: DATAFEED_STATE) {
+ async waitForDatafeedState(
+ datafeedId: string,
+ expectedDatafeedState: DATAFEED_STATE,
+ timeout: number = 2 * 60 * 1000
+ ) {
await retry.waitForWithTimeout(
`datafeed state to be ${expectedDatafeedState}`,
- 2 * 60 * 1000,
+ timeout,
async () => {
const state = await this.getDatafeedState(datafeedId);
if (state === expectedDatafeedState) {
From 13671c45f25399d263dfa45ebcfa647fbceb9b03 Mon Sep 17 00:00:00 2001
From: Christos Nasikas
Date: Mon, 21 Jun 2021 17:38:26 +0300
Subject: [PATCH 007/163] [Cases] Add owner and description properties to
`kibana.json` (#102707)
---
x-pack/plugins/cases/kibana.json | 43 +++++++++++++++++++++-----------
1 file changed, 28 insertions(+), 15 deletions(-)
diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json
index 4a85a64c7e03a..6439f28b958d0 100644
--- a/x-pack/plugins/cases/kibana.json
+++ b/x-pack/plugins/cases/kibana.json
@@ -1,18 +1,31 @@
{
- "configPath": ["xpack", "cases"],
- "id": "cases",
- "kibanaVersion": "kibana",
- "extraPublicDirs": ["common"],
- "requiredPlugins": [
- "actions",
- "esUiShared",
- "features",
- "kibanaReact",
- "kibanaUtils",
- "triggersActionsUi"
+ "configPath":[
+ "cases",
+ "xpack"
],
- "optionalPlugins": ["spaces", "security"],
- "server": true,
- "ui": true,
- "version": "8.0.0"
+ "description":"The Case management system in Kibana",
+ "extraPublicDirs":[
+ "common"
+ ],
+ "id":"cases",
+ "kibanaVersion":"kibana",
+ "optionalPlugins":[
+ "security",
+ "spaces"
+ ],
+ "owner":{
+ "githubTeam":"security-threat-hunting",
+ "name":"Security Solution Threat Hunting"
+ },
+ "requiredPlugins":[
+ "actions",
+ "esUiShared",
+ "features",
+ "kibanaReact",
+ "kibanaUtils",
+ "triggersActionsUi"
+ ],
+ "server":true,
+ "ui":true,
+ "version":"8.0.0"
}
From 17e496c6b25a9bbf6f0cfd697ecb48180597ef3b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Casper=20H=C3=BCbertz?=
Date: Mon, 21 Jun 2021 16:44:06 +0200
Subject: [PATCH 008/163] [APM] Errors: Fix panels styles (#102734)
---
.../components/app/ErrorGroupDetails/DetailView/index.tsx | 2 +-
.../apm/public/components/app/ErrorGroupDetails/index.tsx | 6 +++++-
.../public/components/app/error_group_overview/index.tsx | 4 ++--
3 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx
index da55f274bd77c..11926dd965f95 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx
@@ -85,7 +85,7 @@ export function DetailView({ errorGroup, urlParams }: Props) {
const status = error.http?.response?.status_code;
return (
-
+
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx
index 0f2180721afe3..3d22c3863c100 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx
@@ -146,9 +146,13 @@ export function ErrorGroupDetails({
return (
<>
+
+
-
+
+
+
{showDetails && (
diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx
index 95ec80b1a51bc..886ef8412f35b 100644
--- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx
@@ -73,7 +73,7 @@ export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) {
return (
-
+
-
+
{i18n.translate(
From 7765fc7c14420eee5736cb313e64a0c0b5e47255 Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 21 Jun 2021 16:53:27 +0200
Subject: [PATCH 009/163] [Lens] Fix time shift bug (#102528)
---
...-public.aggconfigs.getresolvedtimerange.md | 19 +++++++++++++++++++
...a-plugin-plugins-data-public.aggconfigs.md | 1 +
.../data/common/search/aggs/agg_config.ts | 5 ++---
.../data/common/search/aggs/agg_configs.ts | 15 ++++++++++++++-
src/plugins/data/public/public.api.md | 1 +
.../run_pipeline/esaggs_timeshift.ts | 19 +++++++++++++++++--
6 files changed, 54 insertions(+), 6 deletions(-)
create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md
new file mode 100644
index 0000000000000..2af44037292a2
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [getResolvedTimeRange](./kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md)
+
+## AggConfigs.getResolvedTimeRange() method
+
+Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set)
+
+Signature:
+
+```typescript
+getResolvedTimeRange(): import("../..").TimeRangeBounds | undefined;
+```
+Returns:
+
+`import("../..").TimeRangeBounds | undefined`
+
+Current time range as resolved date.
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md
index 45333b6767cac..9e671675b0b29 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md
@@ -42,6 +42,7 @@ export declare class AggConfigs
| [getAll()](./kibana-plugin-plugins-data-public.aggconfigs.getall.md) | | |
| [getRequestAggById(id)](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggbyid.md) | | |
| [getRequestAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggs.md) | | |
+| [getResolvedTimeRange()](./kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md) | | Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set) |
| [getResponseAggById(id)](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggbyid.md) | | Find a response agg by it's id. This may be an agg in the aggConfigs, or one created specifically for a response value |
| [getResponseAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggs.md) | | Gets the AggConfigs (and possibly ResponseAggConfigs) that represent the values that will be produced when all aggs are run.With multi-value metric aggs it is possible for a single agg request to result in multiple agg values, which is why the length of a vis' responseValuesAggs may be different than the vis' aggs {array\[AggConfig\]} |
| [getSearchSourceTimeFilter(forceNow)](./kibana-plugin-plugins-data-public.aggconfigs.getsearchsourcetimefilter.md) | | |
diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts
index 3c83b5bdf6084..9a35cf983c805 100644
--- a/src/plugins/data/common/search/aggs/agg_config.ts
+++ b/src/plugins/data/common/search/aggs/agg_config.ts
@@ -192,9 +192,8 @@ export class AggConfig {
} else if (!this.aggConfigs.timeRange) {
return;
}
- return moment.duration(
- moment(this.aggConfigs.timeRange.to).diff(this.aggConfigs.timeRange.from)
- );
+ const resolvedBounds = this.aggConfigs.getResolvedTimeRange()!;
+ return moment.duration(moment(resolvedBounds.max).diff(resolvedBounds.min));
}
return parsedTimeShift;
}
diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts
index 8593a0b0ed0fa..c205b46e077f0 100644
--- a/src/plugins/data/common/search/aggs/agg_configs.ts
+++ b/src/plugins/data/common/search/aggs/agg_configs.ts
@@ -23,7 +23,7 @@ import { IAggType } from './agg_type';
import { AggTypesRegistryStart } from './agg_types_registry';
import { AggGroupNames } from './agg_groups';
import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern';
-import { TimeRange, getTime, isRangeFilter } from '../../../common';
+import { TimeRange, getTime, isRangeFilter, calculateBounds } from '../../../common';
import { IBucketAggConfig } from './buckets';
import { insertTimeShiftSplit, mergeTimeShifts } from './utils/time_splits';
@@ -127,6 +127,19 @@ export class AggConfigs {
this.aggs.forEach(updateAggTimeRange);
}
+ /**
+ * Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set)
+ * @returns Current time range as resolved date.
+ */
+ getResolvedTimeRange() {
+ return (
+ this.timeRange &&
+ calculateBounds(this.timeRange, {
+ forceNow: this.forceNow,
+ })
+ );
+ }
+
// clone method will reuse existing AggConfig in the list (will not create new instances)
clone({ enabledOnly = true } = {}) {
const filterAggs = (agg: AggConfig) => {
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index d56727b468da6..4d9c69b137a3e 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -259,6 +259,7 @@ export class AggConfigs {
getRequestAggById(id: string): AggConfig | undefined;
// (undocumented)
getRequestAggs(): AggConfig[];
+ getResolvedTimeRange(): import("../..").TimeRangeBounds | undefined;
getResponseAggById(id: string): AggConfig | undefined;
getResponseAggs(): AggConfig[];
// (undocumented)
diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts
index c750602f735bd..8fc09ce2d7342 100644
--- a/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts
+++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts
@@ -71,6 +71,21 @@ export default function ({
expect(getCell(result, 0, 2)).to.be(4618);
});
+ it('shifts multiple metrics with relative time range and previous', async () => {
+ const expression = `
+ kibana_context timeRange={timerange from='${timeRange.from}' to='now'}
+ | esaggs index={indexPatternLoad id='logstash-*'}
+ aggs={aggCount id="1" enabled=true schema="metric"}
+ aggs={aggCount id="2" enabled=true schema="metric" timeShift="previous"}
+ `;
+ const result = await expectExpression(
+ 'esaggs_shift_multi_metric_previous',
+ expression
+ ).getResponse();
+ expect(getCell(result, 0, 0)).to.be(9247);
+ expect(getCell(result, 0, 1)).to.be(4763);
+ });
+
it('shifts single percentile', async () => {
const expression = `
kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'}
@@ -137,7 +152,7 @@ export default function ({
customMetric={aggAvg id="3"
field="bytes"
enabled=true
- schema="metric"
+ schema="metric"
}
enabled=true
schema="metric"
@@ -154,7 +169,7 @@ export default function ({
customMetric={aggAvg id="5"
field="bytes"
enabled=true
- schema="metric"
+ schema="metric"
}
enabled=true
schema="metric"
From 863e7091ddd7c39de53c248d17aa75b761d53441 Mon Sep 17 00:00:00 2001
From: Alison Goryachev
Date: Mon, 21 Jun 2021 12:09:57 -0400
Subject: [PATCH 010/163] [Ingest Node Pipelines] Remove default value for
error_distance param (#102222)
---
.../__jest__/processors/circle.test.tsx | 14 +++++-----
.../processor_form/processors/circle.tsx | 26 +++++++------------
2 files changed, 18 insertions(+), 22 deletions(-)
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx
index e29bb2ac6e92e..b8c8f6c58f711 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/circle.test.tsx
@@ -54,9 +54,10 @@ describe('Processor: Circle', () => {
// Click submit button with only the type defined
await saveNewProcessor();
- // Expect form error as "field" and "shape_type" are required parameters
+ // Expect form error as "field", "shape_type" and "error_distance" are required parameters
expect(form.getErrorsMessages()).toEqual([
'A field value is required.',
+ 'An error distance value is required.',
'A shape type value is required.',
]);
});
@@ -91,15 +92,15 @@ describe('Processor: Circle', () => {
form,
} = testBed;
- // Add "field" value (required)
+ // Set required parameters
form.setInputValue('fieldNameField.input', 'field_1');
- // Select the shape
form.setSelectValue('shapeSelectorField', 'geo_shape');
- // Add "target_field" value
- form.setInputValue('targetField.input', 'target_field');
-
form.setInputValue('errorDistanceField.input', '10');
+ // Set optional parameters
+ form.setInputValue('targetField.input', 'target_field');
+ form.toggleEuiSwitch('ignoreMissingSwitch.input');
+
// Save the field with new changes
await saveNewProcessor();
@@ -109,6 +110,7 @@ describe('Processor: Circle', () => {
error_distance: 10,
shape_type: 'geo_shape',
target_field: 'target_field',
+ ignore_missing: true,
});
});
});
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx
index 74a7f37d841ae..87e08eaeea6e6 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/circle.tsx
@@ -13,6 +13,7 @@ import { EuiCode } from '@elastic/eui';
import {
FIELD_TYPES,
fieldValidators,
+ fieldFormatters,
UseField,
SelectField,
NumericField,
@@ -24,13 +25,13 @@ import { FieldNameField } from './common_fields/field_name_field';
import { TargetField } from './common_fields/target_field';
const { emptyField } = fieldValidators;
+const { toInt } = fieldFormatters;
const fieldsConfig: FieldsConfig = {
/* Required fields config */
error_distance: {
type: FIELD_TYPES.NUMBER,
- deserializer: (v) => (typeof v === 'number' && !isNaN(v) ? v : 1.0),
- serializer: Number,
+ formatters: [toInt],
label: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceFieldLabel',
{
@@ -49,18 +50,11 @@ const fieldsConfig: FieldsConfig = {
),
validations: [
{
- validator: ({ value }) => {
- return isNaN(Number(value))
- ? {
- message: i18n.translate(
- 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError',
- {
- defaultMessage: 'An error distance value is required.',
- }
- ),
- }
- : undefined;
- },
+ validator: emptyField(
+ i18n.translate('xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError', {
+ defaultMessage: 'An error distance value is required.',
+ })
+ ),
},
],
},
@@ -110,14 +104,14 @@ export const Circle: FunctionComponent = () => {
options: [
{
value: 'shape',
- label: i18n.translate(
+ text: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeShape',
{ defaultMessage: 'Shape' }
),
},
{
value: 'geo_shape',
- label: i18n.translate(
+ text: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeGeoShape',
{ defaultMessage: 'Geo-shape' }
),
From d82d1ebef7cf912c8d301b45284aac1985ab0631 Mon Sep 17 00:00:00 2001
From: Corey Robertson
Date: Mon, 21 Jun 2021 12:11:36 -0400
Subject: [PATCH 011/163] Fixes issue with undo/redo (#101954)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../view_menu/view_menu.component.tsx | 6 -
.../workpad/hooks/use_workpad_history.test.ts | 272 ++++++++++++++----
.../workpad/hooks/use_workpad_history.ts | 5 +-
3 files changed, 217 insertions(+), 66 deletions(-)
diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx
index 8f92db4e7f3f4..8fb24c1f3c62e 100644
--- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx
+++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx
@@ -85,10 +85,6 @@ export interface Props {
* Current autoplay interval
*/
autoplayInterval: number;
- /**
- * Enables autoplay
- */
- enableAutoplay: (autoplay: boolean) => void;
/**
* Sets autoplay interval
*/
@@ -110,7 +106,6 @@ export const ViewMenu: FunctionComponent = ({
setRefreshInterval,
autoplayEnabled,
autoplayInterval,
- enableAutoplay,
setAutoplayInterval,
}) => {
const setRefresh = (val: number) => setRefreshInterval(val);
@@ -259,6 +254,5 @@ ViewMenu.propTypes = {
setRefreshInterval: PropTypes.func.isRequired,
autoplayEnabled: PropTypes.bool.isRequired,
autoplayInterval: PropTypes.number.isRequired,
- enableAutoplay: PropTypes.func.isRequired,
setAutoplayInterval: PropTypes.func.isRequired,
};
diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts
index b5b9c038cfd2d..515da36ddbb36 100644
--- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts
+++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts
@@ -26,76 +26,234 @@ describe('useRestoreHistory', () => {
jest.resetAllMocks();
});
- test('replaces undefined state with current state', () => {
- const history = {
- location: {
- state: undefined,
- pathname: 'somepath',
- },
- push: jest.fn(),
- replace: jest.fn(),
- };
-
- const state = {
- persistent: { some: 'state' },
- };
-
- mockGetState.mockReturnValue(state);
- mockGetHistory.mockReturnValue(history);
-
- renderHook(() => useWorkpadHistory());
-
- expect(history.replace).toBeCalledWith(history.location.pathname, encode(state.persistent));
+ describe('initial run', () => {
+ test('with undefined location state ', () => {
+ const history = {
+ location: {
+ state: undefined,
+ pathname: 'somepath',
+ },
+ push: jest.fn(),
+ replace: jest.fn(),
+ };
+
+ const state = {
+ persistent: { some: 'state' },
+ };
+
+ mockGetState.mockReturnValue(state);
+ mockGetHistory.mockReturnValue(history);
+
+ renderHook(() => useWorkpadHistory());
+
+ expect(history.replace).toBeCalledWith(history.location.pathname, encode(state.persistent));
+ expect(history.push).not.toBeCalled();
+ });
+
+ test('with location state not matching store state', () => {
+ const history = {
+ location: {
+ state: encode({ prior: 'state' }),
+ pathname: 'somepath',
+ },
+ push: jest.fn(),
+ replace: jest.fn(),
+ };
+
+ const state = {
+ persistent: { some: 'state' },
+ };
+
+ mockGetState.mockReturnValue(state);
+ mockGetHistory.mockReturnValue(history);
+
+ renderHook(() => useWorkpadHistory());
+
+ expect(history.push).not.toBeCalled();
+ expect(history.replace).not.toBeCalled();
+ });
+
+ test('with location state matching store state', () => {
+ const state = { some: 'state' };
+ const history = {
+ location: {
+ state: encode(state),
+ pathname: 'somepath',
+ },
+ push: jest.fn(),
+ replace: jest.fn(),
+ };
+ mockGetState.mockReturnValue(state);
+ mockGetHistory.mockReturnValue(history);
+
+ renderHook(() => useWorkpadHistory());
+
+ expect(history.push).not.toBeCalled();
+ expect(history.replace).not.toBeCalled();
+ });
});
- test('does not do a push on initial render if states do not match', () => {
- const history = {
- location: {
- state: encode({ old: 'state' }),
- pathname: 'somepath',
- },
- push: jest.fn(),
- replace: jest.fn(),
- };
+ describe('state changes', () => {
+ it('does a replace if location state is undefined', () => {
+ const push = jest.fn();
+ const replace = jest.fn();
+
+ const history = {
+ location: {
+ state: encode({ old: 'state' }),
+ pathname: 'somepath',
+ search: '',
+ },
+ push,
+ replace,
+ };
+
+ const state = {
+ persistent: { some: 'state' },
+ };
+
+ const newState = {
+ persistent: { new: 'state' },
+ };
+
+ mockGetState.mockReturnValue(state);
+ mockGetHistory.mockReturnValue(history);
+
+ const { rerender } = renderHook(() => useWorkpadHistory());
+
+ mockGetState.mockReturnValue(newState);
+ // History object from react router will not change, so just modifying here
+ history.location.state = undefined;
+ history.location.pathname = 'newpath';
+ rerender();
- const state = {
- persistent: { some: 'state' },
- };
+ expect(history.replace).toBeCalledWith('newpath', encode(newState.persistent));
+ });
- mockGetState.mockReturnValue(state);
- mockGetHistory.mockReturnValue(history);
+ test('does a push if location state does not match store state', () => {
+ const history = {
+ location: {
+ state: encode({ old: 'state' }),
+ pathname: 'somepath',
+ },
+ push: jest.fn(),
+ replace: jest.fn(),
+ };
- renderHook(() => useWorkpadHistory());
+ const oldState = {
+ persistent: { some: 'state' },
+ };
- expect(history.push).not.toBeCalled();
+ const newState = {
+ persistent: { new: 'state' },
+ };
+
+ mockGetState.mockReturnValue(oldState);
+ mockGetHistory.mockReturnValue(history);
+
+ const { rerender } = renderHook(() => useWorkpadHistory());
+
+ mockGetState.mockReturnValue(newState);
+ rerender();
+
+ expect(history.push).toBeCalledWith(history.location.pathname, encode(newState.persistent));
+ });
+
+ test('does nothing if new state matches location state', () => {
+ const state = {
+ persistent: { some: 'state' },
+ };
+
+ const newState = { ...state };
+
+ const history = {
+ location: {
+ state: encode(state.persistent),
+ pathname: 'somepath',
+ },
+ push: jest.fn(),
+ replace: jest.fn(),
+ };
+
+ mockGetState.mockReturnValue(state);
+ mockGetHistory.mockReturnValue(history);
+
+ const { rerender } = renderHook(() => useWorkpadHistory());
+
+ mockGetState.mockReturnValue(newState);
+ rerender();
+
+ expect(history.push).not.toBeCalled();
+ expect(history.replace).not.toBeCalled();
+ });
});
- test('rerender does a push if location state does not match store state', () => {
- const history = {
- location: {
- state: encode({ old: 'state' }),
- pathname: 'somepath',
- },
- push: jest.fn(),
- replace: jest.fn(),
- };
+ describe('changes to location', () => {
+ test('changes to pathname have no effect', () => {
+ // This is equivalent of navigating to a new page.
+ // The location state will initially be undefined, but
+ // we don't want to take any action because it will cause a state change
+ // and that will be picked up and do the replace
+ const state = {
+ persistent: { some: 'state' },
+ };
+
+ const history = {
+ location: {
+ state: encode(state.persistent),
+ pathname: 'somepath',
+ },
+ push: jest.fn(),
+ replace: jest.fn(),
+ };
+
+ mockGetState.mockReturnValue(state);
+ mockGetHistory.mockReturnValue(history);
+
+ const { rerender } = renderHook(() => useWorkpadHistory());
+
+ history.location.state = undefined;
+ history.location.pathname = 'newpath';
+
+ rerender();
+
+ expect(history.push).not.toBeCalled();
+ expect(history.replace).not.toBeCalled();
+ });
- const oldState = {
- persistent: { some: 'state' },
- };
+ test('changes to search does a replace', () => {
+ // This is equivalent of going from full screen to not full screen
+ // There is no state change that will occur, but we still need to update
+ // the location state
+ const state = {
+ persistent: { some: 'state' },
+ };
- const newState = {
- persistent: { new: 'state' },
- };
+ const history = {
+ location: {
+ state: encode(state.persistent),
+ pathname: 'somepath',
+ search: '',
+ },
+ push: jest.fn(),
+ replace: jest.fn(),
+ };
- mockGetState.mockReturnValue(oldState);
- mockGetHistory.mockReturnValue(history);
+ mockGetState.mockReturnValue(state);
+ mockGetHistory.mockReturnValue(history);
- const { rerender } = renderHook(() => useWorkpadHistory());
+ const { rerender } = renderHook(() => useWorkpadHistory());
+ history.location.pathname = 'somepath';
+ history.location.search = 'newsearch';
+ history.location.state = undefined;
- mockGetState.mockReturnValue(newState);
- rerender();
+ rerender();
- expect(history.push).toBeCalledWith(history.location.pathname, encode(newState.persistent));
+ expect(history.push).not.toBeCalled();
+ expect(history.replace).toBeCalledWith(
+ `somepath?${history.location.search}`,
+ encode(state.persistent)
+ );
+ });
});
});
diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts
index 1f563f7147330..b8880be60e36a 100644
--- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts
+++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts
@@ -31,11 +31,10 @@ export const useWorkpadHistory = () => {
// This will happen when navigating directly to a url (there will be no state on that link click)
if (locationState === undefined) {
history.replace(fullPath, encode(historyState));
- } else if (!doesStateMatchLocationState && !isInitialRun) {
+ } else if (!isInitialRun && !doesStateMatchLocationState) {
// There was a state change here
-
// If the state of the route that we are on does not match this new state, then we are going to push
history.push(fullPath, encode(historyState));
}
- }, [history, historyState]);
+ }, [history, historyState, history.location.search]);
};
From 773647e4f502c6cd401b29403cdc76ad7be0a5c1 Mon Sep 17 00:00:00 2001
From: Kyle Pollich
Date: Mon, 21 Jun 2021 12:13:23 -0400
Subject: [PATCH 012/163] [Integrations UI] Add updated integrations subtitle
(#102738)
* Add updated integrations subtitle
* Fix header image declarations
* Use theme hook for dark mode setting
---
.../integrations/layouts/default.tsx | 62 ++++++++++++++++---
.../sections/epm/screens/home/header.tsx | 62 -------------------
.../translations/translations/ja-JP.json | 2 -
.../translations/translations/zh-CN.json | 2 -
4 files changed, 55 insertions(+), 73 deletions(-)
delete mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx
diff --git a/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx b/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx
index 98b8e9515e689..66b88c020f163 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx
@@ -5,13 +5,19 @@
* 2.0.
*/
import React, { memo } from 'react';
-import { EuiText } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+
+import styled, { useTheme } from 'styled-components';
+
+import type { EuiTheme } from 'src/plugins/kibana_react/common';
+
import { useLink } from '../../../hooks';
import type { Section } from '../sections';
-import { HeroImage } from '../sections/epm/screens/home/header';
+import { useLinks } from '../hooks';
import { WithHeaderLayout } from './';
@@ -20,6 +26,30 @@ interface Props {
children?: React.ReactNode;
}
+const Illustration = styled(EuiImage)`
+ margin-bottom: -68px;
+ width: 80%;
+`;
+
+const HeroImage = memo(() => {
+ const { toAssets } = useLinks();
+ const theme = useTheme() as EuiTheme;
+ const IS_DARK_THEME = theme.darkMode;
+
+ return (
+
+ );
+});
+
export const DefaultLayout: React.FunctionComponent = memo(({ section, children }) => {
const { getHref } = useLink();
@@ -27,11 +57,29 @@ export const DefaultLayout: React.FunctionComponent = memo(({ section, ch
}
leftColumn={
-
-
- {' '}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
}
tabs={[
{
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx
deleted file mode 100644
index 55d058a2d7900..0000000000000
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/header.tsx
+++ /dev/null
@@ -1,62 +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 React, { memo } from 'react';
-import { i18n } from '@kbn/i18n';
-import styled from 'styled-components';
-import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiText } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-import { useLinks, useStartServices } from '../../../../hooks';
-
-export const HeroCopy = memo(() => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-});
-
-const Illustration = styled(EuiImage)`
- margin-bottom: -68px;
- width: 80%;
-`;
-
-export const HeroImage = memo(() => {
- const { toAssets } = useLinks();
- const { uiSettings } = useStartServices();
- const IS_DARK_THEME = uiSettings.get('theme:darkMode');
-
- return (
-
- );
-});
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 6b1b8091ec926..b6e22dc4a519b 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -9064,7 +9064,6 @@
"xpack.fleet.epm.categoryLabel": "カテゴリー",
"xpack.fleet.epm.detailsTitle": "詳細",
"xpack.fleet.epm.featuresLabel": "機能",
- "xpack.fleet.epm.illustrationAltText": "統合の例",
"xpack.fleet.epm.licenseLabel": "ライセンス",
"xpack.fleet.epm.loadingIntegrationErrorTitle": "統合詳細の読み込みエラー",
"xpack.fleet.epm.packageDetails.integrationList.agentCount": "エージェント",
@@ -9079,7 +9078,6 @@
"xpack.fleet.epm.packageDetailsNav.packagePoliciesLinkText": "ポリシー",
"xpack.fleet.epm.packageDetailsNav.settingsLinkText": "設定",
"xpack.fleet.epm.pageSubtitle": "一般的なアプリやサービスの統合を参照する",
- "xpack.fleet.epm.pageTitle": "統合",
"xpack.fleet.epm.releaseBadge.betaDescription": "この統合は本番環境用ではありません。",
"xpack.fleet.epm.releaseBadge.betaLabel": "ベータ",
"xpack.fleet.epm.releaseBadge.experimentalDescription": "この統合は、急に変更されたり、将来のリリースで削除されたりする可能性があります。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 014ebabbe783f..6ad4e7da08293 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -9150,7 +9150,6 @@
"xpack.fleet.epm.categoryLabel": "类别",
"xpack.fleet.epm.detailsTitle": "详情",
"xpack.fleet.epm.featuresLabel": "功能",
- "xpack.fleet.epm.illustrationAltText": "集成的图示",
"xpack.fleet.epm.licenseLabel": "许可证",
"xpack.fleet.epm.loadingIntegrationErrorTitle": "加载集成详情时出错",
"xpack.fleet.epm.packageDetails.integrationList.agentCount": "代理",
@@ -9165,7 +9164,6 @@
"xpack.fleet.epm.packageDetailsNav.packagePoliciesLinkText": "策略",
"xpack.fleet.epm.packageDetailsNav.settingsLinkText": "设置",
"xpack.fleet.epm.pageSubtitle": "浏览集成以了解热门应用和服务。",
- "xpack.fleet.epm.pageTitle": "集成",
"xpack.fleet.epm.releaseBadge.betaDescription": "在生产环境中不推荐使用此集成。",
"xpack.fleet.epm.releaseBadge.betaLabel": "公测版",
"xpack.fleet.epm.releaseBadge.experimentalDescription": "此集成可能有重大更改或将在未来版本中移除。",
From 3589ade859b8058d0f8c633e00710ac0a193ae1e Mon Sep 17 00:00:00 2001
From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com>
Date: Mon, 21 Jun 2021 12:29:37 -0400
Subject: [PATCH 013/163] Adding refresh to ensure status is updated (#102742)
---
.../security_and_spaces/tests/common/comments/post_comment.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts
index e30e276c7b717..f22ce54de193a 100644
--- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts
+++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts
@@ -386,6 +386,8 @@ export default ({ getService }: FtrProviderContext): void => {
},
});
+ await es.indices.refresh({ index: alert._index });
+
const { body: updatedAlert } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
@@ -439,6 +441,8 @@ export default ({ getService }: FtrProviderContext): void => {
},
});
+ await es.indices.refresh({ index: alert._index });
+
const { body: updatedAlert } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
From 79b0949d34e6a19ebfb156ece169c312cc0ce8f6 Mon Sep 17 00:00:00 2001
From: Constance
Date: Mon, 21 Jun 2021 09:36:16 -0700
Subject: [PATCH 014/163] [App Search] Convert Engines overview & engine
creation to new page template (#102677)
* Convert Engine Creation page to new page template
+ simplify EuiForm/form to 1 level of nesting
* Convert Meta Engine Creation page to new page template
* Initial conversion of Engines overview to new page template
* Engines Overview: simplify loading & empty state to use new page template props
* Engines Overview: simplify header
- now that the header/actions, chrome/telemetry, & empty state etc. are all DRY'd out by the template, there's no need for a separate header component
- we can simply dry out the header action, which will shortly be removed with the 7.14 launch
- note: for empty state changes: mostly indenting, the new template automatically creates a subdued panel for us in empty state so no need to include one manually
* Fix broken test mocks
I definitely should not have been importing the index file I'm mocking, but why did this only break now?? :psyduck:
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../app_search/__mocks__/engine_logic.mock.ts | 2 +-
.../engine_creation/engine_creation.tsx | 146 ++++++++-------
.../engines/components/empty_state.tsx | 136 ++++++--------
.../components/engines/components/header.tsx | 54 ------
.../components/engines/components/index.ts | 3 +-
...der.test.tsx => launch_as_button.test.tsx} | 18 +-
.../engines/components/launch_as_button.tsx | 41 +++++
.../engines/components/loading_state.test.tsx | 22 ---
.../engines/components/loading_state.tsx | 25 ---
.../components/engines/constants.ts | 5 +
.../engines/engines_overview.test.tsx | 173 ++++++++----------
.../components/engines/engines_overview.tsx | 38 ++--
.../meta_engine_creation.tsx | 29 ++-
.../public/applications/app_search/index.tsx | 32 ++--
14 files changed, 309 insertions(+), 415 deletions(-)
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx
rename x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/{header.test.tsx => launch_as_button.test.tsx} (64%)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts
index 23d638d5f25f3..b38659b7a9a79 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engine_logic.mock.ts
@@ -6,7 +6,7 @@
*/
import { EngineDetails } from '../components/engine/types';
-import { ENGINES_TITLE } from '../components/engines';
+import { ENGINES_TITLE } from '../components/engines/constants';
import { generateEncodedPath } from '../utils/encode_path_params';
export const mockEngineValues = {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx
index 250c941009ecb..913aa4f0ec845 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx
@@ -16,15 +16,13 @@ import {
EuiFlexItem,
EuiFieldText,
EuiSelect,
- EuiPageHeader,
- EuiPageContent,
+ EuiPanel,
EuiSpacer,
EuiTitle,
EuiButton,
} from '@elastic/eui';
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
+import { AppSearchPageTemplate } from '../layout';
import {
ALLOWED_CHARS_NOTE,
@@ -44,77 +42,77 @@ export const EngineCreation: React.FC = () => {
const { setLanguage, setRawName, submitEngine } = useActions(EngineCreationLogic);
return (
-
-
-
-
-
-
-
+ data-test-subj="EngineCreationNameInput"
+ placeholder={ENGINE_CREATION_FORM_ENGINE_NAME_PLACEHOLDER}
+ autoFocus
+ />
+
+
+
+
+ setLanguage(event.currentTarget.value)}
+ />
+
+
+
+
+
+ {ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL}
+
-
-
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx
index e6a7c03d2aab4..df17d22d387d9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { useValues, useActions } from 'kea';
-import { EuiPageContent, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui';
+import { EuiEmptyPrompt, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiButtonTo } from '../../../../shared/react_router_helpers';
@@ -20,86 +20,72 @@ import { ENGINE_CREATION_PATH } from '../../../routes';
import { SampleEngineCreationCta } from '../../sample_engine_creation_cta/sample_engine_creation_cta';
-import { EnginesOverviewHeader } from './header';
-
export const EmptyState: React.FC = () => {
const {
myRole: { canManageEngines },
} = useValues(AppLogic);
const { sendAppSearchTelemetry } = useActions(TelemetryLogic);
- return (
- <>
-
-
- {canManageEngines ? (
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.title', {
- defaultMessage: 'Create your first engine',
- })}
-
- }
- titleSize="l"
- body={
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.description1', {
- defaultMessage:
- 'An App Search engine stores the documents for your search experience.',
- })}
-
- }
- actions={
- <>
-
- sendAppSearchTelemetry({
- action: 'clicked',
- metric: 'create_first_engine_button',
- })
- }
- >
- {i18n.translate(
- 'xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta',
- { defaultMessage: 'Create an engine' }
- )}
-
-
-
- >
- }
- />
- ) : (
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.title', {
- defaultMessage: 'No engines available',
- })}
-
- }
- body={
-
- {i18n.translate(
- 'xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.description',
- {
- defaultMessage:
- 'Contact your App Search administrator to either create or grant you access to an engine.',
- }
- )}
-
+ return canManageEngines ? (
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.title', {
+ defaultMessage: 'Create your first engine',
+ })}
+
+ }
+ titleSize="l"
+ body={
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.description1', {
+ defaultMessage: 'An App Search engine stores the documents for your search experience.',
+ })}
+
+ }
+ actions={
+ <>
+
+ sendAppSearchTelemetry({
+ action: 'clicked',
+ metric: 'create_first_engine_button',
+ })
}
- />
- )}
-
- >
+ >
+ {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta', {
+ defaultMessage: 'Create an engine',
+ })}
+
+
+
+ >
+ }
+ />
+ ) : (
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.title', {
+ defaultMessage: 'No engines available',
+ })}
+
+ }
+ body={
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.emptyState.nonAdmin.description', {
+ defaultMessage:
+ 'Contact your App Search administrator to either create or grant you access to an engine.',
+ })}
+
+ }
+ />
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx
deleted file mode 100644
index bab67fd0e4bb5..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx
+++ /dev/null
@@ -1,54 +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 React from 'react';
-
-import { useActions } from 'kea';
-
-import { EuiPageHeader, EuiButton } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-
-import { getAppSearchUrl } from '../../../../shared/enterprise_search_url';
-import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
-import { TelemetryLogic } from '../../../../shared/telemetry';
-
-import { ENGINES_TITLE } from '../constants';
-
-export const EnginesOverviewHeader: React.FC = () => {
- const { sendAppSearchTelemetry } = useActions(TelemetryLogic);
-
- return (
- <>
-
-
- sendAppSearchTelemetry({
- action: 'clicked',
- metric: 'header_launch_button',
- })
- }
- data-test-subj="launchButton"
- >
- {i18n.translate('xpack.enterpriseSearch.appSearch.productCta', {
- defaultMessage: 'Launch App Search',
- })}
- ,
- ]}
- />
- >
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts
index 234d3ba31f44b..1d8e578e0edf2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-export { EnginesOverviewHeader } from './header';
-export { LoadingState } from './loading_state';
+export { LaunchAppSearchButton } from './launch_as_button';
export { EmptyState } from './empty_state';
export { EmptyMetaEnginesState } from './empty_meta_engines_state';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.test.tsx
similarity index 64%
rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx
rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.test.tsx
index 9b245a468b083..93c91cc3830f4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.test.tsx
@@ -12,23 +12,11 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPageHeader } from '@elastic/eui';
-
-import { EnginesOverviewHeader } from './';
-
-describe('EnginesOverviewHeader', () => {
- const wrapper = shallow( )
- .find(EuiPageHeader)
- .dive()
- .children()
- .dive();
-
- it('renders', () => {
- expect(wrapper.find('h1').text()).toEqual('Engines overview');
- });
+import { LaunchAppSearchButton } from './';
+describe('LaunchAppSearchButton', () => {
it('renders a launch app search button that sends telemetry on click', () => {
- const button = wrapper.find('[data-test-subj="launchButton"]');
+ const button = shallow( );
expect(button.prop('href')).toBe('http://localhost:3002/as');
expect(button.prop('isDisabled')).toBeFalsy();
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx
new file mode 100644
index 0000000000000..41102cb4fba2e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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 { useActions } from 'kea';
+
+import { EuiButton } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { getAppSearchUrl } from '../../../../shared/enterprise_search_url';
+import { TelemetryLogic } from '../../../../shared/telemetry';
+
+export const LaunchAppSearchButton: React.FC = () => {
+ const { sendAppSearchTelemetry } = useActions(TelemetryLogic);
+
+ return (
+ // eslint-disable-next-line @elastic/eui/href-or-on-click
+
+ sendAppSearchTelemetry({
+ action: 'clicked',
+ metric: 'header_launch_button',
+ })
+ }
+ data-test-subj="launchButton"
+ >
+ {i18n.translate('xpack.enterpriseSearch.appSearch.productCta', {
+ defaultMessage: 'Launch App Search',
+ })}
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx
deleted file mode 100644
index f7ccfea4bb4d4..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx
+++ /dev/null
@@ -1,22 +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 React from 'react';
-
-import { shallow } from 'enzyme';
-
-import { EuiLoadingContent } from '@elastic/eui';
-
-import { LoadingState } from './';
-
-describe('LoadingState', () => {
- it('renders', () => {
- const wrapper = shallow( );
-
- expect(wrapper.find(EuiLoadingContent)).toHaveLength(2);
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx
deleted file mode 100644
index 875c47378d1fb..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx
+++ /dev/null
@@ -1,25 +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 React from 'react';
-
-import { EuiPageContent, EuiSpacer, EuiLoadingContent } from '@elastic/eui';
-
-import { EnginesOverviewHeader } from './header';
-
-export const LoadingState: React.FC = () => {
- return (
- <>
-
-
-
-
-
-
- >
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts
index 8d03e3d23ae23..d01e89e004d28 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts
@@ -11,6 +11,11 @@ export const ENGINES_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.en
defaultMessage: 'Engines',
});
+export const ENGINES_OVERVIEW_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.enginesOverview.title',
+ { defaultMessage: 'Engines overview' }
+);
+
export const META_ENGINES_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.metaEngines.title',
{ defaultMessage: 'Meta Engines' }
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx
index 27fe65fe518eb..8825c322fb8d5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx
@@ -14,7 +14,6 @@ import { shallow, ShallowWrapper } from 'enzyme';
import { rerender } from '../../../test_helpers';
-import { LoadingState, EmptyState } from './components';
import { EnginesTable } from './components/tables/engines_table';
import { MetaEnginesTable } from './components/tables/meta_engines_table';
@@ -61,135 +60,117 @@ describe('EnginesOverview', () => {
setMockActions(actions);
});
- describe('non-happy-path states', () => {
- it('isLoading', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow( );
+ const valuesWithEngines = {
+ ...values,
+ dataLoading: false,
+ engines: ['test-engine'],
+ enginesMeta: {
+ page: {
+ current: 1,
+ size: 10,
+ total_results: 100,
+ },
+ },
+ };
- expect(wrapper.find(LoadingState)).toHaveLength(1);
- });
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockValues(valuesWithEngines);
+ });
- it('isEmpty', () => {
- setMockValues({ ...values, engines: [] });
- const wrapper = shallow( );
+ it('renders and calls the engines API', () => {
+ const wrapper = shallow( );
- expect(wrapper.find(EmptyState)).toHaveLength(1);
- });
+ expect(wrapper.find(EnginesTable)).toHaveLength(1);
+ expect(actions.loadEngines).toHaveBeenCalled();
});
- describe('happy-path states', () => {
- const valuesWithEngines = {
- ...values,
- dataLoading: false,
- engines: ['test-engine'],
- enginesMeta: {
- page: {
- current: 1,
- size: 10,
- total_results: 100,
- },
- },
- };
+ describe('when the user can manage/create engines', () => {
+ it('renders a create engine button which takes users to the create engine page', () => {
+ setMockValues({
+ ...valuesWithEngines,
+ myRole: { canManageEngines: true },
+ });
+ const wrapper = shallow( );
- beforeEach(() => {
- jest.clearAllMocks();
- setMockValues(valuesWithEngines);
+ expect(
+ wrapper.find('[data-test-subj="appSearchEnginesEngineCreationButton"]').prop('to')
+ ).toEqual('/engine_creation');
});
+ });
- it('renders and calls the engines API', () => {
+ describe('when the account has a platinum license', () => {
+ it('renders a 2nd meta engines table & makes a 2nd meta engines call', () => {
+ setMockValues({
+ ...valuesWithEngines,
+ hasPlatinumLicense: true,
+ });
const wrapper = shallow( );
- expect(wrapper.find(EnginesTable)).toHaveLength(1);
- expect(actions.loadEngines).toHaveBeenCalled();
+ expect(wrapper.find(MetaEnginesTable)).toHaveLength(1);
+ expect(actions.loadMetaEngines).toHaveBeenCalled();
});
describe('when the user can manage/create engines', () => {
- it('renders a create engine button which takes users to the create engine page', () => {
+ it('renders a create engine button which takes users to the create meta engine page', () => {
setMockValues({
...valuesWithEngines,
+ hasPlatinumLicense: true,
myRole: { canManageEngines: true },
});
const wrapper = shallow( );
expect(
- wrapper.find('[data-test-subj="appSearchEnginesEngineCreationButton"]').prop('to')
- ).toEqual('/engine_creation');
+ wrapper.find('[data-test-subj="appSearchEnginesMetaEngineCreationButton"]').prop('to')
+ ).toEqual('/meta_engine_creation');
});
});
+ });
- describe('when the account has a platinum license', () => {
- it('renders a 2nd meta engines table & makes a 2nd meta engines call', () => {
- setMockValues({
- ...valuesWithEngines,
- hasPlatinumLicense: true,
- });
- const wrapper = shallow( );
+ describe('pagination', () => {
+ const getTablePagination = (wrapper: ShallowWrapper) =>
+ wrapper.find(EnginesTable).prop('pagination');
- expect(wrapper.find(MetaEnginesTable)).toHaveLength(1);
- expect(actions.loadMetaEngines).toHaveBeenCalled();
- });
+ it('passes down page data from the API', () => {
+ const wrapper = shallow( );
+ const pagination = getTablePagination(wrapper);
- describe('when the user can manage/create engines', () => {
- it('renders a create engine button which takes users to the create meta engine page', () => {
- setMockValues({
- ...valuesWithEngines,
- hasPlatinumLicense: true,
- myRole: { canManageEngines: true },
- });
- const wrapper = shallow( );
-
- expect(
- wrapper.find('[data-test-subj="appSearchEnginesMetaEngineCreationButton"]').prop('to')
- ).toEqual('/meta_engine_creation');
- });
- });
+ expect(pagination.totalItemCount).toEqual(100);
+ expect(pagination.pageIndex).toEqual(0);
});
- describe('pagination', () => {
- const getTablePagination = (wrapper: ShallowWrapper) =>
- wrapper.find(EnginesTable).prop('pagination');
-
- it('passes down page data from the API', () => {
- const wrapper = shallow( );
- const pagination = getTablePagination(wrapper);
+ it('re-polls the API on page change', () => {
+ const wrapper = shallow( );
- expect(pagination.totalItemCount).toEqual(100);
- expect(pagination.pageIndex).toEqual(0);
+ setMockValues({
+ ...valuesWithEngines,
+ enginesMeta: {
+ page: {
+ ...valuesWithEngines.enginesMeta.page,
+ current: 51,
+ },
+ },
});
+ rerender(wrapper);
- it('re-polls the API on page change', () => {
- const wrapper = shallow( );
-
- setMockValues({
- ...valuesWithEngines,
- enginesMeta: {
- page: {
- ...valuesWithEngines.enginesMeta.page,
- current: 51,
- },
- },
- });
- rerender(wrapper);
+ expect(actions.loadEngines).toHaveBeenCalledTimes(2);
+ expect(getTablePagination(wrapper).pageIndex).toEqual(50);
+ });
- expect(actions.loadEngines).toHaveBeenCalledTimes(2);
- expect(getTablePagination(wrapper).pageIndex).toEqual(50);
+ it('calls onPagination handlers', () => {
+ setMockValues({
+ ...valuesWithEngines,
+ hasPlatinumLicense: true,
+ metaEngines: ['test-meta-engine'],
});
+ const wrapper = shallow( );
+ const pageEvent = { page: { index: 0 } };
- it('calls onPagination handlers', () => {
- setMockValues({
- ...valuesWithEngines,
- hasPlatinumLicense: true,
- metaEngines: ['test-meta-engine'],
- });
- const wrapper = shallow( );
- const pageEvent = { page: { index: 0 } };
-
- wrapper.find(EnginesTable).simulate('change', pageEvent);
- expect(actions.onEnginesPagination).toHaveBeenCalledWith(1);
+ wrapper.find(EnginesTable).simulate('change', pageEvent);
+ expect(actions.onEnginesPagination).toHaveBeenCalledWith(1);
- wrapper.find(MetaEnginesTable).simulate('change', pageEvent);
- expect(actions.onMetaEnginesPagination).toHaveBeenCalledWith(1);
- });
+ wrapper.find(MetaEnginesTable).simulate('change', pageEvent);
+ expect(actions.onMetaEnginesPagination).toHaveBeenCalledWith(1);
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx
index 7001ecada999a..44111a5ecbe66 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx
@@ -12,7 +12,7 @@ import { useValues, useActions } from 'kea';
import {
EuiFlexGroup,
EuiFlexItem,
- EuiPageContent,
+ EuiPanel,
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiPageContentBody,
@@ -20,24 +20,19 @@ import {
EuiSpacer,
} from '@elastic/eui';
-import { FlashMessages } from '../../../shared/flash_messages';
import { LicensingLogic } from '../../../shared/licensing';
import { EuiButtonTo } from '../../../shared/react_router_helpers';
import { convertMetaToPagination, handlePageChange } from '../../../shared/table_pagination';
-import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
import { AppLogic } from '../../app_logic';
import { EngineIcon, MetaEngineIcon } from '../../icons';
import { ENGINE_CREATION_PATH, META_ENGINE_CREATION_PATH } from '../../routes';
+import { AppSearchPageTemplate } from '../layout';
-import {
- EnginesOverviewHeader,
- LoadingState,
- EmptyState,
- EmptyMetaEnginesState,
-} from './components';
+import { LaunchAppSearchButton, EmptyState, EmptyMetaEnginesState } from './components';
import { EnginesTable } from './components/tables/engines_table';
import { MetaEnginesTable } from './components/tables/meta_engines_table';
import {
+ ENGINES_OVERVIEW_TITLE,
CREATE_AN_ENGINE_BUTTON_LABEL,
CREATE_A_META_ENGINE_BUTTON_LABEL,
ENGINES_TITLE,
@@ -73,16 +68,19 @@ export const EnginesOverview: React.FC = () => {
if (hasPlatinumLicense) loadMetaEngines();
}, [hasPlatinumLicense, metaEnginesMeta.page.current]);
- if (dataLoading) return ;
- if (!engines.length) return ;
-
return (
- <>
-
-
-
-
-
+ ],
+ }}
+ isLoading={dataLoading}
+ isEmptyState={!engines.length}
+ emptyState={ }
+ >
+
@@ -168,7 +166,7 @@ export const EnginesOverview: React.FC = () => {
>
)}
-
- >
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx
index 02a1768a7528e..325e557acec0c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx
@@ -18,16 +18,14 @@ import {
EuiFormRow,
EuiFlexItem,
EuiFieldText,
- EuiPageContent,
- EuiPageHeader,
+ EuiPanel,
EuiSpacer,
EuiTitle,
EuiButton,
} from '@elastic/eui';
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { AppLogic } from '../../app_logic';
+import { AppSearchPageTemplate } from '../layout';
import {
ALLOWED_CHARS_NOTE,
@@ -74,20 +72,21 @@ export const MetaEngineCreation: React.FC = () => {
}, []);
return (
-
-
-
{META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION}
{META_ENGINE_CREATION_FORM_DOCUMENTATION_DESCRIPTION}
>
- }
- />
-
-
+ ),
+ }}
+ data-test-subj="MetaEngineCreation"
+ >
+
{
{META_ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL}
-
-
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
index caf0f805e8ca7..b2cd3d7b54a1a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
@@ -92,6 +92,22 @@ export const AppSearchConfigured: React.FC> = (props) =
)}
+
+
+
+
+
+
+ {canManageEngines && (
+
+
+
+ )}
+ {canManageMetaEngines && (
+
+
+
+ )}
{canViewRoleMappings && (
@@ -100,12 +116,6 @@ export const AppSearchConfigured: React.FC> = (props) =
} readOnlyMode={readOnlyMode}>
-
-
-
-
-
-
@@ -115,16 +125,6 @@ export const AppSearchConfigured: React.FC> = (props) =
- {canManageEngines && (
-
-
-
- )}
- {canManageMetaEngines && (
-
-
-
- )}
From 491b0d1d0bbe8ee6b8d52d487cc303fccae020f2 Mon Sep 17 00:00:00 2001
From: Steph Milovic
Date: Mon, 21 Jun 2021 10:54:46 -0600
Subject: [PATCH 015/163] [Observability] [Cases] Fix Cases navigation
(#102429)
---
.../components/app/cases/all_cases/index.tsx | 15 +++++---------
.../components/app/cases/case_view/index.tsx | 19 ++++++++----------
.../app/cases/create/index.test.tsx | 20 ++++++++++---------
.../components/app/cases/create/index.tsx | 15 +++++++-------
.../public/pages/cases/case_details.tsx | 5 +++--
.../public/pages/cases/configure_cases.tsx | 9 +++++----
.../public/pages/cases/create_case.tsx | 9 +++++----
7 files changed, 45 insertions(+), 47 deletions(-)
diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx
index 1636d08aa56e4..29a16590f3eb3 100644
--- a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx
@@ -28,19 +28,18 @@ interface AllCasesProps {
export const AllCases = React.memo(({ userCanCrud }) => {
const {
cases: casesUi,
- application: { navigateToApp },
+ application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
const { formatUrl } = useFormatUrl(CASES_APP_ID);
+ const casesUrl = getUrlForApp(CASES_APP_ID);
return casesUi.getAllCases({
caseDetailsNavigation: {
href: ({ detailName, subCaseId }: AllCasesNavProps) => {
return formatUrl(getCaseDetailsUrl({ id: detailName, subCaseId }));
},
onClick: async ({ detailName, subCaseId, search }: AllCasesNavProps) =>
- navigateToApp(`${CASES_APP_ID}`, {
- path: getCaseDetailsUrl({ id: detailName, subCaseId }),
- }),
+ navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: detailName, subCaseId })}`),
},
configureCasesNavigation: {
href: formatUrl(getConfigureCasesUrl()),
@@ -48,9 +47,7 @@ export const AllCases = React.memo(({ userCanCrud }) => {
if (ev != null) {
ev.preventDefault();
}
- return navigateToApp(`${CASES_APP_ID}`, {
- path: getConfigureCasesUrl(),
- });
+ return navigateToUrl(`${casesUrl}${getConfigureCasesUrl()}`);
},
},
createCaseNavigation: {
@@ -59,9 +56,7 @@ export const AllCases = React.memo(({ userCanCrud }) => {
if (ev != null) {
ev.preventDefault();
}
- return navigateToApp(`${CASES_APP_ID}`, {
- path: getCreateCaseUrl(),
- });
+ return navigateToUrl(`${casesUrl}${getCreateCaseUrl()}`);
},
},
disableAlerts: true,
diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx
index 728333ac8c544..07d8019153a06 100644
--- a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx
@@ -42,8 +42,10 @@ export interface CaseProps extends Props {
export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => {
const [caseTitle, setCaseTitle] = useState(null);
- const { cases: casesUi, application } = useKibana().services;
- const { navigateToApp } = application;
+ const {
+ cases: casesUi,
+ application: { getUrlForApp, navigateToUrl },
+ } = useKibana().services;
const allCasesLink = getCaseUrl();
const { formatUrl } = useFormatUrl(CASES_APP_ID);
const href = formatUrl(allCasesLink);
@@ -79,6 +81,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
[caseId, formatUrl, subCaseId]
);
+ const casesUrl = getUrlForApp(CASES_APP_ID);
return casesUi.getCaseView({
allCasesNavigation: {
href: allCasesHref,
@@ -86,9 +89,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
if (ev != null) {
ev.preventDefault();
}
- return navigateToApp(`${CASES_APP_ID}`, {
- path: allCasesLink,
- });
+ return navigateToUrl(casesUrl);
},
},
caseDetailsNavigation: {
@@ -97,9 +98,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
if (ev != null) {
ev.preventDefault();
}
- return navigateToApp(`${CASES_APP_ID}`, {
- path: getCaseDetailsUrl({ id: caseId }),
- });
+ return navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: caseId })}`);
},
},
caseId,
@@ -109,9 +108,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
if (ev != null) {
ev.preventDefault();
}
- return navigateToApp(`${CASES_APP_ID}`, {
- path: configureCasesLink,
- });
+ return navigateToUrl(`${casesUrl}${configureCasesLink}`);
},
},
getCaseDetailHrefWithCommentId,
diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx
index ec7511836328b..6dae88733fd49 100644
--- a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx
@@ -12,7 +12,7 @@ import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_rea
import { Create } from '.';
import { useKibana } from '../../../../utils/kibana_react';
import { basicCase } from '../../../../../../cases/public/containers/mock';
-import { CASES_APP_ID, CASES_OWNER } from '../constants';
+import { CASES_OWNER } from '../constants';
import { Case } from '../../../../../../cases/common';
import { getCaseDetailsUrl } from '../../../../pages/cases/links';
@@ -20,7 +20,8 @@ jest.mock('../../../../utils/kibana_react');
describe('Create case', () => {
const mockCreateCase = jest.fn();
- const mockNavigateToApp = jest.fn();
+ const mockNavigateToUrl = jest.fn();
+ const mockCasesUrl = 'https://elastic.co/app/observability/cases';
beforeEach(() => {
jest.resetAllMocks();
(useKibana as jest.Mock).mockReturnValue({
@@ -28,7 +29,7 @@ describe('Create case', () => {
cases: {
getCreateCase: mockCreateCase,
},
- application: { navigateToApp: mockNavigateToApp },
+ application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl },
},
});
});
@@ -52,7 +53,7 @@ describe('Create case', () => {
onCancel();
},
},
- application: { navigateToApp: mockNavigateToApp },
+ application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl },
},
});
mount(
@@ -61,7 +62,7 @@ describe('Create case', () => {
);
- await waitFor(() => expect(mockNavigateToApp).toHaveBeenCalledWith(`${CASES_APP_ID}`));
+ await waitFor(() => expect(mockNavigateToUrl).toHaveBeenCalledWith(`${mockCasesUrl}`));
});
it('should redirect to new case when posting the case', async () => {
@@ -72,7 +73,7 @@ describe('Create case', () => {
onSuccess(basicCase);
},
},
- application: { navigateToApp: mockNavigateToApp },
+ application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl },
},
});
mount(
@@ -82,9 +83,10 @@ describe('Create case', () => {
);
await waitFor(() =>
- expect(mockNavigateToApp).toHaveBeenNthCalledWith(1, `${CASES_APP_ID}`, {
- path: getCaseDetailsUrl({ id: basicCase.id }),
- })
+ expect(mockNavigateToUrl).toHaveBeenNthCalledWith(
+ 1,
+ `${mockCasesUrl}${getCaseDetailsUrl({ id: basicCase.id })}`
+ )
);
});
});
diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx
index d7e2daea2490b..a3ed234147314 100644
--- a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx
@@ -15,17 +15,18 @@ import { CASES_APP_ID, CASES_OWNER } from '../constants';
export const Create = React.memo(() => {
const {
cases,
- application: { navigateToApp },
+ application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
+ const casesUrl = getUrlForApp(CASES_APP_ID);
const onSuccess = useCallback(
- async ({ id }) =>
- navigateToApp(`${CASES_APP_ID}`, {
- path: getCaseDetailsUrl({ id }),
- }),
- [navigateToApp]
+ async ({ id }) => navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id })}`),
+ [casesUrl, navigateToUrl]
);
- const handleSetIsCancel = useCallback(() => navigateToApp(`${CASES_APP_ID}`), [navigateToApp]);
+ const handleSetIsCancel = useCallback(() => navigateToUrl(`${casesUrl}`), [
+ casesUrl,
+ navigateToUrl,
+ ]);
return (
diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx
index 78f1cb313ea9b..6adf5ad286808 100644
--- a/x-pack/plugins/observability/public/pages/cases/case_details.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx
@@ -16,7 +16,7 @@ import { CaseCallOut, permissionsReadOnlyErrorMessage } from '../../components/a
export const CaseDetailsPage = React.memo(() => {
const {
- application: { navigateToApp },
+ application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
const userPermissions = useGetUserCasesPermissions();
const { detailName: caseId, subCaseId } = useParams<{
@@ -24,8 +24,9 @@ export const CaseDetailsPage = React.memo(() => {
subCaseId?: string;
}>();
+ const casesUrl = getUrlForApp(CASES_APP_ID);
if (userPermissions != null && !userPermissions.read) {
- navigateToApp(`${CASES_APP_ID}`);
+ navigateToUrl(casesUrl);
return null;
}
diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx
index 2986c1ff34e11..a4df4855b0204 100644
--- a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx
@@ -23,22 +23,23 @@ const ButtonEmpty = styled(EuiButtonEmpty)`
function ConfigureCasesPageComponent() {
const {
cases,
- application: { navigateToApp },
+ application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
+ const casesUrl = getUrlForApp(CASES_APP_ID);
const userPermissions = useGetUserCasesPermissions();
const { ObservabilityPageTemplate } = usePluginContext();
const onClickGoToCases = useCallback(
async (ev) => {
ev.preventDefault();
- return navigateToApp(`${CASES_APP_ID}`);
+ return navigateToUrl(casesUrl);
},
- [navigateToApp]
+ [casesUrl, navigateToUrl]
);
const { formatUrl } = useFormatUrl(CASES_APP_ID);
const href = formatUrl(getCaseUrl());
useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.configure]);
if (userPermissions != null && !userPermissions.read) {
- navigateToApp(`${CASES_APP_ID}`);
+ navigateToUrl(casesUrl);
return null;
}
diff --git a/x-pack/plugins/observability/public/pages/cases/create_case.tsx b/x-pack/plugins/observability/public/pages/cases/create_case.tsx
index 11f6d62da6103..96ed59734edda 100644
--- a/x-pack/plugins/observability/public/pages/cases/create_case.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/create_case.tsx
@@ -25,22 +25,23 @@ export const CreateCasePage = React.memo(() => {
const userPermissions = useGetUserCasesPermissions();
const { ObservabilityPageTemplate } = usePluginContext();
const {
- application: { navigateToApp },
+ application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
+ const casesUrl = getUrlForApp(CASES_APP_ID);
const goTo = useCallback(
async (ev) => {
ev.preventDefault();
- return navigateToApp(CASES_APP_ID);
+ return navigateToUrl(casesUrl);
},
- [navigateToApp]
+ [casesUrl, navigateToUrl]
);
const { formatUrl } = useFormatUrl(CASES_APP_ID);
const href = formatUrl(getCaseUrl());
useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.create]);
if (userPermissions != null && !userPermissions.crud) {
- navigateToApp(`${CASES_APP_ID}`);
+ navigateToUrl(casesUrl);
return null;
}
From 49271f30d0dfb096cf6bf773ae1530f5bf7a5616 Mon Sep 17 00:00:00 2001
From: Scotty Bollinger
Date: Mon, 21 Jun 2021 12:14:42 -0500
Subject: [PATCH 016/163] [Workplace Search] Replace library interface with EUI
(#102758)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
One of the Workplace Search components was using the 'react-beautiful-dnd' typing for it’s component. This PR relpaces it with the EUI equivalent.
---
.../components/display_settings/display_settings_logic.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts
index e8b419a31abb2..38424df724bd4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts
@@ -5,11 +5,11 @@
* 2.0.
*/
-import { DropResult } from 'react-beautiful-dnd';
-
import { kea, MakeLogicType } from 'kea';
import { cloneDeep, isEqual, differenceBy } from 'lodash';
+import { DropResult } from '@elastic/eui';
+
import {
setSuccessMessage,
clearFlashMessages,
From 6f2a97b0e8e53eaa5073ad87b976dc2f8dc84cdf Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Mon, 21 Jun 2021 19:17:48 +0200
Subject: [PATCH 017/163] [Lens] Fix formula functional test (#102749)
---
x-pack/test/functional/apps/lens/formula.ts | 4 +---
x-pack/test/functional/page_objects/lens_page.ts | 2 +-
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/formula.ts
index d1376731886eb..e9e5051c006f0 100644
--- a/x-pack/test/functional/apps/lens/formula.ts
+++ b/x-pack/test/functional/apps/lens/formula.ts
@@ -15,8 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const testSubjects = getService('testSubjects');
- // FLAKY: https://github.com/elastic/kibana/issues/102183
- describe.skip('lens formula', () => {
+ describe('lens formula', () => {
it('should transition from count to formula', async () => {
await PageObjects.visualize.gotoVisualizationLandingPage();
await listingTable.searchForItemWithName('lnsXYvis');
@@ -81,7 +80,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
let element = await find.byCssSelector('.monaco-editor');
expect(await element.getVisibleText()).to.equal(`count(kql='Men\\'s Clothing ')`);
- await PageObjects.common.sleep(100);
await PageObjects.lens.typeFormula('count(kql=');
input = await find.activeElement();
await input.type(`Men\'s Clothing`);
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index 332a40795bee9..d416d26baaf0d 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -1066,7 +1066,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await find.byCssSelector('.monaco-editor');
await find.clickByCssSelectorWhenNotDisabled('.monaco-editor');
const input = await find.activeElement();
- await input.clearValueWithKeyboard();
+ await input.clearValueWithKeyboard({ charByChar: true });
await input.type(formula);
},
});
From ceaf9f6242a1e4a63e77846036342c7269954c38 Mon Sep 17 00:00:00 2001
From: Constance
Date: Mon, 21 Jun 2021 10:33:22 -0700
Subject: [PATCH 018/163] Convert EuiCards to non-shadowed defaults to avoid
nested panel/shadows (#102680)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../document_creation/document_creation_buttons.tsx | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx
index 6d3caca87dcc3..7ed9b9ea65025 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx
@@ -60,6 +60,7 @@ export const DocumentCreationButtons: React.FC = ({ disabled = false }) =
= ({ disabled = false }) =
= ({ disabled = false }) =
= ({ disabled = false }) =
Date: Mon, 21 Jun 2021 10:47:15 -0700
Subject: [PATCH 019/163] Osquery telemetry usage bug (#102762)
* guard for null metrics query
* added some tests for the beat usage metric extraction function
---
.../osquery/server/usage/fetchers.test.ts | 105 ++++++++++++++++++
.../plugins/osquery/server/usage/fetchers.ts | 75 +++++++------
2 files changed, 147 insertions(+), 33 deletions(-)
create mode 100644 x-pack/plugins/osquery/server/usage/fetchers.test.ts
diff --git a/x-pack/plugins/osquery/server/usage/fetchers.test.ts b/x-pack/plugins/osquery/server/usage/fetchers.test.ts
new file mode 100644
index 0000000000000..13da639e2c72d
--- /dev/null
+++ b/x-pack/plugins/osquery/server/usage/fetchers.test.ts
@@ -0,0 +1,105 @@
+/*
+ * 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 { extractBeatUsageMetrics } from './fetchers';
+
+describe('extractBeatUsageMetrics', () => {
+ it('should not blow when no values are supplied for the aggregations', () => {
+ expect(extractBeatUsageMetrics({})).toEqual({
+ memory: {
+ rss: {},
+ },
+ cpu: {},
+ });
+ });
+
+ it('should not blow when some values are missing from the aggregations', () => {
+ expect(
+ extractBeatUsageMetrics({
+ aggregations: {
+ lastDay: {
+ max_rss: {
+ value: 1,
+ },
+ },
+ },
+ })
+ ).toEqual({
+ memory: {
+ rss: {
+ max: 1,
+ },
+ },
+ cpu: {},
+ });
+ });
+
+ it('should pick out all the max/avg/latest for memory/cpu', () => {
+ expect(
+ extractBeatUsageMetrics({
+ aggregations: {
+ lastDay: {
+ max_rss: {
+ value: 1,
+ },
+ avg_rss: {
+ value: 1,
+ },
+ max_cpu: {
+ value: 2,
+ },
+ avg_cpu: {
+ value: 2,
+ },
+ latest: {
+ hits: {
+ total: 1,
+ hits: [
+ {
+ _index: '',
+ _id: '',
+ _source: {
+ monitoring: {
+ metrics: {
+ beat: {
+ cpu: {
+ total: {
+ time: {
+ ms: 2,
+ },
+ },
+ },
+ memstats: {
+ rss: 1,
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ })
+ ).toEqual({
+ memory: {
+ rss: {
+ max: 1,
+ avg: 1,
+ latest: 1,
+ },
+ },
+ cpu: {
+ max: 2,
+ avg: 2,
+ latest: 2,
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/osquery/server/usage/fetchers.ts b/x-pack/plugins/osquery/server/usage/fetchers.ts
index 5f5b282331bec..6a4236b5adccd 100644
--- a/x-pack/plugins/osquery/server/usage/fetchers.ts
+++ b/x-pack/plugins/osquery/server/usage/fetchers.ts
@@ -9,6 +9,7 @@ import {
AggregationsSingleBucketAggregate,
AggregationsTopHitsAggregate,
AggregationsValueAggregate,
+ SearchResponse,
} from '@elastic/elasticsearch/api/types';
import { PackagePolicyServiceInterface } from '../../../fleet/server';
import { getRouteMetric } from '../routes/usage';
@@ -133,6 +134,46 @@ export async function getLiveQueryUsage(
return result;
}
+export function extractBeatUsageMetrics(
+ metricResponse: Pick, 'aggregations'>
+) {
+ const lastDayAggs = metricResponse.aggregations?.lastDay as AggregationsSingleBucketAggregate;
+ const result: BeatMetricsUsage = {
+ memory: {
+ rss: {},
+ },
+ cpu: {},
+ };
+
+ if (lastDayAggs) {
+ if ('max_rss' in lastDayAggs) {
+ result.memory.rss.max = (lastDayAggs.max_rss as AggregationsValueAggregate).value;
+ }
+
+ if ('avg_rss' in lastDayAggs) {
+ result.memory.rss.avg = (lastDayAggs.max_rss as AggregationsValueAggregate).value;
+ }
+
+ if ('max_cpu' in lastDayAggs) {
+ result.cpu.max = (lastDayAggs.max_cpu as AggregationsValueAggregate).value;
+ }
+
+ if ('avg_cpu' in lastDayAggs) {
+ result.cpu.avg = (lastDayAggs.max_cpu as AggregationsValueAggregate).value;
+ }
+
+ if ('latest' in lastDayAggs) {
+ const latest = (lastDayAggs.latest as AggregationsTopHitsAggregate).hits.hits[0]?._source
+ ?.monitoring.metrics.beat;
+ if (latest) {
+ result.cpu.latest = latest.cpu.total.time.ms;
+ result.memory.rss.latest = latest.memstats.rss;
+ }
+ }
+ }
+ return result;
+}
+
export async function getBeatUsage(esClient: ElasticsearchClient) {
const { body: metricResponse } = await esClient.search({
body: {
@@ -186,38 +227,6 @@ export async function getBeatUsage(esClient: ElasticsearchClient) {
},
index: METRICS_INDICES,
});
- const lastDayAggs = metricResponse.aggregations?.lastDay as AggregationsSingleBucketAggregate;
- const result: BeatMetricsUsage = {
- memory: {
- rss: {},
- },
- cpu: {},
- };
-
- if ('max_rss' in lastDayAggs) {
- result.memory.rss.max = (lastDayAggs.max_rss as AggregationsValueAggregate).value;
- }
- if ('avg_rss' in lastDayAggs) {
- result.memory.rss.avg = (lastDayAggs.max_rss as AggregationsValueAggregate).value;
- }
-
- if ('max_cpu' in lastDayAggs) {
- result.cpu.max = (lastDayAggs.max_cpu as AggregationsValueAggregate).value;
- }
-
- if ('avg_cpu' in lastDayAggs) {
- result.cpu.avg = (lastDayAggs.max_cpu as AggregationsValueAggregate).value;
- }
-
- if ('latest' in lastDayAggs) {
- const latest = (lastDayAggs.latest as AggregationsTopHitsAggregate).hits.hits[0]?._source
- ?.monitoring.metrics.beat;
- if (latest) {
- result.cpu.latest = latest.cpu.total.time.ms;
- result.memory.rss.latest = latest.memstats.rss;
- }
- }
-
- return result;
+ return extractBeatUsageMetrics(metricResponse);
}
From f448f1f1bf9eeafb4ac19b0a4de53f3e615da52f Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 21 Jun 2021 19:15:21 +0100
Subject: [PATCH 020/163] chore(NA): enable buildbuddy again
---
packages/kbn-pm/dist/index.js | 2 +-
packages/kbn-pm/src/utils/bazel/run.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js
index 1311eb4d7c638..e455f487d1384 100644
--- a/packages/kbn-pm/dist/index.js
+++ b/packages/kbn-pm/dist/index.js
@@ -48479,7 +48479,7 @@ async function runBazelCommandWithRunner(bazelCommandRunner, bazelArgs, offline
stdio: 'pipe'
});
- if (offline || !offline) {
+ if (offline) {
bazelArgs = [...bazelArgs, '--config=offline'];
}
diff --git a/packages/kbn-pm/src/utils/bazel/run.ts b/packages/kbn-pm/src/utils/bazel/run.ts
index 5f3743876e0e4..c030081e53daa 100644
--- a/packages/kbn-pm/src/utils/bazel/run.ts
+++ b/packages/kbn-pm/src/utils/bazel/run.ts
@@ -29,7 +29,7 @@ async function runBazelCommandWithRunner(
stdio: 'pipe',
};
- if (offline || !offline) {
+ if (offline) {
bazelArgs = [...bazelArgs, '--config=offline'];
}
From 6c699a562c9f4ce4a7baa081e02ea89bb8971049 Mon Sep 17 00:00:00 2001
From: ymao1
Date: Mon, 21 Jun 2021 14:17:44 -0400
Subject: [PATCH 021/163] [Alerting] Updating audit event terminology (#102263)
* Updating audit terminology
* Fixing unit tests
* Updating audit docs
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
docs/user/security/audit-logging.asciidoc | 68 ++++++------
.../server/alerts_client/alerts_client.ts | 102 +++++++++---------
.../server/alerts_client/audit_events.test.ts | 28 ++---
.../server/alerts_client/audit_events.ts | 88 +++++++--------
.../server/alerts_client/tests/create.test.ts | 8 +-
.../server/alerts_client/tests/delete.test.ts | 8 +-
.../alerts_client/tests/disable.test.ts | 8 +-
.../server/alerts_client/tests/enable.test.ts | 8 +-
.../server/alerts_client/tests/find.test.ts | 12 +--
.../server/alerts_client/tests/get.test.ts | 8 +-
.../alerts_client/tests/mute_all.test.ts | 8 +-
.../alerts_client/tests/mute_instance.test.ts | 8 +-
.../alerts_client/tests/unmute_all.test.ts | 8 +-
.../tests/unmute_instance.test.ts | 8 +-
.../server/alerts_client/tests/update.test.ts | 8 +-
.../tests/update_api_key.test.ts | 8 +-
16 files changed, 193 insertions(+), 193 deletions(-)
diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc
index b9fc0c9c4ac46..5808e56d6d289 100644
--- a/docs/user/security/audit-logging.asciidoc
+++ b/docs/user/security/audit-logging.asciidoc
@@ -93,9 +93,9 @@ Refer to the corresponding {es} logs for potential write errors.
| `unknown` | User is creating a connector.
| `failure` | User is not authorized to create a connector.
-.2+| `alert_create`
-| `unknown` | User is creating an alert.
-| `failure` | User is not authorized to create an alert.
+.2+| `rule_create`
+| `unknown` | User is creating a rule.
+| `failure` | User is not authorized to create a rule.
.2+| `space_create`
| `unknown` | User is creating a space.
@@ -128,38 +128,38 @@ Refer to the corresponding {es} logs for potential write errors.
| `unknown` | User is updating a connector.
| `failure` | User is not authorized to update a connector.
-.2+| `alert_update`
-| `unknown` | User is updating an alert.
-| `failure` | User is not authorized to update an alert.
+.2+| `rule_update`
+| `unknown` | User is updating a rule.
+| `failure` | User is not authorized to update a rule.
-.2+| `alert_update_api_key`
-| `unknown` | User is updating the API key of an alert.
-| `failure` | User is not authorized to update the API key of an alert.
+.2+| `rule_update_api_key`
+| `unknown` | User is updating the API key of a rule.
+| `failure` | User is not authorized to update the API key of a rule.
-.2+| `alert_enable`
-| `unknown` | User is enabling an alert.
-| `failure` | User is not authorized to enable an alert.
+.2+| `rule_enable`
+| `unknown` | User is enabling a rule.
+| `failure` | User is not authorized to enable a rule.
-.2+| `alert_disable`
-| `unknown` | User is disabling an alert.
-| `failure` | User is not authorized to disable an alert.
+.2+| `rule_disable`
+| `unknown` | User is disabling a rule.
+| `failure` | User is not authorized to disable a rule.
-.2+| `alert_mute`
+.2+| `rule_mute`
+| `unknown` | User is muting a rule.
+| `failure` | User is not authorized to mute a rule.
+
+.2+| `rule_unmute`
+| `unknown` | User is unmuting a rule.
+| `failure` | User is not authorized to unmute a rule.
+
+.2+| `rule_alert_mute`
| `unknown` | User is muting an alert.
| `failure` | User is not authorized to mute an alert.
-.2+| `alert_unmute`
+.2+| `rule_alert_unmute`
| `unknown` | User is unmuting an alert.
| `failure` | User is not authorized to unmute an alert.
-.2+| `alert_instance_mute`
-| `unknown` | User is muting an alert instance.
-| `failure` | User is not authorized to mute an alert instance.
-
-.2+| `alert_instance_unmute`
-| `unknown` | User is unmuting an alert instance.
-| `failure` | User is not authorized to unmute an alert instance.
-
.2+| `space_update`
| `unknown` | User is updating a space.
| `failure` | User is not authorized to update a space.
@@ -183,9 +183,9 @@ Refer to the corresponding {es} logs for potential write errors.
| `unknown` | User is deleting a connector.
| `failure` | User is not authorized to delete a connector.
-.2+| `alert_delete`
-| `unknown` | User is deleting an alert.
-| `failure` | User is not authorized to delete an alert.
+.2+| `rule_delete`
+| `unknown` | User is deleting a rule.
+| `failure` | User is not authorized to delete a rule.
.2+| `space_delete`
| `unknown` | User is deleting a space.
@@ -218,13 +218,13 @@ Refer to the corresponding {es} logs for potential write errors.
| `success` | User has accessed a connector as part of a search operation.
| `failure` | User is not authorized to search for connectors.
-.2+| `alert_get`
-| `success` | User has accessed an alert.
-| `failure` | User is not authorized to access an alert.
+.2+| `rule_get`
+| `success` | User has accessed a rule.
+| `failure` | User is not authorized to access a rule.
-.2+| `alert_find`
-| `success` | User has accessed an alert as part of a search operation.
-| `failure` | User is not authorized to search for alerts.
+.2+| `rule_find`
+| `success` | User has accessed a rule as part of a search operation.
+| `failure` | User is not authorized to search for rules.
.2+| `space_get`
| `success` | User has accessed a space.
diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts
index c81fa7927ef7d..53d888967c431 100644
--- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts
@@ -63,7 +63,7 @@ import { parseDuration } from '../../common/parse_duration';
import { retryIfConflicts } from '../lib/retry_if_conflicts';
import { partiallyUpdateAlert } from '../saved_objects';
import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation';
-import { alertAuditEvent, AlertAuditAction } from './audit_events';
+import { ruleAuditEvent, RuleAuditAction } from './audit_events';
import { KueryNode, nodeBuilder } from '../../../../../src/plugins/data/common';
import { mapSortField } from './lib';
import { getAlertExecutionStatusPending } from '../lib/alert_execution_status';
@@ -253,8 +253,8 @@ export class AlertsClient {
});
} catch (error) {
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.CREATE,
+ ruleAuditEvent({
+ action: RuleAuditAction.CREATE,
savedObject: { type: 'alert', id },
error,
})
@@ -305,8 +305,8 @@ export class AlertsClient {
};
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.CREATE,
+ ruleAuditEvent({
+ action: RuleAuditAction.CREATE,
outcome: 'unknown',
savedObject: { type: 'alert', id },
})
@@ -375,8 +375,8 @@ export class AlertsClient {
});
} catch (error) {
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.GET,
+ ruleAuditEvent({
+ action: RuleAuditAction.GET,
savedObject: { type: 'alert', id },
error,
})
@@ -384,8 +384,8 @@ export class AlertsClient {
throw error;
}
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.GET,
+ ruleAuditEvent({
+ action: RuleAuditAction.GET,
savedObject: { type: 'alert', id },
})
);
@@ -467,8 +467,8 @@ export class AlertsClient {
);
} catch (error) {
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.FIND,
+ ruleAuditEvent({
+ action: RuleAuditAction.FIND,
error,
})
);
@@ -508,8 +508,8 @@ export class AlertsClient {
);
} catch (error) {
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.FIND,
+ ruleAuditEvent({
+ action: RuleAuditAction.FIND,
savedObject: { type: 'alert', id },
error,
})
@@ -525,8 +525,8 @@ export class AlertsClient {
authorizedData.forEach(({ id }) =>
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.FIND,
+ ruleAuditEvent({
+ action: RuleAuditAction.FIND,
savedObject: { type: 'alert', id },
})
)
@@ -620,8 +620,8 @@ export class AlertsClient {
});
} catch (error) {
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.DELETE,
+ ruleAuditEvent({
+ action: RuleAuditAction.DELETE,
savedObject: { type: 'alert', id },
error,
})
@@ -630,8 +630,8 @@ export class AlertsClient {
}
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.DELETE,
+ ruleAuditEvent({
+ action: RuleAuditAction.DELETE,
outcome: 'unknown',
savedObject: { type: 'alert', id },
})
@@ -694,8 +694,8 @@ export class AlertsClient {
});
} catch (error) {
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.UPDATE,
+ ruleAuditEvent({
+ action: RuleAuditAction.UPDATE,
savedObject: { type: 'alert', id },
error,
})
@@ -704,8 +704,8 @@ export class AlertsClient {
}
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.UPDATE,
+ ruleAuditEvent({
+ action: RuleAuditAction.UPDATE,
outcome: 'unknown',
savedObject: { type: 'alert', id },
})
@@ -870,8 +870,8 @@ export class AlertsClient {
}
} catch (error) {
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.UPDATE_API_KEY,
+ ruleAuditEvent({
+ action: RuleAuditAction.UPDATE_API_KEY,
savedObject: { type: 'alert', id },
error,
})
@@ -900,8 +900,8 @@ export class AlertsClient {
});
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.UPDATE_API_KEY,
+ ruleAuditEvent({
+ action: RuleAuditAction.UPDATE_API_KEY,
outcome: 'unknown',
savedObject: { type: 'alert', id },
})
@@ -976,8 +976,8 @@ export class AlertsClient {
}
} catch (error) {
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.ENABLE,
+ ruleAuditEvent({
+ action: RuleAuditAction.ENABLE,
savedObject: { type: 'alert', id },
error,
})
@@ -986,8 +986,8 @@ export class AlertsClient {
}
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.ENABLE,
+ ruleAuditEvent({
+ action: RuleAuditAction.ENABLE,
outcome: 'unknown',
savedObject: { type: 'alert', id },
})
@@ -1090,8 +1090,8 @@ export class AlertsClient {
});
} catch (error) {
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.DISABLE,
+ ruleAuditEvent({
+ action: RuleAuditAction.DISABLE,
savedObject: { type: 'alert', id },
error,
})
@@ -1100,8 +1100,8 @@ export class AlertsClient {
}
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.DISABLE,
+ ruleAuditEvent({
+ action: RuleAuditAction.DISABLE,
outcome: 'unknown',
savedObject: { type: 'alert', id },
})
@@ -1167,8 +1167,8 @@ export class AlertsClient {
}
} catch (error) {
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.MUTE,
+ ruleAuditEvent({
+ action: RuleAuditAction.MUTE,
savedObject: { type: 'alert', id },
error,
})
@@ -1177,8 +1177,8 @@ export class AlertsClient {
}
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.MUTE,
+ ruleAuditEvent({
+ action: RuleAuditAction.MUTE,
outcome: 'unknown',
savedObject: { type: 'alert', id },
})
@@ -1229,8 +1229,8 @@ export class AlertsClient {
}
} catch (error) {
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.UNMUTE,
+ ruleAuditEvent({
+ action: RuleAuditAction.UNMUTE,
savedObject: { type: 'alert', id },
error,
})
@@ -1239,8 +1239,8 @@ export class AlertsClient {
}
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.UNMUTE,
+ ruleAuditEvent({
+ action: RuleAuditAction.UNMUTE,
outcome: 'unknown',
savedObject: { type: 'alert', id },
})
@@ -1291,8 +1291,8 @@ export class AlertsClient {
}
} catch (error) {
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.MUTE_INSTANCE,
+ ruleAuditEvent({
+ action: RuleAuditAction.MUTE_ALERT,
savedObject: { type: 'alert', id: alertId },
error,
})
@@ -1301,8 +1301,8 @@ export class AlertsClient {
}
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.MUTE_INSTANCE,
+ ruleAuditEvent({
+ action: RuleAuditAction.MUTE_ALERT,
outcome: 'unknown',
savedObject: { type: 'alert', id: alertId },
})
@@ -1358,8 +1358,8 @@ export class AlertsClient {
}
} catch (error) {
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.UNMUTE_INSTANCE,
+ ruleAuditEvent({
+ action: RuleAuditAction.UNMUTE_ALERT,
savedObject: { type: 'alert', id: alertId },
error,
})
@@ -1368,8 +1368,8 @@ export class AlertsClient {
}
this.auditLogger?.log(
- alertAuditEvent({
- action: AlertAuditAction.UNMUTE_INSTANCE,
+ ruleAuditEvent({
+ action: RuleAuditAction.UNMUTE_ALERT,
outcome: 'unknown',
savedObject: { type: 'alert', id: alertId },
})
diff --git a/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts b/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts
index 4ccb69832cd26..781b8fe1f4715 100644
--- a/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts
@@ -5,13 +5,13 @@
* 2.0.
*/
-import { AlertAuditAction, alertAuditEvent } from './audit_events';
+import { RuleAuditAction, ruleAuditEvent } from './audit_events';
-describe('#alertAuditEvent', () => {
+describe('#ruleAuditEvent', () => {
test('creates event with `unknown` outcome', () => {
expect(
- alertAuditEvent({
- action: AlertAuditAction.CREATE,
+ ruleAuditEvent({
+ action: RuleAuditAction.CREATE,
outcome: 'unknown',
savedObject: { type: 'alert', id: 'ALERT_ID' },
})
@@ -19,7 +19,7 @@ describe('#alertAuditEvent', () => {
Object {
"error": undefined,
"event": Object {
- "action": "alert_create",
+ "action": "rule_create",
"category": Array [
"database",
],
@@ -34,22 +34,22 @@ describe('#alertAuditEvent', () => {
"type": "alert",
},
},
- "message": "User is creating alert [id=ALERT_ID]",
+ "message": "User is creating rule [id=ALERT_ID]",
}
`);
});
test('creates event with `success` outcome', () => {
expect(
- alertAuditEvent({
- action: AlertAuditAction.CREATE,
+ ruleAuditEvent({
+ action: RuleAuditAction.CREATE,
savedObject: { type: 'alert', id: 'ALERT_ID' },
})
).toMatchInlineSnapshot(`
Object {
"error": undefined,
"event": Object {
- "action": "alert_create",
+ "action": "rule_create",
"category": Array [
"database",
],
@@ -64,15 +64,15 @@ describe('#alertAuditEvent', () => {
"type": "alert",
},
},
- "message": "User has created alert [id=ALERT_ID]",
+ "message": "User has created rule [id=ALERT_ID]",
}
`);
});
test('creates event with `failure` outcome', () => {
expect(
- alertAuditEvent({
- action: AlertAuditAction.CREATE,
+ ruleAuditEvent({
+ action: RuleAuditAction.CREATE,
savedObject: { type: 'alert', id: 'ALERT_ID' },
error: new Error('ERROR_MESSAGE'),
})
@@ -83,7 +83,7 @@ describe('#alertAuditEvent', () => {
"message": "ERROR_MESSAGE",
},
"event": Object {
- "action": "alert_create",
+ "action": "rule_create",
"category": Array [
"database",
],
@@ -98,7 +98,7 @@ describe('#alertAuditEvent', () => {
"type": "alert",
},
},
- "message": "Failed attempt to create alert [id=ALERT_ID]",
+ "message": "Failed attempt to create rule [id=ALERT_ID]",
}
`);
});
diff --git a/x-pack/plugins/alerting/server/alerts_client/audit_events.ts b/x-pack/plugins/alerting/server/alerts_client/audit_events.ts
index 93cca255d6ebc..f04b7c3701974 100644
--- a/x-pack/plugins/alerting/server/alerts_client/audit_events.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/audit_events.ts
@@ -8,67 +8,67 @@
import { EcsEventOutcome, EcsEventType } from 'src/core/server';
import { AuditEvent } from '../../../security/server';
-export enum AlertAuditAction {
- CREATE = 'alert_create',
- GET = 'alert_get',
- UPDATE = 'alert_update',
- UPDATE_API_KEY = 'alert_update_api_key',
- ENABLE = 'alert_enable',
- DISABLE = 'alert_disable',
- DELETE = 'alert_delete',
- FIND = 'alert_find',
- MUTE = 'alert_mute',
- UNMUTE = 'alert_unmute',
- MUTE_INSTANCE = 'alert_instance_mute',
- UNMUTE_INSTANCE = 'alert_instance_unmute',
+export enum RuleAuditAction {
+ CREATE = 'rule_create',
+ GET = 'rule_get',
+ UPDATE = 'rule_update',
+ UPDATE_API_KEY = 'rule_update_api_key',
+ ENABLE = 'rule_enable',
+ DISABLE = 'rule_disable',
+ DELETE = 'rule_delete',
+ FIND = 'rule_find',
+ MUTE = 'rule_mute',
+ UNMUTE = 'rule_unmute',
+ MUTE_ALERT = 'rule_alert_mute',
+ UNMUTE_ALERT = 'rule_alert_unmute',
}
type VerbsTuple = [string, string, string];
-const eventVerbs: Record = {
- alert_create: ['create', 'creating', 'created'],
- alert_get: ['access', 'accessing', 'accessed'],
- alert_update: ['update', 'updating', 'updated'],
- alert_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'],
- alert_enable: ['enable', 'enabling', 'enabled'],
- alert_disable: ['disable', 'disabling', 'disabled'],
- alert_delete: ['delete', 'deleting', 'deleted'],
- alert_find: ['access', 'accessing', 'accessed'],
- alert_mute: ['mute', 'muting', 'muted'],
- alert_unmute: ['unmute', 'unmuting', 'unmuted'],
- alert_instance_mute: ['mute instance of', 'muting instance of', 'muted instance of'],
- alert_instance_unmute: ['unmute instance of', 'unmuting instance of', 'unmuted instance of'],
+const eventVerbs: Record = {
+ rule_create: ['create', 'creating', 'created'],
+ rule_get: ['access', 'accessing', 'accessed'],
+ rule_update: ['update', 'updating', 'updated'],
+ rule_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'],
+ rule_enable: ['enable', 'enabling', 'enabled'],
+ rule_disable: ['disable', 'disabling', 'disabled'],
+ rule_delete: ['delete', 'deleting', 'deleted'],
+ rule_find: ['access', 'accessing', 'accessed'],
+ rule_mute: ['mute', 'muting', 'muted'],
+ rule_unmute: ['unmute', 'unmuting', 'unmuted'],
+ rule_alert_mute: ['mute alert of', 'muting alert of', 'muted alert of'],
+ rule_alert_unmute: ['unmute alert of', 'unmuting alert of', 'unmuted alert of'],
};
-const eventTypes: Record = {
- alert_create: 'creation',
- alert_get: 'access',
- alert_update: 'change',
- alert_update_api_key: 'change',
- alert_enable: 'change',
- alert_disable: 'change',
- alert_delete: 'deletion',
- alert_find: 'access',
- alert_mute: 'change',
- alert_unmute: 'change',
- alert_instance_mute: 'change',
- alert_instance_unmute: 'change',
+const eventTypes: Record = {
+ rule_create: 'creation',
+ rule_get: 'access',
+ rule_update: 'change',
+ rule_update_api_key: 'change',
+ rule_enable: 'change',
+ rule_disable: 'change',
+ rule_delete: 'deletion',
+ rule_find: 'access',
+ rule_mute: 'change',
+ rule_unmute: 'change',
+ rule_alert_mute: 'change',
+ rule_alert_unmute: 'change',
};
-export interface AlertAuditEventParams {
- action: AlertAuditAction;
+export interface RuleAuditEventParams {
+ action: RuleAuditAction;
outcome?: EcsEventOutcome;
savedObject?: NonNullable['saved_object'];
error?: Error;
}
-export function alertAuditEvent({
+export function ruleAuditEvent({
action,
savedObject,
outcome,
error,
-}: AlertAuditEventParams): AuditEvent {
- const doc = savedObject ? `alert [id=${savedObject.id}]` : 'an alert';
+}: RuleAuditEventParams): AuditEvent {
+ const doc = savedObject ? `rule [id=${savedObject.id}]` : 'a rule';
const [present, progressive, past] = eventVerbs[action];
const message = error
? `Failed attempt to ${present} ${doc}`
diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts
index a2d5a5e0386c4..793357215d382 100644
--- a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts
@@ -226,7 +226,7 @@ describe('create()', () => {
});
describe('auditLogger', () => {
- test('logs audit event when creating an alert', async () => {
+ test('logs audit event when creating a rule', async () => {
const data = getMockData({
enabled: false,
actions: [],
@@ -241,7 +241,7 @@ describe('create()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_create',
+ action: 'rule_create',
outcome: 'unknown',
}),
kibana: { saved_object: { id: 'mock-saved-object-id', type: 'alert' } },
@@ -249,7 +249,7 @@ describe('create()', () => {
);
});
- test('logs audit event when not authorised to create an alert', async () => {
+ test('logs audit event when not authorised to create a rule', async () => {
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
await expect(
@@ -263,7 +263,7 @@ describe('create()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_create',
+ action: 'rule_create',
outcome: 'failure',
}),
kibana: {
diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts
index 0f9d91d829854..ca0f0cf0fb5a6 100644
--- a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts
@@ -258,12 +258,12 @@ describe('delete()', () => {
});
describe('auditLogger', () => {
- test('logs audit event when deleting an alert', async () => {
+ test('logs audit event when deleting a rule', async () => {
await alertsClient.delete({ id: '1' });
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_delete',
+ action: 'rule_delete',
outcome: 'unknown',
}),
kibana: { saved_object: { id: '1', type: 'alert' } },
@@ -271,14 +271,14 @@ describe('delete()', () => {
);
});
- test('logs audit event when not authorised to delete an alert', async () => {
+ test('logs audit event when not authorised to delete a rule', async () => {
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
await expect(alertsClient.delete({ id: '1' })).rejects.toThrow();
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_delete',
+ action: 'rule_delete',
outcome: 'failure',
}),
kibana: {
diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts
index 7eb107c2f4dec..da1c5ea8bfe8d 100644
--- a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts
@@ -126,12 +126,12 @@ describe('disable()', () => {
});
describe('auditLogger', () => {
- test('logs audit event when disabling an alert', async () => {
+ test('logs audit event when disabling a rule', async () => {
await alertsClient.disable({ id: '1' });
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_disable',
+ action: 'rule_disable',
outcome: 'unknown',
}),
kibana: { saved_object: { id: '1', type: 'alert' } },
@@ -139,14 +139,14 @@ describe('disable()', () => {
);
});
- test('logs audit event when not authorised to disable an alert', async () => {
+ test('logs audit event when not authorised to disable a rule', async () => {
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
await expect(alertsClient.disable({ id: '1' })).rejects.toThrow();
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_disable',
+ action: 'rule_disable',
outcome: 'failure',
}),
kibana: {
diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts
index 8329e52d7444a..b3c8d3bd83980 100644
--- a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts
@@ -165,12 +165,12 @@ describe('enable()', () => {
});
describe('auditLogger', () => {
- test('logs audit event when enabling an alert', async () => {
+ test('logs audit event when enabling a rule', async () => {
await alertsClient.enable({ id: '1' });
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_enable',
+ action: 'rule_enable',
outcome: 'unknown',
}),
kibana: { saved_object: { id: '1', type: 'alert' } },
@@ -178,14 +178,14 @@ describe('enable()', () => {
);
});
- test('logs audit event when not authorised to enable an alert', async () => {
+ test('logs audit event when not authorised to enable a rule', async () => {
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
await expect(alertsClient.enable({ id: '1' })).rejects.toThrow();
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_enable',
+ action: 'rule_enable',
outcome: 'failure',
}),
kibana: {
diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts
index 8fa8ae7ae38b0..fe788cd43bc2b 100644
--- a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts
@@ -277,13 +277,13 @@ describe('find()', () => {
});
describe('auditLogger', () => {
- test('logs audit event when searching alerts', async () => {
+ test('logs audit event when searching rules', async () => {
const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
await alertsClient.find();
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_find',
+ action: 'rule_find',
outcome: 'success',
}),
kibana: { saved_object: { id: '1', type: 'alert' } },
@@ -291,7 +291,7 @@ describe('find()', () => {
);
});
- test('logs audit event when not authorised to search alerts', async () => {
+ test('logs audit event when not authorised to search rules', async () => {
const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('Unauthorized'));
@@ -299,7 +299,7 @@ describe('find()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_find',
+ action: 'rule_find',
outcome: 'failure',
}),
error: {
@@ -310,7 +310,7 @@ describe('find()', () => {
);
});
- test('logs audit event when not authorised to search alert type', async () => {
+ test('logs audit event when not authorised to search rule type', async () => {
const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
authorization.getFindAuthorizationFilter.mockResolvedValue({
ensureRuleTypeIsAuthorized: jest.fn(() => {
@@ -323,7 +323,7 @@ describe('find()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_find',
+ action: 'rule_find',
outcome: 'failure',
}),
kibana: { saved_object: { id: '1', type: 'alert' } },
diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts
index a958ea4061ae5..1be9d3e3ba2c9 100644
--- a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts
@@ -226,13 +226,13 @@ describe('get()', () => {
});
});
- test('logs audit event when getting an alert', async () => {
+ test('logs audit event when getting a rule', async () => {
const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
await alertsClient.get({ id: '1' });
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_get',
+ action: 'rule_get',
outcome: 'success',
}),
kibana: { saved_object: { id: '1', type: 'alert' } },
@@ -240,7 +240,7 @@ describe('get()', () => {
);
});
- test('logs audit event when not authorised to get an alert', async () => {
+ test('logs audit event when not authorised to get a rule', async () => {
const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
@@ -248,7 +248,7 @@ describe('get()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_get',
+ action: 'rule_get',
outcome: 'failure',
}),
kibana: {
diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts
index 6734ec9b99600..43f43b539ebf2 100644
--- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts
@@ -155,7 +155,7 @@ describe('muteAll()', () => {
});
describe('auditLogger', () => {
- test('logs audit event when muting an alert', async () => {
+ test('logs audit event when muting a rule', async () => {
const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
@@ -181,7 +181,7 @@ describe('muteAll()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_mute',
+ action: 'rule_mute',
outcome: 'unknown',
}),
kibana: { saved_object: { id: '1', type: 'alert' } },
@@ -189,7 +189,7 @@ describe('muteAll()', () => {
);
});
- test('logs audit event when not authorised to mute an alert', async () => {
+ test('logs audit event when not authorised to mute a rule', async () => {
const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
@@ -217,7 +217,7 @@ describe('muteAll()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_mute',
+ action: 'rule_mute',
outcome: 'failure',
}),
kibana: {
diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts
index bc0b7288e952f..e2e4aff61866b 100644
--- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts
@@ -189,7 +189,7 @@ describe('muteInstance()', () => {
});
describe('auditLogger', () => {
- test('logs audit event when muting an alert instance', async () => {
+ test('logs audit event when muting an alert', async () => {
const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
@@ -209,7 +209,7 @@ describe('muteInstance()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_instance_mute',
+ action: 'rule_alert_mute',
outcome: 'unknown',
}),
kibana: { saved_object: { id: '1', type: 'alert' } },
@@ -217,7 +217,7 @@ describe('muteInstance()', () => {
);
});
- test('logs audit event when not authorised to mute an alert instance', async () => {
+ test('logs audit event when not authorised to mute an alert', async () => {
const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
@@ -241,7 +241,7 @@ describe('muteInstance()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_instance_mute',
+ action: 'rule_alert_mute',
outcome: 'failure',
}),
kibana: {
diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts
index c061bc7840fb6..02439d3cd6bad 100644
--- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts
@@ -155,7 +155,7 @@ describe('unmuteAll()', () => {
});
describe('auditLogger', () => {
- test('logs audit event when unmuting an alert', async () => {
+ test('logs audit event when unmuting a rule', async () => {
const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
@@ -181,7 +181,7 @@ describe('unmuteAll()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_unmute',
+ action: 'rule_unmute',
outcome: 'unknown',
}),
kibana: { saved_object: { id: '1', type: 'alert' } },
@@ -189,7 +189,7 @@ describe('unmuteAll()', () => {
);
});
- test('logs audit event when not authorised to unmute an alert', async () => {
+ test('logs audit event when not authorised to unmute a rule', async () => {
const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
@@ -217,7 +217,7 @@ describe('unmuteAll()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_unmute',
+ action: 'rule_unmute',
outcome: 'failure',
}),
kibana: {
diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts
index 4da83b6441a8d..3f3ec697a9478 100644
--- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts
@@ -187,7 +187,7 @@ describe('unmuteInstance()', () => {
});
describe('auditLogger', () => {
- test('logs audit event when unmuting an alert instance', async () => {
+ test('logs audit event when unmuting an alert', async () => {
const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
@@ -207,7 +207,7 @@ describe('unmuteInstance()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_instance_unmute',
+ action: 'rule_alert_unmute',
outcome: 'unknown',
}),
kibana: { saved_object: { id: '1', type: 'alert' } },
@@ -215,7 +215,7 @@ describe('unmuteInstance()', () => {
);
});
- test('logs audit event when not authorised to unmute an alert instance', async () => {
+ test('logs audit event when not authorised to unmute an alert', async () => {
const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger });
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
@@ -239,7 +239,7 @@ describe('unmuteInstance()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_instance_unmute',
+ action: 'rule_alert_unmute',
outcome: 'failure',
}),
kibana: {
diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts
index c743312ef2c4b..350c9ed31298f 100644
--- a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts
@@ -1476,7 +1476,7 @@ describe('update()', () => {
});
});
- test('logs audit event when updating an alert', async () => {
+ test('logs audit event when updating a rule', async () => {
await alertsClient.update({
id: '1',
data: {
@@ -1495,7 +1495,7 @@ describe('update()', () => {
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_update',
+ action: 'rule_update',
outcome: 'unknown',
}),
kibana: { saved_object: { id: '1', type: 'alert' } },
@@ -1503,7 +1503,7 @@ describe('update()', () => {
);
});
- test('logs audit event when not authorised to update an alert', async () => {
+ test('logs audit event when not authorised to update a rule', async () => {
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
await expect(
@@ -1526,7 +1526,7 @@ describe('update()', () => {
expect.objectContaining({
event: expect.objectContaining({
outcome: 'failure',
- action: 'alert_update',
+ action: 'rule_update',
}),
kibana: {
saved_object: {
diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts
index 4215f14b4a560..15aa0dbc64eb8 100644
--- a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts
+++ b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts
@@ -295,13 +295,13 @@ describe('updateApiKey()', () => {
});
describe('auditLogger', () => {
- test('logs audit event when updating the API key of an alert', async () => {
+ test('logs audit event when updating the API key of a rule', async () => {
await alertsClient.updateApiKey({ id: '1' });
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
- action: 'alert_update_api_key',
+ action: 'rule_update_api_key',
outcome: 'unknown',
}),
kibana: { saved_object: { id: '1', type: 'alert' } },
@@ -309,7 +309,7 @@ describe('updateApiKey()', () => {
);
});
- test('logs audit event when not authorised to update the API key of an alert', async () => {
+ test('logs audit event when not authorised to update the API key of a rule', async () => {
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrow();
@@ -317,7 +317,7 @@ describe('updateApiKey()', () => {
expect.objectContaining({
event: expect.objectContaining({
outcome: 'failure',
- action: 'alert_update_api_key',
+ action: 'rule_update_api_key',
}),
kibana: {
saved_object: {
From f53fc8d3de6823a969ef31fb8ddb6f2448e7c0e1 Mon Sep 17 00:00:00 2001
From: ymao1
Date: Mon, 21 Jun 2021 14:27:18 -0400
Subject: [PATCH 022/163] [Alerting][Docs] Removing placeholder sections in
docs (#101953)
* Removing placeholder prerequisite section
* Removing placeholder defining-rules and rule-management
* Fixing links
* Setup to set up
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
docs/user/alerting/alerting-getting-started.asciidoc | 5 -----
docs/user/alerting/alerting-setup.asciidoc | 4 ++--
docs/user/alerting/defining-rules.asciidoc | 11 -----------
docs/user/alerting/index.asciidoc | 2 --
docs/user/alerting/rule-management.asciidoc | 5 -----
docs/user/alerting/rule-types.asciidoc | 2 +-
6 files changed, 3 insertions(+), 26 deletions(-)
delete mode 100644 docs/user/alerting/defining-rules.asciidoc
delete mode 100644 docs/user/alerting/rule-management.asciidoc
diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc
index 8c17f8ec93b96..b699c56ebd944 100644
--- a/docs/user/alerting/alerting-getting-started.asciidoc
+++ b/docs/user/alerting/alerting-getting-started.asciidoc
@@ -136,9 +136,4 @@ Functionally, {kib} alerting differs in that:
At a higher level, {kib} alerting allows rich integrations across use cases like <>, <>, <>, and <>.
Pre-packaged *rule types* simplify setup and hide the details of complex, domain-specific detections, while providing a consistent interface across {kib}.
-[float]
-[[alerting-setup-prerequisites]]
-== Prerequisites
-<>
-
--
\ No newline at end of file
diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc
index 39f1af0030e0a..2ae5160069f0a 100644
--- a/docs/user/alerting/alerting-setup.asciidoc
+++ b/docs/user/alerting/alerting-setup.asciidoc
@@ -1,8 +1,8 @@
[role="xpack"]
[[alerting-setup]]
-== Alerting Setup
+== Alerting Set up
++++
-Setup
+Set up
++++
The Alerting feature is automatically enabled in {kib}, but might require some additional configuration.
diff --git a/docs/user/alerting/defining-rules.asciidoc b/docs/user/alerting/defining-rules.asciidoc
deleted file mode 100644
index 686a7bbc8a37b..0000000000000
--- a/docs/user/alerting/defining-rules.asciidoc
+++ /dev/null
@@ -1,11 +0,0 @@
-[role="xpack"]
-[[defining-alerts]]
-== Defining rules
-
-This content has been moved to <>.
-
-[float]
-[[defining-alerts-general-details]]
-==== General rule details
-
-This content has been moved to <>.
\ No newline at end of file
diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc
index 9ab6a2dc46ebf..957d99a54ebaa 100644
--- a/docs/user/alerting/index.asciidoc
+++ b/docs/user/alerting/index.asciidoc
@@ -1,7 +1,5 @@
include::alerting-getting-started.asciidoc[]
include::alerting-setup.asciidoc[]
include::create-and-manage-rules.asciidoc[]
-include::defining-rules.asciidoc[]
-include::rule-management.asciidoc[]
include::rule-types.asciidoc[]
include::alerting-troubleshooting.asciidoc[]
diff --git a/docs/user/alerting/rule-management.asciidoc b/docs/user/alerting/rule-management.asciidoc
deleted file mode 100644
index d6349a60e08eb..0000000000000
--- a/docs/user/alerting/rule-management.asciidoc
+++ /dev/null
@@ -1,5 +0,0 @@
-[role="xpack"]
-[[alert-management]]
-== Managing rules
-
-This content has been moved to <>.
\ No newline at end of file
diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc
index bb840014fe80f..f7f57d2f845a0 100644
--- a/docs/user/alerting/rule-types.asciidoc
+++ b/docs/user/alerting/rule-types.asciidoc
@@ -15,7 +15,7 @@ see {subscriptions}[the subscription page].
[[stack-rules]]
=== Stack rules
-<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information.
+<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information.
[cols="2*<"]
|===
From 3662cf40923e104c5d24f683141588cba68ea6d8 Mon Sep 17 00:00:00 2001
From: Marius Dragomir
Date: Mon, 21 Jun 2021 20:33:12 +0200
Subject: [PATCH 023/163] [QA] Changes the telemetry banner dismiss location
(#102721)
* change telemetry banner dismiss location
* use dismissBanner method instead
* Update _metricbeat_dashboard.js
* Update _maps.ts
* Update _maps.ts
* Update _metricbeat_dashboard.js
---
x-pack/test/stack_functional_integration/apps/maps/_maps.ts | 5 ++---
.../apps/metricbeat/_metricbeat_dashboard.js | 5 ++---
.../apps/telemetry/_telemetry.js | 3 +++
3 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/x-pack/test/stack_functional_integration/apps/maps/_maps.ts b/x-pack/test/stack_functional_integration/apps/maps/_maps.ts
index 890d467f0311a..4b7b12dc5bef2 100644
--- a/x-pack/test/stack_functional_integration/apps/maps/_maps.ts
+++ b/x-pack/test/stack_functional_integration/apps/maps/_maps.ts
@@ -15,13 +15,12 @@ export default function ({
}: FtrProviderContext & { updateBaselines: boolean }) {
const screenshot = getService('screenshots');
const browser = getService('browser');
- const find = getService('find');
- const PageObjects = getPageObjects(['maps']);
+ const PageObjects = getPageObjects(['common', 'maps']);
describe('check Elastic Maps Server', function () {
before(async function () {
await PageObjects.maps.loadSavedMap('EMS Test');
- await find.clickByButtonText('Dismiss');
+ await PageObjects.common.dismissBanner();
await browser.setScreenshotSize(1000, 1000);
});
diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js
index 2a04d40d0b727..ac911a941c146 100644
--- a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js
+++ b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js
@@ -15,7 +15,6 @@ const ARCHIVE = resolve(INTEGRATION_TEST_ROOT, 'test/es_archives/metricbeat');
export default function ({ getService, getPageObjects, updateBaselines }) {
const screenshot = getService('screenshots');
const browser = getService('browser');
- const find = getService('find');
const log = getService('log');
const esArchiver = getService('esArchiver');
const PageObjects = getPageObjects(['common', 'dashboard', 'timePicker']);
@@ -47,7 +46,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) {
// await PageObjects.dashboard.clickFullScreenMode();
await PageObjects.common.sleep(2000);
- await find.clickByButtonText('Dismiss');
+ await PageObjects.common.dismissBanner();
await PageObjects.dashboard.waitForRenderComplete();
await PageObjects.common.sleep(2000);
await browser.setScreenshotSize(1000, 1337);
@@ -64,7 +63,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) {
'metricbeat_dashboard',
updateBaselines
);
- expect(percentDifference).to.be.lessThan(0.01);
+ expect(percentDifference).to.be.lessThan(0.017);
} finally {
log.debug('### Screenshot taken');
}
diff --git a/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js b/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js
index 71133cf011cfc..d35102cae1b7f 100644
--- a/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js
+++ b/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js
@@ -18,6 +18,9 @@ export default ({ getService, getPageObjects }) => {
await browser.setWindowSize(1200, 800);
await PageObjects.common.navigateToApp('home');
});
+ after(async function () {
+ await PageObjects.common.dismissBanner();
+ });
it('should show banner Help us improve the Elastic Stack', async () => {
const actualMessage = await PageObjects.common.getWelcomeText();
From 3b0be1bbf5ce814cb3d200484450d88eec74d5c0 Mon Sep 17 00:00:00 2001
From: Wylie Conlon
Date: Mon, 21 Jun 2021 14:45:35 -0400
Subject: [PATCH 024/163] [Lens] Error on array values in math (#102371)
* [Lens] Error on array values in math
* Update error messages
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../expression_functions/specs/math_column.ts | 47 ++++++++++++-------
.../specs/tests/math_column.test.ts | 24 ++++++++++
.../common/expression_types/get_type.test.ts | 1 +
.../common/expression_types/get_type.ts | 3 ++
.../operations/definitions/formula/math.tsx | 2 +-
5 files changed, 60 insertions(+), 17 deletions(-)
diff --git a/src/plugins/expressions/common/expression_functions/specs/math_column.ts b/src/plugins/expressions/common/expression_functions/specs/math_column.ts
index 0ff8faf3ce55a..633d912c29502 100644
--- a/src/plugins/expressions/common/expression_functions/specs/math_column.ts
+++ b/src/plugins/expressions/common/expression_functions/specs/math_column.ts
@@ -69,25 +69,40 @@ export const mathColumn: ExpressionFunctionDefinition<
return id === args.id;
});
if (existingColumnIndex > -1) {
- throw new Error('ID must be unique');
+ throw new Error(
+ i18n.translate('expressions.functions.mathColumn.uniqueIdError', {
+ defaultMessage: 'ID must be unique',
+ })
+ );
}
const newRows = input.rows.map((row) => {
- return {
- ...row,
- [args.id]: math.fn(
- {
- type: 'datatable',
- columns: input.columns,
- rows: [row],
- },
- {
- expression: args.expression,
- onError: args.onError,
- },
- context
- ),
- };
+ const result = math.fn(
+ {
+ type: 'datatable',
+ columns: input.columns,
+ rows: [row],
+ },
+ {
+ expression: args.expression,
+ onError: args.onError,
+ },
+ context
+ );
+
+ if (Array.isArray(result)) {
+ if (result.length === 1) {
+ return { ...row, [args.id]: result[0] };
+ }
+ throw new Error(
+ i18n.translate('expressions.functions.mathColumn.arrayValueError', {
+ defaultMessage: 'Cannot perform math on array values at {name}',
+ values: { name: args.name },
+ })
+ );
+ }
+
+ return { ...row, [args.id]: result };
});
const type = newRows.length ? getType(newRows[0][args.id]) : 'null';
const newColumn: DatatableColumn = {
diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts
index bc6699a2b689b..e0fb0a3a9f23d 100644
--- a/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts
+++ b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts
@@ -34,6 +34,30 @@ describe('mathColumn', () => {
});
});
+ it('extracts a single array value, but not a multi-value array', () => {
+ const arrayTable = {
+ ...testTable,
+ rows: [
+ {
+ name: 'product1',
+ time: 1517842800950, // 05 Feb 2018 15:00:00 GMT
+ price: [605, 500],
+ quantity: [100],
+ in_stock: true,
+ },
+ ],
+ };
+ const args = {
+ id: 'output',
+ name: 'output',
+ expression: 'quantity',
+ };
+ expect(fn(arrayTable, args).rows[0].output).toEqual(100);
+ expect(() => fn(arrayTable, { ...args, expression: 'price' })).toThrowError(
+ `Cannot perform math on array values`
+ );
+ });
+
it('handles onError', () => {
const args = {
id: 'output',
diff --git a/src/plugins/expressions/common/expression_types/get_type.test.ts b/src/plugins/expressions/common/expression_types/get_type.test.ts
index 6eca54d2aea44..b1a9cb703182f 100644
--- a/src/plugins/expressions/common/expression_types/get_type.test.ts
+++ b/src/plugins/expressions/common/expression_types/get_type.test.ts
@@ -30,6 +30,7 @@ describe('getType()', () => {
});
test('throws if object has no .type property', () => {
+ expect(() => getType([])).toThrow();
expect(() => getType({})).toThrow();
expect(() => getType({ _type: 'foo' })).toThrow();
expect(() => getType({ tipe: 'foo' })).toThrow();
diff --git a/src/plugins/expressions/common/expression_types/get_type.ts b/src/plugins/expressions/common/expression_types/get_type.ts
index e29a610b3ed90..052508df41329 100644
--- a/src/plugins/expressions/common/expression_types/get_type.ts
+++ b/src/plugins/expressions/common/expression_types/get_type.ts
@@ -8,6 +8,9 @@
export function getType(node: any) {
if (node == null) return 'null';
+ if (Array.isArray(node)) {
+ throw new Error('Unexpected array value encountered.');
+ }
if (typeof node === 'object') {
if (!node.type) throw new Error('Objects must have a type property');
return node.type;
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx
index 52522a18604aa..7aae35f496923 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx
@@ -48,7 +48,7 @@ export const mathOperation: OperationDefinition
Date: Mon, 21 Jun 2021 11:59:52 -0700
Subject: [PATCH 025/163] [App Search] Convert Engine subnav and Engine
Overview pages to new page template (#102679)
* Update routers
- To facilitate this being broken up into multiple PRs, non-migrated views still get a wrapping layout
* Set up Engine subnav in EuiSideNav format
+ tweak CSS for new label - heads up that this will break the old nav, but will disappear as the pages get converted
* Convert Engine Overview pages to new page template
* [PR feedback] Code comment explaining side nav icon reorder
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/engine/engine_nav.scss | 16 +-
.../components/engine/engine_nav.test.tsx | 306 +++++++++++++++++-
.../components/engine/engine_nav.tsx | 261 ++++++++++++++-
.../components/engine/engine_router.test.tsx | 9 +-
.../components/engine/engine_router.tsx | 136 ++++----
.../engine_overview/engine_overview.test.tsx | 6 +-
.../engine_overview/engine_overview.tsx | 6 +-
.../engine_overview_empty.test.tsx | 12 +-
.../engine_overview/engine_overview_empty.tsx | 31 +-
.../engine_overview_metrics.test.tsx | 6 +-
.../engine_overview_metrics.tsx | 21 +-
.../app_search/components/layout/nav.test.tsx | 3 +
.../app_search/components/layout/nav.tsx | 3 +-
.../public/applications/app_search/index.tsx | 6 +-
14 files changed, 695 insertions(+), 127 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss
index c750f63dab248..486abeb3dce4c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss
@@ -6,21 +6,17 @@
*/
.appSearchNavEngineLabel {
- padding-top: $euiSizeS;
+ margin-left: $euiSizeS;
+ padding-top: $euiSizeXS;
padding-bottom: $euiSizeS;
- .euiText {
- font-weight: $euiFontWeightMedium;
- }
.euiBadge {
margin-top: $euiSizeXS;
}
}
-.appSearchNavIcons {
- // EUI override
- &.euiFlexItem {
- flex-grow: 0;
- flex-direction: row;
- }
+.appSearchNavIcon {
+ // EuiSideNav renders icons to the left of the nav link by default, but we use icons
+ // as warning or error indicators & prefer to render them on the right side of the nav
+ order: 1;
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx
index c2b0a6a50fd06..015fb997c29ed 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx
@@ -6,8 +6,14 @@
*/
import { setMockValues } from '../../../__mocks__/kea_logic';
+import { mockUseRouteMatch } from '../../../__mocks__/react_router';
import { mockEngineValues } from '../../__mocks__';
+jest.mock('../../../shared/layout', () => ({
+ ...jest.requireActual('../../../shared/layout'), // TODO: Remove once side nav components are gone
+ generateNavLink: jest.fn(({ to }) => ({ href: to })),
+}));
+
import React from 'react';
import { shallow } from 'enzyme';
@@ -16,7 +22,305 @@ import { EuiBadge, EuiIcon } from '@elastic/eui';
import { rerender } from '../../../test_helpers';
-import { EngineNav } from './engine_nav';
+import { useEngineNav, EngineNav } from './engine_nav';
+
+describe('useEngineNav', () => {
+ const values = { ...mockEngineValues, myRole: {}, dataLoading: false };
+
+ beforeEach(() => {
+ setMockValues(values);
+ mockUseRouteMatch.mockReturnValue(true);
+ });
+
+ describe('returns empty', () => {
+ it('does not return engine nav items if not on an engine route', () => {
+ mockUseRouteMatch.mockReturnValueOnce(false);
+ expect(useEngineNav()).toBeUndefined();
+ });
+
+ it('does not return engine nav items if data is still loading', () => {
+ setMockValues({ ...values, dataLoading: true });
+ expect(useEngineNav()).toBeUndefined();
+ });
+
+ it('does not return engine nav items if engine data is missing', () => {
+ setMockValues({ ...values, engineName: '' });
+ expect(useEngineNav()).toBeUndefined();
+ });
+ });
+
+ describe('returns an array of EUI side nav items', () => {
+ const BASE_NAV = [
+ {
+ id: 'engineName',
+ name: 'some-engine',
+ renderItem: expect.any(Function),
+ 'data-test-subj': 'EngineLabel',
+ },
+ {
+ id: 'overview',
+ name: 'Overview',
+ href: '/engines/some-engine',
+ 'data-test-subj': 'EngineOverviewLink',
+ },
+ ];
+
+ it('always returns an engine label and overview link', () => {
+ expect(useEngineNav()).toEqual(BASE_NAV);
+ });
+
+ describe('engine label', () => {
+ const renderEngineLabel = (engineNav: any) => {
+ return shallow(engineNav[0].renderItem() as any);
+ };
+
+ it('renders the capitalized engine name', () => {
+ const wrapper = renderEngineLabel(useEngineNav());
+ const name = wrapper.find('.eui-textTruncate');
+
+ expect(name.text()).toEqual('SOME-ENGINE');
+ expect(wrapper.find(EuiBadge)).toHaveLength(0);
+ });
+
+ it('renders a sample engine badge for the sample engine', () => {
+ setMockValues({ ...values, isSampleEngine: true });
+ const wrapper = renderEngineLabel(useEngineNav());
+
+ expect(wrapper.find(EuiBadge).prop('children')).toEqual('SAMPLE ENGINE');
+ });
+
+ it('renders a meta engine badge for meta engines', () => {
+ setMockValues({ ...values, isMetaEngine: true });
+ const wrapper = renderEngineLabel(useEngineNav());
+
+ expect(wrapper.find(EuiBadge).prop('children')).toEqual('META ENGINE');
+ });
+ });
+
+ it('returns an analytics nav item', () => {
+ setMockValues({ ...values, myRole: { canViewEngineAnalytics: true } });
+ expect(useEngineNav()).toEqual([
+ ...BASE_NAV,
+ {
+ id: 'analytics',
+ name: 'Analytics',
+ href: '/engines/some-engine/analytics',
+ 'data-test-subj': 'EngineAnalyticsLink',
+ },
+ ]);
+ });
+
+ it('returns a documents nav item', () => {
+ setMockValues({ ...values, myRole: { canViewEngineDocuments: true } });
+ expect(useEngineNav()).toEqual([
+ ...BASE_NAV,
+ {
+ id: 'documents',
+ name: 'Documents',
+ href: '/engines/some-engine/documents',
+ 'data-test-subj': 'EngineDocumentsLink',
+ },
+ ]);
+ });
+
+ it('returns a schema nav item', () => {
+ setMockValues({ ...values, myRole: { canViewEngineSchema: true } });
+ expect(useEngineNav()).toEqual([
+ ...BASE_NAV,
+ {
+ id: 'schema',
+ name: 'Schema',
+ href: '/engines/some-engine/schema',
+ 'data-test-subj': 'EngineSchemaLink',
+ icon: expect.anything(),
+ },
+ ]);
+ });
+
+ describe('schema nav icons', () => {
+ const myRole = { canViewEngineSchema: true };
+
+ const renderIcons = (engineNav: any) => {
+ return shallow({engineNav[2].icon}
);
+ };
+
+ it('renders schema errors alert icon', () => {
+ setMockValues({ ...values, myRole, hasSchemaErrors: true });
+ const wrapper = renderIcons(useEngineNav());
+
+ expect(wrapper.find('[data-test-subj="EngineNavSchemaErrors"]')).toHaveLength(1);
+ });
+
+ it('renders unconfirmed schema fields info icon', () => {
+ setMockValues({ ...values, myRole, hasUnconfirmedSchemaFields: true });
+ const wrapper = renderIcons(useEngineNav());
+
+ expect(wrapper.find('[data-test-subj="EngineNavSchemaUnconfirmedFields"]')).toHaveLength(1);
+ });
+
+ it('renders schema conflicts alert icon', () => {
+ setMockValues({ ...values, myRole, hasSchemaConflicts: true });
+ const wrapper = renderIcons(useEngineNav());
+
+ expect(wrapper.find('[data-test-subj="EngineNavSchemaConflicts"]')).toHaveLength(1);
+ });
+ });
+
+ describe('crawler', () => {
+ const myRole = { canViewEngineCrawler: true };
+
+ it('returns a crawler nav item', () => {
+ setMockValues({ ...values, myRole });
+ expect(useEngineNav()).toEqual([
+ ...BASE_NAV,
+ {
+ id: 'crawler',
+ name: 'Web Crawler',
+ href: '/engines/some-engine/crawler',
+ 'data-test-subj': 'EngineCrawlerLink',
+ },
+ ]);
+ });
+
+ it('does not return a crawler nav item for meta engines', () => {
+ setMockValues({ ...values, myRole, isMetaEngine: true });
+ expect(useEngineNav()).toEqual(BASE_NAV);
+ });
+ });
+
+ describe('meta engine source engines', () => {
+ const myRole = { canViewMetaEngineSourceEngines: true };
+
+ it('returns a source engines nav item', () => {
+ setMockValues({ ...values, myRole, isMetaEngine: true });
+ expect(useEngineNav()).toEqual([
+ ...BASE_NAV,
+ {
+ id: 'sourceEngines',
+ name: 'Engines',
+ href: '/engines/some-engine/engines',
+ 'data-test-subj': 'MetaEngineEnginesLink',
+ },
+ ]);
+ });
+
+ it('does not return a source engines nav item for non-meta engines', () => {
+ setMockValues({ ...values, myRole, isMetaEngine: false });
+ expect(useEngineNav()).toEqual(BASE_NAV);
+ });
+ });
+
+ it('returns a relevance tuning nav item', () => {
+ setMockValues({ ...values, myRole: { canManageEngineRelevanceTuning: true } });
+ expect(useEngineNav()).toEqual([
+ ...BASE_NAV,
+ {
+ id: 'relevanceTuning',
+ name: 'Relevance Tuning',
+ href: '/engines/some-engine/relevance_tuning',
+ 'data-test-subj': 'EngineRelevanceTuningLink',
+ icon: expect.anything(),
+ },
+ ]);
+ });
+
+ describe('relevance tuning nav icons', () => {
+ const myRole = { canManageEngineRelevanceTuning: true };
+
+ const renderIcons = (engineNav: any) => {
+ return shallow({engineNav[2].icon}
);
+ };
+
+ it('renders unconfirmed schema fields info icon', () => {
+ setMockValues({ ...values, myRole, engine: { unsearchedUnconfirmedFields: true } });
+ const wrapper = renderIcons(useEngineNav());
+ expect(
+ wrapper.find('[data-test-subj="EngineNavRelevanceTuningUnsearchedFields"]')
+ ).toHaveLength(1);
+ });
+
+ it('renders schema conflicts alert icon', () => {
+ setMockValues({ ...values, myRole, engine: { invalidBoosts: true } });
+ const wrapper = renderIcons(useEngineNav());
+ expect(
+ wrapper.find('[data-test-subj="EngineNavRelevanceTuningInvalidBoosts"]')
+ ).toHaveLength(1);
+ });
+
+ it('can render multiple icons', () => {
+ const engine = { invalidBoosts: true, unsearchedUnconfirmedFields: true };
+ setMockValues({ ...values, myRole, engine });
+ const wrapper = renderIcons(useEngineNav());
+ expect(wrapper.find(EuiIcon)).toHaveLength(2);
+ });
+ });
+
+ it('returns a synonyms nav item', () => {
+ setMockValues({ ...values, myRole: { canManageEngineSynonyms: true } });
+ expect(useEngineNav()).toEqual([
+ ...BASE_NAV,
+ {
+ id: 'synonyms',
+ name: 'Synonyms',
+ href: '/engines/some-engine/synonyms',
+ 'data-test-subj': 'EngineSynonymsLink',
+ },
+ ]);
+ });
+
+ it('returns a curations nav item', () => {
+ setMockValues({ ...values, myRole: { canManageEngineCurations: true } });
+ expect(useEngineNav()).toEqual([
+ ...BASE_NAV,
+ {
+ id: 'curations',
+ name: 'Curations',
+ href: '/engines/some-engine/curations',
+ 'data-test-subj': 'EngineCurationsLink',
+ },
+ ]);
+ });
+
+ it('returns a results settings nav item', () => {
+ setMockValues({ ...values, myRole: { canManageEngineResultSettings: true } });
+ expect(useEngineNav()).toEqual([
+ ...BASE_NAV,
+ {
+ id: 'resultSettings',
+ name: 'Result Settings',
+ href: '/engines/some-engine/result_settings',
+ 'data-test-subj': 'EngineResultSettingsLink',
+ },
+ ]);
+ });
+
+ it('returns a Search UI nav item', () => {
+ setMockValues({ ...values, myRole: { canManageEngineSearchUi: true } });
+ expect(useEngineNav()).toEqual([
+ ...BASE_NAV,
+ {
+ id: 'searchUI',
+ name: 'Search UI',
+ href: '/engines/some-engine/search_ui',
+ 'data-test-subj': 'EngineSearchUILink',
+ },
+ ]);
+ });
+
+ it('returns an API logs nav item', () => {
+ setMockValues({ ...values, myRole: { canViewEngineApiLogs: true } });
+ expect(useEngineNav()).toEqual([
+ ...BASE_NAV,
+ {
+ id: 'apiLogs',
+ name: 'API Logs',
+ href: '/engines/some-engine/api_logs',
+ 'data-test-subj': 'EngineAPILogsLink',
+ },
+ ]);
+ });
+ });
+});
describe('EngineNav', () => {
const values = { ...mockEngineValues, myRole: {}, dataLoading: false };
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx
index 0edf01bada938..76e751cf4da5f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx
@@ -6,13 +6,21 @@
*/
import React from 'react';
+import { useRouteMatch } from 'react-router-dom';
import { useValues } from 'kea';
-import { EuiText, EuiBadge, EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import {
+ EuiSideNavItemType,
+ EuiText,
+ EuiBadge,
+ EuiIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { SideNavLink, SideNavItem } from '../../../shared/layout';
+import { generateNavLink, SideNavLink, SideNavItem } from '../../../shared/layout';
import { AppLogic } from '../../app_logic';
import {
ENGINE_PATH,
@@ -47,6 +55,255 @@ import { EngineLogic, generateEnginePath } from './';
import './engine_nav.scss';
+export const useEngineNav = () => {
+ const isEngineRoute = !!useRouteMatch(ENGINE_PATH);
+ const {
+ myRole: {
+ canViewEngineAnalytics,
+ canViewEngineDocuments,
+ canViewEngineSchema,
+ canViewEngineCrawler,
+ canViewMetaEngineSourceEngines,
+ canManageEngineSynonyms,
+ canManageEngineCurations,
+ canManageEngineRelevanceTuning,
+ canManageEngineResultSettings,
+ canManageEngineSearchUi,
+ canViewEngineApiLogs,
+ },
+ } = useValues(AppLogic);
+ const {
+ engineName,
+ dataLoading,
+ isSampleEngine,
+ isMetaEngine,
+ hasSchemaErrors,
+ hasSchemaConflicts,
+ hasUnconfirmedSchemaFields,
+ engine,
+ } = useValues(EngineLogic);
+
+ if (!isEngineRoute) return undefined;
+ if (dataLoading) return undefined;
+ if (!engineName) return undefined;
+
+ const navItems: Array> = [
+ {
+ id: 'engineName',
+ name: engineName,
+ renderItem: () => (
+
+ {engineName.toUpperCase()}
+ {isSampleEngine && (
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.sampleEngineBadge', {
+ defaultMessage: 'SAMPLE ENGINE',
+ })}
+
+ )}
+ {isMetaEngine && (
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.metaEngineBadge', {
+ defaultMessage: 'META ENGINE',
+ })}
+
+ )}
+
+ ),
+ 'data-test-subj': 'EngineLabel',
+ },
+ {
+ id: 'overview',
+ name: OVERVIEW_TITLE,
+ ...generateNavLink({ to: generateEnginePath(ENGINE_PATH) }),
+ 'data-test-subj': 'EngineOverviewLink',
+ },
+ ];
+
+ if (canViewEngineAnalytics) {
+ navItems.push({
+ id: 'analytics',
+ name: ANALYTICS_TITLE,
+ ...generateNavLink({
+ to: generateEnginePath(ENGINE_ANALYTICS_PATH),
+ shouldShowActiveForSubroutes: true,
+ }),
+ 'data-test-subj': 'EngineAnalyticsLink',
+ });
+ }
+
+ if (canViewEngineDocuments) {
+ navItems.push({
+ id: 'documents',
+ name: DOCUMENTS_TITLE,
+ ...generateNavLink({
+ to: generateEnginePath(ENGINE_DOCUMENTS_PATH),
+ shouldShowActiveForSubroutes: true,
+ }),
+ 'data-test-subj': 'EngineDocumentsLink',
+ });
+ }
+
+ if (canViewEngineSchema) {
+ navItems.push({
+ id: 'schema',
+ name: SCHEMA_TITLE,
+ ...generateNavLink({
+ to: generateEnginePath(ENGINE_SCHEMA_PATH),
+ shouldShowActiveForSubroutes: true,
+ }),
+ 'data-test-subj': 'EngineSchemaLink',
+ icon: (
+ <>
+ {hasSchemaErrors && (
+
+ )}
+ {hasUnconfirmedSchemaFields && (
+
+ )}
+ {hasSchemaConflicts && (
+
+ )}
+ >
+ ),
+ });
+ }
+
+ if (canViewEngineCrawler && !isMetaEngine) {
+ navItems.push({
+ id: 'crawler',
+ name: CRAWLER_TITLE,
+ ...generateNavLink({ to: generateEnginePath(ENGINE_CRAWLER_PATH) }),
+ 'data-test-subj': 'EngineCrawlerLink',
+ });
+ }
+
+ if (canViewMetaEngineSourceEngines && isMetaEngine) {
+ navItems.push({
+ id: 'sourceEngines',
+ name: ENGINES_TITLE,
+ ...generateNavLink({ to: generateEnginePath(META_ENGINE_SOURCE_ENGINES_PATH) }),
+ 'data-test-subj': 'MetaEngineEnginesLink',
+ });
+ }
+
+ if (canManageEngineRelevanceTuning) {
+ const { invalidBoosts, unsearchedUnconfirmedFields } = engine;
+
+ navItems.push({
+ id: 'relevanceTuning',
+ name: RELEVANCE_TUNING_TITLE,
+ ...generateNavLink({ to: generateEnginePath(ENGINE_RELEVANCE_TUNING_PATH) }),
+ 'data-test-subj': 'EngineRelevanceTuningLink',
+ icon: (
+ <>
+ {invalidBoosts && (
+
+ )}
+ {unsearchedUnconfirmedFields && (
+
+ )}
+ >
+ ),
+ });
+ }
+
+ if (canManageEngineSynonyms) {
+ navItems.push({
+ id: 'synonyms',
+ name: SYNONYMS_TITLE,
+ ...generateNavLink({ to: generateEnginePath(ENGINE_SYNONYMS_PATH) }),
+ 'data-test-subj': 'EngineSynonymsLink',
+ });
+ }
+
+ if (canManageEngineCurations) {
+ navItems.push({
+ id: 'curations',
+ name: CURATIONS_TITLE,
+ ...generateNavLink({
+ to: generateEnginePath(ENGINE_CURATIONS_PATH),
+ shouldShowActiveForSubroutes: true,
+ }),
+ 'data-test-subj': 'EngineCurationsLink',
+ });
+ }
+
+ if (canManageEngineResultSettings) {
+ navItems.push({
+ id: 'resultSettings',
+ name: RESULT_SETTINGS_TITLE,
+ ...generateNavLink({ to: generateEnginePath(ENGINE_RESULT_SETTINGS_PATH) }),
+ 'data-test-subj': 'EngineResultSettingsLink',
+ });
+ }
+
+ if (canManageEngineSearchUi) {
+ navItems.push({
+ id: 'searchUI',
+ name: SEARCH_UI_TITLE,
+ ...generateNavLink({ to: generateEnginePath(ENGINE_SEARCH_UI_PATH) }),
+ 'data-test-subj': 'EngineSearchUILink',
+ });
+ }
+
+ if (canViewEngineApiLogs) {
+ navItems.push({
+ id: 'apiLogs',
+ name: API_LOGS_TITLE,
+ ...generateNavLink({ to: generateEnginePath(ENGINE_API_LOGS_PATH) }),
+ 'data-test-subj': 'EngineAPILogsLink',
+ });
+ }
+
+ return navItems;
+};
+
+// TODO: Delete the below once page template migration is complete
+
export const EngineNav: React.FC = () => {
const {
myRole: {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx
index b74c31adca438..ee1c0578debfc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx
@@ -19,7 +19,6 @@ import { Switch, Redirect } from 'react-router-dom';
import { shallow } from 'enzyme';
-import { Loading } from '../../../shared/loading';
import { AnalyticsRouter } from '../analytics';
import { ApiLogs } from '../api_logs';
import { CrawlerRouter } from '../crawler';
@@ -80,20 +79,20 @@ describe('EngineRouter', () => {
);
});
- it('renders a loading component if async data is still loading', () => {
+ it('renders a loading page template if async data is still loading', () => {
setMockValues({ ...values, dataLoading: true });
const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(wrapper.prop('isLoading')).toEqual(true);
});
// This would happen if a user jumps around from one engine route to another. If the engine name
// on the path has changed, but we still have an engine stored in state, we do not want to load
// any route views as they would be rendering with the wrong data.
- it('renders a loading component if the engine stored in state is stale', () => {
+ it('renders a loading page template if the engine stored in state is stale', () => {
setMockValues({ ...values, engineName: 'some-engine' });
mockUseParams.mockReturnValue({ engineName: 'some-new-engine' });
const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(wrapper.prop('isLoading')).toEqual(true);
});
it('renders a default engine overview', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
index 40cc2ef0368c0..6510e99a000fc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
@@ -13,11 +13,12 @@ import { useValues, useActions } from 'kea';
import { i18n } from '@kbn/i18n';
import { setQueuedErrorMessage } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { Loading } from '../../../shared/loading';
+import { Layout } from '../../../shared/layout';
import { AppLogic } from '../../app_logic';
+import { AppSearchNav } from '../../index';
import {
+ ENGINE_PATH,
ENGINES_PATH,
ENGINE_ANALYTICS_PATH,
ENGINE_DOCUMENTS_PATH,
@@ -38,6 +39,7 @@ import { CrawlerRouter } from '../crawler';
import { CurationsRouter } from '../curations';
import { DocumentDetail, Documents } from '../documents';
import { EngineOverview } from '../engine_overview';
+import { AppSearchPageTemplate } from '../layout';
import { RelevanceTuning } from '../relevance_tuning';
import { ResultSettings } from '../result_settings';
import { SchemaRouter } from '../schema';
@@ -45,7 +47,7 @@ import { SearchUI } from '../search_ui';
import { SourceEngines } from '../source_engines';
import { Synonyms } from '../synonyms';
-import { EngineLogic, getEngineBreadcrumbs } from './';
+import { EngineLogic } from './';
export const EngineRouter: React.FC = () => {
const {
@@ -85,74 +87,76 @@ export const EngineRouter: React.FC = () => {
}
const isLoadingNewEngine = engineName !== engineNameFromUrl;
- if (isLoadingNewEngine || dataLoading) return ;
+ if (isLoadingNewEngine || dataLoading) return ;
return (
- {canViewEngineAnalytics && (
-
-
-
- )}
- {canViewEngineDocuments && (
-
-
-
- )}
- {canViewEngineDocuments && (
-
-
-
- )}
- {canViewEngineSchema && (
-
-
-
- )}
- {canManageEngineCurations && (
-
-
-
- )}
- {canManageEngineRelevanceTuning && (
-
-
-
- )}
- {canManageEngineSynonyms && (
-
-
-
- )}
- {canManageEngineResultSettings && (
-
-
-
- )}
- {canViewEngineApiLogs && (
-
-
-
- )}
- {canManageEngineSearchUi && (
-
-
-
- )}
- {canViewMetaEngineSourceEngines && (
-
-
-
- )}
- {canViewEngineCrawler && (
-
-
-
- )}
-
-
+
+ {/* TODO: Remove layout once page template migration is over */}
+ }>
+ {canViewEngineAnalytics && (
+
+
+
+ )}
+ {canViewEngineDocuments && (
+
+
+
+ )}
+ {canViewEngineDocuments && (
+
+
+
+ )}
+ {canViewEngineSchema && (
+
+
+
+ )}
+ {canManageEngineCurations && (
+
+
+
+ )}
+ {canManageEngineRelevanceTuning && (
+
+
+
+ )}
+ {canManageEngineSynonyms && (
+
+
+
+ )}
+ {canManageEngineResultSettings && (
+
+
+
+ )}
+ {canViewEngineApiLogs && (
+
+
+
+ )}
+ {canManageEngineSearchUi && (
+
+
+
+ )}
+ {canViewMetaEngineSourceEngines && (
+
+
+
+ )}
+ {canViewEngineCrawler && (
+
+
+
+ )}
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
index a3b2f4cfd8b9f..edacd74e046a2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
@@ -12,8 +12,6 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { Loading } from '../../../shared/loading';
-
import { EmptyEngineOverview } from './engine_overview_empty';
import { EngineOverviewMetrics } from './engine_overview_metrics';
@@ -46,10 +44,10 @@ describe('EngineOverview', () => {
expect(actions.pollForOverviewMetrics).toHaveBeenCalledTimes(1);
});
- it('renders a loading component if async data is still loading', () => {
+ it('renders a loading page template if async data is still loading', () => {
setMockValues({ ...values, dataLoading: true });
const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(wrapper.prop('isLoading')).toEqual(true);
});
describe('EmptyEngineOverview', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
index 77552b36af239..4c15ffd8b7f94 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
@@ -9,9 +9,9 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
-import { Loading } from '../../../shared/loading';
import { AppLogic } from '../../app_logic';
import { EngineLogic } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import { EmptyEngineOverview } from './engine_overview_empty';
@@ -32,9 +32,7 @@ export const EngineOverview: React.FC = () => {
pollForOverviewMetrics();
}, []);
- if (dataLoading) {
- return ;
- }
+ if (dataLoading) return ;
const engineHasDocuments = documentCount > 0;
const canAddDocuments = canManageEngineDocuments && canViewEngineCredentials;
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx
index ea47dc8956ddd..6750ebf1140e0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx
@@ -5,13 +5,16 @@
* 2.0.
*/
+import '../../__mocks__/engine_logic.mock';
+
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
-import { EuiPageHeader, EuiButton } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
import { docLinks } from '../../../shared/doc_links';
+import { getPageTitle, getPageHeaderActions } from '../../../test_helpers';
import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation';
@@ -25,12 +28,13 @@ describe('EmptyEngineOverview', () => {
});
it('renders', () => {
- expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Engine setup');
+ expect(getPageTitle(wrapper)).toEqual('Engine setup');
});
it('renders a documentation link', () => {
- const header = wrapper.find(EuiPageHeader).dive().children().dive();
- expect(header.find(EuiButton).prop('href')).toEqual(`${docLinks.appSearchBase}/index.html`);
+ expect(getPageHeaderActions(wrapper).find(EuiButton).prop('href')).toEqual(
+ `${docLinks.appSearchBase}/index.html`
+ );
});
it('renders document creation components', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx
index 959d544a67324..27d9c3723f126 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx
@@ -7,35 +7,36 @@
import React from 'react';
-import { EuiPageHeader, EuiPageContentBody, EuiButton } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../shared/flash_messages';
import { DOCS_PREFIX } from '../../routes';
import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation';
+import { getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
+
export const EmptyEngineOverview: React.FC = () => {
return (
- <>
-
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.overview.empty.headingAction',
{ defaultMessage: 'View documentation' }
)}
,
- ]}
- />
-
-
-
-
-
- >
+ ],
+ }}
+ >
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx
index 00ac2af219bff..620d913c5f9a7 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx
@@ -5,11 +5,13 @@
* 2.0.
*/
+import '../../__mocks__/engine_logic.mock';
+
import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPageHeader } from '@elastic/eui';
+import { getPageTitle } from '../../../test_helpers';
import { TotalStats, TotalCharts, RecentApiLogs } from './components';
import { EngineOverviewMetrics } from './engine_overview_metrics';
@@ -18,7 +20,7 @@ describe('EngineOverviewMetrics', () => {
it('renders', () => {
const wrapper = shallow( );
- expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Engine overview');
+ expect(getPageTitle(wrapper)).toEqual('Engine overview');
expect(wrapper.find(TotalStats)).toHaveLength(1);
expect(wrapper.find(TotalCharts)).toHaveLength(1);
expect(wrapper.find(RecentApiLogs)).toHaveLength(1);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx
index 2b01cfae49a20..b47ae21104ae9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx
@@ -7,23 +7,24 @@
import React from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiPageHeader, EuiSpacer } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../shared/flash_messages';
+import { getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import { TotalStats, TotalCharts, RecentApiLogs } from './components';
export const EngineOverviewMetrics: React.FC = () => {
return (
- <>
-
-
-
+ }),
+ }}
+ >
@@ -34,6 +35,6 @@ export const EngineOverviewMetrics: React.FC = () => {
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx
index 8b06f4b26835d..80230394ce2a2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx
@@ -10,6 +10,9 @@ import { setMockValues } from '../../../__mocks__/kea_logic';
jest.mock('../../../shared/layout', () => ({
generateNavLink: jest.fn(({ to }) => ({ href: to })),
}));
+jest.mock('../engine/engine_nav', () => ({
+ useEngineNav: () => [],
+}));
import { useAppSearchNav } from './nav';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx
index 57fa740caebec..4737fbcf07e23 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx
@@ -15,6 +15,7 @@ import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants';
import { AppLogic } from '../../app_logic';
import { ENGINES_PATH, SETTINGS_PATH, CREDENTIALS_PATH, ROLE_MAPPINGS_PATH } from '../../routes';
import { CREDENTIALS_TITLE } from '../credentials';
+import { useEngineNav } from '../engine/engine_nav';
import { ENGINES_TITLE } from '../engines';
import { SETTINGS_TITLE } from '../settings';
@@ -28,7 +29,7 @@ export const useAppSearchNav = () => {
id: 'engines',
name: ENGINES_TITLE,
...generateNavLink({ to: ENGINES_PATH, isRoot: true }),
- items: [], // TODO: Engine nav
+ items: useEngineNav(),
},
];
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
index b2cd3d7b54a1a..d724371cf1dc6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
@@ -98,6 +98,9 @@ export const AppSearchConfigured: React.FC> = (props) =
+
+
+
{canManageEngines && (
@@ -116,9 +119,6 @@ export const AppSearchConfigured: React.FC> = (props) =
} readOnlyMode={readOnlyMode}>
-
-
-
From 341413abf3911cdddee1e4773defb29609b8ee73 Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Mon, 21 Jun 2021 21:02:29 +0200
Subject: [PATCH 026/163] [home] Fix `add sample data` page layout (#102647)
* Fix add sample data layout
* remove useless change
* use KibanaPageTemplate
* use pageHeader for the header
* move tabs inside header
* tweaks
* fix imports
---
.../components/tutorial_directory.js | 92 +++++++------------
1 file changed, 33 insertions(+), 59 deletions(-)
diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js
index 1fda865ebd847..d7e6c07d6dd18 100644
--- a/src/plugins/home/public/application/components/tutorial_directory.js
+++ b/src/plugins/home/public/application/components/tutorial_directory.js
@@ -9,27 +9,15 @@
import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
+import { EuiFlexItem, EuiFlexGrid, EuiFlexGroup, EuiSpacer } from '@elastic/eui';
+import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
import { Synopsis } from './synopsis';
import { SampleDataSetCards } from './sample_data_set_cards';
import { getServices } from '../kibana_services';
-
-import {
- EuiPage,
- EuiTabs,
- EuiTab,
- EuiFlexItem,
- EuiFlexGrid,
- EuiFlexGroup,
- EuiSpacer,
- EuiTitle,
- EuiPageBody,
-} from '@elastic/eui';
-
+import { KibanaPageTemplate } from '../../../../kibana_react/public';
import { getTutorials } from '../load_tutorials';
-import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
-
const ALL_TAB_ID = 'all';
const SAMPLE_DATA_TAB_ID = 'sampleData';
@@ -184,17 +172,13 @@ class TutorialDirectoryUi extends React.Component {
});
};
- renderTabs = () => {
- return this.tabs.map((tab, index) => (
- this.onSelectedTabChanged(tab.id)}
- isSelected={tab.id === this.state.selectedTabId}
- key={index}
- >
- {tab.name}
-
- ));
+ getTabs = () => {
+ return this.tabs.map((tab) => ({
+ label: tab.name,
+ onClick: () => this.onSelectedTabChanged(tab.id),
+ isSelected: tab.id === this.state.selectedTabId,
+ 'data-test-subj': `homeTab-${tab.id}`,
+ }));
};
renderTabContent = () => {
@@ -258,41 +242,31 @@ class TutorialDirectoryUi extends React.Component {
) : null;
};
- renderHeader = () => {
- const notices = this.renderNotices();
+ render() {
const headerLinks = this.renderHeaderLinks();
+ const tabs = this.getTabs();
+ const notices = this.renderNotices();
return (
- <>
-
-
-
-
-
-
-
-
- {headerLinks ? {headerLinks} : null}
-
- {notices}
- >
- );
- };
-
- render() {
- return (
-
-
- {this.renderHeader()}
-
- {this.renderTabs()}
-
- {this.renderTabContent()}
-
-
+
+ ),
+ tabs,
+ rightSideItems: headerLinks ? [headerLinks] : [],
+ }}
+ >
+ {notices && (
+ <>
+ {notices}
+
+ >
+ )}
+ {this.renderTabContent()}
+
);
}
}
From fd4d4eec5c53970552ad5203e2ccee6fd7a006e5 Mon Sep 17 00:00:00 2001
From: Wylie Conlon
Date: Mon, 21 Jun 2021 15:02:53 -0400
Subject: [PATCH 027/163] Recommendation for known Vega-Lite issue (#102547)
---
docs/user/dashboard/vega-reference.asciidoc | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc
index cc384ec041a9d..6829e129cd3b6 100644
--- a/docs/user/dashboard/vega-reference.asciidoc
+++ b/docs/user/dashboard/vega-reference.asciidoc
@@ -50,6 +50,11 @@ To learn more, read about
https://vega.github.io/vega/docs/specification/#autosize[autosize]
in the Vega documentation.
+WARNING: Autosize in Vega-Lite has https://vega.github.io/vega-lite/docs/size.html#limitations[several limitations]
+that can result in a warning like `Autosize "fit" only works for single views and layered views.`
+The recommended fix for this warning is to convert your spec to Vega using the <>
+`VEGA_DEBUG.vega_spec` output.
+
[float]
[[vega-theme]]
====== Default theme to match {kib}
From 1d8974469728f084a8413ab9acfbd9b415657242 Mon Sep 17 00:00:00 2001
From: Bhavya RM
Date: Mon, 21 Jun 2021 15:04:03 -0400
Subject: [PATCH 028/163] Test for role specific access to non-default space
(#101653)
---
.../feature_controls/spaces_security.ts | 63 ++++++++++++++++++-
1 file changed, 61 insertions(+), 2 deletions(-)
diff --git a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts
index a91166810b626..78207c49c9b75 100644
--- a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts
+++ b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts
@@ -11,16 +11,23 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const security = getService('security');
- const PageObjects = getPageObjects(['common', 'settings', 'security']);
+ const PageObjects = getPageObjects(['common', 'settings', 'security', 'spaceSelector']);
const appsMenu = getService('appsMenu');
+ const spaces = getService('spaces');
const testSubjects = getService('testSubjects');
describe('security feature controls', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana');
+ await spaces.create({
+ id: 'nondefaultspace',
+ name: 'Non-default Space',
+ disabledFeatures: [],
+ });
});
after(async () => {
+ await spaces.delete('nondefaultspace');
await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana');
});
@@ -44,8 +51,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.security.forceLogout();
await PageObjects.security.login('global_all_user', 'global_all_user-password', {
- expectSpaceSelector: false,
+ expectSpaceSelector: true,
});
+ await testSubjects.click('space-card-default');
});
after(async () => {
@@ -174,5 +182,56 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await testSubjects.existOrFail('managementHome');
});
});
+
+ // these tests are testing role specific privilege with non default space
+ describe('Non default space and role specific privilege', () => {
+ before(async () => {
+ await security.role.create('nondefault_space_specific_role', {
+ kibana: [
+ {
+ base: ['all'],
+ spaces: ['nondefaultspace'],
+ },
+ ],
+ });
+
+ await security.user.create('nondefault_space_specific_user', {
+ password: 'nondefault_space_specific_role-password',
+ roles: ['nondefault_space_specific_role'],
+ full_name: 'nondefaultspace_specific_user',
+ });
+
+ await PageObjects.security.forceLogout();
+
+ await PageObjects.security.login(
+ 'nondefault_space_specific_user',
+ 'nondefault_space_specific_role-password',
+ {
+ expectSpaceSelector: false,
+ }
+ );
+ });
+
+ after(async () => {
+ await Promise.all([
+ security.role.delete('nondefault_space_specific_role'),
+ security.user.delete('nondefault_space_specific_user'),
+ PageObjects.security.forceLogout(),
+ ]);
+ });
+
+ it('shows management navlink', async () => {
+ await PageObjects.spaceSelector.expectHomePage('nondefaultspace');
+ const navLinks = (await appsMenu.readLinks()).map((link) => link.text);
+ expect(navLinks).to.contain('Stack Management');
+ });
+
+ it(`doesn't display spaces in the management section`, async () => {
+ await PageObjects.common.navigateToApp('management', {
+ basePath: '/s/nondefaultspace',
+ });
+ await testSubjects.missingOrFail('spaces');
+ });
+ });
});
}
From 7c429f725e090ad6015371d479185ca8382ddfa1 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 21 Jun 2021 20:17:54 +0100
Subject: [PATCH 029/163] chore(NA): moving @kbn/storybook into bazel (#102731)
* chore(NA): moving @kbn/storybook into bazel
* chore(NA): add some typing changes
* chore(NA): rename typings file
* chore(NA): fix typescript configs to build storybooks
* chore(NA): fix eslint and typechecking
---
.../monorepo-packages.asciidoc | 1 +
package.json | 2 +-
packages/BUILD.bazel | 1 +
packages/kbn-storybook/BUILD.bazel | 98 +++++++++++++++++++
packages/kbn-storybook/package.json | 5 -
packages/kbn-storybook/preset.js | 1 +
packages/kbn-storybook/preset/package.json | 4 +
.../{lib => }/templates/index.ejs | 0
packages/kbn-storybook/tsconfig.json | 7 +-
.../{typings.d.ts => typings.ts} | 8 ++
packages/kbn-storybook/webpack.config.ts | 2 +-
x-pack/package.json | 1 -
yarn.lock | 2 +-
13 files changed, 120 insertions(+), 12 deletions(-)
create mode 100644 packages/kbn-storybook/BUILD.bazel
create mode 100644 packages/kbn-storybook/preset/package.json
rename packages/kbn-storybook/{lib => }/templates/index.ejs (100%)
rename packages/kbn-storybook/{typings.d.ts => typings.ts} (56%)
diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc
index c211751c09b49..ebab9de66032f 100644
--- a/docs/developer/getting-started/monorepo-packages.asciidoc
+++ b/docs/developer/getting-started/monorepo-packages.asciidoc
@@ -100,6 +100,7 @@ yarn kbn watch-bazel
- @kbn/server-http-tools
- @kbn/server-route-repository
- @kbn/std
+- @kbn/storybook
- @kbn/telemetry-utils
- @kbn/tinymath
- @kbn/ui-shared-deps
diff --git a/package.json b/package.json
index 310350baf7b2d..ae5749e351647 100644
--- a/package.json
+++ b/package.json
@@ -468,7 +468,7 @@
"@kbn/plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator",
"@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers",
"@kbn/pm": "link:packages/kbn-pm",
- "@kbn/storybook": "link:packages/kbn-storybook",
+ "@kbn/storybook": "link:bazel-bin/packages/kbn-storybook",
"@kbn/telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools",
"@kbn/test": "link:packages/kbn-test",
"@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector",
diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel
index 6208910729625..61034c562b447 100644
--- a/packages/BUILD.bazel
+++ b/packages/BUILD.bazel
@@ -44,6 +44,7 @@ filegroup(
"//packages/kbn-server-http-tools:build",
"//packages/kbn-server-route-repository:build",
"//packages/kbn-std:build",
+ "//packages/kbn-storybook:build",
"//packages/kbn-telemetry-tools:build",
"//packages/kbn-tinymath:build",
"//packages/kbn-ui-shared-deps:build",
diff --git a/packages/kbn-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel
new file mode 100644
index 0000000000000..e18256aeb8da4
--- /dev/null
+++ b/packages/kbn-storybook/BUILD.bazel
@@ -0,0 +1,98 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
+
+PKG_BASE_NAME = "kbn-storybook"
+PKG_REQUIRE_NAME = "@kbn/storybook"
+
+SOURCE_FILES = glob(
+ [
+ "lib/**/*.ts",
+ "lib/**/*.tsx",
+ "*.ts",
+ ],
+ exclude = ["**/*.test.*"],
+)
+
+SRCS = SOURCE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "preset/package.json",
+ "templates/index.ejs",
+ "package.json",
+ "README.md",
+ "preset.js",
+]
+
+SRC_DEPS = [
+ "//packages/kbn-dev-utils",
+ "//packages/kbn-ui-shared-deps",
+ "@npm//@storybook/addons",
+ "@npm//@storybook/api",
+ "@npm//@storybook/components",
+ "@npm//@storybook/core",
+ "@npm//@storybook/node-logger",
+ "@npm//@storybook/react",
+ "@npm//@storybook/theming",
+ "@npm//loader-utils",
+ "@npm//react",
+ "@npm//webpack",
+ "@npm//webpack-merge",
+]
+
+TYPES_DEPS = [
+ "@npm//@types/loader-utils",
+ "@npm//@types/node",
+ "@npm//@types/webpack",
+ "@npm//@types/webpack-merge",
+]
+
+DEPS = SRC_DEPS + TYPES_DEPS
+
+ts_config(
+ name = "tsconfig",
+ src = "tsconfig.json",
+ deps = [
+ "//:tsconfig.base.json",
+ ],
+)
+
+ts_project(
+ name = "tsc",
+ args = ['--pretty'],
+ srcs = SRCS,
+ deps = DEPS,
+ declaration = True,
+ declaration_map = True,
+ incremental = True,
+ out_dir = "target",
+ source_map = True,
+ tsconfig = ":tsconfig",
+)
+
+js_library(
+ name = PKG_BASE_NAME,
+ srcs = NPM_MODULE_EXTRA_FILES,
+ deps = [":tsc"] + DEPS,
+ package_name = PKG_REQUIRE_NAME,
+ visibility = ["//visibility:public"],
+)
+
+pkg_npm(
+ name = "npm_module",
+ deps = [
+ ":%s" % PKG_BASE_NAME,
+ ]
+)
+
+filegroup(
+ name = "build",
+ srcs = [
+ ":npm_module",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json
index f2e4c9b3418b1..f3c12f19a0793 100644
--- a/packages/kbn-storybook/package.json
+++ b/packages/kbn-storybook/package.json
@@ -7,10 +7,5 @@
"types": "./target/index.d.ts",
"kibana": {
"devOnly": true
- },
- "scripts": {
- "build": "../../node_modules/.bin/tsc",
- "kbn:bootstrap": "yarn build",
- "kbn:watch": "yarn build --watch"
}
}
\ No newline at end of file
diff --git a/packages/kbn-storybook/preset.js b/packages/kbn-storybook/preset.js
index c1b7195c141b4..be0012a3818b1 100644
--- a/packages/kbn-storybook/preset.js
+++ b/packages/kbn-storybook/preset.js
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+// eslint-disable-next-line
const webpackConfig = require('./target/webpack.config').default;
module.exports = {
diff --git a/packages/kbn-storybook/preset/package.json b/packages/kbn-storybook/preset/package.json
new file mode 100644
index 0000000000000..7cd7517d64dde
--- /dev/null
+++ b/packages/kbn-storybook/preset/package.json
@@ -0,0 +1,4 @@
+{
+ "private": true,
+ "main": "../preset.js"
+}
\ No newline at end of file
diff --git a/packages/kbn-storybook/lib/templates/index.ejs b/packages/kbn-storybook/templates/index.ejs
similarity index 100%
rename from packages/kbn-storybook/lib/templates/index.ejs
rename to packages/kbn-storybook/templates/index.ejs
diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json
index 586f5ea32c056..1f6886c45c505 100644
--- a/packages/kbn-storybook/tsconfig.json
+++ b/packages/kbn-storybook/tsconfig.json
@@ -1,14 +1,15 @@
{
- "extends": "../../tsconfig.json",
+ "extends": "../../tsconfig.base.json",
"compilerOptions": {
- "incremental": false,
+ "incremental": true,
"outDir": "target",
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"sourceRoot": "../../../../packages/kbn-storybook",
+ "target": "es2015",
"types": ["node"]
},
- "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx", "../../typings/index.d.ts"]
+ "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx"]
}
diff --git a/packages/kbn-storybook/typings.d.ts b/packages/kbn-storybook/typings.ts
similarity index 56%
rename from packages/kbn-storybook/typings.d.ts
rename to packages/kbn-storybook/typings.ts
index b940de2829909..6c5d8f4da5709 100644
--- a/packages/kbn-storybook/typings.d.ts
+++ b/packages/kbn-storybook/typings.ts
@@ -8,3 +8,11 @@
// Storybook react doesn't declare this in its typings, but it's there.
declare module '@storybook/react/standalone';
+
+// Storybook references this module. It's @ts-ignored in the codebase but when
+// built into its dist it strips that out. Add it here to avoid a type checking
+// error.
+//
+// See https://github.com/storybookjs/storybook/issues/11684
+declare module 'react-syntax-highlighter/dist/cjs/create-element';
+declare module 'react-syntax-highlighter/dist/cjs/prism-light';
diff --git a/packages/kbn-storybook/webpack.config.ts b/packages/kbn-storybook/webpack.config.ts
index 972caf8d481fe..41d3ee1f7ee5c 100644
--- a/packages/kbn-storybook/webpack.config.ts
+++ b/packages/kbn-storybook/webpack.config.ts
@@ -94,7 +94,7 @@ export default function ({ config: storybookConfig }: { config: Configuration })
return plugin.options && typeof plugin.options.template === 'string';
});
if (htmlWebpackPlugin) {
- htmlWebpackPlugin.options.template = require.resolve('../lib/templates/index.ejs');
+ htmlWebpackPlugin.options.template = require.resolve('../templates/index.ejs');
}
return webpackMerge(storybookConfig, config);
diff --git a/x-pack/package.json b/x-pack/package.json
index 84fd5ba081d8f..0d2a170d83170 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -27,7 +27,6 @@
},
"devDependencies": {
"@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers",
- "@kbn/storybook": "link:../packages/kbn-storybook",
"@kbn/test": "link:../packages/kbn-test"
},
"dependencies": {
diff --git a/yarn.lock b/yarn.lock
index c9e139e68b592..0e9f7a1c9b167 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2768,7 +2768,7 @@
version "0.0.0"
uid ""
-"@kbn/storybook@link:packages/kbn-storybook":
+"@kbn/storybook@link:bazel-bin/packages/kbn-storybook":
version "0.0.0"
uid ""
From 5ac9a381027499ea298477b07e41ba3df423e763 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 21 Jun 2021 20:31:37 +0100
Subject: [PATCH 030/163] chore(NA): remove webpack build changes for
kbn/ui-shared-deps (#102780)
---
packages/kbn-ui-shared-deps/webpack.config.js | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js
index 438b1e0b2e77b..9d18c8033ff67 100644
--- a/packages/kbn-ui-shared-deps/webpack.config.js
+++ b/packages/kbn-ui-shared-deps/webpack.config.js
@@ -7,7 +7,6 @@
*/
const Path = require('path');
-const Os = require('os');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
@@ -31,7 +30,8 @@ module.exports = {
'kbn-ui-shared-deps.v8.light': ['@elastic/eui/dist/eui_theme_amsterdam_light.css'],
},
context: __dirname,
- devtool: 'cheap-source-map',
+ // cheap-source-map should be used if needed
+ devtool: false,
output: {
path: UiSharedDeps.distDir,
filename: '[name].js',
@@ -39,7 +39,6 @@ module.exports = {
devtoolModuleFilenameTemplate: (info) =>
`kbn-ui-shared-deps/${Path.relative(REPO_ROOT, info.absoluteResourcePath)}`,
library: '__kbnSharedDeps__',
- futureEmitAssets: true,
},
module: {
@@ -111,7 +110,7 @@ module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin({
- parallel: Math.min(Os.cpus().length, 2),
+ parallel: false,
minimizerOptions: {
preset: [
'default',
@@ -125,7 +124,7 @@ module.exports = {
cache: false,
sourceMap: false,
extractComments: false,
- parallel: Math.min(Os.cpus().length, 2),
+ parallel: false,
terserOptions: {
compress: true,
mangle: true,
From ccf6039772c4d7bc7818135d58f2afb8800d5131 Mon Sep 17 00:00:00 2001
From: Mikhail Shustov
Date: Mon, 21 Jun 2021 21:49:02 +0200
Subject: [PATCH 031/163] Upgrade apm nodejs and rum agents (#102723)
* bump apm nodejs agent version to 3.16.0
* bump apm-rum agents
* use ApmConfigOptions exported from rum agent
---
package.json | 8 ++--
src/core/public/apm_system.ts | 7 ++-
yarn.lock | 82 +++++++++++++++--------------------
3 files changed, 43 insertions(+), 54 deletions(-)
diff --git a/package.json b/package.json
index ae5749e351647..29371c9532915 100644
--- a/package.json
+++ b/package.json
@@ -97,8 +97,8 @@
"yarn": "^1.21.1"
},
"dependencies": {
- "@elastic/apm-rum": "^5.6.1",
- "@elastic/apm-rum-react": "^1.2.5",
+ "@elastic/apm-rum": "^5.8.0",
+ "@elastic/apm-rum-react": "^1.2.11",
"@elastic/charts": "30.1.0",
"@elastic/datemath": "link:bazel-bin/packages/elastic-datemath",
"@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13",
@@ -224,7 +224,7 @@
"deep-freeze-strict": "^1.1.1",
"deepmerge": "^4.2.2",
"del": "^5.1.0",
- "elastic-apm-node": "^3.14.0",
+ "elastic-apm-node": "^3.16.0",
"elasticsearch": "^16.7.0",
"execa": "^4.0.2",
"exit-hook": "^2.2.0",
@@ -841,4 +841,4 @@
"yargs": "^15.4.1",
"zlib": "^1.0.5"
}
-}
\ No newline at end of file
+}
diff --git a/src/core/public/apm_system.ts b/src/core/public/apm_system.ts
index 32fc330375991..f5af7011e632e 100644
--- a/src/core/public/apm_system.ts
+++ b/src/core/public/apm_system.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import type { ApmBase } from '@elastic/apm-rum';
+import type { ApmBase, AgentConfigOptions } from '@elastic/apm-rum';
import { modifyUrl } from '@kbn/std';
import type { InternalApplicationStart } from './application';
@@ -18,9 +18,8 @@ const HTTP_REQUEST_TRANSACTION_NAME_REGEX = /^(GET|POST|PUT|HEAD|PATCH|DELETE|OP
* that lives in the Kibana Platform.
*/
-interface ApmConfig {
- // AgentConfigOptions is not exported from @elastic/apm-rum
- active?: boolean;
+interface ApmConfig extends AgentConfigOptions {
+ // Kibana-specific config settings:
globalLabels?: Record;
}
diff --git a/yarn.lock b/yarn.lock
index 0e9f7a1c9b167..cfdac6108b6cf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1328,29 +1328,29 @@
is-absolute "^1.0.0"
is-negated-glob "^1.0.0"
-"@elastic/apm-rum-core@^5.7.0":
- version "5.7.0"
- resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.7.0.tgz#2213987285324781e2ebeca607f3a71245da5a84"
- integrity sha512-YxfyDwlPDRy05ERb8h79eXq2ebDamlyII3sdc8zsfL6Hc1wOHK3uBGelDQjQzkUkRJqJL1Sy6LJqok2mpxQJyw==
+"@elastic/apm-rum-core@^5.11.0":
+ version "5.11.0"
+ resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.11.0.tgz#6cfebb62d5ac33cf5ec9dfbe206f120ff5d17ecc"
+ integrity sha512-JqxsVU6/gHfWe3DiJ7uN0h0e+zFd8LbcC5i/Pa14useiKOVn4r7dHeKoWkBSJCY63cl76hotCbtgqkuVgWVzmA==
dependencies:
error-stack-parser "^1.3.5"
opentracing "^0.14.3"
promise-polyfill "^8.1.3"
-"@elastic/apm-rum-react@^1.2.5":
- version "1.2.5"
- resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.2.5.tgz#ac715a192808e14e62e537e41b70cc8296854051"
- integrity sha512-5+5Q2ztOQT0EbWFZqV2N78tcuA9qPuO5QAtSTQIYgb5lH27Sfa9G4xlTgCbJs9DzCKmhuu27E4DTArrU3tyNzA==
+"@elastic/apm-rum-react@^1.2.11":
+ version "1.2.11"
+ resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.2.11.tgz#945436cbe90507fda85016c0e3a44984c3f0a9c8"
+ integrity sha512-kl+NdNZ0eANAD7DlN3fFR7M9NeEW21rINh9aLSmEMQedUNNn+3K9oQzD4MirjV1TA5hsLSeGiCKrfPzja9Ynjw==
dependencies:
- "@elastic/apm-rum" "^5.6.1"
+ "@elastic/apm-rum" "^5.8.0"
hoist-non-react-statics "^3.3.0"
-"@elastic/apm-rum@^5.6.1":
- version "5.6.1"
- resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.6.1.tgz#0d1bbef774866064795f7a9c6db0c951a900de35"
- integrity sha512-q6ZkDb+m2z29h6/JKqBL/nBf6/x5yYmW1vUpdW3zy03jTQp+A7LpVaPI1HNquyGryqqT/BQl4QivFcNC28pr4w==
+"@elastic/apm-rum@^5.8.0":
+ version "5.8.0"
+ resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.8.0.tgz#ab88dc9e955b7fa2f00d5541d242a91a44c0c931"
+ integrity sha512-lje3SxwqhRkogCsBUsK9y0cn1Kv3dj4Ukbt4VbmNr44KRYoY9A3gTm5e5qKLF6DgsPCOc9EZBF36a0Wtjlkt/g==
dependencies:
- "@elastic/apm-rum-core" "^5.7.0"
+ "@elastic/apm-rum-core" "^5.11.0"
"@elastic/app-search-javascript@^7.3.0":
version "7.8.0"
@@ -12159,10 +12159,10 @@ ejs@^3.1.2, ejs@^3.1.5, ejs@^3.1.6:
dependencies:
jake "^10.6.1"
-elastic-apm-http-client@^9.8.0:
- version "9.8.0"
- resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-9.8.0.tgz#caa738c2663b3ec8521ebede86cc841e4c77863c"
- integrity sha512-JrlQbijs4dY8539zH+QNKLqLDCNyNymyy720tDaj+/i5pcwWYz5ipPARAdrKkor56AmKBxib8Fd6KsSWtIYjcA==
+elastic-apm-http-client@^9.8.1:
+ version "9.8.1"
+ resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-9.8.1.tgz#62a0352849e2d7a75696a1c777ad90ddb55083b0"
+ integrity sha512-tVU7+y4nSDUEZp/TXbXDxE+kXbWHsGVG1umk0OOV71UEPc/AqC7xSP5ACirOlDkewkfCOFXkvNThgu2zlx8PUw==
dependencies:
breadth-filter "^2.0.0"
container-info "^1.0.1"
@@ -12174,24 +12174,28 @@ elastic-apm-http-client@^9.8.0:
stream-chopper "^3.0.1"
unicode-byte-truncate "^1.0.0"
-elastic-apm-node@^3.14.0:
- version "3.14.0"
- resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.14.0.tgz#942d6e86bd9d3710f51f0e43f04965d63c3fefd3"
- integrity sha512-B7Xkz6UL44mm+2URdZy2yxpEB2C5CvZLOP3sGpf2h/hepXr4NgrVoRxGqO1F2b2wCB48smPv4a3v35b396VSwA==
+elastic-apm-node@^3.16.0:
+ version "3.16.0"
+ resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.16.0.tgz#b55ba5c54acd2f40be704dc48c664ddb1729f20f"
+ integrity sha512-WR56cjpvt9ZAAw+4Ct2XjCtmy+lgn5kXZH220TRgC7W71c5uuRdioRJpIdvBPMZmeLnHwzok2+acUB7bxnYvVA==
dependencies:
"@elastic/ecs-pino-format" "^1.1.0"
after-all-results "^2.0.0"
+ async-cache "^1.1.0"
async-value-promise "^1.1.1"
basic-auth "^2.0.1"
cookie "^0.4.0"
core-util-is "^1.0.2"
- elastic-apm-http-client "^9.8.0"
+ elastic-apm-http-client "^9.8.1"
end-of-stream "^1.4.4"
+ error-callsites "^2.0.4"
error-stack-parser "^2.0.6"
escape-string-regexp "^4.0.0"
fast-safe-stringify "^2.0.7"
http-headers "^3.0.2"
is-native "^1.0.1"
+ load-source-map "^2.0.0"
+ lru-cache "^6.0.0"
measured-reporting "^1.51.1"
monitor-event-loop-delay "^1.0.0"
object-filter-sequence "^1.0.0"
@@ -12205,7 +12209,6 @@ elastic-apm-node@^3.14.0:
set-cookie-serde "^1.0.0"
shallow-clone-shim "^2.0.0"
sql-summary "^1.0.1"
- stackman "^4.0.1"
traceparent "^1.0.0"
traverse "^0.6.6"
unicode-byte-truncate "^1.0.0"
@@ -12459,10 +12462,10 @@ errno@^0.1.1, errno@^0.1.3, errno@~0.1.7:
dependencies:
prr "~1.0.1"
-error-callsites@^2.0.3:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/error-callsites/-/error-callsites-2.0.3.tgz#c9278de0d7d4b4861150af295bb92891393ff24a"
- integrity sha512-v036z4IEffZFE5kBkV5/F2MzhLnG0vuDyN+VXpzCf4yWXvX/1WJCI0A+TGTr8HWzBfCw5k8gr9rwAo09V+obTA==
+error-callsites@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/error-callsites/-/error-callsites-2.0.4.tgz#44f09e6a201e9a1603ead81eacac5ba258fca76e"
+ integrity sha512-V877Ch4FC4FN178fDK1fsrHN4I1YQIBdtjKrHh3BUHMnh3SMvwUVrqkaOgDpUuevgSNna0RBq6Ox9SGlxYrigA==
error-ex@^1.2.0, error-ex@^1.3.1:
version "1.3.1"
@@ -18328,14 +18331,12 @@ load-json-file@^6.2.0:
strip-bom "^4.0.0"
type-fest "^0.6.0"
-load-source-map@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/load-source-map/-/load-source-map-1.0.0.tgz#318f49905ce8a709dfb7cc3f16f3efe3bcf1dd05"
- integrity sha1-MY9JkFzopwnft8w/FvPv47zx3QU=
+load-source-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/load-source-map/-/load-source-map-2.0.0.tgz#48f1c7002d7d9e20dd119da6e566104ec46a5683"
+ integrity sha512-QNZzJ2wMrTmCdeobMuMNEXHN1QGk8HG6louEkzD/zwQ7EU2RarrzlhQ4GnUYEFzLhK+Jq7IGyF/qy+XYBSO7AQ==
dependencies:
- in-publish "^2.0.0"
- semver "^5.3.0"
- source-map "^0.5.6"
+ source-map "^0.7.3"
loader-runner@^2.4.0:
version "2.4.0"
@@ -25595,17 +25596,6 @@ stackframe@^1.1.0, stackframe@^1.1.1:
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.1.1.tgz#ffef0a3318b1b60c3b58564989aca5660729ec71"
integrity sha512-0PlYhdKh6AfFxRyK/v+6/k+/mMfyiEBbTM5L94D0ZytQnJ166wuwoTYLHFWGbs2dpA8Rgq763KGWmN1EQEYHRQ==
-stackman@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/stackman/-/stackman-4.0.1.tgz#b5709446f078db9b9dadbb317f296224d9a35b5b"
- integrity sha512-lntIge3BFEElgvpZT2ld5f4U+mF84fRtJ8vA3ymUVx1euVx43ZMkd09+5RWW4FmvYDFhZwPh1gvtdsdnJyF4Fg==
- dependencies:
- after-all-results "^2.0.0"
- async-cache "^1.1.0"
- debug "^4.1.1"
- error-callsites "^2.0.3"
- load-source-map "^1.0.0"
-
stacktrace-gps@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.3.tgz#b89f84cc13bb925b96607e737b617c8715facf57"
From 1fb2640a6fb8daec3e6803be850218b2600becea Mon Sep 17 00:00:00 2001
From: Vadim Dalecky
Date: Mon, 21 Jun 2021 22:11:12 +0200
Subject: [PATCH 032/163] ILM locators (#102313)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: 🎸 add url service types
* refactor: 💡 move locator types into its own folder
* feat: 🎸 add abstract locator implementation
* feat: 🎸 implement abstract locator client
* feat: 🎸 add browser-side locators service
* feat: 🎸 implement locator .getLocation()
* feat: 🎸 implement navigate function
* feat: 🎸 implement locator service in /common folder
* feat: 🎸 expose locators client on browser and server
* refactor: 💡 make locators async
* chore: 🤖 add deprecation notice to URL generators
* docs: ✏️ add deprecation notice to readme
* feat: 🎸 create management app locator
* refactor: 💡 simplify management locator
* feat: 🎸 export management app locator from plugin contract
* feat: 🎸 implement ILM locator
* feat: 🎸 improve share plugin exports
* feat: 🎸 improve management app locator
* feat: 🎸 add useLocatorUrl React hook
* feat: 🎸 add .getUrl() method to locators
* feat: 🎸 migrate ILM app to use URL locators
* fix: 🐛 correct typescript errors
* Fix TypeScript errors in mock
* Fix ILM locator unit tests
* style: 💄 shorten import
Co-authored-by: Vadim Kibana
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
src/plugins/management/common/index.ts | 9 +++
src/plugins/management/common/locator.test.ts | 14 ++---
src/plugins/management/common/locator.ts | 11 ++--
src/plugins/management/public/mocks/index.ts | 4 +-
src/plugins/management/public/plugin.ts | 4 +-
src/plugins/management/server/plugin.ts | 4 +-
src/plugins/share/common/index.ts | 2 +-
.../url_service/__tests__/locators.test.ts | 8 +--
.../common/url_service/__tests__/setup.ts | 5 +-
.../common/url_service/locators/index.ts | 1 +
.../common/url_service/locators/locator.ts | 27 ++++++++
.../common/url_service/locators/types.ts | 43 +++++++++++--
.../url_service/locators/use_locator_url.ts | 46 ++++++++++++++
.../share/common/url_service/url_service.ts | 6 +-
src/plugins/share/public/index.ts | 2 +
src/plugins/share/public/plugin.ts | 16 +++--
src/plugins/share/server/plugin.ts | 5 +-
.../public/index.ts | 2 +-
.../public/locator.ts | 61 +++++++++++++++++++
.../public/plugin.tsx | 10 ++-
.../public/url_generator.ts | 61 -------------------
.../home/data_streams_tab.test.ts | 50 ++++++++++-----
.../public/application/app_context.tsx | 2 +-
.../{ilm_url_generator.ts => ilm_locator.ts} | 2 +-
.../public/application/constants/index.ts | 2 +-
.../application/mount_management_section.ts | 4 +-
.../data_stream_detail_panel.tsx | 12 +---
.../template_details/tabs/tab_summary.tsx | 12 +---
.../application/services/use_ilm_locator.ts | 21 +++++++
.../application/services/use_url_generator.ts | 40 ------------
30 files changed, 311 insertions(+), 175 deletions(-)
create mode 100644 src/plugins/management/common/index.ts
create mode 100644 src/plugins/share/common/url_service/locators/use_locator_url.ts
create mode 100644 x-pack/plugins/index_lifecycle_management/public/locator.ts
delete mode 100644 x-pack/plugins/index_lifecycle_management/public/url_generator.ts
rename x-pack/plugins/index_management/public/application/constants/{ilm_url_generator.ts => ilm_locator.ts} (83%)
create mode 100644 x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts
delete mode 100644 x-pack/plugins/index_management/public/application/services/use_url_generator.ts
diff --git a/src/plugins/management/common/index.ts b/src/plugins/management/common/index.ts
new file mode 100644
index 0000000000000..c701ba846bcac
--- /dev/null
+++ b/src/plugins/management/common/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { ManagementAppLocator } from './locator';
diff --git a/src/plugins/management/common/locator.test.ts b/src/plugins/management/common/locator.test.ts
index dda393a4203ec..20773b9732782 100644
--- a/src/plugins/management/common/locator.test.ts
+++ b/src/plugins/management/common/locator.test.ts
@@ -7,16 +7,16 @@
*/
import { MANAGEMENT_APP_ID } from './contants';
-import { ManagementAppLocator, MANAGEMENT_APP_LOCATOR } from './locator';
+import { ManagementAppLocatorDefinition, MANAGEMENT_APP_LOCATOR } from './locator';
test('locator has the right ID', () => {
- const locator = new ManagementAppLocator();
+ const locator = new ManagementAppLocatorDefinition();
expect(locator.id).toBe(MANAGEMENT_APP_LOCATOR);
});
test('returns management app ID', async () => {
- const locator = new ManagementAppLocator();
+ const locator = new ManagementAppLocatorDefinition();
const location = await locator.getLocation({
sectionId: 'a',
appId: 'b',
@@ -28,26 +28,26 @@ test('returns management app ID', async () => {
});
test('returns Kibana location for section ID and app ID pair', async () => {
- const locator = new ManagementAppLocator();
+ const locator = new ManagementAppLocatorDefinition();
const location = await locator.getLocation({
sectionId: 'ingest',
appId: 'index',
});
expect(location).toMatchObject({
- route: '/ingest/index',
+ path: '/ingest/index',
state: {},
});
});
test('when app ID is not provided, returns path to just the section ID', async () => {
- const locator = new ManagementAppLocator();
+ const locator = new ManagementAppLocatorDefinition();
const location = await locator.getLocation({
sectionId: 'data',
});
expect(location).toMatchObject({
- route: '/data',
+ path: '/data',
state: {},
});
});
diff --git a/src/plugins/management/common/locator.ts b/src/plugins/management/common/locator.ts
index 4a4a50f468adc..7dbf5e2888011 100644
--- a/src/plugins/management/common/locator.ts
+++ b/src/plugins/management/common/locator.ts
@@ -7,7 +7,7 @@
*/
import { SerializableState } from 'src/plugins/kibana_utils/common';
-import { LocatorDefinition } from 'src/plugins/share/common';
+import { LocatorDefinition, LocatorPublic } from 'src/plugins/share/common';
import { MANAGEMENT_APP_ID } from './contants';
export const MANAGEMENT_APP_LOCATOR = 'MANAGEMENT_APP_LOCATOR';
@@ -17,15 +17,18 @@ export interface ManagementAppLocatorParams extends SerializableState {
appId?: string;
}
-export class ManagementAppLocator implements LocatorDefinition {
+export type ManagementAppLocator = LocatorPublic;
+
+export class ManagementAppLocatorDefinition
+ implements LocatorDefinition {
public readonly id = MANAGEMENT_APP_LOCATOR;
public readonly getLocation = async (params: ManagementAppLocatorParams) => {
- const route = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`;
+ const path = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`;
return {
app: MANAGEMENT_APP_ID,
- route,
+ path,
state: {},
};
};
diff --git a/src/plugins/management/public/mocks/index.ts b/src/plugins/management/public/mocks/index.ts
index 70d853f32dfcc..b06e41502e9df 100644
--- a/src/plugins/management/public/mocks/index.ts
+++ b/src/plugins/management/public/mocks/index.ts
@@ -33,9 +33,11 @@ const createSetupContract = (): ManagementSetup => ({
locator: {
getLocation: jest.fn(async () => ({
app: 'MANAGEMENT',
- route: '',
+ path: '',
state: {},
})),
+ getUrl: jest.fn(),
+ useUrl: jest.fn(),
navigate: jest.fn(),
},
});
diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts
index 3289b2f6f5446..34719fb5070e1 100644
--- a/src/plugins/management/public/plugin.ts
+++ b/src/plugins/management/public/plugin.ts
@@ -25,7 +25,7 @@ import {
} from '../../../core/public';
import { MANAGEMENT_APP_ID } from '../common/contants';
-import { ManagementAppLocator } from '../common/locator';
+import { ManagementAppLocatorDefinition } from '../common/locator';
import {
ManagementSectionsService,
getSectionsServiceStartPrivate,
@@ -74,7 +74,7 @@ export class ManagementPlugin
public setup(core: CoreSetup, { home, share }: ManagementSetupDependencies) {
const kibanaVersion = this.initializerContext.env.packageInfo.version;
- const locator = share.url.locators.create(new ManagementAppLocator());
+ const locator = share.url.locators.create(new ManagementAppLocatorDefinition());
if (home) {
home.featureCatalogue.register({
diff --git a/src/plugins/management/server/plugin.ts b/src/plugins/management/server/plugin.ts
index 349cab6206bab..cc3798d855c59 100644
--- a/src/plugins/management/server/plugin.ts
+++ b/src/plugins/management/server/plugin.ts
@@ -9,7 +9,7 @@
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server';
import { LocatorPublic } from 'src/plugins/share/common';
import type { SharePluginSetup } from 'src/plugins/share/server';
-import { ManagementAppLocator, ManagementAppLocatorParams } from '../common/locator';
+import { ManagementAppLocatorDefinition, ManagementAppLocatorParams } from '../common/locator';
import { capabilitiesProvider } from './capabilities_provider';
interface ManagementSetupDependencies {
@@ -31,7 +31,7 @@ export class ManagementServerPlugin
public setup(core: CoreSetup, { share }: ManagementSetupDependencies) {
this.logger.debug('management: Setup');
- const locator = share.url.locators.create(new ManagementAppLocator());
+ const locator = share.url.locators.create(new ManagementAppLocatorDefinition());
core.capabilities.registerProvider(capabilitiesProvider);
diff --git a/src/plugins/share/common/index.ts b/src/plugins/share/common/index.ts
index 8b5d8d4557194..e724117f5b7f7 100644
--- a/src/plugins/share/common/index.ts
+++ b/src/plugins/share/common/index.ts
@@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
-export { LocatorDefinition, LocatorPublic } from './url_service';
+export { LocatorDefinition, LocatorPublic, useLocatorUrl } from './url_service';
diff --git a/src/plugins/share/common/url_service/__tests__/locators.test.ts b/src/plugins/share/common/url_service/__tests__/locators.test.ts
index 45d727df7de48..93ba76c7399f4 100644
--- a/src/plugins/share/common/url_service/__tests__/locators.test.ts
+++ b/src/plugins/share/common/url_service/__tests__/locators.test.ts
@@ -53,7 +53,7 @@ describe('locators', () => {
expect(location).toEqual({
app: 'test_app',
- route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21',
+ path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21',
state: { isFlyoutOpen: true },
});
});
@@ -97,7 +97,7 @@ describe('locators', () => {
expect(deps.navigate).toHaveBeenCalledWith(
{
app: 'test_app',
- route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1',
+ path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1',
state: {
isFlyoutOpen: false,
},
@@ -130,7 +130,7 @@ describe('locators', () => {
expect(deps.navigate).toHaveBeenCalledWith(
{
app: 'test_app',
- route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1',
+ path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1',
state: {
isFlyoutOpen: false,
},
@@ -153,7 +153,7 @@ describe('locators', () => {
expect(deps.navigate).toHaveBeenCalledWith(
{
app: 'test_app',
- route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2',
+ path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2',
state: {
isFlyoutOpen: false,
},
diff --git a/src/plugins/share/common/url_service/__tests__/setup.ts b/src/plugins/share/common/url_service/__tests__/setup.ts
index ad13bb8d8d216..fea3e1b945f99 100644
--- a/src/plugins/share/common/url_service/__tests__/setup.ts
+++ b/src/plugins/share/common/url_service/__tests__/setup.ts
@@ -21,7 +21,7 @@ export const testLocator: LocatorDefinition = {
getLocation: async ({ savedObjectId, pageNumber, showFlyout }) => {
return {
app: 'test_app',
- route: `/my-object/${savedObjectId}?page=${pageNumber}`,
+ path: `/my-object/${savedObjectId}?page=${pageNumber}`,
state: {
isFlyoutOpen: showFlyout,
},
@@ -34,6 +34,9 @@ export const urlServiceTestSetup = (partialDeps: Partial
navigate: async () => {
throw new Error('not implemented');
},
+ getUrl: async () => {
+ throw new Error('not implemented');
+ },
...partialDeps,
};
const service = new UrlService(deps);
diff --git a/src/plugins/share/common/url_service/locators/index.ts b/src/plugins/share/common/url_service/locators/index.ts
index f9f87215eb4db..7ab3938984f23 100644
--- a/src/plugins/share/common/url_service/locators/index.ts
+++ b/src/plugins/share/common/url_service/locators/index.ts
@@ -9,3 +9,4 @@
export * from './types';
export * from './locator';
export * from './locator_client';
+export { useLocatorUrl } from './use_locator_url';
diff --git a/src/plugins/share/common/url_service/locators/locator.ts b/src/plugins/share/common/url_service/locators/locator.ts
index 68c3b05a7f411..680fb2231fc48 100644
--- a/src/plugins/share/common/url_service/locators/locator.ts
+++ b/src/plugins/share/common/url_service/locators/locator.ts
@@ -7,16 +7,27 @@
*/
import type { SavedObjectReference } from 'kibana/server';
+import { DependencyList } from 'react';
import type { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common';
+import { useLocatorUrl } from './use_locator_url';
import type {
LocatorDefinition,
LocatorPublic,
KibanaLocation,
LocatorNavigationParams,
+ LocatorGetUrlParams,
} from './types';
export interface LocatorDependencies {
+ /**
+ * Navigate without reloading the page to a KibanaLocation.
+ */
navigate: (location: KibanaLocation, params?: LocatorNavigationParams) => Promise;
+
+ /**
+ * Resolve a Kibana URL given KibanaLocation.
+ */
+ getUrl: (location: KibanaLocation, getUrlParams: LocatorGetUrlParams) => Promise;
}
export class Locator implements PersistableState
, LocatorPublic
{
@@ -57,13 +68,29 @@ export class Locator
implements PersistableState
return await this.definition.getLocation(params);
}
+ public async getUrl(params: P, { absolute = false }: LocatorGetUrlParams = {}): Promise {
+ const location = await this.getLocation(params);
+ const url = this.deps.getUrl(location, { absolute });
+
+ return url;
+ }
+
public async navigate(
params: P,
{ replace = false }: LocatorNavigationParams = {}
): Promise {
const location = await this.getLocation(params);
+
await this.deps.navigate(location, {
replace,
});
}
+
+ /* eslint-disable react-hooks/rules-of-hooks */
+ public readonly useUrl = (
+ params: P,
+ getUrlParams?: LocatorGetUrlParams,
+ deps: DependencyList = []
+ ): string => useLocatorUrl(this, params, getUrlParams, deps);
+ /* eslint-enable react-hooks/rules-of-hooks */
}
diff --git a/src/plugins/share/common/url_service/locators/types.ts b/src/plugins/share/common/url_service/locators/types.ts
index d811ae0fd4aa2..870eaa3718d3f 100644
--- a/src/plugins/share/common/url_service/locators/types.ts
+++ b/src/plugins/share/common/url_service/locators/types.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import { DependencyList } from 'react';
import { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common';
/**
@@ -51,23 +52,57 @@ export interface LocatorDefinition
*/
export interface LocatorPublic
{
/**
- * Returns a relative URL to the client-side redirect endpoint using this
- * locator. (This method is necessary for compatibility with URL generators.)
+ * Returns a reference to a Kibana client-side location.
+ *
+ * @param params URL locator parameters.
*/
getLocation(params: P): Promise;
+ /**
+ * Returns a URL as a string.
+ *
+ * @param params URL locator parameters.
+ * @param getUrlParams URL construction parameters.
+ */
+ getUrl(params: P, getUrlParams?: LocatorGetUrlParams): Promise;
+
/**
* Navigate using the `core.application.navigateToApp()` method to a Kibana
* location generated by this locator. This method is available only on the
* browser.
+ *
+ * @param params URL locator parameters.
+ * @param navigationParams Navigation parameters.
*/
navigate(params: P, navigationParams?: LocatorNavigationParams): Promise;
+
+ /**
+ * React hook which returns a URL string given locator parameters. Returns
+ * empty string if URL is being loaded or an error happened.
+ */
+ useUrl: (params: P, getUrlParams?: LocatorGetUrlParams, deps?: DependencyList) => string;
}
+/**
+ * Parameters used when navigating on client-side using browser history object.
+ */
export interface LocatorNavigationParams {
+ /**
+ * Whether to replace a navigation entry in history queue or push a new entry.
+ */
replace?: boolean;
}
+/**
+ * Parameters used when constructing a string URL.
+ */
+export interface LocatorGetUrlParams {
+ /**
+ * Whether to return an absolute long URL or relative short URL.
+ */
+ absolute?: boolean;
+}
+
/**
* This interface represents a location in Kibana to which one can navigate
* using the `core.application.navigateToApp()` method.
@@ -79,9 +114,9 @@ export interface KibanaLocation {
app: string;
/**
- * A URL route within a Kibana application.
+ * A relative URL path within a Kibana application.
*/
- route: string;
+ path: string;
/**
* A serializable location state object, which the app can use to determine
diff --git a/src/plugins/share/common/url_service/locators/use_locator_url.ts b/src/plugins/share/common/url_service/locators/use_locator_url.ts
new file mode 100644
index 0000000000000..a84c712e16248
--- /dev/null
+++ b/src/plugins/share/common/url_service/locators/use_locator_url.ts
@@ -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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { DependencyList, useEffect, useState } from 'react';
+import useMountedState from 'react-use/lib/useMountedState';
+import { SerializableState } from 'src/plugins/kibana_utils/common';
+import { LocatorGetUrlParams, LocatorPublic } from '../../../common/url_service';
+
+export const useLocatorUrl = (
+ locator: LocatorPublic
| null | undefined,
+ params: P,
+ getUrlParams?: LocatorGetUrlParams,
+ deps: DependencyList = []
+): string => {
+ const [url, setUrl] = useState('');
+ const isMounted = useMountedState();
+
+ /* eslint-disable react-hooks/exhaustive-deps */
+ useEffect(() => {
+ if (!locator) {
+ setUrl('');
+ return;
+ }
+
+ locator
+ .getUrl(params, getUrlParams)
+ .then((result: string) => {
+ if (!isMounted()) return;
+ setUrl(result);
+ })
+ .catch((error) => {
+ if (!isMounted()) return;
+ // eslint-disable-next-line no-console
+ console.error('useLocatorUrl', error);
+ setUrl('');
+ });
+ }, [locator, ...deps]);
+ /* eslint-enable react-hooks/exhaustive-deps */
+
+ return url;
+};
diff --git a/src/plugins/share/common/url_service/url_service.ts b/src/plugins/share/common/url_service/url_service.ts
index 0c3a0aabb750b..5daba1500cdfd 100644
--- a/src/plugins/share/common/url_service/url_service.ts
+++ b/src/plugins/share/common/url_service/url_service.ts
@@ -17,7 +17,9 @@ export class UrlService {
/**
* Client to work with locators.
*/
- locators: LocatorClient = new LocatorClient(this.deps);
+ public readonly locators: LocatorClient;
- constructor(protected readonly deps: UrlServiceDependencies) {}
+ constructor(protected readonly deps: UrlServiceDependencies) {
+ this.locators = new LocatorClient(deps);
+ }
}
diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts
index d13bb15f8c72c..8f5356f6a2201 100644
--- a/src/plugins/share/public/index.ts
+++ b/src/plugins/share/public/index.ts
@@ -29,6 +29,8 @@ export {
UrlGeneratorsService,
} from './url_generators';
+export { useLocatorUrl } from '../common/url_service/locators/use_locator_url';
+
import { SharePlugin } from './plugin';
export { KibanaURL } from './kibana_url';
diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts
index eb7c46cdaef86..893108b56bcfa 100644
--- a/src/plugins/share/public/plugin.ts
+++ b/src/plugins/share/public/plugin.ts
@@ -68,14 +68,22 @@ export class SharePlugin implements Plugin {
core.application.register(createShortUrlRedirectApp(core, window.location));
this.url = new UrlService({
- navigate: async (location, { replace = false } = {}) => {
+ navigate: async ({ app, path, state }, { replace = false } = {}) => {
const [start] = await core.getStartServices();
- await start.application.navigateToApp(location.app, {
- path: location.route,
- state: location.state,
+ await start.application.navigateToApp(app, {
+ path,
+ state,
replace,
});
},
+ getUrl: async ({ app, path }, { absolute }) => {
+ const start = await core.getStartServices();
+ const url = start[0].application.getUrlForApp(app, {
+ path,
+ absolute,
+ });
+ return url;
+ },
});
return {
diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts
index 6e3c68935f77b..76e10372cdb67 100644
--- a/src/plugins/share/server/plugin.ts
+++ b/src/plugins/share/server/plugin.ts
@@ -32,7 +32,10 @@ export class SharePlugin implements Plugin {
public setup(core: CoreSetup) {
this.url = new UrlService({
navigate: async () => {
- throw new Error('Locator .navigate() does not work on server.');
+ throw new Error('Locator .navigate() currently is not supported on the server.');
+ },
+ getUrl: async () => {
+ throw new Error('Locator .getUrl() currently is not supported on the server.');
},
});
diff --git a/x-pack/plugins/index_lifecycle_management/public/index.ts b/x-pack/plugins/index_lifecycle_management/public/index.ts
index 9bfff971d5e71..cbd23a14a6114 100644
--- a/x-pack/plugins/index_lifecycle_management/public/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/index.ts
@@ -14,4 +14,4 @@ export const plugin = (initializerContext: PluginInitializerContext) => {
return new IndexLifecycleManagementPlugin(initializerContext);
};
-export { ILM_URL_GENERATOR_ID, IlmUrlGeneratorState } from './url_generator';
+export { ILM_LOCATOR_ID, IlmLocatorParams } from './locator';
diff --git a/x-pack/plugins/index_lifecycle_management/public/locator.ts b/x-pack/plugins/index_lifecycle_management/public/locator.ts
new file mode 100644
index 0000000000000..025946a095a6f
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/locator.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { SerializableState } from 'src/plugins/kibana_utils/common';
+import { ManagementAppLocator } from 'src/plugins/management/common';
+import { LocatorDefinition } from '../../../../src/plugins/share/public/';
+import {
+ getPoliciesListPath,
+ getPolicyCreatePath,
+ getPolicyEditPath,
+} from './application/services/navigation';
+import { PLUGIN } from '../common/constants';
+
+export const ILM_LOCATOR_ID = 'ILM_LOCATOR_ID';
+
+export interface IlmLocatorParams extends SerializableState {
+ page: 'policies_list' | 'policy_edit' | 'policy_create';
+ policyName?: string;
+}
+
+export interface IlmLocatorDefinitionDependencies {
+ managementAppLocator: ManagementAppLocator;
+}
+
+export class IlmLocatorDefinition implements LocatorDefinition {
+ constructor(protected readonly deps: IlmLocatorDefinitionDependencies) {}
+
+ public readonly id = ILM_LOCATOR_ID;
+
+ public readonly getLocation = async (params: IlmLocatorParams) => {
+ const location = await this.deps.managementAppLocator.getLocation({
+ sectionId: 'data',
+ appId: PLUGIN.ID,
+ });
+
+ switch (params.page) {
+ case 'policy_create': {
+ return {
+ ...location,
+ path: location.path + getPolicyCreatePath(),
+ };
+ }
+ case 'policy_edit': {
+ return {
+ ...location,
+ path: location.path + getPolicyEditPath(params.policyName!),
+ };
+ }
+ case 'policies_list': {
+ return {
+ ...location,
+ path: location.path + getPoliciesListPath(),
+ };
+ }
+ }
+ };
+}
diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx
index 069d1e0d10e0b..163fe2b3d9b5c 100644
--- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx
@@ -17,7 +17,7 @@ import { init as initNotification } from './application/services/notification';
import { BreadcrumbService } from './application/services/breadcrumbs';
import { addAllExtensions } from './extend_index_management';
import { ClientConfigType, SetupDependencies, StartDependencies } from './types';
-import { registerUrlGenerator } from './url_generator';
+import { IlmLocatorDefinition } from './locator';
export class IndexLifecycleManagementPlugin
implements Plugin {
@@ -38,7 +38,7 @@ export class IndexLifecycleManagementPlugin
getStartServices,
} = coreSetup;
- const { usageCollection, management, indexManagement, home, cloud, share } = plugins;
+ const { usageCollection, management, indexManagement, home, cloud } = plugins;
// Initialize services even if the app isn't mounted, because they're used by index management extensions.
initHttp(http);
@@ -110,7 +110,11 @@ export class IndexLifecycleManagementPlugin
addAllExtensions(indexManagement.extensionsService);
}
- registerUrlGenerator(coreSetup, management, share);
+ plugins.share.url.locators.create(
+ new IlmLocatorDefinition({
+ managementAppLocator: plugins.management.locator,
+ })
+ );
}
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/url_generator.ts b/x-pack/plugins/index_lifecycle_management/public/url_generator.ts
deleted file mode 100644
index f7794c535198f..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/url_generator.ts
+++ /dev/null
@@ -1,61 +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 { CoreSetup } from 'kibana/public';
-import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public/';
-import {
- getPoliciesListPath,
- getPolicyCreatePath,
- getPolicyEditPath,
-} from './application/services/navigation';
-import { MANAGEMENT_APP_ID } from '../../../../src/plugins/management/public';
-import { SetupDependencies } from './types';
-import { PLUGIN } from '../common/constants';
-
-export const ILM_URL_GENERATOR_ID = 'ILM_URL_GENERATOR_ID';
-
-export interface IlmUrlGeneratorState {
- page: 'policies_list' | 'policy_edit' | 'policy_create';
- policyName?: string;
- absolute?: boolean;
-}
-export const createIlmUrlGenerator = (
- getAppBasePath: (absolute?: boolean) => Promise
-): UrlGeneratorsDefinition => {
- return {
- id: ILM_URL_GENERATOR_ID,
- createUrl: async (state: IlmUrlGeneratorState): Promise => {
- switch (state.page) {
- case 'policy_create': {
- return `${await getAppBasePath(!!state.absolute)}${getPolicyCreatePath()}`;
- }
- case 'policy_edit': {
- return `${await getAppBasePath(!!state.absolute)}${getPolicyEditPath(state.policyName!)}`;
- }
- case 'policies_list': {
- return `${await getAppBasePath(!!state.absolute)}${getPoliciesListPath()}`;
- }
- }
- },
- };
-};
-
-export const registerUrlGenerator = (
- coreSetup: CoreSetup,
- management: SetupDependencies['management'],
- share: SetupDependencies['share']
-) => {
- const getAppBasePath = async (absolute = false) => {
- const [coreStart] = await coreSetup.getStartServices();
- return coreStart.application.getUrlForApp(MANAGEMENT_APP_ID, {
- path: management.sections.section.data.getApp(PLUGIN.ID)!.basePath,
- absolute,
- });
- };
-
- share.urlGenerators.registerUrlGenerator(createIlmUrlGenerator(getAppBasePath));
-};
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
index 93cd772ce6658..8e114b0596948 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
@@ -22,6 +22,21 @@ import {
const nonBreakingSpace = ' ';
+const urlServiceMock = {
+ locators: {
+ get: () => ({
+ getLocation: async () => ({
+ app: '',
+ path: '',
+ state: {},
+ }),
+ getUrl: async ({ policyName }: { policyName: string }) => `/test/${policyName}`,
+ navigate: async () => {},
+ useUrl: () => '',
+ }),
+ },
+};
+
describe('Data Streams tab', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();
let testBed: DataStreamsTabTestBed;
@@ -38,7 +53,9 @@ describe('Data Streams tab', () => {
});
test('displays an empty prompt', async () => {
- testBed = await setup();
+ testBed = await setup({
+ url: urlServiceMock,
+ });
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -54,6 +71,7 @@ describe('Data Streams tab', () => {
test('when Ingest Manager is disabled, goes to index templates tab when "Get started" link is clicked', async () => {
testBed = await setup({
plugins: {},
+ url: urlServiceMock,
});
await act(async () => {
@@ -73,6 +91,7 @@ describe('Data Streams tab', () => {
test('when Fleet is enabled, links to Fleet', async () => {
testBed = await setup({
plugins: { isFleetEnabled: true },
+ url: urlServiceMock,
});
await act(async () => {
@@ -95,6 +114,7 @@ describe('Data Streams tab', () => {
testBed = await setup({
plugins: {},
+ url: urlServiceMock,
});
await act(async () => {
@@ -345,6 +365,7 @@ describe('Data Streams tab', () => {
testBed = await setup({
history: createMemoryHistory(),
+ url: urlServiceMock,
});
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -370,13 +391,8 @@ describe('Data Streams tab', () => {
});
});
- describe('url generators', () => {
- const mockIlmUrlGenerator = {
- getUrlGenerator: () => ({
- createUrl: ({ policyName }: { policyName: string }) => `/test/${policyName}`,
- }),
- };
- test('with an ILM url generator and an ILM policy', async () => {
+ describe('url locators', () => {
+ test('with an ILM url locator and an ILM policy', async () => {
const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers;
const dataStreamForDetailPanel = createDataStreamPayload({
@@ -388,7 +404,7 @@ describe('Data Streams tab', () => {
testBed = await setup({
history: createMemoryHistory(),
- urlGenerators: mockIlmUrlGenerator,
+ url: urlServiceMock,
});
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -400,7 +416,7 @@ describe('Data Streams tab', () => {
expect(findDetailPanelIlmPolicyLink().prop('href')).toBe('/test/my_ilm_policy');
});
- test('with an ILM url generator and no ILM policy', async () => {
+ test('with an ILM url locator and no ILM policy', async () => {
const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers;
const dataStreamForDetailPanel = createDataStreamPayload({ name: 'dataStream1' });
@@ -409,7 +425,7 @@ describe('Data Streams tab', () => {
testBed = await setup({
history: createMemoryHistory(),
- urlGenerators: mockIlmUrlGenerator,
+ url: urlServiceMock,
});
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -422,7 +438,7 @@ describe('Data Streams tab', () => {
expect(findDetailPanelIlmPolicyName().contains('None')).toBeTruthy();
});
- test('without an ILM url generator and with an ILM policy', async () => {
+ test('without an ILM url locator and with an ILM policy', async () => {
const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers;
const dataStreamForDetailPanel = createDataStreamPayload({
@@ -434,7 +450,11 @@ describe('Data Streams tab', () => {
testBed = await setup({
history: createMemoryHistory(),
- urlGenerators: { getUrlGenerator: () => {} },
+ url: {
+ locators: {
+ get: () => undefined,
+ },
+ },
});
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -463,6 +483,7 @@ describe('Data Streams tab', () => {
testBed = await setup({
history: createMemoryHistory(),
+ url: urlServiceMock,
});
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -506,6 +527,7 @@ describe('Data Streams tab', () => {
testBed = await setup({
history: createMemoryHistory(),
+ url: urlServiceMock,
});
await act(async () => {
testBed.actions.goToDataStreamsList();
@@ -542,7 +564,7 @@ describe('Data Streams tab', () => {
beforeEach(async () => {
setLoadDataStreamsResponse([dataStreamWithDelete, dataStreamNoDelete]);
- testBed = await setup({ history: createMemoryHistory() });
+ testBed = await setup({ history: createMemoryHistory(), url: urlServiceMock });
await act(async () => {
testBed.actions.goToDataStreamsList();
});
diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx
index 3b06d76cf7c26..f8ebfdf7c46b7 100644
--- a/x-pack/plugins/index_management/public/application/app_context.tsx
+++ b/x-pack/plugins/index_management/public/application/app_context.tsx
@@ -35,7 +35,7 @@ export interface AppDependencies {
history: ScopedHistory;
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
uiSettings: CoreSetup['uiSettings'];
- urlGenerators: SharePluginStart['urlGenerators'];
+ url: SharePluginStart['url'];
docLinks: CoreStart['docLinks'];
}
diff --git a/x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts b/x-pack/plugins/index_management/public/application/constants/ilm_locator.ts
similarity index 83%
rename from x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts
rename to x-pack/plugins/index_management/public/application/constants/ilm_locator.ts
index ea6cf1756b73c..3da13727af8de 100644
--- a/x-pack/plugins/index_management/public/application/constants/ilm_url_generator.ts
+++ b/x-pack/plugins/index_management/public/application/constants/ilm_locator.ts
@@ -5,5 +5,5 @@
* 2.0.
*/
-export const ILM_URL_GENERATOR_ID = 'ILM_URL_GENERATOR_ID';
+export const ILM_LOCATOR_ID = 'ILM_LOCATOR_ID';
export const ILM_PAGES_POLICY_EDIT = 'policy_edit';
diff --git a/x-pack/plugins/index_management/public/application/constants/index.ts b/x-pack/plugins/index_management/public/application/constants/index.ts
index 3bf30517c1145..7a1caf5e50771 100644
--- a/x-pack/plugins/index_management/public/application/constants/index.ts
+++ b/x-pack/plugins/index_management/public/application/constants/index.ts
@@ -17,4 +17,4 @@ export {
export const REACT_ROOT_ID = 'indexManagementReactRoot';
-export * from './ilm_url_generator';
+export * from './ilm_locator';
diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts
index 074334ed87725..083a8831291dd 100644
--- a/x-pack/plugins/index_management/public/application/mount_management_section.ts
+++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts
@@ -62,7 +62,7 @@ export async function mountManagementSection(
uiSettings,
} = core;
- const { urlGenerators } = startDependencies.share;
+ const { url } = startDependencies.share;
docTitle.change(PLUGIN.getI18nName(i18n));
breadcrumbService.setup(setBreadcrumbs);
@@ -86,7 +86,7 @@ export async function mountManagementSection(
history,
setBreadcrumbs,
uiSettings,
- urlGenerators,
+ url,
docLinks,
};
diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
index 773ccd91a5fb1..a9258c6a3b10b 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx
@@ -29,11 +29,11 @@ import { SectionLoading, SectionError, Error, DataHealth } from '../../../../com
import { useLoadDataStream } from '../../../../services/api';
import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal';
import { humanizeTimeStamp } from '../humanize_time_stamp';
-import { useUrlGenerator } from '../../../../services/use_url_generator';
import { getIndexListUri, getTemplateDetailsLink } from '../../../../services/routing';
-import { ILM_PAGES_POLICY_EDIT, ILM_URL_GENERATOR_ID } from '../../../../constants';
+import { ILM_PAGES_POLICY_EDIT } from '../../../../constants';
import { useAppContext } from '../../../../app_context';
import { DataStreamsBadges } from '../data_stream_badges';
+import { useIlmLocator } from '../../../../services/use_ilm_locator';
interface DetailsListProps {
details: Array<{
@@ -89,13 +89,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({
const [isDeleting, setIsDeleting] = useState(false);
- const ilmPolicyLink = useUrlGenerator({
- urlGeneratorId: ILM_URL_GENERATOR_ID,
- urlGeneratorState: {
- page: ILM_PAGES_POLICY_EDIT,
- policyName: dataStream?.ilmPolicyName,
- },
- });
+ const ilmPolicyLink = useIlmLocator(ILM_PAGES_POLICY_EDIT, dataStream?.ilmPolicyName);
const { history } = useAppContext();
let content;
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
index 2dd2c6e30cfcc..c17ccd9ced932 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
@@ -21,8 +21,8 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { TemplateDeserialized } from '../../../../../../../common';
-import { ILM_PAGES_POLICY_EDIT, ILM_URL_GENERATOR_ID } from '../../../../../constants';
-import { useUrlGenerator } from '../../../../../services/use_url_generator';
+import { ILM_PAGES_POLICY_EDIT } from '../../../../../constants';
+import { useIlmLocator } from '../../../../../services/use_ilm_locator';
interface Props {
templateDetails: TemplateDeserialized;
@@ -54,13 +54,7 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails })
const numIndexPatterns = indexPatterns.length;
- const ilmPolicyLink = useUrlGenerator({
- urlGeneratorId: ILM_URL_GENERATOR_ID,
- urlGeneratorState: {
- page: ILM_PAGES_POLICY_EDIT,
- policyName: ilmPolicy?.name,
- },
- });
+ const ilmPolicyLink = useIlmLocator(ILM_PAGES_POLICY_EDIT, ilmPolicy?.name);
return (
<>
diff --git a/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts b/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts
new file mode 100644
index 0000000000000..d60cd1cf8aabf
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/services/use_ilm_locator.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 { useLocatorUrl } from '../../../../../../src/plugins/share/public';
+import { useAppContext } from '../app_context';
+import { ILM_LOCATOR_ID } from '../constants';
+
+export const useIlmLocator = (
+ page: 'policies_list' | 'policy_edit' | 'policy_create',
+ policyName?: string
+): string => {
+ const ctx = useAppContext();
+ const locator = policyName === undefined ? null : ctx.url.locators.get(ILM_LOCATOR_ID)!;
+ const url = useLocatorUrl(locator, { page, policyName }, {}, [page, policyName]);
+
+ return url;
+};
diff --git a/x-pack/plugins/index_management/public/application/services/use_url_generator.ts b/x-pack/plugins/index_management/public/application/services/use_url_generator.ts
deleted file mode 100644
index 2d9ab3959d769..0000000000000
--- a/x-pack/plugins/index_management/public/application/services/use_url_generator.ts
+++ /dev/null
@@ -1,40 +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 { useEffect, useState } from 'react';
-import {
- UrlGeneratorContract,
- UrlGeneratorId,
- UrlGeneratorStateMapping,
-} from '../../../../../../src/plugins/share/public';
-import { useAppContext } from '../app_context';
-
-export const useUrlGenerator = ({
- urlGeneratorId,
- urlGeneratorState,
-}: {
- urlGeneratorId: UrlGeneratorId;
- urlGeneratorState: UrlGeneratorStateMapping[UrlGeneratorId]['State'];
-}) => {
- const { urlGenerators } = useAppContext();
- const [link, setLink] = useState();
- useEffect(() => {
- const updateLink = async (): Promise => {
- let urlGenerator: UrlGeneratorContract;
- try {
- urlGenerator = urlGenerators.getUrlGenerator(urlGeneratorId);
- const url = await urlGenerator.createUrl(urlGeneratorState);
- setLink(url);
- } catch (e) {
- // do nothing
- }
- };
-
- updateLink();
- }, [urlGeneratorId, urlGeneratorState, urlGenerators]);
- return link;
-};
From 62eece5441664747441866772402bd6c9e764017 Mon Sep 17 00:00:00 2001
From: Angela Chuang <6295984+angorayc@users.noreply.github.com>
Date: Mon, 21 Jun 2021 21:37:07 +0100
Subject: [PATCH 033/163] [SecuritySolution] Move manual test cases to Cypress
(#100730)
* add scenarios 1-3
* add tests for toggle full screen
* add tests for timeline pagination
* add tests for timeline correlation tab
* fix cypress tests
* add data-test-subj for timeline tabs content
* fix up
* fix flaky tests
* fix mark as favorite scenario
* fix flaky test
* fix flaky test
* fix flaky test
* refactors 'can be marked as favourite' test
* fixes test
* fixes typecheck issue
* refactors the pipe
* little fix
* mark as favourite refactor
* removes code that causes the flakiness
* apply the fix for 7.13 branch
* fix timeline api call
* fix timeline api call
* fix timeline api call
* fix syntax
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Gloria Hornero
---
.../integration/cases/attach_timeline.spec.ts | 3 +-
.../integration/overview/overview.spec.ts | 19 ++++
.../timeline_templates/creation.spec.ts | 31 ++++++-
.../integration/timelines/creation.spec.ts | 57 ++++++++++--
.../timelines/flyout_button.spec.ts | 6 +-
.../integration/timelines/full_screen.spec.ts | 41 +++++++++
.../integration/timelines/notes_tab.spec.ts | 65 ++++++++++++--
.../integration/timelines/pagination.spec.ts | 59 +++++++++++++
.../integration/timelines/query_tab.spec.ts | 14 ++-
.../timelines/row_renderers.spec.ts | 88 +++++++++++++++++++
.../timelines/search_or_filter.spec.ts | 47 +++++++++-
.../cypress/screens/overview.ts | 2 +
.../cypress/screens/security_header.ts | 2 +
.../cypress/screens/timeline.ts | 77 +++++++++++++++-
.../cypress/tasks/api_calls/timelines.ts | 23 +++++
.../cypress/tasks/security_main.ts | 9 ++
.../cypress/tasks/timeline.ts | 54 +++++++++++-
.../recent_timelines/recent_timelines.tsx | 6 +-
.../timelines/components/flyout/index.tsx | 2 +-
.../row_renderers_browser/index.tsx | 2 +-
.../timelines/components/timeline/index.tsx | 5 +-
.../timeline/search_or_filter/helpers.tsx | 2 +
.../timeline/tabs_content/index.tsx | 20 ++++-
23 files changed, 595 insertions(+), 39 deletions(-)
create mode 100644 x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts
create mode 100644 x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts
create mode 100644 x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts
diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts
index 7f0016e39ff88..3f3209b52120e 100644
--- a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts
@@ -19,8 +19,7 @@ import { createTimeline } from '../../tasks/api_calls/timelines';
import { cleanKibana } from '../../tasks/common';
import { createCase } from '../../tasks/api_calls/cases';
-// TODO: enable once attach timeline to cases is re-enabled
-describe.skip('attach timeline to case', () => {
+describe('attach timeline to case', () => {
context('without cases created', () => {
beforeEach(() => {
cleanKibana();
diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts
index dc5b247e3ec43..78ee3fdcdcdd5 100644
--- a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts
@@ -15,6 +15,8 @@ import { OVERVIEW_URL } from '../../urls/navigation';
import overviewFixture from '../../fixtures/overview_search_strategy.json';
import emptyInstance from '../../fixtures/empty_instance.json';
import { cleanKibana } from '../../tasks/common';
+import { createTimeline, favoriteTimeline } from '../../tasks/api_calls/timelines';
+import { timeline } from '../../objects/timeline';
describe('Overview Page', () => {
before(() => {
@@ -48,4 +50,21 @@ describe('Overview Page', () => {
cy.get(OVERVIEW_EMPTY_PAGE).should('be.visible');
});
});
+
+ describe('Favorite Timelines', () => {
+ it('should appear on overview page', () => {
+ createTimeline(timeline)
+ .then((response) => response.body.data.persistTimeline.timeline.savedObjectId)
+ .then((timelineId: string) => {
+ favoriteTimeline({ timelineId, timelineType: 'default' }).then(() => {
+ cy.stubSearchStrategyApi(overviewFixture, 'overviewNetwork');
+ loginAndWaitForPage(OVERVIEW_URL);
+ cy.get('[data-test-subj="overview-recent-timelines"]').should(
+ 'contain',
+ timeline.title
+ );
+ });
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts
index a600b5edfd632..e2c1d7eef38c3 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts
@@ -16,6 +16,7 @@ import {
NOTES_TEXT_AREA,
PIN_EVENT,
TIMELINE_DESCRIPTION,
+ TIMELINE_FLYOUT_WRAPPER,
TIMELINE_QUERY,
TIMELINE_TITLE,
} from '../../screens/timeline';
@@ -25,34 +26,38 @@ import {
TIMELINES_NOTES_COUNT,
TIMELINES_FAVORITE,
} from '../../screens/timelines';
+import { createTimeline } from '../../tasks/api_calls/timelines';
import { cleanKibana } from '../../tasks/common';
-import { loginAndWaitForPage } from '../../tasks/login';
+import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { openTimelineUsingToggle } from '../../tasks/security_main';
import {
addDescriptionToTimeline,
addFilter,
addNameToTimeline,
addNotesToTimeline,
+ clickingOnCreateTemplateFromTimelineBtn,
closeTimeline,
createNewTimelineTemplate,
+ expandEventAction,
markAsFavorite,
openTimelineTemplateFromSettings,
populateTimeline,
waitForTimelineChanges,
} from '../../tasks/timeline';
-import { openTimeline } from '../../tasks/timelines';
+import { openTimeline, waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines';
-import { OVERVIEW_URL } from '../../urls/navigation';
+import { TIMELINES_URL } from '../../urls/navigation';
describe('Timeline Templates', () => {
beforeEach(() => {
cleanKibana();
+ loginAndWaitForPageWithoutDateRange(TIMELINES_URL);
+
cy.intercept('PATCH', '/api/timeline').as('timeline');
});
it('Creates a timeline template', async () => {
- loginAndWaitForPage(OVERVIEW_URL);
openTimelineUsingToggle();
createNewTimelineTemplate();
populateTimeline();
@@ -97,4 +102,22 @@ describe('Timeline Templates', () => {
cy.get(NOTES).should('have.text', timeline.notes);
});
});
+
+ it('Create template from timeline', () => {
+ waitForTimelinesPanelToBeLoaded();
+
+ createTimeline(timeline).then(() => {
+ expandEventAction();
+ clickingOnCreateTemplateFromTimelineBtn();
+ cy.wait('@timeline', { timeout: 100000 }).then(({ request }) => {
+ expect(request.body.timeline).to.haveOwnProperty('templateTimelineId');
+ expect(request.body.timeline).to.haveOwnProperty('description', timeline.description);
+ expect(request.body.timeline.kqlQuery.filterQuery.kuery).to.haveOwnProperty(
+ 'expression',
+ timeline.query
+ );
+ cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible');
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts
index b08bae26bf7ed..8a90b67682cb2 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts
@@ -8,32 +8,37 @@
import { timeline } from '../../objects/timeline';
import {
- FAVORITE_TIMELINE,
LOCKED_ICON,
NOTES_TEXT,
PIN_EVENT,
+ SERVER_SIDE_EVENT_COUNT,
TIMELINE_FILTER,
+ TIMELINE_FLYOUT_WRAPPER,
TIMELINE_PANEL,
+ TIMELINE_TAB_CONTENT_EQL,
} from '../../screens/timeline';
+import { createTimelineTemplate } from '../../tasks/api_calls/timelines';
import { cleanKibana } from '../../tasks/common';
-import { loginAndWaitForPage } from '../../tasks/login';
+import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { openTimelineUsingToggle } from '../../tasks/security_main';
import {
+ addEqlToTimeline,
addFilter,
addNameAndDescriptionToTimeline,
addNotesToTimeline,
+ clickingOnCreateTimelineFormTemplateBtn,
closeTimeline,
createNewTimeline,
+ expandEventAction,
goToQueryTab,
- markAsFavorite,
pinFirstEvent,
populateTimeline,
- waitForTimelineChanges,
} from '../../tasks/timeline';
-import { OVERVIEW_URL } from '../../urls/navigation';
+import { OVERVIEW_URL, TIMELINE_TEMPLATES_URL } from '../../urls/navigation';
+import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines';
describe('Timelines', (): void => {
before(() => {
@@ -88,10 +93,44 @@ describe('Timelines', (): void => {
cy.get(NOTES_TEXT).should('have.text', timeline.notes);
});
- it('can be marked as favorite', () => {
- markAsFavorite();
- waitForTimelineChanges();
- cy.get(FAVORITE_TIMELINE).should('have.text', 'Remove from favorites');
+ it('should update timeline after adding eql', () => {
+ cy.intercept('PATCH', '/api/timeline').as('updateTimeline');
+ const eql = 'any where process.name == "which"';
+ addEqlToTimeline(eql);
+
+ cy.wait('@updateTimeline', { timeout: 10000 }).its('response.statusCode').should('eq', 200);
+
+ cy.get(`${TIMELINE_TAB_CONTENT_EQL} ${SERVER_SIDE_EVENT_COUNT}`)
+ .invoke('text')
+ .then(parseInt)
+ .should('be.gt', 0);
+ });
+ });
+});
+
+describe('Create a timeline from a template', () => {
+ before(() => {
+ cleanKibana();
+ loginAndWaitForPageWithoutDateRange(TIMELINE_TEMPLATES_URL);
+ waitForTimelinesPanelToBeLoaded();
+ });
+
+ it('Should have the same query and open the timeline modal', () => {
+ createTimelineTemplate(timeline).then(() => {
+ expandEventAction();
+ cy.intercept('/api/timeline').as('timeline');
+
+ clickingOnCreateTimelineFormTemplateBtn();
+ cy.wait('@timeline', { timeout: 100000 }).then(({ request }) => {
+ if (request.body && request.body.timeline) {
+ expect(request.body.timeline).to.haveOwnProperty('description', timeline.description);
+ expect(request.body.timeline.kqlQuery.filterQuery.kuery).to.haveOwnProperty(
+ 'expression',
+ timeline.query
+ );
+ cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible');
+ }
+ });
});
});
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts
index c7ec17d027e80..38c6f41f1049c 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/flyout_button.spec.ts
@@ -61,8 +61,10 @@ describe('timeline flyout button', () => {
it('the `(+)` button popover menu owns focus', () => {
cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click({ force: true });
- cy.get(CREATE_NEW_TIMELINE).should('have.focus');
- cy.get('body').type('{esc}');
+ cy.get(`${CREATE_NEW_TIMELINE}`)
+ .pipe(($el) => $el.trigger('focus'))
+ .should('have.focus');
+ cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').type('{esc}');
cy.get(CREATE_NEW_TIMELINE).should('not.be.visible');
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts
new file mode 100644
index 0000000000000..9cd3b22fc2bb4
--- /dev/null
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/full_screen.spec.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { TIMELINE_HEADER, TIMELINE_TABS } from '../../screens/timeline';
+import { cleanKibana } from '../../tasks/common';
+
+import { loginAndWaitForPage } from '../../tasks/login';
+import {
+ openTimelineUsingToggle,
+ enterFullScreenMode,
+ exitFullScreenMode,
+} from '../../tasks/security_main';
+import { populateTimeline } from '../../tasks/timeline';
+
+import { HOSTS_URL } from '../../urls/navigation';
+
+describe('Toggle full screen', () => {
+ before(() => {
+ cleanKibana();
+ loginAndWaitForPage(HOSTS_URL);
+ openTimelineUsingToggle();
+ populateTimeline();
+ });
+
+ it('Should hide timeline header and tab list area', () => {
+ enterFullScreenMode();
+
+ cy.get(TIMELINE_TABS).should('not.exist');
+ cy.get(TIMELINE_HEADER).should('not.be.visible');
+ });
+
+ it('Should show timeline header and tab list area', () => {
+ exitFullScreenMode();
+ cy.get(TIMELINE_TABS).should('exist');
+ cy.get(TIMELINE_HEADER).should('be.visible');
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts
index 2505930f72f82..24309b8fda084 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts
@@ -7,7 +7,13 @@
import { timelineNonValidQuery } from '../../objects/timeline';
-import { NOTES_TEXT, NOTES_TEXT_AREA } from '../../screens/timeline';
+import {
+ NOTES_AUTHOR,
+ NOTES_CODE_BLOCK,
+ NOTES_LINK,
+ NOTES_TEXT,
+ NOTES_TEXT_AREA,
+} from '../../screens/timeline';
import { createTimeline } from '../../tasks/api_calls/timelines';
import { cleanKibana } from '../../tasks/common';
@@ -16,6 +22,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import {
addNotesToTimeline,
closeTimeline,
+ goToNotesTab,
openTimelineById,
refreshTimelinesUntilTimeLinePresent,
} from '../../tasks/timeline';
@@ -23,8 +30,11 @@ import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines';
import { TIMELINES_URL } from '../../urls/navigation';
+const text = 'elastic';
+const link = 'https://www.elastic.co/';
+
describe('Timeline notes tab', () => {
- before(() => {
+ beforeEach(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(TIMELINES_URL);
waitForTimelinesPanelToBeLoaded();
@@ -37,19 +47,62 @@ describe('Timeline notes tab', () => {
// request responses and indeterminism since on clicks to activates URL's.
.then(() => cy.wait(1000))
.then(() => openTimelineById(timelineId))
- .then(() => addNotesToTimeline(timelineNonValidQuery.notes))
+ .then(() => goToNotesTab())
);
});
after(() => {
closeTimeline();
});
+ it('should render mockdown', () => {
+ cy.intercept('/api/note').as(`updateNote`);
+ addNotesToTimeline(timelineNonValidQuery.notes);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(NOTES_TEXT_AREA).should('exist');
+ });
it('should contain notes', () => {
- cy.get(NOTES_TEXT).should('have.text', timelineNonValidQuery.notes);
+ cy.intercept('/api/note').as(`updateNote`);
+ addNotesToTimeline(timelineNonValidQuery.notes);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(NOTES_TEXT).first().should('have.text', timelineNonValidQuery.notes);
});
- it('should render mockdown', () => {
- cy.get(NOTES_TEXT_AREA).should('exist');
+ it('should be able to render font in bold', () => {
+ cy.intercept('/api/note').as(`updateNote`);
+ addNotesToTimeline(`**bold**`);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(`${NOTES_TEXT} strong`).last().should('have.text', `bold`);
+ });
+
+ it('should be able to render font in italics', () => {
+ cy.intercept('/api/note').as(`updateNote`);
+ addNotesToTimeline(`_italics_`);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(`${NOTES_TEXT} em`).last().should('have.text', `italics`);
+ });
+
+ it('should be able to render code blocks', () => {
+ cy.intercept('/api/note').as(`updateNote`);
+ addNotesToTimeline(`\`code\``);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(NOTES_CODE_BLOCK).should('exist');
+ });
+
+ it('should render the right author', () => {
+ cy.intercept('/api/note').as(`updateNote`);
+ addNotesToTimeline(timelineNonValidQuery.notes);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(NOTES_AUTHOR).first().should('have.text', text);
+ });
+
+ it('should be able to render a link', () => {
+ cy.intercept('/api/note').as(`updateNote`);
+ cy.intercept(link).as(`link`);
+ addNotesToTimeline(`[${text}](${link})`);
+ cy.wait('@updateNote').its('response.statusCode').should('eq', 200);
+ cy.get(NOTES_LINK).last().should('have.text', `${text}(opens in a new tab or window)`);
+ cy.get(NOTES_LINK).last().click();
+ cy.wait('@link').its('response.statusCode').should('eq', 200);
});
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts
new file mode 100644
index 0000000000000..568fb90568fb3
--- /dev/null
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 {
+ TIMELINE_EVENT,
+ TIMELINE_EVENTS_COUNT_NEXT_PAGE,
+ TIMELINE_EVENTS_COUNT_PER_PAGE,
+ TIMELINE_EVENTS_COUNT_PER_PAGE_BTN,
+ TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION,
+ TIMELINE_EVENTS_COUNT_PREV_PAGE,
+} from '../../screens/timeline';
+import { cleanKibana } from '../../tasks/common';
+
+import { loginAndWaitForPage } from '../../tasks/login';
+import { openTimelineUsingToggle } from '../../tasks/security_main';
+import { populateTimeline } from '../../tasks/timeline';
+
+import { HOSTS_URL } from '../../urls/navigation';
+
+const defaultPageSize = 25;
+describe('Pagination', () => {
+ beforeEach(() => {
+ cleanKibana();
+ loginAndWaitForPage(HOSTS_URL);
+ openTimelineUsingToggle();
+ populateTimeline();
+ });
+
+ it(`should have ${defaultPageSize} events in the page by default`, () => {
+ cy.get(TIMELINE_EVENT).should('have.length', defaultPageSize);
+ });
+
+ it(`should select ${defaultPageSize} items per page by default`, () => {
+ cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE).should('contain.text', defaultPageSize);
+ });
+
+ it('should be able to change items count per page with the dropdown', () => {
+ const itemsPerPage = 100;
+ cy.intercept('POST', '/internal/bsearch').as('refetch');
+
+ cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE_BTN).first().click();
+ cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION(itemsPerPage)).click();
+ cy.wait('@refetch').its('response.statusCode').should('eq', 200);
+ cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE).should('contain.text', itemsPerPage);
+ });
+
+ it('should be able to go to next / previous page', () => {
+ cy.intercept('POST', '/internal/bsearch').as('refetch');
+ cy.get(TIMELINE_EVENTS_COUNT_NEXT_PAGE).first().click();
+ cy.wait('@refetch').its('response.statusCode').should('eq', 200);
+
+ cy.get(TIMELINE_EVENTS_COUNT_PREV_PAGE).first().click();
+ cy.wait('@refetch').its('response.statusCode').should('eq', 200);
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts
index 672e930bc5072..f37a66ac048fb 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts
@@ -7,7 +7,13 @@
import { timeline } from '../../objects/timeline';
-import { UNLOCKED_ICON, PIN_EVENT, TIMELINE_FILTER, TIMELINE_QUERY } from '../../screens/timeline';
+import {
+ UNLOCKED_ICON,
+ PIN_EVENT,
+ TIMELINE_FILTER,
+ TIMELINE_QUERY,
+ NOTE_CARD_CONTENT,
+} from '../../screens/timeline';
import { addNoteToTimeline } from '../../tasks/api_calls/notes';
import { createTimeline } from '../../tasks/api_calls/timelines';
@@ -18,6 +24,7 @@ import {
addFilter,
closeTimeline,
openTimelineById,
+ persistNoteToFirstEvent,
pinFirstEvent,
refreshTimelinesUntilTimeLinePresent,
} from '../../tasks/timeline';
@@ -45,6 +52,7 @@ describe('Timeline query tab', () => {
)
.then(() => openTimelineById(timelineId))
.then(() => pinFirstEvent())
+ .then(() => persistNoteToFirstEvent('event note'))
.then(() => addFilter(timeline.filter));
});
});
@@ -58,6 +66,10 @@ describe('Timeline query tab', () => {
cy.get(TIMELINE_QUERY).should('have.text', `${timeline.query}`);
});
+ it('should be able to add event note', () => {
+ cy.get(NOTE_CARD_CONTENT).should('contain', 'event note');
+ });
+
it('should display timeline filter', () => {
cy.get(TIMELINE_FILTER(timeline.filter)).should('exist');
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts
new file mode 100644
index 0000000000000..ed9a7db4702d0
--- /dev/null
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts
@@ -0,0 +1,88 @@
+/*
+ * 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 {
+ TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN,
+ TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON,
+ TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX,
+ TIMELINE_ROW_RENDERERS_SEARCHBOX,
+ TIMELINE_SHOW_ROW_RENDERERS_GEAR,
+} from '../../screens/timeline';
+import { cleanKibana } from '../../tasks/common';
+
+import { loginAndWaitForPage } from '../../tasks/login';
+import { openTimelineUsingToggle } from '../../tasks/security_main';
+import { populateTimeline } from '../../tasks/timeline';
+
+import { HOSTS_URL } from '../../urls/navigation';
+
+const RowRenderersId = [
+ 'alerts',
+ 'auditd',
+ 'auditd_file',
+ 'library',
+ 'netflow',
+ 'plain',
+ 'registry',
+ 'suricata',
+ 'system',
+ 'system_dns',
+ 'system_endgame_process',
+ 'system_file',
+ 'system_fim',
+ 'system_security_event',
+ 'system_socket',
+ 'threat_match',
+ 'zeek',
+];
+
+describe('Row renderers', () => {
+ beforeEach(() => {
+ cleanKibana();
+ loginAndWaitForPage(HOSTS_URL);
+ openTimelineUsingToggle();
+ populateTimeline();
+ cy.get(TIMELINE_SHOW_ROW_RENDERERS_GEAR).first().click({ force: true });
+ });
+
+ afterEach(() => {
+ cy.get(TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON).click({ force: true });
+ });
+
+ it('Row renderers should be enabled by default', () => {
+ cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).should('exist');
+ cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).should('be.checked');
+ });
+
+ it('Selected renderer can be disabled and enabled', () => {
+ cy.get(TIMELINE_ROW_RENDERERS_SEARCHBOX).type('flow');
+
+ cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).first().uncheck();
+ cy.intercept('PATCH', '/api/timeline').as('updateTimeline');
+
+ cy.wait('@updateTimeline').then((interception) => {
+ expect(interception.request.body.timeline.excludedRowRendererIds).to.contain('netflow');
+ });
+
+ cy.get(TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX).first().check();
+
+ cy.wait('@updateTimeline').then((interception) => {
+ expect(interception.request.body.timeline.excludedRowRendererIds).not.to.contain('netflow');
+ });
+ });
+
+ it('Selected renderer can be disabled with one click', () => {
+ cy.get(TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN).click({ force: true });
+
+ cy.intercept('PATCH', '/api/timeline').as('updateTimeline');
+ cy.wait('@updateTimeline').its('response.statusCode').should('eq', 200);
+
+ cy.wait('@updateTimeline').then((interception) => {
+ expect(interception.request.body.timeline.excludedRowRendererIds).to.eql(RowRenderersId);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts
index 48b00f8afd4eb..9d019cf23ebb1 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/search_or_filter.spec.ts
@@ -5,14 +5,21 @@
* 2.0.
*/
-import { SERVER_SIDE_EVENT_COUNT } from '../../screens/timeline';
+import {
+ ADD_FILTER,
+ SERVER_SIDE_EVENT_COUNT,
+ TIMELINE_KQLMODE_FILTER,
+ TIMELINE_KQLMODE_SEARCH,
+ TIMELINE_SEARCH_OR_FILTER,
+} from '../../screens/timeline';
import { cleanKibana } from '../../tasks/common';
-import { loginAndWaitForPage } from '../../tasks/login';
+import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { openTimelineUsingToggle } from '../../tasks/security_main';
import { executeTimelineKQL } from '../../tasks/timeline';
+import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines';
-import { HOSTS_URL } from '../../urls/navigation';
+import { HOSTS_URL, TIMELINES_URL } from '../../urls/navigation';
describe('timeline search or filter KQL bar', () => {
beforeEach(() => {
@@ -28,3 +35,37 @@ describe('timeline search or filter KQL bar', () => {
cy.get(SERVER_SIDE_EVENT_COUNT).should(($count) => expect(+$count.text()).to.be.gt(0));
});
});
+
+describe('Update kqlMode for timeline', () => {
+ before(() => {
+ cleanKibana();
+ loginAndWaitForPageWithoutDateRange(TIMELINES_URL);
+ waitForTimelinesPanelToBeLoaded();
+ openTimelineUsingToggle();
+ });
+
+ beforeEach(() => {
+ cy.intercept('PATCH', '/api/timeline').as('update');
+ cy.get(TIMELINE_SEARCH_OR_FILTER)
+ .pipe(($el) => $el.trigger('click'))
+ .should('exist');
+ });
+
+ it('should be able to update timeline kqlMode with filter', () => {
+ cy.get(TIMELINE_KQLMODE_FILTER).click();
+ cy.wait('@update').then(({ response }) => {
+ cy.wrap(response!.statusCode).should('eql', 200);
+ cy.wrap(response!.body.data.persistTimeline.timeline.kqlMode).should('eql', 'filter');
+ cy.get(ADD_FILTER).should('exist');
+ });
+ });
+
+ it('should be able to update timeline kqlMode with search', () => {
+ cy.get(TIMELINE_KQLMODE_SEARCH).click();
+ cy.wait('@update').then(({ response }) => {
+ cy.wrap(response!.statusCode).should('eql', 200);
+ cy.wrap(response!.body.data.persistTimeline.timeline.kqlMode).should('eql', 'search');
+ cy.get(ADD_FILTER).should('not.exist');
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts
index 1c519b21149a8..ce6c5662ecb9e 100644
--- a/x-pack/plugins/security_solution/cypress/screens/overview.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts
@@ -145,3 +145,5 @@ export const OVERVIEW_HOST_STATS = '[data-test-subj="overview-hosts-stats"]';
export const OVERVIEW_NETWORK_STATS = '[data-test-subj="overview-network-stats"]';
export const OVERVIEW_EMPTY_PAGE = '[data-test-subj="empty-page"]';
+
+export const OVERVIEW_REVENT_TIMELINES = '[data-test-subj="overview-recent-timelines"]';
diff --git a/x-pack/plugins/security_solution/cypress/screens/security_header.ts b/x-pack/plugins/security_solution/cypress/screens/security_header.ts
index cb8502ef96029..a3d5b714cdb3f 100644
--- a/x-pack/plugins/security_solution/cypress/screens/security_header.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/security_header.ts
@@ -24,3 +24,5 @@ export const OVERVIEW = '[data-test-subj="navigation-overview"]';
export const REFRESH_BUTTON = '[data-test-subj="querySubmitButton"]';
export const TIMELINES = '[data-test-subj="navigation-timelines"]';
+
+export const LOADING_INDICATOR = '[data-test-subj="globalLoadingIndicator"]';
diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts
index 88e207fcea339..0a9e5b44feb1f 100644
--- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts
@@ -58,6 +58,10 @@ export const UNLOCKED_ICON = '[data-test-subj="timeline-date-picker-unlock-butto
export const NOTES = '[data-test-subj="note-card-body"]';
+export const NOTE_CARD_CONTENT = '[data-test-subj="notes"]';
+
+export const EVENT_NOTE = '[data-test-subj="timeline-notes-button-small"]';
+
export const NOTE_BY_NOTE_ID = (noteId: string) =>
`[data-test-subj="note-preview-${noteId}"] .euiMarkdownFormat`;
@@ -69,6 +73,12 @@ export const NOTES_TAB_BUTTON = '[data-test-subj="timelineTabs-notes"]';
export const NOTES_TEXT = '.euiMarkdownFormat';
+export const NOTES_CODE_BLOCK = '.euiCodeBlock__code';
+
+export const NOTES_AUTHOR = '.euiCommentEvent__headerUsername';
+
+export const NOTES_LINK = '[data-test-subj="markdown-link"]';
+
export const NOTES_COUNT = '[data-test-subj="timeline-notes-count"]';
export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]';
@@ -110,6 +120,8 @@ export const PINNED_TAB_EVENTS_BODY = '[data-test-subj="pinned-tab-flyout-body"]
export const PINNED_TAB_EVENTS_FOOTER = '[data-test-subj="pinned-tab-flyout-footer"]';
+export const QUERY_TAB_BUTTON = '[data-test-subj="timelineTabs-query"]';
+
export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]';
export const STAR_ICON = '[data-test-subj="timeline-favorite-empty-star"]';
@@ -118,6 +130,17 @@ export const TIMELINE_CHANGES_IN_PROGRESS = '[data-test-subj="timeline"] .euiPro
export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]';
+export const TIMELINE_COLLAPSED_ITEMS_BTN = '[data-test-subj="euiCollapsedItemActionsButton"]';
+
+export const TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN =
+ '[data-test-subj="create-template-from-timeline"]';
+
+export const TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN = '[data-test-subj="create-from-template"]';
+
+export const TIMELINE_CORRELATION_INPUT = '[data-test-subj="eqlQueryBarTextInput"]';
+
+export const TIMELINE_CORRELATION_TAB = '[data-test-subj="timelineTabs-eql"]';
+
export const IS_DRAGGING_DATA_PROVIDERS = '.is-dragging';
export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]';
@@ -143,6 +166,19 @@ export const TIMELINE_DESCRIPTION_INPUT = '[data-test-subj="save-timeline-descri
export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContainer"]';
+export const TIMELINE_EVENT = '[data-test-subj="event"]';
+
+export const TIMELINE_EVENTS_COUNT_PER_PAGE = '[data-test-subj="local-events-count"]';
+
+export const TIMELINE_EVENTS_COUNT_PER_PAGE_BTN = '[data-test-subj="local-events-count-button"]';
+
+export const TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION = (itemsPerPage: number) =>
+ `[data-test-subj="items-per-page-option-${itemsPerPage}"]`;
+
+export const TIMELINE_EVENTS_COUNT_NEXT_PAGE = '[data-test-subj="pagination-button-next"]';
+
+export const TIMELINE_EVENTS_COUNT_PREV_PAGE = '[data-test-subj="pagination-button-previous"]';
+
export const TIMELINE_FIELDS_BUTTON =
'[data-test-subj="timeline"] [data-test-subj="show-field-browser"]';
@@ -164,6 +200,8 @@ export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="query-tab-flyout-header"
export const TIMELINE_FLYOUT_BODY = '[data-test-subj="query-tab-flyout-body"]';
+export const TIMELINE_HEADER = '[data-test-subj="timeline-hide-show-container"]';
+
export const TIMELINE_INSPECT_BUTTON = `${TIMELINE_FLYOUT} [data-test-subj="inspect-icon-button"]`;
export const TIMELINE_PANEL = `[data-test-subj="timeline-flyout-header-panel"]`;
@@ -172,6 +210,14 @@ export const TIMELINE_QUERY = '[data-test-subj="timelineQueryInput"]';
export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-plus-in-circle"]';
+export const TIMELINE_SEARCH_OR_FILTER = '[data-test-subj="timeline-select-search-or-filter"]';
+
+export const TIMELINE_SEARCH_OR_FILTER_CONTENT = '.searchOrFilterPopover';
+
+export const TIMELINE_KQLMODE_SEARCH = '[data-test-subj="kqlModePopoverSearch"]';
+
+export const TIMELINE_KQLMODE_FILTER = '[data-test-subj="kqlModePopoverFilter"]';
+
export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]';
export const TIMELINE_TITLE_INPUT = '[data-test-subj="save-timeline-title"]';
@@ -186,4 +232,33 @@ export const TIMELINE_EDIT_MODAL_OPEN_BUTTON = '[data-test-subj="save-timeline-b
export const TIMELINE_EDIT_MODAL_SAVE_BUTTON = '[data-test-subj="save-button"]';
-export const QUERY_TAB_BUTTON = '[data-test-subj="timelineTabs-query"]';
+export const TIMELINE_EXIT_FULL_SCREEN_BUTTON = '[data-test-subj="exit-full-screen"]';
+
+export const TIMELINE_FLYOUT_WRAPPER = '[data-test-subj="flyout-pane-wrapper"]';
+
+export const TIMELINE_FULL_SCREEN_BUTTON = '[data-test-subj="full-screen-active"]';
+
+export const TIMELINE_ROW_RENDERERS_MODAL = '[data-test-subj="row-renderers-modal"]';
+
+export const TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN = `[data-test-subj="disable-all"]`;
+
+export const TIMELINE_ROW_RENDERERS_ENABLE_ALL_BTN = `button[data-test-subj="enable-alll"]`;
+
+export const TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON = `${TIMELINE_ROW_RENDERERS_MODAL} .euiModal__closeIcon`;
+
+export const TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX = `${TIMELINE_ROW_RENDERERS_MODAL} .euiCheckbox__input`;
+
+export const TIMELINE_ROW_RENDERERS_SEARCHBOX = `${TIMELINE_ROW_RENDERERS_MODAL} input[type="search"]`;
+
+export const TIMELINE_SHOW_ROW_RENDERERS_GEAR = '[data-test-subj="show-row-renderers-gear"]';
+
+export const TIMELINE_TABS = '[data-test-subj="timeline"] .euiTabs';
+
+export const TIMELINE_TAB_CONTENT_EQL = '[data-test-subj="timeline-tab-content-eql"]';
+
+export const TIMELINE_TAB_CONTENT_QUERY = '[data-test-subj="timeline-tab-content-query"]';
+
+export const TIMELINE_TAB_CONTENT_PINNED = '[data-test-subj="timeline-tab-content-pinned"]';
+
+export const TIMELINE_TAB_CONTENT_GRAPHS_NOTES =
+ '[data-test-subj="timeline-tab-content-graph-notes"]';
diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts
index 18359574633e9..8274d19f77a25 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/timelines.ts
@@ -119,3 +119,26 @@ export const loadPrepackagedTimelineTemplates = () =>
url: 'api/timeline/_prepackaged',
headers: { 'kbn-xsrf': 'cypress-creds' },
});
+
+export const favoriteTimeline = ({
+ timelineId,
+ timelineType,
+ templateTimelineId,
+ templateTimelineVersion,
+}: {
+ timelineId: string;
+ timelineType: string;
+ templateTimelineId?: string;
+ templateTimelineVersion?: number;
+}) =>
+ cy.request({
+ method: 'PATCH',
+ url: 'api/timeline/_favorite',
+ body: {
+ timelineId,
+ timelineType,
+ templateTimelineId: templateTimelineId || null,
+ templateTimelineVersion: templateTimelineVersion || null,
+ },
+ headers: { 'kbn-xsrf': 'cypress-creds' },
+ });
diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts
index 189ef1e46e4bc..01651b7b943d0 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts
@@ -11,6 +11,7 @@ import {
TIMELINE_TOGGLE_BUTTON,
TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON,
} from '../screens/security_main';
+import { TIMELINE_EXIT_FULL_SCREEN_BUTTON, TIMELINE_FULL_SCREEN_BUTTON } from '../screens/timeline';
export const openTimelineUsingToggle = () => {
cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).click();
@@ -30,3 +31,11 @@ export const openTimelineIfClosed = () =>
openTimelineUsingToggle();
}
});
+
+export const enterFullScreenMode = () => {
+ cy.get(TIMELINE_FULL_SCREEN_BUTTON).first().click({ force: true });
+};
+
+export const exitFullScreenMode = () => {
+ cy.get(TIMELINE_EXIT_FULL_SCREEN_BUTTON).first().click({ force: true });
+};
diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
index 587e4ec45b8c7..af7a7bb5d4c71 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
@@ -8,6 +8,7 @@
import { Timeline, TimelineFilter } from '../objects/timeline';
import { ALL_CASES_CREATE_NEW_CASE_TABLE_BTN } from '../screens/all_cases';
+import { LOADING_INDICATOR } from '../screens/security_header';
import {
ADD_FILTER,
@@ -56,6 +57,13 @@ import {
TIMELINE_DATA_PROVIDER_OPERATOR,
TIMELINE_DATA_PROVIDER_VALUE,
SAVE_DATA_PROVIDER_BTN,
+ EVENT_NOTE,
+ TIMELINE_CORRELATION_INPUT,
+ TIMELINE_CORRELATION_TAB,
+ TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN,
+ TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN,
+ TIMELINE_COLLAPSED_ITEMS_BTN,
+ TIMELINE_TAB_CONTENT_EQL,
} from '../screens/timeline';
import { REFRESH_BUTTON, TIMELINE } from '../screens/timelines';
@@ -99,6 +107,16 @@ export const goToNotesTab = (): Cypress.Chainable> => {
return cy.root().find(NOTES_TAB_BUTTON);
};
+export const goToCorrelationTab = () => {
+ cy.root()
+ .pipe(($el) => {
+ $el.find(TIMELINE_CORRELATION_TAB).trigger('click');
+ return $el.find(`${TIMELINE_TAB_CONTENT_EQL} ${TIMELINE_CORRELATION_INPUT}`);
+ })
+ .should('be.visible');
+ return cy.root().find(TIMELINE_CORRELATION_TAB);
+};
+
export const getNotePreviewByNoteId = (noteId: string) => {
return cy.get(`[data-test-subj="note-preview-${noteId}"]`);
};
@@ -127,6 +145,12 @@ export const addNotesToTimeline = (notes: string) => {
goToNotesTab();
};
+export const addEqlToTimeline = (eql: string) => {
+ goToCorrelationTab().then(() => {
+ cy.get(TIMELINE_CORRELATION_INPUT).type(eql);
+ });
+};
+
export const addFilter = (filter: TimelineFilter): Cypress.Chainable> => {
cy.get(ADD_FILTER).click();
cy.get(TIMELINE_FILTER_FIELD).type(`${filter.field}{downarrow}{enter}`);
@@ -140,7 +164,8 @@ export const addFilter = (filter: TimelineFilter): Cypress.Chainable> => {
cy.get(TIMELINE_ADD_FIELD_BUTTON).click();
- cy.wait(300);
+ cy.get(TIMELINE_DATA_PROVIDER_VALUE).should('have.focus'); // make sure the focus is ready before start typing
+
cy.get(TIMELINE_DATA_PROVIDER_FIELD).type(`${filter.field}{downarrow}{enter}`);
cy.get(TIMELINE_DATA_PROVIDER_OPERATOR).type(filter.operator);
cy.get(COMBO_BOX).contains(filter.operator).click();
@@ -209,8 +234,10 @@ export const expandFirstTimelineEventDetails = () => {
cy.get(TOGGLE_TIMELINE_EXPAND_EVENT).first().click({ force: true });
};
-export const markAsFavorite = (): Cypress.Chainable> => {
- return cy.get(STAR_ICON).click();
+export const markAsFavorite = () => {
+ const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click();
+ cy.get(STAR_ICON).should('be.visible').pipe(click);
+ cy.get(LOADING_INDICATOR).should('not.exist');
};
export const openTimelineFieldsBrowser = () => {
@@ -249,6 +276,15 @@ export const pinFirstEvent = (): Cypress.Chainable> => {
return cy.get(PIN_EVENT).first().click({ force: true });
};
+export const persistNoteToFirstEvent = (notes: string) => {
+ cy.get(EVENT_NOTE).first().click({ force: true });
+ cy.get(NOTES_TEXT_AREA).type(notes);
+ cy.root().pipe(($el) => {
+ $el.find(ADD_NOTE_BUTTON).trigger('click');
+ return $el.find(NOTES_TAB_BUTTON).find('.euiBadge');
+ });
+};
+
export const populateTimeline = () => {
executeTimelineKQL(hostExistsQuery);
cy.get(SERVER_SIDE_EVENT_COUNT).should('not.have.text', '0');
@@ -325,3 +361,15 @@ export const refreshTimelinesUntilTimeLinePresent = (
})
.should('be.visible');
};
+
+export const clickingOnCreateTimelineFormTemplateBtn = () => {
+ cy.get(TIMELINE_CREATE_TIMELINE_FROM_TEMPLATE_BTN).click({ force: true });
+};
+
+export const clickingOnCreateTemplateFromTimelineBtn = () => {
+ cy.get(TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN).click({ force: true });
+};
+
+export const expandEventAction = () => {
+ cy.get(TIMELINE_COLLAPSED_ITEMS_BTN).first().click();
+};
diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx
index ace78cec1a52f..ee12c12536af5 100644
--- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/recent_timelines.tsx
@@ -45,7 +45,11 @@ const RecentTimelinesItem = React.memo(
const render = useCallback(
(showHoverContent) => (
-
+
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx
index 2602ca3f3cc7c..ec46985450d89 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx
@@ -124,7 +124,7 @@ const FlyoutComponent: React.FC = ({ timelineId, onAppLeave }) => {
<>
-
+
diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx
index 4dcc799d79111..04237bfa43dc6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx
@@ -115,7 +115,7 @@ const StatefulRowRenderersBrowserComponent: React.FC
{show && (
-
+
= ({
{i18n.TIMELINE_TEMPLATE}
)}
-
+
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx
index d087b24239a66..9479c3209ad85 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/helpers.tsx
@@ -65,6 +65,7 @@ export const options = [
>
),
+ 'data-test-subj': 'kqlModePopoverFilter',
},
{
value: modes.search.mode,
@@ -84,6 +85,7 @@ export const options = [
>
),
+ 'data-test-subj': 'kqlModePopoverSearch',
},
];
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx
index 76a2ad0960322..adaa5f98c88c4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx
@@ -146,14 +146,20 @@ const ActiveTimelineTab = memo(
*/
return (
<>
-
+
-
+
(
/>
{timelineType === TimelineType.default && (
-
+
(
/>
)}
-
+
{isGraphOrNotesTabs && getTab(activeTimelineTab)}
>
From 6dc996229cbe752654d23c15b3803b1b284667ed Mon Sep 17 00:00:00 2001
From: Constance
Date: Mon, 21 Jun 2021 13:49:14 -0700
Subject: [PATCH 034/163] [App Search] Engines Overview polish pass (#102778)
* Split up engines vs. meta engines into separate panels
- per Davey's feedback from earlier UI passes
* DRY out manual header/spacing to reusable DataPanel component
+ update DataPanel icon typing to not error when passed a custom icon/svg
- kudos again to Davey for the component
* Typography tweaks
- Update DataPanel component to accept a custom titleSize (to maintain previous UI/sizing)
- Fix meta engines empty prompt title heading to follow heading levels + tweak sizing to not be larger than panel heading
* Set up new license CTA button for upcoming meta engines CTA
falls back to a documentation link! so fancy
* Update Enterprise Search Overview to use new license button
* Add new Meta Engines license upgrade CTA
- Reuse some copy from Meta Engines creation view
- Reuse DataPanel so visuals stay consistent + it looks similar to CTA on Enterprise Search Overview
- Update DataPanel to allow buttons to be responsive + conditionally load spacer between header & children
* Improve responsiveness of app when platinum license changes
Previously, routes/apps were going off the static data passed from the server which was only initialized once on page load. hasPlatinumLicense however changes dynamically and in real-time, removing the need for a hard page refresh.
I could have replaced all `canManageMetaEngine` flags with `isPlatinum && canManageEngines`, but I thought baking license checks into the main ability would be more scalable and potentially open the way to other license-based flags also being dynamic.
* [PR feedback] Typos in test names
Co-authored-by: Jason Stoltzfus
* Fix failing test
Missed updating the heading level
Co-authored-by: Jason Stoltzfus
---
.../kea_logic/licensing_logic.mock.ts | 3 +
.../applications/app_search/app_logic.test.ts | 6 +-
.../applications/app_search/app_logic.ts | 6 +-
.../components/data_panel/data_panel.test.tsx | 32 +++-
.../components/data_panel/data_panel.tsx | 22 ++-
.../empty_meta_engines_state.test.tsx | 2 +-
.../components/empty_meta_engines_state.tsx | 5 +-
.../engines/{constants.ts => constants.tsx} | 28 +++
.../engines/engines_overview.test.tsx | 54 ++++--
.../components/engines/engines_overview.tsx | 159 ++++++++----------
.../utils/role/get_role_abilities.test.ts | 31 +++-
.../utils/role/get_role_abilities.ts | 4 +-
.../components/license_callout/constants.ts | 7 -
.../license_callout/license_callout.test.tsx | 6 +-
.../license_callout/license_callout.tsx | 9 +-
.../public/applications/index.tsx | 1 +
.../applications/shared/licensing/index.ts | 1 +
.../shared/licensing/licensing_logic.test.ts | 12 +-
.../shared/licensing/licensing_logic.ts | 7 +-
.../licensing/manage_license_button.test.tsx | 42 +++++
.../licensing/manage_license_button.tsx | 41 +++++
21 files changed, 337 insertions(+), 141 deletions(-)
rename x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/{constants.ts => constants.tsx} (63%)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts
index 2cea6061b63ab..f227928b45821 100644
--- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts
@@ -11,8 +11,11 @@ export const mockLicensingValues = {
license: licensingMock.createLicense(),
hasPlatinumLicense: false,
hasGoldLicense: false,
+ isTrial: false,
+ canManageLicense: true,
};
jest.mock('../../shared/licensing', () => ({
+ ...(jest.requireActual('../../shared/licensing') as object),
LicensingLogic: { values: mockLicensingValues },
}));
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts
index 7b08e82a4cf20..f69e3492d26eb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts
@@ -6,7 +6,11 @@
*/
import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__';
-import { LogicMounter } from '../__mocks__/kea_logic';
+import { LogicMounter } from '../__mocks__/kea_logic/logic_mounter.test_helper';
+
+jest.mock('../shared/licensing', () => ({
+ LicensingLogic: { selectors: { hasPlatinumLicense: () => false } },
+}));
import { AppLogic } from './app_logic';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts
index 44416b596e6ef..90b37e6a4d4ee 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts
@@ -9,6 +9,8 @@ import { kea, MakeLogicType } from 'kea';
import { InitialAppData } from '../../../common/types';
+import { LicensingLogic } from '../shared/licensing';
+
import { ConfiguredLimits, Account, Role } from './types';
import { getRoleAbilities } from './utils/role';
@@ -43,8 +45,8 @@ export const AppLogic = kea [selectors.account],
- ({ role }) => (role ? getRoleAbilities(role) : {}),
+ (selectors) => [selectors.account, LicensingLogic.selectors.hasPlatinumLicense],
+ ({ role }, hasPlatinumLicense) => (role ? getRoleAbilities(role, hasPlatinumLicense) : {}),
],
},
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx
index 8034b72d885da..04f05349217c0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiIcon, EuiButton } from '@elastic/eui';
+import { EuiIcon, EuiButton, EuiTitle, EuiFlexGroup, EuiSpacer } from '@elastic/eui';
import { LoadingOverlay } from '../../../shared/loading';
@@ -27,6 +27,16 @@ describe('DataPanel', () => {
expect(wrapper.find('[data-test-subj="children"]').text()).toEqual('Look at this graph');
});
+ it('conditionally renders a spacer between the header and children', () => {
+ const wrapper = shallow(Test} />);
+
+ expect(wrapper.find(EuiSpacer)).toHaveLength(0);
+
+ wrapper.setProps({ children: 'hello world' });
+
+ expect(wrapper.find(EuiSpacer)).toHaveLength(1);
+ });
+
describe('components', () => {
it('renders with an icon', () => {
const wrapper = shallow(The Smoke Monster} iconType="eye" />);
@@ -70,6 +80,26 @@ describe('DataPanel', () => {
});
describe('props', () => {
+ it('passes titleSize to the title', () => {
+ const wrapper = shallow(Test} />);
+
+ expect(wrapper.find(EuiTitle).prop('size')).toEqual('xs'); // Default
+
+ wrapper.setProps({ titleSize: 's' });
+
+ expect(wrapper.find(EuiTitle).prop('size')).toEqual('s');
+ });
+
+ it('passes responsive to the header flex group', () => {
+ const wrapper = shallow(Test} />);
+
+ expect(wrapper.find(EuiFlexGroup).first().prop('responsive')).toEqual(false);
+
+ wrapper.setProps({ responsive: true });
+
+ expect(wrapper.find(EuiFlexGroup).first().prop('responsive')).toEqual(true);
+ });
+
it('renders panel color based on filled flag', () => {
const wrapper = shallow(Test} />);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx
index ce878dc3cf29a..4b22fbc93d411 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx
@@ -13,10 +13,12 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
+ EuiIconProps,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
+ EuiTitleProps,
} from '@elastic/eui';
import { LoadingOverlay } from '../../../shared/loading';
@@ -25,9 +27,11 @@ import './data_panel.scss';
interface Props {
title: React.ReactElement; // e.g., h2 tag
- subtitle?: string;
- iconType?: string;
+ titleSize?: EuiTitleProps['size'];
+ subtitle?: React.ReactNode;
+ iconType?: EuiIconProps['type'];
action?: React.ReactNode;
+ responsive?: boolean;
filled?: boolean;
hasBorder?: boolean;
isLoading?: boolean;
@@ -36,9 +40,11 @@ interface Props {
export const DataPanel: React.FC = ({
title,
+ titleSize = 'xs',
subtitle,
iconType,
action,
+ responsive = false,
filled,
hasBorder,
isLoading,
@@ -59,7 +65,7 @@ export const DataPanel: React.FC = ({
hasShadow={false}
aria-busy={isLoading}
>
-
+
{iconType && (
@@ -68,7 +74,7 @@ export const DataPanel: React.FC = ({
)}
- {title}
+ {title}
{subtitle && (
@@ -79,8 +85,12 @@ export const DataPanel: React.FC = ({
{action && {action} }
-
- {children}
+ {children && (
+ <>
+
+ {children}
+ >
+ )}
{isLoading && }
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx
index 1eab32d64b77f..8b4f5a69b8141 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.test.tsx
@@ -19,7 +19,7 @@ describe('EmptyMetaEnginesState', () => {
.find(EuiEmptyPrompt)
.dive();
- expect(wrapper.find('h2').text()).toEqual('Create your first meta engine');
+ expect(wrapper.find('h3').text()).toEqual('Create your first meta engine');
expect(wrapper.find(EuiButton).prop('href')).toEqual(
expect.stringContaining('/meta-engines-guide.html')
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx
index 58bf3f0a0195e..ad96f21022f2b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_meta_engines_state.tsx
@@ -15,12 +15,13 @@ import { DOCS_PREFIX } from '../../../routes';
export const EmptyMetaEnginesState: React.FC = () => (
+
{i18n.translate('xpack.enterpriseSearch.appSearch.engines.metaEngines.emptyPromptTitle', {
defaultMessage: 'Create your first meta engine',
})}
-
+
}
+ titleSize="s"
body={
{i18n.translate(
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx
similarity index 63%
rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts
rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx
index d01e89e004d28..223c33f9b9592 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.tsx
@@ -5,7 +5,17 @@
* 2.0.
*/
+import React from 'react';
+
+import { EuiLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { DOCS_PREFIX } from '../../routes';
+import {
+ META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION,
+ META_ENGINE_CREATION_FORM_DOCUMENTATION_LINK,
+} from '../meta_engine_creation/constants';
export const ENGINES_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.engines.title', {
defaultMessage: 'Engines',
@@ -21,6 +31,24 @@ export const META_ENGINES_TITLE = i18n.translate(
{ defaultMessage: 'Meta Engines' }
);
+export const META_ENGINES_DESCRIPTION = (
+ <>
+ {META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION}
+
+
+ {META_ENGINE_CREATION_FORM_DOCUMENTATION_LINK}
+
+ ),
+ }}
+ />
+ >
+);
+
export const SOURCE_ENGINES_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.enginesOverview.metaEnginesTable.sourceEngines.title',
{ defaultMessage: 'Source Engines' }
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx
index 8825c322fb8d5..a90e1369593d9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx
@@ -42,7 +42,7 @@ describe('EnginesOverview', () => {
metaEnginesLoading: false,
hasPlatinumLicense: false,
// AppLogic
- myRole: { canManageEngines: false },
+ myRole: { canManageEngines: false, canManageMetaEngines: false },
// MetaEnginesTableLogic
expandedSourceEngines: {},
conflictingEnginesSets: {},
@@ -85,17 +85,25 @@ describe('EnginesOverview', () => {
expect(actions.loadEngines).toHaveBeenCalled();
});
- describe('when the user can manage/create engines', () => {
- it('renders a create engine button which takes users to the create engine page', () => {
+ describe('engine creation', () => {
+ it('renders a create engine action when the users can create engines', () => {
setMockValues({
...valuesWithEngines,
myRole: { canManageEngines: true },
});
const wrapper = shallow( );
- expect(
- wrapper.find('[data-test-subj="appSearchEnginesEngineCreationButton"]').prop('to')
- ).toEqual('/engine_creation');
+ expect(wrapper.find('[data-test-subj="appSearchEngines"]').prop('action')).toBeTruthy();
+ });
+
+ it('does not render a create engine action if the user cannot create engines', () => {
+ setMockValues({
+ ...valuesWithEngines,
+ myRole: { canManageEngines: false },
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find('[data-test-subj="appSearchEngines"]').prop('action')).toBeFalsy();
});
});
@@ -111,19 +119,41 @@ describe('EnginesOverview', () => {
expect(actions.loadMetaEngines).toHaveBeenCalled();
});
- describe('when the user can manage/create engines', () => {
- it('renders a create engine button which takes users to the create meta engine page', () => {
+ describe('meta engine creation', () => {
+ it('renders a create meta engine action when the user can create meta engines', () => {
setMockValues({
...valuesWithEngines,
hasPlatinumLicense: true,
- myRole: { canManageEngines: true },
+ myRole: { canManageMetaEngines: true },
});
const wrapper = shallow( );
- expect(
- wrapper.find('[data-test-subj="appSearchEnginesMetaEngineCreationButton"]').prop('to')
- ).toEqual('/meta_engine_creation');
+ expect(wrapper.find('[data-test-subj="appSearchMetaEngines"]').prop('action')).toBeTruthy();
});
+
+ it('does not render a create meta engine action if user cannot create meta engines', () => {
+ setMockValues({
+ ...valuesWithEngines,
+ hasPlatinumLicense: true,
+ myRole: { canManageMetaEngines: false },
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find('[data-test-subj="appSearchMetaEngines"]').prop('action')).toBeFalsy();
+ });
+ });
+ });
+
+ describe('when an account does not have a platinum license', () => {
+ it('renders a license call to action in place of the meta engines table', () => {
+ setMockValues({
+ ...valuesWithEngines,
+ hasPlatinumLicense: false,
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find('[data-test-subj="metaEnginesLicenseCTA"]')).toHaveLength(1);
+ expect(wrapper.find('[data-test-subj="appSearchMetaEngines"]')).toHaveLength(0);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx
index 44111a5ecbe66..4dff246052138 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx
@@ -9,23 +9,15 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiPanel,
- EuiPageContentHeader,
- EuiPageContentHeaderSection,
- EuiPageContentBody,
- EuiTitle,
- EuiSpacer,
-} from '@elastic/eui';
+import { EuiSpacer } from '@elastic/eui';
-import { LicensingLogic } from '../../../shared/licensing';
+import { LicensingLogic, ManageLicenseButton } from '../../../shared/licensing';
import { EuiButtonTo } from '../../../shared/react_router_helpers';
import { convertMetaToPagination, handlePageChange } from '../../../shared/table_pagination';
import { AppLogic } from '../../app_logic';
import { EngineIcon, MetaEngineIcon } from '../../icons';
import { ENGINE_CREATION_PATH, META_ENGINE_CREATION_PATH } from '../../routes';
+import { DataPanel } from '../data_panel';
import { AppSearchPageTemplate } from '../layout';
import { LaunchAppSearchButton, EmptyState, EmptyMetaEnginesState } from './components';
@@ -37,13 +29,14 @@ import {
CREATE_A_META_ENGINE_BUTTON_LABEL,
ENGINES_TITLE,
META_ENGINES_TITLE,
+ META_ENGINES_DESCRIPTION,
} from './constants';
import { EnginesLogic } from './engines_logic';
export const EnginesOverview: React.FC = () => {
const { hasPlatinumLicense } = useValues(LicensingLogic);
const {
- myRole: { canManageEngines },
+ myRole: { canManageEngines, canManageMetaEngines },
} = useValues(AppLogic);
const {
@@ -80,93 +73,81 @@ export const EnginesOverview: React.FC = () => {
isEmptyState={!engines.length}
emptyState={ }
>
-
-
-
-
-
-
-
-
-
- {ENGINES_TITLE}
-
-
-
-
-
- {canManageEngines && (
+ {ENGINES_TITLE}}
+ titleSize="s"
+ action={
+ canManageEngines && (
+
+ {CREATE_AN_ENGINE_BUTTON_LABEL}
+
+ )
+ }
+ data-test-subj="appSearchEngines"
+ >
+
+
+
+ {hasPlatinumLicense ? (
+ {META_ENGINES_TITLE}}
+ titleSize="s"
+ action={
+ canManageMetaEngines && (
- {CREATE_AN_ENGINE_BUTTON_LABEL}
+ {CREATE_A_META_ENGINE_BUTTON_LABEL}
- )}
-
-
-
-
-
+ }
+ onChange={handlePageChange(onMetaEnginesPagination)}
/>
-
-
- {hasPlatinumLicense && (
- <>
-
-
-
-
-
-
-
-
-
- {META_ENGINES_TITLE}
-
-
-
-
-
- {canManageEngines && (
-
- {CREATE_A_META_ENGINE_BUTTON_LABEL}
-
- )}
-
-
-
-
- }
- onChange={handlePageChange(onMetaEnginesPagination)}
- />
-
- >
- )}
-
+
+ ) : (
+ {META_ENGINES_TITLE}}
+ titleSize="s"
+ subtitle={META_ENGINES_DESCRIPTION}
+ action={ }
+ data-test-subj="metaEnginesLicenseCTA"
+ />
+ )}
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts
index 4d4c84e4146ef..60d0dcc0c5911 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.test.ts
@@ -10,7 +10,7 @@ import { DEFAULT_INITIAL_APP_DATA } from '../../../../../common/__mocks__';
import { getRoleAbilities } from './';
describe('getRoleAbilities', () => {
- const mockRole = DEFAULT_INITIAL_APP_DATA.appSearch.role;
+ const mockRole = DEFAULT_INITIAL_APP_DATA.appSearch.role as any;
it('transforms server role data into a flat role obj with helper shorthands', () => {
expect(getRoleAbilities(mockRole)).toEqual({
@@ -53,9 +53,10 @@ describe('getRoleAbilities', () => {
describe('can()', () => {
it('sets view abilities to true if manage abilities are true', () => {
- const role = { ...mockRole };
- role.ability.view = [];
- role.ability.manage = ['account_settings'];
+ const role = {
+ ...mockRole,
+ ability: { view: [], manage: ['account_settings'] },
+ };
const myRole = getRoleAbilities(role);
@@ -70,4 +71,26 @@ describe('getRoleAbilities', () => {
expect(myRole.can('edit', 'fakeSubject')).toEqual(false);
});
});
+
+ describe('canManageMetaEngines', () => {
+ const canManageEngines = { ability: { manage: ['account_engines'] } };
+
+ it('returns true when the user can manage any engines and the account has a platinum license', () => {
+ const myRole = getRoleAbilities({ ...mockRole, ...canManageEngines }, true);
+
+ expect(myRole.canManageMetaEngines).toEqual(true);
+ });
+
+ it('returns false when the user can manage any engines but the account does not have a platinum license', () => {
+ const myRole = getRoleAbilities({ ...mockRole, ...canManageEngines }, false);
+
+ expect(myRole.canManageMetaEngines).toEqual(false);
+ });
+
+ it('returns false when has a platinum license but the user cannot manage any engines', () => {
+ const myRole = getRoleAbilities({ ...mockRole, ability: { manage: [] } }, true);
+
+ expect(myRole.canManageMetaEngines).toEqual(false);
+ });
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts
index 81ac971d00d44..ef3e22d851f38 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/get_role_abilities.ts
@@ -13,7 +13,7 @@ import { RoleTypes, AbilityTypes, Role } from './types';
* Transforms the `role` data we receive from the Enterprise Search
* server into a more convenient format for front-end use
*/
-export const getRoleAbilities = (role: Account['role']): Role => {
+export const getRoleAbilities = (role: Account['role'], hasPlatinumLicense = false): Role => {
// Role ability function helpers
const myRole = {
can: (action: AbilityTypes, subject: string): boolean => {
@@ -49,7 +49,7 @@ export const getRoleAbilities = (role: Account['role']): Role => {
canViewSettings: myRole.can('view', 'account_settings'),
canViewRoleMappings: myRole.can('view', 'role_mappings'),
canManageEngines: myRole.can('manage', 'account_engines'),
- canManageMetaEngines: myRole.can('manage', 'account_meta_engines'),
+ canManageMetaEngines: hasPlatinumLicense && myRole.can('manage', 'account_engines'),
canManageLogSettings: myRole.can('manage', 'account_log_settings'),
canManageSettings: myRole.can('manage', 'account_settings'),
canManageEngineCrawler: myRole.can('manage', 'engine_crawler'),
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts
index 903d1768f3cc1..f51eeb1c8160c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts
@@ -11,10 +11,3 @@ export const LICENSE_CALLOUT_BODY = i18n.translate('xpack.enterpriseSearch.licen
defaultMessage:
'Enterprise authentication via SAML, document-level permission and authorization support, custom search experiences and more are available with a valid Platinum license.',
});
-
-export const LICENSE_CALLOUT_BUTTON = i18n.translate(
- 'xpack.enterpriseSearch.licenseCalloutButton',
- {
- defaultMessage: 'Manage your license',
- }
-);
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx
index 0c77a0fbf6f5a..75a9700691ebb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx
@@ -13,7 +13,7 @@ import { shallow } from 'enzyme';
import { EuiPanel, EuiText } from '@elastic/eui';
-import { EuiButtonTo } from '../../../shared/react_router_helpers';
+import { ManageLicenseButton } from '../../../shared/licensing';
import { LicenseCallout } from './';
@@ -27,9 +27,7 @@ describe('LicenseCallout', () => {
expect(wrapper.find(EuiPanel)).toHaveLength(1);
expect(wrapper.find(EuiText)).toHaveLength(2);
- expect(wrapper.find(EuiButtonTo).prop('to')).toEqual(
- '/app/management/stack/license_management'
- );
+ expect(wrapper.find(ManageLicenseButton)).toHaveLength(1);
});
it('does not render for platinum', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx
index 4a4de17450f1b..f9f329c859110 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx
@@ -11,12 +11,11 @@ import { useValues } from 'kea';
import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
-import { LicensingLogic } from '../../../shared/licensing';
-import { EuiButtonTo } from '../../../shared/react_router_helpers';
+import { LicensingLogic, ManageLicenseButton } from '../../../shared/licensing';
import { PRODUCT_SELECTOR_CALLOUT_HEADING } from '../../constants';
-import { LICENSE_CALLOUT_BODY, LICENSE_CALLOUT_BUTTON } from './constants';
+import { LICENSE_CALLOUT_BODY } from './constants';
export const LicenseCallout: React.FC = () => {
const { hasPlatinumLicense, isTrial } = useValues(LicensingLogic);
@@ -34,9 +33,7 @@ export const LicenseCallout: React.FC = () => {
-
- {LICENSE_CALLOUT_BUTTON}
-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx
index ba2b28e64b9cf..414957656467a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx
@@ -57,6 +57,7 @@ export const renderApp = (
});
const unmountLicensingLogic = mountLicensingLogic({
license$: plugins.licensing.license$,
+ canManageLicense: core.application.capabilities.management?.stack?.license_management,
});
const unmountHttpLogic = mountHttpLogic({
http: core.http,
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts
index c83e578bdd090..74281d45ae0a5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts
@@ -6,3 +6,4 @@
*/
export { LicensingLogic, mountLicensingLogic } from './licensing_logic';
+export { ManageLicenseButton } from './manage_license_button';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts
index 4ea74e1c0d4f2..5d210cee1a926 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts
@@ -15,13 +15,21 @@ import { LicensingLogic, mountLicensingLogic } from './licensing_logic';
describe('LicensingLogic', () => {
const mockLicense = licensingMock.createLicense();
const mockLicense$ = new BehaviorSubject(mockLicense);
- const mount = () => mountLicensingLogic({ license$: mockLicense$ });
+ const mount = (props?: object) =>
+ mountLicensingLogic({ license$: mockLicense$, canManageLicense: true, ...props });
beforeEach(() => {
jest.clearAllMocks();
resetContext({});
});
+ describe('canManageLicense', () => {
+ it('sets value from props', () => {
+ mount({ canManageLicense: false });
+ expect(LicensingLogic.values.canManageLicense).toEqual(false);
+ });
+ });
+
describe('setLicense()', () => {
it('sets license value', () => {
mount();
@@ -61,7 +69,7 @@ describe('LicensingLogic', () => {
describe('on unmount', () => {
it('unsubscribes to the license observable', () => {
const mockUnsubscribe = jest.fn();
- const unmount = mountLicensingLogic({
+ const unmount = mount({
license$: { subscribe: () => ({ unsubscribe: mockUnsubscribe }) } as any,
});
unmount();
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts
index 7d0222f476214..f94a1fff0cd31 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts
@@ -16,6 +16,7 @@ interface LicensingValues {
hasPlatinumLicense: boolean;
hasGoldLicense: boolean;
isTrial: boolean;
+ canManageLicense: boolean;
}
interface LicensingActions {
setLicense(license: ILicense): ILicense;
@@ -28,7 +29,7 @@ export const LicensingLogic = kea license,
setLicenseSubscription: (licenseSubscription) => licenseSubscription,
},
- reducers: {
+ reducers: ({ props }) => ({
license: [
null,
{
@@ -41,7 +42,8 @@ export const LicensingLogic = kea licenseSubscription,
},
],
- },
+ canManageLicense: [props.canManageLicense || false, {}],
+ }),
selectors: {
hasPlatinumLicense: [
(selectors) => [selectors.license],
@@ -80,6 +82,7 @@ export const LicensingLogic = kea;
+ canManageLicense: boolean;
}
export const mountLicensingLogic = (props: LicensingLogicProps) => {
LicensingLogic(props);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx
new file mode 100644
index 0000000000000..1877a4cbd0e42
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.test.tsx
@@ -0,0 +1,42 @@
+/*
+ * 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 { setMockValues } from '../../__mocks__/kea_logic';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiButton } from '@elastic/eui';
+
+import { EuiButtonTo } from '../react_router_helpers';
+
+import { ManageLicenseButton } from './';
+
+describe('ManageLicenseButton', () => {
+ describe('when the user can access license management', () => {
+ it('renders a SPA link to the license management plugin', () => {
+ setMockValues({ canManageLicense: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiButtonTo).prop('to')).toEqual(
+ '/app/management/stack/license_management'
+ );
+ });
+ });
+
+ describe('when the user cannot access license management', () => {
+ it('renders an external link to our license management documentation', () => {
+ setMockValues({ canManageLicense: false });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiButton).prop('href')).toEqual(
+ expect.stringContaining('/license-management.html')
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx
new file mode 100644
index 0000000000000..af3b33e3d7a3d
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/manage_license_button.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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 { useValues } from 'kea';
+
+import { EuiButton, EuiButtonProps } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { docLinks } from '../doc_links';
+import { EuiButtonTo } from '../react_router_helpers';
+
+import { LicensingLogic } from './licensing_logic';
+
+export const ManageLicenseButton: React.FC = (props) => {
+ const { canManageLicense } = useValues(LicensingLogic);
+
+ return canManageLicense ? (
+
+ {i18n.translate('xpack.enterpriseSearch.licenseManagementLink', {
+ defaultMessage: 'Manage your license',
+ })}
+
+ ) : (
+
+ {i18n.translate('xpack.enterpriseSearch.licenseDocumentationLink', {
+ defaultMessage: 'Learn more about license features',
+ })}
+
+ );
+};
From f2ca7fcb96b5b6fdea4cf64ec985c9d2623b8b54 Mon Sep 17 00:00:00 2001
From: Constance
Date: Mon, 21 Jun 2021 14:05:18 -0700
Subject: [PATCH 035/163] [App Search] Convert Settings & Credentials pages to
new page template (#102671)
* Convert Settings to new page template
+ add missing ability check around route
* Convert Credentials to new page template
+ add missing ability check around route
* [Tests refactor] DRY out repeated ability tests to a helper
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../credentials/credentials.test.tsx | 5 +-
.../components/credentials/credentials.tsx | 133 +++++++++---------
.../log_retention/log_retention_panel.tsx | 14 +-
.../components/settings/settings.test.tsx | 4 +-
.../components/settings/settings.tsx | 20 +--
.../applications/app_search/index.test.tsx | 66 +++------
.../public/applications/app_search/index.tsx | 24 +++-
7 files changed, 124 insertions(+), 142 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx
index 286658c011002..737908752911d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx
@@ -12,7 +12,7 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiCopy, EuiLoadingContent, EuiPageContentBody } from '@elastic/eui';
+import { EuiCopy, EuiLoadingContent } from '@elastic/eui';
import { DEFAULT_META } from '../../../shared/constants';
import { externalUrl } from '../../../shared/enterprise_search_url';
@@ -20,6 +20,7 @@ import { externalUrl } from '../../../shared/enterprise_search_url';
import { Credentials } from './credentials';
import { CredentialsFlyout } from './credentials_flyout';
+import { CredentialsList } from './credentials_list';
describe('Credentials', () => {
// Kea mocks
@@ -42,7 +43,7 @@ describe('Credentials', () => {
it('renders', () => {
const wrapper = shallow( );
- expect(wrapper.find(EuiPageContentBody)).toHaveLength(1);
+ expect(wrapper.find(CredentialsList)).toHaveLength(1);
});
it('fetches data on mount', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx
index 8918445982ea6..f81d8d64737df 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx
@@ -10,9 +10,7 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
import {
- EuiPageHeader,
EuiTitle,
- EuiPageContentBody,
EuiPanel,
EuiCopy,
EuiButtonIcon,
@@ -25,8 +23,7 @@ import {
import { i18n } from '@kbn/i18n';
import { externalUrl } from '../../../shared/enterprise_search_url/external_url';
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
+import { AppSearchPageTemplate } from '../layout';
import { CREDENTIALS_TITLE } from './constants';
import { CredentialsFlyout } from './credentials_flyout';
@@ -52,74 +49,72 @@ export const Credentials: React.FC = () => {
}, []);
return (
- <>
-
-
-
- {shouldShowCredentialsForm && }
-
-
+
+ {shouldShowCredentialsForm && }
+
+
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiEndpoint', {
+ defaultMessage: 'Endpoint',
+ })}
+
+
+
+ {(copy) => (
+ <>
+
+ {externalUrl.enterpriseSearchUrl}
+ >
+ )}
+
+
+
+
+
+
- {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiEndpoint', {
- defaultMessage: 'Endpoint',
+ {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiKeys', {
+ defaultMessage: 'API Keys',
})}
-
- {(copy) => (
- <>
-
- {externalUrl.enterpriseSearchUrl}
- >
- )}
-
-
-
-
-
-
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiKeys', {
- defaultMessage: 'API Keys',
- })}
-
-
-
-
- {!dataLoading && (
- showCredentialsForm()}
- >
- {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.createKey', {
- defaultMessage: 'Create a key',
- })}
-
- )}
-
-
-
-
-
- {!!dataLoading ? : }
-
-
- >
+
+
+ {!dataLoading && (
+ showCredentialsForm()}
+ >
+ {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.createKey', {
+ defaultMessage: 'Create a key',
+ })}
+
+ )}
+
+
+
+
+ {!!dataLoading ? : }
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx
index 76fdcdac58ad4..fb4b503c7e62c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx
@@ -9,7 +9,15 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
-import { EuiLink, EuiSpacer, EuiSwitch, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui';
+import {
+ EuiPanel,
+ EuiLink,
+ EuiSpacer,
+ EuiSwitch,
+ EuiText,
+ EuiTextColor,
+ EuiTitle,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DOCS_PREFIX } from '../../../routes';
@@ -30,7 +38,7 @@ export const LogRetentionPanel: React.FC = () => {
}, []);
return (
-
+
{i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.title', {
@@ -104,6 +112,6 @@ export const LogRetentionPanel: React.FC = () => {
data-test-subj="LogRetentionPanelAPISwitch"
/>
-
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx
index 41d446b8e36fc..1ad12856a92e1 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx
@@ -9,13 +9,13 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPageContentBody } from '@elastic/eui';
+import { LogRetentionPanel } from './log_retention';
import { Settings } from './settings';
describe('Settings', () => {
it('renders', () => {
const wrapper = shallow( );
- expect(wrapper.find(EuiPageContentBody)).toHaveLength(1);
+ expect(wrapper.find(LogRetentionPanel)).toHaveLength(1);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx
index 2d5dd08f81288..ddbf046d75ec1 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx
@@ -7,10 +7,7 @@
import React from 'react';
-import { EuiPageHeader, EuiPageContent, EuiPageContentBody } from '@elastic/eui';
-
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
+import { AppSearchPageTemplate } from '../layout';
import { LogRetentionPanel, LogRetentionConfirmationModal } from './log_retention';
@@ -18,16 +15,9 @@ import { SETTINGS_TITLE } from './';
export const Settings: React.FC = () => {
return (
- <>
-
-
-
-
-
-
-
-
-
- >
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx
index 4d8ff80326715..2402a6ecc6401 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx
@@ -24,6 +24,7 @@ import { rerender } from '../test_helpers';
jest.mock('./app_logic', () => ({ AppLogic: jest.fn() }));
import { AppLogic } from './app_logic';
+import { Credentials } from './components/credentials';
import { EngineRouter, EngineNav } from './components/engine';
import { EngineCreation } from './components/engine_creation';
import { EnginesOverview } from './components/engines';
@@ -31,6 +32,7 @@ import { ErrorConnecting } from './components/error_connecting';
import { Library } from './components/library';
import { MetaEngineCreation } from './components/meta_engine_creation';
import { RoleMappings } from './components/role_mappings';
+import { Settings } from './components/settings';
import { SetupGuide } from './components/setup_guide';
import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './';
@@ -103,52 +105,28 @@ describe('AppSearchConfigured', () => {
expect(wrapper.find(Layout).first().prop('readOnlyMode')).toEqual(true);
});
- describe('ability checks', () => {
- describe('canViewRoleMappings', () => {
- it('renders RoleMappings when canViewRoleMappings is true', () => {
- setMockValues({ myRole: { canViewRoleMappings: true } });
- rerender(wrapper);
- expect(wrapper.find(RoleMappings)).toHaveLength(1);
+ describe('routes with ability checks', () => {
+ const runRouteAbilityCheck = (routeAbility: string, View: React.FC) => {
+ describe(View.name, () => {
+ it(`renders ${View.name} when user ${routeAbility} is true`, () => {
+ setMockValues({ myRole: { [routeAbility]: true } });
+ rerender(wrapper);
+ expect(wrapper.find(View)).toHaveLength(1);
+ });
+
+ it(`does not render ${View.name} when user ${routeAbility} is false`, () => {
+ setMockValues({ myRole: { [routeAbility]: false } });
+ rerender(wrapper);
+ expect(wrapper.find(View)).toHaveLength(0);
+ });
});
+ };
- it('does not render RoleMappings when user canViewRoleMappings is false', () => {
- setMockValues({ myRole: { canManageEngines: false } });
- rerender(wrapper);
- expect(wrapper.find(RoleMappings)).toHaveLength(0);
- });
- });
-
- describe('canManageEngines', () => {
- it('renders EngineCreation when user canManageEngines is true', () => {
- setMockValues({ myRole: { canManageEngines: true } });
- rerender(wrapper);
-
- expect(wrapper.find(EngineCreation)).toHaveLength(1);
- });
-
- it('does not render EngineCreation when user canManageEngines is false', () => {
- setMockValues({ myRole: { canManageEngines: false } });
- rerender(wrapper);
-
- expect(wrapper.find(EngineCreation)).toHaveLength(0);
- });
- });
-
- describe('canManageMetaEngines', () => {
- it('renders MetaEngineCreation when user canManageMetaEngines is true', () => {
- setMockValues({ myRole: { canManageMetaEngines: true } });
- rerender(wrapper);
-
- expect(wrapper.find(MetaEngineCreation)).toHaveLength(1);
- });
-
- it('does not render MetaEngineCreation when user canManageMetaEngines is false', () => {
- setMockValues({ myRole: { canManageMetaEngines: false } });
- rerender(wrapper);
-
- expect(wrapper.find(MetaEngineCreation)).toHaveLength(0);
- });
- });
+ runRouteAbilityCheck('canViewSettings', Settings);
+ runRouteAbilityCheck('canViewAccountCredentials', Credentials);
+ runRouteAbilityCheck('canViewRoleMappings', RoleMappings);
+ runRouteAbilityCheck('canManageEngines', EngineCreation);
+ runRouteAbilityCheck('canManageMetaEngines', MetaEngineCreation);
});
describe('library', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
index d724371cf1dc6..7b3b13aef05d6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
@@ -76,7 +76,13 @@ export const AppSearchUnconfigured: React.FC = () => (
export const AppSearchConfigured: React.FC> = (props) => {
const {
- myRole: { canManageEngines, canManageMetaEngines, canViewRoleMappings },
+ myRole: {
+ canManageEngines,
+ canManageMetaEngines,
+ canViewSettings,
+ canViewAccountCredentials,
+ canViewRoleMappings,
+ },
} = useValues(AppLogic(props));
const { renderHeaderActions } = useValues(KibanaLogic);
const { readOnlyMode } = useValues(HttpLogic);
@@ -111,6 +117,16 @@ export const AppSearchConfigured: React.FC> = (props) =
)}
+ {canViewSettings && (
+
+
+
+ )}
+ {canViewAccountCredentials && (
+
+
+
+ )}
{canViewRoleMappings && (
@@ -119,12 +135,6 @@ export const AppSearchConfigured: React.FC> = (props) =
} readOnlyMode={readOnlyMode}>
-
-
-
-
-
-
From 3673019906784679af728c47dd27a1bf707bc3e9 Mon Sep 17 00:00:00 2001
From: Constance
Date: Mon, 21 Jun 2021 14:37:24 -0700
Subject: [PATCH 036/163] [App Search] Convert Documents views to new page
template + minor UI polish (#102807)
* Convert Documents view to new page template
* [UI polish] Move empty state to top-level instead of showing full UI
- per Davey's previous approval
* [UX polish] Show loading indicator on initial documents page load
* Convert single Document detail view to new page template
* Update router
---
.../documents/components/empty_state.tsx | 66 +++++++++----------
.../documents/document_detail.test.tsx | 21 ++----
.../components/documents/document_detail.tsx | 44 +++++--------
.../components/documents/documents.test.tsx | 14 ++--
.../components/documents/documents.tsx | 33 +++++-----
.../search_experience/search_experience.scss | 1 +
.../search_experience.test.tsx | 10 ---
.../search_experience/search_experience.tsx | 7 +-
.../search_experience_content.test.tsx | 5 +-
.../search_experience_content.tsx | 3 +-
.../components/engine/engine_router.tsx | 20 +++---
11 files changed, 90 insertions(+), 134 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx
index 0f9455a3b9228..39fe02a84854c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/empty_state.tsx
@@ -7,43 +7,41 @@
import React from 'react';
-import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui';
+import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DOCS_PREFIX } from '../../../routes';
export const EmptyState = () => (
-
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.title', {
- defaultMessage: 'Add your first documents',
- })}
-
- }
- body={
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.description', {
- defaultMessage:
- 'You can index documents using the App Search Web Crawler, by uploading JSON, or by using the API.',
- })}
-
- }
- actions={
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.engine.documents.empty.buttonLabel', {
- defaultMessage: 'Read the documents guide',
- })}
-
- }
- />
-
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.title', {
+ defaultMessage: 'Add your first documents',
+ })}
+
+ }
+ body={
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.documents.empty.description', {
+ defaultMessage:
+ 'You can index documents using the App Search Web Crawler, by uploading JSON, or by using the API.',
+ })}
+
+ }
+ actions={
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.documents.empty.buttonLabel', {
+ defaultMessage: 'Read the documents guide',
+ })}
+
+ }
+ />
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx
index 4aade8e61b085..90da5bebe6d23 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx
@@ -14,9 +14,10 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPageHeader, EuiPageContent, EuiBasicTable } from '@elastic/eui';
+import { EuiPanel, EuiBasicTable } from '@elastic/eui';
+
+import { getPageHeaderActions } from '../../../test_helpers';
-import { Loading } from '../../../shared/loading';
import { ResultFieldValue } from '../result';
import { DocumentDetail } from '.';
@@ -45,7 +46,7 @@ describe('DocumentDetail', () => {
it('renders', () => {
const wrapper = shallow( );
- expect(wrapper.find(EuiPageContent).length).toBe(1);
+ expect(wrapper.find(EuiPanel).length).toBe(1);
});
it('initializes data on mount', () => {
@@ -59,17 +60,6 @@ describe('DocumentDetail', () => {
expect(actions.setFields).toHaveBeenCalledWith([]);
});
- it('will show a loader while data is loading', () => {
- setMockValues({
- ...values,
- dataLoading: true,
- });
-
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading).length).toBe(1);
- });
-
describe('field values list', () => {
let columns: any;
@@ -102,8 +92,7 @@ describe('DocumentDetail', () => {
it('will delete the document when the delete button is pressed', () => {
const wrapper = shallow( );
- const header = wrapper.find(EuiPageHeader).dive().children().dive();
- const button = header.find('[data-test-subj="DeleteDocumentButton"]');
+ const button = getPageHeaderActions(wrapper).find('[data-test-subj="DeleteDocumentButton"]');
button.simulate('click');
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx
index 314c3529cf4db..175fb1239d380 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx
@@ -10,22 +10,13 @@ import { useParams } from 'react-router-dom';
import { useActions, useValues } from 'kea';
-import {
- EuiButton,
- EuiPageHeader,
- EuiPageContentBody,
- EuiPageContent,
- EuiBasicTable,
- EuiBasicTableColumn,
-} from '@elastic/eui';
+import { EuiPanel, EuiButton, EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DELETE_BUTTON_LABEL } from '../../../shared/constants';
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { Loading } from '../../../shared/loading';
import { useDecodedParams } from '../../utils/encode_path_params';
import { getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import { ResultFieldValue } from '../result';
import { DOCUMENTS_TITLE } from './constants';
@@ -52,10 +43,6 @@ export const DocumentDetail: React.FC = () => {
};
}, []);
- if (dataLoading) {
- return ;
- }
-
const columns: Array> = [
{
name: i18n.translate('xpack.enterpriseSearch.appSearch.documentDetail.fieldHeader', {
@@ -74,11 +61,11 @@ export const DocumentDetail: React.FC = () => {
];
return (
- <>
-
- {
>
{DELETE_BUTTON_LABEL}
,
- ]}
- />
-
-
-
-
-
-
- >
+ ],
+ }}
+ isLoading={dataLoading}
+ >
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx
index 143ad3f55ff2f..b5b6dd453c9df 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx
@@ -10,9 +10,9 @@ import '../../__mocks__/engine_logic.mock';
import React from 'react';
-import { shallow, ShallowWrapper } from 'enzyme';
+import { shallow } from 'enzyme';
-import { EuiPageHeader } from '@elastic/eui';
+import { getPageHeaderActions } from '../../../test_helpers';
import { DocumentCreationButton } from './components';
import { SearchExperience } from './search_experience';
@@ -22,6 +22,7 @@ import { Documents } from '.';
describe('Documents', () => {
const values = {
isMetaEngine: false,
+ engine: { document_count: 1 },
myRole: { canManageEngineDocuments: true },
};
@@ -36,9 +37,6 @@ describe('Documents', () => {
});
describe('DocumentCreationButton', () => {
- const getHeader = (wrapper: ShallowWrapper) =>
- wrapper.find(EuiPageHeader).dive().children().dive();
-
it('renders a DocumentCreationButton if the user can manage engine documents', () => {
setMockValues({
...values,
@@ -46,7 +44,7 @@ describe('Documents', () => {
});
const wrapper = shallow( );
- expect(getHeader(wrapper).find(DocumentCreationButton).exists()).toBe(true);
+ expect(getPageHeaderActions(wrapper).find(DocumentCreationButton).exists()).toBe(true);
});
it('does not render a DocumentCreationButton if the user cannot manage engine documents', () => {
@@ -56,7 +54,7 @@ describe('Documents', () => {
});
const wrapper = shallow( );
- expect(getHeader(wrapper).find(DocumentCreationButton).exists()).toBe(false);
+ expect(getPageHeaderActions(wrapper).find(DocumentCreationButton).exists()).toBe(false);
});
it('does not render a DocumentCreationButton for meta engines even if the user can manage engine documents', () => {
@@ -67,7 +65,7 @@ describe('Documents', () => {
});
const wrapper = shallow( );
- expect(getHeader(wrapper).find(DocumentCreationButton).exists()).toBe(false);
+ expect(getPageHeaderActions(wrapper).find(DocumentCreationButton).exists()).toBe(false);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx
index b4122a715f927..62c7759757bda 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx
@@ -9,35 +9,32 @@ import React from 'react';
import { useValues } from 'kea';
-import { EuiPageHeader, EuiCallOut, EuiSpacer } from '@elastic/eui';
+import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-
import { AppLogic } from '../../app_logic';
import { EngineLogic, getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
-import { DocumentCreationButton } from './components';
+import { DocumentCreationButton, EmptyState } from './components';
import { DOCUMENTS_TITLE } from './constants';
import { SearchExperience } from './search_experience';
export const Documents: React.FC = () => {
- const { isMetaEngine } = useValues(EngineLogic);
+ const { isMetaEngine, engine } = useValues(EngineLogic);
const { myRole } = useValues(AppLogic);
return (
- <>
-
- ]
- : undefined
- }
- />
-
+ ] : [],
+ }}
+ isEmptyState={!engine.document_count}
+ emptyState={ }
+ >
{isMetaEngine && (
<>
{
>
)}
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss
index d2e0a8155fa55..34aac402fbb39 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss
@@ -15,6 +15,7 @@
.documentsSearchExperience__content {
flex-grow: 4;
+ position: relative;
}
.documentsSearchExperience__pagingInfo {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx
index a4d1a92ee45a4..3e8a9c1ab307c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx
@@ -20,8 +20,6 @@ jest.mock('../../../../shared/use_local_storage', () => ({
}));
import { useLocalStorage } from '../../../../shared/use_local_storage';
-import { EmptyState } from '../components';
-
import { CustomizationCallout } from './customization_callout';
import { CustomizationModal } from './customization_modal';
import { SearchExperienceContent } from './search_experience_content';
@@ -58,14 +56,6 @@ describe('SearchExperience', () => {
expect(wrapper.find(SearchExperienceContent)).toHaveLength(1);
});
- it('renders an empty state when the engine does not have documents', () => {
- setMockValues({ ...values, engine: { ...values.engine, document_count: 0 } });
- const wrapper = shallow( );
-
- expect(wrapper.find(EmptyState)).toHaveLength(1);
- expect(wrapper.find(SearchExperienceContent)).toHaveLength(0);
- });
-
describe('when there are no selected filter fields', () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx
index 22029956601a6..709dfc69905f0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx
@@ -21,7 +21,6 @@ import './search_experience.scss';
import { externalUrl } from '../../../../shared/enterprise_search_url';
import { useLocalStorage } from '../../../../shared/use_local_storage';
import { EngineLogic } from '../../engine';
-import { EmptyState } from '../components';
import { buildSearchUIConfig } from './build_search_ui_config';
import { buildSortOptions } from './build_sort_options';
@@ -141,11 +140,7 @@ export const SearchExperience: React.FC = () => {
)}
- {engine.document_count && engine.document_count > 0 ? (
-
- ) : (
-
- )}
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx
index 44a6da51ec8d6..e573502d76b9f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx
@@ -15,6 +15,7 @@ import { shallow } from 'enzyme';
// @ts-expect-error types are not available for this package yet
import { Results } from '@elastic/react-search-ui';
+import { Loading } from '../../../../shared/loading';
import { SchemaType } from '../../../../shared/schema/types';
import { Pagination } from './pagination';
@@ -82,13 +83,13 @@ describe('SearchExperienceContent', () => {
expect(wrapper.find(Pagination).exists()).toBe(true);
});
- it('renders empty if a search was not performed yet', () => {
+ it('renders a loading state if a search was not performed yet', () => {
setMockSearchContextState({
...searchState,
wasSearched: false,
});
const wrapper = shallow( );
- expect(wrapper.isEmptyRender()).toBe(true);
+ expect(wrapper.find(Loading)).toHaveLength(1);
});
it('renders results if a search was performed and there are more than 0 totalResults', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx
index 84fe721f9eb7f..2322bcde831eb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx
@@ -14,6 +14,7 @@ import { EuiFlexGroup, EuiSpacer, EuiEmptyPrompt } from '@elastic/eui';
import { Results, Paging, ResultsPerPage } from '@elastic/react-search-ui';
import { i18n } from '@kbn/i18n';
+import { Loading } from '../../../../shared/loading';
import { EngineLogic } from '../../engine';
import { Result } from '../../result/types';
@@ -26,7 +27,7 @@ export const SearchExperienceContent: React.FC = () => {
const { isMetaEngine, engine } = useValues(EngineLogic);
- if (!wasSearched) return null;
+ if (!wasSearched) return ;
if (totalResults) {
return (
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
index 6510e99a000fc..98627950016fb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
@@ -94,6 +94,16 @@ export const EngineRouter: React.FC = () => {
+ {canViewEngineDocuments && (
+
+
+
+ )}
+ {canViewEngineDocuments && (
+
+
+
+ )}
{/* TODO: Remove layout once page template migration is over */}
}>
{canViewEngineAnalytics && (
@@ -101,16 +111,6 @@ export const EngineRouter: React.FC = () => {
)}
- {canViewEngineDocuments && (
-
-
-
- )}
- {canViewEngineDocuments && (
-
-
-
- )}
{canViewEngineSchema && (
From c52f5edfcc2738fd8f5481d679561dc5e9533f71 Mon Sep 17 00:00:00 2001
From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com>
Date: Mon, 21 Jun 2021 18:34:11 -0400
Subject: [PATCH 037/163] [Security Solution][Exceptions] Fixes empty
exceptions filter bug (#102583)
---
packages/kbn-securitysolution-list-utils/src/helpers/index.ts | 4 ++++
.../public/exceptions/components/builder/helpers.test.ts | 4 ++--
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/packages/kbn-securitysolution-list-utils/src/helpers/index.ts b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts
index a483da152ac89..d208624b69fc5 100644
--- a/packages/kbn-securitysolution-list-utils/src/helpers/index.ts
+++ b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts
@@ -95,6 +95,10 @@ export const filterExceptionItems = (
}
}, []);
+ if (entries.length === 0) {
+ return acc;
+ }
+
const item = { ...exception, entries };
if (exceptionListItemSchema.is(item)) {
diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts
index ec46038c397e5..212db40f3168c 100644
--- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts
+++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts
@@ -1697,9 +1697,9 @@ describe('Exception builder helpers', () => {
namespaceType: 'single',
ruleName: 'rule name',
});
- const exceptions = filterExceptionItems([{ ...rest, meta }]);
+ const exceptions = filterExceptionItems([{ ...rest, entries: [getEntryMatchMock()], meta }]);
- expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]);
+ expect(exceptions).toEqual([{ ...rest, entries: [getEntryMatchMock()], meta: undefined }]);
});
});
From 3084de6782ce245b3635a7240fbdaa3980d97b8b Mon Sep 17 00:00:00 2001
From: Spencer
Date: Mon, 21 Jun 2021 15:36:43 -0700
Subject: [PATCH 038/163] [kbn/test/es] remove unnecessary es user management
logic (#102584)
Co-authored-by: spalger
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../kbn-test/src/functional_tests/lib/auth.ts | 188 ------------------
.../functional_tests/lib/run_elasticsearch.ts | 23 +--
packages/kbn-test/src/index.ts | 2 -
src/core/test_helpers/kbn_server.ts | 25 +--
4 files changed, 3 insertions(+), 235 deletions(-)
delete mode 100644 packages/kbn-test/src/functional_tests/lib/auth.ts
diff --git a/packages/kbn-test/src/functional_tests/lib/auth.ts b/packages/kbn-test/src/functional_tests/lib/auth.ts
deleted file mode 100644
index abd1e0f9e7d5e..0000000000000
--- a/packages/kbn-test/src/functional_tests/lib/auth.ts
+++ /dev/null
@@ -1,188 +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 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import fs from 'fs';
-import util from 'util';
-import { format as formatUrl } from 'url';
-import request from 'request';
-import type { ToolingLog } from '@kbn/dev-utils';
-
-export const DEFAULT_SUPERUSER_PASS = 'changeme';
-const readFile = util.promisify(fs.readFile);
-
-function delay(delayMs: number) {
- return new Promise((res) => setTimeout(res, delayMs));
-}
-
-interface UpdateCredentialsOptions {
- port: number;
- auth: string;
- username: string;
- password: string;
- retries?: number;
- protocol: string;
- caCert?: Buffer | string;
-}
-async function updateCredentials({
- port,
- auth,
- username,
- password,
- retries = 10,
- protocol,
- caCert,
-}: UpdateCredentialsOptions): Promise {
- const result = await new Promise<{ body: any; httpResponse: request.Response }>(
- (resolve, reject) =>
- request(
- {
- method: 'PUT',
- uri: formatUrl({
- protocol: `${protocol}:`,
- auth,
- hostname: 'localhost',
- port,
- pathname: `/_security/user/${username}/_password`,
- }),
- json: true,
- body: { password },
- ca: caCert,
- },
- (err, httpResponse, body) => {
- if (err) return reject(err);
- resolve({ httpResponse, body });
- }
- )
- );
-
- const { body, httpResponse } = result;
- const { statusCode } = httpResponse;
-
- if (statusCode === 200) {
- return;
- }
-
- if (retries > 0) {
- await delay(2500);
- return await updateCredentials({
- port,
- auth,
- username,
- password,
- retries: retries - 1,
- protocol,
- caCert,
- });
- }
-
- throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`);
-}
-
-interface SetupUsersOptions {
- log: ToolingLog;
- esPort: number;
- updates: Array<{ username: string; password: string; roles?: string[] }>;
- protocol?: string;
- caPath?: string;
-}
-
-export async function setupUsers({
- log,
- esPort,
- updates,
- protocol = 'http',
- caPath,
-}: SetupUsersOptions): Promise {
- // track the current credentials for the `elastic` user as
- // they will likely change as we apply updates
- let auth = `elastic:${DEFAULT_SUPERUSER_PASS}`;
- const caCert = caPath ? await readFile(caPath) : undefined;
-
- for (const { username, password, roles } of updates) {
- // If working with a built-in user, just change the password
- if (['logstash_system', 'elastic', 'kibana'].includes(username)) {
- await updateCredentials({ port: esPort, auth, username, password, protocol, caCert });
- log.info('setting %j user password to %j', username, password);
-
- // If not a builtin user, add them
- } else {
- await insertUser({ port: esPort, auth, username, password, roles, protocol, caCert });
- log.info('Added %j user with password to %j', username, password);
- }
-
- if (username === 'elastic') {
- auth = `elastic:${password}`;
- }
- }
-}
-
-interface InserUserOptions {
- port: number;
- auth: string;
- username: string;
- password: string;
- roles?: string[];
- retries?: number;
- protocol: string;
- caCert?: Buffer | string;
-}
-async function insertUser({
- port,
- auth,
- username,
- password,
- roles = [],
- retries = 10,
- protocol,
- caCert,
-}: InserUserOptions): Promise {
- const result = await new Promise<{ body: any; httpResponse: request.Response }>(
- (resolve, reject) =>
- request(
- {
- method: 'POST',
- uri: formatUrl({
- protocol: `${protocol}:`,
- auth,
- hostname: 'localhost',
- port,
- pathname: `/_security/user/${username}`,
- }),
- json: true,
- body: { password, roles },
- ca: caCert,
- },
- (err, httpResponse, body) => {
- if (err) return reject(err);
- resolve({ httpResponse, body });
- }
- )
- );
-
- const { body, httpResponse } = result;
- const { statusCode } = httpResponse;
- if (statusCode === 200) {
- return;
- }
-
- if (retries > 0) {
- await delay(2500);
- return await insertUser({
- port,
- auth,
- username,
- password,
- roles,
- retries: retries - 1,
- protocol,
- caCert,
- });
- }
-
- throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`);
-}
diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts
index 7ba9a3c1c4733..da83d8285a6b5 100644
--- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts
+++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts
@@ -12,8 +12,6 @@ import { KIBANA_ROOT } from './paths';
import type { Config } from '../../functional_test_runner/';
import { createTestEsCluster } from '../../es';
-import { setupUsers, DEFAULT_SUPERUSER_PASS } from './auth';
-
interface RunElasticsearchOptions {
log: ToolingLog;
esFrom: string;
@@ -34,9 +32,7 @@ export async function runElasticsearch({
const cluster = createTestEsCluster({
port: config.get('servers.elasticsearch.port'),
- password: isSecurityEnabled
- ? DEFAULT_SUPERUSER_PASS
- : config.get('servers.elasticsearch.password'),
+ password: isSecurityEnabled ? 'changeme' : config.get('servers.elasticsearch.password'),
license,
log,
basePath: resolve(KIBANA_ROOT, '.es'),
@@ -49,22 +45,5 @@ export async function runElasticsearch({
await cluster.start();
- if (isSecurityEnabled) {
- await setupUsers({
- log,
- esPort: config.get('servers.elasticsearch.port'),
- updates: [config.get('servers.elasticsearch'), config.get('servers.kibana')],
- protocol: config.get('servers.elasticsearch').protocol,
- caPath: getRelativeCertificateAuthorityPath(config.get('kbnTestServer.serverArgs')),
- });
- }
-
return cluster;
}
-
-function getRelativeCertificateAuthorityPath(esConfig: string[] = []) {
- const caConfig = esConfig.find(
- (config) => config.indexOf('--elasticsearch.ssl.certificateAuthorities') === 0
- );
- return caConfig ? caConfig.split('=')[1] : undefined;
-}
diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts
index dd5343b0118b3..af100a33ea3a7 100644
--- a/packages/kbn-test/src/index.ts
+++ b/packages/kbn-test/src/index.ts
@@ -29,8 +29,6 @@ export { esTestConfig, createTestEsCluster } from './es';
export { kbnTestConfig, kibanaServerTestUser, kibanaTestUser, adminTestUser } from './kbn';
-export { setupUsers, DEFAULT_SUPERUSER_PASS } from './functional_tests/lib/auth';
-
export { readConfigFile } from './functional_test_runner/lib/config/read_config_file';
export { runFtrCli } from './functional_test_runner/cli';
diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts
index ba22ecb3b6376..2995ffd08e5c0 100644
--- a/src/core/test_helpers/kbn_server.ts
+++ b/src/core/test_helpers/kbn_server.ts
@@ -7,15 +7,7 @@
*/
import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils';
-import {
- createTestEsCluster,
- DEFAULT_SUPERUSER_PASS,
- esTestConfig,
- kbnTestConfig,
- kibanaServerTestUser,
- kibanaTestUser,
- setupUsers,
-} from '@kbn/test';
+import { createTestEsCluster, esTestConfig, kibanaServerTestUser, kibanaTestUser } from '@kbn/test';
import { defaultsDeep } from 'lodash';
import { resolve } from 'path';
import { BehaviorSubject } from 'rxjs';
@@ -208,7 +200,6 @@ export function createTestServers({
defaultsDeep({}, settings.es ?? {}, {
log,
license,
- password: license === 'trial' ? DEFAULT_SUPERUSER_PASS : undefined,
})
);
@@ -224,19 +215,7 @@ export function createTestServers({
await es.start();
if (['gold', 'trial'].includes(license)) {
- await setupUsers({
- log,
- esPort: esTestConfig.getUrlParts().port,
- updates: [
- ...usersToBeAdded,
- // user elastic
- esTestConfig.getUrlParts() as { username: string; password: string },
- // user kibana
- kbnTestConfig.getUrlParts() as { username: string; password: string },
- ],
- });
-
- // Override provided configs, we know what the elastic user is now
+ // Override provided configs
kbnSettings.elasticsearch = {
hosts: [esTestConfig.getUrl()],
username: kibanaServerTestUser.username,
From 2e3d527696910b58a36ca94506833782110297cc Mon Sep 17 00:00:00 2001
From: Andrew Kroh
Date: Mon, 21 Jun 2021 18:48:19 -0400
Subject: [PATCH 039/163] [Fleet] Update final pipeline based on ECS
event.agent_id_status (#102805)
This updates the Fleet final pipeline added in #100973 to match the specification of
`event.agent_id_status` field as defined in ECS. The field was added to ECS in
https://github.com/elastic/ecs/pull/1454. Basically the values of the field were simplified
from what was originally proposed and implemented.
---
.../ingest_pipeline/final_pipeline.ts | 25 ++++++++++---------
.../apis/epm/final_pipeline.ts | 8 +++---
2 files changed, 17 insertions(+), 16 deletions(-)
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts
index 4c0484c058abf..f929a4f139981 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts
@@ -59,25 +59,26 @@ processors:
}
String verified(def ctx, def params) {
- // Agents only use API keys.
- if (ctx?._security?.authentication_type == null || ctx._security.authentication_type != 'API_KEY') {
- return "no_api_key";
+ // No agent.id field to validate.
+ if (ctx?.agent?.id == null) {
+ return "missing";
}
- // Verify the API key owner before trusting any metadata it contains.
- if (!is_user_trusted(ctx, params.trusted_users)) {
- return "untrusted_user";
- }
-
- // API keys created by Fleet include metadata about the agent they were issued to.
- if (ctx?._security?.api_key?.metadata?.agent_id == null || ctx?.agent?.id == null) {
- return "missing_metadata";
+ // Check auth metadata from API key.
+ if (ctx?._security?.authentication_type == null
+ // Agents only use API keys.
+ || ctx._security.authentication_type != 'API_KEY'
+ // Verify the API key owner before trusting any metadata it contains.
+ || !is_user_trusted(ctx, params.trusted_users)
+ // Verify the API key has metadata indicating the assigned agent ID.
+ || ctx?._security?.api_key?.metadata?.agent_id == null) {
+ return "auth_metadata_missing";
}
// The API key can only be used represent the agent.id it was issued to.
if (ctx._security.api_key.metadata.agent_id != ctx.agent.id) {
// Potential masquerade attempt.
- return "agent_id_mismatch";
+ return "mismatch";
}
return "verified";
diff --git a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts
index a800546a27a3e..81f712e095c78 100644
--- a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts
+++ b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts
@@ -112,14 +112,14 @@ export default function (providerContext: FtrProviderContext) {
// @ts-expect-error
const event = doc._source.event;
- expect(event.agent_id_status).to.be('no_api_key');
+ expect(event.agent_id_status).to.be('auth_metadata_missing');
expect(event).to.have.property('ingested');
});
const scenarios = [
{
name: 'API key without metadata',
- expectedStatus: 'missing_metadata',
+ expectedStatus: 'auth_metadata_missing',
event: { agent: { id: 'agent1' } },
},
{
@@ -134,7 +134,7 @@ export default function (providerContext: FtrProviderContext) {
},
{
name: 'API key with agent id metadata and no agent id in event',
- expectedStatus: 'missing_metadata',
+ expectedStatus: 'missing',
apiKey: {
metadata: {
agent_id: 'agent1',
@@ -143,7 +143,7 @@ export default function (providerContext: FtrProviderContext) {
},
{
name: 'API key with agent id metadata and tampered agent id in event',
- expectedStatus: 'agent_id_mismatch',
+ expectedStatus: 'mismatch',
apiKey: {
metadata: {
agent_id: 'agent2',
From 8619bdbc46a493dc26eee4733a6d0d3cfb2759d1 Mon Sep 17 00:00:00 2001
From: spalger
Date: Mon, 21 Jun 2021 16:55:26 -0700
Subject: [PATCH 040/163] remove duplicate apm-rum deps from devDeps
---
package.json | 2 --
1 file changed, 2 deletions(-)
diff --git a/package.json b/package.json
index 29371c9532915..9e46c9619251b 100644
--- a/package.json
+++ b/package.json
@@ -446,8 +446,6 @@
"@bazel/typescript": "^3.5.1",
"@cypress/snapshot": "^2.1.7",
"@cypress/webpack-preprocessor": "^5.6.0",
- "@elastic/apm-rum": "^5.6.1",
- "@elastic/apm-rum-react": "^1.2.5",
"@elastic/eslint-config-kibana": "link:bazel-bin/packages/elastic-eslint-config-kibana",
"@elastic/eslint-plugin-eui": "0.0.2",
"@elastic/github-checks-reporter": "0.0.20b3",
From 75aafd0ede14e4d0a581df4ec7f138776f353329 Mon Sep 17 00:00:00 2001
From: spalger
Date: Mon, 21 Jun 2021 16:57:43 -0700
Subject: [PATCH 041/163] Revert "remove duplicate apm-rum deps from devDeps"
This reverts commit 8619bdbc46a493dc26eee4733a6d0d3cfb2759d1.
---
package.json | 2 ++
1 file changed, 2 insertions(+)
diff --git a/package.json b/package.json
index 9e46c9619251b..29371c9532915 100644
--- a/package.json
+++ b/package.json
@@ -446,6 +446,8 @@
"@bazel/typescript": "^3.5.1",
"@cypress/snapshot": "^2.1.7",
"@cypress/webpack-preprocessor": "^5.6.0",
+ "@elastic/apm-rum": "^5.6.1",
+ "@elastic/apm-rum-react": "^1.2.5",
"@elastic/eslint-config-kibana": "link:bazel-bin/packages/elastic-eslint-config-kibana",
"@elastic/eslint-plugin-eui": "0.0.2",
"@elastic/github-checks-reporter": "0.0.20b3",
From 138bd0df307bd7f10b6b72b79e7151e7347d5472 Mon Sep 17 00:00:00 2001
From: Constance
Date: Mon, 21 Jun 2021 17:42:16 -0700
Subject: [PATCH 042/163] [Workplace Search] Convert Sources pages to new page
template (+ personal dashboard) (#102592)
* Refactor PersonalDashboardLayout to more closely match new page template
- Remove references to enterpriseSearchLayout CSS (which will be removed in an upcoming PR)
- Prefer to lean more heavily on default EuiPage props/CSS/etc.
- Handle conditional sidebar logic in this layout rather than passing it in as a prop
- Update props & DRY concerns to more closely match WorkplaceSearchPageTemplate
- e.g. isLoading & pageChrome (mostly for document titles)
- make FlashMessage and readOnlyMode work OOTB w/o props)
* Convert Source subnav to EuiSideNav format
+ update PrivateSourcesSidebar to use EuiSIdeNav
* Update routers
- removing wrapping layouts, flash messages, chrome/telemetry
* Refactor SourceRouter into shared layout component
- Remove license callout, page header, and page chrome/telemetry
- NOTE: The early page isLoading behavior (lines 51-) is required to prevent a flash of a completely empty page (instead of preserving the layout/side nav while loading). We cannot let the page fall through to the route because some routes are conditionally rendered based on isCustomSource.
- FWIW: App Search has a similar isLoading early return with its Engine sub nav, and also a similar AnalyticsLayout for DRYing out repeated concerns/UI elements within Analytics subroutes.
* Convert all single source views to new page template
- Mostly removing isLoading tests
- NOTE: Schema page could *possibly* use the new isEmptyState/emptyState page template props, but would need some layout reshuffling
* Convert Add Source pages to conditional page templates
- Opted to give these pages their own conditional layout logic - this could possibly be DRY'd out
- There is possibly extra cleanup here on this file that could have been done (e.g. empty state, titles, etc.) in light of the new templates - but I didn't want to spend extra time here and went with creating as few diffs as possible
* Convert separate Organization Sources & Private Sources views to new page templates
+ fix Link to EuiButtonTo on Organization Sources view
* Update Account Settings with personal layout + write tests
+ add related KibanaLogic branch coverage
* [UX feedback] Do not render page headers while loading on Overview & Sources pages
* [PR feedback] Breadcrumb errors/fallbacks
* [Proposal] Update schema errors routing to better work with nav/breadcrumbs
- `exact` is required to make the parent schemas/ not gobble schema/{errorId}
- added bonus breadcrumb for nicer schema navigation UX
- No tests need to update AFAICT
* Ignore Typescript error on soon-to-come EUI prop
---
.../shared/kibana/kibana_logic.test.ts | 6 +
.../components/layout/nav.test.tsx | 3 +
.../components/layout/nav.tsx | 3 +-
.../personal_dashboard_layout.scss | 24 ++--
.../personal_dashboard_layout.test.tsx | 81 ++++++++++--
.../personal_dashboard_layout.tsx | 65 ++++++----
.../private_sources_sidebar.test.tsx | 53 +++++---
.../private_sources_sidebar.tsx | 14 ++-
.../applications/workplace_search/index.tsx | 44 ++-----
.../workplace_search/routes.test.tsx | 4 +-
.../applications/workplace_search/routes.ts | 2 +-
.../account_settings.test.tsx | 57 +++++++++
.../account_settings/account_settings.tsx | 6 +-
.../components/add_source/add_source.test.tsx | 27 +++-
.../components/add_source/add_source.tsx | 14 ++-
.../add_source/add_source_list.test.tsx | 95 ++++++++------
.../components/add_source/add_source_list.tsx | 27 ++--
.../display_settings.test.tsx | 8 --
.../display_settings/display_settings.tsx | 14 ++-
.../components/overview.test.tsx | 10 --
.../content_sources/components/overview.tsx | 12 +-
.../components/schema/schema.test.tsx | 10 +-
.../components/schema/schema.tsx | 13 +-
.../schema/schema_change_errors.tsx | 9 +-
.../components/source_content.test.tsx | 8 --
.../components/source_content.tsx | 12 +-
.../components/source_layout.test.tsx | 84 +++++++++++++
.../components/source_layout.tsx | 84 +++++++++++++
.../components/source_settings.tsx | 8 +-
.../components/source_sub_nav.test.tsx | 94 +++++++++++---
.../components/source_sub_nav.tsx | 74 ++++++-----
.../organization_sources.test.tsx | 16 +--
.../content_sources/organization_sources.tsx | 71 ++++++-----
.../content_sources/private_sources.test.tsx | 8 --
.../views/content_sources/private_sources.tsx | 17 +--
.../content_sources/source_router.test.tsx | 101 ++++++---------
.../views/content_sources/source_router.tsx | 118 +++++-------------
.../views/content_sources/sources_router.tsx | 110 +++++++---------
.../views/overview/overview.test.tsx | 7 ++
.../views/overview/overview.tsx | 12 +-
.../components/source_config.test.tsx | 7 ++
.../settings/components/source_config.tsx | 2 +-
42 files changed, 882 insertions(+), 552 deletions(-)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts
index 4cc907c3de9e4..39392d0c5c78e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts
@@ -33,6 +33,12 @@ describe('KibanaLogic', () => {
expect(KibanaLogic.values.config).toEqual({});
});
+ it('gracefully handles disabled security', () => {
+ mountKibanaLogic({ ...mockKibanaValues, security: undefined } as any);
+
+ expect(KibanaLogic.values.security).toEqual({});
+ });
+
it('gracefully handles non-cloud installs', () => {
mountKibanaLogic({ ...mockKibanaValues, cloud: undefined } as any);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx
index 3d5d0a8e6f2cf..04b0880a7351c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx
@@ -9,6 +9,9 @@ jest.mock('../../../shared/layout', () => ({
...jest.requireActual('../../../shared/layout'),
generateNavLink: jest.fn(({ to }) => ({ href: to })),
}));
+jest.mock('../../views/content_sources/components/source_sub_nav', () => ({
+ useSourceSubNav: () => [],
+}));
jest.mock('../../views/groups/components/group_sub_nav', () => ({
useGroupSubNav: () => [],
}));
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx
index f59679e0ee048..99225bc36e892 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx
@@ -19,6 +19,7 @@ import {
GROUPS_PATH,
ORG_SETTINGS_PATH,
} from '../../routes';
+import { useSourceSubNav } from '../../views/content_sources/components/source_sub_nav';
import { useGroupSubNav } from '../../views/groups/components/group_sub_nav';
import { useSettingsSubNav } from '../../views/settings/components/settings_sub_nav';
@@ -33,7 +34,7 @@ export const useWorkplaceSearchNav = () => {
id: 'sources',
name: NAV.SOURCES,
...generateNavLink({ to: SOURCES_PATH }),
- items: [], // TODO: Source subnav
+ items: useSourceSubNav(),
},
{
id: 'groups',
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss
index 175f6b9ebca20..3287cb21783cb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.scss
@@ -6,18 +6,20 @@
*/
.personalDashboardLayout {
- $sideBarWidth: $euiSize * 30;
- $consoleHeaderHeight: 48px; // NOTE: Keep an eye on this for changes
- $pageHeight: calc(100vh - #{$consoleHeaderHeight});
+ &__sideBar {
+ padding: $euiSizeXL $euiSizeXXL $euiSizeXXL;
- left: $sideBarWidth;
- width: calc(100% - #{$sideBarWidth});
- min-height: $pageHeight;
+ @include euiBreakpoint('m', 'l') {
+ min-width: $euiSize * 20;
+ }
+ @include euiBreakpoint('xl') {
+ min-width: $euiSize * 30;
+ }
+ }
- &__sideBar {
- padding: 32px 40px 40px;
- width: $sideBarWidth;
- margin-left: -$sideBarWidth;
- height: $pageHeight;
+ &__body {
+ position: relative;
+ width: 100%;
+ height: 100%;
}
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx
index faeaa7323e93f..6847e91d46f6e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.test.tsx
@@ -5,37 +5,102 @@
* 2.0.
*/
+import { setMockValues } from '../../../../__mocks__/kea_logic';
+import { mockUseRouteMatch } from '../../../../__mocks__/react_router';
+
import React from 'react';
import { shallow } from 'enzyme';
import { EuiCallOut } from '@elastic/eui';
-import { AccountHeader } from '..';
+import { FlashMessages } from '../../../../shared/flash_messages';
+import { SetWorkplaceSearchChrome } from '../../../../shared/kibana_chrome';
+import { Loading } from '../../../../shared/loading';
+
+import { AccountHeader, AccountSettingsSidebar, PrivateSourcesSidebar } from '../index';
import { PersonalDashboardLayout } from './personal_dashboard_layout';
describe('PersonalDashboardLayout', () => {
const children = test
;
- const sidebar = test
;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockValues({ readOnlyMode: false });
+ });
it('renders', () => {
- const wrapper = shallow(
- {children}
- );
+ const wrapper = shallow({children} );
expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1);
- expect(wrapper.find('[data-test-subj="TestSidebar"]')).toHaveLength(1);
+ expect(wrapper.find('.personalDashboardLayout')).toHaveLength(1);
expect(wrapper.find(AccountHeader)).toHaveLength(1);
+ expect(wrapper.find(FlashMessages)).toHaveLength(1);
});
- it('renders callout when in read-only mode', () => {
+ describe('renders sidebar content based on the route', () => {
+ it('renders the private sources sidebar on the private sources path', () => {
+ (mockUseRouteMatch as jest.Mock).mockImplementation((path: string) => path === '/p/sources');
+ const wrapper = shallow({children} );
+
+ expect(wrapper.find(PrivateSourcesSidebar)).toHaveLength(1);
+ });
+
+ it('renders the account settings sidebar on the account settings path', () => {
+ (mockUseRouteMatch as jest.Mock).mockImplementation((path: string) => path === '/p/settings');
+ const wrapper = shallow({children} );
+
+ expect(wrapper.find(AccountSettingsSidebar)).toHaveLength(1);
+ });
+
+ it('does not render a sidebar if not on a valid personal dashboard path', () => {
+ (mockUseRouteMatch as jest.Mock).mockImplementation((path: string) => path === '/test');
+ const wrapper = shallow({children} );
+
+ expect(wrapper.find(AccountSettingsSidebar)).toHaveLength(0);
+ expect(wrapper.find(PrivateSourcesSidebar)).toHaveLength(0);
+ });
+ });
+
+ describe('loading state', () => {
+ it('renders a loading icon in place of children', () => {
+ const wrapper = shallow(
+ {children}
+ );
+
+ expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(0);
+ });
+
+ it('renders children & does not render a loading icon when the page is done loading', () => {
+ const wrapper = shallow(
+ {children}
+ );
+
+ expect(wrapper.find(Loading)).toHaveLength(0);
+ expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1);
+ });
+ });
+
+ it('sets WS page chrome (primarily document title)', () => {
const wrapper = shallow(
-
+
{children}
);
+ expect(wrapper.find(SetWorkplaceSearchChrome).prop('trail')).toEqual([
+ 'Sources',
+ 'Add source',
+ 'Gmail',
+ ]);
+ });
+
+ it('renders callout when in read-only mode', () => {
+ setMockValues({ readOnlyMode: true });
+ const wrapper = shallow({children} );
+
expect(wrapper.find(EuiCallOut)).toHaveLength(1);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx
index 1ab9e07dfa14d..5b68d661ac5df 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_layout/personal_dashboard_layout.tsx
@@ -6,44 +6,67 @@
*/
import React from 'react';
+import { useRouteMatch } from 'react-router-dom';
-import { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui';
+import { useValues } from 'kea';
-import { AccountHeader } from '..';
+import {
+ EuiPage,
+ EuiPageSideBar,
+ EuiPageBody,
+ EuiPageContentBody,
+ EuiCallOut,
+ EuiSpacer,
+} from '@elastic/eui';
+import { FlashMessages } from '../../../../shared/flash_messages';
+import { HttpLogic } from '../../../../shared/http';
+import { SetWorkplaceSearchChrome } from '../../../../shared/kibana_chrome';
+import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs';
+import { Loading } from '../../../../shared/loading';
+
+import { PERSONAL_SOURCES_PATH, PERSONAL_SETTINGS_PATH } from '../../../routes';
import { PRIVATE_DASHBOARD_READ_ONLY_MODE_WARNING } from '../../../views/content_sources/constants';
+import { AccountHeader, AccountSettingsSidebar, PrivateSourcesSidebar } from '../index';
import './personal_dashboard_layout.scss';
interface LayoutProps {
- restrictWidth?: boolean;
- readOnlyMode?: boolean;
- sidebar: React.ReactNode;
+ isLoading?: boolean;
+ pageChrome?: BreadcrumbTrail;
}
export const PersonalDashboardLayout: React.FC = ({
children,
- restrictWidth,
- readOnlyMode,
- sidebar,
+ isLoading,
+ pageChrome,
}) => {
+ const { readOnlyMode } = useValues(HttpLogic);
+
return (
<>
+ {pageChrome && }
-
-
- {sidebar}
+
+
+ {useRouteMatch(PERSONAL_SOURCES_PATH) && }
+ {useRouteMatch(PERSONAL_SETTINGS_PATH) && }
-
- {readOnlyMode && (
-
- )}
- {children}
+
+
+ {readOnlyMode && (
+ <>
+
+
+ >
+ )}
+
+ {isLoading ? : children}
+
>
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx
index 387724af970f8..9fa4d4dd1b237 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.test.tsx
@@ -7,17 +7,22 @@
import { setMockValues } from '../../../../__mocks__/kea_logic';
+jest.mock('../../../views/content_sources/components/source_sub_nav', () => ({
+ useSourceSubNav: () => [],
+}));
+
import React from 'react';
import { shallow } from 'enzyme';
+import { EuiSideNav } from '@elastic/eui';
+
import {
PRIVATE_CAN_CREATE_PAGE_TITLE,
PRIVATE_VIEW_ONLY_PAGE_TITLE,
PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION,
PRIVATE_CAN_CREATE_PAGE_DESCRIPTION,
} from '../../../constants';
-import { SourceSubNav } from '../../../views/content_sources/components/source_sub_nav';
import { ViewContentHeader } from '../../shared/view_content_header';
@@ -26,6 +31,7 @@ import { PrivateSourcesSidebar } from './private_sources_sidebar';
describe('PrivateSourcesSidebar', () => {
const mockValues = {
account: { canCreatePersonalSources: true },
+ contentSource: {},
};
beforeEach(() => {
@@ -36,25 +42,42 @@ describe('PrivateSourcesSidebar', () => {
const wrapper = shallow( );
expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
- expect(wrapper.find(SourceSubNav)).toHaveLength(1);
});
- it('uses correct title and description when private sources are enabled', () => {
- const wrapper = shallow( );
+ describe('header text', () => {
+ it('uses correct title and description when private sources are enabled', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_CAN_CREATE_PAGE_TITLE);
+ expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
+ PRIVATE_CAN_CREATE_PAGE_DESCRIPTION
+ );
+ });
- expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_CAN_CREATE_PAGE_TITLE);
- expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
- PRIVATE_CAN_CREATE_PAGE_DESCRIPTION
- );
+ it('uses correct title and description when private sources are disabled', () => {
+ setMockValues({ ...mockValues, account: { canCreatePersonalSources: false } });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_VIEW_ONLY_PAGE_TITLE);
+ expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
+ PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION
+ );
+ });
});
- it('uses correct title and description when private sources are disabled', () => {
- setMockValues({ account: { canCreatePersonalSources: false } });
- const wrapper = shallow( );
+ describe('sub nav', () => {
+ it('renders a side nav when viewing a single source', () => {
+ setMockValues({ ...mockValues, contentSource: { id: '1', name: 'test source' } });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiSideNav)).toHaveLength(1);
+ });
+
+ it('does not render a side nav if not on a source page', () => {
+ setMockValues({ ...mockValues, contentSource: {} });
+ const wrapper = shallow( );
- expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_VIEW_ONLY_PAGE_TITLE);
- expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
- PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION
- );
+ expect(wrapper.find(EuiSideNav)).toHaveLength(0);
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
index 5505ae57b2ad5..36496b83b3123 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx
@@ -9,6 +9,8 @@ import React from 'react';
import { useValues } from 'kea';
+import { EuiSideNav } from '@elastic/eui';
+
import { AppLogic } from '../../../app_logic';
import {
PRIVATE_CAN_CREATE_PAGE_TITLE,
@@ -16,7 +18,8 @@ import {
PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION,
PRIVATE_CAN_CREATE_PAGE_DESCRIPTION,
} from '../../../constants';
-import { SourceSubNav } from '../../../views/content_sources/components/source_sub_nav';
+import { useSourceSubNav } from '../../../views/content_sources/components/source_sub_nav';
+import { SourceLogic } from '../../../views/content_sources/source_logic';
import { ViewContentHeader } from '../../shared/view_content_header';
export const PrivateSourcesSidebar = () => {
@@ -31,10 +34,17 @@ export const PrivateSourcesSidebar = () => {
? PRIVATE_CAN_CREATE_PAGE_DESCRIPTION
: PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION;
+ const {
+ contentSource: { id = '', name = '' },
+ } = useValues(SourceLogic);
+
+ const navItems = [{ id, name, items: useSourceSubNav() }];
+
return (
<>
-
+ {/* @ts-expect-error: TODO, uncomment this once EUI 34.x lands in Kibana & `mobileBreakpoints` is a valid prop */}
+ {id && }
>
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
index f4278d5083143..8a1e9c0275322 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
@@ -19,11 +19,6 @@ import { NotFound } from '../shared/not_found';
import { AppLogic } from './app_logic';
import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout';
-import {
- PersonalDashboardLayout,
- PrivateSourcesSidebar,
- AccountSettingsSidebar,
-} from './components/layout';
import {
GROUPS_PATH,
SETUP_GUIDE_PATH,
@@ -34,11 +29,11 @@ import {
ROLE_MAPPINGS_PATH,
SECURITY_PATH,
PERSONAL_SETTINGS_PATH,
+ PERSONAL_PATH,
} from './routes';
import { AccountSettings } from './views/account_settings';
import { SourcesRouter } from './views/content_sources';
import { SourceAdded } from './views/content_sources/components/source_added';
-import { SourceSubNav } from './views/content_sources/components/source_sub_nav';
import { ErrorState } from './views/error_state';
import { GroupsRouter } from './views/groups';
import { Overview } from './views/overview';
@@ -60,9 +55,6 @@ export const WorkplaceSearchConfigured: React.FC = (props) => {
const { pathname } = useLocation();
- // We don't want so show the subnavs on the container root pages.
- const showSourcesSubnav = pathname !== SOURCES_PATH && pathname !== PERSONAL_SOURCES_PATH;
-
/**
* Personal dashboard urls begin with /p/
* EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources
@@ -95,32 +87,18 @@ export const WorkplaceSearchConfigured: React.FC = (props) => {
-
- }
- >
-
-
-
-
- }
- >
-
-
+
+
+
+
+
+
+
+
+
- } />}
- restrictWidth
- readOnlyMode={readOnlyMode}
- >
-
-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx
index a5a3d6b491bb9..b89a1451f7e57 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx
@@ -76,13 +76,13 @@ describe('getReindexJobRoute', () => {
it('should format org path', () => {
expect(getReindexJobRoute(SOURCE_ID, REINDEX_ID, true)).toEqual(
- `/sources/${SOURCE_ID}/schema_errors/${REINDEX_ID}`
+ `/sources/${SOURCE_ID}/schemas/${REINDEX_ID}`
);
});
it('should format user path', () => {
expect(getReindexJobRoute(SOURCE_ID, REINDEX_ID, false)).toEqual(
- `/p/sources/${SOURCE_ID}/schema_errors/${REINDEX_ID}`
+ `/p/sources/${SOURCE_ID}/schemas/${REINDEX_ID}`
);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts
index 1fe8019c4b364..3c564c1f912ec 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts
@@ -88,7 +88,7 @@ export const SOURCE_CONTENT_PATH = `${SOURCES_PATH}/:sourceId/content`;
export const SOURCE_SCHEMAS_PATH = `${SOURCES_PATH}/:sourceId/schemas`;
export const SOURCE_DISPLAY_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/display_settings`;
export const SOURCE_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/settings`;
-export const REINDEX_JOB_PATH = `${SOURCES_PATH}/:sourceId/schema_errors/:activeReindexJobId`;
+export const REINDEX_JOB_PATH = `${SOURCE_SCHEMAS_PATH}/:activeReindexJobId`;
export const DISPLAY_SETTINGS_SEARCH_RESULT_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/`;
export const DISPLAY_SETTINGS_RESULT_DETAIL_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/result_detail`;
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx
new file mode 100644
index 0000000000000..5ff80a7683db6
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.test.tsx
@@ -0,0 +1,57 @@
+/*
+ * 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 '../../../__mocks__/shallow_useeffect.mock';
+import { mockKibanaValues } from '../../../__mocks__/kea_logic';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { AccountSettings } from './';
+
+describe('AccountSettings', () => {
+ const {
+ security: {
+ authc: { getCurrentUser },
+ uiApi: {
+ components: { getPersonalInfo, getChangePassword },
+ },
+ },
+ } = mockKibanaValues;
+
+ const mockCurrentUser = (user?: unknown) =>
+ (getCurrentUser as jest.Mock).mockReturnValue(Promise.resolve(user));
+
+ beforeAll(() => {
+ mockCurrentUser();
+ });
+
+ it('gets the current user on mount', () => {
+ shallow( );
+
+ expect(getCurrentUser).toHaveBeenCalled();
+ });
+
+ it('does not render if the current user does not exist', async () => {
+ mockCurrentUser(null);
+ const wrapper = await shallow( );
+
+ expect(wrapper.isEmptyRender()).toBe(true);
+ });
+
+ it('renders the security UI components when the user exists', async () => {
+ mockCurrentUser({ username: 'mock user' });
+ (getPersonalInfo as jest.Mock).mockReturnValue(
);
+ (getChangePassword as jest.Mock).mockReturnValue(
);
+
+ const wrapper = await shallow( );
+
+ expect(wrapper.childAt(0).dive().find('[data-test-subj="PersonalInfo"]')).toHaveLength(1);
+ expect(wrapper.childAt(1).dive().find('[data-test-subj="ChangePassword"]')).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx
index e28faaeec8993..313d3ffa59d48 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/account_settings/account_settings.tsx
@@ -11,6 +11,8 @@ import { useValues } from 'kea';
import type { AuthenticatedUser } from '../../../../../../security/public';
import { KibanaLogic } from '../../../shared/kibana/kibana_logic';
+import { PersonalDashboardLayout } from '../../components/layout';
+import { ACCOUNT_SETTINGS_TITLE } from '../../constants';
export const AccountSettings: React.FC = () => {
const { security } = useValues(KibanaLogic);
@@ -31,9 +33,9 @@ export const AccountSettings: React.FC = () => {
}
return (
- <>
+
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx
index 92cbfcf6eeafe..0501509b3a8ef 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx
@@ -17,7 +17,10 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { Loading } from '../../../../../shared/loading';
+import {
+ WorkplaceSearchPageTemplate,
+ PersonalDashboardLayout,
+} from '../../../../components/layout';
import { AddSource } from './add_source';
import { AddSourceSteps } from './add_source_logic';
@@ -68,11 +71,27 @@ describe('AddSourceList', () => {
expect(setAddSourceStep).toHaveBeenCalledWith(AddSourceSteps.SaveConfigStep);
});
- it('handles loading state', () => {
- setMockValues({ ...mockValues, dataLoading: true });
+ describe('layout', () => {
+ it('renders the default workplace search layout when on an organization view', () => {
+ setMockValues({ ...mockValues, isOrganization: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate);
+ });
+
+ it('renders the personal dashboard layout when not in an organization', () => {
+ setMockValues({ ...mockValues, isOrganization: false });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(PersonalDashboardLayout);
+ });
+ });
+
+ it('renders a breadcrumb fallback while data is loading', () => {
+ setMockValues({ ...mockValues, dataLoading: true, sourceConfigData: {} });
const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(wrapper.prop('pageChrome')).toEqual(['Sources', 'Add Source', '...']);
});
it('renders Config Completed step', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx
index ee4bcfb9afd34..b0c3ebe64830c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx
@@ -13,9 +13,12 @@ import { i18n } from '@kbn/i18n';
import { setSuccessMessage } from '../../../../../shared/flash_messages';
import { KibanaLogic } from '../../../../../shared/kibana';
-import { Loading } from '../../../../../shared/loading';
import { AppLogic } from '../../../../app_logic';
-import { CUSTOM_SERVICE_TYPE } from '../../../../constants';
+import {
+ WorkplaceSearchPageTemplate,
+ PersonalDashboardLayout,
+} from '../../../../components/layout';
+import { NAV, CUSTOM_SERVICE_TYPE } from '../../../../constants';
import { SOURCES_PATH, getSourcesPath } from '../../../../routes';
import { SourceDataItem } from '../../../../types';
import { staticSourceData } from '../../source_data';
@@ -71,8 +74,6 @@ export const AddSource: React.FC = (props) => {
return resetSourceState;
}, []);
- if (dataLoading) return ;
-
const goToConfigurationIntro = () => setAddSourceStep(AddSourceSteps.ConfigIntroStep);
const goToSaveConfig = () => setAddSourceStep(AddSourceSteps.SaveConfigStep);
const setConfigCompletedStep = () => setAddSourceStep(AddSourceSteps.ConfigCompletedStep);
@@ -99,9 +100,10 @@ export const AddSource: React.FC = (props) => {
};
const header = ;
+ const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
return (
- <>
+
{addSourceCurrentStep === AddSourceSteps.ConfigIntroStep && (
)}
@@ -158,6 +160,6 @@ export const AddSource: React.FC = (props) => {
{addSourceCurrentStep === AddSourceSteps.ReauthenticateStep && (
)}
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx
index 6bf71cd73ec35..b30511f0a6d80 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx
@@ -19,7 +19,11 @@ import { shallow } from 'enzyme';
import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui';
-import { Loading } from '../../../../../shared/loading';
+import { getPageDescription } from '../../../../../test_helpers';
+import {
+ WorkplaceSearchPageTemplate,
+ PersonalDashboardLayout,
+} from '../../../../components/layout';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
import { AddSourceList } from './add_source_list';
@@ -54,14 +58,21 @@ describe('AddSourceList', () => {
expect(wrapper.find(AvailableSourcesList)).toHaveLength(1);
});
- it('returns loading when loading', () => {
- setMockValues({
- ...mockValues,
- dataLoading: true,
+ describe('layout', () => {
+ it('renders the default workplace search layout when on an organization view', () => {
+ setMockValues({ ...mockValues, isOrganization: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate);
});
- const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
+ it('renders the personal dashboard layout and a header when not in an organization', () => {
+ setMockValues({ ...mockValues, isOrganization: false });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(PersonalDashboardLayout);
+ expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
+ });
});
describe('filters sources', () => {
@@ -97,49 +108,51 @@ describe('AddSourceList', () => {
});
describe('content headings', () => {
- it('should render correct organization heading with sources', () => {
- const wrapper = shallow( );
-
- expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
- ADD_SOURCE_ORG_SOURCE_DESCRIPTION
- );
- });
+ describe('organization view', () => {
+ it('should render the correct organization heading with sources', () => {
+ const wrapper = shallow( );
- it('should render correct organization heading without sources', () => {
- setMockValues({
- ...mockValues,
- contentSources: [],
+ expect(getPageDescription(wrapper)).toEqual(ADD_SOURCE_ORG_SOURCE_DESCRIPTION);
});
- const wrapper = shallow( );
- expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
- ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_ORG_SOURCE_DESCRIPTION
- );
- });
+ it('should render the correct organization heading without sources', () => {
+ setMockValues({
+ ...mockValues,
+ contentSources: [],
+ });
+ const wrapper = shallow( );
- it('should render correct account heading with sources', () => {
- const wrapper = shallow( );
- setMockValues({
- ...mockValues,
- isOrganization: false,
+ expect(getPageDescription(wrapper)).toEqual(
+ ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_ORG_SOURCE_DESCRIPTION
+ );
});
-
- expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
- ADD_SOURCE_ORG_SOURCE_DESCRIPTION
- );
});
- it('should render correct account heading without sources', () => {
- setMockValues({
- ...mockValues,
- isOrganization: false,
- contentSources: [],
+ describe('personal dashboard view', () => {
+ it('should render the correct personal heading with sources', () => {
+ setMockValues({
+ ...mockValues,
+ isOrganization: false,
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
+ ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION
+ );
});
- const wrapper = shallow( );
- expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
- ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION
- );
+ it('should render the correct personal heading without sources', () => {
+ setMockValues({
+ ...mockValues,
+ isOrganization: false,
+ contentSources: [],
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(
+ ADD_SOURCE_NEW_SOURCE_DESCRIPTION + ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION
+ );
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx
index 80d35553bb8bb..a7a64194cb42f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx
@@ -19,12 +19,15 @@ import {
EuiEmptyPrompt,
} from '@elastic/eui';
-import { Loading } from '../../../../../shared/loading';
import { AppLogic } from '../../../../app_logic';
import noSharedSourcesIcon from '../../../../assets/share_circle.svg';
+import {
+ WorkplaceSearchPageTemplate,
+ PersonalDashboardLayout,
+} from '../../../../components/layout';
import { ContentSection } from '../../../../components/shared/content_section';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
-import { CUSTOM_SERVICE_TYPE } from '../../../../constants';
+import { NAV, CUSTOM_SERVICE_TYPE } from '../../../../constants';
import { SourceDataItem } from '../../../../types';
import { SourcesLogic } from '../../sources_logic';
@@ -58,8 +61,6 @@ export const AddSourceList: React.FC = () => {
return resetSourcesState;
}, []);
- if (dataLoading) return ;
-
const hasSources = contentSources.length > 0;
const showConfiguredSourcesList = configuredSources.find(
({ serviceType }) => serviceType !== CUSTOM_SERVICE_TYPE
@@ -97,12 +98,22 @@ export const AddSourceList: React.FC = () => {
filterConfiguredSources
) as SourceDataItem[];
+ const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
+
return (
- <>
-
+
+ {!isOrganization && (
+
+
+
+ )}
{showConfiguredSourcesList || isOrganization ? (
-
{
)}
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx
index aa5cec385738d..e5714bf4bdfbf 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx
@@ -16,7 +16,6 @@ import { shallow } from 'enzyme';
import { EuiButton, EuiTabbedContent } from '@elastic/eui';
-import { Loading } from '../../../../../shared/loading';
import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
@@ -57,13 +56,6 @@ describe('DisplaySettings', () => {
expect(wrapper.find('form')).toHaveLength(1);
});
- it('returns loading when loading', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
describe('tabbed content', () => {
const tabs = [
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx
index d923fbe7a1a8e..ae47e20026b68 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx
@@ -20,10 +20,10 @@ import {
} from '@elastic/eui';
import { clearFlashMessages } from '../../../../../shared/flash_messages';
-import { Loading } from '../../../../../shared/loading';
import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
-import { SAVE_BUTTON } from '../../../../constants';
+import { NAV, SAVE_BUTTON } from '../../../../constants';
+import { SourceLayout } from '../source_layout';
import {
UNSAVED_MESSAGE,
@@ -64,8 +64,6 @@ export const DisplaySettings: React.FC = ({ tabId }) => {
return clearFlashMessages;
}, []);
- if (dataLoading) return ;
-
const tabs = [
{
id: 'search_results',
@@ -89,7 +87,11 @@ export const DisplaySettings: React.FC = ({ tabId }) => {
};
return (
- <>
+
= ({ tabId }) => {
)}
{addFieldModalVisible && }
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx
index f2cf5f50b813b..d99eac5de74e5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx
@@ -5,8 +5,6 @@
* 2.0.
*/
-import '../../../../__mocks__/shallow_useeffect.mock';
-
import { setMockValues } from '../../../../__mocks__/kea_logic';
import { fullContentSources } from '../../../__mocks__/content_sources.mock';
@@ -16,7 +14,6 @@ import { shallow } from 'enzyme';
import { EuiEmptyPrompt, EuiPanel, EuiTable } from '@elastic/eui';
-import { Loading } from '../../../../shared/loading';
import { ComponentLoader } from '../../../components/shared/component_loader';
import { Overview } from './overview';
@@ -44,13 +41,6 @@ describe('Overview', () => {
expect(documentSummary.find('[data-test-subj="DocumentSummaryRow"]')).toHaveLength(1);
});
- it('returns Loading when loading', () => {
- setMockValues({ ...mockValues, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
it('renders ComponentLoader when loading', () => {
setMockValues({
...mockValues,
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx
index 153df1bc00496..cc890e0f104ac 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx
@@ -29,7 +29,6 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { Loading } from '../../../../shared/loading';
import { EuiPanelTo } from '../../../../shared/react_router_helpers';
import { AppLogic } from '../../../app_logic';
import aclImage from '../../../assets/supports_acl.svg';
@@ -78,8 +77,10 @@ import {
} from '../constants';
import { SourceLogic } from '../source_logic';
+import { SourceLayout } from './source_layout';
+
export const Overview: React.FC = () => {
- const { contentSource, dataLoading } = useValues(SourceLogic);
+ const { contentSource } = useValues(SourceLogic);
const { isOrganization } = useValues(AppLogic);
const {
@@ -97,8 +98,6 @@ export const Overview: React.FC = () => {
isFederatedSource,
} = contentSource;
- if (dataLoading) return ;
-
const DocumentSummary = () => {
let totalDocuments = 0;
const tableContent = summary?.map((item, index) => {
@@ -450,8 +449,9 @@ export const Overview: React.FC = () => {
);
return (
- <>
+
+
@@ -513,6 +513,6 @@ export const Overview: React.FC = () => {
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx
index 178c9125ee437..47859e4e67b17 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx
@@ -16,7 +16,6 @@ import { shallow } from 'enzyme';
import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui';
-import { Loading } from '../../../../../shared/loading';
import { SchemaAddFieldModal, SchemaErrorsCallout } from '../../../../../shared/schema';
import { Schema } from './schema';
@@ -71,13 +70,6 @@ describe('Schema', () => {
expect(wrapper.find(SchemaFieldsTable)).toHaveLength(1);
});
- it('returns loading when loading', () => {
- setMockValues({ ...mockValues, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
it('handles empty state', () => {
setMockValues({ ...mockValues, activeSchema: {} });
const wrapper = shallow( );
@@ -106,7 +98,7 @@ describe('Schema', () => {
expect(wrapper.find(SchemaErrorsCallout)).toHaveLength(1);
expect(wrapper.find(SchemaErrorsCallout).prop('viewErrorsPath')).toEqual(
- '/sources/123/schema_errors/123'
+ '/sources/123/schemas/123'
);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx
index 65ed988f45ff0..a0efebdcb5a48 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx
@@ -20,11 +20,12 @@ import {
EuiPanel,
} from '@elastic/eui';
-import { Loading } from '../../../../../shared/loading';
import { SchemaAddFieldModal, SchemaErrorsCallout } from '../../../../../shared/schema';
import { AppLogic } from '../../../../app_logic';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
+import { NAV } from '../../../../constants';
import { getReindexJobRoute } from '../../../../routes';
+import { SourceLayout } from '../source_layout';
import {
SCHEMA_ADD_FIELD_BUTTON,
@@ -65,8 +66,6 @@ export const Schema: React.FC = () => {
initializeSchema();
}, []);
- if (dataLoading) return ;
-
const hasSchemaFields = Object.keys(activeSchema).length > 0;
const { hasErrors, activeReindexJobId } = mostRecentIndexJob;
@@ -77,7 +76,11 @@ export const Schema: React.FC = () => {
);
return (
- <>
+
{
closeAddFieldModal={closeAddFieldModal}
/>
)}
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx
index e300823aa3ed3..eb07beda73327 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx
@@ -12,6 +12,8 @@ import { useActions, useValues } from 'kea';
import { SchemaErrorsAccordion } from '../../../../../shared/schema';
import { ViewContentHeader } from '../../../../components/shared/view_content_header';
+import { NAV } from '../../../../constants';
+import { SourceLayout } from '../source_layout';
import { SCHEMA_ERRORS_HEADING } from './constants';
import { SchemaLogic } from './schema_logic';
@@ -30,9 +32,12 @@ export const SchemaChangeErrors: React.FC = () => {
}, []);
return (
- <>
+
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx
index 4bcc4b16166d1..9304f0f344a1b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx
@@ -25,7 +25,6 @@ import {
} from '@elastic/eui';
import { DEFAULT_META } from '../../../../shared/constants';
-import { Loading } from '../../../../shared/loading';
import { ComponentLoader } from '../../../components/shared/component_loader';
import { TablePaginationBar } from '../../../components/shared/table_pagination_bar';
@@ -61,13 +60,6 @@ describe('SourceContent', () => {
expect(wrapper.find(EuiTable)).toHaveLength(1);
});
- it('returns Loading when loading', () => {
- setMockValues({ ...mockValues, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
it('returns ComponentLoader when section loading', () => {
setMockValues({ ...mockValues, sectionLoading: true });
const wrapper = shallow( );
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx
index fbafe54df7493..a0e3c28f20eb0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx
@@ -31,12 +31,11 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { Loading } from '../../../../shared/loading';
import { TruncatedContent } from '../../../../shared/truncate';
import { ComponentLoader } from '../../../components/shared/component_loader';
import { TablePaginationBar } from '../../../components/shared/table_pagination_bar';
import { ViewContentHeader } from '../../../components/shared/view_content_header';
-import { CUSTOM_SERVICE_TYPE } from '../../../constants';
+import { NAV, CUSTOM_SERVICE_TYPE } from '../../../constants';
import { CUSTOM_SOURCE_DOCS_URL } from '../../../routes';
import { SourceContentItem } from '../../../types';
import {
@@ -51,6 +50,8 @@ import {
} from '../constants';
import { SourceLogic } from '../source_logic';
+import { SourceLayout } from './source_layout';
+
const MAX_LENGTH = 28;
export const SourceContent: React.FC = () => {
@@ -67,7 +68,6 @@ export const SourceContent: React.FC = () => {
},
contentItems,
contentFilterValue,
- dataLoading,
sectionLoading,
} = useValues(SourceLogic);
@@ -75,8 +75,6 @@ export const SourceContent: React.FC = () => {
searchContentSourceDocuments(id);
}, [contentFilterValue, activePage]);
- if (dataLoading) return ;
-
const showPagination = totalPages > 1;
const hasItems = totalItems > 0;
const emptyMessage = contentFilterValue
@@ -193,7 +191,7 @@ export const SourceContent: React.FC = () => {
);
return (
- <>
+
@@ -219,6 +217,6 @@ export const SourceContent: React.FC = () => {
{sectionLoading && }
{!sectionLoading && (hasItems ? contentTable : emptyState)}
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx
new file mode 100644
index 0000000000000..7c7d77ec418e7
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 '../../../../__mocks__/shallow_useeffect.mock';
+
+import { setMockValues } from '../../../../__mocks__/kea_logic';
+import { contentSources } from '../../../__mocks__/content_sources.mock';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiCallOut } from '@elastic/eui';
+
+import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../components/layout';
+
+import { SourceInfoCard } from './source_info_card';
+import { SourceLayout } from './source_layout';
+
+describe('SourceLayout', () => {
+ const contentSource = contentSources[1];
+ const mockValues = {
+ contentSource,
+ dataLoading: false,
+ isOrganization: true,
+ };
+
+ beforeEach(() => {
+ setMockValues({ ...mockValues });
+ });
+
+ it('renders', () => {
+ const wrapper = shallow(
+
+
+
+ );
+
+ expect(wrapper.find(SourceInfoCard)).toHaveLength(1);
+ expect(wrapper.find('.testChild')).toHaveLength(1);
+ });
+
+ it('renders the default Workplace Search layout when on an organization view', () => {
+ setMockValues({ ...mockValues, isOrganization: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate);
+ });
+
+ it('renders a personal dashboard layout when not on an organization view', () => {
+ setMockValues({ ...mockValues, isOrganization: false });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(PersonalDashboardLayout);
+ });
+
+ it('passes any page template props to the underlying page template', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(WorkplaceSearchPageTemplate).prop('pageViewTelemetry')).toEqual('test');
+ });
+
+ it('handles breadcrumbs while loading', () => {
+ setMockValues({
+ ...mockValues,
+ contentSource: {},
+ dataLoading: true,
+ });
+ const wrapper = shallow( );
+
+ expect(wrapper.prop('pageChrome')).toEqual(['Sources', '...']);
+ });
+
+ it('renders a callout when the source is not supported by the current license', () => {
+ setMockValues({ ...mockValues, contentSource: { supportedByLicense: false } });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiCallOut)).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx
new file mode 100644
index 0000000000000..446e93e0c61f3
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 { useValues } from 'kea';
+import moment from 'moment';
+
+import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
+
+import { PageTemplateProps } from '../../../../shared/layout';
+import { AppLogic } from '../../../app_logic';
+import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../components/layout';
+import { NAV } from '../../../constants';
+import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes';
+
+import {
+ SOURCE_DISABLED_CALLOUT_TITLE,
+ SOURCE_DISABLED_CALLOUT_DESCRIPTION,
+ SOURCE_DISABLED_CALLOUT_BUTTON,
+} from '../constants';
+import { SourceLogic } from '../source_logic';
+
+import { SourceInfoCard } from './source_info_card';
+
+export const SourceLayout: React.FC = ({
+ children,
+ pageChrome = [],
+ ...props
+}) => {
+ const { contentSource, dataLoading } = useValues(SourceLogic);
+ const { isOrganization } = useValues(AppLogic);
+
+ const {
+ name,
+ createdAt,
+ serviceType,
+ serviceName,
+ isFederatedSource,
+ supportedByLicense,
+ } = contentSource;
+
+ const pageHeader = (
+ <>
+
+
+ >
+ );
+
+ const callout = (
+ <>
+
+ {SOURCE_DISABLED_CALLOUT_DESCRIPTION}
+
+ {SOURCE_DISABLED_CALLOUT_BUTTON}
+
+
+
+ >
+ );
+
+ const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout;
+
+ return (
+
+ {!supportedByLicense && callout}
+ {pageHeader}
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx
index aa6cbf3cf6574..667e7fd4dbfb4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx
@@ -26,6 +26,8 @@ import { AppLogic } from '../../../app_logic';
import { ContentSection } from '../../../components/shared/content_section';
import { SourceConfigFields } from '../../../components/shared/source_config_fields';
import { ViewContentHeader } from '../../../components/shared/view_content_header';
+import { NAV } from '../../../constants';
+
import {
CANCEL_BUTTON,
OK_BUTTON,
@@ -52,6 +54,8 @@ import {
import { staticSourceData } from '../source_data';
import { SourceLogic } from '../source_logic';
+import { SourceLayout } from './source_layout';
+
export const SourceSettings: React.FC = () => {
const { updateContentSource, removeContentSource } = useActions(SourceLogic);
const { getSourceConfigData } = useActions(AddSourceLogic);
@@ -128,7 +132,7 @@ export const SourceSettings: React.FC = () => {
);
return (
- <>
+
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx
index 25c389419d731..7f07c59587f96 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx
@@ -7,34 +7,92 @@
import { setMockValues } from '../../../../__mocks__/kea_logic';
-import React from 'react';
+jest.mock('../../../../shared/layout', () => ({
+ generateNavLink: jest.fn(({ to }) => ({ href: to })),
+}));
-import { shallow } from 'enzyme';
+import { useSourceSubNav } from './source_sub_nav';
-import { SideNavLink } from '../../../../shared/layout';
-import { CUSTOM_SERVICE_TYPE } from '../../../constants';
+describe('useSourceSubNav', () => {
+ it('returns undefined when no content source id present', () => {
+ setMockValues({ contentSource: {} });
-import { SourceSubNav } from './source_sub_nav';
+ expect(useSourceSubNav()).toEqual(undefined);
+ });
-describe('SourceSubNav', () => {
- it('renders empty when no group id present', () => {
- setMockValues({ contentSource: {} });
- const wrapper = shallow( );
+ it('returns EUI nav items', () => {
+ setMockValues({ isOrganization: true, contentSource: { id: '1' } });
- expect(wrapper.find(SideNavLink)).toHaveLength(0);
+ expect(useSourceSubNav()).toEqual([
+ {
+ id: 'sourceOverview',
+ name: 'Overview',
+ href: '/sources/1',
+ },
+ {
+ id: 'sourceContent',
+ name: 'Content',
+ href: '/sources/1/content',
+ },
+ {
+ id: 'sourceSettings',
+ name: 'Settings',
+ href: '/sources/1/settings',
+ },
+ ]);
});
- it('renders nav items', () => {
- setMockValues({ contentSource: { id: '1' } });
- const wrapper = shallow( );
+ it('returns extra nav items for custom sources', () => {
+ setMockValues({ isOrganization: true, contentSource: { id: '2', serviceType: 'custom' } });
- expect(wrapper.find(SideNavLink)).toHaveLength(3);
+ expect(useSourceSubNav()).toEqual([
+ {
+ id: 'sourceOverview',
+ name: 'Overview',
+ href: '/sources/2',
+ },
+ {
+ id: 'sourceContent',
+ name: 'Content',
+ href: '/sources/2/content',
+ },
+ {
+ id: 'sourceSchema',
+ name: 'Schema',
+ href: '/sources/2/schemas',
+ },
+ {
+ id: 'sourceDisplaySettings',
+ name: 'Display Settings',
+ href: '/sources/2/display_settings',
+ },
+ {
+ id: 'sourceSettings',
+ name: 'Settings',
+ href: '/sources/2/settings',
+ },
+ ]);
});
- it('renders custom source nav items', () => {
- setMockValues({ contentSource: { id: '1', serviceType: CUSTOM_SERVICE_TYPE } });
- const wrapper = shallow( );
+ it('returns nav links to personal dashboard when not on an organization page', () => {
+ setMockValues({ isOrganization: false, contentSource: { id: '3' } });
- expect(wrapper.find(SideNavLink)).toHaveLength(5);
+ expect(useSourceSubNav()).toEqual([
+ {
+ id: 'sourceOverview',
+ name: 'Overview',
+ href: '/p/sources/3',
+ },
+ {
+ id: 'sourceContent',
+ name: 'Content',
+ href: '/p/sources/3/content',
+ },
+ {
+ id: 'sourceSettings',
+ name: 'Settings',
+ href: '/p/sources/3/settings',
+ },
+ ]);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx
index 12e1506ec6efd..6b595a06f0404 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx
@@ -5,11 +5,11 @@
* 2.0.
*/
-import React from 'react';
-
import { useValues } from 'kea';
-import { SideNavLink } from '../../../../shared/layout';
+import { EuiSideNavItemType } from '@elastic/eui';
+
+import { generateNavLink } from '../../../../shared/layout';
import { AppLogic } from '../../../app_logic';
import { NAV, CUSTOM_SERVICE_TYPE } from '../../../constants';
import {
@@ -22,40 +22,52 @@ import {
} from '../../../routes';
import { SourceLogic } from '../source_logic';
-export const SourceSubNav: React.FC = () => {
+export const useSourceSubNav = () => {
const { isOrganization } = useValues(AppLogic);
const {
contentSource: { id, serviceType },
} = useValues(SourceLogic);
- if (!id) return null;
+ if (!id) return undefined;
+
+ const navItems: Array> = [
+ {
+ id: 'sourceOverview',
+ name: NAV.OVERVIEW,
+ ...generateNavLink({ to: getContentSourcePath(SOURCE_DETAILS_PATH, id, isOrganization) }),
+ },
+ {
+ id: 'sourceContent',
+ name: NAV.CONTENT,
+ ...generateNavLink({ to: getContentSourcePath(SOURCE_CONTENT_PATH, id, isOrganization) }),
+ },
+ ];
const isCustom = serviceType === CUSTOM_SERVICE_TYPE;
+ if (isCustom) {
+ navItems.push({
+ id: 'sourceSchema',
+ name: NAV.SCHEMA,
+ ...generateNavLink({
+ to: getContentSourcePath(SOURCE_SCHEMAS_PATH, id, isOrganization),
+ shouldShowActiveForSubroutes: true,
+ }),
+ });
+ navItems.push({
+ id: 'sourceDisplaySettings',
+ name: NAV.DISPLAY_SETTINGS,
+ ...generateNavLink({
+ to: getContentSourcePath(SOURCE_DISPLAY_SETTINGS_PATH, id, isOrganization),
+ shouldShowActiveForSubroutes: true,
+ }),
+ });
+ }
+
+ navItems.push({
+ id: 'sourceSettings',
+ name: NAV.SETTINGS,
+ ...generateNavLink({ to: getContentSourcePath(SOURCE_SETTINGS_PATH, id, isOrganization) }),
+ });
- return (
-
-
- {NAV.OVERVIEW}
-
-
- {NAV.CONTENT}
-
- {isCustom && (
- <>
-
- {NAV.SCHEMA}
-
-
- {NAV.DISPLAY_SETTINGS}
-
- >
- )}
-
- {NAV.SETTINGS}
-
-
- );
+ return navItems;
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx
index 9df91406c4b7b..2317c84af2432 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx
@@ -10,14 +10,10 @@ import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic';
import { contentSources } from '../../__mocks__/content_sources.mock';
import React from 'react';
-import { Redirect } from 'react-router-dom';
import { shallow } from 'enzyme';
-import { Loading } from '../../../shared/loading';
import { SourcesTable } from '../../components/shared/sources_table';
-import { ViewContentHeader } from '../../components/shared/view_content_header';
-import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes';
import { OrganizationSources } from './organization_sources';
@@ -42,20 +38,12 @@ describe('OrganizationSources', () => {
const wrapper = shallow( );
expect(wrapper.find(SourcesTable)).toHaveLength(1);
- expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
});
- it('returns loading when loading', () => {
+ it('does not render a page header when data is loading (to prevent a jump after redirect)', () => {
setMockValues({ ...mockValues, dataLoading: true });
const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
- it('returns redirect when no sources', () => {
- setMockValues({ ...mockValues, contentSources: [] });
- const wrapper = shallow( );
-
- expect(wrapper.find(Redirect).prop('to')).toEqual(getSourcesPath(ADD_SOURCE_PATH, true));
+ expect(wrapper.prop('pageHeader')).toBeUndefined();
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx
index 4559003b4597f..a4273ae2ae6a2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx
@@ -6,16 +6,15 @@
*/
import React, { useEffect } from 'react';
-import { Link, Redirect } from 'react-router-dom';
+import { Redirect } from 'react-router-dom';
import { useActions, useValues } from 'kea';
-import { EuiButton } from '@elastic/eui';
-
-import { Loading } from '../../../shared/loading';
+import { EuiButtonTo } from '../../../shared/react_router_helpers';
+import { WorkplaceSearchPageTemplate } from '../../components/layout';
import { ContentSection } from '../../components/shared/content_section';
import { SourcesTable } from '../../components/shared/sources_table';
-import { ViewContentHeader } from '../../components/shared/view_content_header';
+import { NAV } from '../../constants';
import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes';
import {
@@ -36,33 +35,41 @@ export const OrganizationSources: React.FC = () => {
const { dataLoading, contentSources } = useValues(SourcesLogic);
- if (dataLoading) return ;
-
- if (contentSources.length === 0) return ;
-
return (
-
-
-
- {ORG_SOURCES_LINK}
-
-
- }
- description={ORG_SOURCES_HEADER_DESCRIPTION}
- alignItems="flexStart"
- />
-
-
-
-
-
+
+ {ORG_SOURCES_LINK}
+ ,
+ ],
+ }
+ }
+ isLoading={dataLoading}
+ isEmptyState={!contentSources.length}
+ emptyState={ }
+ >
+
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx
index 08f560c984344..e2b0dfba1fa97 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx
@@ -15,7 +15,6 @@ import { shallow } from 'enzyme';
import { EuiCallOut, EuiEmptyPrompt } from '@elastic/eui';
-import { Loading } from '../../../shared/loading';
import { ContentSection } from '../../components/shared/content_section';
import { SourcesTable } from '../../components/shared/sources_table';
@@ -43,13 +42,6 @@ describe('PrivateSources', () => {
expect(wrapper.find(SourcesView)).toHaveLength(1);
});
- it('renders Loading when loading', () => {
- setMockValues({ ...mockValues, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
it('renders only shared sources section when canCreatePersonalSources is false', () => {
setMockValues({ ...mockValues });
const wrapper = shallow( );
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx
index 128c65eeb95da..693c1e8bd5e40 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx
@@ -13,12 +13,13 @@ import { EuiCallOut, EuiEmptyPrompt, EuiSpacer, EuiPanel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { LicensingLogic } from '../../../shared/licensing';
-import { Loading } from '../../../shared/loading';
import { EuiButtonTo } from '../../../shared/react_router_helpers';
import { AppLogic } from '../../app_logic';
import noSharedSourcesIcon from '../../assets/share_circle.svg';
+import { PersonalDashboardLayout } from '../../components/layout';
import { ContentSection } from '../../components/shared/content_section';
import { SourcesTable } from '../../components/shared/sources_table';
+import { NAV } from '../../constants';
import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes';
import { toSentenceSerial } from '../../utils';
@@ -53,8 +54,6 @@ export const PrivateSources: React.FC = () => {
account: { canCreatePersonalSources, groups },
} = useValues(AppLogic);
- if (dataLoading) return ;
-
const hasConfiguredConnectors = serviceTypes.some(({ configured }) => configured);
const canAddSources = canCreatePersonalSources && hasConfiguredConnectors;
const hasPrivateSources = privateContentSources?.length > 0;
@@ -144,10 +143,12 @@ export const PrivateSources: React.FC = () => {
);
return (
-
- {hasPrivateSources && !hasPlatinumLicense && licenseCallout}
- {canCreatePersonalSources && privateSourcesSection}
- {sharedSourcesSection}
-
+
+
+ {hasPrivateSources && !hasPlatinumLicense && licenseCallout}
+ {canCreatePersonalSources && privateSourcesSection}
+ {sharedSourcesSection}
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx
index 783fc434fe8e5..afe0d1f89faea 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx
@@ -5,21 +5,17 @@
* 2.0.
*/
-import '../../../__mocks__/shallow_useeffect.mock';
-
import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic';
-import { mockLocation, mockUseParams } from '../../../__mocks__/react_router';
+import { mockUseParams } from '../../../__mocks__/react_router';
import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock';
import { contentSources } from '../../__mocks__/content_sources.mock';
import React from 'react';
-import { Route, Switch } from 'react-router-dom';
+import { Route } from 'react-router-dom';
import { shallow } from 'enzyme';
-import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { Loading } from '../../../shared/loading';
-import { NAV } from '../../constants';
+import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout';
import { DisplaySettingsRouter } from './components/display_settings';
import { Overview } from './components/overview';
@@ -37,6 +33,7 @@ describe('SourceRouter', () => {
const mockValues = {
contentSource,
dataLoading: false,
+ isOrganization: true,
};
beforeEach(() => {
@@ -50,11 +47,41 @@ describe('SourceRouter', () => {
}));
});
- it('returns Loading when loading', () => {
- setMockValues({ ...mockValues, dataLoading: true });
- const wrapper = shallow( );
+ describe('mount/unmount events', () => {
+ it('fetches & initializes source data on mount', () => {
+ shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(initializeSource).toHaveBeenCalledWith(contentSource.id);
+ });
+
+ it('resets state on unmount', () => {
+ shallow( );
+ unmountHandler();
+
+ expect(resetSourceState).toHaveBeenCalled();
+ });
+ });
+
+ describe('loading state when fetching source data', () => {
+ // NOTE: The early page isLoading returns are required to prevent a flash of a completely empty
+ // page (instead of preserving the layout/side nav while loading). We also cannot let the code
+ // fall through to the router because some routes are conditionally rendered based on isCustomSource.
+
+ it('returns an empty loading Workplace Search page on organization views', () => {
+ setMockValues({ ...mockValues, dataLoading: true, isOrganization: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate);
+ expect(wrapper.prop('isLoading')).toEqual(true);
+ });
+
+ it('returns an empty loading personal dashboard page when not on an organization view', () => {
+ setMockValues({ ...mockValues, dataLoading: true, isOrganization: false });
+ const wrapper = shallow( );
+
+ expect(wrapper.type()).toEqual(PersonalDashboardLayout);
+ expect(wrapper.prop('isLoading')).toEqual(true);
+ });
});
it('renders source routes (standard)', () => {
@@ -63,7 +90,6 @@ describe('SourceRouter', () => {
expect(wrapper.find(Overview)).toHaveLength(1);
expect(wrapper.find(SourceSettings)).toHaveLength(1);
expect(wrapper.find(SourceContent)).toHaveLength(1);
- expect(wrapper.find(Switch)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(3);
});
@@ -76,55 +102,4 @@ describe('SourceRouter', () => {
expect(wrapper.find(SchemaChangeErrors)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(6);
});
-
- it('handles breadcrumbs while loading (standard)', () => {
- setMockValues({
- ...mockValues,
- contentSource: {},
- });
-
- const loadingBreadcrumbs = ['Sources', '...'];
-
- const wrapper = shallow( );
-
- const overviewBreadCrumb = wrapper.find(SetPageChrome).at(0);
- const contentBreadCrumb = wrapper.find(SetPageChrome).at(1);
- const settingsBreadCrumb = wrapper.find(SetPageChrome).at(2);
-
- expect(overviewBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs]);
- expect(contentBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.CONTENT]);
- expect(settingsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SETTINGS]);
- });
-
- it('handles breadcrumbs while loading (custom)', () => {
- setMockValues({
- ...mockValues,
- contentSource: { serviceType: 'custom' },
- });
-
- const loadingBreadcrumbs = ['Sources', '...'];
-
- const wrapper = shallow( );
-
- const schemaBreadCrumb = wrapper.find(SetPageChrome).at(2);
- const schemaErrorsBreadCrumb = wrapper.find(SetPageChrome).at(3);
- const displaySettingsBreadCrumb = wrapper.find(SetPageChrome).at(4);
-
- expect(schemaBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]);
- expect(schemaErrorsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]);
- expect(displaySettingsBreadCrumb.prop('trail')).toEqual([
- ...loadingBreadcrumbs,
- NAV.DISPLAY_SETTINGS,
- ]);
- });
-
- describe('reset state', () => {
- it('resets state when leaving source tree', () => {
- mockLocation.pathname = '/home';
- shallow( );
- unmountHandler();
-
- expect(resetSourceState).toHaveBeenCalled();
- });
- });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx
index d5d6c8e541e4f..bf68a60757c0d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx
@@ -10,18 +10,11 @@ import React, { useEffect } from 'react';
import { Route, Switch, useLocation, useParams } from 'react-router-dom';
import { useActions, useValues } from 'kea';
-import moment from 'moment';
-import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
-
-import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { Loading } from '../../../shared/loading';
-import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
import { AppLogic } from '../../app_logic';
-import { NAV } from '../../constants';
+import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout';
import { CUSTOM_SERVICE_TYPE } from '../../constants';
import {
- ENT_SEARCH_LICENSE_MANAGEMENT,
REINDEX_JOB_PATH,
SOURCE_DETAILS_PATH,
SOURCE_CONTENT_PATH,
@@ -37,13 +30,7 @@ import { Overview } from './components/overview';
import { Schema } from './components/schema';
import { SchemaChangeErrors } from './components/schema/schema_change_errors';
import { SourceContent } from './components/source_content';
-import { SourceInfoCard } from './components/source_info_card';
import { SourceSettings } from './components/source_settings';
-import {
- SOURCE_DISABLED_CALLOUT_TITLE,
- SOURCE_DISABLED_CALLOUT_DESCRIPTION,
- SOURCE_DISABLED_CALLOUT_BUTTON,
-} from './constants';
import { SourceLogic } from './source_logic';
export const SourceRouter: React.FC = () => {
@@ -61,84 +48,43 @@ export const SourceRouter: React.FC = () => {
return resetSourceState;
}, []);
- if (dataLoading) return ;
+ if (dataLoading) {
+ return isOrganization ? (
+
+ ) : (
+
+ );
+ }
- const {
- name,
- createdAt,
- serviceType,
- serviceName,
- isFederatedSource,
- supportedByLicense,
- } = contentSource;
+ const { serviceType } = contentSource;
const isCustomSource = serviceType === CUSTOM_SERVICE_TYPE;
- const pageHeader = (
- <>
-
-
- >
- );
-
- const callout = (
- <>
-
- {SOURCE_DISABLED_CALLOUT_DESCRIPTION}
-
- {SOURCE_DISABLED_CALLOUT_BUTTON}
-
-
-
- >
- );
-
return (
- <>
- {!supportedByLicense && callout}
- {pageHeader}
-
-
-
-
-
+
+
+
+
+
+
+
+ {isCustomSource && (
+
+
-
-
-
-
+ )}
+ {isCustomSource && (
+
+
- {isCustomSource && (
-
-
-
-
-
- )}
- {isCustomSource && (
-
-
-
-
-
- )}
- {isCustomSource && (
-
-
-
-
-
- )}
-
-
-
-
+ )}
+ {isCustomSource && (
+
+
-
- >
+ )}
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx
index 84bff65e62cef..2abdba07b5c88 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx
@@ -11,12 +11,8 @@ import { Redirect, Route, Switch, useLocation } from 'react-router-dom';
import { Location } from 'history';
import { useActions, useValues } from 'kea';
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { LicensingLogic } from '../../../shared/licensing';
-import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
import { AppLogic } from '../../app_logic';
-import { NAV } from '../../constants';
import {
ADD_SOURCE_PATH,
SOURCE_DETAILS_PATH,
@@ -52,71 +48,53 @@ export const SourcesRouter: React.FC = () => {
}, [pathname]);
return (
- <>
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {staticSourceData.map(({ addPath, accountContextOnly }, i) => (
+
+ {!hasPlatinumLicense && accountContextOnly ? (
+
+ ) : (
+
+ )}
-
-
-
-
+ ))}
+ {staticSourceData.map(({ addPath }, i) => (
+
+
- {staticSourceData.map(({ addPath, accountContextOnly, name }, i) => (
-
-
- {!hasPlatinumLicense && accountContextOnly ? (
-
- ) : (
-
- )}
-
- ))}
- {staticSourceData.map(({ addPath, name }, i) => (
-
-
-
-
- ))}
- {staticSourceData.map(({ addPath, name }, i) => (
-
-
-
-
- ))}
- {staticSourceData.map(({ addPath, name, configuration: { needsConfiguration } }, i) => {
- if (needsConfiguration)
- return (
-
-
-
-
- );
- })}
- {canCreatePersonalSources ? (
-
-
-
-
-
- ) : (
-
- )}
-
-
-
+ ))}
+ {staticSourceData.map(({ addPath }, i) => (
+
+
-
-
+ ))}
+ {staticSourceData.map(({ addPath, configuration: { needsConfiguration } }, i) => {
+ if (needsConfiguration)
+ return (
+
+
+
+ );
+ })}
+ {canCreatePersonalSources ? (
+
+
-
- >
+ ) : (
+
+ )}
+
+
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx
index cf23470e8155e..7bd40d6f04a56 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx
@@ -25,6 +25,13 @@ describe('Overview', () => {
expect(mockActions.initializeOverview).toHaveBeenCalled();
});
+ it('does not render a page header when data is loading (to prevent a jump between non/onboarding headers)', () => {
+ setMockValues({ dataLoading: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.prop('pageHeader')).toBeUndefined();
+ });
+
it('renders onboarding state', () => {
setMockValues({ dataLoading: false });
const wrapper = shallow( );
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx
index 0049c5b732d3d..c51fdb64b8f26 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx
@@ -53,17 +53,15 @@ export const Overview: React.FC = () => {
const hideOnboarding = hasUsers && hasOrgSources && isOldAccount && orgName !== defaultOrgName;
- const headerTitle = dataLoading || hideOnboarding ? HEADER_TITLE : ONBOARDING_HEADER_TITLE;
- const headerDescription =
- dataLoading || hideOnboarding ? HEADER_DESCRIPTION : ONBOARDING_HEADER_DESCRIPTION;
+ const headerTitle = hideOnboarding ? HEADER_TITLE : ONBOARDING_HEADER_TITLE;
+ const headerDescription = hideOnboarding ? HEADER_DESCRIPTION : ONBOARDING_HEADER_DESCRIPTION;
return (
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx
index b32e3af021827..35619d2b2d560 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx
@@ -40,6 +40,13 @@ describe('SourceConfig', () => {
expect(wrapper.find(EuiConfirmModal)).toHaveLength(1);
});
+ it('renders a breadcrumb fallback while data is loading', () => {
+ setMockValues({ dataLoading: true, sourceConfigData: {} });
+ const wrapper = shallow( );
+
+ expect(wrapper.prop('pageChrome')).toEqual(['Settings', 'Content source connectors', '...']);
+ });
+
it('handles delete click', () => {
const wrapper = shallow( );
const saveConfig = wrapper.find(SaveConfig);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx
index f1dfda78ee13f..c2a0b60e1eca3 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx
@@ -47,7 +47,7 @@ export const SourceConfig: React.FC = ({ sourceIndex }) => {
return (
Date: Mon, 21 Jun 2021 21:20:41 -0400
Subject: [PATCH 043/163] [Fleet] Correctly check for degraded status in agent
healthbar (#102821)
---
.../fleet/common/services/agent_status.ts | 2 +-
.../apis/fleet_telemetry.ts | 16 ++++++++++------
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/x-pack/plugins/fleet/common/services/agent_status.ts b/x-pack/plugins/fleet/common/services/agent_status.ts
index df5de6ad98191..b8a59e6447723 100644
--- a/x-pack/plugins/fleet/common/services/agent_status.ts
+++ b/x-pack/plugins/fleet/common/services/agent_status.ts
@@ -54,7 +54,7 @@ export function buildKueryForOnlineAgents() {
}
export function buildKueryForErrorAgents() {
- return 'last_checkin_status:error or .last_checkin_status:degraded';
+ return 'last_checkin_status:error or last_checkin_status:degraded';
}
export function buildKueryForOfflineAgents() {
diff --git a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts
index 5e4a580473dd1..36eef019f7bf7 100644
--- a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts
+++ b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts
@@ -21,9 +21,12 @@ export default function (providerContext: FtrProviderContext) {
let data: any = {};
switch (status) {
- case 'unhealthy':
+ case 'error':
data = { last_checkin_status: 'error' };
break;
+ case 'degraded':
+ data = { last_checkin_status: 'degraded' };
+ break;
case 'offline':
data = { last_checkin: '2017-06-07T18:59:04.498Z' };
break;
@@ -85,12 +88,13 @@ export default function (providerContext: FtrProviderContext) {
// Default Fleet Server
await generateAgent('healthy', defaultFleetServerPolicy.id);
await generateAgent('healthy', defaultFleetServerPolicy.id);
- await generateAgent('unhealthy', defaultFleetServerPolicy.id);
+ await generateAgent('error', defaultFleetServerPolicy.id);
// Default policy
await generateAgent('healthy', defaultServerPolicy.id);
await generateAgent('offline', defaultServerPolicy.id);
- await generateAgent('unhealthy', defaultServerPolicy.id);
+ await generateAgent('error', defaultServerPolicy.id);
+ await generateAgent('degraded', defaultServerPolicy.id);
});
it('should return the correct telemetry values for fleet', async () => {
@@ -105,12 +109,12 @@ export default function (providerContext: FtrProviderContext) {
.expect(200);
expect(apiResponse.stack_stats.kibana.plugins.fleet.agents).eql({
- total_enrolled: 6,
+ total_enrolled: 7,
healthy: 3,
- unhealthy: 2,
+ unhealthy: 3,
offline: 1,
updating: 0,
- total_all_statuses: 6,
+ total_all_statuses: 7,
});
expect(apiResponse.stack_stats.kibana.plugins.fleet.fleet_server).eql({
From 42fc79742f38275d84a224eb04b6a25f93dd78c4 Mon Sep 17 00:00:00 2001
From: Ahmad Bamieh
Date: Tue, 22 Jun 2021 06:21:35 +0300
Subject: [PATCH 044/163] [Telemetry] Track event loop delays on the server
(#101580)
---
.../collectors/event_loop_delays/constants.ts | 37 +++++
.../event_loop_delays.mocks.ts | 49 +++++++
.../event_loop_delays.test.ts | 135 ++++++++++++++++++
.../event_loop_delays/event_loop_delays.ts | 109 ++++++++++++++
.../event_loop_delays_usage_collector.test.ts | 84 +++++++++++
.../event_loop_delays_usage_collector.ts | 53 +++++++
.../collectors/event_loop_delays/index.ts | 11 ++
.../event_loop_delays/rollups/daily.test.ts | 81 +++++++++++
.../event_loop_delays/rollups/daily.ts | 35 +++++
.../event_loop_delays/rollups/index.ts | 9 ++
.../integration_tests/daily_rollups.test.ts | 94 ++++++++++++
.../event_loop_delays/saved_objects.test.ts | 122 ++++++++++++++++
.../event_loop_delays/saved_objects.ts | 72 ++++++++++
.../collectors/event_loop_delays/schema.ts | 111 ++++++++++++++
.../server/collectors/index.ts | 1 +
.../server/plugin.test.ts | 5 +-
.../kibana_usage_collection/server/plugin.ts | 33 +++--
src/plugins/telemetry/schema/oss_plugins.json | 87 +++++++++++
18 files changed, 1114 insertions(+), 14 deletions(-)
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.ts
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/index.ts
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.ts
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.ts
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/index.ts
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts
create mode 100644 src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts
new file mode 100644
index 0000000000000..1753c87c9d005
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+/**
+ * Roll daily indices every 24h
+ */
+export const ROLL_DAILY_INDICES_INTERVAL = 24 * 60 * 60 * 1000;
+
+/**
+ * Start rolling indices after 5 minutes up
+ */
+export const ROLL_INDICES_START = 5 * 60 * 1000;
+
+/**
+ * Reset the event loop delay historgram every 1 hour
+ */
+export const MONITOR_EVENT_LOOP_DELAYS_INTERVAL = 1 * 60 * 60 * 1000;
+
+/**
+ * Reset the event loop delay historgram every 24h
+ */
+export const MONITOR_EVENT_LOOP_DELAYS_RESET = 24 * 60 * 60 * 1000;
+
+/**
+ * Start monitoring the event loop delays after 1 minute
+ */
+export const MONITOR_EVENT_LOOP_DELAYS_START = 1 * 60 * 1000;
+
+/**
+ * Event loop monitoring sampling rate in milliseconds.
+ */
+export const MONITOR_EVENT_LOOP_DELAYS_RESOLUTION = 10;
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts
new file mode 100644
index 0000000000000..6b03d3cc5cbd1
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import moment from 'moment';
+import type { IntervalHistogram } from './event_loop_delays';
+
+export const mockMonitorEnable = jest.fn();
+export const mockMonitorPercentile = jest.fn();
+export const mockMonitorReset = jest.fn();
+export const mockMonitorDisable = jest.fn();
+export const monitorEventLoopDelay = jest.fn().mockReturnValue({
+ enable: mockMonitorEnable,
+ percentile: mockMonitorPercentile,
+ disable: mockMonitorDisable,
+ reset: mockMonitorReset,
+});
+
+jest.doMock('perf_hooks', () => ({
+ monitorEventLoopDelay,
+}));
+
+function createMockHistogram(overwrites: Partial = {}): IntervalHistogram {
+ const now = moment();
+
+ return {
+ min: 9093120,
+ max: 53247999,
+ mean: 11993238.600747818,
+ exceeds: 0,
+ stddev: 1168191.9357543814,
+ fromTimestamp: now.startOf('day').toISOString(),
+ lastUpdatedAt: now.toISOString(),
+ percentiles: {
+ '50': 12607487,
+ '75': 12615679,
+ '95': 12648447,
+ '99': 12713983,
+ },
+ ...overwrites,
+ };
+}
+
+export const mocked = {
+ createHistogram: createMockHistogram,
+};
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts
new file mode 100644
index 0000000000000..d03236a9756b3
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts
@@ -0,0 +1,135 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { Subject } from 'rxjs';
+
+import {
+ mockMonitorEnable,
+ mockMonitorPercentile,
+ monitorEventLoopDelay,
+ mockMonitorReset,
+ mockMonitorDisable,
+} from './event_loop_delays.mocks';
+import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks';
+import { startTrackingEventLoopDelaysUsage, EventLoopDelaysCollector } from './event_loop_delays';
+
+describe('EventLoopDelaysCollector', () => {
+ jest.useFakeTimers('modern');
+ const mockNow = jest.getRealSystemTime();
+ jest.setSystemTime(mockNow);
+
+ beforeEach(() => jest.clearAllMocks());
+ afterAll(() => jest.useRealTimers());
+
+ test('#constructor enables monitoring', () => {
+ new EventLoopDelaysCollector();
+ expect(monitorEventLoopDelay).toBeCalledWith({ resolution: 10 });
+ expect(mockMonitorEnable).toBeCalledTimes(1);
+ });
+
+ test('#collect returns event loop delays histogram', () => {
+ const eventLoopDelaysCollector = new EventLoopDelaysCollector();
+ const histogramData = eventLoopDelaysCollector.collect();
+ expect(mockMonitorPercentile).toHaveBeenNthCalledWith(1, 50);
+ expect(mockMonitorPercentile).toHaveBeenNthCalledWith(2, 75);
+ expect(mockMonitorPercentile).toHaveBeenNthCalledWith(3, 95);
+ expect(mockMonitorPercentile).toHaveBeenNthCalledWith(4, 99);
+
+ expect(Object.keys(histogramData)).toMatchInlineSnapshot(`
+ Array [
+ "min",
+ "max",
+ "mean",
+ "exceeds",
+ "stddev",
+ "fromTimestamp",
+ "lastUpdatedAt",
+ "percentiles",
+ ]
+ `);
+ });
+ test('#reset resets histogram data', () => {
+ const eventLoopDelaysCollector = new EventLoopDelaysCollector();
+ eventLoopDelaysCollector.reset();
+ expect(mockMonitorReset).toBeCalledTimes(1);
+ });
+ test('#stop disables monitoring event loop delays', () => {
+ const eventLoopDelaysCollector = new EventLoopDelaysCollector();
+ eventLoopDelaysCollector.stop();
+ expect(mockMonitorDisable).toBeCalledTimes(1);
+ });
+});
+
+describe('startTrackingEventLoopDelaysUsage', () => {
+ const mockInternalRepository = savedObjectsRepositoryMock.create();
+ const stopMonitoringEventLoop$ = new Subject();
+
+ beforeAll(() => jest.useFakeTimers('modern'));
+ beforeEach(() => jest.clearAllMocks());
+ afterEach(() => stopMonitoringEventLoop$.next());
+
+ it('initializes EventLoopDelaysCollector and starts timer', () => {
+ const collectionStartDelay = 1000;
+ startTrackingEventLoopDelaysUsage(
+ mockInternalRepository,
+ stopMonitoringEventLoop$,
+ collectionStartDelay
+ );
+
+ expect(monitorEventLoopDelay).toBeCalledTimes(1);
+ expect(mockMonitorPercentile).toBeCalledTimes(0);
+ jest.advanceTimersByTime(collectionStartDelay);
+ expect(mockMonitorPercentile).toBeCalled();
+ });
+
+ it('stores event loop delays every collectionInterval duration', () => {
+ const collectionStartDelay = 100;
+ const collectionInterval = 1000;
+ startTrackingEventLoopDelaysUsage(
+ mockInternalRepository,
+ stopMonitoringEventLoop$,
+ collectionStartDelay,
+ collectionInterval
+ );
+
+ expect(mockInternalRepository.create).toBeCalledTimes(0);
+ jest.advanceTimersByTime(collectionStartDelay);
+ expect(mockInternalRepository.create).toBeCalledTimes(1);
+ jest.advanceTimersByTime(collectionInterval);
+ expect(mockInternalRepository.create).toBeCalledTimes(2);
+ jest.advanceTimersByTime(collectionInterval);
+ expect(mockInternalRepository.create).toBeCalledTimes(3);
+ });
+
+ it('resets histogram every histogramReset duration', () => {
+ const collectionStartDelay = 0;
+ const collectionInterval = 1000;
+ const histogramReset = 5000;
+ startTrackingEventLoopDelaysUsage(
+ mockInternalRepository,
+ stopMonitoringEventLoop$,
+ collectionStartDelay,
+ collectionInterval,
+ histogramReset
+ );
+
+ expect(mockMonitorReset).toBeCalledTimes(0);
+ jest.advanceTimersByTime(collectionInterval * 5);
+ expect(mockMonitorReset).toBeCalledTimes(1);
+ jest.advanceTimersByTime(collectionInterval * 5);
+ expect(mockMonitorReset).toBeCalledTimes(2);
+ });
+
+ it('stops monitoring event loop delays once stopMonitoringEventLoop$.next is called', () => {
+ startTrackingEventLoopDelaysUsage(mockInternalRepository, stopMonitoringEventLoop$);
+
+ expect(mockMonitorDisable).toBeCalledTimes(0);
+ stopMonitoringEventLoop$.next();
+ expect(mockMonitorDisable).toBeCalledTimes(1);
+ });
+});
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts
new file mode 100644
index 0000000000000..655cba580fc5d
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts
@@ -0,0 +1,109 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { EventLoopDelayMonitor } from 'perf_hooks';
+import { monitorEventLoopDelay } from 'perf_hooks';
+import { takeUntil, finalize, map } from 'rxjs/operators';
+import { Observable, timer } from 'rxjs';
+import type { ISavedObjectsRepository } from 'kibana/server';
+import {
+ MONITOR_EVENT_LOOP_DELAYS_START,
+ MONITOR_EVENT_LOOP_DELAYS_INTERVAL,
+ MONITOR_EVENT_LOOP_DELAYS_RESET,
+ MONITOR_EVENT_LOOP_DELAYS_RESOLUTION,
+} from './constants';
+import { storeHistogram } from './saved_objects';
+
+export interface IntervalHistogram {
+ fromTimestamp: string;
+ lastUpdatedAt: string;
+ min: number;
+ max: number;
+ mean: number;
+ exceeds: number;
+ stddev: number;
+ percentiles: {
+ 50: number;
+ 75: number;
+ 95: number;
+ 99: number;
+ };
+}
+
+export class EventLoopDelaysCollector {
+ private readonly loopMonitor: EventLoopDelayMonitor;
+ private fromTimestamp: Date;
+
+ constructor() {
+ const monitor = monitorEventLoopDelay({
+ resolution: MONITOR_EVENT_LOOP_DELAYS_RESOLUTION,
+ });
+ monitor.enable();
+ this.fromTimestamp = new Date();
+ this.loopMonitor = monitor;
+ }
+
+ public collect(): IntervalHistogram {
+ const { min, max, mean, exceeds, stddev } = this.loopMonitor;
+
+ return {
+ min,
+ max,
+ mean,
+ exceeds,
+ stddev,
+ fromTimestamp: this.fromTimestamp.toISOString(),
+ lastUpdatedAt: new Date().toISOString(),
+ percentiles: {
+ 50: this.loopMonitor.percentile(50),
+ 75: this.loopMonitor.percentile(75),
+ 95: this.loopMonitor.percentile(95),
+ 99: this.loopMonitor.percentile(99),
+ },
+ };
+ }
+
+ public reset() {
+ this.loopMonitor.reset();
+ this.fromTimestamp = new Date();
+ }
+
+ public stop() {
+ this.loopMonitor.disable();
+ }
+}
+
+/**
+ * The monitoring of the event loop starts immediately.
+ * The first collection of the histogram happens after 1 minute.
+ * The daily histogram data is updated every 1 hour.
+ */
+export function startTrackingEventLoopDelaysUsage(
+ internalRepository: ISavedObjectsRepository,
+ stopMonitoringEventLoop$: Observable,
+ collectionStartDelay = MONITOR_EVENT_LOOP_DELAYS_START,
+ collectionInterval = MONITOR_EVENT_LOOP_DELAYS_INTERVAL,
+ histogramReset = MONITOR_EVENT_LOOP_DELAYS_RESET
+) {
+ const eventLoopDelaysCollector = new EventLoopDelaysCollector();
+
+ const resetOnCount = Math.ceil(histogramReset / collectionInterval);
+ timer(collectionStartDelay, collectionInterval)
+ .pipe(
+ map((i) => (i + 1) % resetOnCount === 0),
+ takeUntil(stopMonitoringEventLoop$),
+ finalize(() => eventLoopDelaysCollector.stop())
+ )
+ .subscribe(async (shouldReset) => {
+ const histogram = eventLoopDelaysCollector.collect();
+ if (shouldReset) {
+ eventLoopDelaysCollector.reset();
+ }
+ await storeHistogram(histogram, internalRepository);
+ });
+}
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts
new file mode 100644
index 0000000000000..06c51f6afa3a8
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts
@@ -0,0 +1,84 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import {
+ Collector,
+ createUsageCollectionSetupMock,
+ createCollectorFetchContextMock,
+} from '../../../../usage_collection/server/mocks';
+import { registerEventLoopDelaysCollector } from './event_loop_delays_usage_collector';
+import { loggingSystemMock, savedObjectsRepositoryMock } from '../../../../../core/server/mocks';
+import type { SavedObjectsFindResponse } from '../../../../../core/server';
+
+const logger = loggingSystemMock.createLogger();
+
+describe('registerEventLoopDelaysCollector', () => {
+ let collector: Collector;
+ const mockRegisterType = jest.fn();
+ const mockInternalRepository = savedObjectsRepositoryMock.create();
+ const mockGetSavedObjectsClient = () => mockInternalRepository;
+
+ const usageCollectionMock = createUsageCollectionSetupMock();
+ usageCollectionMock.makeUsageCollector.mockImplementation((config) => {
+ collector = new Collector(logger, config);
+ return createUsageCollectionSetupMock().makeUsageCollector(config);
+ });
+
+ const collectorFetchContext = createCollectorFetchContextMock();
+
+ beforeAll(() => {
+ registerEventLoopDelaysCollector(
+ logger,
+ usageCollectionMock,
+ mockRegisterType,
+ mockGetSavedObjectsClient
+ );
+ });
+
+ it('registers event_loop_delays collector', () => {
+ expect(collector).not.toBeUndefined();
+ expect(collector.type).toBe('event_loop_delays');
+ });
+
+ it('registers savedObjectType "event_loop_delays_daily"', () => {
+ expect(mockRegisterType).toBeCalledTimes(1);
+ expect(mockRegisterType).toBeCalledWith(
+ expect.objectContaining({
+ name: 'event_loop_delays_daily',
+ })
+ );
+ });
+
+ it('returns objects from event_loop_delays_daily from fetch function', async () => {
+ const mockFind = jest.fn().mockResolvedValue(({
+ saved_objects: [{ attributes: { test: 1 } }],
+ } as unknown) as SavedObjectsFindResponse);
+ mockInternalRepository.find = mockFind;
+ const fetchResult = await collector.fetch(collectorFetchContext);
+
+ expect(fetchResult).toMatchInlineSnapshot(`
+ Object {
+ "daily": Array [
+ Object {
+ "test": 1,
+ },
+ ],
+ }
+ `);
+ expect(mockFind).toBeCalledTimes(1);
+ expect(mockFind.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "sortField": "updated_at",
+ "sortOrder": "desc",
+ "type": "event_loop_delays_daily",
+ },
+ ]
+ `);
+ });
+});
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.ts
new file mode 100644
index 0000000000000..774e021d7a549
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { timer } from 'rxjs';
+import { SavedObjectsServiceSetup, ISavedObjectsRepository, Logger } from 'kibana/server';
+import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+import { rollDailyData } from './rollups';
+import { registerSavedObjectTypes, EventLoopDelaysDaily } from './saved_objects';
+import { eventLoopDelaysUsageSchema, EventLoopDelaysUsageReport } from './schema';
+import { SAVED_OBJECTS_DAILY_TYPE } from './saved_objects';
+import { ROLL_DAILY_INDICES_INTERVAL, ROLL_INDICES_START } from './constants';
+
+export function registerEventLoopDelaysCollector(
+ logger: Logger,
+ usageCollection: UsageCollectionSetup,
+ registerType: SavedObjectsServiceSetup['registerType'],
+ getSavedObjectsClient: () => ISavedObjectsRepository | undefined
+) {
+ registerSavedObjectTypes(registerType);
+
+ timer(ROLL_INDICES_START, ROLL_DAILY_INDICES_INTERVAL).subscribe(() =>
+ rollDailyData(logger, getSavedObjectsClient())
+ );
+
+ const collector = usageCollection.makeUsageCollector({
+ type: 'event_loop_delays',
+ isReady: () => typeof getSavedObjectsClient() !== 'undefined',
+ schema: eventLoopDelaysUsageSchema,
+ fetch: async () => {
+ const internalRepository = getSavedObjectsClient();
+ if (!internalRepository) {
+ return { daily: [] };
+ }
+
+ const { saved_objects: savedObjects } = await internalRepository.find({
+ type: SAVED_OBJECTS_DAILY_TYPE,
+ sortField: 'updated_at',
+ sortOrder: 'desc',
+ });
+
+ return {
+ daily: savedObjects.map((savedObject) => savedObject.attributes),
+ };
+ },
+ });
+
+ usageCollection.registerCollector(collector);
+}
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/index.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/index.ts
new file mode 100644
index 0000000000000..693b173c2759e
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/index.ts
@@ -0,0 +1,11 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { startTrackingEventLoopDelaysUsage } from './event_loop_delays';
+export { registerEventLoopDelaysCollector } from './event_loop_delays_usage_collector';
+export { SAVED_OBJECTS_DAILY_TYPE } from './saved_objects';
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.ts
new file mode 100644
index 0000000000000..cb59d6a44b07e
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.ts
@@ -0,0 +1,81 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { rollDailyData } from './daily';
+import { loggingSystemMock, savedObjectsRepositoryMock } from '../../../../../../core/server/mocks';
+import type { SavedObjectsFindResponse } from '../../../../../../core/server';
+
+describe('rollDailyData', () => {
+ const logger = loggingSystemMock.createLogger();
+ const mockSavedObjectsClient = savedObjectsRepositoryMock.create();
+
+ beforeEach(() => jest.clearAllMocks());
+
+ it('returns false if no savedObjectsClient', async () => {
+ await rollDailyData(logger, undefined);
+ expect(mockSavedObjectsClient.find).toBeCalledTimes(0);
+ });
+
+ it('calls delete on documents older than 3 days', async () => {
+ mockSavedObjectsClient.find.mockResolvedValueOnce({
+ saved_objects: [{ id: 'test_id_1' }, { id: 'test_id_2' }],
+ } as SavedObjectsFindResponse);
+
+ await rollDailyData(logger, mockSavedObjectsClient);
+
+ expect(mockSavedObjectsClient.find).toHaveBeenCalledTimes(1);
+ expect(mockSavedObjectsClient.delete).toBeCalledTimes(2);
+ expect(mockSavedObjectsClient.delete).toHaveBeenNthCalledWith(
+ 1,
+ 'event_loop_delays_daily',
+ 'test_id_1'
+ );
+ expect(mockSavedObjectsClient.delete).toHaveBeenNthCalledWith(
+ 2,
+ 'event_loop_delays_daily',
+ 'test_id_2'
+ );
+ });
+
+ it('calls logger.debug on repository find error', async () => {
+ const mockError = new Error('find error');
+ mockSavedObjectsClient.find.mockRejectedValueOnce(mockError);
+
+ await rollDailyData(logger, mockSavedObjectsClient);
+ expect(logger.debug).toBeCalledTimes(2);
+ expect(logger.debug).toHaveBeenNthCalledWith(
+ 1,
+ 'Failed to rollup transactional to daily entries'
+ );
+ expect(logger.debug).toHaveBeenNthCalledWith(2, mockError);
+ });
+
+ it('settles all deletes before logging failures', async () => {
+ const mockError1 = new Error('delete error 1');
+ const mockError2 = new Error('delete error 2');
+ mockSavedObjectsClient.find.mockResolvedValueOnce({
+ saved_objects: [{ id: 'test_id_1' }, { id: 'test_id_2' }, { id: 'test_id_3' }],
+ } as SavedObjectsFindResponse);
+
+ mockSavedObjectsClient.delete.mockRejectedValueOnce(mockError1);
+ mockSavedObjectsClient.delete.mockResolvedValueOnce(true);
+ mockSavedObjectsClient.delete.mockRejectedValueOnce(mockError2);
+
+ await rollDailyData(logger, mockSavedObjectsClient);
+ expect(mockSavedObjectsClient.delete).toBeCalledTimes(3);
+ expect(logger.debug).toBeCalledTimes(2);
+ expect(logger.debug).toHaveBeenNthCalledWith(
+ 1,
+ 'Failed to rollup transactional to daily entries'
+ );
+ expect(logger.debug).toHaveBeenNthCalledWith(2, [
+ { reason: mockError1, status: 'rejected' },
+ { reason: mockError2, status: 'rejected' },
+ ]);
+ });
+});
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.ts
new file mode 100644
index 0000000000000..29072335d272b
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { Logger } from '@kbn/logging';
+import { ISavedObjectsRepository } from '../../../../../../core/server';
+import { deleteHistogramSavedObjects } from '../saved_objects';
+
+/**
+ * daily rollup function. Deletes histogram saved objects older than 3 days
+ * @param logger
+ * @param savedObjectsClient
+ */
+export async function rollDailyData(
+ logger: Logger,
+ savedObjectsClient?: ISavedObjectsRepository
+): Promise {
+ if (!savedObjectsClient) {
+ return;
+ }
+ try {
+ const settledDeletes = await deleteHistogramSavedObjects(savedObjectsClient);
+ const failedDeletes = settledDeletes.filter(({ status }) => status !== 'fulfilled');
+ if (failedDeletes.length) {
+ throw failedDeletes;
+ }
+ } catch (err) {
+ logger.debug(`Failed to rollup transactional to daily entries`);
+ logger.debug(err);
+ }
+}
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/index.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/index.ts
new file mode 100644
index 0000000000000..4523069a820e7
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { rollDailyData } from './daily';
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts
new file mode 100644
index 0000000000000..8c227f260da6e
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { Logger, ISavedObjectsRepository } from '../../../../../../../core/server';
+import {
+ createTestServers,
+ TestElasticsearchUtils,
+ TestKibanaUtils,
+ createRootWithCorePlugins,
+} from '../../../../../../../core/test_helpers/kbn_server';
+import { rollDailyData } from '../daily';
+import { mocked } from '../../event_loop_delays.mocks';
+
+import {
+ SAVED_OBJECTS_DAILY_TYPE,
+ serializeSavedObjectId,
+ EventLoopDelaysDaily,
+} from '../../saved_objects';
+import moment from 'moment';
+
+const { startES } = createTestServers({
+ adjustTimeout: (t: number) => jest.setTimeout(t),
+});
+
+function createRawObject(date: moment.MomentInput) {
+ const pid = Math.round(Math.random() * 10000);
+ return {
+ type: SAVED_OBJECTS_DAILY_TYPE,
+ id: serializeSavedObjectId({ pid, date }),
+ attributes: {
+ ...mocked.createHistogram({
+ fromTimestamp: moment(date).startOf('day').toISOString(),
+ lastUpdatedAt: moment(date).toISOString(),
+ }),
+ processId: pid,
+ },
+ };
+}
+
+const rawEventLoopDelaysDaily = [
+ createRawObject(moment.now()),
+ createRawObject(moment.now()),
+ createRawObject(moment().subtract(1, 'days')),
+ createRawObject(moment().subtract(3, 'days')),
+];
+
+const outdatedRawEventLoopDelaysDaily = [
+ createRawObject(moment().subtract(5, 'days')),
+ createRawObject(moment().subtract(7, 'days')),
+];
+
+describe('daily rollups integration test', () => {
+ let esServer: TestElasticsearchUtils;
+ let root: TestKibanaUtils['root'];
+ let internalRepository: ISavedObjectsRepository;
+ let logger: Logger;
+
+ beforeAll(async () => {
+ esServer = await startES();
+ root = createRootWithCorePlugins();
+
+ await root.setup();
+ const start = await root.start();
+ logger = root.logger.get('test dailt rollups');
+ internalRepository = start.savedObjects.createInternalRepository([SAVED_OBJECTS_DAILY_TYPE]);
+
+ await internalRepository.bulkCreate(
+ [...rawEventLoopDelaysDaily, ...outdatedRawEventLoopDelaysDaily],
+ { refresh: true }
+ );
+ });
+
+ afterAll(async () => {
+ await esServer.stop();
+ await root.shutdown();
+ });
+
+ it('deletes documents older that 3 days from the saved objects repository', async () => {
+ await rollDailyData(logger, internalRepository);
+ const {
+ total,
+ saved_objects: savedObjects,
+ } = await internalRepository.find({ type: SAVED_OBJECTS_DAILY_TYPE });
+ expect(total).toBe(rawEventLoopDelaysDaily.length);
+ expect(savedObjects.map(({ id, type, attributes }) => ({ id, type, attributes }))).toEqual(
+ rawEventLoopDelaysDaily
+ );
+ });
+});
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts
new file mode 100644
index 0000000000000..022040615bd45
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts
@@ -0,0 +1,122 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import {
+ storeHistogram,
+ serializeSavedObjectId,
+ deleteHistogramSavedObjects,
+} from './saved_objects';
+import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks';
+import type { SavedObjectsFindResponse } from '../../../../../core/server/';
+import { mocked } from './event_loop_delays.mocks';
+
+describe('serializeSavedObjectId', () => {
+ it('returns serialized id', () => {
+ const id = serializeSavedObjectId({ date: 1623233091278, pid: 123 });
+ expect(id).toBe('123::09062021');
+ });
+});
+
+describe('storeHistogram', () => {
+ const mockHistogram = mocked.createHistogram();
+ const mockInternalRepository = savedObjectsRepositoryMock.create();
+
+ jest.useFakeTimers('modern');
+ const mockNow = jest.getRealSystemTime();
+ jest.setSystemTime(mockNow);
+
+ beforeEach(() => jest.clearAllMocks());
+ afterAll(() => jest.useRealTimers());
+
+ it('stores histogram data in a savedObject', async () => {
+ await storeHistogram(mockHistogram, mockInternalRepository);
+ const pid = process.pid;
+ const id = serializeSavedObjectId({ date: mockNow, pid });
+
+ expect(mockInternalRepository.create).toBeCalledWith(
+ 'event_loop_delays_daily',
+ { ...mockHistogram, processId: pid },
+ { id, overwrite: true }
+ );
+ });
+});
+
+describe('deleteHistogramSavedObjects', () => {
+ const mockInternalRepository = savedObjectsRepositoryMock.create();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockInternalRepository.find.mockResolvedValue({
+ saved_objects: [{ id: 'test_obj_1' }, { id: 'test_obj_1' }],
+ } as SavedObjectsFindResponse);
+ });
+
+ it('builds filter query based on time range passed in days', async () => {
+ await deleteHistogramSavedObjects(mockInternalRepository);
+ await deleteHistogramSavedObjects(mockInternalRepository, 20);
+ expect(mockInternalRepository.find.mock.calls).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ Object {
+ "filter": "event_loop_delays_daily.attributes.lastUpdatedAt < \\"now-3d/d\\"",
+ "type": "event_loop_delays_daily",
+ },
+ ],
+ Array [
+ Object {
+ "filter": "event_loop_delays_daily.attributes.lastUpdatedAt < \\"now-20d/d\\"",
+ "type": "event_loop_delays_daily",
+ },
+ ],
+ ]
+ `);
+ });
+
+ it('loops over saved objects and deletes them', async () => {
+ mockInternalRepository.delete.mockImplementation(async (type, id) => {
+ return id;
+ });
+
+ const results = await deleteHistogramSavedObjects(mockInternalRepository);
+ expect(results).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "status": "fulfilled",
+ "value": "test_obj_1",
+ },
+ Object {
+ "status": "fulfilled",
+ "value": "test_obj_1",
+ },
+ ]
+ `);
+ });
+
+ it('settles all promises even if some of the deletes fail.', async () => {
+ mockInternalRepository.delete.mockImplementationOnce(async (type, id) => {
+ throw new Error('Intentional failure');
+ });
+ mockInternalRepository.delete.mockImplementationOnce(async (type, id) => {
+ return id;
+ });
+
+ const results = await deleteHistogramSavedObjects(mockInternalRepository);
+ expect(results).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "reason": [Error: Intentional failure],
+ "status": "rejected",
+ },
+ Object {
+ "status": "fulfilled",
+ "value": "test_obj_1",
+ },
+ ]
+ `);
+ });
+});
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts
new file mode 100644
index 0000000000000..610a6697da364
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type {
+ SavedObjectAttributes,
+ SavedObjectsServiceSetup,
+ ISavedObjectsRepository,
+} from 'kibana/server';
+import moment from 'moment';
+import type { IntervalHistogram } from './event_loop_delays';
+
+export const SAVED_OBJECTS_DAILY_TYPE = 'event_loop_delays_daily';
+
+export interface EventLoopDelaysDaily extends SavedObjectAttributes, IntervalHistogram {
+ processId: number;
+}
+
+export function registerSavedObjectTypes(registerType: SavedObjectsServiceSetup['registerType']) {
+ registerType({
+ name: SAVED_OBJECTS_DAILY_TYPE,
+ hidden: true,
+ namespaceType: 'agnostic',
+ mappings: {
+ dynamic: false,
+ properties: {
+ // This type requires `lastUpdatedAt` to be indexed so we can use it when rolling up totals (lastUpdatedAt < now-90d)
+ lastUpdatedAt: { type: 'date' },
+ },
+ },
+ });
+}
+
+export function serializeSavedObjectId({ date, pid }: { date: moment.MomentInput; pid: number }) {
+ const formattedDate = moment(date).format('DDMMYYYY');
+
+ return `${pid}::${formattedDate}`;
+}
+
+export async function deleteHistogramSavedObjects(
+ internalRepository: ISavedObjectsRepository,
+ daysTimeRange = 3
+) {
+ const { saved_objects: savedObjects } = await internalRepository.find({
+ type: SAVED_OBJECTS_DAILY_TYPE,
+ filter: `${SAVED_OBJECTS_DAILY_TYPE}.attributes.lastUpdatedAt < "now-${daysTimeRange}d/d"`,
+ });
+
+ return await Promise.allSettled(
+ savedObjects.map(async (savedObject) => {
+ return await internalRepository.delete(SAVED_OBJECTS_DAILY_TYPE, savedObject.id);
+ })
+ );
+}
+
+export async function storeHistogram(
+ histogram: IntervalHistogram,
+ internalRepository: ISavedObjectsRepository
+) {
+ const pid = process.pid;
+ const id = serializeSavedObjectId({ date: histogram.lastUpdatedAt, pid });
+
+ return await internalRepository.create(
+ SAVED_OBJECTS_DAILY_TYPE,
+ { ...histogram, processId: pid },
+ { id, overwrite: true }
+ );
+}
diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts
new file mode 100644
index 0000000000000..319e8c77438b8
--- /dev/null
+++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts
@@ -0,0 +1,111 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { MakeSchemaFrom } from 'src/plugins/usage_collection/server';
+
+export interface EventLoopDelaysUsageReport {
+ daily: Array<{
+ processId: number;
+ lastUpdatedAt: string;
+ fromTimestamp: string;
+ min: number;
+ max: number;
+ mean: number;
+ exceeds: number;
+ stddev: number;
+ percentiles: {
+ '50': number;
+ '75': number;
+ '95': number;
+ '99': number;
+ };
+ }>;
+}
+
+export const eventLoopDelaysUsageSchema: MakeSchemaFrom = {
+ daily: {
+ type: 'array',
+ items: {
+ processId: {
+ type: 'long',
+ _meta: {
+ description: 'The process id of the monitored kibana instance.',
+ },
+ },
+ fromTimestamp: {
+ type: 'date',
+ _meta: {
+ description: 'Timestamp at which the histogram started monitoring.',
+ },
+ },
+ lastUpdatedAt: {
+ type: 'date',
+ _meta: {
+ description: 'Latest timestamp this histogram object was updated this day.',
+ },
+ },
+ min: {
+ type: 'long',
+ _meta: {
+ description: 'The minimum recorded event loop delay.',
+ },
+ },
+ max: {
+ type: 'long',
+ _meta: {
+ description: 'The maximum recorded event loop delay.',
+ },
+ },
+ mean: {
+ type: 'long',
+ _meta: {
+ description: 'The mean of the recorded event loop delays.',
+ },
+ },
+ exceeds: {
+ type: 'long',
+ _meta: {
+ description:
+ 'The number of times the event loop delay exceeded the maximum 1 hour eventloop delay threshold.',
+ },
+ },
+ stddev: {
+ type: 'long',
+ _meta: {
+ description: 'The standard deviation of the recorded event loop delays.',
+ },
+ },
+ percentiles: {
+ '50': {
+ type: 'long',
+ _meta: {
+ description: 'The 50th accumulated percentile distribution',
+ },
+ },
+ '75': {
+ type: 'long',
+ _meta: {
+ description: 'The 75th accumulated percentile distribution',
+ },
+ },
+ '95': {
+ type: 'long',
+ _meta: {
+ description: 'The 95th accumulated percentile distribution',
+ },
+ },
+ '99': {
+ type: 'long',
+ _meta: {
+ description: 'The 99th accumulated percentile distribution',
+ },
+ },
+ },
+ },
+ },
+};
diff --git a/src/plugins/kibana_usage_collection/server/collectors/index.ts b/src/plugins/kibana_usage_collection/server/collectors/index.ts
index 761989938e56d..e4ed24611bfa8 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/index.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/index.ts
@@ -28,3 +28,4 @@ export {
registerUsageCountersRollups,
registerUsageCountersUsageCollector,
} from './usage_counters';
+export { registerEventLoopDelaysCollector } from './event_loop_delays';
diff --git a/src/plugins/kibana_usage_collection/server/plugin.test.ts b/src/plugins/kibana_usage_collection/server/plugin.test.ts
index 2100b9bbb918b..1584366a42dc1 100644
--- a/src/plugins/kibana_usage_collection/server/plugin.test.ts
+++ b/src/plugins/kibana_usage_collection/server/plugin.test.ts
@@ -16,7 +16,6 @@ import {
createUsageCollectionSetupMock,
} from '../../usage_collection/server/mocks';
import { cloudDetailsMock } from './mocks';
-
import { plugin } from './';
describe('kibana_usage_collection', () => {
@@ -105,6 +104,10 @@ describe('kibana_usage_collection', () => {
"isReady": true,
"type": "localization",
},
+ Object {
+ "isReady": false,
+ "type": "event_loop_delays",
+ },
]
`);
});
diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts
index da6445ce957d8..4ec717c48610e 100644
--- a/src/plugins/kibana_usage_collection/server/plugin.ts
+++ b/src/plugins/kibana_usage_collection/server/plugin.ts
@@ -22,6 +22,10 @@ import type {
CoreUsageDataStart,
} from 'src/core/server';
import { SavedObjectsClient } from '../../../core/server';
+import {
+ startTrackingEventLoopDelaysUsage,
+ SAVED_OBJECTS_DAILY_TYPE,
+} from './collectors/event_loop_delays';
import {
registerApplicationUsageCollector,
registerKibanaUsageCollector,
@@ -39,6 +43,7 @@ import {
registerUsageCountersRollups,
registerUsageCountersUsageCollector,
registerSavedObjectsCountUsageCollector,
+ registerEventLoopDelaysCollector,
} from './collectors';
interface KibanaUsageCollectionPluginsDepsSetup {
@@ -54,46 +59,46 @@ export class KibanaUsageCollectionPlugin implements Plugin {
private uiSettingsClient?: IUiSettingsClient;
private metric$: Subject;
private coreUsageData?: CoreUsageDataStart;
- private stopUsingUiCounterIndicies$: Subject;
+ private pluginStop$: Subject;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
this.legacyConfig$ = initializerContext.config.legacy.globalConfig$;
this.metric$ = new Subject();
- this.stopUsingUiCounterIndicies$ = new Subject();
+ this.pluginStop$ = new Subject();
}
public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) {
usageCollection.createUsageCounter('uiCounters');
-
this.registerUsageCollectors(
usageCollection,
coreSetup,
this.metric$,
- this.stopUsingUiCounterIndicies$,
+ this.pluginStop$,
coreSetup.savedObjects.registerType.bind(coreSetup.savedObjects)
);
}
public start(core: CoreStart) {
const { savedObjects, uiSettings } = core;
- this.savedObjectsClient = savedObjects.createInternalRepository();
+ this.savedObjectsClient = savedObjects.createInternalRepository([SAVED_OBJECTS_DAILY_TYPE]);
const savedObjectsClient = new SavedObjectsClient(this.savedObjectsClient);
this.uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient);
core.metrics.getOpsMetrics$().subscribe(this.metric$);
this.coreUsageData = core.coreUsageData;
+ startTrackingEventLoopDelaysUsage(this.savedObjectsClient, this.pluginStop$.asObservable());
}
public stop() {
this.metric$.complete();
- this.stopUsingUiCounterIndicies$.complete();
+ this.pluginStop$.complete();
}
private registerUsageCollectors(
usageCollection: UsageCollectionSetup,
coreSetup: CoreSetup,
metric$: Subject,
- stopUsingUiCounterIndicies$: Subject,
+ pluginStop$: Subject,
registerType: SavedObjectsRegisterType
) {
const getSavedObjectsClient = () => this.savedObjectsClient;
@@ -101,12 +106,8 @@ export class KibanaUsageCollectionPlugin implements Plugin {
const getCoreUsageDataService = () => this.coreUsageData!;
registerUiCounterSavedObjectType(coreSetup.savedObjects);
- registerUiCountersRollups(
- this.logger.get('ui-counters'),
- stopUsingUiCounterIndicies$,
- getSavedObjectsClient
- );
- registerUiCountersUsageCollector(usageCollection, stopUsingUiCounterIndicies$);
+ registerUiCountersRollups(this.logger.get('ui-counters'), pluginStop$, getSavedObjectsClient);
+ registerUiCountersUsageCollector(usageCollection, pluginStop$);
registerUsageCountersRollups(this.logger.get('usage-counters-rollup'), getSavedObjectsClient);
registerUsageCountersUsageCollector(usageCollection);
@@ -127,5 +128,11 @@ export class KibanaUsageCollectionPlugin implements Plugin {
registerCoreUsageCollector(usageCollection, getCoreUsageDataService);
registerConfigUsageCollector(usageCollection, getCoreUsageDataService);
registerLocalizationUsageCollector(usageCollection, coreSetup.i18n);
+ registerEventLoopDelaysCollector(
+ this.logger.get('event-loop-delays'),
+ usageCollection,
+ registerType,
+ getSavedObjectsClient
+ );
}
}
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index 6ab550389a12d..99c6dcb40e57d 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -7900,6 +7900,93 @@
}
}
},
+ "event_loop_delays": {
+ "properties": {
+ "daily": {
+ "type": "array",
+ "items": {
+ "properties": {
+ "processId": {
+ "type": "long",
+ "_meta": {
+ "description": "The process id of the monitored kibana instance."
+ }
+ },
+ "fromTimestamp": {
+ "type": "date",
+ "_meta": {
+ "description": "Timestamp at which the histogram started monitoring."
+ }
+ },
+ "lastUpdatedAt": {
+ "type": "date",
+ "_meta": {
+ "description": "Latest timestamp this histogram object was updated this day."
+ }
+ },
+ "min": {
+ "type": "long",
+ "_meta": {
+ "description": "The minimum recorded event loop delay."
+ }
+ },
+ "max": {
+ "type": "long",
+ "_meta": {
+ "description": "The maximum recorded event loop delay."
+ }
+ },
+ "mean": {
+ "type": "long",
+ "_meta": {
+ "description": "The mean of the recorded event loop delays."
+ }
+ },
+ "exceeds": {
+ "type": "long",
+ "_meta": {
+ "description": "The number of times the event loop delay exceeded the maximum 1 hour eventloop delay threshold."
+ }
+ },
+ "stddev": {
+ "type": "long",
+ "_meta": {
+ "description": "The standard deviation of the recorded event loop delays."
+ }
+ },
+ "percentiles": {
+ "properties": {
+ "50": {
+ "type": "long",
+ "_meta": {
+ "description": "The 50th accumulated percentile distribution"
+ }
+ },
+ "75": {
+ "type": "long",
+ "_meta": {
+ "description": "The 75th accumulated percentile distribution"
+ }
+ },
+ "95": {
+ "type": "long",
+ "_meta": {
+ "description": "The 95th accumulated percentile distribution"
+ }
+ },
+ "99": {
+ "type": "long",
+ "_meta": {
+ "description": "The 99th accumulated percentile distribution"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"localization": {
"properties": {
"locale": {
From 9e1390e118273d90fe33d065398a6c3d0cc2bba1 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Tue, 22 Jun 2021 08:37:03 +0300
Subject: [PATCH 045/163] [Lens] Adds filter from legend in xy and partition
charts (#102026)
* WIP add filtering capabilities to XY legend
* Fix filter by legend on xy axis charts
* Filter pie and xy axis by legend
* create a shared component
* Add functional test
* Add functional test for pie
* Make the buttons keyboard accessible
* Fix functional test
* move function to retry
* Give another try
* Enable the rest od the tests
* Address PR comments
* Address PR comments
* Apply PR comments, fix popover label for alreadyformatted layers
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../get_legend_action.test.tsx | 79 ++++++
.../pie_visualization/get_legend_action.tsx | 44 ++++
.../pie_visualization/render_function.tsx | 2 +
.../lens/public/shared_components/index.ts | 1 +
.../legend_action_popover.tsx | 102 ++++++++
.../__snapshots__/expression.test.tsx.snap | 49 ++++
.../public/xy_visualization/expression.tsx | 9 +
.../get_legend_action.test.tsx | 232 ++++++++++++++++++
.../xy_visualization/get_legend_action.tsx | 72 ++++++
x-pack/test/functional/apps/lens/dashboard.ts | 2 +-
.../test/functional/apps/lens/smokescreen.ts | 62 +++++
.../test/functional/page_objects/lens_page.ts | 6 +
12 files changed, 659 insertions(+), 1 deletion(-)
create mode 100644 x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx
create mode 100644 x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx
create mode 100644 x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx
create mode 100644 x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx
create mode 100644 x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx
diff --git a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx
new file mode 100644
index 0000000000000..67e57dadd4935
--- /dev/null
+++ b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 { LegendActionProps, SeriesIdentifier } from '@elastic/charts';
+import { EuiPopover } from '@elastic/eui';
+import { mountWithIntl } from '@kbn/test/jest';
+import { ComponentType, ReactWrapper } from 'enzyme';
+import type { Datatable } from 'src/plugins/expressions/public';
+import { getLegendAction } from './get_legend_action';
+import { LegendActionPopover } from '../shared_components';
+
+const table: Datatable = {
+ type: 'datatable',
+ columns: [
+ { id: 'a', name: 'A', meta: { type: 'string' } },
+ { id: 'b', name: 'B', meta: { type: 'number' } },
+ ],
+ rows: [
+ { a: 'Hi', b: 2 },
+ { a: 'Test', b: 4 },
+ { a: 'Foo', b: 6 },
+ ],
+};
+
+describe('getLegendAction', function () {
+ let wrapperProps: LegendActionProps;
+ const Component: ComponentType = getLegendAction(table, jest.fn());
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapperProps = {
+ color: 'rgb(109, 204, 177)',
+ label: 'Bar',
+ series: ([
+ {
+ specId: 'donut',
+ key: 'Bar',
+ },
+ ] as unknown) as SeriesIdentifier[],
+ };
+ });
+
+ it('is not rendered if row does not exist', () => {
+ wrapper = mountWithIntl( );
+ expect(wrapper).toEqual({});
+ expect(wrapper.find(EuiPopover).length).toBe(0);
+ });
+
+ it('is rendered if row is detected', () => {
+ const newProps = {
+ ...wrapperProps,
+ label: 'Hi',
+ series: ([
+ {
+ specId: 'donut',
+ key: 'Hi',
+ },
+ ] as unknown) as SeriesIdentifier[],
+ };
+ wrapper = mountWithIntl( );
+ expect(wrapper.find(EuiPopover).length).toBe(1);
+ expect(wrapper.find(EuiPopover).prop('title')).toEqual('Hi, filter options');
+ expect(wrapper.find(LegendActionPopover).prop('context')).toEqual({
+ data: [
+ {
+ column: 0,
+ row: 0,
+ table,
+ value: 'Hi',
+ },
+ ],
+ });
+ });
+});
diff --git a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx
new file mode 100644
index 0000000000000..9f16ad863a415
--- /dev/null
+++ b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 type { LegendAction } from '@elastic/charts';
+import type { Datatable } from 'src/plugins/expressions/public';
+import type { LensFilterEvent } from '../types';
+import { LegendActionPopover } from '../shared_components';
+
+export const getLegendAction = (
+ table: Datatable,
+ onFilter: (data: LensFilterEvent['data']) => void
+): LegendAction =>
+ React.memo(({ series: [pieSeries], label }) => {
+ const data = table.columns.reduce((acc, { id }, column) => {
+ const value = pieSeries.key;
+ const row = table.rows.findIndex((r) => r[id] === value);
+ if (row > -1) {
+ acc.push({
+ table,
+ column,
+ row,
+ value,
+ });
+ }
+
+ return acc;
+ }, []);
+
+ if (data.length === 0) {
+ return null;
+ }
+
+ const context: LensFilterEvent['data'] = {
+ data,
+ };
+
+ return ;
+ });
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
index 6c1cbe63a5a3e..f329cfe1bb8b9 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
@@ -38,6 +38,7 @@ import {
SeriesLayer,
} from '../../../../../src/plugins/charts/public';
import { LensIconChartDonut } from '../assets/chart_donut';
+import { getLegendAction } from './get_legend_action';
declare global {
interface Window {
@@ -281,6 +282,7 @@ export function PieComponent(
onElementClick={
props.renderMode !== 'noInteractivity' ? onElementClickHandler : undefined
}
+ legendAction={getLegendAction(firstTable, onClickValue)}
theme={{
...chartTheme,
background: {
diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts
index cf8536884acdf..c200a18a25caf 100644
--- a/x-pack/plugins/lens/public/shared_components/index.ts
+++ b/x-pack/plugins/lens/public/shared_components/index.ts
@@ -13,3 +13,4 @@ export { TooltipWrapper } from './tooltip_wrapper';
export * from './coloring';
export { useDebouncedValue } from './debounced_value';
export * from './helpers';
+export { LegendActionPopover } from './legend_action_popover';
diff --git a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx
new file mode 100644
index 0000000000000..e344cb5289f51
--- /dev/null
+++ b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx
@@ -0,0 +1,102 @@
+/*
+ * 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, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui';
+import type { LensFilterEvent } from '../types';
+import { desanitizeFilterContext } from '../utils';
+
+export interface LegendActionPopoverProps {
+ /**
+ * Determines the panels label
+ */
+ label: string;
+ /**
+ * Callback on filter value
+ */
+ onFilter: (data: LensFilterEvent['data']) => void;
+ /**
+ * Determines the filter event data
+ */
+ context: LensFilterEvent['data'];
+}
+
+export const LegendActionPopover: React.FunctionComponent = ({
+ label,
+ onFilter,
+ context,
+}) => {
+ const [popoverOpen, setPopoverOpen] = useState(false);
+ const panels: EuiContextMenuPanelDescriptor[] = [
+ {
+ id: 'main',
+ title: label,
+ items: [
+ {
+ name: i18n.translate('xpack.lens.shared.legend.filterForValueButtonAriaLabel', {
+ defaultMessage: 'Filter for value',
+ }),
+ 'data-test-subj': `legend-${label}-filterIn`,
+ icon: ,
+ onClick: () => {
+ setPopoverOpen(false);
+ onFilter(desanitizeFilterContext(context));
+ },
+ },
+ {
+ name: i18n.translate('xpack.lens.shared.legend.filterOutValueButtonAriaLabel', {
+ defaultMessage: 'Filter out value',
+ }),
+ 'data-test-subj': `legend-${label}-filterOut`,
+ icon: ,
+ onClick: () => {
+ setPopoverOpen(false);
+ onFilter(desanitizeFilterContext({ ...context, negate: true }));
+ },
+ },
+ ],
+ },
+ ];
+
+ const Button = (
+ setPopoverOpen(!popoverOpen)}
+ onClick={() => setPopoverOpen(!popoverOpen)}
+ >
+
+
+ );
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap
index f9b4e33072c81..1f647680408d7 100644
--- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap
+++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap
@@ -7,6 +7,13 @@ exports[`xy_expression XYChart component it renders area 1`] = `
= {};
+
// This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers
const safeXAccessorLabelRenderer = (value: unknown): string =>
xAxisColumn && layersAlreadyFormatted[xAxisColumn.id]
@@ -629,6 +631,13 @@ export function XYChart({
xDomain={xDomain}
onBrushEnd={renderMode !== 'noInteractivity' ? brushHandler : undefined}
onElementClick={renderMode !== 'noInteractivity' ? clickHandler : undefined}
+ legendAction={getLegendAction(
+ filteredLayers,
+ data.tables,
+ onClickValue,
+ formatFactory,
+ layersAlreadyFormatted
+ )}
showLegendExtra={isHistogramViz && valuesInLegend}
/>
diff --git a/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx
new file mode 100644
index 0000000000000..e4edfe918a242
--- /dev/null
+++ b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.test.tsx
@@ -0,0 +1,232 @@
+/*
+ * 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 { LegendActionProps, SeriesIdentifier } from '@elastic/charts';
+import { EuiPopover } from '@elastic/eui';
+import { mountWithIntl } from '@kbn/test/jest';
+import { ComponentType, ReactWrapper } from 'enzyme';
+import type { LayerArgs } from './types';
+import type { LensMultiTable } from '../types';
+import { getLegendAction } from './get_legend_action';
+import { LegendActionPopover } from '../shared_components';
+
+const sampleLayer = {
+ layerId: 'first',
+ seriesType: 'line',
+ xAccessor: 'c',
+ accessors: ['a', 'b'],
+ splitAccessor: 'splitAccessorId',
+ columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}',
+ xScaleType: 'ordinal',
+ yScaleType: 'linear',
+ isHistogram: false,
+} as LayerArgs;
+
+const tables = {
+ first: {
+ type: 'datatable',
+ rows: [
+ {
+ xAccessorId: 1585758120000,
+ splitAccessorId: "Men's Clothing",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585758360000,
+ splitAccessorId: "Women's Accessories",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585758360000,
+ splitAccessorId: "Women's Clothing",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585759380000,
+ splitAccessorId: "Men's Clothing",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585759380000,
+ splitAccessorId: "Men's Shoes",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585759380000,
+ splitAccessorId: "Women's Clothing",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585760700000,
+ splitAccessorId: "Men's Clothing",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585760760000,
+ splitAccessorId: "Men's Clothing",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585760760000,
+ splitAccessorId: "Men's Shoes",
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 1585761120000,
+ splitAccessorId: "Men's Shoes",
+ yAccessorId: 1,
+ },
+ ],
+ columns: [
+ {
+ id: 'xAccessorId',
+ name: 'order_date per minute',
+ meta: {
+ type: 'date',
+ field: 'order_date',
+ source: 'esaggs',
+ index: 'indexPatternId',
+ sourceParams: {
+ indexPatternId: 'indexPatternId',
+ type: 'date_histogram',
+ params: {
+ field: 'order_date',
+ timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' },
+ useNormalizedEsInterval: true,
+ scaleMetricValues: false,
+ interval: '1m',
+ drop_partials: false,
+ min_doc_count: 0,
+ extended_bounds: {},
+ },
+ },
+ params: { id: 'date', params: { pattern: 'HH:mm' } },
+ },
+ },
+ {
+ id: 'splitAccessorId',
+ name: 'Top values of category.keyword',
+ meta: {
+ type: 'string',
+ field: 'category.keyword',
+ source: 'esaggs',
+ index: 'indexPatternId',
+ sourceParams: {
+ indexPatternId: 'indexPatternId',
+ type: 'terms',
+ params: {
+ field: 'category.keyword',
+ orderBy: 'yAccessorId',
+ order: 'desc',
+ size: 3,
+ otherBucket: false,
+ otherBucketLabel: 'Other',
+ missingBucket: false,
+ missingBucketLabel: 'Missing',
+ },
+ },
+ params: {
+ id: 'terms',
+ params: {
+ id: 'string',
+ otherBucketLabel: 'Other',
+ missingBucketLabel: 'Missing',
+ parsedUrl: {
+ origin: 'http://localhost:5601',
+ pathname: '/jiy/app/kibana',
+ basePath: '/jiy',
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'yAccessorId',
+ name: 'Count of records',
+ meta: {
+ type: 'number',
+ source: 'esaggs',
+ index: 'indexPatternId',
+ sourceParams: {
+ indexPatternId: 'indexPatternId',
+ params: {},
+ },
+ params: { id: 'number' },
+ },
+ },
+ ],
+ },
+} as LensMultiTable['tables'];
+
+describe('getLegendAction', function () {
+ let wrapperProps: LegendActionProps;
+ const Component: ComponentType = getLegendAction(
+ [sampleLayer],
+ tables,
+ jest.fn(),
+ jest.fn(),
+ {}
+ );
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapperProps = {
+ color: 'rgb(109, 204, 177)',
+ label: "Women's Accessories",
+ series: ([
+ {
+ seriesKeys: ["Women's Accessories", 'test'],
+ },
+ ] as unknown) as SeriesIdentifier[],
+ };
+ });
+
+ it('is not rendered if not layer is detected', () => {
+ wrapper = mountWithIntl( );
+ expect(wrapper).toEqual({});
+ expect(wrapper.find(EuiPopover).length).toBe(0);
+ });
+
+ it('is rendered if row does not exist', () => {
+ const newProps = {
+ ...wrapperProps,
+ series: ([
+ {
+ seriesKeys: ['test', 'b'],
+ },
+ ] as unknown) as SeriesIdentifier[],
+ };
+ wrapper = mountWithIntl( );
+ expect(wrapper).toEqual({});
+ expect(wrapper.find(EuiPopover).length).toBe(0);
+ });
+
+ it('is rendered if layer is detected', () => {
+ const newProps = {
+ ...wrapperProps,
+ series: ([
+ {
+ seriesKeys: ["Women's Accessories", 'b'],
+ },
+ ] as unknown) as SeriesIdentifier[],
+ };
+ wrapper = mountWithIntl( );
+ expect(wrapper.find(EuiPopover).length).toBe(1);
+ expect(wrapper.find(EuiPopover).prop('title')).toEqual("Women's Accessories, filter options");
+ expect(wrapper.find(LegendActionPopover).prop('context')).toEqual({
+ data: [
+ {
+ column: 1,
+ row: 1,
+ table: tables.first,
+ value: "Women's Accessories",
+ },
+ ],
+ });
+ });
+});
diff --git a/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx
new file mode 100644
index 0000000000000..c99bf948d6e37
--- /dev/null
+++ b/x-pack/plugins/lens/public/xy_visualization/get_legend_action.tsx
@@ -0,0 +1,72 @@
+/*
+ * 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 type { LegendAction, XYChartSeriesIdentifier } from '@elastic/charts';
+import type { LayerArgs } from './types';
+import type { LensMultiTable, LensFilterEvent, FormatFactory } from '../types';
+import { LegendActionPopover } from '../shared_components';
+
+export const getLegendAction = (
+ filteredLayers: LayerArgs[],
+ tables: LensMultiTable['tables'],
+ onFilter: (data: LensFilterEvent['data']) => void,
+ formatFactory: FormatFactory,
+ layersAlreadyFormatted: Record
+): LegendAction =>
+ React.memo(({ series: [xySeries] }) => {
+ const series = xySeries as XYChartSeriesIdentifier;
+ const layer = filteredLayers.find((l) =>
+ series.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString()))
+ );
+
+ if (!layer || !layer.splitAccessor) {
+ return null;
+ }
+
+ const splitLabel = series.seriesKeys[0] as string;
+ const accessor = layer.splitAccessor;
+
+ const table = tables[layer.layerId];
+ const splitColumn = table.columns.find(({ id }) => id === layer.splitAccessor);
+ const formatter = formatFactory(splitColumn && splitColumn.meta?.params);
+
+ const rowIndex = table.rows.findIndex((row) => {
+ if (layersAlreadyFormatted[accessor]) {
+ // stringify the value to compare with the chart value
+ return formatter.convert(row[accessor]) === splitLabel;
+ }
+ return row[accessor] === splitLabel;
+ });
+
+ if (rowIndex < 0) return null;
+
+ const data = [
+ {
+ row: rowIndex,
+ column: table.columns.findIndex((col) => col.id === accessor),
+ value: accessor ? table.rows[rowIndex][accessor] : splitLabel,
+ table,
+ },
+ ];
+
+ const context: LensFilterEvent['data'] = {
+ data,
+ };
+
+ return (
+
+ );
+ });
diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts
index 9998f1dd4cdcb..844b074e42e74 100644
--- a/x-pack/test/functional/apps/lens/dashboard.ts
+++ b/x-pack/test/functional/apps/lens/dashboard.ts
@@ -68,7 +68,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await find.clickByButtonText('lnsXYvis');
await dashboardAddPanel.closeAddPanel();
await PageObjects.lens.goToTimeRange();
- await clickInChart(5, 5); // hardcoded position of bar, depends heavy on data and charts implementation
+ await clickInChart(6, 5); // hardcoded position of bar, depends heavy on data and charts implementation
await retry.try(async () => {
await testSubjects.click('applyFiltersPopoverButton');
diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts
index 5d775f154c943..ec32d7620fcf9 100644
--- a/x-pack/test/functional/apps/lens/smokescreen.ts
+++ b/x-pack/test/functional/apps/lens/smokescreen.ts
@@ -15,6 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const listingTable = getService('listingTable');
const testSubjects = getService('testSubjects');
const elasticChart = getService('elasticChart');
+ const filterBar = getService('filterBar');
describe('lens smokescreen tests', () => {
it('should allow creation of lens xy chart', async () => {
@@ -686,5 +687,66 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
expect(await testSubjects.isEnabled('lnsApp_downloadCSVButton')).to.eql(true);
});
+
+ it('should allow filtering by legend on an xy chart', async () => {
+ await PageObjects.visualize.navigateToNewVisualization();
+ await PageObjects.visualize.clickVisType('lens');
+ await PageObjects.lens.goToTimeRange();
+
+ await PageObjects.lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: '@timestamp',
+ });
+
+ await PageObjects.lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'average',
+ field: 'bytes',
+ });
+
+ await PageObjects.lens.configureDimension({
+ dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
+ operation: 'terms',
+ field: 'extension.raw',
+ });
+
+ await PageObjects.lens.filterLegend('jpg');
+ const hasExtensionFilter = await filterBar.hasFilter('extension.raw', 'jpg');
+ expect(hasExtensionFilter).to.be(true);
+
+ await filterBar.removeFilter('extension.raw');
+ });
+
+ it('should allow filtering by legend on a pie chart', async () => {
+ await PageObjects.visualize.navigateToNewVisualization();
+ await PageObjects.visualize.clickVisType('lens');
+ await PageObjects.lens.goToTimeRange();
+ await PageObjects.lens.switchToVisualization('pie');
+
+ await PageObjects.lens.configureDimension({
+ dimension: 'lnsPie_sliceByDimensionPanel > lns-empty-dimension',
+ operation: 'terms',
+ field: 'extension.raw',
+ });
+
+ await PageObjects.lens.configureDimension({
+ dimension: 'lnsPie_sliceByDimensionPanel > lns-empty-dimension',
+ operation: 'terms',
+ field: 'agent.raw',
+ });
+
+ await PageObjects.lens.configureDimension({
+ dimension: 'lnsPie_sizeByDimensionPanel > lns-empty-dimension',
+ operation: 'average',
+ field: 'bytes',
+ });
+
+ await PageObjects.lens.filterLegend('jpg');
+ const hasExtensionFilter = await filterBar.hasFilter('extension.raw', 'jpg');
+ expect(hasExtensionFilter).to.be(true);
+
+ await filterBar.removeFilter('extension.raw');
+ });
});
}
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index d416d26baaf0d..9953ab3dfcead 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -1069,5 +1069,11 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await input.clearValueWithKeyboard({ charByChar: true });
await input.type(formula);
},
+
+ async filterLegend(value: string) {
+ await testSubjects.click(`legend-${value}`);
+ const filterIn = await testSubjects.find(`legend-${value}-filterIn`);
+ await filterIn.click();
+ },
});
}
From e806dde4e8e4a97d0ec96b6b4cc4dcddebdd2351 Mon Sep 17 00:00:00 2001
From: Ignacio Rivas
Date: Tue, 22 Jun 2021 09:17:52 +0200
Subject: [PATCH 046/163] [License management] Migrate to new page layout
(#102218)
* start working on license management
* migrate permissions check to new layout
* refactor license expiration as a subtitle of the page header
* finish up working on page title
* Fix linter errors and update snapshots
* update method name
* CR changes
* update snapshots
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../__snapshots__/add_license.test.js.snap | 4 +-
.../license_page_header.test.js.snap | 5 +
.../__snapshots__/license_status.test.js.snap | 5 -
.../request_trial_extension.test.js.snap | 8 +-
.../revert_to_basic.test.js.snap | 6 +-
.../__snapshots__/start_trial.test.js.snap | 8 +-
.../upload_license.test.tsx.snap | 20 +-
...us.test.js => license_page_header.test.js} | 6 +-
x-pack/plugins/license_management/kibana.json | 1 +
.../public/application/app.js | 63 +++----
.../add_license/add_license.js | 1 +
.../license_dashboard/license_dashboard.js | 33 ++--
.../index.js | 2 +-
.../license_page_header.js | 106 +++++++++++
.../license_status.container.js | 36 ----
.../license_status/license_status.js | 98 ----------
.../request_trial_extension.js | 1 +
.../revert_to_basic/revert_to_basic.js | 1 +
.../start_trial/start_trial.tsx | 2 +
.../sections/upload_license/upload_license.js | 176 +++++++++---------
.../store/reducers/license_management.js | 32 ++++
.../public/shared_imports.ts | 8 +
.../translations/translations/ja-JP.json | 4 -
.../translations/translations/zh-CN.json | 4 -
24 files changed, 325 insertions(+), 305 deletions(-)
create mode 100644 x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap
delete mode 100644 x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap
rename x-pack/plugins/license_management/__jest__/{license_status.test.js => license_page_header.test.js} (83%)
rename x-pack/plugins/license_management/public/application/sections/license_dashboard/{license_status => license_page_header}/index.js (80%)
create mode 100644 x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js
delete mode 100644 x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js
delete mode 100644 x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js
create mode 100644 x-pack/plugins/license_management/public/shared_imports.ts
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap
index 95921fa61233c..90a3eb98c64a1 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/add_license.test.js.snap
@@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`AddLicense component when license is active should display correct verbiage 1`] = `"Update your license If you already have a new license, upload it now.
"`;
+exports[`AddLicense component when license is active should display correct verbiage 1`] = `"Update your license If you already have a new license, upload it now.
"`;
-exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"Update your license If you already have a new license, upload it now.
"`;
+exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"Update your license If you already have a new license, upload it now.
"`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap
new file mode 100644
index 0000000000000..047e311f3d325
--- /dev/null
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/license_page_header.test.js.snap
@@ -0,0 +1,5 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`LicenseStatus component should display display warning is expired 1`] = `"
"`;
+
+exports[`LicenseStatus component should display normally when license is active 1`] = `"
"`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap
deleted file mode 100644
index 9bd1c878f8679..0000000000000
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/license_status.test.js.snap
+++ /dev/null
@@ -1,5 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`LicenseStatus component should display display warning is expired 1`] = `"
Your Platinum license has expired
Your license expired on
"`;
-
-exports[`LicenseStatus component should display normally when license is active 1`] = `"
Your Gold license is active
Your license will expire on October 12, 2099 7:00 PM EST
"`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
index 4d8b653c4b10d..fda479f2888ce 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `""`;
+exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `""`;
-exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `""`;
+exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `""`;
-exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `""`;
+exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `""`;
-exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `""`;
+exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `""`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
index be634a5b4f748..4fa45c4bec5ce 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`RevertToBasic component should display when license is about to expire 1`] = `""`;
+exports[`RevertToBasic component should display when license is about to expire 1`] = `""`;
-exports[`RevertToBasic component should display when license is expired 1`] = `""`;
+exports[`RevertToBasic component should display when license is expired 1`] = `""`;
-exports[`RevertToBasic component should display when trial is active 1`] = `""`;
+exports[`RevertToBasic component should display when trial is active 1`] = `""`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
index 1cacadb824630..622bff86ead16 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`StartTrial component when trial is allowed display for basic license 1`] = `""`;
+exports[`StartTrial component when trial is allowed display for basic license 1`] = `""`;
-exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `""`;
+exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `""`;
-exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `""`;
+exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `""`;
-exports[`StartTrial component when trial is allowed should display for gold license 1`] = `""`;
+exports[`StartTrial component when trial is allowed should display for gold license 1`] = `""`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
index 9f89179d207e0..29ec3ddbfdc02 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
@@ -262,16 +262,18 @@ exports[`UploadLicense should display a modal when license requires acknowledgem
uploadLicenseStatus={[Function]}
>
@@ -1301,16 +1303,18 @@ exports[`UploadLicense should display an error when ES says license is expired 1
uploadLicenseStatus={[Function]}
>
@@ -2031,16 +2035,18 @@ exports[`UploadLicense should display an error when ES says license is invalid 1
uploadLicenseStatus={[Function]}
>
@@ -2761,16 +2767,18 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`]
uploadLicenseStatus={[Function]}
>
@@ -3491,16 +3499,18 @@ exports[`UploadLicense should display error when ES returns error 1`] = `
uploadLicenseStatus={[Function]}
>
diff --git a/x-pack/plugins/license_management/__jest__/license_status.test.js b/x-pack/plugins/license_management/__jest__/license_page_header.test.js
similarity index 83%
rename from x-pack/plugins/license_management/__jest__/license_status.test.js
rename to x-pack/plugins/license_management/__jest__/license_page_header.test.js
index 898667e13a1b3..56a71eb8d252e 100644
--- a/x-pack/plugins/license_management/__jest__/license_status.test.js
+++ b/x-pack/plugins/license_management/__jest__/license_page_header.test.js
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { LicenseStatus } from '../public/application/sections/license_dashboard/license_status';
+import { LicensePageHeader } from '../public/application/sections/license_dashboard/license_page_header';
import { createMockLicense, getComponent } from './util';
describe('LicenseStatus component', () => {
@@ -14,7 +14,7 @@ describe('LicenseStatus component', () => {
{
license: createMockLicense('gold'),
},
- LicenseStatus
+ LicensePageHeader
);
expect(rendered.html()).toMatchSnapshot();
});
@@ -23,7 +23,7 @@ describe('LicenseStatus component', () => {
{
license: createMockLicense('platinum', 0),
},
- LicenseStatus
+ LicensePageHeader
);
expect(rendered.html()).toMatchSnapshot();
});
diff --git a/x-pack/plugins/license_management/kibana.json b/x-pack/plugins/license_management/kibana.json
index 1f925a453898e..be2e21c7eb41e 100644
--- a/x-pack/plugins/license_management/kibana.json
+++ b/x-pack/plugins/license_management/kibana.json
@@ -9,6 +9,7 @@
"extraPublicDirs": ["common/constants"],
"requiredBundles": [
"telemetryManagementSection",
+ "esUiShared",
"kibanaReact"
]
}
diff --git a/x-pack/plugins/license_management/public/application/app.js b/x-pack/plugins/license_management/public/application/app.js
index 3bfa22dd72921..4b5a6144dbdc9 100644
--- a/x-pack/plugins/license_management/public/application/app.js
+++ b/x-pack/plugins/license_management/public/application/app.js
@@ -10,7 +10,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { LicenseDashboard, UploadLicense } from './sections';
import { Switch, Route } from 'react-router-dom';
import { APP_PERMISSION } from '../../common/constants';
-import { EuiPageBody, EuiEmptyPrompt, EuiText, EuiLoadingSpinner, EuiCallOut } from '@elastic/eui';
+import { SectionLoading } from '../shared_imports';
+import { EuiPageContent, EuiPageBody, EuiEmptyPrompt } from '@elastic/eui';
export class App extends Component {
componentDidMount() {
@@ -23,52 +24,50 @@ export class App extends Component {
if (permissionsLoading) {
return (
- }
- body={
-
-
-
- }
- data-test-subj="sectionLoading"
- />
+
+
+
+
+
);
}
if (permissionsError) {
+ const error = permissionsError?.data?.message;
+
return (
-
- }
- color="danger"
- iconType="alert"
- >
- {permissionsError.data && permissionsError.data.message ? (
- {permissionsError.data.message}
- ) : null}
-
+
+
+
+
+ }
+ body={error ? {error}
: null}
+ />
+
);
}
if (!hasPermission) {
return (
-
+
+
-
+
}
body={
@@ -82,7 +81,7 @@ export class App extends Component {
}
/>
-
+
);
}
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js
index 4120b2280a7a6..90de14b167e52 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/add_license/add_license.js
@@ -18,6 +18,7 @@ export const AddLicense = ({ uploadPath = `/upload_license` }) => {
return (
{} }) => {
useEffect(() => {
@@ -19,17 +20,19 @@ export const LicenseDashboard = ({ setBreadcrumb, telemetry } = { setBreadcrumb:
});
return (
-
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
);
};
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/index.js
similarity index 80%
rename from x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js
rename to x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/index.js
index efd4da2770db4..303e30040ab50 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/index.js
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/index.js
@@ -5,4 +5,4 @@
* 2.0.
*/
-export { LicenseStatus } from './license_status.container';
+export { LicensePageHeader } from './license_page_header';
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js
new file mode 100644
index 0000000000000..df41d46ac5789
--- /dev/null
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_page_header/license_page_header.js
@@ -0,0 +1,106 @@
+/*
+ * 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 { useSelector } from 'react-redux';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiPageHeader, EuiSpacer } from '@elastic/eui';
+
+import { getLicenseState } from '../../../store/reducers/license_management';
+
+export const ActiveLicensePageHeader = ({ license, ...props }) => {
+ return (
+
+
+
+ }
+ description={
+
+ {license.expirationDate ? (
+ {license.expirationDate},
+ }}
+ />
+ ) : (
+
+ )}
+
+ }
+ />
+ );
+};
+
+export const ExpiredLicensePageHeader = ({ license, ...props }) => {
+ return (
+
+
+
+ }
+ description={
+
+ {license.expirationDate},
+ }}
+ />
+
+ }
+ />
+ );
+};
+
+export const LicensePageHeader = () => {
+ const license = useSelector(getLicenseState);
+
+ return (
+ <>
+ {license.isExpired ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js
deleted file mode 100644
index 01577e79fd6ec..0000000000000
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.container.js
+++ /dev/null
@@ -1,36 +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 { LicenseStatus as PresentationComponent } from './license_status';
-import { connect } from 'react-redux';
-import {
- getLicense,
- getExpirationDateFormatted,
- isExpired,
-} from '../../../store/reducers/license_management';
-import { i18n } from '@kbn/i18n';
-
-const mapStateToProps = (state) => {
- const { isActive, type } = getLicense(state);
- return {
- status: isActive
- ? i18n.translate('xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText', {
- defaultMessage: 'Active',
- })
- : i18n.translate(
- 'xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText',
- {
- defaultMessage: 'Inactive',
- }
- ),
- type,
- isExpired: isExpired(state),
- expiryDate: getExpirationDateFormatted(state),
- };
-};
-
-export const LicenseStatus = connect(mapStateToProps)(PresentationComponent);
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js
deleted file mode 100644
index 5f7e59bf1ceba..0000000000000
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/license_status/license_status.js
+++ /dev/null
@@ -1,98 +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 React, { Fragment } from 'react';
-
-import {
- EuiIcon,
- EuiFlexGroup,
- EuiFlexItem,
- EuiText,
- EuiTitle,
- EuiSpacer,
- EuiTextAlign,
-} from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-export class LicenseStatus extends React.PureComponent {
- render() {
- const { isExpired, status, type, expiryDate } = this.props;
- const typeTitleCase = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase();
- let icon;
- let title;
- let message;
- if (isExpired) {
- icon = ;
- message = (
-
- {expiryDate},
- }}
- />
-
- );
- title = (
-
- );
- } else {
- icon = ;
- message = expiryDate ? (
-
- {expiryDate},
- }}
- />
-
- ) : (
-
-
-
- );
- title = (
-
- );
- }
- return (
-
-
- {icon}
-
-
- {title}
-
-
-
-
-
-
- {message}
-
- );
- }
-}
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js
index 8c694cf27765a..e578c372b9c9f 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js
@@ -37,6 +37,7 @@ export const RequestTrialExtension = ({ shouldShowRequestTrialExtension }) => {
return (
{this.acknowledgeModal()}
{
{this.acknowledgeModal(dependencies!.docLinks)}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
- {this.acknowledgeModal()}
+ {this.acknowledgeModal()}
-
-
-
-
-
- {currentLicenseType.toUpperCase()},
- }}
- />
-
-
-
-
-
-
-
-
- }
- onChange={this.handleFile}
+
+
+
+
+
+ {currentLicenseType.toUpperCase()},
+ }}
+ />
+
+
+
+
+
+
+
+
+ }
+ onChange={this.handleFile}
+ />
+
+
+
+
+ {shouldShowTelemetryOptIn(telemetry) && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ {applying ? (
+
-
-
-
-
- {shouldShowTelemetryOptIn(telemetry) && (
-
- )}
-
-
-
-
+ ) : (
-
-
-
-
- {applying ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
-
+ )}
+
+
+
+
+
+
);
}
}
diff --git a/x-pack/plugins/license_management/public/application/store/reducers/license_management.js b/x-pack/plugins/license_management/public/application/store/reducers/license_management.js
index 20e31cf89da72..1a985cd8ee623 100644
--- a/x-pack/plugins/license_management/public/application/store/reducers/license_management.js
+++ b/x-pack/plugins/license_management/public/application/store/reducers/license_management.js
@@ -6,6 +6,10 @@
*/
import { combineReducers } from 'redux';
+import { i18n } from '@kbn/i18n';
+import { capitalize } from 'lodash';
+import { createSelector } from 'reselect';
+
import { license } from './license';
import { uploadStatus } from './upload_status';
import { startBasicStatus } from './start_basic_license_status';
@@ -135,3 +139,31 @@ export const startBasicLicenseNeedsAcknowledgement = (state) => {
export const getStartBasicMessages = (state) => {
return state.startBasicStatus.messages;
};
+
+export const getLicenseState = createSelector(
+ getLicense,
+ getExpirationDateFormatted,
+ isExpired,
+ (license, expirationDate, isExpired) => {
+ const { isActive, type } = license;
+
+ return {
+ type: capitalize(type),
+ isExpired,
+ expirationDate,
+ status: isActive
+ ? i18n.translate(
+ 'xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText',
+ {
+ defaultMessage: 'active',
+ }
+ )
+ : i18n.translate(
+ 'xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText',
+ {
+ defaultMessage: 'inactive',
+ }
+ ),
+ };
+ }
+);
diff --git a/x-pack/plugins/license_management/public/shared_imports.ts b/x-pack/plugins/license_management/public/shared_imports.ts
new file mode 100644
index 0000000000000..695432684a660
--- /dev/null
+++ b/x-pack/plugins/license_management/public/shared_imports.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export { SectionLoading } from '../../../../src/plugins/es_ui_shared/public/';
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index b6e22dc4a519b..227afc122f804 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -12969,11 +12969,7 @@
"xpack.licenseMgmt.licenseDashboard.addLicense.updateLicenseButtonLabel": "ライセンスを更新",
"xpack.licenseMgmt.licenseDashboard.addLicense.updateLicenseTitle": "ライセンスの更新",
"xpack.licenseMgmt.licenseDashboard.addLicense.useAvailableLicenseDescription": "すでに新しいライセンスがある場合は、今すぐアップロードしてください。",
- "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusDescription": "ライセンスは{expiryDate}に期限切れになります",
"xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText": "アクティブ",
- "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusTitle": "ご使用の{typeTitleCase}ライセンスは{status}です",
- "xpack.licenseMgmt.licenseDashboard.licenseStatus.expiredLicenseStatusDescription": "ご使用のライセンスは{expiryDate}に期限切れになりました",
- "xpack.licenseMgmt.licenseDashboard.licenseStatus.expiredLicenseStatusTitle": "ご使用の{typeTitleCase}ライセンスは期限切れです",
"xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText": "非アクティブ",
"xpack.licenseMgmt.licenseDashboard.licenseStatus.permanentActiveLicenseStatusDescription": "ご使用のライセンスには有効期限がありません。",
"xpack.licenseMgmt.licenseDashboard.requestTrialExtension.extendTrialButtonLabel": "トライアルを延長",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 6ad4e7da08293..ac43a6938aac3 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -13143,11 +13143,7 @@
"xpack.licenseMgmt.licenseDashboard.addLicense.updateLicenseButtonLabel": "更新许可证",
"xpack.licenseMgmt.licenseDashboard.addLicense.updateLicenseTitle": "更新您的许可证",
"xpack.licenseMgmt.licenseDashboard.addLicense.useAvailableLicenseDescription": "如果已有新的许可证,请立即上传。",
- "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusDescription": "您的许可证将于 {expiryDate}过期",
"xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusText": "活动",
- "xpack.licenseMgmt.licenseDashboard.licenseStatus.activeLicenseStatusTitle": "您的{typeTitleCase}许可证{status}",
- "xpack.licenseMgmt.licenseDashboard.licenseStatus.expiredLicenseStatusDescription": "您的许可证已于 {expiryDate}过期",
- "xpack.licenseMgmt.licenseDashboard.licenseStatus.expiredLicenseStatusTitle": "您的{typeTitleCase}许可证已过期",
"xpack.licenseMgmt.licenseDashboard.licenseStatus.inactiveLicenseStatusText": "非活动",
"xpack.licenseMgmt.licenseDashboard.licenseStatus.permanentActiveLicenseStatusDescription": "您的许可证永不会过期。",
"xpack.licenseMgmt.licenseDashboard.requestTrialExtension.extendTrialButtonLabel": "延期试用",
From 7df924f828272554f662aa3b97ce2d7f7905f6c1 Mon Sep 17 00:00:00 2001
From: Katrin Freihofner
Date: Tue, 22 Jun 2021 09:31:15 +0200
Subject: [PATCH 047/163] Wording update for case settings, fixes #102462
(#102496)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../public/components/configure_cases/translations.ts | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts
index 1a60521667bba..ca41db577700e 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/translations.ts
+++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts
@@ -12,7 +12,7 @@ export * from '../../common/translations';
export const INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate(
'xpack.cases.configureCases.incidentManagementSystemTitle',
{
- defaultMessage: 'Connect to external incident management system',
+ defaultMessage: 'External incident management system',
}
);
@@ -20,7 +20,7 @@ export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate(
'xpack.cases.configureCases.incidentManagementSystemDesc',
{
defaultMessage:
- 'You may optionally connect cases to an external incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.',
+ 'Connect your cases to an external incident management system. You can then push case data as an incident in a third-party system.',
}
);
@@ -38,7 +38,7 @@ export const ADD_NEW_CONNECTOR = i18n.translate('xpack.cases.configureCases.addN
export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate(
'xpack.cases.configureCases.caseClosureOptionsTitle',
{
- defaultMessage: 'Case Closures',
+ defaultMessage: 'Case closures',
}
);
@@ -46,14 +46,14 @@ export const CASE_CLOSURE_OPTIONS_DESC = i18n.translate(
'xpack.cases.configureCases.caseClosureOptionsDesc',
{
defaultMessage:
- 'Define how you wish cases to be closed. Automated case closures require an established connection to an external incident management system.',
+ 'Define how to close your cases. Automatic closures require an established connection to an external incident management system.',
}
);
export const CASE_CLOSURE_OPTIONS_SUB_CASES = i18n.translate(
'xpack.cases.configureCases.caseClosureOptionsSubCases',
{
- defaultMessage: 'Automated closures of sub-cases is not currently supported.',
+ defaultMessage: 'Automatic closure of sub-cases is not supported.',
}
);
From 1ea35069c08938027567ecb360ea0730efbfa42a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20S=C3=A1nchez?=
Date: Tue, 22 Jun 2021 09:47:05 +0200
Subject: [PATCH 048/163] [Security solution][Endpoint] Removes 'none'
compression as it not used anymore (#102767)
* Removes 'none' compression as it not used anymore
* Revert type because none type is needed for the first time the artifact is created befor the compression
---
.../services/artifacts/manifest_manager/manifest_manager.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
index 27108a03f3403..f2d1d3660d78e 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
@@ -380,7 +380,6 @@ export class ManifestManager {
for (const result of results) {
await iterateArtifactsBuildResult(result, async (artifact, policyId) => {
const artifactToAdd = baselineManifest.getArtifact(getArtifactId(artifact)) || artifact;
- artifactToAdd.compressionAlgorithm = 'none';
if (!internalArtifactCompleteSchema.is(artifactToAdd)) {
throw new Error(`Incomplete artifact detected: ${getArtifactId(artifactToAdd)}`);
}
From 62fc27bf5583c5d87af202b1cff8479335ec6545 Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Tue, 22 Jun 2021 09:59:36 +0200
Subject: [PATCH 049/163] unksip functional test (#102633)
---
test/functional/page_objects/time_to_visualize_page.ts | 5 ++++-
x-pack/test/functional/apps/lens/add_to_dashboard.ts | 3 +--
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/test/functional/page_objects/time_to_visualize_page.ts b/test/functional/page_objects/time_to_visualize_page.ts
index 287b03ec60d88..57a22103f6409 100644
--- a/test/functional/page_objects/time_to_visualize_page.ts
+++ b/test/functional/page_objects/time_to_visualize_page.ts
@@ -51,7 +51,10 @@ export class TimeToVisualizePageObject extends FtrService {
vizName: string,
{ saveAsNew, redirectToOrigin, addToDashboard, dashboardId, saveToLibrary }: SaveModalArgs = {}
) {
- await this.testSubjects.setValue('savedObjectTitle', vizName);
+ await this.testSubjects.setValue('savedObjectTitle', vizName, {
+ typeCharByChar: true,
+ clearWithKeyboard: true,
+ });
const hasSaveAsNew = await this.testSubjects.exists('saveAsNewCheckbox');
if (hasSaveAsNew && saveAsNew !== undefined) {
diff --git a/x-pack/test/functional/apps/lens/add_to_dashboard.ts b/x-pack/test/functional/apps/lens/add_to_dashboard.ts
index 61b0c63d226fa..5e51573e32503 100644
--- a/x-pack/test/functional/apps/lens/add_to_dashboard.ts
+++ b/x-pack/test/functional/apps/lens/add_to_dashboard.ts
@@ -62,8 +62,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.lens.assertMetric('Maximum of bytes', '19,986');
};
- // flaky https://github.com/elastic/kibana/issues/102332
- describe.skip('lens add-to-dashboards tests', () => {
+ describe('lens add-to-dashboards tests', () => {
it('should allow new lens to be added by value to a new dashboard', async () => {
await createNewLens();
await PageObjects.lens.save('New Lens from Modal', false, false, false, 'new');
From 38604863e593e9c7aa239d077a1317709ac50131 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Casper=20H=C3=BCbertz?=
Date: Tue, 22 Jun 2021 10:07:20 +0200
Subject: [PATCH 050/163] [Metrics] Update ActionsMenu create alert styles
(#102316)
* [Metrics] Add divider in the actions menu
* [Metrics] Add color and icon to the alert link
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../inventory_view/components/waffle/node_context_menu.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx
index 94b16448a6b61..ea80bd13e8a4d 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx
@@ -25,6 +25,7 @@ import {
SectionSubtitle,
SectionLinks,
SectionLink,
+ ActionMenuDivider,
} from '../../../../../../../observability/public';
import { useLinkProps } from '../../../../../hooks/use_link_props';
@@ -173,7 +174,10 @@ export const NodeContextMenu: React.FC = withTheme
-
+
+
+
+
From df8637ae47747091d6e1ae2caafadc8fe69f913c Mon Sep 17 00:00:00 2001
From: Julien Mailleret <8582351+jmlrt@users.noreply.github.com>
Date: Tue, 22 Jun 2021 12:30:05 +0200
Subject: [PATCH 051/163] Fix UBI source URL (#102736)
* Fix UBI source URL
This commit fix the source URL for UBI image to ensure that it stays
consistent with the URL generated in
https://artifacts.elastic.co/reports/dependencies/dependencies-current.html
* Update src/dev/run_licenses_csv_report.js
Co-authored-by: Jonathan Budzenski
* Update src/dev/run_licenses_csv_report.js
Co-authored-by: Jonathan Budzenski
* try to make eslint happy
Co-authored-by: Jonathan Budzenski
---
src/dev/run_licenses_csv_report.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/dev/run_licenses_csv_report.js b/src/dev/run_licenses_csv_report.js
index 8a612c9e3d878..1923eddff33e9 100644
--- a/src/dev/run_licenses_csv_report.js
+++ b/src/dev/run_licenses_csv_report.js
@@ -71,7 +71,8 @@ run(
licenses: [
'Custom;https://www.redhat.com/licenses/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf',
],
- sourceURL: 'https://oss-dependencies.elastic.co/redhat/ubi/ubi-minimal-8-source.tar.gz',
+ sourceURL:
+ 'https://oss-dependencies.elastic.co/red-hat-universal-base-image-minimal/8/ubi-minimal-8-source.tar.gz',
}
);
From fc55c30e8bff46b4e39928ca423fa7f425d8b8ec Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Tue, 22 Jun 2021 12:53:32 +0200
Subject: [PATCH 052/163] Add cache-control for assets served via
`registerStaticDir` (#102756)
* Add cache-control for assets served via `registerStaticDir`
* fix case
* add test for 'dynamic' file content
---
src/core/server/http/http_server.test.ts | 127 +++++++++++++++++-
src/core/server/http/http_server.ts | 8 +-
.../static/compression_available.json | 3 +
.../static/compression_available.json.gz | Bin 0 -> 70 bytes
.../fixtures/static/some_json.json | 3 +
5 files changed, 136 insertions(+), 5 deletions(-)
create mode 100644 src/core/server/http/integration_tests/fixtures/static/compression_available.json
create mode 100644 src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz
create mode 100644 src/core/server/http/integration_tests/fixtures/static/some_json.json
diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts
index 7624a11a6f03f..ffbd91c645382 100644
--- a/src/core/server/http/http_server.test.ts
+++ b/src/core/server/http/http_server.test.ts
@@ -7,9 +7,10 @@
*/
import { Server } from 'http';
-import { readFileSync } from 'fs';
+import { rmdir, mkdtemp, readFile, writeFile } from 'fs/promises';
import supertest from 'supertest';
import { omit } from 'lodash';
+import { join } from 'path';
import { ByteSizeValue, schema } from '@kbn/config-schema';
import { HttpConfig } from './http_config';
@@ -47,9 +48,9 @@ const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
let certificate: string;
let key: string;
-beforeAll(() => {
- certificate = readFileSync(KBN_CERT_PATH, 'utf8');
- key = readFileSync(KBN_KEY_PATH, 'utf8');
+beforeAll(async () => {
+ certificate = await readFile(KBN_CERT_PATH, 'utf8');
+ key = await readFile(KBN_KEY_PATH, 'utf8');
});
beforeEach(() => {
@@ -1409,6 +1410,19 @@ describe('setup contract', () => {
});
describe('#registerStaticDir', () => {
+ const assetFolder = join(__dirname, 'integration_tests', 'fixtures', 'static');
+ let tempDir: string;
+
+ beforeAll(async () => {
+ tempDir = await mkdtemp('cache-test');
+ });
+
+ afterAll(async () => {
+ if (tempDir) {
+ await rmdir(tempDir, { recursive: true });
+ }
+ });
+
test('does not throw if called after stop', async () => {
const { registerStaticDir } = await server.setup(config);
await server.stop();
@@ -1416,6 +1430,111 @@ describe('setup contract', () => {
registerStaticDir('/path1/{path*}', '/path/to/resource');
}).not.toThrow();
});
+
+ test('returns correct headers for static assets', async () => {
+ const { registerStaticDir, server: innerServer } = await server.setup(config);
+
+ registerStaticDir('/static/{path*}', assetFolder);
+
+ await server.start();
+ const response = await supertest(innerServer.listener)
+ .get('/static/some_json.json')
+ .expect(200);
+
+ expect(response.get('cache-control')).toEqual('must-revalidate');
+ expect(response.get('etag')).not.toBeUndefined();
+ });
+
+ test('returns compressed version if present', async () => {
+ const { registerStaticDir, server: innerServer } = await server.setup(config);
+
+ registerStaticDir('/static/{path*}', assetFolder);
+
+ await server.start();
+ const response = await supertest(innerServer.listener)
+ .get('/static/compression_available.json')
+ .set('accept-encoding', 'gzip')
+ .expect(200);
+
+ expect(response.get('cache-control')).toEqual('must-revalidate');
+ expect(response.get('etag')).not.toBeUndefined();
+ expect(response.get('content-encoding')).toEqual('gzip');
+ });
+
+ test('returns uncompressed version if compressed asset is not available', async () => {
+ const { registerStaticDir, server: innerServer } = await server.setup(config);
+
+ registerStaticDir('/static/{path*}', assetFolder);
+
+ await server.start();
+ const response = await supertest(innerServer.listener)
+ .get('/static/some_json.json')
+ .set('accept-encoding', 'gzip')
+ .expect(200);
+
+ expect(response.get('cache-control')).toEqual('must-revalidate');
+ expect(response.get('etag')).not.toBeUndefined();
+ expect(response.get('content-encoding')).toBeUndefined();
+ });
+
+ test('returns a 304 if etag value matches', async () => {
+ const { registerStaticDir, server: innerServer } = await server.setup(config);
+
+ registerStaticDir('/static/{path*}', assetFolder);
+
+ await server.start();
+ const response = await supertest(innerServer.listener)
+ .get('/static/some_json.json')
+ .expect(200);
+
+ const etag = response.get('etag');
+ expect(etag).not.toBeUndefined();
+
+ await supertest(innerServer.listener)
+ .get('/static/some_json.json')
+ .set('If-None-Match', etag)
+ .expect(304);
+ });
+
+ test('serves content if etag values does not match', async () => {
+ const { registerStaticDir, server: innerServer } = await server.setup(config);
+
+ registerStaticDir('/static/{path*}', assetFolder);
+
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/static/some_json.json')
+ .set('If-None-Match', `"definitely not a valid etag"`)
+ .expect(200);
+ });
+
+ test('dynamically updates depending on the content of the file', async () => {
+ const tempFile = join(tempDir, 'some_file.json');
+
+ const { registerStaticDir, server: innerServer } = await server.setup(config);
+ registerStaticDir('/static/{path*}', tempDir);
+
+ await server.start();
+
+ await supertest(innerServer.listener).get('/static/some_file.json').expect(404);
+
+ await writeFile(tempFile, `{ "over": 9000 }`);
+
+ let response = await supertest(innerServer.listener)
+ .get('/static/some_file.json')
+ .expect(200);
+
+ const etag1 = response.get('etag');
+
+ await writeFile(tempFile, `{ "over": 42 }`);
+
+ response = await supertest(innerServer.listener).get('/static/some_file.json').expect(200);
+
+ const etag2 = response.get('etag');
+
+ expect(etag1).not.toEqual(etag2);
+ });
});
describe('#registerOnPreRouting', () => {
diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts
index 8b4c3b9416152..d43d86d587d06 100644
--- a/src/core/server/http/http_server.ts
+++ b/src/core/server/http/http_server.ts
@@ -465,7 +465,13 @@ export class HttpServer {
lookupCompressed: true,
},
},
- options: { auth: false },
+ options: {
+ auth: false,
+ cache: {
+ privacy: 'public',
+ otherwise: 'must-revalidate',
+ },
+ },
});
}
diff --git a/src/core/server/http/integration_tests/fixtures/static/compression_available.json b/src/core/server/http/integration_tests/fixtures/static/compression_available.json
new file mode 100644
index 0000000000000..1f878fb465cff
--- /dev/null
+++ b/src/core/server/http/integration_tests/fixtures/static/compression_available.json
@@ -0,0 +1,3 @@
+{
+ "hello": "dolly"
+}
diff --git a/src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz b/src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz
new file mode 100644
index 0000000000000000000000000000000000000000..e77819d2e8e59a357c56c3c74624d3b82476bdf1
GIT binary patch
literal 70
zcmV-M0J;AkiwFp-o6%qZ17mM(aB^jHb7^mGUtxA(X>4I)Y-KKLb8l_{tL9QrP|8Tn
c$;nr;Qcz0C&&jD&;;Q8W0MZJ;EEfO(0RJ`}XaE2J
literal 0
HcmV?d00001
diff --git a/src/core/server/http/integration_tests/fixtures/static/some_json.json b/src/core/server/http/integration_tests/fixtures/static/some_json.json
new file mode 100644
index 0000000000000..c8c4105eb57cd
--- /dev/null
+++ b/src/core/server/http/integration_tests/fixtures/static/some_json.json
@@ -0,0 +1,3 @@
+{
+ "foo": "bar"
+}
From 65de579d5a2e85f5295e98ad94163c3af2ebec7e Mon Sep 17 00:00:00 2001
From: Katrin Freihofner
Date: Tue, 22 Jun 2021 13:15:17 +0200
Subject: [PATCH 053/163] Renamed button and dropdown items in headers (apm,
logs, metrics and uptime) from alerts to rules (#100918)
Co-authored-by: Jonathan Buttner
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Christos Nasikas
Co-authored-by: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com>
Co-authored-by: Steph Milovic
---
.../alerting_popover_flyout.tsx | 8 ++--
.../components/metrics_alert_dropdown.tsx | 12 +++---
.../manage_alerts_context_menu_item.tsx | 2 +-
.../components/alert_dropdown.tsx | 4 +-
.../public/alerts/configuration.tsx | 10 ++---
.../public/pages/overview/empty_section.ts | 2 +-
.../translations/translations/ja-JP.json | 38 -------------------
.../translations/translations/zh-CN.json | 38 -------------------
.../header/action_menu_content.test.tsx | 6 +--
.../alerts/toggle_alert_flyout_button.tsx | 6 +--
.../overview/alerts/translations.ts | 19 ++++++----
.../__snapshots__/monitor_list.test.tsx.snap | 4 +-
.../columns/define_connectors.tsx | 14 +++----
.../columns/enable_alert.test.tsx | 4 +-
.../monitor_list/columns/translations.ts | 4 +-
.../monitor_list_drawer/enabled_alerts.tsx | 6 +--
.../public/lib/alert_types/alert_messages.tsx | 2 +-
.../uptime/public/state/alerts/alerts.ts | 6 +--
18 files changed, 55 insertions(+), 130 deletions(-)
diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx
index 5b4f4e24af44d..ca73f6ddd05b3 100644
--- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx
+++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx
@@ -18,7 +18,7 @@ import { AlertType } from '../../../../common/alert_types';
import { AlertingFlyout } from '../../alerting/alerting_flyout';
const alertLabel = i18n.translate('xpack.apm.home.alertsMenu.alerts', {
- defaultMessage: 'Alerts',
+ defaultMessage: 'Alerts and rules',
});
const transactionDurationLabel = i18n.translate(
'xpack.apm.home.alertsMenu.transactionDuration',
@@ -33,11 +33,11 @@ const errorCountLabel = i18n.translate('xpack.apm.home.alertsMenu.errorCount', {
});
const createThresholdAlertLabel = i18n.translate(
'xpack.apm.home.alertsMenu.createThresholdAlert',
- { defaultMessage: 'Create threshold alert' }
+ { defaultMessage: 'Create threshold rule' }
);
const createAnomalyAlertAlertLabel = i18n.translate(
'xpack.apm.home.alertsMenu.createAnomalyAlert',
- { defaultMessage: 'Create anomaly alert' }
+ { defaultMessage: 'Create anomaly rule' }
);
const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID =
@@ -102,7 +102,7 @@ export function AlertingPopoverAndFlyout({
{
name: i18n.translate(
'xpack.apm.home.alertsMenu.viewActiveAlerts',
- { defaultMessage: 'View active alerts' }
+ { defaultMessage: 'Manage rules' }
),
href: basePath.prepend(
'/app/management/insightsAndAlerting/triggersActions/alerts'
diff --git a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx
index 41867053c3a0f..c3327dc3fe85d 100644
--- a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx
+++ b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx
@@ -36,12 +36,12 @@ export const MetricsAlertDropdown = () => {
() => ({
id: 1,
title: i18n.translate('xpack.infra.alerting.infrastructureDropdownTitle', {
- defaultMessage: 'Infrastructure alerts',
+ defaultMessage: 'Infrastructure rules',
}),
items: [
{
name: i18n.translate('xpack.infra.alerting.createInventoryAlertButton', {
- defaultMessage: 'Create inventory alert',
+ defaultMessage: 'Create inventory rule',
}),
onClick: () => setVisibleFlyoutType('inventory'),
},
@@ -54,12 +54,12 @@ export const MetricsAlertDropdown = () => {
() => ({
id: 2,
title: i18n.translate('xpack.infra.alerting.metricsDropdownTitle', {
- defaultMessage: 'Metrics alerts',
+ defaultMessage: 'Metrics rules',
}),
items: [
{
name: i18n.translate('xpack.infra.alerting.createThresholdAlertButton', {
- defaultMessage: 'Create threshold alert',
+ defaultMessage: 'Create threshold rule',
}),
onClick: () => setVisibleFlyoutType('threshold'),
},
@@ -76,7 +76,7 @@ export const MetricsAlertDropdown = () => {
const manageAlertsMenuItem = useMemo(
() => ({
name: i18n.translate('xpack.infra.alerting.manageAlerts', {
- defaultMessage: 'Manage alerts',
+ defaultMessage: 'Manage rules',
}),
icon: 'tableOfContents',
onClick: manageAlertsLinkProps.onClick,
@@ -112,7 +112,7 @@ export const MetricsAlertDropdown = () => {
{
id: 0,
title: i18n.translate('xpack.infra.alerting.alertDropdownTitle', {
- defaultMessage: 'Alerts',
+ defaultMessage: 'Alerts and rules',
}),
items: firstPanelMenuItems,
},
diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx
index a6b69a37f780e..c9b6275264f91 100644
--- a/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx
+++ b/x-pack/plugins/infra/public/alerting/inventory/components/manage_alerts_context_menu_item.tsx
@@ -17,7 +17,7 @@ export const ManageAlertsContextMenuItem = () => {
});
return (
-
+
);
};
diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx
index 66c77fbf875a4..c1733d4af0589 100644
--- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx
+++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx
@@ -66,13 +66,13 @@ export const AlertDropdown = () => {
>
,
,
];
diff --git a/x-pack/plugins/monitoring/public/alerts/configuration.tsx b/x-pack/plugins/monitoring/public/alerts/configuration.tsx
index 5416095671d71..7825fe8e20617 100644
--- a/x-pack/plugins/monitoring/public/alerts/configuration.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/configuration.tsx
@@ -32,7 +32,7 @@ export const AlertConfiguration: React.FC = (props: Props) => {
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.disableAlert.errorTitle', {
- defaultMessage: `Unable to disable alert`,
+ defaultMessage: `Unable to disable rule`,
}),
text: err.message,
});
@@ -46,7 +46,7 @@ export const AlertConfiguration: React.FC = (props: Props) => {
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.enableAlert.errorTitle', {
- defaultMessage: `Unable to enable alert`,
+ defaultMessage: `Unable to enable rule`,
}),
text: err.message,
});
@@ -60,7 +60,7 @@ export const AlertConfiguration: React.FC = (props: Props) => {
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.muteAlert.errorTitle', {
- defaultMessage: `Unable to mute alert`,
+ defaultMessage: `Unable to mute rule`,
}),
text: err.message,
});
@@ -74,7 +74,7 @@ export const AlertConfiguration: React.FC = (props: Props) => {
} catch (err) {
Legacy.shims.toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.alerts.panel.ummuteAlert.errorTitle', {
- defaultMessage: `Unable to unmute alert`,
+ defaultMessage: `Unable to unmute rule`,
}),
text: err.message,
});
@@ -112,7 +112,7 @@ export const AlertConfiguration: React.FC = (props: Props) => {
}}
>
{i18n.translate('xpack.monitoring.alerts.panel.editAlert', {
- defaultMessage: `Edit alert`,
+ defaultMessage: `Edit rule`,
})}
diff --git a/x-pack/plugins/observability/public/pages/overview/empty_section.ts b/x-pack/plugins/observability/public/pages/overview/empty_section.ts
index 40b1157b29e35..2747b2ecdebc9 100644
--- a/x-pack/plugins/observability/public/pages/overview/empty_section.ts
+++ b/x-pack/plugins/observability/public/pages/overview/empty_section.ts
@@ -97,7 +97,7 @@ export const getEmptySections = ({ core }: { core: CoreStart }): ISection[] => {
'Are 503 errors stacking up? Are services responding? Is CPU and RAM utilization jumping? See warnings as they happen—not as part of the post-mortem.',
}),
linkTitle: i18n.translate('xpack.observability.emptySection.apps.alert.link', {
- defaultMessage: 'Create alert',
+ defaultMessage: 'Create rule',
}),
href: core.http.basePath.prepend(
'/app/management/insightsAndAlerting/triggersActions/alerts'
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 227afc122f804..9520c1ad0d9c1 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -5490,13 +5490,9 @@
"xpack.apm.header.badge.readOnly.text": "読み取り専用",
"xpack.apm.header.badge.readOnly.tooltip": "を保存できませんでした",
"xpack.apm.helpMenu.upgradeAssistantLink": "アップグレードアシスタント",
- "xpack.apm.home.alertsMenu.alerts": "アラート",
"xpack.apm.home.alertsMenu.createAnomalyAlert": "異常アラートを作成",
- "xpack.apm.home.alertsMenu.createThresholdAlert": "しきい値アラートを作成",
"xpack.apm.home.alertsMenu.errorCount": "エラー数",
- "xpack.apm.home.alertsMenu.transactionDuration": "レイテンシ",
"xpack.apm.home.alertsMenu.transactionErrorRate": "トランザクションエラー率",
- "xpack.apm.home.alertsMenu.viewActiveAlerts": "アクティブアラートを表示",
"xpack.apm.home.serviceMapTabLabel": "サービスマップ",
"xpack.apm.instancesLatencyDistributionChartLegend": "インスタンス",
"xpack.apm.instancesLatencyDistributionChartLegend.previousPeriod": "前の期間",
@@ -10876,20 +10872,10 @@
"xpack.indexLifecycleMgmt.timeline.title": "ポリシー概要",
"xpack.indexLifecycleMgmt.timeline.warmPhaseSectionTitle": "ウォームフェーズ",
"xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。",
- "xpack.infra.alerting.alertDropdownTitle": "アラート",
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし (グループなし) ",
"xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件",
- "xpack.infra.alerting.alertsButton": "アラート",
- "xpack.infra.alerting.createInventoryAlertButton": "インベントリアラートの作成",
- "xpack.infra.alerting.createThresholdAlertButton": "しきい値アラートを作成",
"xpack.infra.alerting.infrastructureDropdownMenu": "インフラストラクチャー",
- "xpack.infra.alerting.infrastructureDropdownTitle": "インフラストラクチャーアラート",
- "xpack.infra.alerting.logs.alertsButton": "アラート",
- "xpack.infra.alerting.logs.createAlertButton": "アラートの作成",
- "xpack.infra.alerting.logs.manageAlerts": "アラートを管理",
- "xpack.infra.alerting.manageAlerts": "アラートを管理",
"xpack.infra.alerting.metricsDropdownMenu": "メトリック",
- "xpack.infra.alerting.metricsDropdownTitle": "メトリックアラート",
"xpack.infra.alerts.charts.errorMessage": "問題が発生しました",
"xpack.infra.alerts.charts.loadingMessage": "読み込み中",
"xpack.infra.alerts.charts.noDataMessage": "グラフデータがありません",
@@ -15902,13 +15888,8 @@
"xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage": "Elasticsearchノード「{removed}」がこのクラスターから削除されました。",
"xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage": "このクラスターのElasticsearchノードは変更されていません。",
"xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage": "このクラスターでElasticsearchノード「{restarted}」が再起動しました。",
- "xpack.monitoring.alerts.panel.disableAlert.errorTitle": "アラートを無効にできません",
"xpack.monitoring.alerts.panel.disableTitle": "無効にする",
- "xpack.monitoring.alerts.panel.editAlert": "アラートを編集",
- "xpack.monitoring.alerts.panel.enableAlert.errorTitle": "アラートを有効にできません",
- "xpack.monitoring.alerts.panel.muteAlert.errorTitle": "アラートをミュートできません",
"xpack.monitoring.alerts.panel.muteTitle": "ミュート",
- "xpack.monitoring.alerts.panel.ummuteAlert.errorTitle": "アラートをミュート解除できません",
"xpack.monitoring.alerts.rejection.paramDetails.duration.label": "最後の",
"xpack.monitoring.alerts.rejection.paramDetails.threshold.label": "{type} 拒否カウントが超過するときに通知",
"xpack.monitoring.alerts.searchThreadPoolRejections.description": "検索スレッドプールの拒否数がしきい値を超過するときにアラートを発行します。",
@@ -17235,7 +17216,6 @@
"xpack.observability.alertsTable.viewInAppButtonLabel": "アプリで表示",
"xpack.observability.alertsTitle": "アラート",
"xpack.observability.emptySection.apps.alert.description": "503 エラーが累積していますか?サービスは応答していますか?CPUとRAMの使用量が跳ね上がっていますか?このような警告を、事後にではなく、発生と同時に把握しましょう。",
- "xpack.observability.emptySection.apps.alert.link": "アラートの作成",
"xpack.observability.emptySection.apps.alert.title": "アラートが見つかりません。",
"xpack.observability.emptySection.apps.apm.description": "分散アーキテクチャ全体でトランザクションを追跡し、サービスの通信をマップ化して、簡単にパフォーマンスボトルネックを特定できます。",
"xpack.observability.emptySection.apps.apm.link": "エージェントのインストール",
@@ -23526,8 +23506,6 @@
"xpack.uptime.alerts.tls.validAfterExpiringString": "{relativeDate}日以内、{date}に期限切れになります。",
"xpack.uptime.alerts.tls.validBeforeExpiredString": "{relativeDate}日前、{date}以降有効です。",
"xpack.uptime.alerts.tls.validBeforeExpiringString": "今から{relativeDate}日間、{date}まで無効です。",
- "xpack.uptime.alerts.toggleAlertFlyoutButtonText": "アラート",
- "xpack.uptime.alertsPopover.toggleButton.ariaLabel": "アラートコンテキストメニューを開く",
"xpack.uptime.apmIntegrationAction.description": "このモニターの検索 APM",
"xpack.uptime.apmIntegrationAction.text": "APMデータを表示",
"xpack.uptime.availabilityLabelText": "{value} %",
@@ -23746,15 +23724,11 @@
"xpack.uptime.monitorDetails.title.pingType.tcp": "TCP ping",
"xpack.uptime.monitorList.anomalyColumn.label": "レスポンス異常スコア",
"xpack.uptime.monitorList.defineConnector.description": "アラートを有効にするには、デフォルトのアラートアクションコネクターを定義してください。",
- "xpack.uptime.monitorList.disableDownAlert": "ステータスアラートを無効にする",
"xpack.uptime.monitorList.downLineSeries.downLabel": "ダウン",
"xpack.uptime.monitorList.drawer.missingLocation": "一部の Heartbeat インスタンスには位置情報が定義されていません。Heartbeat 構成への{link}。",
"xpack.uptime.monitorList.drawer.mostRecentRun": "直近のテスト実行",
"xpack.uptime.monitorList.drawer.statusRowLocationList": "前回の確認時に\"{status}\"ステータスだった場所のリスト。",
"xpack.uptime.monitorList.drawer.url": "Url",
- "xpack.uptime.monitorList.enabledAlerts.noAlert": "このモニターではアラートが有効ではありません。",
- "xpack.uptime.monitorList.enabledAlerts.title": "有効なアラート",
- "xpack.uptime.monitorList.enableDownAlert": "ステータスアラートを有効にする",
"xpack.uptime.monitorList.expandDrawerButton.ariaLabel": "ID {id}のモニターの行を展開",
"xpack.uptime.monitorList.geoName.helpLinkAnnotation": "場所を追加",
"xpack.uptime.monitorList.infraIntegrationAction.container.message": "コンテナーメトリックを表示",
@@ -23828,15 +23802,7 @@
"xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel": "最終確認からの経過時間",
"xpack.uptime.monitorStatusBar.type.ariaLabel": "モニタータイプ",
"xpack.uptime.monitorStatusBar.type.label": "型",
- "xpack.uptime.navigateToAlertingButton.content": "アラートを管理",
- "xpack.uptime.navigateToAlertingUi": "Uptime を離れてアラート管理ページに移動します",
"xpack.uptime.notFountPage.homeLinkText": "ホームへ戻る",
- "xpack.uptime.openAlertContextPanel.ariaLabel": "アラートコンテキストパネルを開くと、アラートタイプを選択できます",
- "xpack.uptime.openAlertContextPanel.label": "アラートの作成",
- "xpack.uptime.overview.alerts.disabled.failed": "アラートを無効にできません。",
- "xpack.uptime.overview.alerts.disabled.success": "アラートが正常に無効にされました。",
- "xpack.uptime.overview.alerts.enabled.failed": "アラートを有効にできません。",
- "xpack.uptime.overview.alerts.enabled.success": "アラートが正常に有効にされました。 ",
"xpack.uptime.overview.alerts.enabled.success.description": "この監視が停止しているときには、メッセージが {actionConnectors} に送信されます。",
"xpack.uptime.overview.filterButton.label": "{title}フィルターのフィルターグループを展開",
"xpack.uptime.overview.pageHeader.syntheticsCallout.announcementLink": "お知らせを読む",
@@ -24004,10 +23970,6 @@
"xpack.uptime.synthetics.waterfallChart.labels.timings.ssl": "TLS",
"xpack.uptime.synthetics.waterfallChart.labels.timings.wait": "待機中 (TTFB) ",
"xpack.uptime.title": "アップタイム",
- "xpack.uptime.toggleAlertButton.content": "ステータスアラートを監視",
- "xpack.uptime.toggleAlertFlyout.ariaLabel": "アラートの追加ポップアップを開く",
- "xpack.uptime.toggleTlsAlertButton.ariaLabel": "TLSアラートの追加ポップアップを開く",
- "xpack.uptime.toggleTlsAlertButton.content": "TLSアラート",
"xpack.uptime.uptimeFeatureCatalogueTitle": "アップタイム",
"xpack.urlDrilldown.click.event.key.documentation": "クリックしたデータポイントの後ろのフィールド名。",
"xpack.urlDrilldown.click.event.key.title": "クリックしたフィールドの名前。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index ac43a6938aac3..f74d27eb8b214 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -5520,13 +5520,9 @@
"xpack.apm.header.badge.readOnly.text": "只读",
"xpack.apm.header.badge.readOnly.tooltip": "无法保存",
"xpack.apm.helpMenu.upgradeAssistantLink": "升级助手",
- "xpack.apm.home.alertsMenu.alerts": "告警",
"xpack.apm.home.alertsMenu.createAnomalyAlert": "创建异常告警",
- "xpack.apm.home.alertsMenu.createThresholdAlert": "创建阈值告警",
"xpack.apm.home.alertsMenu.errorCount": "错误计数",
- "xpack.apm.home.alertsMenu.transactionDuration": "延迟",
"xpack.apm.home.alertsMenu.transactionErrorRate": "事务错误率",
- "xpack.apm.home.alertsMenu.viewActiveAlerts": "查看活动告警",
"xpack.apm.home.serviceMapTabLabel": "服务地图",
"xpack.apm.instancesLatencyDistributionChartLegend": "实例",
"xpack.apm.instancesLatencyDistributionChartLegend.previousPeriod": "上一时段",
@@ -11015,20 +11011,10 @@
"xpack.indexLifecycleMgmt.timeline.title": "策略摘要",
"xpack.indexLifecycleMgmt.timeline.warmPhaseSectionTitle": "温阶段",
"xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。",
- "xpack.infra.alerting.alertDropdownTitle": "告警",
"xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容 (未分组) ",
"xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据",
- "xpack.infra.alerting.alertsButton": "告警",
- "xpack.infra.alerting.createInventoryAlertButton": "创建库存告警",
- "xpack.infra.alerting.createThresholdAlertButton": "创建阈值告警",
"xpack.infra.alerting.infrastructureDropdownMenu": "基础设施",
- "xpack.infra.alerting.infrastructureDropdownTitle": "基础架构告警",
- "xpack.infra.alerting.logs.alertsButton": "告警",
- "xpack.infra.alerting.logs.createAlertButton": "创建告警",
- "xpack.infra.alerting.logs.manageAlerts": "管理告警",
- "xpack.infra.alerting.manageAlerts": "管理告警",
"xpack.infra.alerting.metricsDropdownMenu": "指标",
- "xpack.infra.alerting.metricsDropdownTitle": "指标告警",
"xpack.infra.alerts.charts.errorMessage": "哇哦,出问题了",
"xpack.infra.alerts.charts.loadingMessage": "正在加载",
"xpack.infra.alerts.charts.noDataMessage": "没有可用图表数据",
@@ -16138,13 +16124,8 @@
"xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage": "Elasticsearch 节点“{removed}”已从此集群中移除。",
"xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage": "此集群的 Elasticsearch 节点中没有更改。",
"xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage": "此集群中 Elasticsearch 节点“{restarted}”已重新启动。",
- "xpack.monitoring.alerts.panel.disableAlert.errorTitle": "无法禁用告警",
"xpack.monitoring.alerts.panel.disableTitle": "禁用",
- "xpack.monitoring.alerts.panel.editAlert": "编辑告警",
- "xpack.monitoring.alerts.panel.enableAlert.errorTitle": "无法启用告警",
- "xpack.monitoring.alerts.panel.muteAlert.errorTitle": "无法静音告警",
"xpack.monitoring.alerts.panel.muteTitle": "静音",
- "xpack.monitoring.alerts.panel.ummuteAlert.errorTitle": "无法取消告警静音",
"xpack.monitoring.alerts.rejection.paramDetails.duration.label": "过去",
"xpack.monitoring.alerts.rejection.paramDetails.threshold.label": "当 {type} 拒绝计数超过以下阈值时通知:",
"xpack.monitoring.alerts.searchThreadPoolRejections.description": "当搜索线程池中的拒绝数目超过阈值时告警。",
@@ -17471,7 +17452,6 @@
"xpack.observability.alertsTable.viewInAppButtonLabel": "在应用中查看",
"xpack.observability.alertsTitle": "告警",
"xpack.observability.emptySection.apps.alert.description": "503 错误是否越来越多?服务是否响应?CPU 和 RAM 利用率是否激增?实时查看警告,而不是事后再进行剖析。",
- "xpack.observability.emptySection.apps.alert.link": "创建告警",
"xpack.observability.emptySection.apps.alert.title": "未找到告警。",
"xpack.observability.emptySection.apps.apm.description": "通过分布式体系结构跟踪事务并映射服务的交互以轻松发现性能瓶颈。",
"xpack.observability.emptySection.apps.apm.link": "安装代理",
@@ -23892,8 +23872,6 @@
"xpack.uptime.alerts.tls.validAfterExpiringString": "将在{relativeDate} 天后,即 {date}到期。",
"xpack.uptime.alerts.tls.validBeforeExpiredString": "自 {relativeDate} 天前,即 {date}开始生效。",
"xpack.uptime.alerts.tls.validBeforeExpiringString": "从现在到 {date}的 {relativeDate} 天里无效。",
- "xpack.uptime.alerts.toggleAlertFlyoutButtonText": "告警",
- "xpack.uptime.alertsPopover.toggleButton.ariaLabel": "打开告警上下文菜单",
"xpack.uptime.apmIntegrationAction.description": "在 APM 中搜索此监测",
"xpack.uptime.apmIntegrationAction.text": "显示 APM 数据",
"xpack.uptime.availabilityLabelText": "{value} %",
@@ -24112,15 +24090,11 @@
"xpack.uptime.monitorDetails.title.pingType.tcp": "TCP ping",
"xpack.uptime.monitorList.anomalyColumn.label": "响应异常分数",
"xpack.uptime.monitorList.defineConnector.description": "要开始启用告警,请在以下位置定义默认告警操作连接器",
- "xpack.uptime.monitorList.disableDownAlert": "禁用状态告警",
"xpack.uptime.monitorList.downLineSeries.downLabel": "关闭检查",
"xpack.uptime.monitorList.drawer.missingLocation": "某些 Heartbeat 实例未定义位置。{link}到您的 Heartbeat 配置。",
"xpack.uptime.monitorList.drawer.mostRecentRun": "最新测试运行",
"xpack.uptime.monitorList.drawer.statusRowLocationList": "上次检查时状态为“{status}”的位置列表。",
"xpack.uptime.monitorList.drawer.url": "URL",
- "xpack.uptime.monitorList.enabledAlerts.noAlert": "没有为此监测启用告警。",
- "xpack.uptime.monitorList.enabledAlerts.title": "已启用的告警",
- "xpack.uptime.monitorList.enableDownAlert": "启用状态告警",
"xpack.uptime.monitorList.expandDrawerButton.ariaLabel": "展开 ID {id} 的监测行",
"xpack.uptime.monitorList.geoName.helpLinkAnnotation": "添加位置",
"xpack.uptime.monitorList.infraIntegrationAction.container.message": "显示容器指标",
@@ -24194,15 +24168,7 @@
"xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel": "自上次检查以来经过的时间",
"xpack.uptime.monitorStatusBar.type.ariaLabel": "监测类型",
"xpack.uptime.monitorStatusBar.type.label": "类型",
- "xpack.uptime.navigateToAlertingButton.content": "管理告警",
- "xpack.uptime.navigateToAlertingUi": "离开 Uptime 并前往“Alerting 管理”页面",
"xpack.uptime.notFountPage.homeLinkText": "返回主页",
- "xpack.uptime.openAlertContextPanel.ariaLabel": "打开告警上下文面板,以便可以选择告警类型",
- "xpack.uptime.openAlertContextPanel.label": "创建告警",
- "xpack.uptime.overview.alerts.disabled.failed": "无法禁用告警!",
- "xpack.uptime.overview.alerts.disabled.success": "已成功禁用告警!",
- "xpack.uptime.overview.alerts.enabled.failed": "无法启用告警!",
- "xpack.uptime.overview.alerts.enabled.success": "已成功启用告警 ",
"xpack.uptime.overview.alerts.enabled.success.description": "此监测关闭时,将有消息发送到 {actionConnectors}。",
"xpack.uptime.overview.filterButton.label": "展开筛选 {title} 的筛选组",
"xpack.uptime.overview.pageHeader.syntheticsCallout.announcementLink": "阅读公告",
@@ -24370,10 +24336,6 @@
"xpack.uptime.synthetics.waterfallChart.labels.timings.ssl": "TLS",
"xpack.uptime.synthetics.waterfallChart.labels.timings.wait": "等待中 (TTFB)",
"xpack.uptime.title": "运行时间",
- "xpack.uptime.toggleAlertButton.content": "监测状态告警",
- "xpack.uptime.toggleAlertFlyout.ariaLabel": "打开添加告警浮出控件",
- "xpack.uptime.toggleTlsAlertButton.ariaLabel": "打开 TLS 告警浮出控件",
- "xpack.uptime.toggleTlsAlertButton.content": "TLS 告警",
"xpack.uptime.uptimeFeatureCatalogueTitle": "运行时间",
"xpack.urlDrilldown.click.event.key.documentation": "已点击数据点背后的字段名称。",
"xpack.urlDrilldown.click.event.key.title": "已点击字段的名称。",
diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx
index 89d8f38b1e3b3..0265588c3fdeb 100644
--- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx
+++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx
@@ -14,12 +14,12 @@ describe('ActionMenuContent', () => {
it('renders alerts dropdown', async () => {
const { getByLabelText, getByText } = render( );
- const alertsDropdown = getByLabelText('Open alert context menu');
+ const alertsDropdown = getByLabelText('Open alerts and rules context menu');
fireEvent.click(alertsDropdown);
await waitFor(() => {
- expect(getByText('Create alert'));
- expect(getByText('Manage alerts'));
+ expect(getByText('Create rule'));
+ expect(getByText('Manage rules'));
});
});
diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx
index a1b745d07924e..278958bd1987b 100644
--- a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx
@@ -67,7 +67,7 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({
>
),
@@ -114,7 +114,7 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({
},
{
id: ALERT_CONTEXT_SELECT_TYPE_PANEL_ID,
- title: 'create alerts',
+ title: ToggleFlyoutTranslations.toggleAlertFlyoutButtonLabel,
items: selectionItems,
},
];
@@ -134,7 +134,7 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({
>
}
diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts b/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts
index 00a00a4664cd8..7cfcdabe5562b 100644
--- a/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts
+++ b/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts
@@ -283,30 +283,33 @@ export const TlsTranslations = {
export const ToggleFlyoutTranslations = {
toggleButtonAriaLabel: i18n.translate('xpack.uptime.alertsPopover.toggleButton.ariaLabel', {
- defaultMessage: 'Open alert context menu',
+ defaultMessage: 'Open alerts and rules context menu',
}),
openAlertContextPanelAriaLabel: i18n.translate('xpack.uptime.openAlertContextPanel.ariaLabel', {
- defaultMessage: 'Open the alert context panel so you can choose an alert type',
+ defaultMessage: 'Open the rule context panel so you can choose a rule type',
}),
openAlertContextPanelLabel: i18n.translate('xpack.uptime.openAlertContextPanel.label', {
- defaultMessage: 'Create alert',
+ defaultMessage: 'Create rule',
}),
toggleTlsAriaLabel: i18n.translate('xpack.uptime.toggleTlsAlertButton.ariaLabel', {
- defaultMessage: 'Open TLS alert flyout',
+ defaultMessage: 'Open TLS rule flyout',
}),
toggleTlsContent: i18n.translate('xpack.uptime.toggleTlsAlertButton.content', {
- defaultMessage: 'TLS alert',
+ defaultMessage: 'TLS rule',
}),
toggleMonitorStatusAriaLabel: i18n.translate('xpack.uptime.toggleAlertFlyout.ariaLabel', {
- defaultMessage: 'Open add alert flyout',
+ defaultMessage: 'Open add rule flyout',
}),
toggleMonitorStatusContent: i18n.translate('xpack.uptime.toggleAlertButton.content', {
- defaultMessage: 'Monitor status alert',
+ defaultMessage: 'Monitor status rule',
}),
navigateToAlertingUIAriaLabel: i18n.translate('xpack.uptime.navigateToAlertingUi', {
defaultMessage: 'Leave Uptime and go to Alerting Management page',
}),
navigateToAlertingButtonContent: i18n.translate('xpack.uptime.navigateToAlertingButton.content', {
- defaultMessage: 'Manage alerts',
+ defaultMessage: 'Manage rules',
+ }),
+ toggleAlertFlyoutButtonLabel: i18n.translate('xpack.uptime.alerts.createRulesPanel.title', {
+ defaultMessage: 'Create rules',
}),
};
diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap
index 115dab1095dc1..cfdf7afba4e85 100644
--- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap
+++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__snapshots__/monitor_list.test.tsx.snap
@@ -1303,7 +1303,7 @@ exports[`MonitorList component renders the monitor list 1`] = `
>
{
@@ -44,16 +44,16 @@ export const DefineAlertConnectors = () => {
closePopover={closePopover}
>
- {' '}
{SETTINGS_LINK_TEXT}
-
+ {' '}
+
);
diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.test.tsx
index 8aa63504df813..427985839ba89 100644
--- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.test.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.test.tsx
@@ -33,9 +33,7 @@ describe('EnableAlertComponent', () => {
'href',
'/settings?focusConnectorField=true'
);
- expect(
- getByText('To start enabling alerts, please define a default alert action connector in')
- );
+ expect(getByText('to receive status alerts.'));
});
it('does not displays define connectors when there is connector', () => {
diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/translations.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/translations.ts
index 9c598a4ac5112..eadf8febebcf2 100644
--- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/translations.ts
+++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/translations.ts
@@ -8,11 +8,11 @@
import { i18n } from '@kbn/i18n';
export const ENABLE_STATUS_ALERT = i18n.translate('xpack.uptime.monitorList.enableDownAlert', {
- defaultMessage: 'Enable status alert',
+ defaultMessage: 'Enable rule',
});
export const DISABLE_STATUS_ALERT = i18n.translate('xpack.uptime.monitorList.disableDownAlert', {
- defaultMessage: 'Disable status alert',
+ defaultMessage: 'Disable rule',
});
export const EXPAND_TAGS_LABEL = i18n.translate('xpack.uptime.monitorList.tags.expand', {
diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx
index 9b8456d850ce0..33e1303b41643 100644
--- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx
@@ -52,8 +52,8 @@ export const EnabledAlerts = ({ monitorAlerts, loading }: Props) => {
{i18n.translate('xpack.uptime.monitorList.enabledAlerts.title', {
- defaultMessage: 'Enabled alerts',
- description: 'Alerts enabled for this monitor',
+ defaultMessage: 'Enabled rules',
+ description: 'Rules enabled for this monitor',
})}
@@ -61,7 +61,7 @@ export const EnabledAlerts = ({ monitorAlerts, loading }: Props) => {
)}
diff --git a/x-pack/plugins/uptime/public/lib/alert_types/alert_messages.tsx b/x-pack/plugins/uptime/public/lib/alert_types/alert_messages.tsx
index 204073afc5c26..8d08bdbb26c91 100644
--- a/x-pack/plugins/uptime/public/lib/alert_types/alert_messages.tsx
+++ b/x-pack/plugins/uptime/public/lib/alert_types/alert_messages.tsx
@@ -14,7 +14,7 @@ import { ActionConnector } from '../../state/alerts/alerts';
export const simpleAlertEnabled = (defaultActions: ActionConnector[]) => {
return {
title: i18n.translate('xpack.uptime.overview.alerts.enabled.success', {
- defaultMessage: 'Alert successfully enabled ',
+ defaultMessage: 'Rule successfully enabled ',
}),
text: toMountPoint(
(
const showAlertDisabledSuccess = () => {
kibanaService.core.notifications.toasts.addSuccess(
i18n.translate('xpack.uptime.overview.alerts.disabled.success', {
- defaultMessage: 'Alert successfully disabled!',
+ defaultMessage: 'Rule successfully disabled!',
})
);
};
@@ -94,7 +94,7 @@ const showAlertDisabledSuccess = () => {
const showAlertDisabledFailed = (err: Error) => {
kibanaService.core.notifications.toasts.addError(err, {
title: i18n.translate('xpack.uptime.overview.alerts.disabled.failed', {
- defaultMessage: 'Alert cannot be disabled!',
+ defaultMessage: 'Rule cannot be disabled!',
}),
});
};
@@ -157,7 +157,7 @@ export function* fetchAlertsEffect() {
} catch (err) {
kibanaService.core.notifications.toasts.addError(err, {
title: i18n.translate('xpack.uptime.overview.alerts.enabled.failed', {
- defaultMessage: 'Alert cannot be enabled!',
+ defaultMessage: 'Rule cannot be enabled!',
}),
});
yield put(createAlertAction.fail(err));
From 89e851fd3077141a7816344facba5f2551269edd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Tue, 22 Jun 2021 07:58:49 -0400
Subject: [PATCH 054/163] [APM] Fleet: Introduce API for uploading source maps
for RUM (#101623)
* creating fleet source maps apis
* fixing ts issues
* fixing test
* fixing ts issue
* nests `rum` under `config.apm-server.value` within the package policy input
* refactoring and adding test
* removing unit test
* removing unused imports
* addressing PR comments
* addressing PR comments
* addressing PR comments
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Oliver Gupte
---
x-pack/plugins/apm/kibana.json | 2 +-
.../fleet/register_fleet_policy_callbacks.ts | 14 +-
.../apm/server/lib/fleet/source_maps.test.ts | 148 +++++++++++++++
.../apm/server/lib/fleet/source_maps.ts | 174 ++++++++++++++++++
.../get_global_apm_server_route_repository.ts | 4 +-
.../plugins/apm/server/routes/source_maps.ts | 138 ++++++++++++++
.../services/artifacts/artifacts.test.ts | 1 +
.../server/services/artifacts/artifacts.ts | 1 +
8 files changed, 474 insertions(+), 8 deletions(-)
create mode 100644 x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts
create mode 100644 x-pack/plugins/apm/server/lib/fleet/source_maps.ts
create mode 100644 x-pack/plugins/apm/server/routes/source_maps.ts
diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json
index e33c410668c25..21aef379715c7 100644
--- a/x-pack/plugins/apm/kibana.json
+++ b/x-pack/plugins/apm/kibana.json
@@ -43,4 +43,4 @@
"ml",
"observability"
]
-}
+}
\ No newline at end of file
diff --git a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts
index 35c7f0dfdfd73..c122a5c406eab 100644
--- a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts
+++ b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts
@@ -12,6 +12,7 @@ import { APMPluginStartDependencies } from '../../types';
import { ExternalCallback } from '../../../../fleet/server';
import { AGENT_NAME } from '../../../common/elasticsearch_fieldnames';
import { AgentConfiguration } from '../../../common/agent_configuration/configuration_types';
+import { getPackagePolicyWithSourceMap, listArtifacts } from './source_maps';
export async function registerFleetPolicyCallbacks({
plugins,
@@ -31,7 +32,7 @@ export async function registerFleetPolicyCallbacks({
// Registers a callback invoked when a policy is created to populate the APM
// integration policy with pre-existing agent configurations
- registerAgentConfigExternalCallback({
+ registerPackagePolicyExternalCallback({
fleetPluginStart,
callbackName: 'packagePolicyCreate',
plugins,
@@ -42,7 +43,7 @@ export async function registerFleetPolicyCallbacks({
// Registers a callback invoked when a policy is updated to populate the APM
// integration policy with existing agent configurations
- registerAgentConfigExternalCallback({
+ registerPackagePolicyExternalCallback({
fleetPluginStart,
callbackName: 'packagePolicyUpdate',
plugins,
@@ -53,11 +54,11 @@ export async function registerFleetPolicyCallbacks({
}
type ExternalCallbackParams = Parameters;
-type PackagePolicy = ExternalCallbackParams[0];
+export type PackagePolicy = ExternalCallbackParams[0];
type Context = ExternalCallbackParams[1];
type Request = ExternalCallbackParams[2];
-function registerAgentConfigExternalCallback({
+function registerPackagePolicyExternalCallback({
fleetPluginStart,
callbackName,
plugins,
@@ -91,8 +92,9 @@ function registerAgentConfigExternalCallback({
ruleDataClient,
});
const agentConfigurations = await listConfigurations({ setup });
+ const artifacts = await listArtifacts({ fleetPluginStart });
return getPackagePolicyWithAgentConfigurations(
- packagePolicy,
+ getPackagePolicyWithSourceMap({ packagePolicy, artifacts }),
agentConfigurations
);
};
@@ -100,7 +102,7 @@ function registerAgentConfigExternalCallback({
fleetPluginStart.registerExternalCallback(callbackName, callbackFn);
}
-const APM_SERVER = 'apm-server';
+export const APM_SERVER = 'apm-server';
// Immutable function applies the given package policy with a set of agent configurations
export function getPackagePolicyWithAgentConfigurations(
diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts
new file mode 100644
index 0000000000000..61a4fa4436e69
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts
@@ -0,0 +1,148 @@
+/*
+ * 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 {
+ ArtifactSourceMap,
+ getPackagePolicyWithSourceMap,
+} from './source_maps';
+
+const packagePolicy = {
+ id: '123',
+ version: 'WzMxNDI2LDFd',
+ name: 'apm-1',
+ description: '',
+ namespace: 'default',
+ policy_id: '7a87c160-c961-11eb-81e2-f7327d61c92a',
+ enabled: true,
+ output_id: '',
+ inputs: [
+ {
+ policy_template: 'apmserver',
+ streams: [],
+ vars: {},
+ type: 'apm',
+ enabled: true,
+ compiled_input: {
+ 'apm-server': {
+ capture_personal_data: true,
+ max_event_size: 307200,
+ api_key: { limit: 100, enabled: false },
+ default_service_environment: null,
+ host: 'localhost:8200',
+ kibana: { api_key: null },
+ secret_token: null,
+ },
+ },
+ },
+ ],
+ package: { name: 'apm', title: 'Elastic APM', version: '0.2.0' },
+ created_at: '2021-06-16T14:54:32.195Z',
+ created_by: 'elastic',
+};
+
+const artifacts = [
+ {
+ type: 'sourcemap',
+ identifier: 'service_name-1.0.0',
+ relative_url: '/api/fleet/artifacts/service_name-1.0.0/my-id-1',
+ body: {
+ serviceName: 'service_name',
+ serviceVersion: '1.0.0',
+ bundleFilepath: 'http://localhost:3000/static/js/main.chunk.js',
+ sourceMap: {
+ version: 3,
+ file: 'static/js/main.chunk.js',
+ sources: ['foo'],
+ sourcesContent: ['foo'],
+ mappings: 'foo',
+ sourceRoot: '',
+ },
+ },
+ created: '2021-06-16T15:03:55.049Z',
+ id: 'apm:service_name-1.0.0-my-id-1',
+ compressionAlgorithm: 'zlib',
+ decodedSha256: 'my-id-1',
+ decodedSize: 9440,
+ encodedSha256: 'sha123',
+ encodedSize: 2622,
+ encryptionAlgorithm: 'none',
+ packageName: 'apm',
+ },
+ {
+ type: 'sourcemap',
+ identifier: 'service_name-2.0.0',
+ relative_url: '/api/fleet/artifacts/service_name-2.0.0/my-id-2',
+ body: {
+ serviceName: 'service_name',
+ serviceVersion: '2.0.0',
+ bundleFilepath: 'http://localhost:3000/static/js/main.chunk.js',
+ sourceMap: {
+ version: 3,
+ file: 'static/js/main.chunk.js',
+ sources: ['foo'],
+ sourcesContent: ['foo'],
+ mappings: 'foo',
+ sourceRoot: '',
+ },
+ },
+ created: '2021-06-16T15:03:55.049Z',
+ id: 'apm:service_name-2.0.0-my-id-2',
+ compressionAlgorithm: 'zlib',
+ decodedSha256: 'my-id-2',
+ decodedSize: 9440,
+ encodedSha256: 'sha456',
+ encodedSize: 2622,
+ encryptionAlgorithm: 'none',
+ packageName: 'apm',
+ },
+] as ArtifactSourceMap[];
+
+describe('Source maps', () => {
+ describe('getPackagePolicyWithSourceMap', () => {
+ it('returns unchanged package policy when artifacts is empty', () => {
+ const updatedPackagePolicy = getPackagePolicyWithSourceMap({
+ packagePolicy,
+ artifacts: [],
+ });
+ expect(updatedPackagePolicy).toEqual(packagePolicy);
+ });
+ it('adds source maps into the package policy', () => {
+ const updatedPackagePolicy = getPackagePolicyWithSourceMap({
+ packagePolicy,
+ artifacts,
+ });
+ expect(updatedPackagePolicy.inputs[0].config).toEqual({
+ 'apm-server': {
+ value: {
+ rum: {
+ source_mapping: {
+ metadata: [
+ {
+ 'service.name': 'service_name',
+ 'service.version': '1.0.0',
+ 'bundle.filepath':
+ 'http://localhost:3000/static/js/main.chunk.js',
+ 'sourcemap.url':
+ '/api/fleet/artifacts/service_name-1.0.0/my-id-1',
+ },
+ {
+ 'service.name': 'service_name',
+ 'service.version': '2.0.0',
+ 'bundle.filepath':
+ 'http://localhost:3000/static/js/main.chunk.js',
+ 'sourcemap.url':
+ '/api/fleet/artifacts/service_name-2.0.0/my-id-2',
+ },
+ ],
+ },
+ },
+ },
+ },
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts
new file mode 100644
index 0000000000000..b313fbad2806f
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts
@@ -0,0 +1,174 @@
+/*
+ * 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 * as t from 'io-ts';
+import {
+ CoreSetup,
+ CoreStart,
+ ElasticsearchClient,
+ SavedObjectsClientContract,
+} from 'kibana/server';
+import { promisify } from 'util';
+import { unzip } from 'zlib';
+import { Artifact } from '../../../../fleet/server';
+import { sourceMapRt } from '../../routes/source_maps';
+import { APMPluginStartDependencies } from '../../types';
+import { getApmPackgePolicies } from './get_apm_package_policies';
+import { APM_SERVER, PackagePolicy } from './register_fleet_policy_callbacks';
+
+export interface ApmArtifactBody {
+ serviceName: string;
+ serviceVersion: string;
+ bundleFilepath: string;
+ sourceMap: t.TypeOf;
+}
+export type ArtifactSourceMap = Omit & {
+ body: ApmArtifactBody;
+};
+
+export type FleetPluginStart = NonNullable;
+
+const doUnzip = promisify(unzip);
+
+function decodeArtifacts(artifacts: Artifact[]): Promise {
+ return Promise.all(
+ artifacts.map(async (artifact) => {
+ const body = await doUnzip(Buffer.from(artifact.body, 'base64'));
+ return {
+ ...artifact,
+ body: JSON.parse(body.toString()) as ApmArtifactBody,
+ };
+ })
+ );
+}
+
+function getApmArtifactClient(fleetPluginStart: FleetPluginStart) {
+ return fleetPluginStart.createArtifactsClient('apm');
+}
+
+export async function listArtifacts({
+ fleetPluginStart,
+}: {
+ fleetPluginStart: FleetPluginStart;
+}) {
+ const apmArtifactClient = getApmArtifactClient(fleetPluginStart);
+ const artifacts = await apmArtifactClient.listArtifacts({
+ kuery: 'type: sourcemap',
+ });
+
+ return decodeArtifacts(artifacts.items);
+}
+
+export async function createApmArtifact({
+ apmArtifactBody,
+ fleetPluginStart,
+}: {
+ apmArtifactBody: ApmArtifactBody;
+ fleetPluginStart: FleetPluginStart;
+}) {
+ const apmArtifactClient = getApmArtifactClient(fleetPluginStart);
+ const identifier = `${apmArtifactBody.serviceName}-${apmArtifactBody.serviceVersion}`;
+
+ return apmArtifactClient.createArtifact({
+ type: 'sourcemap',
+ identifier,
+ content: JSON.stringify(apmArtifactBody),
+ });
+}
+
+export async function deleteApmArtifact({
+ id,
+ fleetPluginStart,
+}: {
+ id: string;
+ fleetPluginStart: FleetPluginStart;
+}) {
+ const apmArtifactClient = getApmArtifactClient(fleetPluginStart);
+ return apmArtifactClient.deleteArtifact(id);
+}
+
+export function getPackagePolicyWithSourceMap({
+ packagePolicy,
+ artifacts,
+}: {
+ packagePolicy: PackagePolicy;
+ artifacts: ArtifactSourceMap[];
+}) {
+ if (!artifacts.length) {
+ return packagePolicy;
+ }
+ const [firstInput, ...restInputs] = packagePolicy.inputs;
+ return {
+ ...packagePolicy,
+ inputs: [
+ {
+ ...firstInput,
+ config: {
+ ...firstInput.config,
+ [APM_SERVER]: {
+ value: {
+ ...firstInput?.config?.[APM_SERVER].value,
+ rum: {
+ source_mapping: {
+ metadata: artifacts.map((artifact) => ({
+ 'service.name': artifact.body.serviceName,
+ 'service.version': artifact.body.serviceVersion,
+ 'bundle.filepath': artifact.body.bundleFilepath,
+ 'sourcemap.url': artifact.relative_url,
+ })),
+ },
+ },
+ },
+ },
+ },
+ },
+ ...restInputs,
+ ],
+ };
+}
+
+export async function updateSourceMapsOnFleetPolicies({
+ core,
+ fleetPluginStart,
+ savedObjectsClient,
+ elasticsearchClient,
+}: {
+ core: { setup: CoreSetup; start: () => Promise };
+ fleetPluginStart: FleetPluginStart;
+ savedObjectsClient: SavedObjectsClientContract;
+ elasticsearchClient: ElasticsearchClient;
+}) {
+ const artifacts = await listArtifacts({ fleetPluginStart });
+
+ const apmFleetPolicies = await getApmPackgePolicies({
+ core,
+ fleetPluginStart,
+ });
+
+ return Promise.all(
+ apmFleetPolicies.items.map(async (item) => {
+ const {
+ id,
+ revision,
+ updated_at: updatedAt,
+ updated_by: updatedBy,
+ ...packagePolicy
+ } = item;
+
+ const updatedPackagePolicy = getPackagePolicyWithSourceMap({
+ packagePolicy,
+ artifacts,
+ });
+
+ await fleetPluginStart.packagePolicyService.update(
+ savedObjectsClient,
+ elasticsearchClient,
+ id,
+ updatedPackagePolicy
+ );
+ })
+ );
+}
diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
index c151752b4b6e0..f1c08444d2e1e 100644
--- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
+++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
@@ -26,6 +26,7 @@ import { agentConfigurationRouteRepository } from './settings/agent_configuratio
import { anomalyDetectionRouteRepository } from './settings/anomaly_detection';
import { apmIndicesRouteRepository } from './settings/apm_indices';
import { customLinkRouteRepository } from './settings/custom_link';
+import { sourceMapsRouteRepository } from './source_maps';
import { traceRouteRepository } from './traces';
import { transactionRouteRepository } from './transactions';
import { APMRouteHandlerResources } from './typings';
@@ -48,7 +49,8 @@ const getTypedGlobalApmServerRouteRepository = () => {
.merge(agentConfigurationRouteRepository)
.merge(anomalyDetectionRouteRepository)
.merge(apmIndicesRouteRepository)
- .merge(customLinkRouteRepository);
+ .merge(customLinkRouteRepository)
+ .merge(sourceMapsRouteRepository);
return repository;
};
diff --git a/x-pack/plugins/apm/server/routes/source_maps.ts b/x-pack/plugins/apm/server/routes/source_maps.ts
new file mode 100644
index 0000000000000..24ea825774b0a
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/source_maps.ts
@@ -0,0 +1,138 @@
+/*
+ * 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 Boom from '@hapi/boom';
+import * as t from 'io-ts';
+import { SavedObjectsClientContract } from 'kibana/server';
+import {
+ createApmArtifact,
+ deleteApmArtifact,
+ listArtifacts,
+ updateSourceMapsOnFleetPolicies,
+} from '../lib/fleet/source_maps';
+import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
+
+export const sourceMapRt = t.intersection([
+ t.type({
+ version: t.number,
+ sources: t.array(t.string),
+ mappings: t.string,
+ }),
+ t.partial({
+ names: t.array(t.string),
+ file: t.string,
+ sourceRoot: t.string,
+ sourcesContent: t.array(t.string),
+ }),
+]);
+
+const listSourceMapRoute = createApmServerRoute({
+ endpoint: 'GET /api/apm/sourcemaps',
+ options: { tags: ['access:apm'] },
+ handler: async ({ plugins, logger }) => {
+ try {
+ const fleetPluginStart = await plugins.fleet?.start();
+ if (fleetPluginStart) {
+ const artifacts = await listArtifacts({ fleetPluginStart });
+ return { artifacts };
+ }
+ } catch (e) {
+ throw Boom.internal(
+ 'Something went wrong while fetching artifacts source maps',
+ e
+ );
+ }
+ },
+});
+
+const uploadSourceMapRoute = createApmServerRoute({
+ endpoint: 'POST /api/apm/sourcemaps/{serviceName}/{serviceVersion}',
+ options: { tags: ['access:apm', 'access:apm_write'] },
+ params: t.type({
+ path: t.type({
+ serviceName: t.string,
+ serviceVersion: t.string,
+ }),
+ body: t.type({
+ bundleFilepath: t.string,
+ sourceMap: sourceMapRt,
+ }),
+ }),
+ handler: async ({ params, plugins, core }) => {
+ const { serviceName, serviceVersion } = params.path;
+ const { bundleFilepath, sourceMap } = params.body;
+ const fleetPluginStart = await plugins.fleet?.start();
+ const coreStart = await core.start();
+ const esClient = coreStart.elasticsearch.client.asInternalUser;
+ const savedObjectsClient = await getInternalSavedObjectsClient(core.setup);
+ try {
+ if (fleetPluginStart) {
+ const artifact = await createApmArtifact({
+ fleetPluginStart,
+ apmArtifactBody: {
+ serviceName,
+ serviceVersion,
+ bundleFilepath,
+ sourceMap,
+ },
+ });
+ await updateSourceMapsOnFleetPolicies({
+ core,
+ fleetPluginStart,
+ savedObjectsClient: (savedObjectsClient as unknown) as SavedObjectsClientContract,
+ elasticsearchClient: esClient,
+ });
+
+ return artifact;
+ }
+ } catch (e) {
+ throw Boom.internal(
+ 'Something went wrong while creating a new source map',
+ e
+ );
+ }
+ },
+});
+
+const deleteSourceMapRoute = createApmServerRoute({
+ endpoint: 'DELETE /api/apm/sourcemaps/{id}',
+ options: { tags: ['access:apm', 'access:apm_write'] },
+ params: t.type({
+ path: t.type({
+ id: t.string,
+ }),
+ }),
+ handler: async ({ context, params, plugins, core }) => {
+ const fleetPluginStart = await plugins.fleet?.start();
+ const { id } = params.path;
+ const coreStart = await core.start();
+ const esClient = coreStart.elasticsearch.client.asInternalUser;
+ const savedObjectsClient = await getInternalSavedObjectsClient(core.setup);
+ try {
+ if (fleetPluginStart) {
+ await deleteApmArtifact({ id, fleetPluginStart });
+ await updateSourceMapsOnFleetPolicies({
+ core,
+ fleetPluginStart,
+ savedObjectsClient: (savedObjectsClient as unknown) as SavedObjectsClientContract,
+ elasticsearchClient: esClient,
+ });
+ }
+ } catch (e) {
+ throw Boom.internal(
+ `Something went wrong while deleting source map. id: ${id}`,
+ e
+ );
+ }
+ },
+});
+
+export const sourceMapsRouteRepository = createApmServerRouteRepository()
+ .add(listSourceMapRoute)
+ .add(uploadSourceMapRoute)
+ .add(deleteSourceMapRoute);
diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts
index 5681be3e8793b..b046b41d73722 100644
--- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts
+++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts
@@ -125,6 +125,7 @@ describe('When using the artifacts services', () => {
expect(esClientMock.delete).toHaveBeenCalledWith({
index: FLEET_SERVER_ARTIFACTS_INDEX,
id: '123',
+ refresh: 'wait_for',
});
});
diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts
index 26032ab94dbc8..6ac23cb1f9ef8 100644
--- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts
+++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts
@@ -87,6 +87,7 @@ export const deleteArtifact = async (esClient: ElasticsearchClient, id: string):
await esClient.delete({
index: FLEET_SERVER_ARTIFACTS_INDEX,
id,
+ refresh: 'wait_for',
});
} catch (e) {
throw new ArtifactsElasticsearchError(e);
From 60a3d5d415932757394a80ddcb53566e2631afd4 Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Tue, 22 Jun 2021 14:12:07 +0200
Subject: [PATCH 055/163] added ability to filter in global search by
"type:package" (#102754)
---
.../fleet/public/search_provider.test.ts | 100 ++++++++++++++++++
.../plugins/fleet/public/search_provider.ts | 69 ++++++++----
2 files changed, 146 insertions(+), 23 deletions(-)
diff --git a/x-pack/plugins/fleet/public/search_provider.test.ts b/x-pack/plugins/fleet/public/search_provider.test.ts
index fa2e8508c938e..c42ac2e5a1551 100644
--- a/x-pack/plugins/fleet/public/search_provider.test.ts
+++ b/x-pack/plugins/fleet/public/search_provider.test.ts
@@ -185,5 +185,105 @@ describe('Package search provider', () => {
expect(sendGetPackages).toHaveBeenCalledTimes(1);
});
+
+ describe('tags', () => {
+ test('without packages tag, without search term', () => {
+ getTestScheduler().run(({ hot, expectObservable }) => {
+ mockSendGetPackages.mockReturnValue(
+ hot('--(a|)', { a: { data: { response: testResponse } } })
+ );
+ setupMock.getStartServices.mockReturnValue(
+ hot('--(a|)', { a: [coreMock.createStart()] }) as any
+ );
+ const packageSearchProvider = createPackageSearchProvider(setupMock);
+ expectObservable(
+ packageSearchProvider.find(
+ { types: ['test'] },
+ { aborted$: NEVER, maxResults: 100, preference: '' }
+ )
+ ).toBe('(a|)', {
+ a: [],
+ });
+ });
+
+ expect(sendGetPackages).toHaveBeenCalledTimes(0);
+ });
+
+ test('with packages tag, with no search term', () => {
+ getTestScheduler().run(({ hot, expectObservable }) => {
+ mockSendGetPackages.mockReturnValue(
+ hot('--(a|)', { a: { data: { response: testResponse } } })
+ );
+ setupMock.getStartServices.mockReturnValue(
+ hot('--(a|)', { a: [coreMock.createStart()] }) as any
+ );
+ const packageSearchProvider = createPackageSearchProvider(setupMock);
+ expectObservable(
+ packageSearchProvider.find(
+ { types: ['package'] },
+ { aborted$: NEVER, maxResults: 100, preference: '' }
+ )
+ ).toBe('--(a|)', {
+ a: [
+ {
+ id: 'test-test',
+ score: 80,
+ title: 'test',
+ type: 'package',
+ url: {
+ path: 'undefined#/detail/test-test/overview',
+ prependBasePath: false,
+ },
+ },
+ {
+ id: 'test1-test1',
+ score: 80,
+ title: 'test1',
+ type: 'package',
+ url: {
+ path: 'undefined#/detail/test1-test1/overview',
+ prependBasePath: false,
+ },
+ },
+ ],
+ });
+ });
+
+ expect(sendGetPackages).toHaveBeenCalledTimes(1);
+ });
+
+ test('with packages tag, with search term', () => {
+ getTestScheduler().run(({ hot, expectObservable }) => {
+ mockSendGetPackages.mockReturnValue(
+ hot('--(a|)', { a: { data: { response: testResponse } } })
+ );
+ setupMock.getStartServices.mockReturnValue(
+ hot('--(a|)', { a: [coreMock.createStart()] }) as any
+ );
+ const packageSearchProvider = createPackageSearchProvider(setupMock);
+ expectObservable(
+ packageSearchProvider.find(
+ { term: 'test1', types: ['package'] },
+ { aborted$: NEVER, maxResults: 100, preference: '' }
+ )
+ ).toBe('--(a|)', {
+ a: [
+ {
+ id: 'test1-test1',
+ score: 80,
+ title: 'test1',
+ type: 'package',
+ url: {
+ path: 'undefined#/detail/test1-test1/overview',
+ prependBasePath: false,
+ },
+ },
+ ],
+ });
+ });
+
+ expect(sendGetPackages).toHaveBeenCalledTimes(1);
+ });
+ });
});
});
diff --git a/x-pack/plugins/fleet/public/search_provider.ts b/x-pack/plugins/fleet/public/search_provider.ts
index cd4ec1c29b457..56e08ecad29fb 100644
--- a/x-pack/plugins/fleet/public/search_provider.ts
+++ b/x-pack/plugins/fleet/public/search_provider.ts
@@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import type { CoreSetup, CoreStart } from 'src/core/public';
+import type { CoreSetup, CoreStart, ApplicationStart } from 'src/core/public';
import type { Observable } from 'rxjs';
import { from, of, combineLatest } from 'rxjs';
@@ -34,6 +34,26 @@ const createPackages$ = () =>
shareReplay(1)
);
+const toSearchResult = (
+ pkg: GetPackagesResponse['response'][number],
+ application: ApplicationStart
+) => {
+ const pkgkey = `${pkg.name}-${pkg.version}`;
+ return {
+ id: pkgkey,
+ type: packageType,
+ title: pkg.title,
+ score: 80,
+ url: {
+ // TODO: See https://github.com/elastic/kibana/issues/96134 for details about why we use '#' here. Below should be updated
+ // as part of migrating to non-hash based router.
+ // prettier-ignore
+ path: `${application.getUrlForApp(INTEGRATIONS_PLUGIN_ID)}#${pagePathGetters.integration_details_overview({ pkgkey })[1]}`,
+ prependBasePath: false,
+ },
+ };
+};
+
export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResultProvider => {
const coreStart$ = from(core.getStartServices()).pipe(
map(([coreStart]) => coreStart),
@@ -52,12 +72,23 @@ export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResult
return {
id: 'packages',
getSearchableTypes: () => [packageType],
- find: ({ term }, { maxResults, aborted$ }) => {
- if (!term) {
+ find: ({ term, types }, { maxResults, aborted$ }) => {
+ if (types?.includes(packageType) === false) {
return of([]);
}
- term = term.toLowerCase();
+ const hasTypes = Boolean(types);
+ const typesIncludePackage = hasTypes && types!.includes(packageType);
+ const noSearchTerm = !term;
+ const includeAllPackages = typesIncludePackage && noSearchTerm;
+
+ if (!includeAllPackages && noSearchTerm) {
+ return of([]);
+ }
+
+ if (term) {
+ term = term.toLowerCase();
+ }
const toSearchResults = (
coreStart: CoreStart,
@@ -65,25 +96,17 @@ export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResult
): GlobalSearchProviderResult[] => {
const packages = packagesResponse.slice(0, maxResults);
- return packages.flatMap((pkg) => {
- if (!term || !pkg.title.toLowerCase().includes(term)) {
- return [];
- }
- const pkgkey = `${pkg.name}-${pkg.version}`;
- return {
- id: pkgkey,
- type: packageType,
- title: pkg.title,
- score: 80,
- url: {
- // TODO: See https://github.com/elastic/kibana/issues/96134 for details about why we use '#' here. Below should be updated
- // as part of migrating to non-hash based router.
- // prettier-ignore
- path: `${coreStart.application.getUrlForApp(INTEGRATIONS_PLUGIN_ID)}#${pagePathGetters.integration_details_overview({ pkgkey })[1]}`,
- prependBasePath: false,
- },
- };
- });
+ return packages.flatMap(
+ includeAllPackages
+ ? (pkg) => toSearchResult(pkg, coreStart.application)
+ : (pkg) => {
+ if (!term || !pkg.title.toLowerCase().includes(term)) {
+ return [];
+ }
+
+ return toSearchResult(pkg, coreStart.application);
+ }
+ );
};
return combineLatest([coreStart$, getPackages$()]).pipe(
From 1d59abc60562cc5a7e105a0e3b696a289f31a0ca Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Tue, 22 Jun 2021 14:19:19 +0200
Subject: [PATCH 056/163] [Lens] Carry over filter extra fields with in
incomplete state (#102509)
* :bug: Carry over filter extra fields with in incomplete state
* :ok_hand: Integrated feedback
* :ok_hand: Integrated feedback
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../dimension_panel/dimension_editor.tsx | 8 ++-
.../operations/layer_helpers.test.ts | 40 ++++++++++++
.../operations/layer_helpers.ts | 63 ++++++++++++++++---
3 files changed, 100 insertions(+), 11 deletions(-)
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
index 74b47b55fdd0e..dca8e926646f0 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
@@ -141,8 +141,11 @@ export function DimensionEditor(props: DimensionEditorProps) {
};
const incompleteInfo = (state.layers[layerId].incompleteColumns ?? {})[columnId];
- const incompleteOperation = incompleteInfo?.operationType;
- const incompleteField = incompleteInfo?.sourceField ?? null;
+ const {
+ operationType: incompleteOperation,
+ sourceField: incompleteField = null,
+ ...incompleteParams
+ } = incompleteInfo || {};
const ParamEditor = selectedOperationDefinition?.paramEditor;
@@ -486,6 +489,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
field: currentIndexPattern.getFieldByName(choice.field),
visualizationGroups: dimensionGroups,
targetGroup: props.groupId,
+ incompleteParams,
})
);
}}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
index 34765217da664..387a61ff79264 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
@@ -583,6 +583,46 @@ describe('state_helpers', () => {
).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] }));
});
+ it('should inherit filters from the incomplete column when passed', () => {
+ expect(
+ insertNewColumn({
+ layer: {
+ indexPatternId: '1',
+ columnOrder: ['col1'],
+ columns: {
+ col1: {
+ label: 'Date histogram of timestamp',
+ dataType: 'date',
+ isBucketed: true,
+
+ // Private
+ operationType: 'date_histogram',
+ sourceField: 'timestamp',
+ params: {
+ interval: 'h',
+ },
+ },
+ },
+ },
+ columnId: 'col2',
+ indexPattern,
+ op: 'average',
+ field: indexPattern.fields[2],
+ visualizationGroups: [],
+ incompleteParams: { filter: { language: 'kuery', query: '' }, timeShift: '3d' },
+ })
+ ).toEqual(
+ expect.objectContaining({
+ columns: expect.objectContaining({
+ col2: expect.objectContaining({
+ filter: { language: 'kuery', query: '' },
+ timeShift: '3d',
+ }),
+ }),
+ })
+ );
+ });
+
describe('inserting a new reference', () => {
it('should throw if the required references are impossible to match', () => {
// @ts-expect-error this function is not valid
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
index f0095b66e2ba6..fd3df9f97cecf 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
@@ -7,6 +7,7 @@
import { partition, mapValues, pickBy } from 'lodash';
import { CoreStart } from 'kibana/public';
+import { Query } from 'src/plugins/data/common';
import type {
FramePublicAPI,
OperationMetadata,
@@ -18,6 +19,7 @@ import {
OperationType,
IndexPatternColumn,
RequiredReference,
+ GenericOperationDefinition,
} from './definitions';
import type {
IndexPattern,
@@ -29,6 +31,13 @@ import { getSortScoreByPriority } from './operations';
import { generateId } from '../../id_generator';
import { ReferenceBasedIndexPatternColumn } from './definitions/column_types';
import { FormulaIndexPatternColumn, regenerateLayerFromAst } from './definitions/formula';
+import { TimeScaleUnit } from '../time_scale';
+
+interface ColumnAdvancedParams {
+ filter?: Query | undefined;
+ timeShift?: string | undefined;
+ timeScale?: TimeScaleUnit | undefined;
+}
interface ColumnChange {
op: OperationType;
@@ -39,6 +48,7 @@ interface ColumnChange {
visualizationGroups: VisualizationDimensionGroupConfig[];
targetGroup?: string;
shouldResetLabel?: boolean;
+ incompleteParams?: ColumnAdvancedParams;
}
interface ColumnCopy {
@@ -141,6 +151,24 @@ export function insertOrReplaceColumn(args: ColumnChange): IndexPatternLayer {
return insertNewColumn(args);
}
+function ensureCompatibleParamsAreMoved(
+ column: T,
+ referencedOperation: GenericOperationDefinition,
+ previousColumn: ColumnAdvancedParams
+) {
+ const newColumn = { ...column };
+ if (referencedOperation.filterable) {
+ newColumn.filter = (previousColumn as ReferenceBasedIndexPatternColumn).filter;
+ }
+ if (referencedOperation.shiftable) {
+ newColumn.timeShift = (previousColumn as ReferenceBasedIndexPatternColumn).timeShift;
+ }
+ if (referencedOperation.timeScalingMode !== 'disabled') {
+ newColumn.timeScale = (previousColumn as ReferenceBasedIndexPatternColumn).timeScale;
+ }
+ return newColumn;
+}
+
// Insert a column into an empty ID. The field parameter is required when constructing
// a field-based operation, but will cause the function to fail for any other type of operation.
export function insertNewColumn({
@@ -152,6 +180,7 @@ export function insertNewColumn({
visualizationGroups,
targetGroup,
shouldResetLabel,
+ incompleteParams,
}: ColumnChange): IndexPatternLayer {
const operationDefinition = operationDefinitionMap[op];
@@ -163,7 +192,10 @@ export function insertNewColumn({
throw new Error(`Can't insert a column with an ID that is already in use`);
}
- const baseOptions = { indexPattern, previousColumn: layer.columns[columnId] };
+ const baseOptions = {
+ indexPattern,
+ previousColumn: { ...incompleteParams, ...layer.columns[columnId] },
+ };
if (operationDefinition.input === 'none' || operationDefinition.input === 'managedReference') {
if (field) {
@@ -414,15 +446,13 @@ export function replaceColumn({
indexPattern,
});
- const column = copyCustomLabel({ ...referenceColumn }, previousColumn);
// do not forget to move over also any filter/shift/anything (if compatible)
// from the reference definition to the new operation.
- if (referencedOperation.filterable) {
- column.filter = (previousColumn as ReferenceBasedIndexPatternColumn).filter;
- }
- if (referencedOperation.shiftable) {
- column.timeShift = (previousColumn as ReferenceBasedIndexPatternColumn).timeShift;
- }
+ const column = ensureCompatibleParamsAreMoved(
+ copyCustomLabel({ ...referenceColumn }, previousColumn),
+ referencedOperation,
+ previousColumn as ReferenceBasedIndexPatternColumn
+ );
tempLayer = {
...tempLayer,
@@ -529,15 +559,30 @@ export function replaceColumn({
}
if (!field) {
+ let incompleteColumn: {
+ operationType: OperationType;
+ } & ColumnAdvancedParams = { operationType: op };
// if no field is available perform a full clean of the column from the layer
if (previousDefinition.input === 'fullReference') {
tempLayer = deleteColumn({ layer: tempLayer, columnId, indexPattern });
+ const previousReferenceId = (previousColumn as ReferenceBasedIndexPatternColumn)
+ .references[0];
+ const referenceColumn = layer.columns[previousReferenceId];
+ if (referenceColumn) {
+ const referencedOperation = operationDefinitionMap[referenceColumn.operationType];
+
+ incompleteColumn = ensureCompatibleParamsAreMoved(
+ incompleteColumn,
+ referencedOperation,
+ previousColumn
+ );
+ }
}
return {
...tempLayer,
incompleteColumns: {
...(tempLayer.incompleteColumns ?? {}),
- [columnId]: { operationType: op },
+ [columnId]: incompleteColumn,
},
};
}
From 1036a5bbd701616f8a1df838adff9fbc529e868e Mon Sep 17 00:00:00 2001
From: Tim Roes
Date: Tue, 22 Jun 2021 14:44:28 +0200
Subject: [PATCH 057/163] Improve Discover sidebar performance (#102646)
* Improve Discover sidebar performance
* Remove unused import
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/sidebar/discover_field.tsx | 436 +++++++++---------
.../sidebar/discover_field_details.tsx | 11 -
.../components/sidebar/discover_sidebar.tsx | 11 +-
.../public/field_button/field_button.scss | 1 +
4 files changed, 236 insertions(+), 223 deletions(-)
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx
index e60dabd1d8d8c..26a3c482e9d3c 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx
@@ -8,7 +8,7 @@
import './discover_field.scss';
-import React, { useState } from 'react';
+import React, { useState, useCallback, memo } from 'react';
import {
EuiPopover,
EuiPopoverTitle,
@@ -29,6 +29,172 @@ import { IndexPatternField, IndexPattern } from '../../../../../../../data/publi
import { getFieldTypeName } from './lib/get_field_type_name';
import { DiscoverFieldDetailsFooter } from './discover_field_details_footer';
+function wrapOnDot(str?: string) {
+ // u200B is a non-width white-space character, which allows
+ // the browser to efficiently word-wrap right after the dot
+ // without us having to draw a lot of extra DOM elements, etc
+ return str ? str.replace(/\./g, '.\u200B') : '';
+}
+
+const FieldInfoIcon: React.FC = memo(() => (
+
+
+
+));
+
+const DiscoverFieldTypeIcon: React.FC<{ field: IndexPatternField }> = memo(({ field }) => (
+
+));
+
+const FieldName: React.FC<{ field: IndexPatternField }> = memo(({ field }) => {
+ const title =
+ field.displayName !== field.name
+ ? i18n.translate('discover.field.title', {
+ defaultMessage: '{fieldName} ({fieldDisplayName})',
+ values: {
+ fieldName: field.name,
+ fieldDisplayName: field.displayName,
+ },
+ })
+ : field.displayName;
+
+ return (
+
+ {wrapOnDot(field.displayName)}
+
+ );
+});
+
+interface ActionButtonProps {
+ field: IndexPatternField;
+ isSelected?: boolean;
+ alwaysShow: boolean;
+ toggleDisplay: (field: IndexPatternField) => void;
+}
+
+const ActionButton: React.FC = memo(
+ ({ field, isSelected, alwaysShow, toggleDisplay }) => {
+ const actionBtnClassName = classNames('dscSidebarItem__action', {
+ ['dscSidebarItem__mobile']: alwaysShow,
+ });
+ if (field.name === '_source') {
+ return null;
+ }
+ if (!isSelected) {
+ return (
+
+ ) => {
+ if (ev.type === 'click') {
+ ev.currentTarget.focus();
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ toggleDisplay(field);
+ }}
+ data-test-subj={`fieldToggle-${field.name}`}
+ aria-label={i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', {
+ defaultMessage: 'Add {field} to table',
+ values: { field: field.name },
+ })}
+ />
+
+ );
+ } else {
+ return (
+
+ ) => {
+ if (ev.type === 'click') {
+ ev.currentTarget.focus();
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ toggleDisplay(field);
+ }}
+ data-test-subj={`fieldToggle-${field.name}`}
+ aria-label={i18n.translate(
+ 'discover.fieldChooser.discoverField.removeButtonAriaLabel',
+ {
+ defaultMessage: 'Remove {field} from table',
+ values: { field: field.name },
+ }
+ )}
+ />
+
+ );
+ }
+ }
+);
+
+interface MultiFieldsProps {
+ multiFields: NonNullable;
+ toggleDisplay: (field: IndexPatternField) => void;
+ alwaysShowActionButton: boolean;
+}
+
+const MultiFields: React.FC = memo(
+ ({ multiFields, toggleDisplay, alwaysShowActionButton }) => (
+
+
+
+ {i18n.translate('discover.fieldChooser.discoverField.multiFields', {
+ defaultMessage: 'Multi fields',
+ })}
+
+
+ {multiFields.map((entry) => (
+ }
+ fieldAction={
+
+ }
+ fieldName={ }
+ key={entry.field.name}
+ />
+ ))}
+
+ )
+);
+
export interface DiscoverFieldProps {
/**
* Determines whether add/remove button is displayed not only when focused
@@ -85,7 +251,7 @@ export interface DiscoverFieldProps {
onDeleteField?: (fieldName: string) => void;
}
-export function DiscoverField({
+function DiscoverFieldComponent({
alwaysShowActionButton = false,
field,
indexPattern,
@@ -99,133 +265,22 @@ export function DiscoverField({
onEditField,
onDeleteField,
}: DiscoverFieldProps) {
- const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', {
- defaultMessage: 'Add {field} to table',
- values: { field: field.name },
- });
- const removeLabelAria = i18n.translate(
- 'discover.fieldChooser.discoverField.removeButtonAriaLabel',
- {
- defaultMessage: 'Remove {field} from table',
- values: { field: field.name },
- }
- );
-
const [infoIsOpen, setOpen] = useState(false);
- const toggleDisplay = (f: IndexPatternField, isSelected: boolean) => {
- if (isSelected) {
- onRemoveField(f.name);
- } else {
- onAddField(f.name);
- }
- };
+ const toggleDisplay = useCallback(
+ (f: IndexPatternField) => {
+ if (selected) {
+ onRemoveField(f.name);
+ } else {
+ onAddField(f.name);
+ }
+ },
+ [onAddField, onRemoveField, selected]
+ );
- function togglePopover() {
+ const togglePopover = useCallback(() => {
setOpen(!infoIsOpen);
- }
-
- function wrapOnDot(str?: string) {
- // u200B is a non-width white-space character, which allows
- // the browser to efficiently word-wrap right after the dot
- // without us having to draw a lot of extra DOM elements, etc
- return str ? str.replace(/\./g, '.\u200B') : '';
- }
-
- const getDscFieldIcon = (indexPatternField: IndexPatternField) => {
- return (
-
- );
- };
-
- const dscFieldIcon = getDscFieldIcon(field);
-
- const getTitle = (indexPatternField: IndexPatternField) => {
- return indexPatternField.displayName !== indexPatternField.name
- ? i18n.translate('discover.field.title', {
- defaultMessage: '{fieldName} ({fieldDisplayName})',
- values: {
- fieldName: indexPatternField.name,
- fieldDisplayName: indexPatternField.displayName,
- },
- })
- : indexPatternField.displayName;
- };
-
- const getFieldName = (indexPatternField: IndexPatternField) => {
- return (
-
- {wrapOnDot(indexPatternField.displayName)}
-
- );
- };
- const fieldName = getFieldName(field);
-
- const actionBtnClassName = classNames('dscSidebarItem__action', {
- ['dscSidebarItem__mobile']: alwaysShowActionButton,
- });
- const getActionButton = (f: IndexPatternField, isSelected?: boolean) => {
- if (f.name !== '_source' && !isSelected) {
- return (
-
- ) => {
- if (ev.type === 'click') {
- ev.currentTarget.focus();
- }
- ev.preventDefault();
- ev.stopPropagation();
- toggleDisplay(f, false);
- }}
- data-test-subj={`fieldToggle-${f.name}`}
- aria-label={addLabelAria}
- />
-
- );
- } else if (f.name !== '_source' && isSelected) {
- return (
-
- ) => {
- if (ev.type === 'click') {
- ev.currentTarget.focus();
- }
- ev.preventDefault();
- ev.stopPropagation();
- toggleDisplay(f, isSelected);
- }}
- data-test-subj={`fieldToggle-${f.name}`}
- aria-label={removeLabelAria}
- />
-
- );
- }
- };
-
- const actionButton = getActionButton(field, selected);
+ }, [infoIsOpen]);
if (field.type === '_source') {
return (
@@ -233,71 +288,20 @@ export function DiscoverField({
size="s"
className="dscSidebarItem"
dataTestSubj={`field-${field.name}-showDetails`}
- fieldIcon={dscFieldIcon}
- fieldAction={actionButton}
- fieldName={fieldName}
+ fieldIcon={ }
+ fieldAction={
+
+ }
+ fieldName={ }
/>
);
}
- const getFieldInfoIcon = () => {
- if (field.type !== 'conflict') {
- return null;
- }
- return (
-
-
-
- );
- };
-
- const fieldInfoIcon = getFieldInfoIcon();
-
- const shouldRenderMultiFields = !!multiFields;
- const renderMultiFields = () => {
- if (!multiFields) {
- return null;
- }
- return (
-
-
-
- {i18n.translate('discover.fieldChooser.discoverField.multiFields', {
- defaultMessage: 'Multi fields',
- })}
-
-
- {multiFields.map((entry) => (
- {}}
- dataTestSubj={`field-${entry.field.name}-showDetails`}
- fieldIcon={getDscFieldIcon(entry.field)}
- fieldAction={getActionButton(entry.field, entry.isSelected)}
- fieldName={getFieldName(entry.field)}
- key={entry.field.name}
- />
- ))}
-
- );
- };
-
const isRuntimeField = Boolean(indexPattern.getFieldByName(field.name)?.runtimeField);
const isUnknownField = field.type === 'unknown' || field.type === 'unknown_selected';
const canEditField = onEditField && (!isUnknownField || isRuntimeField);
@@ -334,9 +338,7 @@ export function DiscoverField({
>
{
- if (onDeleteField) {
- onDeleteField(field.name);
- }
+ onDeleteField?.(field.name);
}}
iconType="trash"
data-test-subj={`discoverFieldListPanelDelete-${field.name}`}
@@ -352,6 +354,8 @@ export function DiscoverField({
);
+ const details = getDetails(field);
+
return (
{
- togglePopover();
- }}
+ onClick={togglePopover}
dataTestSubj={`field-${field.name}-showDetails`}
- fieldIcon={dscFieldIcon}
- fieldAction={actionButton}
- fieldName={fieldName}
- fieldInfoIcon={fieldInfoIcon}
+ fieldIcon={ }
+ fieldAction={
+
+ }
+ fieldName={ }
+ fieldInfoIcon={field.type === 'conflict' && }
/>
}
isOpen={infoIsOpen}
@@ -384,26 +393,33 @@ export function DiscoverField({
{infoIsOpen && (
-
- )}
- {shouldRenderMultiFields ? (
<>
- {renderMultiFields()}
-
+ {multiFields && (
+
+ )}
+ {!details.error && (
+
+ )}
>
- ) : null}
+ )}
);
}
+
+export const DiscoverField = memo(DiscoverFieldComponent);
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx
index d7008ba3e310f..ffa7b30de5280 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx
@@ -20,7 +20,6 @@ import {
import { Bucket, FieldDetails } from './types';
import { IndexPatternField, IndexPattern } from '../../../../../../../data/public';
import './discover_field_details.scss';
-import { DiscoverFieldDetailsFooter } from './discover_field_details_footer';
interface DiscoverFieldDetailsProps {
field: IndexPatternField;
@@ -28,7 +27,6 @@ interface DiscoverFieldDetailsProps {
details: FieldDetails;
onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
- showFooter?: boolean;
}
export function DiscoverFieldDetails({
@@ -37,7 +35,6 @@ export function DiscoverFieldDetails({
details,
onAddFilter,
trackUiMetric,
- showFooter = true,
}: DiscoverFieldDetailsProps) {
const warnings = getWarnings(field);
const [showVisualizeLink, setShowVisualizeLink] = useState(false);
@@ -111,14 +108,6 @@ export function DiscoverFieldDetails({
>
)}
- {!details.error && showFooter && (
-
- )}
>
);
}
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx
index 0bebec61657b4..7fbbf6fd3ffdc 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx
@@ -21,6 +21,7 @@ import {
EuiPageSideBar,
useResizeObserver,
} from '@elastic/eui';
+import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect';
import { isEqual, sortBy } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -205,7 +206,7 @@ export function DiscoverSidebar({
return result;
}, [fields]);
- const multiFields = useMemo(() => {
+ const calculateMultiFields = () => {
if (!useNewFieldsApi || !fields) {
return undefined;
}
@@ -224,7 +225,13 @@ export function DiscoverSidebar({
map.set(parent, value);
});
return map;
- }, [fields, useNewFieldsApi, selectedFields]);
+ };
+
+ const [multiFields, setMultiFields] = useState(() => calculateMultiFields());
+
+ useShallowCompareEffect(() => {
+ setMultiFields(calculateMultiFields());
+ }, [fields, selectedFields, useNewFieldsApi]);
const deleteField = useMemo(
() =>
diff --git a/src/plugins/kibana_react/public/field_button/field_button.scss b/src/plugins/kibana_react/public/field_button/field_button.scss
index 43f60e4503576..f71e097ab7138 100644
--- a/src/plugins/kibana_react/public/field_button/field_button.scss
+++ b/src/plugins/kibana_react/public/field_button/field_button.scss
@@ -38,6 +38,7 @@
padding: $euiSizeS;
display: flex;
align-items: flex-start;
+ line-height: normal;
}
.kbnFieldButton__fieldIcon {
From dc9daed8c29d0acd61a8f6f72b22571ec0b2f40b Mon Sep 17 00:00:00 2001
From: Thomas Neirynck
Date: Tue, 22 Jun 2021 15:10:53 +0200
Subject: [PATCH 058/163] [Maps] bump ems client to 7.14 (#102770)
---
package.json | 2 +-
src/dev/license_checker/config.ts | 2 ++
yarn.lock | 8 ++++----
3 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/package.json b/package.json
index 29371c9532915..114a9ac98df72 100644
--- a/package.json
+++ b/package.json
@@ -102,7 +102,7 @@
"@elastic/charts": "30.1.0",
"@elastic/datemath": "link:bazel-bin/packages/elastic-datemath",
"@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13",
- "@elastic/ems-client": "7.13.0",
+ "@elastic/ems-client": "7.14.0",
"@elastic/eui": "33.0.0",
"@elastic/filesaver": "1.1.2",
"@elastic/good": "^9.0.1-kibana3",
diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts
index ebf56166a8922..b3b7bf5e8eed7 100644
--- a/src/dev/license_checker/config.ts
+++ b/src/dev/license_checker/config.ts
@@ -10,6 +10,7 @@
// used as dependencies or dev dependencies
export const LICENSE_ALLOWED = [
'Elastic-License',
+ 'Elastic License 2.0',
'SSPL-1.0 OR Elastic License 2.0',
'0BSD',
'(BSD-2-Clause OR MIT OR Apache-2.0)',
@@ -72,6 +73,7 @@ export const DEV_ONLY_LICENSE_ALLOWED = ['MPL-2.0'];
export const LICENSE_OVERRIDES = {
'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts
'@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint
+ '@elastic/ems-client@7.14.0': ['Elastic License 2.0'],
// TODO can be removed if the https://github.com/jindw/xmldom/issues/239 is released
'xmldom@0.1.27': ['MIT'],
diff --git a/yarn.lock b/yarn.lock
index cfdac6108b6cf..bcb5e607a44ee 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1413,10 +1413,10 @@
ms "^2.1.3"
secure-json-parse "^2.4.0"
-"@elastic/ems-client@7.13.0":
- version "7.13.0"
- resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.13.0.tgz#de291a6eb25523e5844a9e74ae72fd2e81a1f4d9"
- integrity sha512-VdK5jZdnC+5BSkMRQsqHqrsZ9HttnPjQmCjRlAGuV8y6g0eKVP9ZiMRQFKFKmuSKpx0kHGsSV/1kBglTmSl/3g==
+"@elastic/ems-client@7.14.0":
+ version "7.14.0"
+ resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.14.0.tgz#7c8095086bd9a637f72d6d810d494a460c68e0fc"
+ integrity sha512-axXTyBrC1I2TMmcxGC04SgODwb5Cp6svcW64RoTr8X2XrSSuH0gh+X5qMsC9FgGGnmbVNCEYIs3JK4AJ7X4bxA==
dependencies:
"@types/geojson" "^7946.0.7"
"@types/lru-cache" "^5.1.0"
From 01ce7ac6e1698ffdcd898dc4e726e3b552291d5d Mon Sep 17 00:00:00 2001
From: Sergi Massaneda
Date: Tue, 22 Jun 2021 15:14:22 +0200
Subject: [PATCH 059/163] [core][deepLinks] Fix getAppInfo deepLinks order
(#102879)
* getAppInfo sets deepLinks order properly
* remove overriding same spread fields
---
src/core/public/application/utils/get_app_info.test.ts | 6 ++++++
src/core/public/application/utils/get_app_info.ts | 4 +---
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/src/core/public/application/utils/get_app_info.test.ts b/src/core/public/application/utils/get_app_info.test.ts
index fa1e2dd9a4537..25614d1d1dca9 100644
--- a/src/core/public/application/utils/get_app_info.test.ts
+++ b/src/core/public/application/utils/get_app_info.test.ts
@@ -185,15 +185,18 @@ describe('getAppInfo', () => {
it('adds default deepLinks when needed', () => {
const app = createApp({
+ order: 3,
deepLinks: [
{
id: 'sub-id',
title: 'sub-title',
+ order: 2,
deepLinks: [
{
id: 'sub-sub-id',
title: 'sub-sub-title',
path: '/sub-sub',
+ order: 1,
keywords: ['sub sub'],
},
],
@@ -210,12 +213,14 @@ describe('getAppInfo', () => {
searchable: true,
appRoute: `/app/some-id`,
keywords: [],
+ order: 3,
deepLinks: [
{
id: 'sub-id',
title: 'sub-title',
navLinkStatus: AppNavLinkStatus.hidden,
searchable: true,
+ order: 2,
keywords: [],
deepLinks: [
{
@@ -223,6 +228,7 @@ describe('getAppInfo', () => {
title: 'sub-sub-title',
navLinkStatus: AppNavLinkStatus.hidden,
searchable: true,
+ order: 1,
path: '/sub-sub',
keywords: ['sub sub'],
deepLinks: [],
diff --git a/src/core/public/application/utils/get_app_info.ts b/src/core/public/application/utils/get_app_info.ts
index 6c753b7a71a0f..b5a3f0b0a0f13 100644
--- a/src/core/public/application/utils/get_app_info.ts
+++ b/src/core/public/application/utils/get_app_info.ts
@@ -41,9 +41,7 @@ function getDeepLinkInfos(deepLinks?: AppDeepLink[]): PublicAppDeepLinkInfo[] {
return deepLinks.map(
({ navLinkStatus = AppNavLinkStatus.default, ...rawDeepLink }): PublicAppDeepLinkInfo => {
return {
- id: rawDeepLink.id,
- title: rawDeepLink.title,
- path: rawDeepLink.path,
+ ...rawDeepLink,
keywords: rawDeepLink.keywords ?? [],
navLinkStatus:
navLinkStatus === AppNavLinkStatus.default ? AppNavLinkStatus.hidden : navLinkStatus,
From 06900307169ecd99d1a13c9cab0cf19415248f02 Mon Sep 17 00:00:00 2001
From: Robert Oskamp
Date: Tue, 22 Jun 2021 15:37:08 +0200
Subject: [PATCH 060/163] [ML] Functional tests - explicitly delete jobs after
setupModule tests (#102882)
This PR explicitly deletes the jobs created by the `setupModule` tests.
---
x-pack/test/api_integration/apis/ml/modules/setup_module.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
index 6011c38255cdc..c4dd529ac14f5 100644
--- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
+++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
@@ -1048,6 +1048,9 @@ export default ({ getService }: FtrProviderContext) => {
for (const dashboard of testData.expected.dashboards) {
await ml.testResources.deleteDashboardById(dashboard);
}
+ for (const job of testData.expected.jobs) {
+ await ml.api.deleteAnomalyDetectionJobES(job.jobId);
+ }
await ml.api.cleanMlIndices();
});
From 564807c0b05b32baf81c57b07bfc5e0026fe134f Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Tue, 22 Jun 2021 15:51:37 +0200
Subject: [PATCH 061/163] increase chart switch width (#102520)
---
.../editor_frame/workspace_panel/chart_switch.scss | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss
index 3fafa8b37a42f..a4e22b4ef558c 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.scss
@@ -22,5 +22,5 @@ img.lnsChartSwitch__chartIcon { // stylelint-disable-line selector-no-qualifying
}
.lnsChartSwitch__search {
- width: 7 * $euiSizeXXL;
+ width: 10 * $euiSizeXXL;
}
From 34490a355e9f498a7001c54002aed83610e1807b Mon Sep 17 00:00:00 2001
From: Dominique Clarke
Date: Tue, 22 Jun 2021 10:14:56 -0400
Subject: [PATCH 062/163] [Uptime] [Synthetics Integration] transition to
monaco code editor (#102642)
* update synthetics integration code editor
* add basic support for xml and javascript
* fix types
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
packages/kbn-monaco/src/monaco_imports.ts | 4 +-
.../components/fleet_package/code_editor.tsx | 45 ++++++++++
.../contexts/advanced_fields_http_context.tsx | 2 +-
.../fleet_package/header_field.test.tsx | 4 +-
.../components/fleet_package/header_field.tsx | 2 +-
.../fleet_package/request_body_field.test.tsx | 4 +-
.../fleet_package/request_body_field.tsx | 83 ++++---------------
.../public/components/fleet_package/types.tsx | 13 ++-
.../apps/uptime/synthetics_integration.ts | 4 +-
9 files changed, 84 insertions(+), 77 deletions(-)
create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/code_editor.tsx
diff --git a/packages/kbn-monaco/src/monaco_imports.ts b/packages/kbn-monaco/src/monaco_imports.ts
index 92ea23347c374..3f689e6ec0c01 100644
--- a/packages/kbn-monaco/src/monaco_imports.ts
+++ b/packages/kbn-monaco/src/monaco_imports.ts
@@ -7,7 +7,6 @@
*/
/* eslint-disable @kbn/eslint/module_migration */
-
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import 'monaco-editor/esm/vs/base/common/worker/simpleWorker';
@@ -23,4 +22,7 @@ import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js'; // Needed for hover
import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js'; // Needed for signature
import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching.js'; // Needed for brackets matching highlight
+import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js'; // Needed for basic javascript support
+import 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution.js'; // Needed for basic xml support
+
export { monaco };
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/code_editor.tsx b/x-pack/plugins/uptime/public/components/fleet_package/code_editor.tsx
new file mode 100644
index 0000000000000..d2fe3f9b30e84
--- /dev/null
+++ b/x-pack/plugins/uptime/public/components/fleet_package/code_editor.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 styled from 'styled-components';
+
+import { EuiPanel } from '@elastic/eui';
+import { CodeEditor as MonacoCodeEditor } from '../../../../../../src/plugins/kibana_react/public';
+
+import { MonacoEditorLangId } from './types';
+
+const CodeEditorContainer = styled(EuiPanel)`
+ padding: 0;
+`;
+
+interface Props {
+ ariaLabel: string;
+ id: string;
+ languageId: MonacoEditorLangId;
+ onChange: (value: string) => void;
+ value: string;
+}
+
+export const CodeEditor = ({ ariaLabel, id, languageId, onChange, value }: Props) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx
index c257a8f71b77a..b51aa6cbf3a7c 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx
@@ -36,7 +36,7 @@ export const initialValues = {
[ConfigKeys.RESPONSE_STATUS_CHECK]: [],
[ConfigKeys.REQUEST_BODY_CHECK]: {
value: '',
- type: Mode.TEXT,
+ type: Mode.PLAINTEXT,
},
[ConfigKeys.REQUEST_HEADERS_CHECK]: {},
[ConfigKeys.REQUEST_METHOD_CHECK]: HTTPMethod.GET,
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/header_field.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/header_field.test.tsx
index ee33083b3eae9..6d9e578fe53f5 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/header_field.test.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/header_field.test.tsx
@@ -76,14 +76,14 @@ describe(' ', () => {
});
it('handles content mode', async () => {
- const contentMode: Mode = Mode.TEXT;
+ const contentMode: Mode = Mode.PLAINTEXT;
render(
);
await waitFor(() => {
expect(onChange).toBeCalledWith({
- 'Content-Type': contentTypes[Mode.TEXT],
+ 'Content-Type': contentTypes[Mode.PLAINTEXT],
});
});
});
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/header_field.tsx b/x-pack/plugins/uptime/public/components/fleet_package/header_field.tsx
index 9f337d4b00704..eaf9be50e9665 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/header_field.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/header_field.tsx
@@ -61,7 +61,7 @@ export const HeaderField = ({ contentMode, defaultValue, onChange }: Props) => {
export const contentTypes: Record = {
[Mode.JSON]: ContentType.JSON,
- [Mode.TEXT]: ContentType.TEXT,
+ [Mode.PLAINTEXT]: ContentType.TEXT,
[Mode.XML]: ContentType.XML,
[Mode.FORM]: ContentType.FORM,
};
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.test.tsx
index 849809eae52a4..fa666ac764ac7 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.test.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.test.tsx
@@ -5,6 +5,8 @@
* 2.0.
*/
+import 'jest-canvas-mock';
+
import React, { useState, useCallback } from 'react';
import { fireEvent, waitFor } from '@testing-library/react';
import { render } from '../../lib/helper/rtl_helpers';
@@ -16,7 +18,7 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
}));
describe(' ', () => {
- const defaultMode = Mode.TEXT;
+ const defaultMode = Mode.PLAINTEXT;
const defaultValue = 'sample value';
const WrappedComponent = () => {
const [config, setConfig] = useState({
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.tsx b/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.tsx
index 1ef8fdd75e7f3..1fdde7c2b63fc 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/request_body_field.tsx
@@ -5,67 +5,13 @@
* 2.0.
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { stringify, parse } from 'query-string';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { stringify, parse } from 'query-string';
-
-import styled from 'styled-components';
-
-import { EuiCodeEditor, EuiPanel, EuiTabbedContent } from '@elastic/eui';
-
-import { Mode } from './types';
-
+import { EuiTabbedContent } from '@elastic/eui';
+import { Mode, MonacoEditorLangId } from './types';
import { KeyValuePairsField, Pair } from './key_value_field';
-
-import 'brace/theme/github';
-import 'brace/mode/xml';
-import 'brace/mode/json';
-import 'brace/ext/language_tools';
-
-const CodeEditorContainer = styled(EuiPanel)`
- padding: 0;
-`;
-
-enum ResponseBodyType {
- CODE = 'code',
- FORM = 'form',
-}
-
-const CodeEditor = ({
- ariaLabel,
- id,
- mode,
- onChange,
- value,
-}: {
- ariaLabel: string;
- id: string;
- mode: Mode;
- onChange: (value: string) => void;
- value: string;
-}) => {
- return (
-
-
-
-
-
- );
-};
+import { CodeEditor } from './code_editor';
interface Props {
onChange: (requestBody: { type: Mode; value: string }) => void;
@@ -73,6 +19,11 @@ interface Props {
value: string;
}
+enum ResponseBodyType {
+ CODE = 'code',
+ FORM = 'form',
+}
+
// TO DO: Look into whether or not code editor reports errors, in order to prevent form submission on an error
export const RequestBodyField = ({ onChange, type, value }: Props) => {
const [values, setValues] = useState>({
@@ -129,9 +80,9 @@ export const RequestBodyField = ({ onChange, type, value }: Props) => {
const tabs = [
{
- id: Mode.TEXT,
- name: modeLabels[Mode.TEXT],
- 'data-test-subj': `syntheticsRequestBodyTab__${Mode.TEXT}`,
+ id: Mode.PLAINTEXT,
+ name: modeLabels[Mode.PLAINTEXT],
+ 'data-test-subj': `syntheticsRequestBodyTab__${Mode.PLAINTEXT}`,
content: (
{
defaultMessage: 'Text code editor',
}
)}
- id={Mode.TEXT}
- mode={Mode.TEXT}
+ id={Mode.PLAINTEXT}
+ languageId={MonacoEditorLangId.PLAINTEXT}
onChange={(code) =>
setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code }))
}
@@ -162,7 +113,7 @@ export const RequestBodyField = ({ onChange, type, value }: Props) => {
}
)}
id={Mode.JSON}
- mode={Mode.JSON}
+ languageId={MonacoEditorLangId.JSON}
onChange={(code) =>
setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code }))
}
@@ -183,7 +134,7 @@ export const RequestBodyField = ({ onChange, type, value }: Props) => {
}
)}
id={Mode.XML}
- mode={Mode.XML}
+ languageId={MonacoEditorLangId.XML}
onChange={(code) =>
setValues((prevValues) => ({ ...prevValues, [ResponseBodyType.CODE]: code }))
}
@@ -229,7 +180,7 @@ const modeLabels = {
defaultMessage: 'Form',
}
),
- [Mode.TEXT]: i18n.translate(
+ [Mode.PLAINTEXT]: i18n.translate(
'xpack.uptime.createPackagePolicy.stepConfigure.requestBodyType.text',
{
defaultMessage: 'Text',
diff --git a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx
index 4d44b4f074e82..7a16d1352c40a 100644
--- a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx
+++ b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx
@@ -25,10 +25,17 @@ export enum ResponseBodyIndexPolicy {
ON_ERROR = 'on_error',
}
+export enum MonacoEditorLangId {
+ JSON = 'xjson',
+ PLAINTEXT = 'plaintext',
+ XML = 'xml',
+ JAVASCRIPT = 'javascript',
+}
+
export enum Mode {
FORM = 'form',
JSON = 'json',
- TEXT = 'text',
+ PLAINTEXT = 'text',
XML = 'xml',
}
@@ -192,11 +199,11 @@ export interface PolicyConfig {
[DataStream.ICMP]: ICMPFields;
}
-export type Validation = Partial void>>;
+export type Validation = Partial boolean>>;
export const contentTypesToMode = {
[ContentType.FORM]: Mode.FORM,
[ContentType.JSON]: Mode.JSON,
- [ContentType.TEXT]: Mode.TEXT,
+ [ContentType.TEXT]: Mode.PLAINTEXT,
[ContentType.XML]: Mode.XML,
};
diff --git a/x-pack/test/functional/apps/uptime/synthetics_integration.ts b/x-pack/test/functional/apps/uptime/synthetics_integration.ts
index 0872abfcaa4f8..a4740de8e9a2b 100644
--- a/x-pack/test/functional/apps/uptime/synthetics_integration.ts
+++ b/x-pack/test/functional/apps/uptime/synthetics_integration.ts
@@ -277,7 +277,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
},
requestBody: {
type: 'xml',
- value: 'samplexml',
+ value: 'samplexml',
},
indexResponseBody: false,
indexResponseHeaders: false,
@@ -308,7 +308,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
},
'check.response.headers': advancedConfig.responseHeaders,
'check.response.status': [advancedConfig.responseStatusCheck],
- 'check.request.body': `${advancedConfig.requestBody.value} `, // code editor adds closing tag
+ 'check.request.body': advancedConfig.requestBody.value,
'check.response.body.positive': [advancedConfig.responseBodyCheckPositive],
'check.response.body.negative': [advancedConfig.responseBodyCheckNegative],
'response.include_body': advancedConfig.indexResponseBody ? 'on_error' : 'never',
From fbf4f26e398f049640cc46731e48001fd8d9bd3b Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Tue, 22 Jun 2021 17:26:28 +0300
Subject: [PATCH 063/163] [Docs] Drilldowns only for timeseries TSVB charts
(#102481)
* [Docs] Drilldowns only for timeseries TSVB charts
* Update docs/user/dashboard/drilldowns.asciidoc
Co-authored-by: Kaarina Tungseth
Co-authored-by: Wylie Conlon
Co-authored-by: Kaarina Tungseth
---
docs/user/dashboard/drilldowns.asciidoc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc
index 0eb4b43466ff9..84c33db31d575 100644
--- a/docs/user/dashboard/drilldowns.asciidoc
+++ b/docs/user/dashboard/drilldowns.asciidoc
@@ -112,7 +112,7 @@ The following panel types support drilldowns.
^| X
^| X
-| TSVB
+| TSVB (only for time series visualizations)
^| X
^| X
From 0ba8b43228bde0407e724b0fa732227fa91716b6 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Tue, 22 Jun 2021 16:28:10 +0200
Subject: [PATCH 064/163] [Lens] Clicking number histogram bar applies global
filter instead of time filter (#102730)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../xy_visualization/expression.test.tsx | 146 ++++++++++++++++++
.../public/xy_visualization/expression.tsx | 8 +-
2 files changed, 150 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx
index ee1f66063ad1d..930f6888ce532 100644
--- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx
@@ -1099,6 +1099,152 @@ describe('xy_expression', () => {
});
});
+ test('onElementClick returns correct context data for date histogram', () => {
+ const geometry: GeometryValue = {
+ x: 1585758120000,
+ y: 1,
+ accessor: 'y1',
+ mark: null,
+ datum: {},
+ };
+ const series = {
+ key: 'spec{d}yAccessor{d}splitAccessors{b-2}',
+ specId: 'd',
+ yAccessor: 'yAccessorId',
+ splitAccessors: {},
+ seriesKeys: ['yAccessorId'],
+ };
+
+ const { args } = sampleArgs();
+
+ const wrapper = mountWithIntl(
+
+ );
+
+ wrapper.find(Settings).first().prop('onElementClick')!([
+ [geometry, series as XYChartSeriesIdentifier],
+ ]);
+
+ expect(onClickValue).toHaveBeenCalledWith({
+ data: [
+ {
+ column: 0,
+ row: 0,
+ table: dateHistogramData.tables.timeLayer,
+ value: 1585758120000,
+ },
+ ],
+ timeFieldName: 'order_date',
+ });
+ });
+
+ test('onElementClick returns correct context data for numeric histogram', () => {
+ const { args } = sampleArgs();
+
+ const numberLayer: LayerArgs = {
+ layerId: 'numberLayer',
+ hide: false,
+ xAccessor: 'xAccessorId',
+ yScaleType: 'linear',
+ xScaleType: 'linear',
+ isHistogram: true,
+ seriesType: 'bar_stacked',
+ accessors: ['yAccessorId'],
+ palette: mockPaletteOutput,
+ };
+
+ const numberHistogramData: LensMultiTable = {
+ type: 'lens_multitable',
+ tables: {
+ numberLayer: {
+ type: 'datatable',
+ rows: [
+ {
+ xAccessorId: 5,
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 7,
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 8,
+ yAccessorId: 1,
+ },
+ {
+ xAccessorId: 10,
+ yAccessorId: 1,
+ },
+ ],
+ columns: [
+ {
+ id: 'xAccessorId',
+ name: 'bytes',
+ meta: { type: 'number' },
+ },
+ {
+ id: 'yAccessorId',
+ name: 'Count of records',
+ meta: { type: 'number' },
+ },
+ ],
+ },
+ },
+ dateRange: {
+ fromDate: new Date('2020-04-01T16:14:16.246Z'),
+ toDate: new Date('2020-04-01T17:15:41.263Z'),
+ },
+ };
+ const geometry: GeometryValue = {
+ x: 5,
+ y: 1,
+ accessor: 'y1',
+ mark: null,
+ datum: {},
+ };
+ const series = {
+ key: 'spec{d}yAccessor{d}splitAccessors{b-2}',
+ specId: 'd',
+ yAccessor: 'yAccessorId',
+ splitAccessors: {},
+ seriesKeys: ['yAccessorId'],
+ };
+
+ const wrapper = mountWithIntl(
+
+ );
+
+ wrapper.find(Settings).first().prop('onElementClick')!([
+ [geometry, series as XYChartSeriesIdentifier],
+ ]);
+
+ expect(onClickValue).toHaveBeenCalledWith({
+ data: [
+ {
+ column: 0,
+ row: 0,
+ table: numberHistogramData.tables.numberLayer,
+ value: 5,
+ },
+ ],
+ timeFieldName: undefined,
+ });
+ });
+
test('returns correct original data for ordinal x axis with special formatter', () => {
const geometry: GeometryValue = { x: 'BAR', y: 1, accessor: 'y1', mark: null, datum: {} };
const series = {
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
index 24842c83c23b1..1de5cf6b30533 100644
--- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
@@ -562,9 +562,9 @@ export function XYChart({
value: pointValue,
});
}
-
- const xAxisFieldName = table.columns.find((el) => el.id === layer.xAccessor)?.meta?.field;
- const timeFieldName = xDomain && xAxisFieldName;
+ const currentColumnMeta = table.columns.find((el) => el.id === layer.xAccessor)?.meta;
+ const xAxisFieldName = currentColumnMeta?.field;
+ const isDateField = currentColumnMeta?.type === 'date';
const context: LensFilterEvent['data'] = {
data: points.map((point) => ({
@@ -573,7 +573,7 @@ export function XYChart({
value: point.value,
table,
})),
- timeFieldName,
+ timeFieldName: xDomain && isDateField ? xAxisFieldName : undefined,
};
onClickValue(desanitizeFilterContext(context));
};
From a2e7b388a4072b3e64721e0e55d345381ced469d Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Tue, 22 Jun 2021 16:30:02 +0200
Subject: [PATCH 065/163] [Lens] Update dimension panel copy to suggested one
(#102890)
---
.../editor_frame/config_panel/dimension_container.tsx | 2 +-
.../dimension_panel/dimension_editor.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
index 2f3eb5043d610..c62b10093e6e5 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_container.tsx
@@ -113,7 +113,7 @@ export function DimensionContainer({
>
{i18n.translate('xpack.lens.configure.configurePanelTitle', {
- defaultMessage: '{groupLabel} configuration',
+ defaultMessage: '{groupLabel}',
values: {
groupLabel,
},
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
index dca8e926646f0..b35986c42054d 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
@@ -750,7 +750,7 @@ function getErrorMessage(
if (selectedColumn && incompleteOperation) {
if (input === 'field') {
return i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', {
- defaultMessage: 'To use this function, select a different field.',
+ defaultMessage: 'This field does not work with the selected function.',
});
}
return i18n.translate('xpack.lens.indexPattern.chooseFieldLabel', {
From 1397461aca4b586f4740de50a83064fc560397a2 Mon Sep 17 00:00:00 2001
From: Corey Robertson
Date: Tue, 22 Jun 2021 10:56:17 -0400
Subject: [PATCH 066/163] Fixes onDestroy handler (#101959)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/canvas/public/lib/create_handlers.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts
index 9f531d6921417..928a33de80848 100644
--- a/x-pack/plugins/canvas/public/lib/create_handlers.ts
+++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts
@@ -44,6 +44,10 @@ export const createHandlers = (baseHandlers = createBaseHandlers()): RendererHan
this.done = fn;
},
+ onDestroy(fn: () => void) {
+ this.destroy = fn;
+ },
+
// TODO: these functions do not match the `onXYZ` and `xyz` pattern elsewhere.
onEmbeddableDestroyed() {},
onEmbeddableInputChange() {},
From 46f43784b0c4911f717e45aed020e18e4d9a7967 Mon Sep 17 00:00:00 2001
From: Corey Robertson
Date: Tue, 22 Jun 2021 10:56:47 -0400
Subject: [PATCH 067/163] Handle element changing into a filter (#97890)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../renderers/filters/dropdown_filter/index.tsx | 7 +++++--
.../renderers/filters/time_filter/index.tsx | 9 +++++++--
x-pack/plugins/canvas/public/lib/create_handlers.ts | 2 +-
3 files changed, 13 insertions(+), 5 deletions(-)
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx
index 97b5e592552ed..fbcba9e56aef5 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx
@@ -45,9 +45,12 @@ export const dropdownFilter: RendererFactory = () => ({
reuseDomNode: true,
height: 50,
render(domNode, config, handlers) {
- const filterExpression = handlers.getFilter();
+ let filterExpression = handlers.getFilter();
- if (filterExpression !== '') {
+ if (filterExpression === undefined || filterExpression.indexOf('exactly')) {
+ filterExpression = '';
+ handlers.setFilter(filterExpression);
+ } else if (filterExpression !== '') {
// NOTE: setFilter() will cause a data refresh, avoid calling unless required
// compare expression and filter, update filter if needed
const { changed, newAst } = syncFilterExpression(config, filterExpression, ['filterGroup']);
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx
index ff781bb294db4..02a36b80fa364 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx
@@ -19,6 +19,8 @@ import { RendererFactory } from '../../../../types';
const { timeFilter: strings } = RendererStrings;
+const defaultTimeFilterExpression = 'timefilter column=@timestamp from=now-24h to=now';
+
export const timeFilterFactory: StartInitializer> = (core, plugins) => {
const { uiSettings } = core;
@@ -38,9 +40,12 @@ export const timeFilterFactory: StartInitializer> = (
help: strings.getHelpDescription(),
reuseDomNode: true, // must be true, otherwise popovers don't work
render: async (domNode: HTMLElement, config: Arguments, handlers: RendererHandlers) => {
- const filterExpression = handlers.getFilter();
+ let filterExpression = handlers.getFilter();
- if (filterExpression !== '') {
+ if (filterExpression === undefined || filterExpression.indexOf('timefilter') !== 0) {
+ filterExpression = defaultTimeFilterExpression;
+ handlers.setFilter(filterExpression);
+ } else if (filterExpression !== '') {
// NOTE: setFilter() will cause a data refresh, avoid calling unless required
// compare expression and filter, update filter if needed
const { changed, newAst } = syncFilterExpression(config, filterExpression, [
diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts
index 928a33de80848..aba29ccd542be 100644
--- a/x-pack/plugins/canvas/public/lib/create_handlers.ts
+++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts
@@ -109,7 +109,7 @@ export const createDispatchedHandlerFactory = (
},
getFilter() {
- return element.filter;
+ return element.filter || '';
},
onComplete(fn: () => void) {
From 11e68fda87ba1b410fc2fa0642909a4084130ac9 Mon Sep 17 00:00:00 2001
From: Jonathan Budzenski
Date: Tue, 22 Jun 2021 09:59:20 -0500
Subject: [PATCH 068/163] [packages] Move @kbn/interpreter to Bazel (#101089)
Co-authored-by: Tiago Costa
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.eslintignore | 2 -
.../monorepo-packages.asciidoc | 1 +
package.json | 2 +-
packages/BUILD.bazel | 1 +
packages/kbn-interpreter/.babelrc | 9 -
packages/kbn-interpreter/.npmignore | 3 -
packages/kbn-interpreter/BUILD.bazel | 99 ++
packages/kbn-interpreter/common/package.json | 3 +-
.../lib/grammar.peg => grammar/grammar.pegjs} | 0
packages/kbn-interpreter/package.json | 8 +-
packages/kbn-interpreter/scripts/build.js | 9 -
.../kbn-interpreter/src/common/index.d.ts | 12 -
.../src/common/{index.js => index.ts} | 12 +-
.../kbn-interpreter/src/common/lib/ast.d.ts | 25 -
.../common/lib/ast.from_expression.test.js | 2 +-
.../src/common/lib/ast.to_expression.test.js | 2 +-
.../src/common/lib/{ast.js => ast.ts} | 48 +-
.../src/common/lib/get_type.d.ts | 9 -
.../common/lib/{get_type.js => get_type.ts} | 2 +-
.../kbn-interpreter/src/common/lib/grammar.js | 1053 -----------------
.../src/common/lib/registry.d.ts | 25 -
.../common/lib/{registry.js => registry.ts} | 26 +-
.../tasks/build/__fixtures__/sample.js | 3 -
packages/kbn-interpreter/tasks/build/cli.js | 82 --
packages/kbn-interpreter/tasks/build/paths.js | 15 -
packages/kbn-interpreter/tsconfig.json | 18 +-
.../charts/public/services/palettes/types.ts | 4 +-
x-pack/package.json | 1 -
x-pack/plugins/canvas/public/functions/to.ts | 1 -
x-pack/plugins/canvas/public/registries.ts | 1 -
.../canvas/public/state/selectors/workpad.ts | 1 -
yarn.lock | 2 +-
32 files changed, 191 insertions(+), 1290 deletions(-)
delete mode 100644 packages/kbn-interpreter/.babelrc
delete mode 100644 packages/kbn-interpreter/.npmignore
create mode 100644 packages/kbn-interpreter/BUILD.bazel
rename packages/kbn-interpreter/{src/common/lib/grammar.peg => grammar/grammar.pegjs} (100%)
delete mode 100644 packages/kbn-interpreter/scripts/build.js
delete mode 100644 packages/kbn-interpreter/src/common/index.d.ts
rename packages/kbn-interpreter/src/common/{index.js => index.ts} (76%)
delete mode 100644 packages/kbn-interpreter/src/common/lib/ast.d.ts
rename packages/kbn-interpreter/src/common/lib/{ast.js => ast.ts} (75%)
delete mode 100644 packages/kbn-interpreter/src/common/lib/get_type.d.ts
rename packages/kbn-interpreter/src/common/lib/{get_type.js => get_type.ts} (92%)
delete mode 100644 packages/kbn-interpreter/src/common/lib/grammar.js
delete mode 100644 packages/kbn-interpreter/src/common/lib/registry.d.ts
rename packages/kbn-interpreter/src/common/lib/{registry.js => registry.ts} (73%)
delete mode 100644 packages/kbn-interpreter/tasks/build/__fixtures__/sample.js
delete mode 100644 packages/kbn-interpreter/tasks/build/cli.js
delete mode 100644 packages/kbn-interpreter/tasks/build/paths.js
diff --git a/.eslintignore b/.eslintignore
index 63cd01d6e90db..f757ed9a1bf98 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -30,8 +30,6 @@ snapshots.js
# package overrides
/packages/elastic-eslint-config-kibana
-/packages/kbn-interpreter/src/common/lib/grammar.js
-/packages/kbn-tinymath/src/grammar.js
/packages/kbn-plugin-generator/template
/packages/kbn-pm/dist
/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/
diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc
index ebab9de66032f..48d0d40d0abb0 100644
--- a/docs/developer/getting-started/monorepo-packages.asciidoc
+++ b/docs/developer/getting-started/monorepo-packages.asciidoc
@@ -80,6 +80,7 @@ yarn kbn watch-bazel
- @kbn/eslint-plugin-eslint
- @kbn/expect
- @kbn/i18n
+- @kbn/interpreter
- @kbn/io-ts-utils
- @kbn/legacy-logging
- @kbn/logging
diff --git a/package.json b/package.json
index 114a9ac98df72..873dffeed38f8 100644
--- a/package.json
+++ b/package.json
@@ -133,7 +133,7 @@
"@kbn/crypto": "link:bazel-bin/packages/kbn-crypto",
"@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl",
"@kbn/i18n": "link:bazel-bin/packages/kbn-i18n",
- "@kbn/interpreter": "link:packages/kbn-interpreter",
+ "@kbn/interpreter": "link:bazel-bin/packages/kbn-interpreter",
"@kbn/io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils",
"@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging",
"@kbn/logging": "link:bazel-bin/packages/kbn-logging",
diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel
index 61034c562b447..70a3d1eacc7c5 100644
--- a/packages/BUILD.bazel
+++ b/packages/BUILD.bazel
@@ -23,6 +23,7 @@ filegroup(
"//packages/kbn-eslint-plugin-eslint:build",
"//packages/kbn-expect:build",
"//packages/kbn-i18n:build",
+ "//packages/kbn-interpreter:build",
"//packages/kbn-io-ts-utils:build",
"//packages/kbn-legacy-logging:build",
"//packages/kbn-logging:build",
diff --git a/packages/kbn-interpreter/.babelrc b/packages/kbn-interpreter/.babelrc
deleted file mode 100644
index 309b3d5b3233d..0000000000000
--- a/packages/kbn-interpreter/.babelrc
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "presets": ["@kbn/babel-preset/webpack_preset"],
- "plugins": [
- "@babel/plugin-transform-modules-commonjs",
- ["@babel/plugin-transform-runtime", {
- "regenerator": true
- }]
- ]
-}
diff --git a/packages/kbn-interpreter/.npmignore b/packages/kbn-interpreter/.npmignore
deleted file mode 100644
index b9bc539e63ce4..0000000000000
--- a/packages/kbn-interpreter/.npmignore
+++ /dev/null
@@ -1,3 +0,0 @@
-src
-tasks
-.babelrc
diff --git a/packages/kbn-interpreter/BUILD.bazel b/packages/kbn-interpreter/BUILD.bazel
new file mode 100644
index 0000000000000..4492faabfdf81
--- /dev/null
+++ b/packages/kbn-interpreter/BUILD.bazel
@@ -0,0 +1,99 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
+load("@npm//pegjs:index.bzl", "pegjs")
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
+
+PKG_BASE_NAME = "kbn-interpreter"
+PKG_REQUIRE_NAME = "@kbn/interpreter"
+
+SOURCE_FILES = glob(
+ [
+ "src/**/*",
+ ]
+)
+
+TYPE_FILES = []
+
+SRCS = SOURCE_FILES + TYPE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "common/package.json",
+ "package.json",
+]
+
+SRC_DEPS = [
+ "@npm//lodash",
+]
+
+TYPES_DEPS = [
+ "@npm//@types/jest",
+ "@npm//@types/lodash",
+ "@npm//@types/node",
+]
+
+DEPS = SRC_DEPS + TYPES_DEPS
+
+pegjs(
+ name = "grammar",
+ data = [
+ ":grammar/grammar.pegjs"
+ ],
+ output_dir = True,
+ args = [
+ "--allowed-start-rules",
+ "expression,argument",
+ "-o",
+ "$(@D)/index.js",
+ "./%s/grammar/grammar.pegjs" % package_name()
+ ],
+)
+
+ts_config(
+ name = "tsconfig",
+ src = "tsconfig.json",
+ deps = [
+ "//:tsconfig.base.json",
+ ],
+)
+
+ts_project(
+ name = "tsc",
+ args = ['--pretty'],
+ srcs = SRCS,
+ deps = DEPS,
+ allow_js = True,
+ declaration = True,
+ declaration_map = True,
+ incremental = True,
+ out_dir = "target",
+ source_map = True,
+ root_dir = "src",
+ tsconfig = ":tsconfig",
+)
+
+js_library(
+ name = PKG_BASE_NAME,
+ srcs = NPM_MODULE_EXTRA_FILES + [":grammar"],
+ deps = DEPS + [":tsc"],
+ package_name = PKG_REQUIRE_NAME,
+ visibility = ["//visibility:public"],
+)
+
+pkg_npm(
+ name = "npm_module",
+ deps = [
+ ":%s" % PKG_BASE_NAME,
+ ]
+)
+
+filegroup(
+ name = "build",
+ srcs = [
+ ":npm_module",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/packages/kbn-interpreter/common/package.json b/packages/kbn-interpreter/common/package.json
index 62061138234d9..2f5277a8e8652 100644
--- a/packages/kbn-interpreter/common/package.json
+++ b/packages/kbn-interpreter/common/package.json
@@ -1,6 +1,5 @@
{
"private": true,
"main": "../target/common/index.js",
- "types": "../target/common/index.d.ts",
- "jsnext:main": "../src/common/index.js"
+ "types": "../target/common/index.d.ts"
}
\ No newline at end of file
diff --git a/packages/kbn-interpreter/src/common/lib/grammar.peg b/packages/kbn-interpreter/grammar/grammar.pegjs
similarity index 100%
rename from packages/kbn-interpreter/src/common/lib/grammar.peg
rename to packages/kbn-interpreter/grammar/grammar.pegjs
diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json
index fc0936f4b5f53..efdb30e105186 100644
--- a/packages/kbn-interpreter/package.json
+++ b/packages/kbn-interpreter/package.json
@@ -2,11 +2,5 @@
"name": "@kbn/interpreter",
"private": "true",
"version": "1.0.0",
- "license": "SSPL-1.0 OR Elastic License 2.0",
- "scripts": {
- "interpreter:peg": "../../node_modules/.bin/pegjs src/common/lib/grammar.peg",
- "build": "node scripts/build",
- "kbn:bootstrap": "node scripts/build --dev",
- "kbn:watch": "node scripts/build --dev --watch"
- }
+ "license": "SSPL-1.0 OR Elastic License 2.0"
}
\ No newline at end of file
diff --git a/packages/kbn-interpreter/scripts/build.js b/packages/kbn-interpreter/scripts/build.js
deleted file mode 100644
index 21b7f86c6bc34..0000000000000
--- a/packages/kbn-interpreter/scripts/build.js
+++ /dev/null
@@ -1,9 +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 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-require('../tasks/build/cli');
diff --git a/packages/kbn-interpreter/src/common/index.d.ts b/packages/kbn-interpreter/src/common/index.d.ts
deleted file mode 100644
index 6f54d07590973..0000000000000
--- a/packages/kbn-interpreter/src/common/index.d.ts
+++ /dev/null
@@ -1,12 +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 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-export { Registry } from './lib/registry';
-
-export { fromExpression, toExpression, Ast, ExpressionFunctionAST } from './lib/ast';
-export { getType } from './lib/get_type';
diff --git a/packages/kbn-interpreter/src/common/index.js b/packages/kbn-interpreter/src/common/index.ts
similarity index 76%
rename from packages/kbn-interpreter/src/common/index.js
rename to packages/kbn-interpreter/src/common/index.ts
index b83d8180980cd..524c854b40429 100644
--- a/packages/kbn-interpreter/src/common/index.js
+++ b/packages/kbn-interpreter/src/common/index.ts
@@ -6,11 +6,19 @@
* Side Public License, v 1.
*/
-export { fromExpression, toExpression, safeElementFromExpression } from './lib/ast';
+export {
+ fromExpression,
+ toExpression,
+ safeElementFromExpression,
+ Ast,
+ ExpressionFunctionAST,
+} from './lib/ast';
export { Fn } from './lib/fn';
export { getType } from './lib/get_type';
export { castProvider } from './lib/cast';
-export { parse } from './lib/grammar';
+// @ts-expect-error
+// @internal
+export { parse } from '../../grammar';
export { getByAlias } from './lib/get_by_alias';
export { Registry } from './lib/registry';
export { addRegistries, register, registryFactory } from './registries';
diff --git a/packages/kbn-interpreter/src/common/lib/ast.d.ts b/packages/kbn-interpreter/src/common/lib/ast.d.ts
deleted file mode 100644
index 0e95cb9901df0..0000000000000
--- a/packages/kbn-interpreter/src/common/lib/ast.d.ts
+++ /dev/null
@@ -1,25 +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 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-export type ExpressionArgAST = string | boolean | number | Ast;
-
-export interface ExpressionFunctionAST {
- type: 'function';
- function: string;
- arguments: {
- [key: string]: ExpressionArgAST[];
- };
-}
-
-export interface Ast {
- type: 'expression';
- chain: ExpressionFunctionAST[];
-}
-
-export declare function fromExpression(expression: string): Ast;
-export declare function toExpression(astObj: Ast, type?: string): string;
diff --git a/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js b/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js
index c67a266e1276a..a098a3fdce0f6 100644
--- a/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js
+++ b/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { fromExpression } from './ast';
+import { fromExpression } from '@kbn/interpreter/target/common/lib/ast';
import { getType } from './get_type';
describe('ast fromExpression', () => {
diff --git a/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js b/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js
index c60412f05c15a..b500ca06836a4 100644
--- a/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js
+++ b/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { toExpression } from './ast';
+import { toExpression } from '@kbn/interpreter/common';
describe('ast toExpression', () => {
describe('single expression', () => {
diff --git a/packages/kbn-interpreter/src/common/lib/ast.js b/packages/kbn-interpreter/src/common/lib/ast.ts
similarity index 75%
rename from packages/kbn-interpreter/src/common/lib/ast.js
rename to packages/kbn-interpreter/src/common/lib/ast.ts
index fb471e34ccc69..791c94809f35c 100644
--- a/packages/kbn-interpreter/src/common/lib/ast.js
+++ b/packages/kbn-interpreter/src/common/lib/ast.ts
@@ -7,12 +7,35 @@
*/
import { getType } from './get_type';
-import { parse } from './grammar';
+// @ts-expect-error
+import { parse } from '../../../grammar';
-function getArgumentString(arg, argKey, level = 0) {
+export type ExpressionArgAST = string | boolean | number | Ast;
+
+export interface ExpressionFunctionAST {
+ type: 'function';
+ function: string;
+ arguments: {
+ [key: string]: ExpressionArgAST[];
+ };
+}
+
+export interface Ast {
+ /** @internal */
+ function: any;
+ /** @internal */
+ arguments: any;
+ type: 'expression';
+ chain: ExpressionFunctionAST[];
+ /** @internal */
+ replace(regExp: RegExp, s: string): string;
+}
+
+function getArgumentString(arg: Ast, argKey: string | undefined, level = 0) {
const type = getType(arg);
- function maybeArgKey(argKey, argString) {
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ function maybeArgKey(argKey: string | null | undefined, argString: string) {
return argKey == null || argKey === '_' ? argString : `${argKey}=${argString}`;
}
@@ -36,7 +59,7 @@ function getArgumentString(arg, argKey, level = 0) {
throw new Error(`Invalid argument type in AST: ${type}`);
}
-function getExpressionArgs(block, level = 0) {
+function getExpressionArgs(block: Ast, level = 0) {
const args = block.arguments;
const hasValidArgs = typeof args === 'object' && args != null && !Array.isArray(args);
@@ -45,7 +68,7 @@ function getExpressionArgs(block, level = 0) {
const argKeys = Object.keys(args);
const MAX_LINE_LENGTH = 80; // length before wrapping arguments
return argKeys.map((argKey) =>
- args[argKey].reduce((acc, arg) => {
+ args[argKey].reduce((acc: any, arg: any) => {
const argString = getArgumentString(arg, argKey, level);
const lineLength = acc.split('\n').pop().length;
@@ -63,12 +86,12 @@ function getExpressionArgs(block, level = 0) {
);
}
-function fnWithArgs(fnName, args) {
+function fnWithArgs(fnName: any, args: any[]) {
if (!args || args.length === 0) return fnName;
return `${fnName} ${args.join(' ')}`;
}
-function getExpression(chain, level = 0) {
+function getExpression(chain: any[], level = 0) {
if (!chain) throw new Error('Expressions must contain a chain');
// break new functions onto new lines if we're not in a nested/sub-expression
@@ -90,7 +113,7 @@ function getExpression(chain, level = 0) {
.join(separator);
}
-export function fromExpression(expression, type = 'expression') {
+export function fromExpression(expression: string, type = 'expression'): Ast {
try {
return parse(String(expression), { startRule: type });
} catch (e) {
@@ -99,7 +122,7 @@ export function fromExpression(expression, type = 'expression') {
}
// TODO: OMG This is so bad, we need to talk about the right way to handle bad expressions since some are element based and others not
-export function safeElementFromExpression(expression) {
+export function safeElementFromExpression(expression: string) {
try {
return fromExpression(expression);
} catch (e) {
@@ -116,8 +139,11 @@ Thanks for understanding,
}
// TODO: Respect the user's existing formatting
-export function toExpression(astObj, type = 'expression') {
- if (type === 'argument') return getArgumentString(astObj);
+export function toExpression(astObj: Ast, type = 'expression'): string {
+ if (type === 'argument') {
+ // @ts-ignore
+ return getArgumentString(astObj);
+ }
const validType = ['expression', 'function'].includes(getType(astObj));
if (!validType) throw new Error('Expression must be an expression or argument function');
diff --git a/packages/kbn-interpreter/src/common/lib/get_type.d.ts b/packages/kbn-interpreter/src/common/lib/get_type.d.ts
deleted file mode 100644
index 568658c780333..0000000000000
--- a/packages/kbn-interpreter/src/common/lib/get_type.d.ts
+++ /dev/null
@@ -1,9 +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 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-export declare function getType(node: any): string;
diff --git a/packages/kbn-interpreter/src/common/lib/get_type.js b/packages/kbn-interpreter/src/common/lib/get_type.ts
similarity index 92%
rename from packages/kbn-interpreter/src/common/lib/get_type.js
rename to packages/kbn-interpreter/src/common/lib/get_type.ts
index 7ae6dab029176..b6dff67bf5dc9 100644
--- a/packages/kbn-interpreter/src/common/lib/get_type.js
+++ b/packages/kbn-interpreter/src/common/lib/get_type.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-export function getType(node) {
+export function getType(node: any): string {
if (node == null) return 'null';
if (typeof node === 'object') {
if (!node.type) throw new Error('Objects must have a type property');
diff --git a/packages/kbn-interpreter/src/common/lib/grammar.js b/packages/kbn-interpreter/src/common/lib/grammar.js
deleted file mode 100644
index 3f473b1beea63..0000000000000
--- a/packages/kbn-interpreter/src/common/lib/grammar.js
+++ /dev/null
@@ -1,1053 +0,0 @@
-/*
- * Generated by PEG.js 0.10.0.
- *
- * http://pegjs.org/
- */
-
-"use strict";
-
-function peg$subclass(child, parent) {
- function ctor() { this.constructor = child; }
- ctor.prototype = parent.prototype;
- child.prototype = new ctor();
-}
-
-function peg$SyntaxError(message, expected, found, location) {
- this.message = message;
- this.expected = expected;
- this.found = found;
- this.location = location;
- this.name = "SyntaxError";
-
- if (typeof Error.captureStackTrace === "function") {
- Error.captureStackTrace(this, peg$SyntaxError);
- }
-}
-
-peg$subclass(peg$SyntaxError, Error);
-
-peg$SyntaxError.buildMessage = function(expected, found) {
- var DESCRIBE_EXPECTATION_FNS = {
- literal: function(expectation) {
- return "\"" + literalEscape(expectation.text) + "\"";
- },
-
- "class": function(expectation) {
- var escapedParts = "",
- i;
-
- for (i = 0; i < expectation.parts.length; i++) {
- escapedParts += expectation.parts[i] instanceof Array
- ? classEscape(expectation.parts[i][0]) + "-" + classEscape(expectation.parts[i][1])
- : classEscape(expectation.parts[i]);
- }
-
- return "[" + (expectation.inverted ? "^" : "") + escapedParts + "]";
- },
-
- any: function(expectation) {
- return "any character";
- },
-
- end: function(expectation) {
- return "end of input";
- },
-
- other: function(expectation) {
- return expectation.description;
- }
- };
-
- function hex(ch) {
- return ch.charCodeAt(0).toString(16).toUpperCase();
- }
-
- function literalEscape(s) {
- return s
- .replace(/\\/g, '\\\\')
- .replace(/"/g, '\\"')
- .replace(/\0/g, '\\0')
- .replace(/\t/g, '\\t')
- .replace(/\n/g, '\\n')
- .replace(/\r/g, '\\r')
- .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); })
- .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); });
- }
-
- function classEscape(s) {
- return s
- .replace(/\\/g, '\\\\')
- .replace(/\]/g, '\\]')
- .replace(/\^/g, '\\^')
- .replace(/-/g, '\\-')
- .replace(/\0/g, '\\0')
- .replace(/\t/g, '\\t')
- .replace(/\n/g, '\\n')
- .replace(/\r/g, '\\r')
- .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); })
- .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); });
- }
-
- function describeExpectation(expectation) {
- return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);
- }
-
- function describeExpected(expected) {
- var descriptions = new Array(expected.length),
- i, j;
-
- for (i = 0; i < expected.length; i++) {
- descriptions[i] = describeExpectation(expected[i]);
- }
-
- descriptions.sort();
-
- if (descriptions.length > 0) {
- for (i = 1, j = 1; i < descriptions.length; i++) {
- if (descriptions[i - 1] !== descriptions[i]) {
- descriptions[j] = descriptions[i];
- j++;
- }
- }
- descriptions.length = j;
- }
-
- switch (descriptions.length) {
- case 1:
- return descriptions[0];
-
- case 2:
- return descriptions[0] + " or " + descriptions[1];
-
- default:
- return descriptions.slice(0, -1).join(", ")
- + ", or "
- + descriptions[descriptions.length - 1];
- }
- }
-
- function describeFound(found) {
- return found ? "\"" + literalEscape(found) + "\"" : "end of input";
- }
-
- return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found.";
-};
-
-function peg$parse(input, options) {
- options = options !== void 0 ? options : {};
-
- var peg$FAILED = {},
-
- peg$startRuleFunctions = { expression: peg$parseexpression, argument: peg$parseargument },
- peg$startRuleFunction = peg$parseexpression,
-
- peg$c0 = "|",
- peg$c1 = peg$literalExpectation("|", false),
- peg$c2 = function(first, fn) { return fn; },
- peg$c3 = function(first, rest) {
- return addMeta({
- type: 'expression',
- chain: first ? [first].concat(rest) : []
- }, text(), location());
- },
- peg$c4 = peg$otherExpectation("function"),
- peg$c5 = function(name, arg_list) {
- return addMeta({
- type: 'function',
- function: name,
- arguments: arg_list
- }, text(), location());
- },
- peg$c6 = "=",
- peg$c7 = peg$literalExpectation("=", false),
- peg$c8 = function(name, value) {
- return { name, value };
- },
- peg$c9 = function(value) {
- return { name: '_', value };
- },
- peg$c10 = "$",
- peg$c11 = peg$literalExpectation("$", false),
- peg$c12 = "{",
- peg$c13 = peg$literalExpectation("{", false),
- peg$c14 = "}",
- peg$c15 = peg$literalExpectation("}", false),
- peg$c16 = function(expression) { return expression; },
- peg$c17 = function(value) {
- return addMeta(value, text(), location());
- },
- peg$c18 = function(arg) { return arg; },
- peg$c19 = function(args) {
- return args.reduce((accumulator, { name, value }) => ({
- ...accumulator,
- [name]: (accumulator[name] || []).concat(value)
- }), {});
- },
- peg$c20 = /^[a-zA-Z0-9_\-]/,
- peg$c21 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"], "_", "-"], false, false),
- peg$c22 = function(name) {
- return name.join('');
- },
- peg$c23 = peg$otherExpectation("literal"),
- peg$c24 = "\"",
- peg$c25 = peg$literalExpectation("\"", false),
- peg$c26 = function(chars) { return chars.join(''); },
- peg$c27 = "'",
- peg$c28 = peg$literalExpectation("'", false),
- peg$c29 = function(string) { // this also matches nulls, booleans, and numbers
- var result = string.join('');
- // Sort of hacky, but PEG doesn't have backtracking so
- // a null/boolean/number rule is hard to read, and performs worse
- if (result === 'null') return null;
- if (result === 'true') return true;
- if (result === 'false') return false;
- if (isNaN(Number(result))) return result; // 5bears
- return Number(result);
- },
- peg$c30 = /^[ \t\r\n]/,
- peg$c31 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false),
- peg$c32 = "\\",
- peg$c33 = peg$literalExpectation("\\", false),
- peg$c34 = /^["'(){}<>[\]$`|= \t\n\r]/,
- peg$c35 = peg$classExpectation(["\"", "'", "(", ")", "{", "}", "<", ">", "[", "]", "$", "`", "|", "=", " ", "\t", "\n", "\r"], false, false),
- peg$c36 = function(sequence) { return sequence; },
- peg$c37 = /^[^"'(){}<>[\]$`|= \t\n\r]/,
- peg$c38 = peg$classExpectation(["\"", "'", "(", ")", "{", "}", "<", ">", "[", "]", "$", "`", "|", "=", " ", "\t", "\n", "\r"], true, false),
- peg$c39 = /^[^"]/,
- peg$c40 = peg$classExpectation(["\""], true, false),
- peg$c41 = /^[^']/,
- peg$c42 = peg$classExpectation(["'"], true, false),
-
- peg$currPos = 0,
- peg$savedPos = 0,
- peg$posDetailsCache = [{ line: 1, column: 1 }],
- peg$maxFailPos = 0,
- peg$maxFailExpected = [],
- peg$silentFails = 0,
-
- peg$result;
-
- if ("startRule" in options) {
- if (!(options.startRule in peg$startRuleFunctions)) {
- throw new Error("Can't start parsing from rule \"" + options.startRule + "\".");
- }
-
- peg$startRuleFunction = peg$startRuleFunctions[options.startRule];
- }
-
- function text() {
- return input.substring(peg$savedPos, peg$currPos);
- }
-
- function location() {
- return peg$computeLocation(peg$savedPos, peg$currPos);
- }
-
- function expected(description, location) {
- location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos)
-
- throw peg$buildStructuredError(
- [peg$otherExpectation(description)],
- input.substring(peg$savedPos, peg$currPos),
- location
- );
- }
-
- function error(message, location) {
- location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos)
-
- throw peg$buildSimpleError(message, location);
- }
-
- function peg$literalExpectation(text, ignoreCase) {
- return { type: "literal", text: text, ignoreCase: ignoreCase };
- }
-
- function peg$classExpectation(parts, inverted, ignoreCase) {
- return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase };
- }
-
- function peg$anyExpectation() {
- return { type: "any" };
- }
-
- function peg$endExpectation() {
- return { type: "end" };
- }
-
- function peg$otherExpectation(description) {
- return { type: "other", description: description };
- }
-
- function peg$computePosDetails(pos) {
- var details = peg$posDetailsCache[pos], p;
-
- if (details) {
- return details;
- } else {
- p = pos - 1;
- while (!peg$posDetailsCache[p]) {
- p--;
- }
-
- details = peg$posDetailsCache[p];
- details = {
- line: details.line,
- column: details.column
- };
-
- while (p < pos) {
- if (input.charCodeAt(p) === 10) {
- details.line++;
- details.column = 1;
- } else {
- details.column++;
- }
-
- p++;
- }
-
- peg$posDetailsCache[pos] = details;
- return details;
- }
- }
-
- function peg$computeLocation(startPos, endPos) {
- var startPosDetails = peg$computePosDetails(startPos),
- endPosDetails = peg$computePosDetails(endPos);
-
- return {
- start: {
- offset: startPos,
- line: startPosDetails.line,
- column: startPosDetails.column
- },
- end: {
- offset: endPos,
- line: endPosDetails.line,
- column: endPosDetails.column
- }
- };
- }
-
- function peg$fail(expected) {
- if (peg$currPos < peg$maxFailPos) { return; }
-
- if (peg$currPos > peg$maxFailPos) {
- peg$maxFailPos = peg$currPos;
- peg$maxFailExpected = [];
- }
-
- peg$maxFailExpected.push(expected);
- }
-
- function peg$buildSimpleError(message, location) {
- return new peg$SyntaxError(message, null, null, location);
- }
-
- function peg$buildStructuredError(expected, found, location) {
- return new peg$SyntaxError(
- peg$SyntaxError.buildMessage(expected, found),
- expected,
- found,
- location
- );
- }
-
- function peg$parseexpression() {
- var s0, s1, s2, s3, s4, s5, s6, s7;
-
- s0 = peg$currPos;
- s1 = peg$parsespace();
- if (s1 === peg$FAILED) {
- s1 = null;
- }
- if (s1 !== peg$FAILED) {
- s2 = peg$parsefunction();
- if (s2 === peg$FAILED) {
- s2 = null;
- }
- if (s2 !== peg$FAILED) {
- s3 = [];
- s4 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 124) {
- s5 = peg$c0;
- peg$currPos++;
- } else {
- s5 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c1); }
- }
- if (s5 !== peg$FAILED) {
- s6 = peg$parsespace();
- if (s6 === peg$FAILED) {
- s6 = null;
- }
- if (s6 !== peg$FAILED) {
- s7 = peg$parsefunction();
- if (s7 !== peg$FAILED) {
- peg$savedPos = s4;
- s5 = peg$c2(s2, s7);
- s4 = s5;
- } else {
- peg$currPos = s4;
- s4 = peg$FAILED;
- }
- } else {
- peg$currPos = s4;
- s4 = peg$FAILED;
- }
- } else {
- peg$currPos = s4;
- s4 = peg$FAILED;
- }
- while (s4 !== peg$FAILED) {
- s3.push(s4);
- s4 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 124) {
- s5 = peg$c0;
- peg$currPos++;
- } else {
- s5 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c1); }
- }
- if (s5 !== peg$FAILED) {
- s6 = peg$parsespace();
- if (s6 === peg$FAILED) {
- s6 = null;
- }
- if (s6 !== peg$FAILED) {
- s7 = peg$parsefunction();
- if (s7 !== peg$FAILED) {
- peg$savedPos = s4;
- s5 = peg$c2(s2, s7);
- s4 = s5;
- } else {
- peg$currPos = s4;
- s4 = peg$FAILED;
- }
- } else {
- peg$currPos = s4;
- s4 = peg$FAILED;
- }
- } else {
- peg$currPos = s4;
- s4 = peg$FAILED;
- }
- }
- if (s3 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c3(s2, s3);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
-
- return s0;
- }
-
- function peg$parsefunction() {
- var s0, s1, s2;
-
- peg$silentFails++;
- s0 = peg$currPos;
- s1 = peg$parseidentifier();
- if (s1 !== peg$FAILED) {
- s2 = peg$parsearg_list();
- if (s2 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c5(s1, s2);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- peg$silentFails--;
- if (s0 === peg$FAILED) {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c4); }
- }
-
- return s0;
- }
-
- function peg$parseargument_assignment() {
- var s0, s1, s2, s3, s4, s5;
-
- s0 = peg$currPos;
- s1 = peg$parseidentifier();
- if (s1 !== peg$FAILED) {
- s2 = peg$parsespace();
- if (s2 === peg$FAILED) {
- s2 = null;
- }
- if (s2 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 61) {
- s3 = peg$c6;
- peg$currPos++;
- } else {
- s3 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c7); }
- }
- if (s3 !== peg$FAILED) {
- s4 = peg$parsespace();
- if (s4 === peg$FAILED) {
- s4 = null;
- }
- if (s4 !== peg$FAILED) {
- s5 = peg$parseargument();
- if (s5 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c8(s1, s5);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- if (s0 === peg$FAILED) {
- s0 = peg$currPos;
- s1 = peg$parseargument();
- if (s1 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c9(s1);
- }
- s0 = s1;
- }
-
- return s0;
- }
-
- function peg$parseargument() {
- var s0, s1, s2, s3, s4;
-
- s0 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 36) {
- s1 = peg$c10;
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c11); }
- }
- if (s1 === peg$FAILED) {
- s1 = null;
- }
- if (s1 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 123) {
- s2 = peg$c12;
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c13); }
- }
- if (s2 !== peg$FAILED) {
- s3 = peg$parseexpression();
- if (s3 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 125) {
- s4 = peg$c14;
- peg$currPos++;
- } else {
- s4 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c15); }
- }
- if (s4 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c16(s3);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- if (s0 === peg$FAILED) {
- s0 = peg$currPos;
- s1 = peg$parseliteral();
- if (s1 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c17(s1);
- }
- s0 = s1;
- }
-
- return s0;
- }
-
- function peg$parsearg_list() {
- var s0, s1, s2, s3, s4;
-
- s0 = peg$currPos;
- s1 = [];
- s2 = peg$currPos;
- s3 = peg$parsespace();
- if (s3 !== peg$FAILED) {
- s4 = peg$parseargument_assignment();
- if (s4 !== peg$FAILED) {
- peg$savedPos = s2;
- s3 = peg$c18(s4);
- s2 = s3;
- } else {
- peg$currPos = s2;
- s2 = peg$FAILED;
- }
- } else {
- peg$currPos = s2;
- s2 = peg$FAILED;
- }
- while (s2 !== peg$FAILED) {
- s1.push(s2);
- s2 = peg$currPos;
- s3 = peg$parsespace();
- if (s3 !== peg$FAILED) {
- s4 = peg$parseargument_assignment();
- if (s4 !== peg$FAILED) {
- peg$savedPos = s2;
- s3 = peg$c18(s4);
- s2 = s3;
- } else {
- peg$currPos = s2;
- s2 = peg$FAILED;
- }
- } else {
- peg$currPos = s2;
- s2 = peg$FAILED;
- }
- }
- if (s1 !== peg$FAILED) {
- s2 = peg$parsespace();
- if (s2 === peg$FAILED) {
- s2 = null;
- }
- if (s2 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c19(s1);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
-
- return s0;
- }
-
- function peg$parseidentifier() {
- var s0, s1, s2;
-
- s0 = peg$currPos;
- s1 = [];
- if (peg$c20.test(input.charAt(peg$currPos))) {
- s2 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c21); }
- }
- if (s2 !== peg$FAILED) {
- while (s2 !== peg$FAILED) {
- s1.push(s2);
- if (peg$c20.test(input.charAt(peg$currPos))) {
- s2 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c21); }
- }
- }
- } else {
- s1 = peg$FAILED;
- }
- if (s1 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c22(s1);
- }
- s0 = s1;
-
- return s0;
- }
-
- function peg$parseliteral() {
- var s0, s1;
-
- peg$silentFails++;
- s0 = peg$parsephrase();
- if (s0 === peg$FAILED) {
- s0 = peg$parseunquoted_string_or_number();
- }
- peg$silentFails--;
- if (s0 === peg$FAILED) {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c23); }
- }
-
- return s0;
- }
-
- function peg$parsephrase() {
- var s0, s1, s2, s3;
-
- s0 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 34) {
- s1 = peg$c24;
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c25); }
- }
- if (s1 !== peg$FAILED) {
- s2 = [];
- s3 = peg$parsedq_char();
- while (s3 !== peg$FAILED) {
- s2.push(s3);
- s3 = peg$parsedq_char();
- }
- if (s2 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 34) {
- s3 = peg$c24;
- peg$currPos++;
- } else {
- s3 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c25); }
- }
- if (s3 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c26(s2);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- if (s0 === peg$FAILED) {
- s0 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 39) {
- s1 = peg$c27;
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c28); }
- }
- if (s1 !== peg$FAILED) {
- s2 = [];
- s3 = peg$parsesq_char();
- while (s3 !== peg$FAILED) {
- s2.push(s3);
- s3 = peg$parsesq_char();
- }
- if (s2 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 39) {
- s3 = peg$c27;
- peg$currPos++;
- } else {
- s3 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c28); }
- }
- if (s3 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c26(s2);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- }
-
- return s0;
- }
-
- function peg$parseunquoted_string_or_number() {
- var s0, s1, s2;
-
- s0 = peg$currPos;
- s1 = [];
- s2 = peg$parseunquoted();
- if (s2 !== peg$FAILED) {
- while (s2 !== peg$FAILED) {
- s1.push(s2);
- s2 = peg$parseunquoted();
- }
- } else {
- s1 = peg$FAILED;
- }
- if (s1 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c29(s1);
- }
- s0 = s1;
-
- return s0;
- }
-
- function peg$parsespace() {
- var s0, s1;
-
- s0 = [];
- if (peg$c30.test(input.charAt(peg$currPos))) {
- s1 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c31); }
- }
- if (s1 !== peg$FAILED) {
- while (s1 !== peg$FAILED) {
- s0.push(s1);
- if (peg$c30.test(input.charAt(peg$currPos))) {
- s1 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c31); }
- }
- }
- } else {
- s0 = peg$FAILED;
- }
-
- return s0;
- }
-
- function peg$parseunquoted() {
- var s0, s1, s2;
-
- s0 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 92) {
- s1 = peg$c32;
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c33); }
- }
- if (s1 !== peg$FAILED) {
- if (peg$c34.test(input.charAt(peg$currPos))) {
- s2 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c35); }
- }
- if (s2 === peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 92) {
- s2 = peg$c32;
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c33); }
- }
- }
- if (s2 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c36(s2);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- if (s0 === peg$FAILED) {
- if (peg$c37.test(input.charAt(peg$currPos))) {
- s0 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s0 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c38); }
- }
- }
-
- return s0;
- }
-
- function peg$parsedq_char() {
- var s0, s1, s2;
-
- s0 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 92) {
- s1 = peg$c32;
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c33); }
- }
- if (s1 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 34) {
- s2 = peg$c24;
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c25); }
- }
- if (s2 === peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 92) {
- s2 = peg$c32;
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c33); }
- }
- }
- if (s2 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c36(s2);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- if (s0 === peg$FAILED) {
- if (peg$c39.test(input.charAt(peg$currPos))) {
- s0 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s0 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c40); }
- }
- }
-
- return s0;
- }
-
- function peg$parsesq_char() {
- var s0, s1, s2;
-
- s0 = peg$currPos;
- if (input.charCodeAt(peg$currPos) === 92) {
- s1 = peg$c32;
- peg$currPos++;
- } else {
- s1 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c33); }
- }
- if (s1 !== peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 39) {
- s2 = peg$c27;
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c28); }
- }
- if (s2 === peg$FAILED) {
- if (input.charCodeAt(peg$currPos) === 92) {
- s2 = peg$c32;
- peg$currPos++;
- } else {
- s2 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c33); }
- }
- }
- if (s2 !== peg$FAILED) {
- peg$savedPos = s0;
- s1 = peg$c36(s2);
- s0 = s1;
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- } else {
- peg$currPos = s0;
- s0 = peg$FAILED;
- }
- if (s0 === peg$FAILED) {
- if (peg$c41.test(input.charAt(peg$currPos))) {
- s0 = input.charAt(peg$currPos);
- peg$currPos++;
- } else {
- s0 = peg$FAILED;
- if (peg$silentFails === 0) { peg$fail(peg$c42); }
- }
- }
-
- return s0;
- }
-
-
- function addMeta(node, text, { start: { offset: start }, end: { offset: end } }) {
- if (!options.addMeta) return node;
- return { node, text, start, end };
- }
-
-
- peg$result = peg$startRuleFunction();
-
- if (peg$result !== peg$FAILED && peg$currPos === input.length) {
- return peg$result;
- } else {
- if (peg$result !== peg$FAILED && peg$currPos < input.length) {
- peg$fail(peg$endExpectation());
- }
-
- throw peg$buildStructuredError(
- peg$maxFailExpected,
- peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,
- peg$maxFailPos < input.length
- ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)
- : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
- );
- }
-}
-
-module.exports = {
- SyntaxError: peg$SyntaxError,
- parse: peg$parse
-};
diff --git a/packages/kbn-interpreter/src/common/lib/registry.d.ts b/packages/kbn-interpreter/src/common/lib/registry.d.ts
deleted file mode 100644
index 766839ebf0e02..0000000000000
--- a/packages/kbn-interpreter/src/common/lib/registry.d.ts
+++ /dev/null
@@ -1,25 +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 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-export class Registry {
- constructor(prop?: string);
-
- public wrapper(obj: ItemSpec): Item;
-
- public register(fn: () => ItemSpec): void;
-
- public toJS(): { [key: string]: any };
-
- public toArray(): Item[];
-
- public get(name: string): Item;
-
- public getProp(): string;
-
- public reset(): void;
-}
diff --git a/packages/kbn-interpreter/src/common/lib/registry.js b/packages/kbn-interpreter/src/common/lib/registry.ts
similarity index 73%
rename from packages/kbn-interpreter/src/common/lib/registry.js
rename to packages/kbn-interpreter/src/common/lib/registry.ts
index 309f92ea24f6d..11f41ff736e96 100644
--- a/packages/kbn-interpreter/src/common/lib/registry.js
+++ b/packages/kbn-interpreter/src/common/lib/registry.ts
@@ -8,49 +8,59 @@
import { clone } from 'lodash';
-export class Registry {
+export class Registry {
+ private readonly _prop: string;
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ private _indexed: Object;
+
constructor(prop = 'name') {
if (typeof prop !== 'string') throw new Error('Registry property name must be a string');
this._prop = prop;
this._indexed = new Object();
}
- wrapper(obj) {
+ wrapper(obj: ItemSpec): Item {
+ // @ts-ignore
return obj;
}
- register(fn) {
+ register(fn: () => ItemSpec): void {
const obj = typeof fn === 'function' ? fn() : fn;
+ // @ts-ignore
if (typeof obj !== 'object' || !obj[this._prop]) {
throw new Error(`Registered functions must return an object with a ${this._prop} property`);
}
+ // @ts-ignore
this._indexed[obj[this._prop].toLowerCase()] = this.wrapper(obj);
}
- toJS() {
+ toJS(): { [key: string]: any } {
return Object.keys(this._indexed).reduce((acc, key) => {
+ // @ts-ignore
acc[key] = this.get(key);
return acc;
}, {});
}
- toArray() {
+ toArray(): Item[] {
return Object.keys(this._indexed).map((key) => this.get(key));
}
- get(name) {
+ get(name: string): Item {
+ // @ts-ignore
if (name === undefined) return null;
const lowerCaseName = name.toLowerCase();
+ // @ts-ignore
return this._indexed[lowerCaseName] ? clone(this._indexed[lowerCaseName]) : null;
}
- getProp() {
+ getProp(): string {
return this._prop;
}
- reset() {
+ reset(): void {
this._indexed = new Object();
}
}
diff --git a/packages/kbn-interpreter/tasks/build/__fixtures__/sample.js b/packages/kbn-interpreter/tasks/build/__fixtures__/sample.js
deleted file mode 100644
index f831545743f10..0000000000000
--- a/packages/kbn-interpreter/tasks/build/__fixtures__/sample.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/* eslint-disable */
-import util from 'util';
-console.log(util.format('hello world'));
diff --git a/packages/kbn-interpreter/tasks/build/cli.js b/packages/kbn-interpreter/tasks/build/cli.js
deleted file mode 100644
index 82e4475b409c3..0000000000000
--- a/packages/kbn-interpreter/tasks/build/cli.js
+++ /dev/null
@@ -1,82 +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 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-const { relative } = require('path');
-
-const getopts = require('getopts');
-const del = require('del');
-const supportsColor = require('supports-color');
-const { ToolingLog, withProcRunner, pickLevelFromFlags } = require('@kbn/dev-utils');
-
-const { ROOT_DIR, BUILD_DIR } = require('./paths');
-
-const unknownFlags = [];
-const flags = getopts(process.argv, {
- boolean: ['watch', 'dev', 'help', 'debug'],
- unknown(name) {
- unknownFlags.push(name);
- },
-});
-
-const log = new ToolingLog({
- level: pickLevelFromFlags(flags),
- writeTo: process.stdout,
-});
-
-if (unknownFlags.length) {
- log.error(`Unknown flag(s): ${unknownFlags.join(', ')}`);
- flags.help = true;
- process.exitCode = 1;
-}
-
-if (flags.help) {
- log.info(`
- Simple build tool for @kbn/interpreter package
-
- --dev Build for development, include source maps
- --watch Run in watch mode
- --debug Turn on debug logging
- `);
- process.exit();
-}
-
-withProcRunner(log, async (proc) => {
- log.info('Deleting old output');
- await del(BUILD_DIR);
-
- const cwd = ROOT_DIR;
- const env = { ...process.env };
- if (supportsColor.stdout) {
- env.FORCE_COLOR = 'true';
- }
-
- log.info(`Starting babel ${flags.watch ? ' in watch mode' : ''}`);
- await Promise.all([
- proc.run('babel ', {
- cmd: 'babel',
- args: [
- 'src',
- '--ignore',
- `*.test.js`,
- '--out-dir',
- relative(cwd, BUILD_DIR),
- '--copy-files',
- ...(flags.dev ? ['--source-maps', 'inline'] : []),
- ...(flags.watch ? ['--watch'] : ['--quiet']),
- ],
- wait: true,
- env,
- cwd,
- }),
- ]);
-
- log.success('Complete');
-}).catch((error) => {
- log.error(error);
- process.exit(1);
-});
diff --git a/packages/kbn-interpreter/tasks/build/paths.js b/packages/kbn-interpreter/tasks/build/paths.js
deleted file mode 100644
index a4cdba90a110a..0000000000000
--- a/packages/kbn-interpreter/tasks/build/paths.js
+++ /dev/null
@@ -1,15 +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 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-const { resolve } = require('path');
-
-exports.ROOT_DIR = resolve(__dirname, '../../');
-exports.SOURCE_DIR = resolve(exports.ROOT_DIR, 'src');
-exports.BUILD_DIR = resolve(exports.ROOT_DIR, 'target');
-
-exports.BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset');
diff --git a/packages/kbn-interpreter/tsconfig.json b/packages/kbn-interpreter/tsconfig.json
index 3b81bbb118a55..011ed877146e8 100644
--- a/packages/kbn-interpreter/tsconfig.json
+++ b/packages/kbn-interpreter/tsconfig.json
@@ -1,7 +1,21 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
- "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-interpreter"
+ "allowJs": true,
+ "incremental": true,
+ "outDir": "./target",
+ "declaration": true,
+ "declarationMap": true,
+ "rootDir": "src",
+ "sourceMap": true,
+ "sourceRoot": "../../../../packages/kbn-interpreter/src",
+ "stripInternal": true,
+ "types": [
+ "jest",
+ "node"
+ ]
},
- "include": ["index.d.ts", "src/**/*.d.ts"]
+ "include": [
+ "src/**/*",
+ ]
}
diff --git a/src/plugins/charts/public/services/palettes/types.ts b/src/plugins/charts/public/services/palettes/types.ts
index 6f13f62178364..7a870504270d7 100644
--- a/src/plugins/charts/public/services/palettes/types.ts
+++ b/src/plugins/charts/public/services/palettes/types.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { Ast } from '@kbn/interpreter/common';
+import { ExpressionAstExpression } from '../../../../expressions/common/ast';
/**
* Information about a series in a chart used to determine its color.
@@ -78,7 +78,7 @@ export interface PaletteDefinition {
* This function should be used to pass the palette to the expression function applying color and other styles
* @param state The internal state of the palette
*/
- toExpression: (state?: T) => Ast;
+ toExpression: (state?: T) => ExpressionAstExpression;
/**
* Color a series according to the internal rules of the palette.
* @param series The current series along with its ancestors.
diff --git a/x-pack/package.json b/x-pack/package.json
index 0d2a170d83170..01571cbb823fd 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -30,7 +30,6 @@
"@kbn/test": "link:../packages/kbn-test"
},
"dependencies": {
- "@kbn/interpreter": "link:../packages/kbn-interpreter",
"@kbn/ui-framework": "link:../packages/kbn-ui-framework"
}
}
\ No newline at end of file
diff --git a/x-pack/plugins/canvas/public/functions/to.ts b/x-pack/plugins/canvas/public/functions/to.ts
index dcb1972c6bdfb..907d1f3d3a635 100644
--- a/x-pack/plugins/canvas/public/functions/to.ts
+++ b/x-pack/plugins/canvas/public/functions/to.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-// @ts-expect-error untyped Elastic library
import { castProvider } from '@kbn/interpreter/common';
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public';
import { getFunctionHelp, getFunctionErrors } from '../../i18n';
diff --git a/x-pack/plugins/canvas/public/registries.ts b/x-pack/plugins/canvas/public/registries.ts
index 8484380830422..1ad7fa6905c22 100644
--- a/x-pack/plugins/canvas/public/registries.ts
+++ b/x-pack/plugins/canvas/public/registries.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-// @ts-expect-error untyped module
import { addRegistries, register } from '@kbn/interpreter/common';
// @ts-expect-error untyped local
import { elementsRegistry } from './lib/elements_registry';
diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/plugins/canvas/public/state/selectors/workpad.ts
index 43e4bc6bdb64f..e1cebeb65bd21 100644
--- a/x-pack/plugins/canvas/public/state/selectors/workpad.ts
+++ b/x-pack/plugins/canvas/public/state/selectors/workpad.ts
@@ -6,7 +6,6 @@
*/
import { get, omit } from 'lodash';
-// @ts-expect-error untyped local
import { safeElementFromExpression, fromExpression } from '@kbn/interpreter/common';
import { append } from '../../lib/modify_path';
import { getAssets } from './assets';
diff --git a/yarn.lock b/yarn.lock
index bcb5e607a44ee..153309ad56f19 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2668,7 +2668,7 @@
version "0.0.0"
uid ""
-"@kbn/interpreter@link:packages/kbn-interpreter":
+"@kbn/interpreter@link:bazel-bin/packages/kbn-interpreter":
version "0.0.0"
uid ""
From 494a841a59c4be07df526dcf9e9b9029c32edf77 Mon Sep 17 00:00:00 2001
From: John Dorlus
Date: Tue, 22 Jun 2021 10:59:47 -0400
Subject: [PATCH 069/163] Unskipping test to test on Firefox. (#102839)
* Unskipping test to test on Firefox.
* Added .only to only run those tests
* Reenabled test after troubleshooting tests. No failures on FF.
---
x-pack/test/functional/apps/grok_debugger/grok_debugger.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js
index 68cd5820e2a32..0162b660a1408 100644
--- a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js
+++ b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js
@@ -11,8 +11,7 @@ export default function ({ getService, getPageObjects }) {
const esArchiver = getService('esArchiver');
const PageObjects = getPageObjects(['grokDebugger']);
- // FLAKY: https://github.com/elastic/kibana/issues/84440
- describe.skip('grok debugger app', function () {
+ describe('grok debugger app', function () {
this.tags('includeFirefox');
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana');
From 6cc3b84d6fa97fb6f2db24d30ab84ac7b35bc3ce Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Tue, 22 Jun 2021 17:10:37 +0200
Subject: [PATCH 070/163] [Fleet] Add assets tab (#102517)
* very wip
* added new assets screen
* added routes to new assets view on the package details view
* Finished styling the assets page layout, need to work on adding
links
* rather use EuiHorizontalRule
* only show the assets tab if installed
* Added hacky version of linking to assets.
* added comment about deprecation of current linking functionality
* added an initial version of the success toast with a link to the agent flyout
* First iteration of end-to-end UX working. Need to add a lot of tests!
* fixed navigation bug and added a comment
* added a lot more padding to bottom of form
* restructured code for clarity, updated deprecation comments and moved relevant code closer together
* added a longer form comment about the origin policyId
* added logic for handling load error
* refactor assets accordions out of assets page component
* slightly larger text in badge
* added some basic jest test for view data step in enrollment flyout
* adjusted sizing of numbers in badges again, EuiText does not know about size="l"
* updated size limits for fleet
* updated styling and layout of assets accordion based on original
designs
* remove unused EuiTitle
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
packages/kbn-optimizer/limits.yml | 2 +-
.../plugins/fleet/common/types/models/epm.ts | 2 +-
.../create_package_policy_page/index.tsx | 91 ++++++++--
.../package_policies/no_package_policies.tsx | 12 +-
.../epm/screens/detail/assets/assets.tsx | 138 +++++++++++++++
.../detail/assets/assets_accordion.tsx | 92 ++++++++++
.../epm/screens/detail/assets/constants.ts | 16 ++
.../epm/screens/detail/assets/index.ts | 7 +
.../epm/screens/detail/assets/types.ts | 20 +++
.../sections/epm/screens/detail/index.tsx | 22 +++
.../detail/policies/package_policies.tsx | 73 ++++++--
.../agent_enrollment_flyout.test.mocks.ts | 1 +
.../agent_enrollment_flyout.test.tsx | 42 ++++-
.../agent_enrollment_flyout/index.tsx | 13 +-
.../managed_instructions.tsx | 167 ++++++++++--------
.../agent_enrollment_flyout/steps.tsx | 13 ++
.../agent_enrollment_flyout/types.ts | 12 +-
.../package_policy_actions_menu.tsx | 9 +-
.../fleet/public/constants/page_paths.ts | 13 +-
x-pack/plugins/fleet/public/hooks/index.ts | 2 +-
.../fleet/public/hooks/use_kibana_link.ts | 54 +++++-
21 files changed, 676 insertions(+), 125 deletions(-)
create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx
create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx
create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts
create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts
create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts
diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml
index 9adc075a7983f..f9127e4629f43 100644
--- a/packages/kbn-optimizer/limits.yml
+++ b/packages/kbn-optimizer/limits.yml
@@ -34,7 +34,7 @@ pageLoadAssetSize:
indexManagement: 140608
indexPatternManagement: 28222
infra: 184320
- fleet: 450005
+ fleet: 465774
ingestPipelines: 58003
inputControlVis: 172675
inspector: 148711
diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts
index 83875801300d3..aece658083196 100644
--- a/x-pack/plugins/fleet/common/types/models/epm.ts
+++ b/x-pack/plugins/fleet/common/types/models/epm.ts
@@ -43,7 +43,7 @@ export type InstallSource = 'registry' | 'upload';
export type EpmPackageInstallStatus = 'installed' | 'installing';
-export type DetailViewPanelName = 'overview' | 'policies' | 'settings' | 'custom';
+export type DetailViewPanelName = 'overview' | 'policies' | 'assets' | 'settings' | 'custom';
export type ServiceName = 'kibana' | 'elasticsearch';
export type AgentAssetType = typeof agentAssetTypes;
export type DocAssetType = 'doc' | 'notice';
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
index 75fc06c1a4494..b3b0d6ed51cb4 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
@@ -19,10 +19,12 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
+ EuiLink,
} from '@elastic/eui';
import type { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import type { ApplicationStart } from 'kibana/public';
+import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public';
import type {
AgentPolicy,
PackageInfo,
@@ -60,7 +62,7 @@ const StepsWithLessPadding = styled(EuiSteps)`
`;
const CustomEuiBottomBar = styled(EuiBottomBar)`
- // Set a relatively _low_ z-index value here to account for EuiComboBox popover that might appear under the bottom bar
+ /* A relatively _low_ z-index value here to account for EuiComboBox popover that might appear under the bottom bar */
z-index: 50;
`;
@@ -84,11 +86,26 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
const history = useHistory();
const handleNavigateTo = useNavigateToCallback();
const routeState = useIntraAppState();
- const from: CreatePackagePolicyFrom = 'policyId' in params ? 'policy' : 'package';
const { search } = useLocation();
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
- const policyId = useMemo(() => queryParams.get('policyId') ?? undefined, [queryParams]);
+ const queryParamsPolicyId = useMemo(() => queryParams.get('policyId') ?? undefined, [
+ queryParams,
+ ]);
+
+ /**
+ * Please note: policyId can come from one of two sources. The URL param (in the URL path) or
+ * in the query params (?policyId=foo).
+ *
+ * Either way, we take this as an indication that a user is "coming from" the fleet policy UI
+ * since we link them out to packages (a.k.a. integrations) UI when choosing a new package. It is
+ * no longer possible to choose a package directly in the create package form.
+ *
+ * We may want to deprecate the ability to pass in policyId from URL params since there is no package
+ * creation possible if a user has not chosen one from the packages UI.
+ */
+ const from: CreatePackagePolicyFrom =
+ 'policyId' in params || queryParamsPolicyId ? 'policy' : 'package';
// Agent policy and package info states
const [agentPolicy, setAgentPolicy] = useState();
@@ -280,6 +297,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
);
}
+ const fromPolicyWithoutAgentsAssigned = from === 'policy' && agentPolicy && agentCount === 0;
+
+ const fromPackageWithoutAgentsAssigned =
+ from === 'package' && packageInfo && agentPolicy && agentCount === 0;
+
+ const hasAgentsAssigned = agentCount && agentPolicy;
+
notifications.toasts.addSuccess({
title: i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationTitle', {
defaultMessage: `'{packagePolicyName}' integration added.`,
@@ -287,22 +311,47 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
packagePolicyName: packagePolicy.name,
},
}),
- text:
- agentCount && agentPolicy
- ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', {
- defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`,
- values: {
- agentPolicyName: agentPolicy.name,
- },
- })
- : (params as AddToPolicyParams)?.policyId && agentPolicy && agentCount === 0
- ? i18n.translate('xpack.fleet.createPackagePolicy.addAgentNextNotification', {
+ text: fromPolicyWithoutAgentsAssigned
+ ? i18n.translate(
+ 'xpack.fleet.createPackagePolicy.policyContextAddAgentNextNotificationMessage',
+ {
defaultMessage: `The policy has been updated. Add an agent to the '{agentPolicyName}' policy to deploy this policy.`,
values: {
- agentPolicyName: agentPolicy.name,
+ agentPolicyName: agentPolicy!.name,
},
- })
- : undefined,
+ }
+ )
+ : fromPackageWithoutAgentsAssigned
+ ? toMountPoint(
+ // To render the link below we need to mount this JSX in the success toast
+
+ {i18n.translate(
+ 'xpack.fleet.createPackagePolicy.integrationsContextAddAgentLinkMessage',
+ { defaultMessage: 'add an agent' }
+ )}
+
+ ),
+ }}
+ />
+ )
+ : hasAgentsAssigned
+ ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', {
+ defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`,
+ values: {
+ agentPolicyName: agentPolicy!.name,
+ },
+ })
+ : undefined,
'data-test-subj': 'packagePolicyCreateSuccessToast',
});
} else {
@@ -312,6 +361,9 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
setFormState('VALID');
}
}, [
+ getHref,
+ from,
+ packageInfo,
agentCount,
agentPolicy,
formState,
@@ -353,13 +405,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
),
- [params, updatePackageInfo, agentPolicy, updateAgentPolicy, policyId]
+ [params, updatePackageInfo, agentPolicy, updateAgentPolicy, queryParamsPolicyId]
);
const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create');
@@ -455,7 +507,8 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
)}
-
+
+
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx
index 54adbd78ab75a..39340a21d349b 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx
@@ -9,10 +9,11 @@ import React, { memo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
-import { useCapabilities, useLink } from '../../../../../hooks';
+import { useCapabilities, useStartServices } from '../../../../../hooks';
+import { pagePathGetters, INTEGRATIONS_PLUGIN_ID } from '../../../../../constants';
export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => {
- const { getHref } = useLink();
+ const { application } = useStartServices();
const hasWriteCapabilities = useCapabilities().write;
return (
@@ -36,7 +37,12 @@ export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => {
+ application.navigateToApp(INTEGRATIONS_PLUGIN_ID, {
+ path: `#${pagePathGetters.integrations_all()[1]}`,
+ state: { forAgentPolicyId: policyId },
+ })
+ }
>
{
+ const { name, version } = packageInfo;
+ const {
+ savedObjects: { client: savedObjectsClient },
+ } = useStartServices();
+
+ const { getPath } = useLink();
+ const getPackageInstallStatus = useGetPackageInstallStatus();
+ const packageInstallStatus = getPackageInstallStatus(packageInfo.name);
+
+ const [assetSavedObjects, setAssetsSavedObjects] = useState();
+ const [fetchError, setFetchError] = useState();
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchAssetSavedObjects = async () => {
+ if ('savedObject' in packageInfo) {
+ const {
+ savedObject: { attributes: packageAttributes },
+ } = packageInfo;
+
+ if (
+ !packageAttributes.installed_kibana ||
+ packageAttributes.installed_kibana.length === 0
+ ) {
+ setIsLoading(false);
+ return;
+ }
+
+ try {
+ const objectsToGet = packageAttributes.installed_kibana.map(({ id, type }) => ({
+ id,
+ type,
+ }));
+ const { savedObjects } = await savedObjectsClient.bulkGet(objectsToGet);
+ setAssetsSavedObjects(savedObjects as AssetSavedObject[]);
+ } catch (e) {
+ setFetchError(e);
+ } finally {
+ setIsLoading(false);
+ }
+ } else {
+ setIsLoading(false);
+ }
+ };
+ fetchAssetSavedObjects();
+ }, [savedObjectsClient, packageInfo]);
+
+ // if they arrive at this page and the package is not installed, send them to overview
+ // this happens if they arrive with a direct url or they uninstall while on this tab
+ if (packageInstallStatus.status !== InstallStatus.installed) {
+ return (
+
+ );
+ }
+
+ let content: JSX.Element | Array;
+
+ if (isLoading) {
+ content = ;
+ } else if (fetchError) {
+ content = (
+
+ }
+ error={fetchError}
+ />
+ );
+ } else if (assetSavedObjects === undefined) {
+ content = (
+
+
+
+
+
+ );
+ } else {
+ content = allowedAssetTypes.map((assetType) => {
+ const sectionAssetSavedObjects = assetSavedObjects.filter((so) => so.type === assetType);
+
+ if (!sectionAssetSavedObjects.length) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+ });
+ }
+
+ return (
+
+
+ {content}
+
+ );
+};
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx
new file mode 100644
index 0000000000000..abfdd88d27162
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets_accordion.tsx
@@ -0,0 +1,92 @@
+/*
+ * 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 type { FunctionComponent } from 'react';
+
+import {
+ EuiAccordion,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSplitPanel,
+ EuiSpacer,
+ EuiText,
+ EuiLink,
+ EuiHorizontalRule,
+ EuiNotificationBadge,
+} from '@elastic/eui';
+
+import { AssetTitleMap } from '../../../../../constants';
+
+import { getHrefToObjectInKibanaApp, useStartServices } from '../../../../../hooks';
+
+import type { AllowedAssetType, AssetSavedObject } from './types';
+
+interface Props {
+ type: AllowedAssetType;
+ savedObjects: AssetSavedObject[];
+}
+
+export const AssetsAccordion: FunctionComponent = ({ savedObjects, type }) => {
+ const { http } = useStartServices();
+ return (
+
+
+
+ {AssetTitleMap[type]}
+
+
+
+
+ {savedObjects.length}
+
+
+
+ }
+ id={type}
+ >
+ <>
+
+
+ {savedObjects.map(({ id, attributes: { title, description } }, idx) => {
+ const pathToObjectInApp = getHrefToObjectInKibanaApp({
+ http,
+ id,
+ type,
+ });
+ return (
+ <>
+
+
+
+ {pathToObjectInApp ? (
+ {title}
+ ) : (
+ title
+ )}
+
+
+ {description && (
+ <>
+
+
+ {description}
+
+ >
+ )}
+
+ {idx + 1 < savedObjects.length && }
+ >
+ );
+ })}
+
+ >
+
+ );
+};
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts
new file mode 100644
index 0000000000000..d6d88f7935eb4
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/constants.ts
@@ -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 { KibanaAssetType } from '../../../../../types';
+
+import type { AllowedAssetTypes } from './types';
+
+export const allowedAssetTypes: AllowedAssetTypes = [
+ KibanaAssetType.dashboard,
+ KibanaAssetType.search,
+ KibanaAssetType.visualization,
+];
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts
new file mode 100644
index 0000000000000..ceb030b7ce02e
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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.
+ */
+export { AssetsPage } from './assets';
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts
new file mode 100644
index 0000000000000..21efd1cd562e8
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/types.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 type { SimpleSavedObject } from 'src/core/public';
+
+import type { KibanaAssetType } from '../../../../../types';
+
+export type AssetSavedObject = SimpleSavedObject<{ title: string; description?: string }>;
+
+export type AllowedAssetTypes = [
+ KibanaAssetType.dashboard,
+ KibanaAssetType.search,
+ KibanaAssetType.visualization
+];
+
+export type AllowedAssetType = AllowedAssetTypes[number];
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
index 99a29a8194f9b..cf6007026afeb 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
@@ -56,6 +56,7 @@ import { WithHeaderLayout } from '../../../../layouts';
import { RELEASE_BADGE_DESCRIPTION, RELEASE_BADGE_LABEL } from '../../components/release_badge';
import { IntegrationAgentPolicyCount, UpdateIcon, IconPanel, LoadingIconPanel } from './components';
+import { AssetsPage } from './assets';
import { OverviewPage } from './overview';
import { PackagePoliciesPage } from './policies';
import { SettingsPage } from './settings';
@@ -408,6 +409,24 @@ export function Detail() {
});
}
+ if (packageInstallStatus === InstallStatus.installed && packageInfo.assets) {
+ tabs.push({
+ id: 'assets',
+ name: (
+
+ ),
+ isSelected: panel === 'assets',
+ 'data-test-subj': `tab-assets`,
+ href: getHref('integration_details_assets', {
+ pkgkey: packageInfoKey,
+ ...(integration ? { integration } : {}),
+ }),
+ });
+ }
+
tabs.push({
id: 'settings',
name: (
@@ -476,6 +495,9 @@ export function Detail() {
+
+
+
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
index 7da7328fdebbc..c672abeb1c903 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx
@@ -4,9 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
+import { stringify, parse } from 'query-string';
import React, { memo, useCallback, useMemo, useState } from 'react';
-import { Redirect } from 'react-router-dom';
+import { Redirect, useLocation, useHistory } from 'react-router-dom';
import type { CriteriaWithPagination, EuiTableFieldDataColumnType } from '@elastic/eui';
import {
EuiButtonIcon,
@@ -15,6 +15,9 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiToolTip,
+ EuiText,
+ EuiButton,
+ EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react';
@@ -66,8 +69,16 @@ interface PackagePoliciesPanelProps {
version: string;
}
export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps) => {
- const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState(null);
- const { getPath } = useLink();
+ const { search } = useLocation();
+ const history = useHistory();
+ const queryParams = useMemo(() => new URLSearchParams(search), [search]);
+ const agentPolicyIdFromParams = useMemo(() => queryParams.get('addAgentToPolicyId'), [
+ queryParams,
+ ]);
+ const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState(
+ agentPolicyIdFromParams
+ );
+ const { getPath, getHref } = useLink();
const getPackageInstallStatus = useGetPackageInstallStatus();
const packageInstallStatus = getPackageInstallStatus(name);
const { pagination, pageSizeOptions, setPagination } = useUrlPagination();
@@ -87,6 +98,36 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
[setPagination]
);
+ const renderViewDataStepContent = useCallback(
+ () => (
+ <>
+
+
+ {i18n.translate(
+ 'xpack.fleet.epm.agentEnrollment.viewDataDescription.pleaseNoteLabel',
+ { defaultMessage: 'Please note' }
+ )}
+
+ ),
+ }}
+ />
+
+
+
+ {i18n.translate('xpack.fleet.epm.agentEnrollment.viewDataAssetsLabel', {
+ defaultMessage: 'View assets',
+ })}
+
+ >
+ ),
+ [name, version, getHref]
+ );
+
const columns: Array> = useMemo(
() => [
{
@@ -186,12 +227,16 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
align: 'right',
render({ agentPolicy, packagePolicy }) {
return (
-
+
);
},
},
],
- []
+ [renderViewDataStepContent]
);
const noItemsMessage = useMemo(() => {
@@ -236,14 +281,18 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
/>
- {flyoutOpenForPolicyId && (
+ {flyoutOpenForPolicyId && !isLoading && (
setFlyoutOpenForPolicyId(null)}
- agentPolicies={
- data?.items
- .filter(({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId)
- .map(({ agentPolicy }) => agentPolicy) ?? []
+ onClose={() => {
+ setFlyoutOpenForPolicyId(null);
+ const { addAgentToPolicyId, ...rest } = parse(search);
+ history.replace({ search: stringify(rest) });
+ }}
+ agentPolicy={
+ data?.items.find(({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId)
+ ?.agentPolicy
}
+ viewDataStepContent={renderViewDataStepContent()}
/>
)}
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts
index f1055e7e2583e..fcf1078566498 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts
@@ -37,6 +37,7 @@ jest.mock('./steps', () => {
...module,
AgentPolicySelectionStep: jest.fn(),
AgentEnrollmentKeySelectionStep: jest.fn(),
+ ViewDataStep: jest.fn(),
};
});
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx
index db9245b11b0f9..65118044e98c5 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx
@@ -21,7 +21,7 @@ import { FleetStatusProvider, ConfigContext } from '../../hooks';
import { useFleetServerInstructions } from '../../applications/fleet/sections/agents/agent_requirements_page';
-import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep } from './steps';
+import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep, ViewDataStep } from './steps';
import type { Props } from '.';
import { AgentEnrollmentFlyout } from '.';
@@ -128,6 +128,46 @@ describe(' ', () => {
expect(AgentEnrollmentKeySelectionStep).toHaveBeenCalled();
});
});
+
+ describe('"View data" extension point', () => {
+ it('calls the "View data" step when UI extension is provided', async () => {
+ jest.clearAllMocks();
+ await act(async () => {
+ testBed = await setup({
+ agentPolicies: [],
+ onClose: jest.fn(),
+ viewDataStepContent:
,
+ });
+ testBed.component.update();
+ });
+ const { exists, actions } = testBed;
+ expect(exists('agentEnrollmentFlyout')).toBe(true);
+ expect(ViewDataStep).toHaveBeenCalled();
+
+ jest.clearAllMocks();
+ actions.goToStandaloneTab();
+ expect(ViewDataStep).not.toHaveBeenCalled();
+ });
+
+ it('does not call the "View data" step when UI extension is not provided', async () => {
+ jest.clearAllMocks();
+ await act(async () => {
+ testBed = await setup({
+ agentPolicies: [],
+ onClose: jest.fn(),
+ viewDataStepContent: undefined,
+ });
+ testBed.component.update();
+ });
+ const { exists, actions } = testBed;
+ expect(exists('agentEnrollmentFlyout')).toBe(true);
+ expect(ViewDataStep).not.toHaveBeenCalled();
+
+ jest.clearAllMocks();
+ actions.goToStandaloneTab();
+ expect(ViewDataStep).not.toHaveBeenCalled();
+ });
+ });
});
describe('standalone instructions', () => {
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx
index b91af80691033..58362d85e2fb3 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx
@@ -42,6 +42,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({
onClose,
agentPolicy,
agentPolicies,
+ viewDataStepContent,
}) => {
const [mode, setMode] = useState<'managed' | 'standalone'>('managed');
@@ -109,9 +110,17 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({
}
>
{fleetServerHosts.length === 0 && mode === 'managed' ? null : mode === 'managed' ? (
-
+
) : (
-
+
)}
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx
index e7045173f1257..919f0c3052db9 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx
@@ -21,7 +21,12 @@ import {
useFleetServerInstructions,
} from '../../applications/fleet/sections/agents/agent_requirements_page';
-import { DownloadStep, AgentPolicySelectionStep, AgentEnrollmentKeySelectionStep } from './steps';
+import {
+ DownloadStep,
+ AgentPolicySelectionStep,
+ AgentEnrollmentKeySelectionStep,
+ ViewDataStep,
+} from './steps';
import type { BaseProps } from './types';
type Props = BaseProps;
@@ -53,83 +58,91 @@ const FleetServerMissingRequirements = () => {
return ;
};
-export const ManagedInstructions = React.memo(({ agentPolicy, agentPolicies }) => {
- const fleetStatus = useFleetStatus();
+export const ManagedInstructions = React.memo(
+ ({ agentPolicy, agentPolicies, viewDataStepContent }) => {
+ const fleetStatus = useFleetStatus();
- const [selectedAPIKeyId, setSelectedAPIKeyId] = useState();
- const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState(false);
+ const [selectedAPIKeyId, setSelectedAPIKeyId] = useState();
+ const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState(false);
- const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId);
- const settings = useGetSettings();
- const fleetServerInstructions = useFleetServerInstructions(apiKey?.data?.item?.policy_id);
+ const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId);
+ const settings = useGetSettings();
+ const fleetServerInstructions = useFleetServerInstructions(apiKey?.data?.item?.policy_id);
- const steps = useMemo(() => {
- const {
- serviceToken,
- getServiceToken,
- isLoadingServiceToken,
- installCommand,
- platform,
- setPlatform,
- } = fleetServerInstructions;
- const fleetServerHosts = settings.data?.item?.fleet_server_hosts || [];
- const baseSteps: EuiContainedStepProps[] = [
- DownloadStep(),
- !agentPolicy
- ? AgentPolicySelectionStep({
- agentPolicies,
- setSelectedAPIKeyId,
- setIsFleetServerPolicySelected,
- })
- : AgentEnrollmentKeySelectionStep({ agentPolicy, setSelectedAPIKeyId }),
- ];
- if (isFleetServerPolicySelected) {
- baseSteps.push(
- ...[
- ServiceTokenStep({ serviceToken, getServiceToken, isLoadingServiceToken }),
- FleetServerCommandStep({ serviceToken, installCommand, platform, setPlatform }),
- ]
- );
- } else {
- baseSteps.push({
- title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', {
- defaultMessage: 'Enroll and start the Elastic Agent',
- }),
- children: selectedAPIKeyId && apiKey.data && (
-
- ),
- });
- }
- return baseSteps;
- }, [
- agentPolicy,
- agentPolicies,
- selectedAPIKeyId,
- apiKey.data,
- isFleetServerPolicySelected,
- settings.data?.item?.fleet_server_hosts,
- fleetServerInstructions,
- ]);
+ const steps = useMemo(() => {
+ const {
+ serviceToken,
+ getServiceToken,
+ isLoadingServiceToken,
+ installCommand,
+ platform,
+ setPlatform,
+ } = fleetServerInstructions;
+ const fleetServerHosts = settings.data?.item?.fleet_server_hosts || [];
+ const baseSteps: EuiContainedStepProps[] = [
+ DownloadStep(),
+ !agentPolicy
+ ? AgentPolicySelectionStep({
+ agentPolicies,
+ setSelectedAPIKeyId,
+ setIsFleetServerPolicySelected,
+ })
+ : AgentEnrollmentKeySelectionStep({ agentPolicy, setSelectedAPIKeyId }),
+ ];
+ if (isFleetServerPolicySelected) {
+ baseSteps.push(
+ ...[
+ ServiceTokenStep({ serviceToken, getServiceToken, isLoadingServiceToken }),
+ FleetServerCommandStep({ serviceToken, installCommand, platform, setPlatform }),
+ ]
+ );
+ } else {
+ baseSteps.push({
+ title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', {
+ defaultMessage: 'Enroll and start the Elastic Agent',
+ }),
+ children: selectedAPIKeyId && apiKey.data && (
+
+ ),
+ });
+ }
- return (
- <>
- {fleetStatus.isReady ? (
- <>
-
-
-
-
-
- >
- ) : fleetStatus.missingRequirements?.length === 1 &&
- fleetStatus.missingRequirements[0] === 'fleet_server' ? (
-
- ) : (
-
- )}
- >
- );
-});
+ if (viewDataStepContent) {
+ baseSteps.push(ViewDataStep(viewDataStepContent));
+ }
+
+ return baseSteps;
+ }, [
+ agentPolicy,
+ agentPolicies,
+ selectedAPIKeyId,
+ apiKey.data,
+ isFleetServerPolicySelected,
+ settings.data?.item?.fleet_server_hosts,
+ fleetServerInstructions,
+ viewDataStepContent,
+ ]);
+
+ return (
+ <>
+ {fleetStatus.isReady ? (
+ <>
+
+
+
+
+
+ >
+ ) : fleetStatus.missingRequirements?.length === 1 &&
+ fleetStatus.missingRequirements[0] === 'fleet_server' ? (
+
+ ) : (
+
+ )}
+ >
+ );
+ }
+);
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx
index ea4fa626afbb6..03cff88e63969 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx
@@ -138,3 +138,16 @@ export const AgentEnrollmentKeySelectionStep = ({
),
};
};
+
+/**
+ * Send users to assets installed by the package in Kibana so they can
+ * view their data.
+ */
+export const ViewDataStep = (content: JSX.Element) => {
+ return {
+ title: i18n.translate('xpack.fleet.agentEnrollment.stepViewDataTitle', {
+ defaultMessage: 'View your data',
+ }),
+ children: content,
+ };
+};
diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts
index b9bcf8fb3e4b2..e0c5b040a61fb 100644
--- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts
+++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts
@@ -9,12 +9,20 @@ import type { AgentPolicy } from '../../types';
export interface BaseProps {
/**
- * The user selected policy to be used
+ * The user selected policy to be used. If this value is `undefined` a value must be provided for `agentPolicies`.
*/
agentPolicy?: AgentPolicy;
/**
- * A selection of policies for the user to choose from, will be ignored if `agentPolicy` has been provided
+ * A selection of policies for the user to choose from, will be ignored if `agentPolicy` has been provided.
+ *
+ * If this value is `undefined` a value must be provided for `agentPolicy`.
*/
agentPolicies?: AgentPolicy[];
+
+ /**
+ * There is a step in the agent enrollment process that allows users to see the data from an integration represented in the UI
+ * in some way. This is an area for consumers to render a button and text explaining how data can be viewed.
+ */
+ viewDataStepContent?: JSX.Element;
}
diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx
index 03bf2095f7f3e..1f64de27fce39 100644
--- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx
+++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx
@@ -21,7 +21,8 @@ import { PackagePolicyDeleteProvider } from './package_policy_delete_provider';
export const PackagePolicyActionsMenu: React.FunctionComponent<{
agentPolicy: AgentPolicy;
packagePolicy: PackagePolicy;
-}> = ({ agentPolicy, packagePolicy }) => {
+ viewDataStepContent?: JSX.Element;
+}> = ({ agentPolicy, packagePolicy, viewDataStepContent }) => {
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false);
const { getHref } = useLink();
const hasWriteCapabilities = useCapabilities().write;
@@ -103,7 +104,11 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
<>
{isEnrollmentFlyoutOpen && (
-
+
)}
diff --git a/x-pack/plugins/fleet/public/constants/page_paths.ts b/x-pack/plugins/fleet/public/constants/page_paths.ts
index 326cfd804bd57..1688a396cd5a1 100644
--- a/x-pack/plugins/fleet/public/constants/page_paths.ts
+++ b/x-pack/plugins/fleet/public/constants/page_paths.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { stringify } from 'query-string';
+
export type StaticPage =
| 'base'
| 'overview'
@@ -19,6 +21,7 @@ export type StaticPage =
export type DynamicPage =
| 'integration_details_overview'
| 'integration_details_policies'
+ | 'integration_details_assets'
| 'integration_details_settings'
| 'integration_details_custom'
| 'integration_policy_edit'
@@ -66,6 +69,7 @@ export const INTEGRATIONS_ROUTING_PATHS = {
integration_details: '/detail/:pkgkey/:panel?',
integration_details_overview: '/detail/:pkgkey/overview',
integration_details_policies: '/detail/:pkgkey/policies',
+ integration_details_assets: '/detail/:pkgkey/assets',
integration_details_settings: '/detail/:pkgkey/settings',
integration_details_custom: '/detail/:pkgkey/custom',
integration_policy_edit: '/edit-integration/:packagePolicyId',
@@ -86,9 +90,13 @@ export const pagePathGetters: {
INTEGRATIONS_BASE_PATH,
`/detail/${pkgkey}/overview${integration ? `?integration=${integration}` : ''}`,
],
- integration_details_policies: ({ pkgkey, integration }) => [
+ integration_details_policies: ({ pkgkey, integration, addAgentToPolicyId }) => {
+ const qs = stringify({ integration, addAgentToPolicyId });
+ return [INTEGRATIONS_BASE_PATH, `/detail/${pkgkey}/policies${qs ? `?${qs}` : ''}`];
+ },
+ integration_details_assets: ({ pkgkey, integration }) => [
INTEGRATIONS_BASE_PATH,
- `/detail/${pkgkey}/policies${integration ? `?integration=${integration}` : ''}`,
+ `/detail/${pkgkey}/assets${integration ? `?integration=${integration}` : ''}`,
],
integration_details_settings: ({ pkgkey, integration }) => [
INTEGRATIONS_BASE_PATH,
@@ -108,6 +116,7 @@ export const pagePathGetters: {
FLEET_BASE_PATH,
`/policies/${policyId}${tabId ? `/${tabId}` : ''}`,
],
+ // TODO: This might need to be removed because we do not have a way to pick an integration in line anymore
add_integration_from_policy: ({ policyId }) => [
FLEET_BASE_PATH,
`/policies/${policyId}/add-integration`,
diff --git a/x-pack/plugins/fleet/public/hooks/index.ts b/x-pack/plugins/fleet/public/hooks/index.ts
index 9f41e5c7cc92b..a00c0c5dacf11 100644
--- a/x-pack/plugins/fleet/public/hooks/index.ts
+++ b/x-pack/plugins/fleet/public/hooks/index.ts
@@ -11,7 +11,7 @@ export { useConfig, ConfigContext } from './use_config';
export { useKibanaVersion, KibanaVersionContext } from './use_kibana_version';
export { licenseService, useLicense } from './use_license';
export { useLink } from './use_link';
-export { useKibanaLink } from './use_kibana_link';
+export { useKibanaLink, getHrefToObjectInKibanaApp } from './use_kibana_link';
export { usePackageIconType, UsePackageIconType } from './use_package_icon_type';
export { usePagination, Pagination, PAGE_SIZE_OPTIONS } from './use_pagination';
export { useUrlPagination } from './use_url_pagination';
diff --git a/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts b/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts
index 29f4f8748d1a0..3ad01620b9780 100644
--- a/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts
@@ -4,12 +4,62 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import type { HttpStart } from 'src/core/public';
+
+import { KibanaAssetType } from '../types';
import { useStartServices } from './';
const KIBANA_BASE_PATH = '/app/kibana';
+const getKibanaLink = (http: HttpStart, path: string) => {
+ return http.basePath.prepend(`${KIBANA_BASE_PATH}#${path}`);
+};
+
+/**
+ * TODO: This is a temporary solution for getting links to various assets. It is very risky because:
+ *
+ * 1. The plugin might not exist/be enabled
+ * 2. URLs and paths might not always be supported
+ *
+ * We should migrate to using the new URL service locators.
+ *
+ * @deprecated {@link Locators} from the new URL service need to be used instead.
+
+ */
+export const getHrefToObjectInKibanaApp = ({
+ type,
+ id,
+ http,
+}: {
+ type: KibanaAssetType;
+ id: string;
+ http: HttpStart;
+}): undefined | string => {
+ let kibanaAppPath: undefined | string;
+ switch (type) {
+ case KibanaAssetType.dashboard:
+ kibanaAppPath = `/dashboard/${id}`;
+ break;
+ case KibanaAssetType.search:
+ kibanaAppPath = `/discover/${id}`;
+ break;
+ case KibanaAssetType.visualization:
+ kibanaAppPath = `/visualize/edit/${id}`;
+ break;
+ default:
+ return undefined;
+ }
+
+ return getKibanaLink(http, kibanaAppPath);
+};
+
+/**
+ * TODO: This functionality needs to be replaced with use of the new URL service locators
+ *
+ * @deprecated {@link Locators} from the new URL service need to be used instead.
+ */
export function useKibanaLink(path: string = '/') {
- const core = useStartServices();
- return core.http.basePath.prepend(`${KIBANA_BASE_PATH}#${path}`);
+ const { http } = useStartServices();
+ return getKibanaLink(http, path);
}
From c940da4bd0ecd5dd3c5d8d6f4f6b3aa259633727 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20S=C3=A1nchez?=
Date: Tue, 22 Jun 2021 17:25:41 +0200
Subject: [PATCH 071/163] Wraps query in parentheses to avoid quering exception
lists (#102612)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../public/management/common/utils.test.ts | 8 ++++----
.../security_solution/public/management/common/utils.ts | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/management/common/utils.test.ts b/x-pack/plugins/security_solution/public/management/common/utils.test.ts
index 59455ccd6bb04..30354c141f833 100644
--- a/x-pack/plugins/security_solution/public/management/common/utils.test.ts
+++ b/x-pack/plugins/security_solution/public/management/common/utils.test.ts
@@ -15,17 +15,17 @@ describe('utils', () => {
});
it('should parse simple query with term', () => {
expect(parseQueryFilterToKQL('simpleQuery', searchableFields)).toBe(
- 'exception-list-agnostic.attributes.name:(*simpleQuery*) OR exception-list-agnostic.attributes.description:(*simpleQuery*) OR exception-list-agnostic.attributes.entries.value:(*simpleQuery*) OR exception-list-agnostic.attributes.entries.entries.value:(*simpleQuery*)'
+ '(exception-list-agnostic.attributes.name:(*simpleQuery*) OR exception-list-agnostic.attributes.description:(*simpleQuery*) OR exception-list-agnostic.attributes.entries.value:(*simpleQuery*) OR exception-list-agnostic.attributes.entries.entries.value:(*simpleQuery*))'
);
});
it('should parse complex query with term', () => {
expect(parseQueryFilterToKQL('complex query', searchableFields)).toBe(
- 'exception-list-agnostic.attributes.name:(*complex*query*) OR exception-list-agnostic.attributes.description:(*complex*query*) OR exception-list-agnostic.attributes.entries.value:(*complex*query*) OR exception-list-agnostic.attributes.entries.entries.value:(*complex*query*)'
+ '(exception-list-agnostic.attributes.name:(*complex*query*) OR exception-list-agnostic.attributes.description:(*complex*query*) OR exception-list-agnostic.attributes.entries.value:(*complex*query*) OR exception-list-agnostic.attributes.entries.entries.value:(*complex*query*))'
);
});
it('should parse complex query with colon and backslash chars term', () => {
expect(parseQueryFilterToKQL('C:\\tmpes', searchableFields)).toBe(
- 'exception-list-agnostic.attributes.name:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.description:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.entries.value:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.entries.entries.value:(*C\\:\\\\tmpes*)'
+ '(exception-list-agnostic.attributes.name:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.description:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.entries.value:(*C\\:\\\\tmpes*) OR exception-list-agnostic.attributes.entries.entries.value:(*C\\:\\\\tmpes*))'
);
});
it('should parse complex query with special chars term', () => {
@@ -35,7 +35,7 @@ describe('utils', () => {
searchableFields
)
).toBe(
- "exception-list-agnostic.attributes.name:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.description:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.entries.value:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.entries.entries.value:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*)"
+ "(exception-list-agnostic.attributes.name:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.description:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.entries.value:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*) OR exception-list-agnostic.attributes.entries.entries.value:(*this'is%&query\\{\\}[]!¿?with.,-+`´special\\<\\>ºª@#|·chars*))"
);
});
});
diff --git a/x-pack/plugins/security_solution/public/management/common/utils.ts b/x-pack/plugins/security_solution/public/management/common/utils.ts
index c8cf761ccaf86..616e395c8ad47 100644
--- a/x-pack/plugins/security_solution/public/management/common/utils.ts
+++ b/x-pack/plugins/security_solution/public/management/common/utils.ts
@@ -17,5 +17,5 @@ export const parseQueryFilterToKQL = (filter: string, fields: Readonly
)
.join(' OR ');
- return kuery;
+ return `(${kuery})`;
};
From fd0c1fa490c2e4eab5a3cbc96c8f3703cc4962f6 Mon Sep 17 00:00:00 2001
From: Andrew Stucki
Date: Tue, 22 Jun 2021 11:30:32 -0400
Subject: [PATCH 072/163] [Agent Packages] Extend 'contains' helper to work on
strings (#102786)
* Extend 'contains' helper to work on strings
* remove stray import
---
.../server/services/epm/agent/agent.test.ts | 29 +++++++++++++++++++
.../fleet/server/services/epm/agent/agent.ts | 5 ++--
2 files changed, 32 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts
index bc4ffffb68358..1be0f73a347e9 100644
--- a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts
@@ -129,6 +129,13 @@ processors:
password: {{password}}
{{#if password}}
hidden_password: {{password}}
+{{/if}}
+ `;
+ const streamTemplateWithString = `
+{{#if (contains ".pcap" file)}}
+pcap: true
+{{else}}
+pcap: false
{{/if}}
`;
@@ -168,6 +175,28 @@ hidden_password: {{password}}
tags: ['foo', 'bar'],
});
});
+
+ it('should support strings', () => {
+ const vars = {
+ file: { value: 'foo.pcap' },
+ };
+
+ const output = compileTemplate(vars, streamTemplateWithString);
+ expect(output).toEqual({
+ pcap: true,
+ });
+ });
+
+ it('should support strings with no match', () => {
+ const vars = {
+ file: { value: 'file' },
+ };
+
+ const output = compileTemplate(vars, streamTemplateWithString);
+ expect(output).toEqual({
+ pcap: false,
+ });
+ });
});
it('should support optional yaml values at root level', () => {
diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts
index 84a8ab581354a..a0d14e6962a8d 100644
--- a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts
+++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts
@@ -111,11 +111,12 @@ function buildTemplateVariables(variables: PackagePolicyConfigRecord, templateSt
return { vars, yamlValues };
}
-function containsHelper(this: any, item: string, list: string[], options: any) {
- if (Array.isArray(list) && list.includes(item)) {
+function containsHelper(this: any, item: string, check: string | string[], options: any) {
+ if ((Array.isArray(check) || typeof check === 'string') && check.includes(item)) {
if (options && options.fn) {
return options.fn(this);
}
+ return true;
}
return '';
}
From 00a9f8495152b7fb0d7284e963f86da11d6aee5f Mon Sep 17 00:00:00 2001
From: Constance
Date: Tue, 22 Jun 2021 08:40:58 -0700
Subject: [PATCH 073/163] [App Search] Convert Analytics views to new page
template (#102851)
* Convert AnalyticsHeader to AnalyticsFilters
- it's basically the same component as before, but without the title section/log retention tooltip, since the header/title will be handled by the new page template
* Update AnalyticsLayout to use new page template
+ add new test_helper for header children
* Update breadcrumb behavior
- Set analytic breadcrumbs in AnalyticsLayout rather than AnalyticsRouter
- Update individual views to pass breadcrumbs (consistent with new page template API)
* Update router
---
.../analytics/analytics_layout.test.tsx | 28 ++--
.../components/analytics/analytics_layout.tsx | 34 +++--
.../components/analytics/analytics_router.tsx | 25 +---
...er.test.tsx => analytics_filters.test.tsx} | 24 ++--
.../components/analytics_filters.tsx | 111 ++++++++++++++
.../components/analytics_header.scss | 15 --
.../analytics/components/analytics_header.tsx | 136 ------------------
.../components/analytics/components/index.ts | 2 +-
.../analytics/views/query_detail.test.tsx | 16 +--
.../analytics/views/query_detail.tsx | 11 +-
.../analytics/views/recent_queries.tsx | 2 +-
.../analytics/views/top_queries.tsx | 2 +-
.../analytics/views/top_queries_no_clicks.tsx | 6 +-
.../views/top_queries_no_results.tsx | 6 +-
.../views/top_queries_with_clicks.tsx | 6 +-
.../components/engine/engine_router.tsx | 10 +-
.../test_helpers/get_page_header.tsx | 6 +
.../public/applications/test_helpers/index.ts | 1 +
18 files changed, 199 insertions(+), 242 deletions(-)
rename x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/{analytics_header.test.tsx => analytics_filters.test.tsx} (87%)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.tsx
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.scss
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.test.tsx
index 9832915f19e9e..280282a2fc6ec 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.test.tsx
@@ -8,18 +8,22 @@
import '../../../__mocks__/shallow_useeffect.mock';
import { mockKibanaValues, setMockValues, setMockActions } from '../../../__mocks__/kea_logic';
import { mockUseParams } from '../../../__mocks__/react_router';
+import '../../__mocks__/engine_logic.mock';
import React from 'react';
import { shallow } from 'enzyme';
-import { FlashMessages } from '../../../shared/flash_messages';
-import { Loading } from '../../../shared/loading';
-import { rerender } from '../../../test_helpers';
-import { LogRetentionCallout } from '../log_retention';
+import {
+ rerender,
+ getPageTitle,
+ getPageHeaderActions,
+ getPageHeaderChildren,
+} from '../../../test_helpers';
+import { LogRetentionTooltip, LogRetentionCallout } from '../log_retention';
import { AnalyticsLayout } from './analytics_layout';
-import { AnalyticsHeader } from './components';
+import { AnalyticsFilters } from './components';
describe('AnalyticsLayout', () => {
const { history } = mockKibanaValues;
@@ -47,18 +51,20 @@ describe('AnalyticsLayout', () => {
);
- expect(wrapper.find(FlashMessages)).toHaveLength(1);
expect(wrapper.find(LogRetentionCallout)).toHaveLength(1);
+ expect(getPageHeaderActions(wrapper).find(LogRetentionTooltip)).toHaveLength(1);
+ expect(getPageHeaderChildren(wrapper).find(AnalyticsFilters)).toHaveLength(1);
- expect(wrapper.find(AnalyticsHeader).prop('title')).toEqual('Hello');
+ expect(getPageTitle(wrapper)).toEqual('Hello');
expect(wrapper.find('[data-test-subj="world"]').text()).toEqual('World!');
+
+ expect(wrapper.prop('pageChrome')).toEqual(['Engines', 'some-engine', 'Analytics']);
});
- it('renders a loading component if data is not done loading', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow( );
+ it('passes analytics breadcrumbs', () => {
+ const wrapper = shallow( );
- expect(wrapper.type()).toEqual(Loading);
+ expect(wrapper.prop('pageChrome')).toEqual(['Engines', 'some-engine', 'Analytics', 'Queries']);
});
describe('data loading', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx
index 91de4cc498988..0923f9497a8fe 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx
@@ -10,25 +10,27 @@ import { useParams } from 'react-router-dom';
import { useValues, useActions } from 'kea';
-import { EuiSpacer } from '@elastic/eui';
-
-import { FlashMessages } from '../../../shared/flash_messages';
import { KibanaLogic } from '../../../shared/kibana';
-import { Loading } from '../../../shared/loading';
+import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs';
+import { getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
-import { LogRetentionCallout, LogRetentionOptions } from '../log_retention';
+import { LogRetentionTooltip, LogRetentionCallout, LogRetentionOptions } from '../log_retention';
-import { AnalyticsHeader } from './components';
+import { AnalyticsFilters } from './components';
+import { ANALYTICS_TITLE } from './constants';
import { AnalyticsLogic } from './';
interface Props {
title: string;
+ breadcrumbs?: BreadcrumbTrail;
isQueryView?: boolean;
isAnalyticsView?: boolean;
}
export const AnalyticsLayout: React.FC = ({
title,
+ breadcrumbs = [],
isQueryView,
isAnalyticsView,
children,
@@ -43,15 +45,21 @@ export const AnalyticsLayout: React.FC = ({
if (isAnalyticsView) loadAnalyticsData();
}, [history.location.search]);
- if (dataLoading) return ;
-
return (
- <>
-
-
+ ,
+ ],
+ children: ,
+ responsive: false,
+ }}
+ isLoading={dataLoading}
+ >
{children}
-
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx
index 397f1f1e1e1c3..d56fe949431c3 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx
@@ -9,7 +9,6 @@ import React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { APP_SEARCH_PLUGIN } from '../../../../../common/constants';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { NotFound } from '../../../shared/not_found';
import {
ENGINE_ANALYTICS_PATH,
@@ -23,14 +22,7 @@ import {
} from '../../routes';
import { generateEnginePath, getEngineBreadcrumbs } from '../engine';
-import {
- ANALYTICS_TITLE,
- TOP_QUERIES,
- TOP_QUERIES_NO_RESULTS,
- TOP_QUERIES_NO_CLICKS,
- TOP_QUERIES_WITH_CLICKS,
- RECENT_QUERIES,
-} from './constants';
+import { ANALYTICS_TITLE } from './constants';
import {
Analytics,
TopQueries,
@@ -42,42 +34,37 @@ import {
} from './views';
export const AnalyticsRouter: React.FC = () => {
- const ANALYTICS_BREADCRUMB = getEngineBreadcrumbs([ANALYTICS_TITLE]);
-
return (
-
-
-
-
-
-
-
+
-
+
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.test.tsx
similarity index 87%
rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.test.tsx
rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.test.tsx
index 5269ea9110065..7abb02110e2d9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.test.tsx
@@ -12,15 +12,13 @@ import React, { ReactElement } from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import moment, { Moment } from 'moment';
-import { EuiPageHeader, EuiSelect, EuiDatePickerRange, EuiButton } from '@elastic/eui';
-
-import { LogRetentionTooltip } from '../../log_retention';
+import { EuiSelect, EuiDatePickerRange, EuiButton } from '@elastic/eui';
import { DEFAULT_START_DATE, DEFAULT_END_DATE } from '../constants';
-import { AnalyticsHeader } from './';
+import { AnalyticsFilters } from './';
-describe('AnalyticsHeader', () => {
+describe('AnalyticsFilters', () => {
const { history } = mockKibanaValues;
const values = {
@@ -45,18 +43,14 @@ describe('AnalyticsHeader', () => {
});
it('renders', () => {
- wrapper = shallow( );
-
- expect(wrapper.type()).toEqual(EuiPageHeader);
- expect(wrapper.find('h1').text()).toEqual('Hello world');
+ wrapper = shallow( );
- expect(wrapper.find(LogRetentionTooltip)).toHaveLength(1);
expect(wrapper.find(EuiSelect)).toHaveLength(1);
expect(wrapper.find(EuiDatePickerRange)).toHaveLength(1);
});
it('renders tags & dates with default values when no search query params are present', () => {
- wrapper = shallow( );
+ wrapper = shallow( );
expect(getTagsSelect().prop('value')).toEqual('');
expect(getStartDatePicker().props.startDate._i).toEqual(DEFAULT_START_DATE);
@@ -69,7 +63,7 @@ describe('AnalyticsHeader', () => {
const allTags = [...values.allTags, 'tag1', 'tag2', 'tag3'];
setMockValues({ ...values, allTags });
- wrapper = shallow( );
+ wrapper = shallow( );
});
it('renders the tags select with currentTag value and allTags options', () => {
@@ -95,7 +89,7 @@ describe('AnalyticsHeader', () => {
beforeEach(() => {
history.location.search = '?start=1970-01-01&end=1970-01-02';
- wrapper = shallow( );
+ wrapper = shallow( );
});
it('renders the start date picker', () => {
@@ -127,7 +121,7 @@ describe('AnalyticsHeader', () => {
beforeEach(() => {
history.location.search = '?start=1970-01-02&end=1970-01-01';
- wrapper = shallow( );
+ wrapper = shallow( );
});
it('renders the date pickers as invalid', () => {
@@ -148,7 +142,7 @@ describe('AnalyticsHeader', () => {
};
beforeEach(() => {
- wrapper = shallow( );
+ wrapper = shallow( );
});
it('pushes up new tag & date state to the search query', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.tsx
new file mode 100644
index 0000000000000..0c8455e986ae1
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.tsx
@@ -0,0 +1,111 @@
+/*
+ * 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, { useState } from 'react';
+
+import { useValues } from 'kea';
+import moment from 'moment';
+import queryString from 'query-string';
+
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSelect,
+ EuiDatePickerRange,
+ EuiDatePicker,
+ EuiButton,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { AnalyticsLogic } from '../';
+import { KibanaLogic } from '../../../../shared/kibana';
+
+import { DEFAULT_START_DATE, DEFAULT_END_DATE, SERVER_DATE_FORMAT } from '../constants';
+import { convertTagsToSelectOptions } from '../utils';
+
+export const AnalyticsFilters: React.FC = () => {
+ const { allTags } = useValues(AnalyticsLogic);
+ const { history } = useValues(KibanaLogic);
+
+ // Parse out existing filters from URL query string
+ const { start, end, tag } = queryString.parse(history.location.search);
+ const [startDate, setStartDate] = useState(
+ start ? moment(start, SERVER_DATE_FORMAT) : moment(DEFAULT_START_DATE)
+ );
+ const [endDate, setEndDate] = useState(
+ end ? moment(end, SERVER_DATE_FORMAT) : moment(DEFAULT_END_DATE)
+ );
+ const [currentTag, setCurrentTag] = useState((tag as string) || '');
+
+ // Set the current URL query string on filter
+ const onApplyFilters = () => {
+ const search = queryString.stringify({
+ start: moment(startDate).format(SERVER_DATE_FORMAT),
+ end: moment(endDate).format(SERVER_DATE_FORMAT),
+ tag: currentTag || undefined,
+ });
+ history.push({ search });
+ };
+
+ const hasInvalidDateRange = startDate > endDate;
+
+ return (
+
+
+ setCurrentTag(e.target.value)}
+ aria-label={i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.tagAriaLabel',
+ { defaultMessage: 'Filter by analytics tag"' }
+ )}
+ fullWidth
+ />
+
+
+ date && setStartDate(date)}
+ startDate={startDate}
+ endDate={endDate}
+ isInvalid={hasInvalidDateRange}
+ aria-label={i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.startDateAriaLabel',
+ { defaultMessage: 'Filter by start date' }
+ )}
+ />
+ }
+ endDateControl={
+ date && setEndDate(date)}
+ startDate={startDate}
+ endDate={endDate}
+ isInvalid={hasInvalidDateRange}
+ aria-label={i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.endDateAriaLabel',
+ { defaultMessage: 'Filter by end date' }
+ )}
+ />
+ }
+ fullWidth
+ />
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.applyButtonLabel',
+ { defaultMessage: 'Apply filters' }
+ )}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.scss
deleted file mode 100644
index abe6c0e0789a8..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.scss
+++ /dev/null
@@ -1,15 +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.
- */
-
-.analyticsHeader {
- flex-wrap: wrap;
-
- &__filters.euiPageHeaderSection {
- width: 100%;
- margin: $euiSizeM 0;
- }
-}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx
deleted file mode 100644
index 8a87a5e8c211c..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx
+++ /dev/null
@@ -1,136 +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 React, { useState } from 'react';
-
-import { useValues } from 'kea';
-import moment from 'moment';
-import queryString from 'query-string';
-
-import {
- EuiPageHeader,
- EuiPageHeaderSection,
- EuiTitle,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSelect,
- EuiDatePickerRange,
- EuiDatePicker,
- EuiButton,
-} from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-
-import { AnalyticsLogic } from '../';
-import { KibanaLogic } from '../../../../shared/kibana';
-import { LogRetentionTooltip, LogRetentionOptions } from '../../log_retention';
-
-import { DEFAULT_START_DATE, DEFAULT_END_DATE, SERVER_DATE_FORMAT } from '../constants';
-import { convertTagsToSelectOptions } from '../utils';
-
-import './analytics_header.scss';
-
-interface Props {
- title: string;
-}
-export const AnalyticsHeader: React.FC = ({ title }) => {
- const { allTags } = useValues(AnalyticsLogic);
- const { history } = useValues(KibanaLogic);
-
- // Parse out existing filters from URL query string
- const { start, end, tag } = queryString.parse(history.location.search);
- const [startDate, setStartDate] = useState(
- start ? moment(start, SERVER_DATE_FORMAT) : moment(DEFAULT_START_DATE)
- );
- const [endDate, setEndDate] = useState(
- end ? moment(end, SERVER_DATE_FORMAT) : moment(DEFAULT_END_DATE)
- );
- const [currentTag, setCurrentTag] = useState((tag as string) || '');
-
- // Set the current URL query string on filter
- const onApplyFilters = () => {
- const search = queryString.stringify({
- start: moment(startDate).format(SERVER_DATE_FORMAT),
- end: moment(endDate).format(SERVER_DATE_FORMAT),
- tag: currentTag || undefined,
- });
- history.push({ search });
- };
-
- const hasInvalidDateRange = startDate > endDate;
-
- return (
-
-
-
-
-
- {title}
-
-
-
-
-
-
-
-
-
-
- setCurrentTag(e.target.value)}
- aria-label={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.tagAriaLabel',
- { defaultMessage: 'Filter by analytics tag"' }
- )}
- fullWidth
- />
-
-
- date && setStartDate(date)}
- startDate={startDate}
- endDate={endDate}
- isInvalid={hasInvalidDateRange}
- aria-label={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.startDateAriaLabel',
- { defaultMessage: 'Filter by start date' }
- )}
- />
- }
- endDateControl={
- date && setEndDate(date)}
- startDate={startDate}
- endDate={endDate}
- isInvalid={hasInvalidDateRange}
- aria-label={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.endDateAriaLabel',
- { defaultMessage: 'Filter by end date' }
- )}
- />
- }
- fullWidth
- />
-
-
-
- {i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.applyButtonLabel',
- { defaultMessage: 'Apply filters' }
- )}
-
-
-
-
-
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts
index de5c8209d2347..5309681b80d6d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/index.ts
@@ -7,7 +7,7 @@
export { AnalyticsCards } from './analytics_cards';
export { AnalyticsChart } from './analytics_chart';
-export { AnalyticsHeader } from './analytics_header';
+export { AnalyticsFilters } from './analytics_filters';
export { AnalyticsSection } from './analytics_section';
export { AnalyticsSearch } from './analytics_search';
export { AnalyticsTable, RecentQueriesTable, QueryClicksTable } from './analytics_tables';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx
index a942918fa9c62..f3fee2553d2fd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx
@@ -12,16 +12,12 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
-
import { AnalyticsLayout } from '../analytics_layout';
import { AnalyticsCards, AnalyticsChart, QueryClicksTable } from '../components';
import { QueryDetail } from './';
describe('QueryDetail', () => {
- const mockBreadcrumbs = ['Engines', 'some-engine', 'Analytics'];
-
beforeEach(() => {
mockUseParams.mockReturnValue({ query: 'some-query' });
@@ -32,16 +28,10 @@ describe('QueryDetail', () => {
});
it('renders', () => {
- const wrapper = shallow( );
+ const wrapper = shallow( );
expect(wrapper.find(AnalyticsLayout).prop('title')).toEqual('"some-query"');
- expect(wrapper.find(SetPageChrome).prop('trail')).toEqual([
- 'Engines',
- 'some-engine',
- 'Analytics',
- 'Query',
- 'some-query',
- ]);
+ expect(wrapper.find(AnalyticsLayout).prop('breadcrumbs')).toEqual(['Query', 'some-query']);
expect(wrapper.find(AnalyticsCards)).toHaveLength(1);
expect(wrapper.find(AnalyticsChart)).toHaveLength(1);
@@ -50,7 +40,7 @@ describe('QueryDetail', () => {
it('renders empty "" search titles correctly', () => {
mockUseParams.mockReturnValue({ query: '""' });
- const wrapper = shallow( );
+ const wrapper = shallow( );
expect(wrapper.find(AnalyticsLayout).prop('title')).toEqual('""');
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx
index 83c83aa36f1bb..e68984459cf10 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx
@@ -12,8 +12,6 @@ import { useValues } from 'kea';
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
-import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs';
import { useDecodedParams } from '../../../utils/encode_path_params';
import { AnalyticsLayout } from '../analytics_layout';
@@ -25,10 +23,7 @@ const QUERY_DETAIL_TITLE = i18n.translate(
{ defaultMessage: 'Query' }
);
-interface Props {
- breadcrumbs: BreadcrumbTrail;
-}
-export const QueryDetail: React.FC = ({ breadcrumbs }) => {
+export const QueryDetail: React.FC = () => {
const { query } = useDecodedParams();
const queryTitle = query === '""' ? query : `"${query}"`;
@@ -37,9 +32,7 @@ export const QueryDetail: React.FC = ({ breadcrumbs }) => {
);
return (
-
-
-
+
{
const { recentQueries } = useValues(AnalyticsLogic);
return (
-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx
index 6459126560b3a..81b3d08770be6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx
@@ -18,7 +18,7 @@ export const TopQueries: React.FC = () => {
const { topQueries } = useValues(AnalyticsLogic);
return (
-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx
index 8e2591697feaa..2aec88bd372fe 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx
@@ -18,7 +18,11 @@ export const TopQueriesNoClicks: React.FC = () => {
const { topQueriesNoClicks } = useValues(AnalyticsLogic);
return (
-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx
index e093a5130d204..835b259330c83 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx
@@ -18,7 +18,11 @@ export const TopQueriesNoResults: React.FC = () => {
const { topQueriesNoResults } = useValues(AnalyticsLogic);
return (
-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx
index 87e276a8382c3..9bea265df55ae 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx
@@ -18,7 +18,11 @@ export const TopQueriesWithClicks: React.FC = () => {
const { topQueriesWithClicks } = useValues(AnalyticsLogic);
return (
-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
index 98627950016fb..b390b1a52b927 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
@@ -94,6 +94,11 @@ export const EngineRouter: React.FC = () => {
+ {canViewEngineAnalytics && (
+
+
+
+ )}
{canViewEngineDocuments && (
@@ -106,11 +111,6 @@ export const EngineRouter: React.FC = () => {
)}
{/* TODO: Remove layout once page template migration is over */}
}>
- {canViewEngineAnalytics && (
-
-
-
- )}
{canViewEngineSchema && (
diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx b/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx
index 6e89274dca570..a251188b5cd90 100644
--- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/get_page_header.tsx
@@ -41,3 +41,9 @@ export const getPageHeaderActions = (wrapper: ShallowWrapper) => {
);
};
+
+export const getPageHeaderChildren = (wrapper: ShallowWrapper) => {
+ const children = getPageHeader(wrapper).children || null;
+
+ return shallow({children}
);
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts b/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts
index ed5c3f85a888e..7903b4a31c8a9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts
@@ -15,6 +15,7 @@ export {
getPageTitle,
getPageDescription,
getPageHeaderActions,
+ getPageHeaderChildren,
} from './get_page_header';
// Misc
From 69a5d01bde7d9d42bed5549cfd511c0ee54d4b0d Mon Sep 17 00:00:00 2001
From: Ignacio Rivas
Date: Tue, 22 Jun 2021 17:47:24 +0200
Subject: [PATCH 074/163] [CCR] Migrate to new page layout structure (#102507)
* wip: start migrating views from ccr
* finish up migrating ccr pages to new nav layout
* Fix tests, linter errors and i18n strings
* remove todo
* Render loading and error states centered in screen without page title
* Keep loader going while we still setting the payload
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../auto_follow_pattern_add.test.js | 8 +-
.../follower_index_add.test.js | 8 +-
.../public/app/app.tsx | 62 ++----
.../auto_follow_pattern_page_title.js | 61 ++----
.../components/follower_index_page_title.js | 61 ++----
.../auto_follow_pattern_add.js | 56 ++---
.../auto_follow_pattern_edit.js | 152 ++++++-------
.../follower_index_add/follower_index_add.js | 55 ++---
.../follower_index_edit.js | 162 +++++++-------
.../auto_follow_pattern_list.js | 204 +++++++----------
.../auto_follow_pattern_table.js | 17 ++
.../components/context_menu/context_menu.js | 2 +-
.../follower_indices_table.js | 16 ++
.../follower_indices_list.js | 205 +++++++-----------
.../public/app/sections/home/home.js | 51 ++---
.../public/shared_imports.ts | 7 +-
16 files changed, 513 insertions(+), 614 deletions(-)
diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js
index 86abbba968781..e49751cecc1d0 100644
--- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js
@@ -39,10 +39,6 @@ describe('Create Auto-follow pattern', () => {
expect(exists('remoteClustersLoading')).toBe(true);
expect(find('remoteClustersLoading').text()).toBe('Loading remote clusters…');
});
-
- test('should have a link to the documentation', () => {
- expect(exists('docsButton')).toBe(true);
- });
});
describe('when remote clusters are loaded', () => {
@@ -59,6 +55,10 @@ describe('Create Auto-follow pattern', () => {
component.update();
});
+ test('should have a link to the documentation', () => {
+ expect(exists('docsButton')).toBe(true);
+ });
+
test('should display the Auto-follow pattern form', async () => {
expect(exists('autoFollowPatternForm')).toBe(true);
});
diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js
index 228868194b231..6d54444df4273 100644
--- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js
+++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js
@@ -42,10 +42,6 @@ describe('Create Follower index', () => {
expect(exists('remoteClustersLoading')).toBe(true);
expect(find('remoteClustersLoading').text()).toBe('Loading remote clusters…');
});
-
- test('should have a link to the documentation', () => {
- expect(exists('docsButton')).toBe(true);
- });
});
describe('when remote clusters are loaded', () => {
@@ -62,6 +58,10 @@ describe('Create Follower index', () => {
component.update();
});
+ test('should have a link to the documentation', () => {
+ expect(exists('docsButton')).toBe(true);
+ });
+
test('should display the Follower index form', async () => {
expect(exists('followerIndexForm')).toBe(true);
});
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/app.tsx b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx
index 50a6cfb1b4bb9..c6144143e1849 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/app.tsx
+++ b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx
@@ -5,27 +5,19 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import { Route, Switch, Router, Redirect } from 'react-router-dom';
import { ScopedHistory, ApplicationStart } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiEmptyPrompt,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
- EuiPageContent,
- EuiSpacer,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiEmptyPrompt, EuiPageContent } from '@elastic/eui';
import { getFatalErrors } from './services/notifications';
-import { SectionError } from './components';
import { routing } from './services/routing';
// @ts-ignore
import { loadPermissions } from './services/api';
+import { SectionLoading, PageError } from '../shared_imports';
// @ts-ignore
import {
@@ -119,48 +111,34 @@ class AppComponent extends Component {
if (isFetchingPermissions) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
);
}
if (fetchPermissionError) {
return (
-
-
- }
- error={fetchPermissionError}
- />
-
-
-
+
+ }
+ error={fetchPermissionError}
+ />
);
}
if (!hasPermission) {
return (
-
+
(
-
-
+ <>
+ {title}}
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
-
-
-
-
- {title}
-
-
-
-
-
-
-
-
-
-
-
+
+ >
);
AutoFollowPatternPageTitle.propTypes = {
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js
index b5652d3f2b6e6..6d523cf2c470f 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js
@@ -5,51 +5,38 @@
* 2.0.
*/
-import React, { Fragment } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiPageContentHeader,
- EuiSpacer,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiSpacer, EuiPageHeader, EuiButtonEmpty } from '@elastic/eui';
import { documentationLinks } from '../services/documentation_links';
export const FollowerIndexPageTitle = ({ title }) => (
-
-
+ <>
+ {title}}
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
-
-
-
-
- {title}
-
-
-
-
-
-
-
-
-
-
-
+
+ >
);
FollowerIndexPageTitle.propTypes = {
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js
index 0fe562b7a8f05..118e3103008d0 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js
@@ -8,16 +8,15 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
-
-import { EuiPageContent } from '@elastic/eui';
+import { EuiPageContentBody, EuiPageContent } from '@elastic/eui';
import { listBreadcrumb, addBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs';
import {
AutoFollowPatternForm,
AutoFollowPatternPageTitle,
RemoteClustersProvider,
- SectionLoading,
} from '../../components';
+import { SectionLoading } from '../../../shared_imports';
export class AutoFollowPatternAdd extends PureComponent {
static propTypes = {
@@ -44,30 +43,37 @@ export class AutoFollowPatternAdd extends PureComponent {
} = this.props;
return (
-
-
- }
- />
-
-
- {({ isLoading, error, remoteClusters }) => {
- if (isLoading) {
- return (
-
+
+ {({ isLoading, error, remoteClusters }) => {
+ if (isLoading) {
+ return (
+
+
- );
- }
+
+ );
+ }
+
+ return (
+
+
+ }
+ />
- return (
}
/>
- );
- }}
-
-
+
+ );
+ }}
+
);
}
}
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js
index d060fb83832c6..fa97b28c8b472 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js
@@ -5,12 +5,12 @@
* 2.0.
*/
-import React, { PureComponent, Fragment } from 'react';
+import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPageContent, EuiSpacer } from '@elastic/eui';
+import { EuiButton, EuiPageContent, EuiEmptyPrompt, EuiPageContentBody } from '@elastic/eui';
import { listBreadcrumb, editBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs';
import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public';
@@ -18,10 +18,9 @@ import {
AutoFollowPatternForm,
AutoFollowPatternPageTitle,
RemoteClustersProvider,
- SectionLoading,
- SectionError,
} from '../../components';
import { API_STATUS } from '../../constants';
+import { SectionLoading } from '../../../shared_imports';
export class AutoFollowPatternEdit extends PureComponent {
static propTypes = {
@@ -80,13 +79,6 @@ export class AutoFollowPatternEdit extends PureComponent {
},
} = this.props;
- const title = i18n.translate(
- 'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingErrorTitle',
- {
- defaultMessage: 'Error loading auto-follow pattern',
- }
- );
-
const errorMessage =
error.body.statusCode === 404
? {
@@ -101,38 +93,42 @@ export class AutoFollowPatternEdit extends PureComponent {
: error;
return (
-
-
-
-
-
-
-
-
+
+
+
+ }
+ body={{errorMessage}
}
+ actions={
+
-
-
-
-
+
+ }
+ />
+
);
}
- renderLoadingAutoFollowPattern() {
+ renderLoading(loadingTitle) {
return (
-
-
-
+
+ {loadingTitle}
+
);
}
@@ -145,55 +141,59 @@ export class AutoFollowPatternEdit extends PureComponent {
match: { url: currentUrl },
} = this.props;
+ if (apiStatus.get === API_STATUS.LOADING || !autoFollowPattern) {
+ return this.renderLoading(
+ i18n.translate('xpack.crossClusterReplication.autoFollowPatternEditForm.loadingTitle', {
+ defaultMessage: 'Loading auto-follow pattern…',
+ })
+ );
+ }
+
+ if (apiError.get) {
+ return this.renderGetAutoFollowPatternError(apiError.get);
+ }
+
return (
-
-
+
+ {({ isLoading, error, remoteClusters }) => {
+ if (isLoading) {
+ return this.renderLoading(
+ i18n.translate(
+ 'xpack.crossClusterReplication.autoFollowPatternEditForm.loadingRemoteClustersMessage',
+ { defaultMessage: 'Loading remote clusters…' }
+ )
+ );
}
- />
- {apiStatus.get === API_STATUS.LOADING && this.renderLoadingAutoFollowPattern()}
-
- {apiError.get && this.renderGetAutoFollowPatternError(apiError.get)}
-
- {autoFollowPattern && (
-
- {({ isLoading, error, remoteClusters }) => {
- if (isLoading) {
- return (
-
-
-
- );
- }
+ return (
+
+
+ }
+ />
- return (
-
- }
- />
- );
- }}
-
- )}
-
+
+ }
+ />
+
+ );
+ }}
+
);
}
}
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js
index 836a4f5cc36fa..325c23641580c 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js
@@ -8,16 +8,15 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
-
-import { EuiPageContent } from '@elastic/eui';
+import { EuiPageContentBody, EuiPageContent } from '@elastic/eui';
import { setBreadcrumbs, listBreadcrumb, addBreadcrumb } from '../../services/breadcrumbs';
import {
FollowerIndexForm,
FollowerIndexPageTitle,
RemoteClustersProvider,
- SectionLoading,
} from '../../components';
+import { SectionLoading } from '../../../shared_imports';
export class FollowerIndexAdd extends PureComponent {
static propTypes = {
@@ -45,30 +44,36 @@ export class FollowerIndexAdd extends PureComponent {
} = this.props;
return (
-
-
- }
- />
-
-
- {({ isLoading, error, remoteClusters }) => {
- if (isLoading) {
- return (
-
+
+ {({ isLoading, error, remoteClusters }) => {
+ if (isLoading) {
+ return (
+
+
- );
- }
+
+ );
+ }
- return (
+ return (
+
+
+ }
+ />
}
/>
- );
- }}
-
-
+
+ );
+ }}
+
);
}
}
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js
index 41b09a398b1f2..618d97f186516 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js
@@ -5,18 +5,17 @@
* 2.0.
*/
-import React, { PureComponent, Fragment } from 'react';
+import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
- EuiButtonEmpty,
+ EuiButton,
EuiConfirmModal,
- EuiFlexGroup,
- EuiFlexItem,
+ EuiPageContentBody,
EuiPageContent,
- EuiSpacer,
+ EuiEmptyPrompt,
} from '@elastic/eui';
import { setBreadcrumbs, listBreadcrumb, editBreadcrumb } from '../../services/breadcrumbs';
@@ -24,11 +23,10 @@ import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_rea
import {
FollowerIndexForm,
FollowerIndexPageTitle,
- SectionLoading,
- SectionError,
RemoteClustersProvider,
} from '../../components';
import { API_STATUS } from '../../constants';
+import { SectionLoading } from '../../../shared_imports';
export class FollowerIndexEdit extends PureComponent {
static propTypes = {
@@ -104,14 +102,11 @@ export class FollowerIndexEdit extends PureComponent {
closeConfirmModal = () => this.setState({ showConfirmModal: false });
- renderLoadingFollowerIndex() {
+ renderLoading(loadingTitle) {
return (
-
-
-
+
+ {loadingTitle}
+
);
}
@@ -122,13 +117,6 @@ export class FollowerIndexEdit extends PureComponent {
},
} = this.props;
- const title = i18n.translate(
- 'xpack.crossClusterReplication.followerIndexEditForm.loadingErrorTitle',
- {
- defaultMessage: 'Error loading follower index',
- }
- );
-
const errorMessage =
error.body.statusCode === 404
? {
@@ -143,27 +131,33 @@ export class FollowerIndexEdit extends PureComponent {
: error;
return (
-
-
-
-
-
-
-
-
+
+
+
+ }
+ body={{errorMessage}
}
+ actions={
+
-
-
-
-
+
+ }
+ />
+
);
}
@@ -237,57 +231,63 @@ export class FollowerIndexEdit extends PureComponent {
/* remove non-editable properties */
const { shards, ...rest } = followerIndex || {}; // eslint-disable-line no-unused-vars
+ if (apiStatus.get === API_STATUS.LOADING || !followerIndex) {
+ return this.renderLoading(
+ i18n.translate(
+ 'xpack.crossClusterReplication.followerIndexEditForm.loadingFollowerIndexTitle',
+ { defaultMessage: 'Loading follower index…' }
+ )
+ );
+ }
+
+ if (apiError.get) {
+ return this.renderGetFollowerIndexError(apiError.get);
+ }
+
return (
-
-
+
+ {({ isLoading, error, remoteClusters }) => {
+ if (isLoading) {
+ return this.renderLoading(
+ i18n.translate(
+ 'xpack.crossClusterReplication.followerIndexEditForm.loadingRemoteClustersMessage',
+ { defaultMessage: 'Loading remote clusters…' }
+ )
+ );
}
- />
- {apiStatus.get === API_STATUS.LOADING && this.renderLoadingFollowerIndex()}
-
- {apiError.get && this.renderGetFollowerIndexError(apiError.get)}
- {followerIndex && (
-
- {({ isLoading, error, remoteClusters }) => {
- if (isLoading) {
- return (
-
-
-
- );
- }
+ return (
+
+
+ }
+ />
- return (
-
- }
- />
- );
- }}
-
- )}
+
+ }
+ />
- {showConfirmModal && this.renderConfirmModal()}
-
+ {showConfirmModal && this.renderConfirmModal()}
+
+ );
+ }}
+
);
}
}
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js
index 1885f33f9d633..1ab4e1a3e003a 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js
@@ -5,24 +5,17 @@
* 2.0.
*/
-import React, { PureComponent, Fragment } from 'react';
+import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiButton,
- EuiEmptyPrompt,
- EuiFlexGroup,
- EuiFlexItem,
- EuiText,
- EuiSpacer,
-} from '@elastic/eui';
+import { EuiButton, EuiEmptyPrompt, EuiText, EuiSpacer } from '@elastic/eui';
import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public';
-import { extractQueryParams } from '../../../../shared_imports';
+import { extractQueryParams, SectionLoading } from '../../../../shared_imports';
import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric';
import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD } from '../../../constants';
-import { SectionLoading, SectionError, SectionUnauthorized } from '../../../components';
+import { SectionError, SectionUnauthorized } from '../../../components';
import { AutoFollowPatternTable, DetailPanel } from './components';
const REFRESH_RATE_MS = 30000;
@@ -103,47 +96,77 @@ export class AutoFollowPatternList extends PureComponent {
clearInterval(this.interval);
}
- renderHeader() {
- const { isAuthorized, history } = this.props;
+ renderEmpty() {
+ return (
+
+
+
+
+ }
+ body={
+
+
+
+ }
+ actions={
+
+
+
+ }
+ />
+
+ );
+ }
+
+ renderList() {
+ const { selectAutoFollowPattern, autoFollowPatterns } = this.props;
+ const { isDetailPanelOpen } = this.state;
+
return (
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
+
-
- {isAuthorized && (
-
-
-
- )}
-
-
+
-
-
+ {isDetailPanelOpen && (
+
selectAutoFollowPattern(null)} />
+ )}
+ >
);
}
- renderContent(isEmpty) {
- const { apiError, apiStatus, isAuthorized } = this.props;
+ render() {
+ const { autoFollowPatterns, apiError, apiStatus, isAuthorized } = this.props;
+ const isEmpty = apiStatus === API_STATUS.IDLE && !autoFollowPatterns.length;
if (!isAuthorized) {
return (
@@ -171,12 +194,7 @@ export class AutoFollowPatternList extends PureComponent {
}
);
- return (
-
-
-
-
- );
+ return ;
}
if (isEmpty) {
@@ -185,83 +203,17 @@ export class AutoFollowPatternList extends PureComponent {
if (apiStatus === API_STATUS.LOADING) {
return (
-
-
-
+
);
}
return this.renderList();
}
-
- renderEmpty() {
- return (
-
-
-
- }
- body={
-
-
-
-
-
- }
- actions={
-
-
-
- }
- data-test-subj="emptyPrompt"
- />
- );
- }
-
- renderList() {
- const { selectAutoFollowPattern, autoFollowPatterns } = this.props;
-
- const { isDetailPanelOpen } = this.state;
-
- return (
- <>
-
- {isDetailPanelOpen && (
- selectAutoFollowPattern(null)} />
- )}
- >
- );
- }
-
- render() {
- const { autoFollowPatterns, apiStatus } = this.props;
- const isEmpty = apiStatus === API_STATUS.IDLE && !autoFollowPatterns.length;
-
- return (
-
- {!isEmpty && this.renderHeader()}
- {this.renderContent(isEmpty)}
-
- );
- }
}
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js
index 87002c936179a..0d228f2e63802 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js
@@ -8,13 +8,17 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
import {
EuiInMemoryTable,
+ EuiButton,
EuiLink,
EuiLoadingKibana,
EuiOverlayMask,
EuiHealth,
} from '@elastic/eui';
+import { reactRouterNavigate } from '../../../../../../../../../../src/plugins/kibana_react/public';
import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK } from '../../../../../constants';
import {
AutoFollowPatternDeleteProvider,
@@ -305,6 +309,19 @@ export class AutoFollowPatternTable extends PureComponent {
)}
/>
) : undefined,
+ toolsRight: (
+
+
+
+ ),
onChange: this.onSearch,
box: {
incremental: true,
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js
index 0d0943d870266..866afa3e6e6dc 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js
@@ -97,7 +97,7 @@ export class ContextMenu extends PureComponent {
anchorPosition={anchorPosition}
repositionOnScroll
>
-
+
) : undefined,
+ toolsRight: (
+
+
+
+ ),
onChange: this.onSearch,
box: {
incremental: true,
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js
index 743a9ec47e689..a52ba0e613ca9 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js
@@ -5,24 +5,17 @@
* 2.0.
*/
-import React, { PureComponent, Fragment } from 'react';
+import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiButton,
- EuiEmptyPrompt,
- EuiFlexGroup,
- EuiFlexItem,
- EuiText,
- EuiSpacer,
-} from '@elastic/eui';
+import { EuiButton, EuiEmptyPrompt, EuiText, EuiSpacer } from '@elastic/eui';
import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public';
-import { extractQueryParams } from '../../../../shared_imports';
+import { extractQueryParams, SectionLoading } from '../../../../shared_imports';
import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric';
import { API_STATUS, UIM_FOLLOWER_INDEX_LIST_LOAD } from '../../../constants';
-import { SectionLoading, SectionError, SectionUnauthorized } from '../../../components';
+import { SectionError, SectionUnauthorized } from '../../../components';
import { FollowerIndicesTable, DetailPanel } from './components';
const REFRESH_RATE_MS = 30000;
@@ -94,47 +87,87 @@ export class FollowerIndicesList extends PureComponent {
clearInterval(this.interval);
}
- renderHeader() {
- const { isAuthorized, history } = this.props;
+ renderEmpty() {
+ return (
+
+
+
+
+ }
+ body={
+
+
+
+ }
+ actions={
+
+
+
+ }
+ />
+
+ );
+ }
+ renderLoading() {
return (
-
-
-
-
-
-
-
-
-
+
+ );
+ }
+
+ renderList() {
+ const { selectFollowerIndex, followerIndices } = this.props;
+
+ const { isDetailPanelOpen } = this.state;
+
+ return (
+ <>
+
+
+
+
+
-
- {isAuthorized && (
-
-
-
- )}
-
-
+
-
-
+
+
+ {isDetailPanelOpen && selectFollowerIndex(null)} />}
+ >
);
}
- renderContent(isEmpty) {
- const { apiError, isAuthorized, apiStatus } = this.props;
+ render() {
+ const { followerIndices, apiError, isAuthorized, apiStatus } = this.props;
+ const isEmpty = apiStatus === API_STATUS.IDLE && !followerIndices.length;
if (!isAuthorized) {
return (
@@ -162,12 +195,7 @@ export class FollowerIndicesList extends PureComponent {
}
);
- return (
-
-
-
-
- );
+ return ;
}
if (isEmpty) {
@@ -180,79 +208,4 @@ export class FollowerIndicesList extends PureComponent {
return this.renderList();
}
-
- renderEmpty() {
- return (
-
-
-
- }
- body={
-
-
-
-
-
- }
- actions={
-
-
-
- }
- data-test-subj="emptyPrompt"
- />
- );
- }
-
- renderLoading() {
- return (
-
-
-
- );
- }
-
- renderList() {
- const { selectFollowerIndex, followerIndices } = this.props;
-
- const { isDetailPanelOpen } = this.state;
-
- return (
-
-
- {isDetailPanelOpen && selectFollowerIndex(null)} />}
-
- );
- }
-
- render() {
- const { followerIndices, apiStatus } = this.props;
- const isEmpty = apiStatus === API_STATUS.IDLE && !followerIndices.length;
- return (
-
- {!isEmpty && this.renderHeader()}
- {this.renderContent(isEmpty)}
-
- );
- }
}
diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js
index ff37c2157d515..70d35dcb22569 100644
--- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js
+++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js
@@ -9,7 +9,7 @@ import React, { PureComponent } from 'react';
import { Route, Switch } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui';
+import { EuiSpacer, EuiPageHeader } from '@elastic/eui';
import { setBreadcrumbs, listBreadcrumb } from '../../services/breadcrumbs';
import { routing } from '../../services/routing';
@@ -66,40 +66,33 @@ export class CrossClusterReplicationHome extends PureComponent {
render() {
return (
-
-
-
-
+ <>
+
-
-
-
-
-
-
- {this.tabs.map((tab) => (
- this.onSectionChange(tab.id)}
- isSelected={tab.id === this.state.activeSection}
- key={tab.id}
- data-test-subj={tab.testSubj}
- >
- {tab.name}
-
- ))}
-
+
+ }
+ tabs={this.tabs.map((tab) => ({
+ onClick: () => this.onSectionChange(tab.id),
+ isSelected: tab.id === this.state.activeSection,
+ key: tab.id,
+ 'data-test-subj': tab.testSubj,
+ label: tab.name,
+ }))}
+ />
-
+
-
-
-
-
-
-
+
+
+
+
+ >
);
}
}
diff --git a/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts b/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts
index fd28175318666..55a10749230c7 100644
--- a/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts
+++ b/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts
@@ -5,4 +5,9 @@
* 2.0.
*/
-export { extractQueryParams, indices } from '../../../../src/plugins/es_ui_shared/public';
+export {
+ extractQueryParams,
+ indices,
+ SectionLoading,
+ PageError,
+} from '../../../../src/plugins/es_ui_shared/public';
From 016259d19c55d4d7be88adc445af3a6001391566 Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Tue, 22 Jun 2021 17:48:20 +0200
Subject: [PATCH 075/163] [AppService] fix deepLinks being lost when updating
the app with other fields (#102895)
* fix app updater for deepLinks
* improve implem
---
.../application/application_service.test.ts | 83 ++++++++++++++++++-
.../application/application_service.tsx | 7 +-
2 files changed, 84 insertions(+), 6 deletions(-)
diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts
index 3ed164088bf5c..de9e4d4496f3b 100644
--- a/src/core/public/application/application_service.test.ts
+++ b/src/core/public/application/application_service.test.ts
@@ -15,13 +15,13 @@ import {
import { createElement } from 'react';
import { BehaviorSubject, Subject } from 'rxjs';
import { bufferCount, take, takeUntil } from 'rxjs/operators';
-import { shallow, mount } from 'enzyme';
+import { mount, shallow } from 'enzyme';
import { httpServiceMock } from '../http/http_service.mock';
import { overlayServiceMock } from '../overlays/overlay_service.mock';
import { MockLifecycle } from './test_types';
import { ApplicationService } from './application_service';
-import { App, PublicAppInfo, AppNavLinkStatus, AppStatus, AppUpdater } from './types';
+import { App, AppDeepLink, AppNavLinkStatus, AppStatus, AppUpdater, PublicAppInfo } from './types';
import { act } from 'react-dom/test-utils';
const createApp = (props: Partial): App => {
@@ -365,6 +365,85 @@ describe('#setup()', () => {
expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined);
MockHistory.push.mockClear();
});
+
+ it('preserves the deep links if the update does not modify them', async () => {
+ const setup = service.setup(setupDeps);
+
+ const pluginId = Symbol('plugin');
+ const updater$ = new BehaviorSubject((app) => ({}));
+
+ const deepLinks: AppDeepLink[] = [
+ {
+ id: 'foo',
+ title: 'Foo',
+ searchable: true,
+ navLinkStatus: AppNavLinkStatus.visible,
+ path: '/foo',
+ },
+ {
+ id: 'bar',
+ title: 'Bar',
+ searchable: false,
+ navLinkStatus: AppNavLinkStatus.hidden,
+ path: '/bar',
+ },
+ ];
+
+ setup.register(pluginId, createApp({ id: 'app1', deepLinks, updater$ }));
+
+ const { applications$ } = await service.start(startDeps);
+
+ updater$.next((app) => ({ defaultPath: '/foo' }));
+
+ let appInfos = await applications$.pipe(take(1)).toPromise();
+
+ expect(appInfos.get('app1')!.deepLinks).toEqual([
+ {
+ deepLinks: [],
+ id: 'foo',
+ keywords: [],
+ navLinkStatus: 1,
+ path: '/foo',
+ searchable: true,
+ title: 'Foo',
+ },
+ {
+ deepLinks: [],
+ id: 'bar',
+ keywords: [],
+ navLinkStatus: 3,
+ path: '/bar',
+ searchable: false,
+ title: 'Bar',
+ },
+ ]);
+
+ updater$.next((app) => ({
+ deepLinks: [
+ {
+ id: 'bar',
+ title: 'Bar',
+ searchable: false,
+ navLinkStatus: AppNavLinkStatus.hidden,
+ path: '/bar',
+ },
+ ],
+ }));
+
+ appInfos = await applications$.pipe(take(1)).toPromise();
+
+ expect(appInfos.get('app1')!.deepLinks).toEqual([
+ {
+ deepLinks: [],
+ id: 'bar',
+ keywords: [],
+ navLinkStatus: 3,
+ path: '/bar',
+ searchable: false,
+ title: 'Bar',
+ },
+ ]);
+ });
});
});
diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx
index 8c6090caabce1..2e804bf2f5413 100644
--- a/src/core/public/application/application_service.tsx
+++ b/src/core/public/application/application_service.tsx
@@ -54,6 +54,7 @@ function filterAvailable(m: Map, capabilities: Capabilities) {
)
);
}
+
const findMounter = (mounters: Map, appRoute?: string) =>
[...mounters].find(([, mounter]) => mounter.appRoute === appRoute);
@@ -414,13 +415,11 @@ const updateStatus = (app: App, statusUpdaters: AppUpdaterWrapper[]): App => {
changes.navLinkStatus ?? AppNavLinkStatus.default,
fields.navLinkStatus ?? AppNavLinkStatus.default
),
- // deepLinks take the last defined update
- deepLinks: fields.deepLinks
- ? populateDeepLinkDefaults(fields.deepLinks)
- : changes.deepLinks,
+ ...(fields.deepLinks ? { deepLinks: populateDeepLinkDefaults(fields.deepLinks) } : {}),
};
}
});
+
return {
...app,
...changes,
From 2323b9864239e8db789a70b24327ac17d7353fdd Mon Sep 17 00:00:00 2001
From: Mikhail Shustov
Date: Tue, 22 Jun 2021 17:48:47 +0200
Subject: [PATCH 076/163] [jest] use circus runner for the integration tests
(#102782)
* use circus runner for integration tests
* do not use done callback. https://github.com/facebook/jest/issues/10529
* fix type error
---
jest.config.integration.js | 1 -
.../http/integration_tests/request.test.ts | 51 ++++++++++---------
.../integration_tests/migration.test.ts | 6 +++
.../integration_tests/index.test.ts | 2 +-
.../integration_tests/lib/servers.ts | 4 +-
5 files changed, 37 insertions(+), 27 deletions(-)
diff --git a/jest.config.integration.js b/jest.config.integration.js
index 50767932a52d7..b6ecb4569b643 100644
--- a/jest.config.integration.js
+++ b/jest.config.integration.js
@@ -13,7 +13,6 @@ module.exports = {
rootDir: '.',
roots: ['/src', '/packages'],
testMatch: ['**/integration_tests**/*.test.{js,mjs,ts,tsx}'],
- testRunner: 'jasmine2',
testPathIgnorePatterns: preset.testPathIgnorePatterns.filter(
(pattern) => !pattern.includes('integration_tests')
),
diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts
index 7571184363d2e..dfc47098724cc 100644
--- a/src/core/server/http/integration_tests/request.test.ts
+++ b/src/core/server/http/integration_tests/request.test.ts
@@ -163,24 +163,24 @@ describe('KibanaRequest', () => {
describe('events', () => {
describe('aborted$', () => {
- it('emits once and completes when request aborted', async (done) => {
+ it('emits once and completes when request aborted', async () => {
expect.assertions(1);
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
const nextSpy = jest.fn();
- router.get({ path: '/', validate: false }, async (context, request, res) => {
- request.events.aborted$.subscribe({
- next: nextSpy,
- complete: () => {
- expect(nextSpy).toHaveBeenCalledTimes(1);
- done();
- },
- });
- // prevents the server to respond
- await delay(30000);
- return res.ok({ body: 'ok' });
+ const done = new Promise((resolve) => {
+ router.get({ path: '/', validate: false }, async (context, request, res) => {
+ request.events.aborted$.subscribe({
+ next: nextSpy,
+ complete: resolve,
+ });
+
+ // prevents the server to respond
+ await delay(30000);
+ return res.ok({ body: 'ok' });
+ });
});
await server.start();
@@ -191,6 +191,8 @@ describe('KibanaRequest', () => {
.end();
setTimeout(() => incomingRequest.abort(), 50);
+ await done;
+ expect(nextSpy).toHaveBeenCalledTimes(1);
});
it('completes & does not emit when request handled', async () => {
@@ -299,25 +301,24 @@ describe('KibanaRequest', () => {
expect(completeSpy).toHaveBeenCalledTimes(1);
});
- it('emits once and completes when response is aborted', async (done) => {
+ it('emits once and completes when response is aborted', async () => {
expect.assertions(2);
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
const nextSpy = jest.fn();
- router.get({ path: '/', validate: false }, async (context, req, res) => {
- req.events.completed$.subscribe({
- next: nextSpy,
- complete: () => {
- expect(nextSpy).toHaveBeenCalledTimes(1);
- done();
- },
- });
+ const done = new Promise((resolve) => {
+ router.get({ path: '/', validate: false }, async (context, req, res) => {
+ req.events.completed$.subscribe({
+ next: nextSpy,
+ complete: resolve,
+ });
- expect(nextSpy).not.toHaveBeenCalled();
- await delay(30000);
- return res.ok({ body: 'ok' });
+ expect(nextSpy).not.toHaveBeenCalled();
+ await delay(30000);
+ return res.ok({ body: 'ok' });
+ });
});
await server.start();
@@ -327,6 +328,8 @@ describe('KibanaRequest', () => {
// end required to send request
.end();
setTimeout(() => incomingRequest.abort(), 50);
+ await done;
+ expect(nextSpy).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts
index f4e0dd8fffcab..4c9e37d17f2e7 100644
--- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts
+++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts
@@ -95,6 +95,12 @@ describe('migration v2', () => {
},
],
},
+ // reporting loads headless browser, that prevents nodejs process from exiting.
+ xpack: {
+ reporting: {
+ enabled: false,
+ },
+ },
},
{
oss,
diff --git a/src/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts
index 6c7cdfa43cf57..61e55284a20b8 100644
--- a/src/core/server/ui_settings/integration_tests/index.test.ts
+++ b/src/core/server/ui_settings/integration_tests/index.test.ts
@@ -17,7 +17,7 @@ const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.
const savedObjectIndex = `.kibana_${kibanaVersion}_001`;
describe('uiSettings/routes', function () {
- jest.setTimeout(10000);
+ jest.setTimeout(120_000);
beforeAll(startServers);
/* eslint-disable jest/valid-describe */
diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts
index b18d9926649aa..96ba08a0728ab 100644
--- a/src/core/server/ui_settings/integration_tests/lib/servers.ts
+++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts
@@ -75,8 +75,10 @@ export function getServices() {
export async function stopServers() {
services = null!;
- if (servers) {
+ if (esServer) {
await esServer.stop();
+ }
+ if (kbn) {
await kbn.stop();
}
}
From 84d999d7478bce6656877cdc259b552c59463076 Mon Sep 17 00:00:00 2001
From: Constance
Date: Tue, 22 Jun 2021 09:01:25 -0700
Subject: [PATCH 077/163] [App Search] Convert Search UI view to new page
template + minor UI polish (#102813)
* Convert Search UI view to use new page template
+ update tests TODO
* [UX polish] Add empty state to Search UI view
- On a totally new engine, all pages except this one* had an empty state, so per Davey's recommendations I whipped up a new empty state for this page
* Overview has a custom 'empty' state, analytics does not have an empty state
* Update router
* Fix bad merge conflict resolution
* [Polish] Copy feedback proposed by Davey
- see https://github.com/elastic/kibana/pull/101958/commits/cbc3706223eb47be3d854a1cf4e3c7275d88ca39
---
.../components/engine/engine_router.tsx | 10 +-
.../search_ui/components/empty_state.test.tsx | 27 ++++
.../search_ui/components/empty_state.tsx | 46 +++++++
.../components/search_ui/search_ui.test.tsx | 13 +-
.../components/search_ui/search_ui.tsx | 120 ++++++++----------
5 files changed, 143 insertions(+), 73 deletions(-)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
index b390b1a52b927..3e18c9e680de2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
@@ -109,6 +109,11 @@ export const EngineRouter: React.FC = () => {
)}
+ {canManageEngineSearchUi && (
+
+
+
+ )}
{/* TODO: Remove layout once page template migration is over */}
}>
{canViewEngineSchema && (
@@ -141,11 +146,6 @@ export const EngineRouter: React.FC = () => {
)}
- {canManageEngineSearchUi && (
-
-
-
- )}
{canViewMetaEngineSourceEngines && (
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx
new file mode 100644
index 0000000000000..39f0cb376b325
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.test.tsx
@@ -0,0 +1,27 @@
+/*
+ * 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 { shallow } from 'enzyme';
+
+import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+
+import { EmptyState } from './empty_state';
+
+describe('EmptyState', () => {
+ it('renders', () => {
+ const wrapper = shallow( )
+ .find(EuiEmptyPrompt)
+ .dive();
+
+ expect(wrapper.find('h2').text()).toEqual('Add documents to generate a Search UI');
+ expect(wrapper.find(EuiButton).prop('href')).toEqual(
+ expect.stringContaining('/reference-ui-guide.html')
+ );
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.tsx
new file mode 100644
index 0000000000000..b7665a58de300
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/components/empty_state.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 { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { DOCS_PREFIX } from '../../../routes';
+
+export const EmptyState: React.FC = () => (
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.searchUI.empty.title', {
+ defaultMessage: 'Add documents to generate a Search UI',
+ })}
+
+ }
+ body={
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.searchUI.empty.description', {
+ defaultMessage:
+ 'A schema will be automatically created for you after you index some documents.',
+ })}
+
+ }
+ actions={
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.searchUI.empty.buttonLabel', {
+ defaultMessage: 'Read the Search UI guide',
+ })}
+
+ }
+ />
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx
index edec376dd3edd..f9f0dd611b953 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.test.tsx
@@ -6,14 +6,17 @@
*/
import '../../../__mocks__/shallow_useeffect.mock';
-import '../../__mocks__/engine_logic.mock';
-import { setMockActions } from '../../../__mocks__/kea_logic';
+import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic';
+import { mockEngineValues } from '../../__mocks__/engine_logic.mock';
import React from 'react';
import { shallow } from 'enzyme';
+import { SearchUIForm } from './components/search_ui_form';
+import { SearchUIGraphic } from './components/search_ui_graphic';
+
import { SearchUI } from './';
describe('SearchUI', () => {
@@ -24,11 +27,13 @@ describe('SearchUI', () => {
beforeEach(() => {
jest.clearAllMocks();
setMockActions(actions);
+ setMockValues(mockEngineValues);
});
it('renders', () => {
- shallow( );
- // TODO: Check for form
+ const wrapper = shallow( );
+ expect(wrapper.find(SearchUIForm).exists()).toBe(true);
+ expect(wrapper.find(SearchUIGraphic).exists()).toBe(true);
});
it('initializes data on mount', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx
index e75bc36177151..0ac59a33068ba 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui.tsx
@@ -7,25 +7,16 @@
import React, { useEffect } from 'react';
-import { useActions } from 'kea';
+import { useActions, useValues } from 'kea';
-import {
- EuiPageHeader,
- EuiPageContentBody,
- EuiText,
- EuiFlexItem,
- EuiFlexGroup,
- EuiSpacer,
- EuiLink,
-} from '@elastic/eui';
+import { EuiText, EuiFlexItem, EuiFlexGroup, EuiSpacer, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-
import { DOCS_PREFIX } from '../../routes';
-import { getEngineBreadcrumbs } from '../engine';
+import { EngineLogic, getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
+import { EmptyState } from './components/empty_state';
import { SearchUIForm } from './components/search_ui_form';
import { SearchUIGraphic } from './components/search_ui_graphic';
import { SEARCH_UI_TITLE } from './i18n';
@@ -33,61 +24,62 @@ import { SearchUILogic } from './search_ui_logic';
export const SearchUI: React.FC = () => {
const { loadFieldData } = useActions(SearchUILogic);
+ const { engine } = useValues(EngineLogic);
useEffect(() => {
loadFieldData();
}, []);
return (
- <>
-
-
-
-
-
-
-
-
-
-
-
- ),
- }}
- />
-
-
-
-
-
- ),
- }}
- />
-
-
-
-
-
-
-
-
-
-
- >
+ }
+ >
+
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
);
};
From 21f6a1bc92dd30b4eaff1a14ba14538aa25be46d Mon Sep 17 00:00:00 2001
From: Matthias Wilhelm
Date: Tue, 22 Jun 2021 18:23:30 +0200
Subject: [PATCH 078/163] [Discover][Main] Improve state related code (#102028)
---
.../layout/discover_layout.test.tsx | 4 +-
.../components/layout/discover_layout.tsx | 91 ++-------
.../apps/main/components/layout/types.ts | 8 +-
.../components/sidebar/discover_sidebar.tsx | 22 +-
.../discover_sidebar_responsive.test.tsx | 11 -
.../sidebar/discover_sidebar_responsive.tsx | 9 -
.../sidebar/lib/group_fields.test.ts | 6 +-
.../components/sidebar/lib/group_fields.tsx | 4 +-
.../apps/main/discover_main_app.tsx | 46 +----
.../apps/main/services/discover_state.ts | 68 ++++++-
.../main/services/use_discover_state.test.ts | 4 -
.../apps/main/services/use_discover_state.ts | 188 ++++++++++++------
.../main/services/use_saved_search.test.ts | 52 ++++-
.../apps/main/services/use_saved_search.ts | 135 +++++--------
14 files changed, 323 insertions(+), 325 deletions(-)
diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx
index 2fd394d98281b..57a9d518f838e 100644
--- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx
@@ -18,7 +18,6 @@ import { createSearchSourceMock } from '../../../../../../../data/common/search/
import { IndexPattern, IndexPatternAttributes } from '../../../../../../../data/common';
import { SavedObject } from '../../../../../../../../core/types';
import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield';
-import { DiscoverSearchSessionManager } from '../../services/discover_search_session';
import { GetStateReturn } from '../../services/discover_state';
import { DiscoverLayoutProps } from './types';
import { SavedSearchDataSubject } from '../../services/use_saved_search';
@@ -50,11 +49,12 @@ function getProps(indexPattern: IndexPattern): DiscoverLayoutProps {
indexPattern,
indexPatternList,
navigateTo: jest.fn(),
+ onChangeIndexPattern: jest.fn(),
+ onUpdateQuery: jest.fn(),
resetQuery: jest.fn(),
savedSearch: savedSearchMock,
savedSearchData$: savedSearch$,
savedSearchRefetch$: new Subject(),
- searchSessionManager: {} as DiscoverSearchSessionManager,
searchSource: searchSourceMock,
services,
state: { columns: [] },
diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx
index 0430614d413b6..a10674323e5cb 100644
--- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx
@@ -36,10 +36,8 @@ import { SortPairArr } from '../../../../angular/doc_table/lib/get_sort';
import {
DOC_HIDE_TIME_COLUMN_SETTING,
DOC_TABLE_LEGACY,
- MODIFY_COLUMNS_ON_SWITCH,
SAMPLE_SIZE_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
- SORT_DEFAULT_ORDER_SETTING,
} from '../../../../../../common';
import { popularizeField } from '../../../../helpers/popularize_field';
import { DocViewFilterFn } from '../../../../doc_views/doc_views_types';
@@ -52,7 +50,6 @@ import { InspectorSession } from '../../../../../../../inspector/public';
import { DiscoverUninitialized } from '../uninitialized/uninitialized';
import { SavedSearchDataMessage } from '../../services/use_saved_search';
import { useDataGridColumns } from '../../../../helpers/use_data_grid_columns';
-import { getSwitchIndexPatternAppState } from '../../utils/get_switch_index_pattern_app_state';
import { FetchStatus } from '../../../../types';
const DocTableLegacyMemoized = React.memo(DocTableLegacy);
@@ -72,26 +69,20 @@ export function DiscoverLayout({
indexPattern,
indexPatternList,
navigateTo,
+ onChangeIndexPattern,
+ onUpdateQuery,
savedSearchRefetch$,
resetQuery,
savedSearchData$,
savedSearch,
- searchSessionManager,
searchSource,
services,
state,
stateContainer,
}: DiscoverLayoutProps) {
- const {
- trackUiMetric,
- capabilities,
- indexPatterns,
- data,
- uiSettings: config,
- filterManager,
- } = services;
+ const { trackUiMetric, capabilities, indexPatterns, data, uiSettings, filterManager } = services;
- const sampleSize = useMemo(() => config.get(SAMPLE_SIZE_SETTING), [config]);
+ const sampleSize = useMemo(() => uiSettings.get(SAMPLE_SIZE_SETTING), [uiSettings]);
const [expandedDoc, setExpandedDoc] = useState(undefined);
const [inspectorSession, setInspectorSession] = useState(undefined);
const scrollableDesktop = useRef(null);
@@ -121,42 +112,21 @@ export function DiscoverLayout({
};
}, [savedSearchData$, fetchState]);
- const isMobile = () => {
- // collapse icon isn't displayed in mobile view, use it to detect which view is displayed
- return collapseIcon && !collapseIcon.current;
- };
+ // collapse icon isn't displayed in mobile view, use it to detect which view is displayed
+ const isMobile = () => collapseIcon && !collapseIcon.current;
const timeField = useMemo(() => {
return indexPatternsUtils.isDefault(indexPattern) ? indexPattern.timeFieldName : undefined;
}, [indexPattern]);
const [isSidebarClosed, setIsSidebarClosed] = useState(false);
- const isLegacy = useMemo(() => services.uiSettings.get(DOC_TABLE_LEGACY), [services]);
- const useNewFieldsApi = useMemo(() => !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [
- services,
- ]);
-
- const unmappedFieldsConfig = useMemo(
- () => ({
- showUnmappedFields: useNewFieldsApi,
- }),
- [useNewFieldsApi]
- );
+ const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]);
+ const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
const resultState = useMemo(() => getResultState(fetchStatus, rows!), [fetchStatus, rows]);
- const updateQuery = useCallback(
- (_payload, isUpdate?: boolean) => {
- if (isUpdate === false) {
- searchSessionManager.removeSearchSessionIdFromURL({ replace: false });
- savedSearchRefetch$.next();
- }
- },
- [savedSearchRefetch$, searchSessionManager]
- );
-
const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useDataGridColumns({
capabilities,
- config,
+ config: uiSettings,
indexPattern,
indexPatterns,
setAppState: stateContainer.setAppState,
@@ -243,42 +213,8 @@ export function DiscoverLayout({
const contentCentered = resultState === 'uninitialized';
const showTimeCol = useMemo(
- () => !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName,
- [config, indexPattern.timeFieldName]
- );
-
- const onChangeIndexPattern = useCallback(
- async (id: string) => {
- const nextIndexPattern = await indexPatterns.get(id);
- if (nextIndexPattern && indexPattern) {
- /**
- * Without resetting the fetch state, e.g. a time column would be displayed when switching
- * from a index pattern without to a index pattern with time filter for a brief moment
- * That's because appState is updated before savedSearchData$
- * The following line of code catches this, but should be improved
- */
- savedSearchData$.next({ rows: [], state: FetchStatus.LOADING, fieldCounts: {} });
-
- const nextAppState = getSwitchIndexPatternAppState(
- indexPattern,
- nextIndexPattern,
- state.columns || [],
- (state.sort || []) as SortPairArr[],
- config.get(MODIFY_COLUMNS_ON_SWITCH),
- config.get(SORT_DEFAULT_ORDER_SETTING)
- );
- stateContainer.setAppState(nextAppState);
- }
- },
- [
- config,
- indexPattern,
- indexPatterns,
- savedSearchData$,
- state.columns,
- state.sort,
- stateContainer,
- ]
+ () => !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName,
+ [uiSettings, indexPattern.timeFieldName]
);
return (
@@ -294,7 +230,7 @@ export function DiscoverLayout({
searchSource={searchSource}
services={services}
stateContainer={stateContainer}
- updateQuery={updateQuery}
+ updateQuery={onUpdateQuery}
/>
@@ -316,7 +252,6 @@ export function DiscoverLayout({
state={state}
isClosed={isSidebarClosed}
trackUiMetric={trackUiMetric}
- unmappedFieldsConfig={unmappedFieldsConfig}
useNewFieldsApi={useNewFieldsApi}
onEditRuntimeField={onEditRuntimeField}
/>
@@ -373,7 +308,7 @@ export function DiscoverLayout({
>
>;
- resetQuery: () => void;
navigateTo: (url: string) => void;
+ onChangeIndexPattern: (id: string) => void;
+ onUpdateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
+ resetQuery: () => void;
savedSearch: SavedSearch;
savedSearchData$: SavedSearchDataSubject;
savedSearchRefetch$: SavedSearchRefetchSubject;
- searchSessionManager: DiscoverSearchSessionManager;
searchSource: ISearchSource;
services: DiscoverServices;
state: AppState;
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx
index 7fbbf6fd3ffdc..7f8866a2ee369 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx
@@ -82,7 +82,6 @@ export function DiscoverSidebar({
trackUiMetric,
useNewFieldsApi = false,
useFlyout = false,
- unmappedFieldsConfig,
onEditRuntimeField,
onChangeIndexPattern,
setFieldEditorRef,
@@ -129,25 +128,8 @@ export function DiscoverSidebar({
popular: popularFields,
unpopular: unpopularFields,
} = useMemo(
- () =>
- groupFields(
- fields,
- columns,
- popularLimit,
- fieldCounts,
- fieldFilter,
- useNewFieldsApi,
- !!unmappedFieldsConfig?.showUnmappedFields
- ),
- [
- fields,
- columns,
- popularLimit,
- fieldCounts,
- fieldFilter,
- useNewFieldsApi,
- unmappedFieldsConfig?.showUnmappedFields,
- ]
+ () => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi),
+ [fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi]
);
const paginate = useCallback(() => {
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx
index 2ad75806173eb..6973221fd3624 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx
@@ -25,7 +25,6 @@ import {
} from './discover_sidebar_responsive';
import { DiscoverServices } from '../../../../../build_services';
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
-import { DiscoverSidebar } from './discover_sidebar';
const mockServices = ({
history: () => ({
@@ -132,14 +131,4 @@ describe('discover responsive sidebar', function () {
findTestSubject(comp, 'plus-extension-gif').simulate('click');
expect(props.onAddFilter).toHaveBeenCalled();
});
- it('renders sidebar with unmapped fields config', function () {
- const unmappedFieldsConfig = {
- showUnmappedFields: false,
- };
- const componentProps = { ...props, unmappedFieldsConfig };
- const component = mountWithIntl( );
- const discoverSidebar = component.find(DiscoverSidebar);
- expect(discoverSidebar).toHaveLength(1);
- expect(discoverSidebar.props().unmappedFieldsConfig).toEqual(unmappedFieldsConfig);
- });
});
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx
index cc33601f77728..003bb22599e48 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx
@@ -105,15 +105,6 @@ export interface DiscoverSidebarResponsiveProps {
* Read from the Fields API
*/
useNewFieldsApi?: boolean;
- /**
- * an object containing properties for proper handling of unmapped fields
- */
- unmappedFieldsConfig?: {
- /**
- * determines whether to display unmapped fields
- */
- showUnmappedFields: boolean;
- };
/**
* callback to execute on edit runtime field
*/
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts
index 5869720635621..cd9f6b3cac4a5 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts
@@ -244,8 +244,7 @@ describe('group_fields', function () {
5,
fieldCounts,
fieldFilterState,
- true,
- false
+ true
);
expect(actual.unpopular).toEqual([]);
});
@@ -270,8 +269,7 @@ describe('group_fields', function () {
5,
fieldCounts,
fieldFilterState,
- false,
- undefined
+ false
);
expect(actual.unpopular.map((field) => field.name)).toEqual(['unknown_field']);
});
diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx
index dc6cbcedc8086..2007d32fe84be 100644
--- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx
+++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx
@@ -24,9 +24,9 @@ export function groupFields(
popularLimit: number,
fieldCounts: Record,
fieldFilterState: FieldFilterState,
- useNewFieldsApi: boolean,
- showUnmappedFields = true
+ useNewFieldsApi: boolean
): GroupedFields {
+ const showUnmappedFields = useNewFieldsApi;
const result: GroupedFields = {
selected: [],
popular: [],
diff --git a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx
index 5cc7147b49ff9..07939fff6e7f4 100644
--- a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx
+++ b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx
@@ -5,15 +5,12 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-import React, { useMemo, useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect } from 'react';
import { History } from 'history';
import { DiscoverLayout } from './components/layout';
-import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common';
-import { useSavedSearch as useSavedSearchData } from './services/use_saved_search';
import { setBreadcrumbsTitle } from '../../helpers/breadcrumbs';
import { addHelpMenuToAppChrome } from '../../components/help_menu/help_menu_util';
import { useDiscoverState } from './services/use_discover_state';
-import { useSearchSession } from './services/use_search_session';
import { useUrl } from './services/use_url';
import { IndexPattern, IndexPatternAttributes, SavedObject } from '../../../../../data/common';
import { DiscoverServices } from '../../../build_services';
@@ -55,18 +52,20 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
const { services, history, navigateTo, indexPatternList } = props.opts;
const { chrome, docLinks, uiSettings: config, data } = services;
- const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]);
-
/**
* State related logic
*/
const {
- stateContainer,
- state,
+ data$,
indexPattern,
- searchSource,
- savedSearch,
+ onChangeIndexPattern,
+ onUpdateQuery,
+ refetch$,
resetSavedSearch,
+ savedSearch,
+ searchSource,
+ state,
+ stateContainer,
} = useDiscoverState({
services,
history,
@@ -79,25 +78,6 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
*/
useUrl({ history, resetSavedSearch });
- /**
- * Search session logic
- */
- const searchSessionManager = useSearchSession({ services, history, stateContainer, savedSearch });
-
- /**
- * Data fetching logic
- */
- const { data$, refetch$ } = useSavedSearchData({
- indexPattern,
- savedSearch,
- searchSessionManager,
- searchSource,
- services,
- state,
- stateContainer,
- useNewFieldsApi,
- });
-
/**
* SavedSearch depended initializing
*/
@@ -115,11 +95,6 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
*/
useEffect(() => {
addHelpMenuToAppChrome(chrome, docLinks);
- stateContainer.replaceUrlAppState({}).then(() => {
- stateContainer.startSync();
- });
-
- return () => stateContainer.stopSync();
}, [stateContainer, chrome, docLinks]);
const resetQuery = useCallback(() => {
@@ -130,12 +105,13 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
;
+ /**
+ * Function starting state sync when Discover main is loaded
+ */
+ initializeAndSync: (
+ indexPattern: IndexPattern,
+ filterManager: FilterManager,
+ data: DataPublicPluginStart
+ ) => () => void;
/**
* Start sync between state and URL
*/
@@ -204,16 +216,18 @@ export function getState({
stateStorage,
});
+ const replaceUrlAppState = async (newPartial: AppState = {}) => {
+ const state = { ...appStateContainer.getState(), ...newPartial };
+ await stateStorage.set(APP_STATE_URL_KEY, state, { replace: true });
+ };
+
return {
kbnUrlStateStorage: stateStorage,
appStateContainer: appStateContainerModified,
startSync: start,
stopSync: stop,
setAppState: (newPartial: AppState) => setState(appStateContainerModified, newPartial),
- replaceUrlAppState: async (newPartial: AppState = {}) => {
- const state = { ...appStateContainer.getState(), ...newPartial };
- await stateStorage.set(APP_STATE_URL_KEY, state, { replace: true });
- },
+ replaceUrlAppState,
resetInitialAppState: () => {
initialAppState = appStateContainer.getState();
},
@@ -224,6 +238,50 @@ export function getState({
getPreviousAppState: () => previousAppState,
flushToUrl: () => stateStorage.kbnUrlControls.flush(),
isAppStateDirty: () => !isEqualState(initialAppState, appStateContainer.getState()),
+ initializeAndSync: (
+ indexPattern: IndexPattern,
+ filterManager: FilterManager,
+ data: DataPublicPluginStart
+ ) => {
+ if (appStateContainer.getState().index !== indexPattern.id) {
+ // used index pattern is different than the given by url/state which is invalid
+ setState(appStateContainerModified, { index: indexPattern.id });
+ }
+ // sync initial app filters from state to filterManager
+ const filters = appStateContainer.getState().filters;
+ if (filters) {
+ filterManager.setAppFilters(cloneDeep(filters));
+ }
+ const query = appStateContainer.getState().query;
+ if (query) {
+ data.query.queryString.setQuery(query);
+ }
+
+ const stopSyncingQueryAppStateWithStateContainer = connectToQueryState(
+ data.query,
+ appStateContainer,
+ {
+ filters: esFilters.FilterStateStore.APP_STATE,
+ query: true,
+ }
+ );
+
+ // syncs `_g` portion of url with query services
+ const { stop: stopSyncingGlobalStateWithUrl } = syncQueryStateWithUrl(
+ data.query,
+ stateStorage
+ );
+
+ replaceUrlAppState({}).then(() => {
+ start();
+ });
+
+ return () => {
+ stopSyncingQueryAppStateWithStateContainer();
+ stopSyncingGlobalStateWithUrl();
+ stop();
+ };
+ },
};
}
diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts
index 051a2d2dcd9cc..4c3d819f063a0 100644
--- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts
+++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts
@@ -62,10 +62,6 @@ describe('test useDiscoverState', () => {
});
});
- await act(async () => {
- result.current.stateContainer.startSync();
- });
-
const initialColumns = result.current.state.columns;
await act(async () => {
result.current.setState({ columns: ['123'] });
diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts
index a3546d54cd493..3c736f09a8296 100644
--- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts
+++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts
@@ -6,19 +6,25 @@
* Side Public License, v 1.
*/
import { useMemo, useEffect, useState, useCallback } from 'react';
-import { cloneDeep } from 'lodash';
+import { isEqual } from 'lodash';
import { History } from 'history';
import { getState } from './discover_state';
import { getStateDefaults } from '../utils/get_state_defaults';
-import {
- esFilters,
- connectToQueryState,
- syncQueryStateWithUrl,
- IndexPattern,
-} from '../../../../../../data/public';
+import { IndexPattern } from '../../../../../../data/public';
import { DiscoverServices } from '../../../../build_services';
import { SavedSearch } from '../../../../saved_searches';
import { loadIndexPattern } from '../utils/resolve_index_pattern';
+import { useSavedSearch as useSavedSearchData } from './use_saved_search';
+import {
+ MODIFY_COLUMNS_ON_SWITCH,
+ SEARCH_FIELDS_FROM_SOURCE,
+ SEARCH_ON_PAGE_LOAD_SETTING,
+ SORT_DEFAULT_ORDER_SETTING,
+} from '../../../../../common';
+import { useSearchSession } from './use_search_session';
+import { FetchStatus } from '../../../types';
+import { getSwitchIndexPatternAppState } from '../utils/get_switch_index_pattern_app_state';
+import { SortPairArr } from '../../../angular/doc_table/lib/get_sort';
export function useDiscoverState({
services,
@@ -31,9 +37,11 @@ export function useDiscoverState({
history: History;
initialIndexPattern: IndexPattern;
}) {
- const { uiSettings: config, data, filterManager } = services;
+ const { uiSettings: config, data, filterManager, indexPatterns } = services;
const [indexPattern, setIndexPattern] = useState(initialIndexPattern);
const [savedSearch, setSavedSearch] = useState(initialSavedSearch);
+ const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]);
+ const timefilter = data.query.timefilter.timefilter;
const searchSource = useMemo(() => {
savedSearch.searchSource.setField('index', indexPattern);
@@ -57,73 +65,80 @@ export function useDiscoverState({
[config, data, history, savedSearch, services.core.notifications.toasts]
);
- const { appStateContainer, getPreviousAppState } = stateContainer;
+ const { appStateContainer } = stateContainer;
const [state, setState] = useState(appStateContainer.getState());
- useEffect(() => {
- if (stateContainer.appStateContainer.getState().index !== indexPattern.id) {
- // used index pattern is different than the given by url/state which is invalid
- stateContainer.setAppState({ index: indexPattern.id });
- }
- // sync initial app filters from state to filterManager
- const filters = appStateContainer.getState().filters;
- if (filters) {
- filterManager.setAppFilters(cloneDeep(filters));
- }
- const query = appStateContainer.getState().query;
- if (query) {
- data.query.queryString.setQuery(query);
- }
+ /**
+ * Search session logic
+ */
+ const searchSessionManager = useSearchSession({ services, history, stateContainer, savedSearch });
- const stopSyncingQueryAppStateWithStateContainer = connectToQueryState(
- data.query,
- appStateContainer,
- {
- filters: esFilters.FilterStateStore.APP_STATE,
- query: true,
- }
- );
+ const initialFetchStatus: FetchStatus = useMemo(() => {
+ // A saved search is created on every page load, so we check the ID to see if we're loading a
+ // previously saved search or if it is just transient
+ const shouldSearchOnPageLoad =
+ config.get(SEARCH_ON_PAGE_LOAD_SETTING) ||
+ savedSearch.id !== undefined ||
+ timefilter.getRefreshInterval().pause === false ||
+ searchSessionManager.hasSearchSessionIdInURL();
+ return shouldSearchOnPageLoad ? FetchStatus.LOADING : FetchStatus.UNINITIALIZED;
+ }, [config, savedSearch.id, searchSessionManager, timefilter]);
- // syncs `_g` portion of url with query services
- const { stop: stopSyncingGlobalStateWithUrl } = syncQueryStateWithUrl(
- data.query,
- stateContainer.kbnUrlStateStorage
- );
+ /**
+ * Data fetching logic
+ */
+ const { data$, refetch$, reset } = useSavedSearchData({
+ indexPattern,
+ initialFetchStatus,
+ searchSessionManager,
+ searchSource,
+ services,
+ stateContainer,
+ useNewFieldsApi,
+ });
+
+ useEffect(() => {
+ const stopSync = stateContainer.initializeAndSync(indexPattern, filterManager, data);
return () => {
- stopSyncingQueryAppStateWithStateContainer();
- stopSyncingGlobalStateWithUrl();
+ stopSync();
};
- }, [
- appStateContainer,
- config,
- data.query,
- data.search.session,
- getPreviousAppState,
- indexPattern.id,
- filterManager,
- services.indexPatterns,
- stateContainer,
- ]);
+ }, [stateContainer, filterManager, data, indexPattern]);
+ /**
+ * Track state changes that should trigger a fetch
+ */
useEffect(() => {
- const unsubscribe = stateContainer.appStateContainer.subscribe(async (nextState) => {
+ const unsubscribe = appStateContainer.subscribe(async (nextState) => {
+ const { hideChart, interval, sort, index } = state;
+ // chart was hidden, now it should be displayed, so data is needed
+ const chartDisplayChanged = nextState.hideChart !== hideChart && hideChart;
+ const chartIntervalChanged = nextState.interval !== interval;
+ const docTableSortChanged = !isEqual(nextState.sort, sort);
+ const indexPatternChanged = !isEqual(nextState.index, index);
// NOTE: this is also called when navigating from discover app to context app
- if (nextState.index && state.index !== nextState.index) {
- const nextIndexPattern = await loadIndexPattern(
- nextState.index,
- services.indexPatterns,
- config
- );
+ if (nextState.index && indexPatternChanged) {
+ /**
+ * Without resetting the fetch state, e.g. a time column would be displayed when switching
+ * from a index pattern without to a index pattern with time filter for a brief moment
+ * That's because appState is updated before savedSearchData$
+ * The following line of code catches this, but should be improved
+ */
+ reset();
+ const nextIndexPattern = await loadIndexPattern(nextState.index, indexPatterns, config);
if (nextIndexPattern) {
setIndexPattern(nextIndexPattern.loaded);
}
}
+
+ if (chartDisplayChanged || chartIntervalChanged || docTableSortChanged) {
+ refetch$.next();
+ }
setState(nextState);
});
return () => unsubscribe();
- }, [config, services.indexPatterns, state.index, stateContainer.appStateContainer, setState]);
+ }, [config, indexPatterns, appStateContainer, setState, state, refetch$, data$, reset]);
const resetSavedSearch = useCallback(
async (id?: string) => {
@@ -143,13 +158,62 @@ export function useDiscoverState({
[services, indexPattern, config, data, stateContainer, savedSearch.id]
);
+ /**
+ * Function triggered when user changes index pattern in the sidebar
+ */
+ const onChangeIndexPattern = useCallback(
+ async (id: string) => {
+ const nextIndexPattern = await indexPatterns.get(id);
+ if (nextIndexPattern && indexPattern) {
+ const nextAppState = getSwitchIndexPatternAppState(
+ indexPattern,
+ nextIndexPattern,
+ state.columns || [],
+ (state.sort || []) as SortPairArr[],
+ config.get(MODIFY_COLUMNS_ON_SWITCH),
+ config.get(SORT_DEFAULT_ORDER_SETTING)
+ );
+ stateContainer.setAppState(nextAppState);
+ }
+ },
+ [config, indexPattern, indexPatterns, state.columns, state.sort, stateContainer]
+ );
+ /**
+ * Function triggered when the user changes the query in the search bar
+ */
+ const onUpdateQuery = useCallback(
+ (_payload, isUpdate?: boolean) => {
+ if (isUpdate === false) {
+ searchSessionManager.removeSearchSessionIdFromURL({ replace: false });
+ refetch$.next();
+ }
+ },
+ [refetch$, searchSessionManager]
+ );
+
+ /**
+ * Initial data fetching, also triggered when index pattern changes
+ */
+ useEffect(() => {
+ if (!indexPattern) {
+ return;
+ }
+ if (initialFetchStatus === FetchStatus.LOADING) {
+ refetch$.next();
+ }
+ }, [initialFetchStatus, refetch$, indexPattern, data$]);
+
return {
- state,
- setState,
- stateContainer,
+ data$,
indexPattern,
- searchSource,
- savedSearch,
+ refetch$,
resetSavedSearch,
+ onChangeIndexPattern,
+ onUpdateQuery,
+ savedSearch,
+ searchSource,
+ setState,
+ state,
+ stateContainer,
};
}
diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts
index 5976c8fea5ea4..128c94f284f56 100644
--- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts
+++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts
@@ -12,9 +12,10 @@ import { discoverServiceMock } from '../../../../__mocks__/services';
import { savedSearchMock } from '../../../../__mocks__/saved_search';
import { indexPatternMock } from '../../../../__mocks__/index_pattern';
import { useSavedSearch } from './use_saved_search';
-import { AppState, getState } from './discover_state';
+import { getState } from './discover_state';
import { uiSettingsMock } from '../../../../__mocks__/ui_settings';
import { useDiscoverState } from './use_discover_state';
+import { FetchStatus } from '../../../types';
describe('test useSavedSearch', () => {
test('useSavedSearch return is valid', async () => {
@@ -28,11 +29,10 @@ describe('test useSavedSearch', () => {
const { result } = renderHook(() => {
return useSavedSearch({
indexPattern: indexPatternMock,
- savedSearch: savedSearchMock,
+ initialFetchStatus: FetchStatus.LOADING,
searchSessionManager,
searchSource: savedSearchMock.searchSource.createCopy(),
services: discoverServiceMock,
- state: {} as AppState,
stateContainer,
useNewFieldsApi: true,
});
@@ -69,11 +69,10 @@ describe('test useSavedSearch', () => {
const { result, waitForValueToChange } = renderHook(() => {
return useSavedSearch({
indexPattern: indexPatternMock,
- savedSearch: savedSearchMock,
+ initialFetchStatus: FetchStatus.LOADING,
searchSessionManager,
searchSource: resultState.current.searchSource,
services: discoverServiceMock,
- state: {} as AppState,
stateContainer,
useNewFieldsApi: true,
});
@@ -88,4 +87,47 @@ describe('test useSavedSearch', () => {
expect(result.current.data$.value.hits).toBe(0);
expect(result.current.data$.value.rows).toEqual([]);
});
+
+ test('reset sets back to initial state', async () => {
+ const { history, searchSessionManager } = createSearchSessionMock();
+ const stateContainer = getState({
+ getStateDefaults: () => ({ index: 'the-index-pattern-id' }),
+ history,
+ uiSettings: uiSettingsMock,
+ });
+
+ discoverServiceMock.data.query.timefilter.timefilter.getTime = jest.fn(() => {
+ return { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' };
+ });
+
+ const { result: resultState } = renderHook(() => {
+ return useDiscoverState({
+ services: discoverServiceMock,
+ history,
+ initialIndexPattern: indexPatternMock,
+ initialSavedSearch: savedSearchMock,
+ });
+ });
+
+ const { result, waitForValueToChange } = renderHook(() => {
+ return useSavedSearch({
+ indexPattern: indexPatternMock,
+ initialFetchStatus: FetchStatus.LOADING,
+ searchSessionManager,
+ searchSource: resultState.current.searchSource,
+ services: discoverServiceMock,
+ stateContainer,
+ useNewFieldsApi: true,
+ });
+ });
+
+ result.current.refetch$.next();
+
+ await waitForValueToChange(() => {
+ return result.current.data$.value.state === FetchStatus.COMPLETE;
+ });
+
+ result.current.reset();
+ expect(result.current.data$.value.state).toBe(FetchStatus.LOADING);
+ });
});
diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
index 2b0d951724869..8c847b54078eb 100644
--- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
+++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts
@@ -5,11 +5,10 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-import { useEffect, useRef, useCallback, useMemo } from 'react';
+import { useEffect, useRef, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { merge, Subject, BehaviorSubject } from 'rxjs';
import { debounceTime, tap, filter } from 'rxjs/operators';
-import { isEqual } from 'lodash';
import { DiscoverServices } from '../../../../build_services';
import { DiscoverSearchSessionManager } from './discover_search_session';
import {
@@ -18,13 +17,11 @@ import {
SearchSource,
tabifyAggResponse,
} from '../../../../../../data/common';
-import { SavedSearch } from '../../../../saved_searches';
-import { AppState, GetStateReturn } from './discover_state';
+import { GetStateReturn } from './discover_state';
import { ElasticSearchHit } from '../../../doc_views/doc_views_types';
import { RequestAdapter } from '../../../../../../inspector/public';
import { AutoRefreshDoneFn, search } from '../../../../../../data/public';
import { calcFieldCounts } from '../utils/calc_field_counts';
-import { SEARCH_ON_PAGE_LOAD_SETTING } from '../../../../../common';
import { validateTimeRange } from '../utils/validate_time_range';
import { updateSearchSource } from '../utils/update_search_source';
import { SortOrder } from '../../../../saved_searches/types';
@@ -40,6 +37,7 @@ export type SavedSearchRefetchSubject = Subject;
export interface UseSavedSearch {
refetch$: SavedSearchRefetchSubject;
data$: SavedSearchDataSubject;
+ reset: () => void;
}
export type SavedSearchRefetchMsg = 'reset' | undefined;
@@ -59,48 +57,27 @@ export interface SavedSearchDataMessage {
/**
* This hook return 2 observables, refetch$ allows to trigger data fetching, data$ to subscribe
* to the data fetching
- * @param indexPattern
- * @param savedSearch
- * @param searchSessionManager
- * @param searchSource
- * @param services
- * @param state
- * @param stateContainer
- * @param useNewFieldsApi
*/
export const useSavedSearch = ({
indexPattern,
- savedSearch,
+ initialFetchStatus,
searchSessionManager,
searchSource,
services,
- state,
stateContainer,
useNewFieldsApi,
}: {
indexPattern: IndexPattern;
- savedSearch: SavedSearch;
+ initialFetchStatus: FetchStatus;
searchSessionManager: DiscoverSearchSessionManager;
searchSource: SearchSource;
services: DiscoverServices;
- state: AppState;
stateContainer: GetStateReturn;
useNewFieldsApi: boolean;
}): UseSavedSearch => {
- const { data, filterManager, uiSettings } = services;
+ const { data, filterManager } = services;
const timefilter = data.query.timefilter.timefilter;
- const initFetchState: FetchStatus = useMemo(() => {
- // A saved search is created on every page load, so we check the ID to see if we're loading a
- // previously saved search or if it is just transient
- const shouldSearchOnPageLoad =
- uiSettings.get(SEARCH_ON_PAGE_LOAD_SETTING) ||
- savedSearch.id !== undefined ||
- timefilter.getRefreshInterval().pause === false ||
- searchSessionManager.hasSearchSessionIdInURL();
- return shouldSearchOnPageLoad ? FetchStatus.LOADING : FetchStatus.UNINITIALIZED;
- }, [uiSettings, savedSearch.id, searchSessionManager, timefilter]);
-
/**
* The observable the UI (aka React component) subscribes to get notified about
* the changes in the data fetching process (high level: fetching started, data was received)
@@ -108,7 +85,7 @@ export const useSavedSearch = ({
const data$: SavedSearchDataSubject = useSingleton(
() =>
new BehaviorSubject({
- state: initFetchState,
+ state: initialFetchStatus,
})
);
/**
@@ -123,15 +100,14 @@ export const useSavedSearch = ({
*/
const refs = useRef<{
abortController?: AbortController;
- /**
- * used to compare a new state against an old one, to evaluate if data needs to be fetched
- */
- appState: AppState;
/**
* handler emitted by `timefilter.getAutoRefreshFetch$()`
* to notify when data completed loading and to start a new autorefresh loop
*/
autoRefreshDoneCb?: AutoRefreshDoneFn;
+ /**
+ * Number of fetches used for functional testing
+ */
fetchCounter: number;
/**
* needed to right auto refresh behavior, a new auto refresh shouldnt be triggered when
@@ -144,12 +120,34 @@ export const useSavedSearch = ({
*/
fieldCounts: Record;
}>({
- appState: state,
fetchCounter: 0,
fieldCounts: {},
- fetchStatus: initFetchState,
+ fetchStatus: initialFetchStatus,
});
+ /**
+ * Resets the fieldCounts cache and sends a reset message
+ * It is set to initial state (no documents, fetchCounter to 0)
+ * Needed when index pattern is switched or a new runtime field is added
+ */
+ const sendResetMsg = useCallback(
+ (fetchStatus?: FetchStatus) => {
+ refs.current.fieldCounts = {};
+ refs.current.fetchStatus = fetchStatus ?? initialFetchStatus;
+ data$.next({
+ state: initialFetchStatus,
+ fetchCounter: 0,
+ rows: [],
+ fieldCounts: {},
+ chartData: undefined,
+ bucketInterval: undefined,
+ });
+ },
+ [data$, initialFetchStatus]
+ );
+ /**
+ * Function to fetch data from ElasticSearch
+ */
const fetchAll = useCallback(
(reset = false) => {
if (!validateTimeRange(timefilter.getTime(), services.toastNotifications)) {
@@ -161,23 +159,18 @@ export const useSavedSearch = ({
refs.current.abortController = new AbortController();
const sessionId = searchSessionManager.getNextSearchSessionId();
- // Let the UI know, data fetching started
- const loadingMessage: SavedSearchDataMessage = {
- state: FetchStatus.LOADING,
- fetchCounter: ++refs.current.fetchCounter,
- };
-
if (reset) {
- // when runtime field was added, changed, deleted, index pattern was switched
- loadingMessage.rows = [];
- loadingMessage.fieldCounts = {};
- loadingMessage.chartData = undefined;
- loadingMessage.bucketInterval = undefined;
+ sendResetMsg(FetchStatus.LOADING);
+ } else {
+ // Let the UI know, data fetching started
+ data$.next({
+ state: FetchStatus.LOADING,
+ fetchCounter: ++refs.current.fetchCounter,
+ });
+ refs.current.fetchStatus = FetchStatus.LOADING;
}
- data$.next(loadingMessage);
- refs.current.fetchStatus = loadingMessage.state;
- const { sort } = stateContainer.appStateContainer.getState();
+ const { sort, hideChart, interval } = stateContainer.appStateContainer.getState();
updateSearchSource(searchSource, false, {
indexPattern,
services,
@@ -185,8 +178,8 @@ export const useSavedSearch = ({
useNewFieldsApi,
});
const chartAggConfigs =
- indexPattern.timeFieldName && !state.hideChart && state.interval
- ? getChartAggConfigs(searchSource, state.interval, data)
+ indexPattern.timeFieldName && !hideChart && interval
+ ? getChartAggConfigs(searchSource, interval, data)
: undefined;
if (!chartAggConfigs) {
@@ -217,16 +210,12 @@ export const useSavedSearch = ({
state: FetchStatus.COMPLETE,
rows: documents,
inspectorAdapters,
- fieldCounts: calcFieldCounts(
- reset ? {} : refs.current.fieldCounts,
- documents,
- indexPattern
- ),
+ fieldCounts: calcFieldCounts(refs.current.fieldCounts, documents, indexPattern),
hits: res.rawResponse.hits.total as number,
};
if (chartAggConfigs) {
- const bucketAggConfig = chartAggConfigs!.aggs[1];
+ const bucketAggConfig = chartAggConfigs.aggs[1];
const tabifiedData = tabifyAggResponse(chartAggConfigs, res.rawResponse);
const dimensions = getDimensions(chartAggConfigs, data);
if (dimensions) {
@@ -259,14 +248,13 @@ export const useSavedSearch = ({
[
timefilter,
services,
+ searchSessionManager,
stateContainer.appStateContainer,
searchSource,
indexPattern,
useNewFieldsApi,
- state.hideChart,
- state.interval,
data,
- searchSessionManager,
+ sendResetMsg,
data$,
]
);
@@ -306,32 +294,9 @@ export const useSavedSearch = ({
fetchAll,
]);
- /**
- * Track state changes that should trigger a fetch
- */
- useEffect(() => {
- const prevAppState = refs.current.appState;
-
- // chart was hidden, now it should be displayed, so data is needed
- const chartDisplayChanged = state.hideChart !== prevAppState.hideChart && !state.hideChart;
- const chartIntervalChanged = state.interval !== prevAppState.interval;
- const docTableSortChanged = !isEqual(state.sort, prevAppState.sort);
- const indexPatternChanged = !isEqual(state.index, prevAppState.index);
-
- refs.current.appState = state;
- if (chartDisplayChanged || chartIntervalChanged || docTableSortChanged || indexPatternChanged) {
- refetch$.next(indexPatternChanged ? 'reset' : undefined);
- }
- }, [refetch$, state.interval, state.sort, state]);
-
- useEffect(() => {
- if (initFetchState === FetchStatus.LOADING) {
- refetch$.next();
- }
- }, [initFetchState, refetch$]);
-
return {
refetch$,
data$,
+ reset: sendResetMsg,
};
};
From 19d2d17158e27597df713a7f4f5c999ba83632fa Mon Sep 17 00:00:00 2001
From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
Date: Tue, 22 Jun 2021 12:25:10 -0400
Subject: [PATCH 079/163] [Security Solution][Endpoint] Rename `Unisolating`
and other like words to `Releasing` (#102582)
* Update all instances of `unisolate` to `release` (along with variation of unisolate)
* just width of Agent Status column on endpoint list
---
.../host_isolation/endpoint_host_isolation_status.test.tsx | 4 ++--
.../host_isolation/endpoint_host_isolation_status.tsx | 6 +++---
.../components/endpoint/host_isolation/translations.ts | 6 +++---
.../detections/components/host_isolation/translations.ts | 2 +-
.../view/components/endpoint_agent_status.test.tsx | 4 +---
.../endpoint_hosts/view/hooks/use_endpoint_action_items.tsx | 2 +-
.../public/management/pages/endpoint_hosts/view/index.tsx | 4 ++--
.../management/pages/endpoint_hosts/view/translations.ts | 6 +++---
8 files changed, 16 insertions(+), 18 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx
index 44405748b6373..4ceacc40942e2 100644
--- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.test.tsx
@@ -44,8 +44,8 @@ describe('when using the EndpointHostIsolationStatus component', () => {
});
it.each([
- ['Isolating pending', { pendingIsolate: 2 }],
- ['Unisolating pending', { pendingUnIsolate: 2 }],
+ ['Isolating', { pendingIsolate: 2 }],
+ ['Releasing', { pendingUnIsolate: 2 }],
['4 actions pending', { isIsolated: true, pendingUnIsolate: 2, pendingIsolate: 2 }],
])('should show %s}', (expectedLabel, componentProps) => {
const { getByTestId } = render(componentProps);
diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx
index 0fe3a8e4337cb..7ae7cae89f19e 100644
--- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx
@@ -74,7 +74,7 @@ export const EndpointHostIsolationStatus = memo
{pendingUnIsolate}
@@ -101,12 +101,12 @@ export const EndpointHostIsolationStatus = memo
) : (
)}
diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts
index 790c951f61ccd..66d9bf3a7c71b 100644
--- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts
+++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts
@@ -26,13 +26,13 @@ export const COMMENT_PLACEHOLDER = i18n.translate(
export const GET_ISOLATION_SUCCESS_MESSAGE = (hostName: string) =>
i18n.translate('xpack.securitySolution.endpoint.hostIsolation.isolation.successfulMessage', {
- defaultMessage: 'Host Isolation on {hostName} successfully submitted',
+ defaultMessage: 'Isolation on host {hostName} successfully submitted',
values: { hostName },
});
export const GET_UNISOLATION_SUCCESS_MESSAGE = (hostName: string) =>
i18n.translate('xpack.securitySolution.endpoint.hostIsolation.unisolate.successfulMessage', {
- defaultMessage: 'Host Unisolation on {hostName} successfully submitted',
+ defaultMessage: 'Release on host {hostName} successfully submitted',
values: { hostName },
});
@@ -41,7 +41,7 @@ export const ISOLATE = i18n.translate('xpack.securitySolution.endpoint.hostisola
});
export const UNISOLATE = i18n.translate('xpack.securitySolution.endpoint.hostisolation.unisolate', {
- defaultMessage: 'unisolate',
+ defaultMessage: 'release',
});
export const NOT_ISOLATED = i18n.translate(
diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts
index 98b74817cabb6..58667c26ce2e6 100644
--- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts
@@ -17,7 +17,7 @@ export const ISOLATE_HOST = i18n.translate(
export const UNISOLATE_HOST = i18n.translate(
'xpack.securitySolution.endpoint.hostIsolation.unisolateHost',
{
- defaultMessage: 'Unisolate host',
+ defaultMessage: 'Release host',
}
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx
index 9010bb5785c1d..a860e3c45deee 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx
@@ -82,9 +82,7 @@ describe('When using the EndpointAgentStatus component', () => {
});
it('should show host pending action', () => {
- expect(renderResult.getByTestId('rowIsolationStatus').textContent).toEqual(
- 'Isolating pending'
- );
+ expect(renderResult.getByTestId('rowIsolationStatus').textContent).toEqual('Isolating');
});
});
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx
index 7c38c935a0b9f..408e1794ef680 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx
@@ -76,7 +76,7 @@ export const useEndpointActionItems = (
children: (
),
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
index d1dab3dd07a7e..9316d2539d133 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
@@ -272,7 +272,7 @@ export const EndpointList = () => {
},
{
field: 'host_status',
- width: '9%',
+ width: '14%',
name: i18n.translate('xpack.securitySolution.endpoint.list.hostStatus', {
defaultMessage: 'Agent Status',
}),
@@ -356,7 +356,7 @@ export const EndpointList = () => {
},
{
field: 'metadata.host.os.name',
- width: '10%',
+ width: '9%',
name: i18n.translate('xpack.securitySolution.endpoint.list.os', {
defaultMessage: 'Operating System',
}),
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts
index 1a7889f22db16..18a5bd1e5130a 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts
@@ -26,7 +26,7 @@ export const ACTIVITY_LOG = {
unisolatedAction: i18n.translate(
'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.unisolated',
{
- defaultMessage: 'unisolated host',
+ defaultMessage: 'released host',
}
),
},
@@ -46,13 +46,13 @@ export const ACTIVITY_LOG = {
unisolationSuccessful: i18n.translate(
'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationSuccessful',
{
- defaultMessage: 'host unisolation successful',
+ defaultMessage: 'host release successful',
}
),
unisolationFailed: i18n.translate(
'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationFailed',
{
- defaultMessage: 'host unisolation failed',
+ defaultMessage: 'host release failed',
}
),
},
From 5ffe26cbf3e0ef0764a7c296634cd0ed1214b7f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?=
Date: Tue, 22 Jun 2021 12:30:40 -0400
Subject: [PATCH 080/163] Add "Unable to decrypt attribute apiKey" to the
alerting troubleshooting docs (#101315)
* Initial commit
* PR feedback
* PR feedback pt 2
* PR feedback pt 3
---
.../alerting-troubleshooting.asciidoc | 27 ++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/docs/user/alerting/alerting-troubleshooting.asciidoc b/docs/user/alerting/alerting-troubleshooting.asciidoc
index b7b0c749dfe14..08655508b3cba 100644
--- a/docs/user/alerting/alerting-troubleshooting.asciidoc
+++ b/docs/user/alerting/alerting-troubleshooting.asciidoc
@@ -12,6 +12,32 @@ If your problem isn’t described here, please review open issues in the followi
Have a question? Contact us in the https://discuss.elastic.co/[discuss forum].
+[float]
+[[rule-cannot-decrypt-api-key]]
+=== Rule cannot decrypt apiKey
+
+*Problem*:
+
+The rule fails to execute and has an `Unable to decrypt attribute "apiKey"` error.
+
+*Solution*:
+
+This error happens when the `xpack.encryptedSavedObjects.encryptionKey` value used to create the rule does not match the value used during rule execution. Depending on the scenario, there are different ways to solve this problem:
+
+[cols="2*<"]
+|===
+
+| If the value in `xpack.encryptedSavedObjects.encryptionKey` was manually changed, and the previous encryption key is still known.
+| Ensure any previous encryption key is included in the keys used for <>.
+
+| If another {kib} instance with a different encryption key connects to the cluster.
+| The other {kib} instance might be trying to run the rule using a different encryption key than what the rule was created with. Ensure the encryption keys among all the {kib} instances are the same, and setting <> for previously used encryption keys.
+
+| If other scenarios don't apply.
+| Generate a new API key for the rule by disabling then enabling the rule.
+
+|===
+
[float]
[[rules-small-check-interval-run-late]]
=== Rules with small check intervals run late
@@ -29,7 +55,6 @@ Either tweak the <> or increa
For more details, see <>.
-
[float]
[[scheduled-rules-run-late]]
=== Rules run late
From 81e1debf7407a07e54810fcab2ddb274fe5ea767 Mon Sep 17 00:00:00 2001
From: Maja Grubic
Date: Tue, 22 Jun 2021 17:33:23 +0100
Subject: [PATCH 081/163] [Discover] Add source to doc viewer (#101392)
* [Discover] Add source to doc viewer
* Updating a unit test
* Fix typescript errors
* Add unit test
* Add a functional test
* Fixing a typo
* Remove unnecessary import
* Always request fields and source
* Remove unused import
* Move initialization of SourceViewer back to setup
* Trying to get rid of null value
* Readding null
* Try to get rid of null value
* Addressing PR comments
* Return early if jsonValue is not set
* Fix loading spinner style
* Add refresh on error
* Fix error message
* Add loading indicator on an empty string
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../public/application/components/doc/doc.tsx | 7 +-
.../components/doc/elastic_request_state.ts | 15 +
.../components/doc/use_es_doc_search.test.tsx | 39 +-
.../components/doc/use_es_doc_search.ts | 84 +-
.../json_code_editor.test.tsx.snap | 60 +-
.../json_code_editor/json_code_editor.scss | 2 +-
.../json_code_editor/json_code_editor.tsx | 59 +-
.../json_code_editor_common.tsx | 86 ++
.../__snapshots__/source_viewer.test.tsx.snap | 760 ++++++++++++++++++
.../source_viewer/source_viewer.scss | 14 +
.../source_viewer/source_viewer.test.tsx | 118 +++
.../source_viewer/source_viewer.tsx | 129 +++
src/plugins/discover/public/plugin.tsx | 15 +-
.../apps/discover/_discover_fields_api.ts | 9 +
test/functional/page_objects/discover_page.ts | 8 +
15 files changed, 1248 insertions(+), 157 deletions(-)
create mode 100644 src/plugins/discover/public/application/components/doc/elastic_request_state.ts
create mode 100644 src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx
create mode 100644 src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap
create mode 100644 src/plugins/discover/public/application/components/source_viewer/source_viewer.scss
create mode 100644 src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx
create mode 100644 src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx
diff --git a/src/plugins/discover/public/application/components/doc/doc.tsx b/src/plugins/discover/public/application/components/doc/doc.tsx
index e38709b465174..ed8bcf30d2bd1 100644
--- a/src/plugins/discover/public/application/components/doc/doc.tsx
+++ b/src/plugins/discover/public/application/components/doc/doc.tsx
@@ -10,9 +10,10 @@ import React from 'react';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent, EuiPage } from '@elastic/eui';
import { IndexPatternsContract } from 'src/plugins/data/public';
-import { ElasticRequestState, useEsDocSearch } from './use_es_doc_search';
+import { useEsDocSearch } from './use_es_doc_search';
import { getServices } from '../../../kibana_services';
import { DocViewer } from '../doc_viewer/doc_viewer';
+import { ElasticRequestState } from './elastic_request_state';
export interface DocProps {
/**
@@ -32,6 +33,10 @@ export interface DocProps {
* IndexPatternService to get a given index pattern by ID
*/
indexPatternService: IndexPatternsContract;
+ /**
+ * If set, will always request source, regardless of the global `fieldsFromSource` setting
+ */
+ requestSource?: boolean;
}
export function Doc(props: DocProps) {
diff --git a/src/plugins/discover/public/application/components/doc/elastic_request_state.ts b/src/plugins/discover/public/application/components/doc/elastic_request_state.ts
new file mode 100644
index 0000000000000..241e37c47a7e7
--- /dev/null
+++ b/src/plugins/discover/public/application/components/doc/elastic_request_state.ts
@@ -0,0 +1,15 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export enum ElasticRequestState {
+ Loading,
+ NotFound,
+ Found,
+ Error,
+ NotFoundIndexPattern,
+}
diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx
index f3a6b274649f5..9fdb564cb518d 100644
--- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx
+++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx
@@ -7,11 +7,12 @@
*/
import { renderHook, act } from '@testing-library/react-hooks';
-import { buildSearchBody, useEsDocSearch, ElasticRequestState } from './use_es_doc_search';
+import { buildSearchBody, useEsDocSearch } from './use_es_doc_search';
import { DocProps } from './doc';
import { Observable } from 'rxjs';
import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../common';
import { IndexPattern } from 'src/plugins/data/common';
+import { ElasticRequestState } from './elastic_request_state';
const mockSearchResult = new Observable();
@@ -88,6 +89,36 @@ describe('Test of helper / hook', () => {
`);
});
+ test('buildSearchBody with requestSource', () => {
+ const indexPattern = ({
+ getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }),
+ } as unknown) as IndexPattern;
+ const actual = buildSearchBody('1', indexPattern, true, true);
+ expect(actual).toMatchInlineSnapshot(`
+ Object {
+ "body": Object {
+ "_source": true,
+ "fields": Array [
+ Object {
+ "field": "*",
+ "include_unmapped": "true",
+ },
+ ],
+ "query": Object {
+ "ids": Object {
+ "values": Array [
+ "1",
+ ],
+ },
+ },
+ "runtime_mappings": Object {},
+ "script_fields": Array [],
+ "stored_fields": Array [],
+ },
+ }
+ `);
+ });
+
test('buildSearchBody with runtime fields', () => {
const indexPattern = ({
getComputedFields: () => ({
@@ -155,7 +186,11 @@ describe('Test of helper / hook', () => {
await act(async () => {
hook = renderHook((p: DocProps) => useEsDocSearch(p), { initialProps: props });
});
- expect(hook.result.current).toEqual([ElasticRequestState.Loading, null, indexPattern]);
+ expect(hook.result.current.slice(0, 3)).toEqual([
+ ElasticRequestState.Loading,
+ null,
+ indexPattern,
+ ]);
expect(getMock).toHaveBeenCalled();
});
});
diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts
index 7a3320d43c8b5..71a32b758aca7 100644
--- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts
+++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts
@@ -6,23 +6,16 @@
* Side Public License, v 1.
*/
-import { useEffect, useState, useMemo } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import type { estypes } from '@elastic/elasticsearch';
-import { IndexPattern, getServices } from '../../../kibana_services';
+import { getServices, IndexPattern } from '../../../kibana_services';
import { DocProps } from './doc';
import { ElasticSearchHit } from '../../doc_views/doc_views_types';
import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common';
+import { ElasticRequestState } from './elastic_request_state';
type RequestBody = Pick;
-export enum ElasticRequestState {
- Loading,
- NotFound,
- Found,
- Error,
- NotFoundIndexPattern,
-}
-
/**
* helper function to build a query body for Elasticsearch
* https://www.elastic.co/guide/en/elasticsearch/reference/current//query-dsl-ids-query.html
@@ -30,7 +23,8 @@ export enum ElasticRequestState {
export function buildSearchBody(
id: string,
indexPattern: IndexPattern,
- useNewFieldsApi: boolean
+ useNewFieldsApi: boolean,
+ requestAllFields?: boolean
): RequestBody | undefined {
const computedFields = indexPattern.getComputedFields();
const runtimeFields = computedFields.runtimeFields as estypes.MappingRuntimeFields;
@@ -52,6 +46,9 @@ export function buildSearchBody(
// @ts-expect-error
request.body.fields = [{ field: '*', include_unmapped: 'true' }];
request.body.runtime_mappings = runtimeFields ? runtimeFields : {};
+ if (requestAllFields) {
+ request.body._source = true;
+ }
} else {
request.body._source = true;
}
@@ -67,47 +64,50 @@ export function useEsDocSearch({
index,
indexPatternId,
indexPatternService,
-}: DocProps): [ElasticRequestState, ElasticSearchHit | null, IndexPattern | null] {
+ requestSource,
+}: DocProps): [ElasticRequestState, ElasticSearchHit | null, IndexPattern | null, () => void] {
const [indexPattern, setIndexPattern] = useState(null);
const [status, setStatus] = useState(ElasticRequestState.Loading);
const [hit, setHit] = useState(null);
const { data, uiSettings } = useMemo(() => getServices(), []);
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
- useEffect(() => {
- async function requestData() {
- try {
- const indexPatternEntity = await indexPatternService.get(indexPatternId);
- setIndexPattern(indexPatternEntity);
+ const requestData = useCallback(async () => {
+ try {
+ const indexPatternEntity = await indexPatternService.get(indexPatternId);
+ setIndexPattern(indexPatternEntity);
- const { rawResponse } = await data.search
- .search({
- params: {
- index,
- body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi)?.body,
- },
- })
- .toPromise();
+ const { rawResponse } = await data.search
+ .search({
+ params: {
+ index,
+ body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi, requestSource)?.body,
+ },
+ })
+ .toPromise();
- const hits = rawResponse.hits;
+ const hits = rawResponse.hits;
- if (hits?.hits?.[0]) {
- setStatus(ElasticRequestState.Found);
- setHit(hits.hits[0]);
- } else {
- setStatus(ElasticRequestState.NotFound);
- }
- } catch (err) {
- if (err.savedObjectId) {
- setStatus(ElasticRequestState.NotFoundIndexPattern);
- } else if (err.status === 404) {
- setStatus(ElasticRequestState.NotFound);
- } else {
- setStatus(ElasticRequestState.Error);
- }
+ if (hits?.hits?.[0]) {
+ setStatus(ElasticRequestState.Found);
+ setHit(hits.hits[0]);
+ } else {
+ setStatus(ElasticRequestState.NotFound);
+ }
+ } catch (err) {
+ if (err.savedObjectId) {
+ setStatus(ElasticRequestState.NotFoundIndexPattern);
+ } else if (err.status === 404) {
+ setStatus(ElasticRequestState.NotFound);
+ } else {
+ setStatus(ElasticRequestState.Error);
}
}
+ }, [id, index, indexPatternId, indexPatternService, data.search, useNewFieldsApi, requestSource]);
+
+ useEffect(() => {
requestData();
- }, [id, index, indexPatternId, indexPatternService, data.search, useNewFieldsApi]);
- return [status, hit, indexPattern];
+ }, [requestData]);
+
+ return [status, hit, indexPattern, requestData];
}
diff --git a/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap b/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap
index 8f07614813495..31dd6347218b5 100644
--- a/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap
+++ b/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap
@@ -1,21 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`returns the \`JsonCodeEditor\` component 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
+ onEditorDidMount={[Function]}
+/>
`;
diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss
index 5521df5b363ac..335805ed28493 100644
--- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss
+++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss
@@ -1,3 +1,3 @@
.dscJsonCodeEditor {
- width: 100%
+ width: 100%;
}
diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx
index b8427bb6bbdd2..f1ecd3ae3b70b 100644
--- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx
+++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx
@@ -9,17 +9,8 @@
import './json_code_editor.scss';
import React, { useCallback } from 'react';
-import { i18n } from '@kbn/i18n';
-import { monaco, XJsonLang } from '@kbn/monaco';
-import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
-import { CodeEditor } from '../../../../../kibana_react/public';
-
-const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', {
- defaultMessage: 'Read only JSON view of an elasticsearch document',
-});
-const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel', {
- defaultMessage: 'Copy to clipboard',
-});
+import { monaco } from '@kbn/monaco';
+import { JsonCodeEditorCommon } from './json_code_editor_common';
interface JsonCodeEditorProps {
json: Record;
@@ -47,45 +38,11 @@ export const JsonCodeEditor = ({ json, width, hasLineNumbers }: JsonCodeEditorPr
}, []);
return (
-
-
-
-
-
- {(copy) => (
-
- {copyToClipboardLabel}
-
- )}
-
-
-
-
- {}}
- editorDidMount={setEditorCalculatedHeight}
- aria-label={codeEditorAriaLabel}
- options={{
- automaticLayout: true,
- fontSize: 12,
- lineNumbers: hasLineNumbers ? 'on' : 'off',
- minimap: {
- enabled: false,
- },
- overviewRulerBorder: false,
- readOnly: true,
- scrollbar: {
- alwaysConsumeMouseWheel: false,
- },
- scrollBeyondLastLine: false,
- wordWrap: 'on',
- wrappingIndent: 'indent',
- }}
- />
-
-
+
);
};
diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx
new file mode 100644
index 0000000000000..e5ab8bf4d1a0d
--- /dev/null
+++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx
@@ -0,0 +1,86 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import './json_code_editor.scss';
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { monaco, XJsonLang } from '@kbn/monaco';
+import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { CodeEditor } from '../../../../../kibana_react/public';
+
+const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', {
+ defaultMessage: 'Read only JSON view of an elasticsearch document',
+});
+const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel', {
+ defaultMessage: 'Copy to clipboard',
+});
+
+interface JsonCodeEditorCommonProps {
+ jsonValue: string;
+ onEditorDidMount: (editor: monaco.editor.IStandaloneCodeEditor) => void;
+ width?: string | number;
+ hasLineNumbers?: boolean;
+}
+
+export const JsonCodeEditorCommon = ({
+ jsonValue,
+ width,
+ hasLineNumbers,
+ onEditorDidMount,
+}: JsonCodeEditorCommonProps) => {
+ if (jsonValue === '') {
+ return null;
+ }
+ return (
+
+
+
+
+
+ {(copy) => (
+
+ {copyToClipboardLabel}
+
+ )}
+
+
+
+
+ {}}
+ editorDidMount={onEditorDidMount}
+ aria-label={codeEditorAriaLabel}
+ options={{
+ automaticLayout: true,
+ fontSize: 12,
+ lineNumbers: hasLineNumbers ? 'on' : 'off',
+ minimap: {
+ enabled: false,
+ },
+ overviewRulerBorder: false,
+ readOnly: true,
+ scrollbar: {
+ alwaysConsumeMouseWheel: false,
+ },
+ scrollBeyondLastLine: false,
+ wordWrap: 'on',
+ wrappingIndent: 'indent',
+ }}
+ />
+
+
+ );
+};
+
+export const JSONCodeEditorCommonMemoized = React.memo((props: JsonCodeEditorCommonProps) => {
+ return ;
+});
diff --git a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap
new file mode 100644
index 0000000000000..f40dbbbae1f87
--- /dev/null
+++ b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap
@@ -0,0 +1,760 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Source Viewer component renders error state 1`] = `
+
+
+ Could not fetch data at this time. Refresh the tab to try again.
+
+
+ Refresh
+
+
+ }
+ iconType="alert"
+ title={
+
+ An Error Occurred
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+ An Error Occurred
+
+
+
+
+
+
+
+
+ Could not fetch data at this time. Refresh the tab to try again.
+
+
+
+
+
+
+
+
+
+
+
+
+ Refresh
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Source Viewer component renders json code editor 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Copy to clipboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Source Viewer component renders loading state 1`] = `
+
+
+
+
+
+
+
+
+
+
+ Loading JSON
+
+
+
+
+
+
+
+`;
diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.scss b/src/plugins/discover/public/application/components/source_viewer/source_viewer.scss
new file mode 100644
index 0000000000000..224e84ca50b52
--- /dev/null
+++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.scss
@@ -0,0 +1,14 @@
+.sourceViewer__loading {
+ display: flex;
+ flex-direction: row;
+ justify-content: left;
+ flex: 1 0 100%;
+ text-align: center;
+ height: 100%;
+ width: 100%;
+ margin-top: $euiSizeS;
+}
+
+.sourceViewer__loadingSpinner {
+ margin-right: $euiSizeS;
+}
diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx
new file mode 100644
index 0000000000000..86433e5df6401
--- /dev/null
+++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx
@@ -0,0 +1,118 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { mountWithIntl } from '@kbn/test/jest';
+import { SourceViewer } from './source_viewer';
+import * as hooks from '../doc/use_es_doc_search';
+import * as useUiSettingHook from 'src/plugins/kibana_react/public/ui_settings/use_ui_setting';
+import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
+import { JsonCodeEditorCommon } from '../json_code_editor/json_code_editor_common';
+
+jest.mock('../../../kibana_services', () => ({
+ getServices: jest.fn(),
+}));
+
+import { getServices, IndexPattern } from '../../../kibana_services';
+
+const mockIndexPattern = {
+ getComputedFields: () => [],
+} as never;
+const getMock = jest.fn(() => Promise.resolve(mockIndexPattern));
+const mockIndexPatternService = ({
+ get: getMock,
+} as unknown) as IndexPattern;
+
+(getServices as jest.Mock).mockImplementation(() => ({
+ uiSettings: {
+ get: (key: string) => {
+ if (key === 'discover:useNewFieldsApi') {
+ return true;
+ }
+ },
+ },
+ data: {
+ indexPatternService: mockIndexPatternService,
+ },
+}));
+describe('Source Viewer component', () => {
+ test('renders loading state', () => {
+ jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [0, null, null, () => {}]);
+
+ const comp = mountWithIntl(
+
+ );
+ expect(comp).toMatchSnapshot();
+ const loadingIndicator = comp.find(EuiLoadingSpinner);
+ expect(loadingIndicator).not.toBe(null);
+ });
+
+ test('renders error state', () => {
+ jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [3, null, null, () => {}]);
+
+ const comp = mountWithIntl(
+
+ );
+ expect(comp).toMatchSnapshot();
+ const errorPrompt = comp.find(EuiEmptyPrompt);
+ expect(errorPrompt.length).toBe(1);
+ const refreshButton = comp.find(EuiButton);
+ expect(refreshButton.length).toBe(1);
+ });
+
+ test('renders json code editor', () => {
+ const mockHit = {
+ _index: 'logstash-2014.09.09',
+ _type: 'doc',
+ _id: 'id123',
+ _score: 1,
+ _source: {
+ message: 'Lorem ipsum dolor sit amet',
+ extension: 'html',
+ not_mapped: 'yes',
+ bytes: 100,
+ objectArray: [{ foo: true }],
+ relatedContent: {
+ test: 1,
+ },
+ scripted: 123,
+ _underscore: 123,
+ },
+ } as never;
+ jest
+ .spyOn(hooks, 'useEsDocSearch')
+ .mockImplementation(() => [2, mockHit, mockIndexPattern, () => {}]);
+ jest.spyOn(useUiSettingHook, 'useUiSetting').mockImplementation(() => {
+ return false;
+ });
+ const comp = mountWithIntl(
+
+ );
+ expect(comp).toMatchSnapshot();
+ const jsonCodeEditor = comp.find(JsonCodeEditorCommon);
+ expect(jsonCodeEditor).not.toBe(null);
+ });
+});
diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx
new file mode 100644
index 0000000000000..94a12c04613a9
--- /dev/null
+++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx
@@ -0,0 +1,129 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import './source_viewer.scss';
+
+import React, { useEffect, useState } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { monaco } from '@kbn/monaco';
+import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useEsDocSearch } from '../doc/use_es_doc_search';
+import { JSONCodeEditorCommonMemoized } from '../json_code_editor/json_code_editor_common';
+import { ElasticRequestState } from '../doc/elastic_request_state';
+import { getServices } from '../../../../public/kibana_services';
+import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common';
+
+interface SourceViewerProps {
+ id: string;
+ index: string;
+ indexPatternId: string;
+ hasLineNumbers: boolean;
+ width?: number;
+}
+
+export const SourceViewer = ({
+ id,
+ index,
+ indexPatternId,
+ width,
+ hasLineNumbers,
+}: SourceViewerProps) => {
+ const [editor, setEditor] = useState();
+ const [jsonValue, setJsonValue] = useState('');
+ const indexPatternService = getServices().data.indexPatterns;
+ const useNewFieldsApi = !getServices().uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
+ const [reqState, hit, , requestData] = useEsDocSearch({
+ id,
+ index,
+ indexPatternId,
+ indexPatternService,
+ requestSource: useNewFieldsApi,
+ });
+
+ useEffect(() => {
+ if (reqState === ElasticRequestState.Found && hit) {
+ setJsonValue(JSON.stringify(hit, undefined, 2));
+ }
+ }, [reqState, hit]);
+
+ // setting editor height based on lines height and count to stretch and fit its content
+ useEffect(() => {
+ if (!editor) {
+ return;
+ }
+ const editorElement = editor.getDomNode();
+
+ if (!editorElement) {
+ return;
+ }
+
+ const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight);
+ const lineCount = editor.getModel()?.getLineCount() || 1;
+ const height = editor.getTopForLineNumber(lineCount + 1) + lineHeight;
+ if (!jsonValue || jsonValue === '') {
+ editorElement.style.height = '0px';
+ } else {
+ editorElement.style.height = `${height}px`;
+ }
+ editor.layout();
+ }, [editor, jsonValue]);
+
+ const loadingState = (
+
+
+
+
+
+
+ );
+
+ const errorMessageTitle = (
+
+ {i18n.translate('discover.sourceViewer.errorMessageTitle', {
+ defaultMessage: 'An Error Occurred',
+ })}
+
+ );
+ const errorMessage = (
+
+ {i18n.translate('discover.sourceViewer.errorMessage', {
+ defaultMessage: 'Could not fetch data at this time. Refresh the tab to try again.',
+ })}
+
+
+ {i18n.translate('discover.sourceViewer.refresh', {
+ defaultMessage: 'Refresh',
+ })}
+
+
+ );
+ const errorState = (
+
+ );
+
+ if (
+ reqState === ElasticRequestState.Error ||
+ reqState === ElasticRequestState.NotFound ||
+ reqState === ElasticRequestState.NotFoundIndexPattern
+ ) {
+ return errorState;
+ }
+
+ if (reqState === ElasticRequestState.Loading || jsonValue === '') {
+ return loadingState;
+ }
+
+ return (
+ setEditor(editorNode)}
+ />
+ );
+};
diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx
index 139b23d28a1d4..7b4e7bb67c00e 100644
--- a/src/plugins/discover/public/plugin.tsx
+++ b/src/plugins/discover/public/plugin.tsx
@@ -37,7 +37,7 @@ import { UrlGeneratorState } from '../../share/public';
import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types';
import { DocViewsRegistry } from './application/doc_views/doc_views_registry';
import { DocViewTable } from './application/components/table/table';
-import { JsonCodeEditor } from './application/components/json_code_editor/json_code_editor';
+
import {
setDocViewsRegistry,
setUrlTracker,
@@ -63,6 +63,7 @@ import { SearchEmbeddableFactory } from './application/embeddable';
import { UsageCollectionSetup } from '../../usage_collection/public';
import { replaceUrlHashQuery } from '../../kibana_utils/public/';
import { IndexPatternFieldEditorStart } from '../../../plugins/index_pattern_field_editor/public';
+import { SourceViewer } from './application/components/source_viewer/source_viewer';
declare module '../../share/public' {
export interface UrlGeneratorStateMapping {
@@ -178,7 +179,6 @@ export class DiscoverPlugin
})
);
}
-
this.docViewsRegistry = new DocViewsRegistry();
setDocViewsRegistry(this.docViewsRegistry);
this.docViewsRegistry.addDocView({
@@ -193,8 +193,14 @@ export class DiscoverPlugin
defaultMessage: 'JSON',
}),
order: 20,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- component: ({ hit }) => ,
+ component: ({ hit, indexPattern }) => (
+
+ ),
});
const {
@@ -273,6 +279,7 @@ export class DiscoverPlugin
// make sure the index pattern list is up to date
await dataStart.indexPatterns.clearCache();
+
const { renderApp } = await import('./application/application');
params.element.classList.add('dscAppWrapper');
const unmount = await renderApp(innerAngularName, params.element);
diff --git a/test/functional/apps/discover/_discover_fields_api.ts b/test/functional/apps/discover/_discover_fields_api.ts
index 614a0794ffb3b..42e2a94b36462 100644
--- a/test/functional/apps/discover/_discover_fields_api.ts
+++ b/test/functional/apps/discover/_discover_fields_api.ts
@@ -11,6 +11,7 @@ import { FtrProviderContext } from './ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
+ const docTable = getService('docTable');
const retry = getService('retry');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
@@ -58,5 +59,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await PageObjects.discover.getDocHeader()).not.to.have.string('_score');
expect(await PageObjects.discover.getDocHeader()).to.have.string('Document');
});
+
+ it('displays _source viewer in doc viewer', async function () {
+ await docTable.clickRowToggle({ rowIndex: 0 });
+
+ await PageObjects.discover.isShowingDocViewer();
+ await PageObjects.discover.clickDocViewerTab(1);
+ await PageObjects.discover.expectSourceViewerToExist();
+ });
});
}
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index 41c4441a1c95d..65b899d2e2fb0 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -289,6 +289,14 @@ export class DiscoverPageObject extends FtrService {
return await this.testSubjects.exists('kbnDocViewer');
}
+ public async clickDocViewerTab(index: number) {
+ return await this.find.clickByCssSelector(`#kbn_doc_viewer_tab_${index}`);
+ }
+
+ public async expectSourceViewerToExist() {
+ return await this.find.byClassName('monaco-editor');
+ }
+
public async getMarks() {
const table = await this.docTable.getTable();
const marks = await table.findAllByTagName('mark');
From 91c584d766b8983694868a2869363d7ad5ebe4c0 Mon Sep 17 00:00:00 2001
From: Spencer
Date: Tue, 22 Jun 2021 09:41:38 -0700
Subject: [PATCH 082/163] remove duplicate apm-rum deps from devDeps (#102838)
Co-authored-by: spalger
---
package.json | 2 --
1 file changed, 2 deletions(-)
diff --git a/package.json b/package.json
index 873dffeed38f8..f654f5f62ba9c 100644
--- a/package.json
+++ b/package.json
@@ -446,8 +446,6 @@
"@bazel/typescript": "^3.5.1",
"@cypress/snapshot": "^2.1.7",
"@cypress/webpack-preprocessor": "^5.6.0",
- "@elastic/apm-rum": "^5.6.1",
- "@elastic/apm-rum-react": "^1.2.5",
"@elastic/eslint-config-kibana": "link:bazel-bin/packages/elastic-eslint-config-kibana",
"@elastic/eslint-plugin-eui": "0.0.2",
"@elastic/github-checks-reporter": "0.0.20b3",
From 537fcf4ff2eefd15ffb928e002327849fc17639b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20S=C3=A1nchez?=
Date: Tue, 22 Jun 2021 18:49:34 +0200
Subject: [PATCH 083/163] [Security Solution][Endpoint] Don't create event
filters list from manifest manager (#102618)
* Check if endpoint event filters list exists before create and create it without specific id
* Removes creation of endpoint event filters list in manifest manager
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../create_endoint_event_filters_list.ts | 82 -------------------
.../exception_lists/exception_list_client.ts | 13 ---
.../server/endpoint/lib/artifacts/lists.ts | 2 -
3 files changed, 97 deletions(-)
delete mode 100644 x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts
diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts
deleted file mode 100644
index 94a049d10cc45..0000000000000
--- a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts
+++ /dev/null
@@ -1,82 +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 { SavedObjectsClientContract } from 'kibana/server';
-import uuid from 'uuid';
-import { Version } from '@kbn/securitysolution-io-ts-types';
-import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types';
-import { getSavedObjectType } from '@kbn/securitysolution-list-utils';
-import {
- ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION,
- ENDPOINT_EVENT_FILTERS_LIST_ID,
- ENDPOINT_EVENT_FILTERS_LIST_NAME,
-} from '@kbn/securitysolution-list-constants';
-
-import { ExceptionListSoSchema } from '../../schemas/saved_objects';
-
-import { transformSavedObjectToExceptionList } from './utils';
-
-interface CreateEndpointEventFiltersListOptions {
- savedObjectsClient: SavedObjectsClientContract;
- user: string;
- tieBreaker?: string;
- version: Version;
-}
-
-/**
- * Creates the Endpoint Trusted Apps agnostic list if it does not yet exist
- *
- * @param savedObjectsClient
- * @param user
- * @param tieBreaker
- * @param version
- */
-export const createEndpointEventFiltersList = async ({
- savedObjectsClient,
- user,
- tieBreaker,
- version,
-}: CreateEndpointEventFiltersListOptions): Promise => {
- const savedObjectType = getSavedObjectType({ namespaceType: 'agnostic' });
- const dateNow = new Date().toISOString();
- try {
- const savedObject = await savedObjectsClient.create(
- savedObjectType,
- {
- comments: undefined,
- created_at: dateNow,
- created_by: user,
- description: ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION,
- entries: undefined,
- immutable: false,
- item_id: undefined,
- list_id: ENDPOINT_EVENT_FILTERS_LIST_ID,
- list_type: 'list',
- meta: undefined,
- name: ENDPOINT_EVENT_FILTERS_LIST_NAME,
- os_types: [],
- tags: [],
- tie_breaker_id: tieBreaker ?? uuid.v4(),
- type: 'endpoint_events',
- updated_by: user,
- version,
- },
- {
- // We intentionally hard coding the id so that there can only be one Event Filters list within the space
- id: ENDPOINT_EVENT_FILTERS_LIST_ID,
- }
- );
-
- return transformSavedObjectToExceptionList({ savedObject });
- } catch (err) {
- if (savedObjectsClient.errors.isConflictError(err)) {
- return null;
- } else {
- throw err;
- }
- }
-};
diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
index 4ccff2dd000b9..77e82bf0f7578 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
@@ -54,7 +54,6 @@ import {
} from './find_exception_list_items';
import { createEndpointList } from './create_endpoint_list';
import { createEndpointTrustedAppsList } from './create_endpoint_trusted_apps_list';
-import { createEndpointEventFiltersList } from './create_endoint_event_filters_list';
export class ExceptionListClient {
private readonly user: string;
@@ -120,18 +119,6 @@ export class ExceptionListClient {
});
};
- /**
- * Create the Endpoint Event Filters Agnostic list if it does not yet exist (`null` is returned if it does exist)
- */
- public createEndpointEventFiltersList = async (): Promise => {
- const { savedObjectsClient, user } = this;
- return createEndpointEventFiltersList({
- savedObjectsClient,
- user,
- version: 1,
- });
- };
-
/**
* This is the same as "createListItem" except it applies specifically to the agnostic endpoint list and will
* auto-call the "createEndpointList" for you so that you have the best chance of the agnostic endpoint
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
index f5d3b30bf15fa..e27a09efd9710 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
@@ -140,8 +140,6 @@ export async function getEndpointEventFiltersList(
policyId ? ` or exception-list-agnostic.attributes.tags:\"policy:${policyId}\"` : ''
})`;
- await eClient.createEndpointEventFiltersList();
-
return getFilteredEndpointExceptionList(
eClient,
schemaVersion,
From 3da2ac8927eaaeae7fecfb49aba9687454358310 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Tue, 22 Jun 2021 18:11:24 +0100
Subject: [PATCH 084/163] chore(NA): moving @kbn/ui-framework into bazel
(#102908)
---
.../monorepo-packages.asciidoc | 1 +
package.json | 2 +-
packages/BUILD.bazel | 1 +
packages/kbn-ui-framework/BUILD.bazel | 47 +++++++++++++++++++
x-pack/package.json | 3 --
yarn.lock | 2 +-
6 files changed, 51 insertions(+), 5 deletions(-)
create mode 100644 packages/kbn-ui-framework/BUILD.bazel
diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc
index 48d0d40d0abb0..e8b950a696f55 100644
--- a/docs/developer/getting-started/monorepo-packages.asciidoc
+++ b/docs/developer/getting-started/monorepo-packages.asciidoc
@@ -104,6 +104,7 @@ yarn kbn watch-bazel
- @kbn/storybook
- @kbn/telemetry-utils
- @kbn/tinymath
+- @kbn/ui-framework
- @kbn/ui-shared-deps
- @kbn/utility-types
- @kbn/utils
diff --git a/package.json b/package.json
index f654f5f62ba9c..36fa086657adf 100644
--- a/package.json
+++ b/package.json
@@ -154,7 +154,7 @@
"@kbn/server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository",
"@kbn/std": "link:bazel-bin/packages/kbn-std",
"@kbn/tinymath": "link:bazel-bin/packages/kbn-tinymath",
- "@kbn/ui-framework": "link:packages/kbn-ui-framework",
+ "@kbn/ui-framework": "link:bazel-bin/packages/kbn-ui-framework",
"@kbn/ui-shared-deps": "link:bazel-bin/packages/kbn-ui-shared-deps",
"@kbn/utility-types": "link:bazel-bin/packages/kbn-utility-types",
"@kbn/common-utils": "link:bazel-bin/packages/kbn-common-utils",
diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel
index 70a3d1eacc7c5..b1c3f580c6baf 100644
--- a/packages/BUILD.bazel
+++ b/packages/BUILD.bazel
@@ -48,6 +48,7 @@ filegroup(
"//packages/kbn-storybook:build",
"//packages/kbn-telemetry-tools:build",
"//packages/kbn-tinymath:build",
+ "//packages/kbn-ui-framework:build",
"//packages/kbn-ui-shared-deps:build",
"//packages/kbn-utility-types:build",
"//packages/kbn-utils:build",
diff --git a/packages/kbn-ui-framework/BUILD.bazel b/packages/kbn-ui-framework/BUILD.bazel
new file mode 100644
index 0000000000000..f8cf5035bdc5f
--- /dev/null
+++ b/packages/kbn-ui-framework/BUILD.bazel
@@ -0,0 +1,47 @@
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
+
+PKG_BASE_NAME = "kbn-ui-framework"
+PKG_REQUIRE_NAME = "@kbn/ui-framework"
+
+SOURCE_FILES = glob([
+ "dist/**/*",
+])
+
+SRCS = SOURCE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "package.json",
+ "README.md",
+]
+
+DEPS = []
+
+js_library(
+ name = PKG_BASE_NAME,
+ srcs = NPM_MODULE_EXTRA_FILES + [
+ ":srcs",
+ ],
+ deps = DEPS,
+ package_name = PKG_REQUIRE_NAME,
+ visibility = ["//visibility:public"],
+)
+
+pkg_npm(
+ name = "npm_module",
+ deps = [
+ ":%s" % PKG_BASE_NAME,
+ ]
+)
+
+filegroup(
+ name = "build",
+ srcs = [
+ ":npm_module",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/x-pack/package.json b/x-pack/package.json
index 01571cbb823fd..1397a3da81072 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -28,8 +28,5 @@
"devDependencies": {
"@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers",
"@kbn/test": "link:../packages/kbn-test"
- },
- "dependencies": {
- "@kbn/ui-framework": "link:../packages/kbn-ui-framework"
}
}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 153309ad56f19..953e7907590e7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2788,7 +2788,7 @@
version "0.0.0"
uid ""
-"@kbn/ui-framework@link:packages/kbn-ui-framework":
+"@kbn/ui-framework@link:bazel-bin/packages/kbn-ui-framework":
version "0.0.0"
uid ""
From c5e8df02c1ced111c26e607867cf4d73c940ad7b Mon Sep 17 00:00:00 2001
From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com>
Date: Tue, 22 Jun 2021 13:52:03 -0400
Subject: [PATCH 085/163] [Cases] RBAC Bugs (#101325)
* Adding feature flag for auth
* Hiding SOs and adding consumer field
* First pass at adding security changes
* Consumer as the app's plugin ID
* Create addConsumerToSO migration helper
* Fix mapping's SO consumer
* Add test for CasesActions
* Declare hidden types on SO client
* Restructure integration tests
* Init spaces_only integration tests
* Implementing the cases security string
* Adding security plugin tests for cases
* Rough concept for authorization class
* Adding comments
* Fix merge
* Get requiredPrivileges for classes
* Check privillages
* Ensure that all classes are available
* Success if hasAllRequested is true
* Failure if hasAllRequested is false
* Adding schema updates for feature plugin
* Seperate basic from trial
* Enable SIR on integration tests
* Starting the plumbing for authorization in plugin
* Unit tests working
* Move find route logic to case client
* Create integration test helper functions
* Adding auth to create call
* Create getClassFilter helper
* Add class attribute to find request
* Create getFindAuthorizationFilter
* Ensure savedObject is authorized in find method
* Include fields for authorization
* Combine authorization filter with cases & subcases filter
* Fix isAuthorized flag
* Fix merge issue
* Create/delete spaces & users before and after tests
* Add more user and roles
* [Cases] Convert filters from strings to KueryNode (#95288)
* [Cases] RBAC: Rename class to scope (#95535)
* [Cases][RBAC] Rename scope to owner (#96035)
* [Cases] RBAC: Create & Find integration tests (#95511)
* [Cases] Cases client enchantment (#95923)
* [Cases] Authorization and Client Audit Logger (#95477)
* Starting audit logger
* Finishing auth audit logger
* Fixing tests and types
* Adding audit event creator
* Renaming class to scope
* Adding audit logger messages to create and find
* Adding comments and fixing import issue
* Fixing type errors
* Fixing tests and adding username to message
* Addressing PR feedback
* Removing unneccessary log and generating id
* Fixing module issue and remove expect.anything
* [Cases] Migrate sub cases routes to a client (#96461)
* Adding sub cases client
* Move sub case routes to case client
* Throw when attempting to access the sub cases client
* Fixing throw and removing user ans soclients
* [Cases] RBAC: Migrate routes' unit tests to integration tests (#96374)
Co-authored-by: Jonathan Buttner
* [Cases] Move remaining HTTP functionality to client (#96507)
* Moving deletes and find for attachments
* Moving rest of comment apis
* Migrating configuration routes to client
* Finished moving routes, starting utils refactor
* Refactoring utilites and fixing integration tests
* Addressing PR feedback
* Fixing mocks and types
* Fixing integration tests
* Renaming status_stats
* Fixing test type errors
* Adding plugins to kibana.json
* Adding cases to required plugin
* [Cases] Refactoring authorization (#97483)
* Refactoring authorization
* Wrapping auth calls in helper for try catch
* Reverting name change
* Hardcoding the saved object types
* Switching ensure to owner array
* [Cases] Add authorization to configuration & cases routes (#97228)
* [Cases] Attachments RBAC (#97756)
* Starting rbac for comments
* Adding authorization to rest of comment apis
* Starting the comment rbac tests
* Fixing some of the rbac tests
* Adding some integration tests
* Starting patch tests
* Working tests for comments
* Working tests
* Fixing some tests
* Fixing type issues from pulling in master
* Fixing connector tests that only work in trial license
* Attempting to fix cypress
* Mock return of array for configure
* Fixing cypress test
* Cleaning up
* Addressing PR comments
* Reducing operations
* [Cases] Add RBAC to remaining Cases APIs (#98762)
* Starting rbac for comments
* Adding authorization to rest of comment apis
* Starting the comment rbac tests
* Fixing some of the rbac tests
* Adding some integration tests
* Starting patch tests
* Working tests for comments
* Working tests
* Fixing some tests
* Fixing type issues from pulling in master
* Fixing connector tests that only work in trial license
* Attempting to fix cypress
* Mock return of array for configure
* Fixing cypress test
* Cleaning up
* Working case update tests
* Addressing PR comments
* Reducing operations
* Working rbac push case tests
* Starting stats apis
* Working status tests
* User action tests and fixing migration errors
* Fixing type errors
* including error in message
* Addressing pr feedback
* Fixing some type errors
* [Cases] Add space only tests (#99409)
* Starting spaces tests
* Finishing space only tests
* Refactoring createCaseWithConnector
* Fixing spelling
* Addressing PR feedback and creating alert tests
* Fixing mocks
* [Cases] Add security only tests (#99679)
* Starting spaces tests
* Finishing space only tests
* Refactoring createCaseWithConnector
* Fixing spelling
* Addressing PR feedback and creating alert tests
* Fixing mocks
* Starting security only tests
* Adding remainder security only tests
* Using helper objects
* Fixing type error for null space
* Renaming utility variables
* Refactoring users and roles for security only tests
* Adding sub feature
* [Cases] Cleaning up the services and TODOs (#99723)
* Cleaning up the service intialization
* Fixing type errors
* Adding comments for the api
* Working test for cases client
* Fix type error
* Adding generated docs
* Adding more docs and cleaning up types
* Cleaning up readme
* More clean up and links
* Changing some file names
* Renaming docs
* Integration tests for cases privs and fixes (#100038)
* [Cases] RBAC on UI (#99478)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
* Fixing case ids by alert id route call
* [Cases] Fixing UI feature permissions and adding UI tests (#100074)
* Integration tests for cases privs and fixes
* Fixing ui cases permissions and adding tests
* Adding test for collection failure and fixing jest
* Renaming variables
* Fixing type error
* Adding some comments
* Validate cases features
* Fix new schema
* Adding owner param for the status stats
* Fix get case status tests
* Adjusting permissions text and fixing status
* Address PR feedback
* Adding top level feature back
* Fixing feature privileges
* Renaming
* Removing uneeded else
* Fixing tests and adding cases merge tests
* [Cases][Security Solution] Basic license security solution API tests (#100925)
* Cleaning up the fixture plugins
* Adding basic feature test
* renaming to unsecuredSavedObjectsClient (#101215)
* [Cases] RBAC Refactoring audit logging (#100952)
* Refactoring audit logging
* Adding unit tests for authorization classes
* Addressing feedback and adding util tests
* return undefined on empty array
* fixing eslint
* conditional rendering the recently created cases
* Remove unnecessary Array.from
* Cleaning up overview page for permissions
* Fixing log message for attachments
* hiding add to cases button
* Disable the Cases app from the global nav
* Hide the add to cases button from detections
* Fixing merge
* Making progress on removing icons
* Hding edit icons on detail view
* Trying to get connector error msg tests working
* Removing test
* Disable error callouts
* Fixing spacing and removing cases tab one no read
* Adding read only badge
* Cleaning up and adding badge
* Wrapping in use effect
* Default toasting permissions errors
* Removing actions icon on comments
* Addressing feedback
* Fixing type
Co-authored-by: Christos Nasikas
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/add_comment/index.test.tsx | 26 ++-
.../public/components/add_comment/index.tsx | 58 +++----
.../public/components/all_cases/header.tsx | 32 ++--
.../components/all_cases/nav_buttons.tsx | 5 +-
.../public/components/all_cases/table.tsx | 26 +--
.../components/all_cases/translations.ts | 8 +
.../public/components/callout/helpers.tsx | 11 --
.../public/components/callout/translations.ts | 9 -
.../components/case_action_bar/actions.tsx | 5 +-
.../components/case_action_bar/index.test.tsx | 1 +
.../components/case_action_bar/index.tsx | 27 +--
.../public/components/case_view/index.tsx | 15 +-
.../components/edit_connector/index.test.tsx | 55 +++++-
.../components/edit_connector/index.tsx | 43 +++--
.../components/recent_cases/index.test.tsx | 1 +
.../public/components/recent_cases/index.tsx | 3 +
.../recent_cases/no_cases/index.test.tsx | 13 +-
.../recent_cases/no_cases/index.tsx | 30 ++--
.../components/recent_cases/recent_cases.tsx | 4 +-
.../components/recent_cases/translations.ts | 4 +
.../cases/public/components/status/button.tsx | 9 +-
.../public/components/status/status.test.tsx | 9 +-
.../cases/public/components/status/status.tsx | 8 +-
.../public/components/tag_list/index.test.tsx | 16 +-
.../public/components/tag_list/index.tsx | 10 +-
.../use_push_to_service/index.test.tsx | 153 +++++++++++++++++
.../components/use_push_to_service/index.tsx | 11 +-
.../components/user_action_tree/index.tsx | 37 +++--
.../user_action_content_toolbar.test.tsx | 9 +-
.../user_action_content_toolbar.tsx | 29 ++--
.../user_action_property_actions.tsx | 6 +-
.../containers/configure/translations.ts | 8 +
.../containers/configure/use_connectors.tsx | 46 ++++--
x-pack/plugins/cases/public/mocks.ts | 21 +++
.../server/authorization/audit_logger.test.ts | 8 +-
x-pack/plugins/cases/server/plugin.ts | 2 +-
.../components/app/cases/callout/helpers.tsx | 11 --
.../app/cases/callout/translations.ts | 15 --
.../components/app/cases/translations.ts | 14 ++
.../public/hooks/use_readonly_header.tsx | 40 +++++
.../public/pages/cases/all_cases.tsx | 25 +--
.../public/pages/cases/case_details.tsx | 30 ++--
.../public/pages/cases/configure_cases.tsx | 12 +-
.../public/pages/cases/create_case.tsx | 12 +-
.../security_solution/common/constants.ts | 3 +
.../detection_alerts/attach_to_case.spec.ts | 2 +-
.../cases/components/callout/helpers.tsx | 11 --
.../cases/components/callout/translations.ts | 15 --
.../add_to_case_action.test.tsx | 6 +-
.../timeline_actions/add_to_case_action.tsx | 28 ++--
.../public/cases/pages/case.tsx | 7 -
.../public/cases/pages/case_details.tsx | 18 +-
.../public/cases/pages/configure_cases.tsx | 13 +-
.../public/cases/pages/create_case.tsx | 16 +-
.../public/cases/pages/index.test.tsx | 91 ++++++++++
.../public/cases/pages/index.tsx | 77 ++++++---
.../public/cases/pages/translations.ts | 21 +++
.../components/header_global/index.test.tsx | 51 ++++++
.../common/components/header_global/index.tsx | 23 ++-
.../components/recent_cases/index.tsx | 5 +-
.../components/sidebar/sidebar.test.tsx | 72 ++++++++
.../overview/components/sidebar/sidebar.tsx | 16 +-
.../components/flyout/header/index.test.tsx | 156 +++++++++++-------
.../components/flyout/header/index.tsx | 12 +-
.../security_solution/server/plugin.ts | 20 ++-
.../observability_security.ts | 10 +-
.../page_objects/observability_page.ts | 16 +-
67 files changed, 1114 insertions(+), 492 deletions(-)
create mode 100644 x-pack/plugins/cases/public/mocks.ts
create mode 100644 x-pack/plugins/observability/public/hooks/use_readonly_header.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/pages/index.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx
diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx
index 19c303840fc1a..078db1e6dbe6d 100644
--- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx
@@ -14,7 +14,7 @@ import { TestProviders } from '../../common/mock';
import { CommentRequest, CommentType, SECURITY_SOLUTION_OWNER } from '../../../common';
import { usePostComment } from '../../containers/use_post_comment';
-import { AddComment, AddCommentRefObject } from '.';
+import { AddComment, AddCommentProps, AddCommentRefObject } from '.';
import { CasesTimelineIntegrationProvider } from '../timeline_context';
import { timelineIntegrationMock } from '../__mock__/timeline';
@@ -25,10 +25,9 @@ const onCommentSaving = jest.fn();
const onCommentPosted = jest.fn();
const postComment = jest.fn();
-const addCommentProps = {
+const addCommentProps: AddCommentProps = {
caseId: '1234',
- disabled: false,
- insertQuote: null,
+ userCanCrud: true,
onCommentSaving,
onCommentPosted,
showLoading: false,
@@ -94,11 +93,11 @@ describe('AddComment ', () => {
).toBeTruthy();
});
- it('should disable submit button when disabled prop passed', () => {
+ it('should disable submit button when isLoading is true', () => {
usePostCommentMock.mockImplementation(() => ({ ...defaultPostComment, isLoading: true }));
const wrapper = mount(
-
+
);
@@ -107,12 +106,23 @@ describe('AddComment ', () => {
).toBeTruthy();
});
+ it('should hide the component when the user does not have crud permissions', () => {
+ usePostCommentMock.mockImplementation(() => ({ ...defaultPostComment, isLoading: true }));
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeFalsy();
+ });
+
it('should insert a quote', async () => {
const sampleQuote = 'what a cool quote';
const ref = React.createRef();
const wrapper = mount(
-
+
);
@@ -143,7 +153,7 @@ describe('AddComment ', () => {
const wrapper = mount(
-
+
);
diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx
index 04104f0b9471d..6604f3d2b8bc8 100644
--- a/x-pack/plugins/cases/public/components/add_comment/index.tsx
+++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx
@@ -33,9 +33,9 @@ export interface AddCommentRefObject {
addQuote: (quote: string) => void;
}
-interface AddCommentProps {
+export interface AddCommentProps {
caseId: string;
- disabled?: boolean;
+ userCanCrud?: boolean;
onCommentSaving?: () => void;
onCommentPosted: (newCase: Case) => void;
showLoading?: boolean;
@@ -45,7 +45,7 @@ interface AddCommentProps {
export const AddComment = React.memo(
forwardRef(
(
- { caseId, disabled, onCommentPosted, onCommentSaving, showLoading = true, subCaseId },
+ { caseId, userCanCrud, onCommentPosted, onCommentSaving, showLoading = true, subCaseId },
ref
) => {
const owner = useOwnerContext();
@@ -91,31 +91,33 @@ export const AddComment = React.memo(
return (
);
}
diff --git a/x-pack/plugins/cases/public/components/all_cases/header.tsx b/x-pack/plugins/cases/public/components/all_cases/header.tsx
index 7452fe7e44b3c..73dcc18b97108 100644
--- a/x-pack/plugins/cases/public/components/all_cases/header.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/header.tsx
@@ -52,17 +52,27 @@ export const CasesTableHeader: FunctionComponent = ({
wrap={true}
data-test-subj="all-cases-header"
>
-
-
-
-
-
-
+ {userCanCrud ? (
+ <>
+
+
+
+
+
+
+
+ >
+ ) : (
+ // doesn't include the horizontal bar that divides the buttons and other padding since we don't have any buttons
+ // to the right
+
+
+
+ )}
);
diff --git a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx
index e29551f43c2bd..b8755d03e0b00 100644
--- a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx
@@ -17,7 +17,6 @@ interface OwnProps {
actionsErrors: ErrorMessage[];
configureCasesNavigation: CasesNavigation;
createCaseNavigation: CasesNavigation;
- userCanCrud: boolean;
}
type Props = OwnProps;
@@ -26,14 +25,13 @@ export const NavButtons: FunctionComponent = ({
actionsErrors,
configureCasesNavigation,
createCaseNavigation,
- userCanCrud,
}) => (
>}
titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''}
@@ -41,7 +39,6 @@ export const NavButtons: FunctionComponent = ({
= ({
{i18n.NO_CASES}}
titleSize="xs"
- body={i18n.NO_CASES_BODY}
+ body={userCanCrud ? i18n.NO_CASES_BODY : i18n.NO_CASES_BODY_READ_ONLY}
actions={
-
- {i18n.ADD_NEW_CASE}
-
+ userCanCrud && (
+
+ {i18n.ADD_NEW_CASE}
+
+ )
}
/>
}
diff --git a/x-pack/plugins/cases/public/components/all_cases/translations.ts b/x-pack/plugins/cases/public/components/all_cases/translations.ts
index 0f535b771ec8a..8da90f32fabdf 100644
--- a/x-pack/plugins/cases/public/components/all_cases/translations.ts
+++ b/x-pack/plugins/cases/public/components/all_cases/translations.ts
@@ -12,11 +12,19 @@ export * from '../../common/translations';
export const NO_CASES = i18n.translate('xpack.cases.caseTable.noCases.title', {
defaultMessage: 'No Cases',
});
+
export const NO_CASES_BODY = i18n.translate('xpack.cases.caseTable.noCases.body', {
defaultMessage:
'There are no cases to display. Please create a new case or change your filter settings above.',
});
+export const NO_CASES_BODY_READ_ONLY = i18n.translate(
+ 'xpack.cases.caseTable.noCases.readonly.body',
+ {
+ defaultMessage: 'There are no cases to display. Please change your filter settings above.',
+ }
+);
+
export const ADD_NEW_CASE = i18n.translate('xpack.cases.caseTable.addNewCase', {
defaultMessage: 'Add New Case',
});
diff --git a/x-pack/plugins/cases/public/components/callout/helpers.tsx b/x-pack/plugins/cases/public/components/callout/helpers.tsx
index 29b17cd426c58..fdd49ad17168d 100644
--- a/x-pack/plugins/cases/public/components/callout/helpers.tsx
+++ b/x-pack/plugins/cases/public/components/callout/helpers.tsx
@@ -5,18 +5,7 @@
* 2.0.
*/
-import React from 'react';
import md5 from 'md5';
-import * as i18n from './translations';
-import { ErrorMessage } from './types';
-
-export const permissionsReadOnlyErrorMessage: ErrorMessage = {
- id: 'read-only-privileges-error',
- title: i18n.READ_ONLY_FEATURE_TITLE,
- description: <>{i18n.READ_ONLY_FEATURE_MSG}>,
- errorType: 'warning',
-};
-
export const createCalloutId = (ids: string[], delimiter: string = '|'): string =>
md5(ids.join(delimiter));
diff --git a/x-pack/plugins/cases/public/components/callout/translations.ts b/x-pack/plugins/cases/public/components/callout/translations.ts
index dca622e60c863..8b0ad31dba88e 100644
--- a/x-pack/plugins/cases/public/components/callout/translations.ts
+++ b/x-pack/plugins/cases/public/components/callout/translations.ts
@@ -7,15 +7,6 @@
import { i18n } from '@kbn/i18n';
-export const READ_ONLY_FEATURE_TITLE = i18n.translate('xpack.cases.readOnlyFeatureTitle', {
- defaultMessage: 'You cannot open new or update existing cases',
-});
-
-export const READ_ONLY_FEATURE_MSG = i18n.translate('xpack.cases.readOnlyFeatureDescription', {
- defaultMessage:
- 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.',
-});
-
export const DISMISS_CALLOUT = i18n.translate('xpack.cases.dismissErrorsPushServiceCallOutTitle', {
defaultMessage: 'Dismiss',
});
diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx
index c2578dc3debdb..6816575d649f7 100644
--- a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx
+++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx
@@ -19,14 +19,12 @@ interface CaseViewActions {
allCasesNavigation: CasesNavigation;
caseData: Case;
currentExternalIncident: CaseService | null;
- disabled?: boolean;
}
const ActionsComponent: React.FC = ({
allCasesNavigation,
caseData,
currentExternalIncident,
- disabled = false,
}) => {
// Delete case
const {
@@ -39,7 +37,6 @@ const ActionsComponent: React.FC = ({
const propertyActions = useMemo(
() => [
{
- disabled,
iconType: 'trash',
label: i18n.DELETE_CASE(),
onClick: handleToggleModal,
@@ -54,7 +51,7 @@ const ActionsComponent: React.FC = ({
]
: []),
],
- [disabled, handleToggleModal, currentExternalIncident]
+ [handleToggleModal, currentExternalIncident]
);
if (isDeleted) {
diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx
index 724d35b20df53..3040b0fe47a47 100644
--- a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx
@@ -26,6 +26,7 @@ describe('CaseActionBar', () => {
onRefresh,
onUpdateField,
currentExternalIncident: null,
+ userCanCrud: true,
};
beforeEach(() => {
diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx
index d8e012b072106..3448d112dadd1 100644
--- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx
+++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx
@@ -40,7 +40,7 @@ interface CaseActionBarProps {
allCasesNavigation: CasesNavigation;
caseData: Case;
currentExternalIncident: CaseService | null;
- disabled?: boolean;
+ userCanCrud: boolean;
disableAlerting: boolean;
isLoading: boolean;
onRefresh: () => void;
@@ -50,8 +50,8 @@ const CaseActionBarComponent: React.FC = ({
allCasesNavigation,
caseData,
currentExternalIncident,
- disabled = false,
disableAlerting,
+ userCanCrud,
isLoading,
onRefresh,
onUpdateField,
@@ -87,7 +87,7 @@ const CaseActionBarComponent: React.FC = ({
@@ -108,7 +108,7 @@ const CaseActionBarComponent: React.FC = ({
- {!disableAlerting && (
+ {userCanCrud && !disableAlerting && (
@@ -122,7 +122,7 @@ const CaseActionBarComponent: React.FC = ({
@@ -134,14 +134,15 @@ const CaseActionBarComponent: React.FC = ({
{i18n.CASE_REFRESH}
-
-
-
+ {userCanCrud && (
+
+
+
+ )}
diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx
index df57e49073a60..05f1c6727b168 100644
--- a/x-pack/plugins/cases/public/components/case_view/index.tsx
+++ b/x-pack/plugins/cases/public/components/case_view/index.tsx
@@ -230,7 +230,9 @@ export const CaseComponent = React.memo(
[updateCase, fetchCaseUserActions, caseId, subCaseId]
);
- const { loading: isLoadingConnectors, connectors } = useConnectors();
+ const { loading: isLoadingConnectors, connectors, permissionsError } = useConnectors({
+ toastPermissionsErrors: false,
+ });
const [connectorName, isValidConnector] = useMemo(() => {
const connector = connectors.find((c) => c.id === caseData.connector.id);
@@ -363,7 +365,7 @@ export const CaseComponent = React.memo(
allCasesNavigation={allCasesNavigation}
caseData={caseData}
currentExternalIncident={currentExternalIncident}
- disabled={!userCanCrud}
+ userCanCrud={userCanCrud}
disableAlerting={ruleDetailsNavigation == null}
isLoading={isLoading && (updateKey === 'status' || updateKey === 'settings')}
onRefresh={handleRefresh}
@@ -406,7 +408,7 @@ export const CaseComponent = React.memo(
useFetchAlertData={useFetchAlertData}
userCanCrud={userCanCrud}
/>
- {(caseData.type !== CaseType.collection || hasDataToPush) && (
+ {(caseData.type !== CaseType.collection || hasDataToPush) && userCanCrud && (
<>
(
@@ -450,16 +451,15 @@ export const CaseComponent = React.memo(
/>
(
onSubmit={onSubmitConnector}
selectedConnector={caseData.connector.id}
userActions={caseUserActions}
+ permissionsError={permissionsError}
/>
diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx
index 1385e8e8664c3..33efb7e447583 100644
--- a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
-import { EditConnector } from './index';
+import { EditConnector, EditConnectorProps } from './index';
import { getFormMock, useFormMock } from '../__mock__/form';
import { TestProviders } from '../../common/mock';
import { connectorsMock } from '../../containers/configure/mock';
@@ -21,9 +21,9 @@ jest.mock('../../common/lib/kibana');
const useKibanaMock = useKibana as jest.Mocked;
const onSubmit = jest.fn();
-const defaultProps = {
+const defaultProps: EditConnectorProps = {
connectors: connectorsMock,
- disabled: false,
+ userCanCrud: true,
isLoading: false,
onSubmit,
selectedConnector: 'none',
@@ -144,4 +144,53 @@ describe('EditConnector ', () => {
expect(wrapper.find(`[data-test-subj="connector-loading"]`).last().exists()).toBeTruthy()
);
});
+
+ it('does not allow the connector to be edited when the user does not have write permissions', async () => {
+ const props = { ...defaultProps, userCanCrud: false };
+ const wrapper = mount(
+
+
+
+ );
+ await waitFor(() =>
+ expect(wrapper.find(`[data-test-subj="connector-edit"]`).exists()).toBeFalsy()
+ );
+ });
+
+ it('displays the permissions error message when one is provided', async () => {
+ const props = { ...defaultProps, permissionsError: 'error message' };
+ const wrapper = mount(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(
+ wrapper.find(`[data-test-subj="edit-connector-permissions-error-msg"]`).exists()
+ ).toBeTruthy();
+
+ expect(
+ wrapper.find(`[data-test-subj="edit-connector-no-connectors-msg"]`).exists()
+ ).toBeFalsy();
+ });
+ });
+
+ it('displays the default none connector message', async () => {
+ const props = { ...defaultProps };
+ const wrapper = mount(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(
+ wrapper.find(`[data-test-subj="edit-connector-permissions-error-msg"]`).exists()
+ ).toBeFalsy();
+ expect(
+ wrapper.find(`[data-test-subj="edit-connector-no-connectors-msg"]`).exists()
+ ).toBeTruthy();
+ });
+ });
});
diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx
index ad6b5a5e7cddf..570f6e34d2528 100644
--- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx
+++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx
@@ -30,7 +30,7 @@ import { schema } from './schema';
import { getConnectorFieldsFromUserActions } from './helpers';
import * as i18n from './translations';
-interface EditConnectorProps {
+export interface EditConnectorProps {
caseFields: ConnectorTypeFields['fields'];
connectors: ActionConnector[];
isLoading: boolean;
@@ -42,8 +42,9 @@ interface EditConnectorProps {
) => void;
selectedConnector: string;
userActions: CaseUserActions[];
- disabled?: boolean;
+ userCanCrud?: boolean;
hideConnectorServiceNowSir?: boolean;
+ permissionsError?: string;
}
const MyFlexGroup = styled(EuiFlexGroup)`
@@ -104,12 +105,13 @@ export const EditConnector = React.memo(
({
caseFields,
connectors,
- disabled = false,
+ userCanCrud = true,
hideConnectorServiceNowSir = false,
isLoading,
onSubmit,
selectedConnector,
userActions,
+ permissionsError,
}: EditConnectorProps) => {
const { form } = useForm({
defaultValue: { connectorId: selectedConnector },
@@ -203,6 +205,18 @@ export const EditConnector = React.memo(
});
}, [dispatch]);
+ /**
+ * if this evaluates to true it means that the connector was likely deleted because the case connector was set to something
+ * other than none but we don't find it in the list of connectors returned from the actions plugin
+ */
+ const connectorFromCaseMissing = currentConnector == null && selectedConnector !== 'none';
+
+ /**
+ * True if the chosen connector from the form was the "none" connector or no connector was in the case. The
+ * currentConnector will be null initially and after the form initializes if the case connector is "none"
+ */
+ const connectorUndefinedOrNone = currentConnector == null || currentConnector?.id === 'none';
+
return (
@@ -210,11 +224,10 @@ export const EditConnector = React.memo(
{i18n.CONNECTORS}
{isLoading && }
- {!isLoading && !editConnector && (
+ {!isLoading && !editConnector && userCanCrud && (
- {(currentConnector == null || currentConnector?.id === 'none') && // Connector is none or not defined.
- !(currentConnector === null && selectedConnector !== 'none') && // Connector has not been deleted.
- !editConnector && (
-
+ {!editConnector && permissionsError ? (
+
+ {permissionsError}
+
+ ) : (
+ // if we're not editing the connectors and the connector specified in the case was found and the connector
+ // is undefined or explicitly set to none
+ !editConnector &&
+ !connectorFromCaseMissing &&
+ connectorUndefinedOrNone && (
+
{i18n.NO_CONNECTOR}
- )}
+ )
+ )}
;
createCaseNavigation: CasesNavigation;
+ hasWritePermissions: boolean;
maxCasesToShow: number;
}
@@ -29,6 +30,7 @@ const RecentCasesComponent = ({
caseDetailsNavigation,
createCaseNavigation,
maxCasesToShow,
+ hasWritePermissions,
}: Omit) => {
const currentUser = useCurrentUser();
const [recentCasesFilterBy, setRecentCasesFilterBy] = useState(
@@ -77,6 +79,7 @@ const RecentCasesComponent = ({
createCaseNavigation={createCaseNavigation}
filterOptions={recentCasesFilterOptions}
maxCasesToShow={maxCasesToShow}
+ hasWritePermissions={hasWritePermissions}
/>
diff --git a/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.test.tsx b/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.test.tsx
index 0295632cc137a..10fef0bb82df9 100644
--- a/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.test.tsx
@@ -16,11 +16,22 @@ describe('RecentCases', () => {
const createCaseHref = '/create';
const wrapper = mount(
-
+
);
expect(wrapper.find(`[data-test-subj="no-cases-create-case"]`).first().prop('href')).toEqual(
createCaseHref
);
});
+
+ it('displays a message without a link to create a case when the user does not have write permissions', () => {
+ const createCaseHref = '/create';
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find(`[data-test-subj="no-cases-create-case"]`).exists()).toBeFalsy();
+ expect(wrapper.find(`[data-test-subj="no-cases-readonly"]`).exists()).toBeTruthy();
+ });
});
diff --git a/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx b/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx
index df0efcec4552c..a5b90943a219a 100644
--- a/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx
+++ b/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx
@@ -10,16 +10,26 @@ import React from 'react';
import { EuiLink } from '@elastic/eui';
import * as i18n from '../translations';
-const NoCasesComponent = ({ createCaseHref }: { createCaseHref: string }) => (
- <>
- {i18n.NO_CASES}
- {` ${i18n.START_A_NEW_CASE}`}
- {'!'}
- >
-);
+const NoCasesComponent = ({
+ createCaseHref,
+ hasWritePermissions,
+}: {
+ createCaseHref: string;
+ hasWritePermissions: boolean;
+}) => {
+ return hasWritePermissions ? (
+ <>
+ {i18n.NO_CASES}
+ {` ${i18n.START_A_NEW_CASE}`}
+ {'!'}
+ >
+ ) : (
+ {i18n.NO_CASES_READ_ONLY}
+ );
+};
NoCasesComponent.displayName = 'NoCasesComponent';
diff --git a/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx b/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx
index 5b4313530e490..bfe44dda6c6ef 100644
--- a/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx
+++ b/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx
@@ -31,6 +31,7 @@ export interface RecentCasesProps {
caseDetailsNavigation: CasesNavigation;
createCaseNavigation: CasesNavigation;
maxCasesToShow: number;
+ hasWritePermissions: boolean;
}
const usePrevious = (value: Partial) => {
@@ -45,6 +46,7 @@ export const RecentCasesComp = ({
createCaseNavigation,
filterOptions,
maxCasesToShow,
+ hasWritePermissions,
}: RecentCasesProps) => {
const previousFilterOptions = usePrevious(filterOptions);
const { data, loading, setFilters } = useGetCases({
@@ -65,7 +67,7 @@ export const RecentCasesComp = ({
return isLoadingCases ? (
) : !isLoadingCases && data.cases.length === 0 ? (
-
+
) : (
<>
{data.cases.map((c, i) => (
diff --git a/x-pack/plugins/cases/public/components/recent_cases/translations.ts b/x-pack/plugins/cases/public/components/recent_cases/translations.ts
index c8f6c349d8f72..653bda4be2ebc 100644
--- a/x-pack/plugins/cases/public/components/recent_cases/translations.ts
+++ b/x-pack/plugins/cases/public/components/recent_cases/translations.ts
@@ -22,6 +22,10 @@ export const NO_CASES = i18n.translate('xpack.cases.recentCases.noCasesMessage',
defaultMessage: 'No cases have been created yet. Put your detective hat on and',
});
+export const NO_CASES_READ_ONLY = i18n.translate('xpack.cases.recentCases.noCasesMessageReadOnly', {
+ defaultMessage: 'No cases have been created yet.',
+});
+
export const RECENT_CASES = i18n.translate('xpack.cases.recentCases.recentCasesSidebarTitle', {
defaultMessage: 'Recent cases',
});
diff --git a/x-pack/plugins/cases/public/components/status/button.tsx b/x-pack/plugins/cases/public/components/status/button.tsx
index 623afeb43c596..675d83c759bc7 100644
--- a/x-pack/plugins/cases/public/components/status/button.tsx
+++ b/x-pack/plugins/cases/public/components/status/button.tsx
@@ -13,7 +13,6 @@ import { statuses } from './config';
interface Props {
status: CaseStatuses;
- disabled: boolean;
isLoading: boolean;
onStatusChanged: (status: CaseStatuses) => void;
}
@@ -21,12 +20,7 @@ interface Props {
// Rotate over the statuses. open -> in-progress -> closes -> open...
const getNextItem = (item: number) => (item + 1) % caseStatuses.length;
-const StatusActionButtonComponent: React.FC = ({
- status,
- onStatusChanged,
- disabled,
- isLoading,
-}) => {
+const StatusActionButtonComponent: React.FC = ({ status, onStatusChanged, isLoading }) => {
const indexOfCurrentStatus = useMemo(
() => caseStatuses.findIndex((caseStatus) => caseStatus === status),
[status]
@@ -41,7 +35,6 @@ const StatusActionButtonComponent: React.FC = ({
diff --git a/x-pack/plugins/cases/public/components/status/status.test.tsx b/x-pack/plugins/cases/public/components/status/status.test.tsx
index 4d13e57fbdee7..a685256741c43 100644
--- a/x-pack/plugins/cases/public/components/status/status.test.tsx
+++ b/x-pack/plugins/cases/public/components/status/status.test.tsx
@@ -42,17 +42,14 @@ describe('Stats', () => {
).toBe(false);
});
- it('it renders with the pop over disabled when initialized disabled', async () => {
+ it('renders without the arrow and is not clickable when initialized disabled', async () => {
const wrapper = mount(
);
expect(
- wrapper
- .find(`[data-test-subj="status-badge-open"] .euiBadge__iconButton`)
- .first()
- .prop('disabled')
- ).toBe(true);
+ wrapper.find(`[data-test-subj="status-badge-open"] .euiBadge__iconButton`).exists()
+ ).toBeFalsy();
});
it('it calls onClick when pressing the badge', async () => {
diff --git a/x-pack/plugins/cases/public/components/status/status.tsx b/x-pack/plugins/cases/public/components/status/status.tsx
index 3b832ce155400..3c186313a151a 100644
--- a/x-pack/plugins/cases/public/components/status/status.tsx
+++ b/x-pack/plugins/cases/public/components/status/status.tsx
@@ -29,18 +29,18 @@ const StatusComponent: React.FC = ({
const props = useMemo(
() => ({
color: type === StatusAll ? allCaseStatus[StatusAll].color : statuses[type].color,
- ...(withArrow ? { iconType: 'arrowDown', iconSide: 'right' as const } : {}),
+ // if we are disabled, don't show the arrow and don't allow the user to click
+ ...(withArrow && !disabled ? { iconType: 'arrowDown', iconSide: 'right' as const } : {}),
+ ...(!disabled ? { iconOnClick: onClick } : { iconOnClick: noop }),
}),
- [withArrow, type]
+ [disabled, onClick, withArrow, type]
);
return (
{type === StatusAll ? allCaseStatus[StatusAll].label : statuses[type].label}
diff --git a/x-pack/plugins/cases/public/components/tag_list/index.test.tsx b/x-pack/plugins/cases/public/components/tag_list/index.test.tsx
index b3fbcd30d4e97..2ced7502b3c3f 100644
--- a/x-pack/plugins/cases/public/components/tag_list/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/tag_list/index.test.tsx
@@ -8,13 +8,12 @@
import React from 'react';
import { mount } from 'enzyme';
-import { TagList } from '.';
+import { TagList, TagListProps } from '.';
import { getFormMock } from '../__mock__/form';
import { TestProviders } from '../../common/mock';
import { waitFor } from '@testing-library/react';
import { useForm } from '../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form';
import { useGetTags } from '../../containers/use_get_tags';
-import { SECURITY_SOLUTION_OWNER } from '../../../common';
jest.mock('../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form');
jest.mock('../../containers/use_get_tags');
@@ -33,12 +32,11 @@ jest.mock('@elastic/eui', () => {
};
});
const onSubmit = jest.fn();
-const defaultProps = {
- disabled: false,
+const defaultProps: TagListProps = {
+ userCanCrud: true,
isLoading: false,
onSubmit,
tags: [],
- owner: [SECURITY_SOLUTION_OWNER],
};
describe('TagList ', () => {
@@ -110,15 +108,13 @@ describe('TagList ', () => {
expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeTruthy();
});
- it('Renders disabled button', () => {
- const props = { ...defaultProps, disabled: true };
+ it('does not render when the user does not have write permissions', () => {
+ const props = { ...defaultProps, userCanCrud: false };
const wrapper = mount(
);
- expect(
- wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().prop('disabled')
- ).toBeTruthy();
+ expect(wrapper.find(`[data-test-subj="tag-list-edit"]`).exists()).toBeFalsy();
});
});
diff --git a/x-pack/plugins/cases/public/components/tag_list/index.tsx b/x-pack/plugins/cases/public/components/tag_list/index.tsx
index f260593369679..4e8946a6589a3 100644
--- a/x-pack/plugins/cases/public/components/tag_list/index.tsx
+++ b/x-pack/plugins/cases/public/components/tag_list/index.tsx
@@ -27,12 +27,11 @@ import { Tags } from './tags';
const CommonUseField = getUseField({ component: Field });
-interface TagListProps {
- disabled?: boolean;
+export interface TagListProps {
+ userCanCrud?: boolean;
isLoading: boolean;
onSubmit: (a: string[]) => void;
tags: string[];
- owner: string[];
}
const MyFlexGroup = styled(EuiFlexGroup)`
@@ -45,7 +44,7 @@ const MyFlexGroup = styled(EuiFlexGroup)`
`;
export const TagList = React.memo(
- ({ disabled = false, isLoading, onSubmit, tags, owner }: TagListProps) => {
+ ({ userCanCrud = true, isLoading, onSubmit, tags }: TagListProps) => {
const initialState = { tags };
const { form } = useForm({
defaultValue: initialState,
@@ -86,11 +85,10 @@ export const TagList = React.memo(
{i18n.TAGS}
{isLoading && }
- {!isLoading && (
+ {!isLoading && userCanCrud && (
{
expect(errorsMsg[0].id).toEqual('closed-case-push-error');
});
});
+
+ describe('user does not have write permissions', () => {
+ const noWriteProps = { ...defaultArgs, userCanCrud: false };
+
+ it('does not display a message when user does not have a premium license', async () => {
+ (useGetActionLicense as jest.Mock).mockImplementation(() => ({
+ isLoading: false,
+ actionLicense: {
+ ...actionLicense,
+ enabledInLicense: false,
+ },
+ }));
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () => usePushToService(noWriteProps),
+ {
+ wrapper: ({ children }) => {children} ,
+ }
+ );
+ await waitForNextUpdate();
+ expect(result.current.pushCallouts).toBeNull();
+ });
+ });
+
+ it('does not display a message when user does not have case enabled in config', async () => {
+ (useGetActionLicense as jest.Mock).mockImplementation(() => ({
+ isLoading: false,
+ actionLicense: {
+ ...actionLicense,
+ enabledInConfig: false,
+ },
+ }));
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () => usePushToService(noWriteProps),
+ {
+ wrapper: ({ children }) => {children} ,
+ }
+ );
+ await waitForNextUpdate();
+ expect(result.current.pushCallouts).toBeNull();
+ });
+ });
+
+ it('does not display a message when user does not have any connector configured', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () =>
+ usePushToService({
+ ...noWriteProps,
+ connectors: [],
+ connector: {
+ id: 'none',
+ name: 'none',
+ type: ConnectorTypes.none,
+ fields: null,
+ },
+ }),
+ {
+ wrapper: ({ children }) => {children} ,
+ }
+ );
+ await waitForNextUpdate();
+ expect(result.current.pushCallouts).toBeNull();
+ });
+ });
+
+ it('does not display a message when user does have a connector but is configured to none', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () =>
+ usePushToService({
+ ...noWriteProps,
+ connector: {
+ id: 'none',
+ name: 'none',
+ type: ConnectorTypes.none,
+ fields: null,
+ },
+ }),
+ {
+ wrapper: ({ children }) => {children} ,
+ }
+ );
+ await waitForNextUpdate();
+ expect(result.current.pushCallouts).toBeNull();
+ });
+ });
+
+ it('does not display a message when connector is deleted', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () =>
+ usePushToService({
+ ...noWriteProps,
+ connector: {
+ id: 'not-exist',
+ name: 'not-exist',
+ type: ConnectorTypes.none,
+ fields: null,
+ },
+ isValidConnector: false,
+ }),
+ {
+ wrapper: ({ children }) => {children} ,
+ }
+ );
+ await waitForNextUpdate();
+ expect(result.current.pushCallouts).toBeNull();
+ });
+ });
+
+ it('does not display a message when connector is deleted with empty connectors', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () =>
+ usePushToService({
+ ...noWriteProps,
+ connectors: [],
+ connector: {
+ id: 'not-exist',
+ name: 'not-exist',
+ type: ConnectorTypes.none,
+ fields: null,
+ },
+ isValidConnector: false,
+ }),
+ {
+ wrapper: ({ children }) => {children} ,
+ }
+ );
+ await waitForNextUpdate();
+ expect(result.current.pushCallouts).toBeNull();
+ });
+ });
+
+ it('does not display a message when case is closed', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(
+ () =>
+ usePushToService({
+ ...noWriteProps,
+ caseStatus: CaseStatuses.closed,
+ }),
+ {
+ wrapper: ({ children }) => {children} ,
+ }
+ );
+ await waitForNextUpdate();
+ expect(result.current.pushCallouts).toBeNull();
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx
index 00b88d372584b..6f711150b7744 100644
--- a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx
+++ b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx
@@ -67,9 +67,17 @@ export const usePushToService = ({
const errorsMsg = useMemo(() => {
let errors: ErrorMessage[] = [];
+
+ // these message require that the user do some sort of write action as a result of the message, readonly users won't
+ // be able to perform such an action so let's not display the error to the user in that situation
+ if (!userCanCrud) {
+ return errors;
+ }
+
if (actionLicense != null && !actionLicense.enabledInLicense) {
errors = [...errors, getLicenseError()];
}
+
if (connectors.length === 0 && connector.id === 'none' && !loadingLicense) {
errors = [
...errors,
@@ -136,12 +144,13 @@ export const usePushToService = ({
},
];
}
+
if (actionLicense != null && !actionLicense.enabledInConfig) {
errors = [...errors, getKibanaConfigError()];
}
return errors;
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [actionLicense, caseStatus, connectors.length, connector, loadingLicense]);
+ }, [actionLicense, caseStatus, connectors.length, connector, loadingLicense, userCanCrud]);
const pushToServiceButton = useMemo(
() => (
diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx
index f9bd941547078..c7cc71da92947 100644
--- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx
+++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx
@@ -241,7 +241,7 @@ export const UserActionTree = React.memo(
() => (
),
}),
@@ -363,10 +363,10 @@ export const UserActionTree = React.memo(
id={comment.id}
editLabel={i18n.EDIT_COMMENT}
quoteLabel={i18n.QUOTE}
- disabled={!userCanCrud}
isLoading={isLoadingIds.includes(comment.id)}
onEdit={handleManageMarkdownEditId.bind(null, comment.id)}
onQuote={handleManageQuote.bind(null, comment.comment)}
+ userCanCrud={userCanCrud}
/>
),
},
@@ -571,19 +571,24 @@ export const UserActionTree = React.memo(
]
);
- const bottomActions = [
- {
- username: (
-
- ),
- 'data-test-subj': 'add-comment',
- timelineIcon: (
-
- ),
- className: 'isEdit',
- children: MarkdownNewComment,
- },
- ];
+ const bottomActions = userCanCrud
+ ? [
+ {
+ username: (
+
+ ),
+ 'data-test-subj': 'add-comment',
+ timelineIcon: (
+
+ ),
+ className: 'isEdit',
+ children: MarkdownNewComment,
+ },
+ ]
+ : [];
const comments = [...userActions, ...bottomActions];
diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.test.tsx
index a5244e14ad243..155e9e2323e64 100644
--- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.test.tsx
+++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.test.tsx
@@ -7,7 +7,10 @@
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
-import { UserActionContentToolbar } from './user_action_content_toolbar';
+import {
+ UserActionContentToolbar,
+ UserActionContentToolbarProps,
+} from './user_action_content_toolbar';
jest.mock('react-router-dom', () => {
const originalModule = jest.requireActual('react-router-dom');
@@ -28,12 +31,12 @@ jest.mock('../../common/lib/kibana', () => ({
}),
}));
-const props = {
+const props: UserActionContentToolbarProps = {
getCaseDetailHrefWithCommentId: jest.fn().mockReturnValue('case-detail-url-with-comment-id-1'),
id: '1',
editLabel: 'edit',
quoteLabel: 'quote',
- disabled: false,
+ userCanCrud: true,
isLoading: false,
onEdit: jest.fn(),
onQuote: jest.fn(),
diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.tsx
index 7adaffce22c54..5fa12b8cfa434 100644
--- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.tsx
+++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_content_toolbar.tsx
@@ -11,15 +11,15 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { UserActionCopyLink } from './user_action_copy_link';
import { UserActionPropertyActions } from './user_action_property_actions';
-interface UserActionContentToolbarProps {
+export interface UserActionContentToolbarProps {
id: string;
getCaseDetailHrefWithCommentId: (commentId: string) => string;
editLabel: string;
quoteLabel: string;
- disabled: boolean;
isLoading: boolean;
onEdit: (id: string) => void;
onQuote: (id: string) => void;
+ userCanCrud: boolean;
}
const UserActionContentToolbarComponent = ({
@@ -27,26 +27,27 @@ const UserActionContentToolbarComponent = ({
getCaseDetailHrefWithCommentId,
editLabel,
quoteLabel,
- disabled,
isLoading,
onEdit,
onQuote,
+ userCanCrud,
}: UserActionContentToolbarProps) => (
-
-
-
+ {userCanCrud && (
+
+
+
+ )}
);
diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_property_actions.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_property_actions.tsx
index 44b5baf3246cc..ebc83de1ef36a 100644
--- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_property_actions.tsx
+++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_property_actions.tsx
@@ -14,7 +14,6 @@ interface UserActionPropertyActionsProps {
id: string;
editLabel: string;
quoteLabel: string;
- disabled: boolean;
isLoading: boolean;
onEdit: (id: string) => void;
onQuote: (id: string) => void;
@@ -24,7 +23,6 @@ const UserActionPropertyActionsComponent = ({
id,
editLabel,
quoteLabel,
- disabled,
isLoading,
onEdit,
onQuote,
@@ -35,19 +33,17 @@ const UserActionPropertyActionsComponent = ({
const propertyActions = useMemo(
() => [
{
- disabled,
iconType: 'pencil',
label: editLabel,
onClick: onEditClick,
},
{
- disabled,
iconType: 'quote',
label: quoteLabel,
onClick: onQuoteClick,
},
],
- [disabled, editLabel, quoteLabel, onEditClick, onQuoteClick]
+ [editLabel, quoteLabel, onEditClick, onQuoteClick]
);
return (
<>
diff --git a/x-pack/plugins/cases/public/containers/configure/translations.ts b/x-pack/plugins/cases/public/containers/configure/translations.ts
index e77b9f57c8f4c..01900b8850c19 100644
--- a/x-pack/plugins/cases/public/containers/configure/translations.ts
+++ b/x-pack/plugins/cases/public/containers/configure/translations.ts
@@ -12,3 +12,11 @@ export * from '../translations';
export const SUCCESS_CONFIGURE = i18n.translate('xpack.cases.configure.successSaveToast', {
defaultMessage: 'Saved external connection settings',
});
+
+export const READ_PERMISSIONS_ERROR_MSG = i18n.translate(
+ 'xpack.cases.configure.readPermissionsErrorDescription',
+ {
+ defaultMessage:
+ 'You do not have permissions to view connectors. If you would like to view the connectors associated with this case, contact your Kibana administrator.',
+ }
+);
diff --git a/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx b/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx
index 3b91c77d0235a..e350146c650ce 100644
--- a/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx
+++ b/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx
@@ -7,26 +7,40 @@
import { useState, useEffect, useCallback, useRef } from 'react';
-import * as i18n from '../translations';
import { fetchConnectors } from './api';
import { ActionConnector } from './types';
import { useToasts } from '../../common/lib/kibana';
+import * as i18n from './translations';
+
+interface ConnectorsState {
+ loading: boolean;
+ connectors: ActionConnector[];
+ permissionsError?: string;
+}
export interface UseConnectorsResponse {
loading: boolean;
connectors: ActionConnector[];
refetchConnectors: () => void;
+ permissionsError?: string;
}
-export const useConnectors = (): UseConnectorsResponse => {
+/**
+ * Retrieves the configured case connectors
+ *
+ * @param toastPermissionsErrors boolean controlling whether 403 and 401 errors should be displayed in a toast error
+ */
+export const useConnectors = ({
+ toastPermissionsErrors = true,
+}: {
+ toastPermissionsErrors?: boolean;
+} = {}): UseConnectorsResponse => {
const toasts = useToasts();
- const [state, setState] = useState<{
- loading: boolean;
- connectors: ActionConnector[];
- }>({
+ const [state, setState] = useState({
loading: true,
connectors: [],
});
+
const isCancelledRef = useRef(false);
const abortCtrlRef = useRef(new AbortController());
@@ -49,15 +63,26 @@ export const useConnectors = (): UseConnectorsResponse => {
}
} catch (error) {
if (!isCancelledRef.current) {
+ let permissionsError: string | undefined;
if (error.name !== 'AbortError') {
- toasts.addError(
- error.body && error.body.message ? new Error(error.body.message) : error,
- { title: i18n.ERROR_TITLE }
- );
+ // if the error was related to permissions then let's return a boilerplate error message describing the problem
+ if (error.body?.statusCode === 403 || error.body?.statusCode === 401) {
+ permissionsError = i18n.READ_PERMISSIONS_ERROR_MSG;
+ }
+
+ // if the error was not permissions related then toast it
+ // if it was permissions related (permissionsError was defined) and the caller wants to toast, then create a toast
+ if (permissionsError === undefined || toastPermissionsErrors) {
+ toasts.addError(
+ error.body && error.body.message ? new Error(error.body.message) : error,
+ { title: i18n.ERROR_TITLE }
+ );
+ }
}
setState({
loading: false,
connectors: [],
+ permissionsError,
});
}
}
@@ -77,5 +102,6 @@ export const useConnectors = (): UseConnectorsResponse => {
loading: state.loading,
connectors: state.connectors,
refetchConnectors,
+ permissionsError: state.permissionsError,
};
};
diff --git a/x-pack/plugins/cases/public/mocks.ts b/x-pack/plugins/cases/public/mocks.ts
new file mode 100644
index 0000000000000..c543baa477475
--- /dev/null
+++ b/x-pack/plugins/cases/public/mocks.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 { CasesUiStart } from './types';
+
+const createStartContract = (): jest.Mocked => ({
+ getAllCases: jest.fn(),
+ getAllCasesSelectorModal: jest.fn(),
+ getCaseView: jest.fn(),
+ getConfigureCases: jest.fn(),
+ getCreateCase: jest.fn(),
+ getRecentCases: jest.fn(),
+});
+
+export const casesPluginMock = {
+ createStartContract,
+};
diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.test.ts b/x-pack/plugins/cases/server/authorization/audit_logger.test.ts
index d54b5164b10b9..48c6e9ebcd07a 100644
--- a/x-pack/plugins/cases/server/authorization/audit_logger.test.ts
+++ b/x-pack/plugins/cases/server/authorization/audit_logger.test.ts
@@ -143,7 +143,7 @@ describe('audit_logger', () => {
// for reference: https://github.com/facebook/jest/issues/9409#issuecomment-629272237
// This loops through all operation keys
- it.each(Array.from(Object.keys(Operations)))(
+ it.each(Object.keys(Operations))(
`creates the correct audit event for operation: "%s" without an error or entity`,
(operationKey) => {
// forcing the cast here because using a string throws a type error
@@ -156,7 +156,7 @@ describe('audit_logger', () => {
);
// This loops through all operation keys
- it.each(Array.from(Object.keys(Operations)))(
+ it.each(Object.keys(Operations))(
`creates the correct audit event for operation: "%s" with an error but no entity`,
(operationKey) => {
// forcing the cast here because using a string throws a type error
@@ -170,7 +170,7 @@ describe('audit_logger', () => {
);
// This loops through all operation keys
- it.each(Array.from(Object.keys(Operations)))(
+ it.each(Object.keys(Operations))(
`creates the correct audit event for operation: "%s" with an error and entity`,
(operationKey) => {
// forcing the cast here because using a string throws a type error
@@ -188,7 +188,7 @@ describe('audit_logger', () => {
);
// This loops through all operation keys
- it.each(Array.from(Object.keys(Operations)))(
+ it.each(Object.keys(Operations))(
`creates the correct audit event for operation: "%s" without an error but with an entity`,
(operationKey) => {
// forcing the cast here because using a string throws a type error
diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts
index 28b9cf9e4e032..b1e2f61a595ee 100644
--- a/x-pack/plugins/cases/server/plugin.ts
+++ b/x-pack/plugins/cases/server/plugin.ts
@@ -72,7 +72,7 @@ export class CasePlugin {
this.clientFactory = new CasesClientFactory(this.log);
}
- public async setup(core: CoreSetup, plugins: PluginsSetup) {
+ public setup(core: CoreSetup, plugins: PluginsSetup) {
const config = createConfig(this.initializerContext);
if (!config.enabled) {
diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx
index 29b17cd426c58..fdd49ad17168d 100644
--- a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx
+++ b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx
@@ -5,18 +5,7 @@
* 2.0.
*/
-import React from 'react';
import md5 from 'md5';
-import * as i18n from './translations';
-import { ErrorMessage } from './types';
-
-export const permissionsReadOnlyErrorMessage: ErrorMessage = {
- id: 'read-only-privileges-error',
- title: i18n.READ_ONLY_FEATURE_TITLE,
- description: <>{i18n.READ_ONLY_FEATURE_MSG}>,
- errorType: 'warning',
-};
-
export const createCalloutId = (ids: string[], delimiter: string = '|'): string =>
md5(ids.join(delimiter));
diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts
index cb7236b445be1..20bb57daf5841 100644
--- a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts
+++ b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts
@@ -7,21 +7,6 @@
import { i18n } from '@kbn/i18n';
-export const READ_ONLY_FEATURE_TITLE = i18n.translate(
- 'xpack.observability.cases.readOnlyFeatureTitle',
- {
- defaultMessage: 'You cannot open new or update existing cases',
- }
-);
-
-export const READ_ONLY_FEATURE_MSG = i18n.translate(
- 'xpack.observability.cases.readOnlyFeatureDescription',
- {
- defaultMessage:
- 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.',
- }
-);
-
export const DISMISS_CALLOUT = i18n.translate(
'xpack.observability.cases.dismissErrorsPushServiceCallOutTitle',
{
diff --git a/x-pack/plugins/observability/public/components/app/cases/translations.ts b/x-pack/plugins/observability/public/components/app/cases/translations.ts
index 1a5abe218edf5..a85b0bc744e66 100644
--- a/x-pack/plugins/observability/public/components/app/cases/translations.ts
+++ b/x-pack/plugins/observability/public/components/app/cases/translations.ts
@@ -201,3 +201,17 @@ export const CONNECTORS = i18n.translate('xpack.observability.cases.caseView.con
export const EDIT_CONNECTOR = i18n.translate('xpack.observability.cases.caseView.editConnector', {
defaultMessage: 'Change external incident management system',
});
+
+export const READ_ONLY_BADGE_TEXT = i18n.translate(
+ 'xpack.observability.cases.badge.readOnly.text',
+ {
+ defaultMessage: 'Read only',
+ }
+);
+
+export const READ_ONLY_BADGE_TOOLTIP = i18n.translate(
+ 'xpack.observability.cases.badge.readOnly.tooltip',
+ {
+ defaultMessage: 'Unable to create or edit cases',
+ }
+);
diff --git a/x-pack/plugins/observability/public/hooks/use_readonly_header.tsx b/x-pack/plugins/observability/public/hooks/use_readonly_header.tsx
new file mode 100644
index 0000000000000..4d8779e1ea150
--- /dev/null
+++ b/x-pack/plugins/observability/public/hooks/use_readonly_header.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 { useCallback, useEffect } from 'react';
+
+import * as i18n from '../components/app/cases/translations';
+import { useGetUserCasesPermissions } from '../hooks/use_get_user_cases_permissions';
+import { useKibana } from '../utils/kibana_react';
+
+/**
+ * This component places a read-only icon badge in the header if user only has read permissions
+ */
+export function useReadonlyHeader() {
+ const userPermissions = useGetUserCasesPermissions();
+ const chrome = useKibana().services.chrome;
+
+ // if the user is read only then display the glasses badge in the global navigation header
+ const setBadge = useCallback(() => {
+ if (userPermissions != null && !userPermissions.crud && userPermissions.read) {
+ chrome.setBadge({
+ text: i18n.READ_ONLY_BADGE_TEXT,
+ tooltip: i18n.READ_ONLY_BADGE_TOOLTIP,
+ iconType: 'glasses',
+ });
+ }
+ }, [chrome, userPermissions]);
+
+ useEffect(() => {
+ setBadge();
+
+ // remove the icon after the component unmounts
+ return () => {
+ chrome.setBadge();
+ };
+ }, [setBadge, chrome]);
+}
diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx
index f73f3b4cf57d7..442104a710601 100644
--- a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx
@@ -10,35 +10,28 @@ import React from 'react';
import { AllCases } from '../../components/app/cases/all_cases';
import * as i18n from '../../components/app/cases/translations';
-import { permissionsReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout';
import { CaseFeatureNoPermissions } from './feature_no_permissions';
import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
import { usePluginContext } from '../../hooks/use_plugin_context';
+import { useReadonlyHeader } from '../../hooks/use_readonly_header';
import { casesBreadcrumbs } from './links';
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
export const AllCasesPage = React.memo(() => {
const userPermissions = useGetUserCasesPermissions();
const { ObservabilityPageTemplate } = usePluginContext();
+ useReadonlyHeader();
useBreadcrumbs([casesBreadcrumbs.cases]);
return userPermissions == null || userPermissions?.read ? (
- <>
- {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
-
- )}
- {i18n.PAGE_TITLE}>,
- }}
- >
-
-
- >
+ {i18n.PAGE_TITLE}>,
+ }}
+ >
+
+
) : (
);
diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx
index 6adf5ad286808..f93cb5c4e7919 100644
--- a/x-pack/plugins/observability/public/pages/cases/case_details.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx
@@ -5,45 +5,35 @@
* 2.0.
*/
-import React from 'react';
+import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { CaseView } from '../../components/app/cases/case_view';
import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
import { useKibana } from '../../utils/kibana_react';
import { CASES_APP_ID } from '../../components/app/cases/constants';
-import { CaseCallOut, permissionsReadOnlyErrorMessage } from '../../components/app/cases/callout';
+import { useReadonlyHeader } from '../../hooks/use_readonly_header';
export const CaseDetailsPage = React.memo(() => {
const {
application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
+ const casesUrl = getUrlForApp(CASES_APP_ID);
const userPermissions = useGetUserCasesPermissions();
const { detailName: caseId, subCaseId } = useParams<{
detailName?: string;
subCaseId?: string;
}>();
+ useReadonlyHeader();
- const casesUrl = getUrlForApp(CASES_APP_ID);
- if (userPermissions != null && !userPermissions.read) {
- navigateToUrl(casesUrl);
- return null;
- }
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.read) {
+ navigateToUrl(casesUrl);
+ }
+ }, [casesUrl, navigateToUrl, userPermissions]);
return caseId != null ? (
- <>
- {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
-
- )}
-
- >
+
) : null;
});
diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx
index a4df4855b0204..9676eb7eba147 100644
--- a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect } from 'react';
import styled from 'styled-components';
import { EuiButtonEmpty } from '@elastic/eui';
@@ -38,10 +38,12 @@ function ConfigureCasesPageComponent() {
const { formatUrl } = useFormatUrl(CASES_APP_ID);
const href = formatUrl(getCaseUrl());
useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.configure]);
- if (userPermissions != null && !userPermissions.read) {
- navigateToUrl(casesUrl);
- return null;
- }
+
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.read) {
+ navigateToUrl(casesUrl);
+ }
+ }, [casesUrl, userPermissions, navigateToUrl]);
return (
{
const { formatUrl } = useFormatUrl(CASES_APP_ID);
const href = formatUrl(getCaseUrl());
useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.create]);
- if (userPermissions != null && !userPermissions.crud) {
- navigateToUrl(casesUrl);
- return null;
- }
+
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.crud) {
+ navigateToUrl(casesUrl);
+ }
+ }, [casesUrl, navigateToUrl, userPermissions]);
return (
{
});
it('should not allow user with read only privileges to attach alerts to cases', () => {
- cy.get(ATTACH_ALERT_TO_CASE_BUTTON).first().should('be.disabled');
+ cy.get(ATTACH_ALERT_TO_CASE_BUTTON).should('not.exist');
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx
index 29b17cd426c58..fdd49ad17168d 100644
--- a/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx
@@ -5,18 +5,7 @@
* 2.0.
*/
-import React from 'react';
import md5 from 'md5';
-import * as i18n from './translations';
-import { ErrorMessage } from './types';
-
-export const permissionsReadOnlyErrorMessage: ErrorMessage = {
- id: 'read-only-privileges-error',
- title: i18n.READ_ONLY_FEATURE_TITLE,
- description: <>{i18n.READ_ONLY_FEATURE_MSG}>,
- errorType: 'warning',
-};
-
export const createCalloutId = (ids: string[], delimiter: string = '|'): string =>
md5(ids.join(delimiter));
diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts b/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts
index db4809126452f..617995cc366b0 100644
--- a/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts
+++ b/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts
@@ -7,21 +7,6 @@
import { i18n } from '@kbn/i18n';
-export const READ_ONLY_FEATURE_TITLE = i18n.translate(
- 'xpack.securitySolution.cases.readOnlyFeatureTitle',
- {
- defaultMessage: 'You cannot open new or update existing cases',
- }
-);
-
-export const READ_ONLY_FEATURE_MSG = i18n.translate(
- 'xpack.securitySolution.cases.readOnlyFeatureDescription',
- {
- defaultMessage:
- 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.',
- }
-);
-
export const DISMISS_CALLOUT = i18n.translate(
'xpack.securitySolution.cases.dismissErrorsPushServiceCallOutTitle',
{
diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx
index 77fa9e8b3cc8c..02047c774ca6f 100644
--- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx
@@ -200,7 +200,7 @@ describe('AddToCaseAction', () => {
).toBeTruthy();
});
- it('disabled when user does not have crud permissions', () => {
+ it('hides the icon when user does not have crud permissions', () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
crud: false,
read: true,
@@ -212,8 +212,6 @@ describe('AddToCaseAction', () => {
);
- expect(
- wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().prop('isDisabled')
- ).toBeTruthy();
+ expect(wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).exists()).toBeFalsy();
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx
index eaad912a4dc51..7025bff1ce49a 100644
--- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx
@@ -208,19 +208,21 @@ const AddToCaseActionComponent: React.FC = ({
return (
<>
-
-
-
-
-
+ {userCanCrud && (
+
+
+
+
+
+ )}
{isCreateCaseFlyoutOpen && (
{
return userPermissions == null || userPermissions?.read ? (
<>
- {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
-
- )}
diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
index 7307733426862..a086409e55df5 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React from 'react';
+import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { SecurityPageName } from '../../app/types';
@@ -16,7 +16,6 @@ import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana';
import { getCaseUrl } from '../../common/components/link_to';
import { navTabs } from '../../app/home/home_navigations';
import { CaseView } from '../components/case_view';
-import { permissionsReadOnlyErrorMessage, CaseCallOut } from '../components/callout';
import { CASES_APP_ID } from '../../../common/constants';
export const CaseDetailsPage = React.memo(() => {
@@ -30,20 +29,15 @@ export const CaseDetailsPage = React.memo(() => {
}>();
const search = useGetUrlSearch(navTabs.case);
- if (userPermissions != null && !userPermissions.read) {
- navigateToApp(CASES_APP_ID, { path: getCaseUrl(search) });
- return null;
- }
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.read) {
+ navigateToApp(CASES_APP_ID, { path: getCaseUrl(search) });
+ }
+ }, [navigateToApp, userPermissions, search]);
return caseId != null ? (
<>
- {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
-
- )}
{
[search]
);
- if (userPermissions != null && !userPermissions.read) {
- navigateToApp(CASES_APP_ID, { path: getCaseUrl(search) });
- return null;
- }
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.read) {
+ navigateToApp(CASES_APP_ID, {
+ path: getCaseUrl(search),
+ });
+ }
+ }, [navigateToApp, userPermissions, search]);
const HeaderWrapper = styled.div`
padding-top: ${({ theme }) => theme.eui.paddingSizes.l};
diff --git a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx
index 19f97bae60ebe..3c5197f19eff1 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useMemo } from 'react';
+import React, { useEffect, useMemo } from 'react';
import { SecurityPageName } from '../../app/types';
import { getCaseUrl } from '../../common/components/link_to';
@@ -25,6 +25,7 @@ export const CreateCasePage = React.memo(() => {
const {
application: { navigateToApp },
} = useKibana().services;
+
const backOptions = useMemo(
() => ({
href: getCaseUrl(search),
@@ -34,12 +35,13 @@ export const CreateCasePage = React.memo(() => {
[search]
);
- if (userPermissions != null && !userPermissions.crud) {
- navigateToApp(CASES_APP_ID, {
- path: getCaseUrl(search),
- });
- return null;
- }
+ useEffect(() => {
+ if (userPermissions != null && !userPermissions.crud) {
+ navigateToApp(CASES_APP_ID, {
+ path: getCaseUrl(search),
+ });
+ }
+ }, [userPermissions, navigateToApp, search]);
return (
<>
diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.test.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.test.tsx
new file mode 100644
index 0000000000000..0d12d63fdc244
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/pages/index.test.tsx
@@ -0,0 +1,91 @@
+/*
+ * 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 { mount } from 'enzyme';
+import { BrowserRouter as Router } from 'react-router-dom';
+
+import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana';
+import { TestProviders } from '../../common/mock';
+import { Case } from '.';
+
+const useKibanaMock = useKibana as jest.Mocked;
+jest.mock('../../common/lib/kibana');
+
+const mockedSetBadge = jest.fn();
+
+describe('CaseContainerComponent', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ useKibanaMock().services.chrome.setBadge = mockedSetBadge;
+ });
+
+ it('does not display the readonly glasses badge when the user has write permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: true,
+ read: false,
+ });
+
+ mount(
+
+
+
+
+
+ );
+
+ expect(mockedSetBadge).not.toBeCalled();
+ });
+
+ it('does not display the readonly glasses badge when the user has neither write nor read permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: false,
+ read: false,
+ });
+
+ mount(
+
+
+
+
+
+ );
+
+ expect(mockedSetBadge).not.toBeCalled();
+ });
+
+ it('does not display the readonly glasses badge when the user has null permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue(null);
+
+ mount(
+
+
+
+
+
+ );
+
+ expect(mockedSetBadge).not.toBeCalled();
+ });
+
+ it('displays the readonly glasses badge read permissions but not write', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: false,
+ read: true,
+ });
+
+ mount(
+
+
+
+
+
+ );
+
+ expect(mockedSetBadge).toBeCalledTimes(1);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx
index 314bdc9bfd117..fca19cf5c70a7 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx
@@ -5,13 +5,15 @@
* 2.0.
*/
-import React from 'react';
-
+import React, { useEffect } from 'react';
import { Route, Switch } from 'react-router-dom';
+
+import * as i18n from './translations';
import { CaseDetailsPage } from './case_details';
import { CasesPage } from './case';
import { CreateCasePage } from './create_case';
import { ConfigureCasesPage } from './configure_cases';
+import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana';
const casesPagePath = '';
const caseDetailsPagePath = `${casesPagePath}/:detailName`;
@@ -21,30 +23,51 @@ const subCaseDetailsPagePathWithCommentId = `${subCaseDetailsPagePath}/:commentI
const createCasePagePath = `${casesPagePath}/create`;
const configureCasesPagePath = `${casesPagePath}/configure`;
-const CaseContainerComponent: React.FC = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
+const CaseContainerComponent: React.FC = () => {
+ const userPermissions = useGetUserCasesPermissions();
+ const chrome = useKibana().services.chrome;
+
+ useEffect(() => {
+ // if the user is read only then display the glasses badge in the global navigation header
+ if (userPermissions != null && !userPermissions.crud && userPermissions.read) {
+ chrome.setBadge({
+ text: i18n.READ_ONLY_BADGE_TEXT,
+ tooltip: i18n.READ_ONLY_BADGE_TOOLTIP,
+ iconType: 'glasses',
+ });
+ }
+
+ // remove the icon after the component unmounts
+ return () => {
+ chrome.setBadge();
+ };
+ }, [userPermissions, chrome]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
export const Case = React.memo(CaseContainerComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/pages/translations.ts b/x-pack/plugins/security_solution/public/cases/pages/translations.ts
index 1a811a3fd7bbc..6768401b3f608 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/translations.ts
+++ b/x-pack/plugins/security_solution/public/cases/pages/translations.ts
@@ -157,3 +157,24 @@ export const GO_TO_DOCUMENTATION = i18n.translate(
export const CONNECTORS = i18n.translate('xpack.securitySolution.cases.caseView.connectors', {
defaultMessage: 'External Incident Management System',
});
+
+export const EDIT_CONNECTOR = i18n.translate(
+ 'xpack.securitySolution.cases.caseView.editConnector',
+ {
+ defaultMessage: 'Change external incident management system',
+ }
+);
+
+export const READ_ONLY_BADGE_TEXT = i18n.translate(
+ 'xpack.securitySolution.cases.badge.readOnly.text',
+ {
+ defaultMessage: 'Read only',
+ }
+);
+
+export const READ_ONLY_BADGE_TOOLTIP = i18n.translate(
+ 'xpack.securitySolution.cases.badge.readOnly.tooltip',
+ {
+ defaultMessage: 'Unable to create or edit cases',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx
new file mode 100644
index 0000000000000..96a7eacb7fb08
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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 { mount } from 'enzyme';
+
+import { useGetUserCasesPermissions } from '../../../common/lib/kibana';
+import { TestProviders } from '../../../common/mock';
+import { HeaderGlobal } from '.';
+
+jest.mock('../../../common/lib/kibana');
+
+describe('HeaderGlobal', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('does not display the cases tab when the user does not have read permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: false,
+ read: false,
+ });
+
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find(`[data-test-subj="navigation-case"]`).exists()).toBeFalsy();
+ });
+
+ it('displays the cases tab when the user has read permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: true,
+ read: true,
+ });
+
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find(`[data-test-subj="navigation-case"]`).exists()).toBeTruthy();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
index 4a7ac8a148f64..e91905183aab1 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
@@ -19,7 +19,7 @@ import { MlPopover } from '../ml_popover/ml_popover';
import { SiemNavigation } from '../navigation';
import * as i18n from './translations';
import { useGetUrlSearch } from '../navigation/use_get_url_search';
-import { useKibana } from '../../lib/kibana';
+import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana';
import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants';
import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal';
import { LinkAnchor } from '../links';
@@ -91,6 +91,18 @@ export const HeaderGlobal = React.memo(
},
[navigateToApp, search]
);
+
+ const hasCasesReadPermissions = useGetUserCasesPermissions()?.read;
+
+ // build a list of tabs to exclude
+ const tabsToExclude = new Set([
+ ...(hideDetectionEngine ? [SecurityPageName.detections] : []),
+ ...(!hasCasesReadPermissions ? [SecurityPageName.case] : []),
+ ]);
+
+ // include the tab if it is not in the set of excluded ones
+ const tabsToDisplay = pickBy((_, key) => !tabsToExclude.has(key), navTabs);
+
return (
@@ -109,14 +121,7 @@ export const HeaderGlobal = React.memo(
- key !== SecurityPageName.detections, navTabs)
- : navTabs
- }
- />
+
diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx
index 996835296fcc4..cb7733e304985 100644
--- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx
@@ -13,7 +13,7 @@ import {
getCreateCaseUrl,
} from '../../../common/components/link_to/redirect_to_case';
import { useFormatUrl } from '../../../common/components/link_to';
-import { useKibana } from '../../../common/lib/kibana';
+import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana';
import { APP_ID, CASES_APP_ID } from '../../../../common/constants';
import { SecurityPageName } from '../../../app/types';
import { AllCasesNavProps } from '../../../cases/components/all_cases';
@@ -26,6 +26,8 @@ const RecentCasesComponent = () => {
application: { navigateToApp },
} = useKibana().services;
+ const hasWritePermissions = useGetUserCasesPermissions()?.crud ?? false;
+
return casesUi.getRecentCases({
allCasesNavigation: {
href: formatUrl(getCaseUrl()),
@@ -60,6 +62,7 @@ const RecentCasesComponent = () => {
});
},
},
+ hasWritePermissions,
maxCasesToShow: MAX_CASES_TO_SHOW,
owner: [APP_ID],
});
diff --git a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx
new file mode 100644
index 0000000000000..76c5663644a78
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx
@@ -0,0 +1,72 @@
+/*
+ * 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 { mount } from 'enzyme';
+import { waitFor } from '@testing-library/react';
+import { TestProviders } from '../../../common/mock';
+import { Sidebar } from './sidebar';
+import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana';
+import { casesPluginMock } from '../../../../../cases/public/mocks';
+import { CasesUiStart } from '../../../../../cases/public';
+
+jest.mock('../../../common/lib/kibana');
+
+const useKibanaMock = useKibana as jest.MockedFunction;
+
+describe('Sidebar', () => {
+ let casesMock: jest.Mocked;
+
+ beforeEach(() => {
+ casesMock = casesPluginMock.createStartContract();
+ casesMock.getRecentCases.mockImplementation(() => <>{'test'}>);
+ useKibanaMock.mockReturnValue(({
+ services: {
+ cases: casesMock,
+ application: {
+ // these are needed by the RecentCases component if it is rendered.
+ navigateToApp: jest.fn(),
+ getUrlForApp: jest.fn(() => ''),
+ },
+ },
+ } as unknown) as ReturnType);
+ });
+
+ it('does not render the recently created cases section when the user does not have read permissions', async () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: false,
+ read: false,
+ });
+
+ await waitFor(() =>
+ mount(
+
+ {}} />
+
+ )
+ );
+
+ expect(casesMock.getRecentCases).not.toHaveBeenCalled();
+ });
+
+ it('does render the recently created cases section when the user has read permissions', async () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: false,
+ read: true,
+ });
+
+ await waitFor(() =>
+ mount(
+
+ {}} />
+
+ )
+ );
+
+ expect(casesMock.getRecentCases).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx
index 77cfa220f0722..b8701f3ef1639 100644
--- a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx
@@ -18,6 +18,7 @@ import { SidebarHeader } from '../../../common/components/sidebar_header';
import * as i18n from '../../pages/translations';
import { RecentCases } from '../recent_cases';
+import { useGetUserCasesPermissions } from '../../../common/lib/kibana';
const SidebarFlexGroup = styled(EuiFlexGroup)`
width: 305px;
@@ -46,13 +47,20 @@ export const Sidebar = React.memo<{
[recentTimelinesFilterBy, setRecentTimelinesFilterBy]
);
+ // only render the recently created cases view if the user has at least read permissions
+ const hasCasesReadPermissions = useGetUserCasesPermissions()?.read;
+
return (
-
-
-
+ {hasCasesReadPermissions && (
+ <>
+
+
+
-
+
+ >
+ )}
{recentTimelinesFilters}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx
index 68b4f2e4a0c31..206fcb2dc087c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
-import { useKibana } from '../../../../common/lib/kibana';
+import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana';
import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock';
import { TimelineId } from '../../../../../common/types/timeline';
import { useTimelineKpis } from '../../../containers/kpis';
@@ -57,7 +57,7 @@ const defaultMocks = {
loading: false,
selectedPatterns: mockIndexNames,
};
-describe('Timeline KPIs', () => {
+describe('header', () => {
const mount = useMountAppended();
beforeEach(() => {
@@ -75,86 +75,124 @@ describe('Timeline KPIs', () => {
jest.clearAllMocks();
});
- describe('when the data is not loading and the response contains data', () => {
+ describe('AddToCaseButton', () => {
beforeEach(() => {
mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]);
});
- it('renders the component, labels and values succesfully', async () => {
+
+ it('renders the button when the user has write permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: true,
+ read: false,
+ });
+
const wrapper = mount(
);
- expect(wrapper.find('[data-test-subj="siem-timeline-kpis"]').exists()).toEqual(true);
- // label
- expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
- expect.stringContaining('Processes')
- );
- // value
- expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
- expect.stringContaining('1')
- );
- });
- });
- describe('when the data is loading', () => {
- beforeEach(() => {
- mockUseTimelineKpis.mockReturnValue([true, mockUseTimelineKpiResponse]);
+ expect(wrapper.find('[data-test-subj="attach-timeline-case-button"]').exists()).toBeTruthy();
});
- it('renders a loading indicator for values', async () => {
+
+ it('does not render the button when the user does not have write permissions', () => {
+ (useGetUserCasesPermissions as jest.Mock).mockReturnValue({
+ crud: false,
+ read: false,
+ });
+
const wrapper = mount(
);
- expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
- expect.stringContaining('--')
- );
+
+ expect(wrapper.find('[data-test-subj="attach-timeline-case-button"]').exists()).toBeFalsy();
});
});
- describe('when the response is null and timeline is blank', () => {
- beforeEach(() => {
- mockUseTimelineKpis.mockReturnValue([false, null]);
+ describe('Timeline KPIs', () => {
+ describe('when the data is not loading and the response contains data', () => {
+ beforeEach(() => {
+ mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]);
+ });
+ it('renders the component, labels and values successfully', async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-kpis"]').exists()).toEqual(true);
+ // label
+ expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
+ expect.stringContaining('Processes')
+ );
+ // value
+ expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
+ expect.stringContaining('1')
+ );
+ });
});
- it('renders labels and the default empty string', async () => {
- const wrapper = mount(
-
-
-
- );
- expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
- expect.stringContaining('Processes')
- );
- expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
- expect.stringContaining(getEmptyValue())
- );
+ describe('when the data is loading', () => {
+ beforeEach(() => {
+ mockUseTimelineKpis.mockReturnValue([true, mockUseTimelineKpiResponse]);
+ });
+ it('renders a loading indicator for values', async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
+ expect.stringContaining('--')
+ );
+ });
});
- });
- describe('when the response contains numbers larger than one thousand', () => {
- beforeEach(() => {
- mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineLargeKpiResponse]);
+ describe('when the response is null and timeline is blank', () => {
+ beforeEach(() => {
+ mockUseTimelineKpis.mockReturnValue([false, null]);
+ });
+ it('renders labels and the default empty string', async () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
+ expect.stringContaining('Processes')
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
+ expect.stringContaining(getEmptyValue())
+ );
+ });
});
- it('formats the numbers correctly', async () => {
- const wrapper = mount(
-
-
-
- );
- expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
- expect.stringContaining('1k')
- );
- expect(wrapper.find('[data-test-subj="siem-timeline-user-kpi"]').first().text()).toEqual(
- expect.stringContaining('1m')
- );
- expect(wrapper.find('[data-test-subj="siem-timeline-source-ip-kpi"]').first().text()).toEqual(
- expect.stringContaining('1b')
- );
- expect(wrapper.find('[data-test-subj="siem-timeline-host-kpi"]').first().text()).toEqual(
- expect.stringContaining('999')
- );
+
+ describe('when the response contains numbers larger than one thousand', () => {
+ beforeEach(() => {
+ mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineLargeKpiResponse]);
+ });
+ it('formats the numbers correctly', async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
+ expect.stringContaining('1k')
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-user-kpi"]').first().text()).toEqual(
+ expect.stringContaining('1m')
+ );
+ expect(
+ wrapper.find('[data-test-subj="siem-timeline-source-ip-kpi"]').first().text()
+ ).toEqual(expect.stringContaining('1b'));
+ expect(wrapper.find('[data-test-subj="siem-timeline-host-kpi"]').first().text()).toEqual(
+ expect.stringContaining('999')
+ );
+ });
});
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
index dd8cdb818cad7..216282b72920c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
@@ -35,7 +35,7 @@ import { TimerangeInput } from '../../../../../common/search_strategy';
import { AddToCaseButton } from '../add_to_case_button';
import { AddTimelineButton } from '../add_timeline_button';
import { SaveTimelineButton } from '../../timeline/header/save_timeline_button';
-import { useKibana } from '../../../../common/lib/kibana';
+import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana';
import { InspectButton } from '../../../../common/components/inspect';
import { useTimelineKpis } from '../../../containers/kpis';
import { esQuery } from '../../../../../../../../src/plugins/data/public';
@@ -319,6 +319,8 @@ const FlyoutHeaderComponent: React.FC = ({ timelineId }) => {
filterQuery: combinedQueries?.filterQuery ?? '',
});
+ const hasWritePermissions = useGetUserCasesPermissions()?.crud ?? false;
+
return (
@@ -350,9 +352,11 @@ const FlyoutHeaderComponent: React.FC = ({ timelineId }) => {
-
-
-
+ {hasWritePermissions && (
+
+
+
+ )}
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index 7e4d0989af413..ac9d854f18211 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -129,17 +129,23 @@ export interface PluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PluginStart {}
-const securitySubPlugins = [
+const casesSubPlugin = `${APP_ID}:${SecurityPageName.case}`;
+
+/**
+ * Don't include cases here so that the sub feature can govern whether Cases is enabled in the navigation
+ */
+const securitySubPluginsNoCases = [
APP_ID,
`${APP_ID}:${SecurityPageName.overview}`,
`${APP_ID}:${SecurityPageName.detections}`,
`${APP_ID}:${SecurityPageName.hosts}`,
`${APP_ID}:${SecurityPageName.network}`,
`${APP_ID}:${SecurityPageName.timelines}`,
- `${APP_ID}:${SecurityPageName.case}`,
`${APP_ID}:${SecurityPageName.administration}`,
];
+const allSecuritySubPlugins = [...securitySubPluginsNoCases, casesSubPlugin];
+
export class Plugin implements IPlugin {
private readonly logger: Logger;
private readonly config: ConfigType;
@@ -305,7 +311,7 @@ export class Plugin implements IPlugin {
await PageObjects.common.navigateToActualUrl('observabilityCases');
- await PageObjects.observability.expectCreateCaseButtonDisabled();
+ await PageObjects.observability.expectCreateCaseButtonMissing();
});
- it(`shows read-only callout`, async () => {
- await PageObjects.observability.expectReadOnlyCallout();
+ it(`shows read-only glasses badge`, async () => {
+ await PageObjects.observability.expectReadOnlyGlassesBadge();
});
it(`does not allow a case to be created`, async () => {
@@ -151,7 +151,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
// expect redirection to observability cases landing
- await PageObjects.observability.expectCreateCaseButtonDisabled();
+ await PageObjects.observability.expectCreateCaseButtonMissing();
});
it(`does not allow a case to be edited`, async () => {
@@ -162,7 +162,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
shouldUseHashForSubUrl: false,
}
);
- await PageObjects.observability.expectAddCommentButtonDisabled();
+ await PageObjects.observability.expectAddCommentButtonMissing();
});
});
diff --git a/x-pack/test/functional/page_objects/observability_page.ts b/x-pack/test/functional/page_objects/observability_page.ts
index 95016c31d1054..d9e413d473adf 100644
--- a/x-pack/test/functional/page_objects/observability_page.ts
+++ b/x-pack/test/functional/page_objects/observability_page.ts
@@ -20,14 +20,12 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro
expect(disabledAttr).to.be(null);
},
- async expectCreateCaseButtonDisabled() {
- const button = await testSubjects.find('createNewCaseBtn', 20000);
- const disabledAttr = await button.getAttribute('disabled');
- expect(disabledAttr).to.be('true');
+ async expectCreateCaseButtonMissing() {
+ await testSubjects.missingOrFail('createNewCaseBtn');
},
- async expectReadOnlyCallout() {
- await testSubjects.existOrFail('case-callout-e41900b01c9ef0fa81dd6ff326083fb3');
+ async expectReadOnlyGlassesBadge() {
+ await testSubjects.existOrFail('headerBadge');
},
async expectNoReadOnlyCallout() {
@@ -44,10 +42,8 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro
expect(disabledAttr).to.be(null);
},
- async expectAddCommentButtonDisabled() {
- const button = await testSubjects.find('submit-comment', 20000);
- const disabledAttr = await button.getAttribute('disabled');
- expect(disabledAttr).to.be('true');
+ async expectAddCommentButtonMissing() {
+ await testSubjects.missingOrFail('submit-comment');
},
async expectForbidden() {
From bfbe6ab0b248b2160a4abb468483bbb15edf4e11 Mon Sep 17 00:00:00 2001
From: Kevin Logan <56395104+kevinlog@users.noreply.github.com>
Date: Tue, 22 Jun 2021 14:01:24 -0400
Subject: [PATCH 086/163] [Security Solution] show case names in isolation
success message (#102664)
---
x-pack/plugins/cases/common/api/cases/case.ts | 25 ++++++++
.../plugins/cases/common/api/cases/comment.ts | 16 -----
.../classes/client.casesclient.md | 28 ++++-----
.../interfaces/attachments_add.addargs.md | 4 +-
...attachments_client.attachmentssubclient.md | 16 ++---
.../attachments_delete.deleteallargs.md | 4 +-
.../attachments_delete.deleteargs.md | 6 +-
.../interfaces/attachments_get.findargs.md | 4 +-
...ttachments_get.getallalertsattachtocase.md | 2 +-
.../interfaces/attachments_get.getallargs.md | 6 +-
.../interfaces/attachments_get.getargs.md | 4 +-
.../attachments_update.updateargs.md | 6 +-
.../interfaces/cases_client.casessubclient.md | 30 +++++-----
.../cases_get.caseidsbyalertidparams.md | 40 -------------
.../cases_get.casesbyalertidparams.md | 40 +++++++++++++
.../interfaces/cases_get.getparams.md | 6 +-
.../interfaces/cases_push.pushparams.md | 4 +-
.../configure_client.configuresubclient.md | 8 +--
.../interfaces/stats_client.statssubclient.md | 2 +-
.../sub_cases_client.subcasesclient.md | 8 +--
.../user_actions_client.useractionget.md | 4 +-
...ser_actions_client.useractionssubclient.md | 2 +-
.../docs/cases_client/modules/cases_get.md | 6 +-
.../cases/server/client/cases/client.ts | 12 ++--
.../plugins/cases/server/client/cases/get.ts | 47 +++++++++++++--
x-pack/plugins/cases/server/client/mocks.ts | 2 +-
.../routes/api/cases/alerts/get_cases.ts | 4 +-
.../plugins/cases/server/routes/api/index.ts | 4 +-
.../components/host_isolation/index.tsx | 20 +++----
.../components/host_isolation/isolate.tsx | 11 +++-
.../components/host_isolation/unisolate.tsx | 11 +++-
.../detection_engine/alerts/mock.ts | 4 +-
.../detection_engine/alerts/types.ts | 2 +-
.../alerts/use_cases_from_alerts.test.tsx | 2 +-
.../alerts/use_cases_from_alerts.tsx | 4 +-
.../endpoint/routes/actions/isolation.ts | 14 +++--
.../case_api_integration/common/lib/utils.ts | 5 +-
.../common/lib/validation.ts | 27 +++++++++
.../tests/common/alerts/get_cases.ts | 58 +++++++++----------
.../tests/common/alerts/get_cases.ts | 38 ++++++------
.../tests/common/alerts/get_cases.ts | 17 +++---
41 files changed, 318 insertions(+), 235 deletions(-)
delete mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md
create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.casesbyalertidparams.md
create mode 100644 x-pack/test/case_api_integration/common/lib/validation.ts
diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts
index b3f7952a61ee7..a72eda5bb1207 100644
--- a/x-pack/plugins/cases/common/api/cases/case.ts
+++ b/x-pack/plugins/cases/common/api/cases/case.ts
@@ -14,6 +14,28 @@ import { CasesStatusResponseRt, CaseStatusRt } from './status';
import { CaseConnectorRt, ESCaseConnector } from '../connectors';
import { SubCaseResponseRt } from './sub_case';
+const BucketsAggs = rt.array(
+ rt.type({
+ key: rt.string,
+ })
+);
+
+export const GetCaseIdsByAlertIdAggsRt = rt.type({
+ references: rt.type({
+ doc_count: rt.number,
+ caseIds: rt.type({
+ buckets: BucketsAggs,
+ }),
+ }),
+});
+
+export const CasesByAlertIdRt = rt.array(
+ rt.type({
+ id: rt.string,
+ title: rt.string,
+ })
+);
+
export enum CaseType {
collection = 'collection',
individual = 'individual',
@@ -311,3 +333,6 @@ export type ESCasePatchRequest = Omit & {
export type AllTagsFindRequest = rt.TypeOf;
export type AllReportersFindRequest = AllTagsFindRequest;
+
+export type GetCaseIdsByAlertIdAggs = rt.TypeOf;
+export type CasesByAlertId = rt.TypeOf;
diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts
index 5bc8da95639c8..746c28f994239 100644
--- a/x-pack/plugins/cases/common/api/cases/comment.ts
+++ b/x-pack/plugins/cases/common/api/cases/comment.ts
@@ -10,21 +10,6 @@ import { SavedObjectFindOptionsRt } from '../saved_object';
import { UserRT } from '../user';
-const BucketsAggs = rt.array(
- rt.type({
- key: rt.string,
- })
-);
-
-export const GetCaseIdsByAlertIdAggsRt = rt.type({
- references: rt.type({
- doc_count: rt.number,
- caseIds: rt.type({
- buckets: BucketsAggs,
- }),
- }),
-});
-
/**
* this is used to differentiate between an alert attached to a top-level case and a group of alerts that should only
* be attached to a sub case. The reason we need this is because an alert group comment will have references to both a case and
@@ -152,4 +137,3 @@ export type CommentPatchRequest = rt.TypeOf;
export type CommentPatchAttributes = rt.TypeOf;
export type CommentRequestUserType = rt.TypeOf;
export type CommentRequestAlertType = rt.TypeOf;
-export type GetCaseIdsByAlertIdAggs = rt.TypeOf;
diff --git a/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md b/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md
index a20f018cffeb8..bd07a44a2bfdf 100644
--- a/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md
+++ b/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md
@@ -45,7 +45,7 @@ Client wrapper that contains accessor methods for individual entities within the
**Returns:** [*CasesClient*](client.casesclient.md)
-Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L28)
+Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L28)
## Properties
@@ -53,7 +53,7 @@ Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/0e98e
• `Private` `Readonly` **\_attachments**: [*AttachmentsSubClient*](../interfaces/attachments_client.attachmentssubclient.md)
-Defined in: [client.ts:24](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L24)
+Defined in: [client.ts:24](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L24)
___
@@ -61,7 +61,7 @@ ___
• `Private` `Readonly` **\_cases**: [*CasesSubClient*](../interfaces/cases_client.casessubclient.md)
-Defined in: [client.ts:23](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L23)
+Defined in: [client.ts:23](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L23)
___
@@ -69,7 +69,7 @@ ___
• `Private` `Readonly` **\_casesClientInternal**: *CasesClientInternal*
-Defined in: [client.ts:22](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L22)
+Defined in: [client.ts:22](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L22)
___
@@ -77,7 +77,7 @@ ___
• `Private` `Readonly` **\_configure**: [*ConfigureSubClient*](../interfaces/configure_client.configuresubclient.md)
-Defined in: [client.ts:27](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L27)
+Defined in: [client.ts:27](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L27)
___
@@ -85,7 +85,7 @@ ___
• `Private` `Readonly` **\_stats**: [*StatsSubClient*](../interfaces/stats_client.statssubclient.md)
-Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L28)
+Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L28)
___
@@ -93,7 +93,7 @@ ___
• `Private` `Readonly` **\_subCases**: [*SubCasesClient*](../interfaces/sub_cases_client.subcasesclient.md)
-Defined in: [client.ts:26](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L26)
+Defined in: [client.ts:26](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L26)
___
@@ -101,7 +101,7 @@ ___
• `Private` `Readonly` **\_userActions**: [*UserActionsSubClient*](../interfaces/user_actions_client.useractionssubclient.md)
-Defined in: [client.ts:25](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L25)
+Defined in: [client.ts:25](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L25)
## Accessors
@@ -113,7 +113,7 @@ Retrieves an interface for interacting with attachments (comments) entities.
**Returns:** [*AttachmentsSubClient*](../interfaces/attachments_client.attachmentssubclient.md)
-Defined in: [client.ts:50](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L50)
+Defined in: [client.ts:50](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L50)
___
@@ -125,7 +125,7 @@ Retrieves an interface for interacting with cases entities.
**Returns:** [*CasesSubClient*](../interfaces/cases_client.casessubclient.md)
-Defined in: [client.ts:43](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L43)
+Defined in: [client.ts:43](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L43)
___
@@ -137,7 +137,7 @@ Retrieves an interface for interacting with the configuration of external connec
**Returns:** [*ConfigureSubClient*](../interfaces/configure_client.configuresubclient.md)
-Defined in: [client.ts:76](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L76)
+Defined in: [client.ts:76](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L76)
___
@@ -149,7 +149,7 @@ Retrieves an interface for retrieving statistics related to the cases entities.
**Returns:** [*StatsSubClient*](../interfaces/stats_client.statssubclient.md)
-Defined in: [client.ts:83](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L83)
+Defined in: [client.ts:83](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L83)
___
@@ -163,7 +163,7 @@ Currently this functionality is disabled and will throw an error if this functio
**Returns:** [*SubCasesClient*](../interfaces/sub_cases_client.subcasesclient.md)
-Defined in: [client.ts:66](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L66)
+Defined in: [client.ts:66](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L66)
___
@@ -175,4 +175,4 @@ Retrieves an interface for interacting with the user actions associated with the
**Returns:** [*UserActionsSubClient*](../interfaces/user_actions_client.useractionssubclient.md)
-Defined in: [client.ts:57](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/client.ts#L57)
+Defined in: [client.ts:57](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/client.ts#L57)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md
index d5233ab6d8cb4..f8f7babd15b90 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md
@@ -21,7 +21,7 @@ The arguments needed for creating a new attachment to a case.
The case ID that this attachment will be associated with
-Defined in: [attachments/add.ts:305](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/add.ts#L305)
+Defined in: [attachments/add.ts:305](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/add.ts#L305)
___
@@ -31,4 +31,4 @@ ___
The attachment values.
-Defined in: [attachments/add.ts:309](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/add.ts#L309)
+Defined in: [attachments/add.ts:309](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/add.ts#L309)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md
index 1a9a687aa812b..57141796f6f67 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md
@@ -35,7 +35,7 @@ Adds an attachment to a case.
**Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\>
-Defined in: [attachments/client.ts:35](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L35)
+Defined in: [attachments/client.ts:35](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L35)
___
@@ -53,7 +53,7 @@ Deletes a single attachment for a specific case.
**Returns:** *Promise*
-Defined in: [attachments/client.ts:43](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L43)
+Defined in: [attachments/client.ts:43](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L43)
___
@@ -71,7 +71,7 @@ Deletes all attachments associated with a single case.
**Returns:** *Promise*
-Defined in: [attachments/client.ts:39](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L39)
+Defined in: [attachments/client.ts:39](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L39)
___
@@ -89,7 +89,7 @@ Retrieves all comments matching the search criteria.
**Returns:** *Promise*<[*ICommentsResponse*](typedoc_interfaces.icommentsresponse.md)\>
-Defined in: [attachments/client.ts:47](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L47)
+Defined in: [attachments/client.ts:47](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L47)
___
@@ -107,7 +107,7 @@ Retrieves a single attachment for a case.
**Returns:** *Promise*<{ `comment`: *string* ; `owner`: *string* ; `type`: user } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* } & { `alertId`: *string* \| *string*[] ; `index`: *string* \| *string*[] ; `owner`: *string* ; `rule`: { id: string \| null; name: string \| null; } ; `type`: alert \| generatedAlert } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* }\>
-Defined in: [attachments/client.ts:59](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L59)
+Defined in: [attachments/client.ts:59](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L59)
___
@@ -125,7 +125,7 @@ Gets all attachments for a single case.
**Returns:** *Promise*<[*IAllCommentsResponse*](typedoc_interfaces.iallcommentsresponse.md)\>
-Defined in: [attachments/client.ts:55](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L55)
+Defined in: [attachments/client.ts:55](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L55)
___
@@ -143,7 +143,7 @@ Retrieves all alerts attach to a case given a single case ID
**Returns:** *Promise*<{ `attached_at`: *string* ; `id`: *string* ; `index`: *string* }[]\>
-Defined in: [attachments/client.ts:51](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L51)
+Defined in: [attachments/client.ts:51](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L51)
___
@@ -163,4 +163,4 @@ The request must include all fields for the attachment. Even the fields that are
**Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\>
-Defined in: [attachments/client.ts:65](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/client.ts#L65)
+Defined in: [attachments/client.ts:65](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/client.ts#L65)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md
index 437758a0147f2..d134c92e282a3 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md
@@ -21,7 +21,7 @@ Parameters for deleting all comments of a case or sub case.
The case ID to delete all attachments for
-Defined in: [attachments/delete.ts:31](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/delete.ts#L31)
+Defined in: [attachments/delete.ts:31](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/delete.ts#L31)
___
@@ -31,4 +31,4 @@ ___
If specified the caseID will be ignored and this value will be used to find a sub case for deleting all the attachments
-Defined in: [attachments/delete.ts:35](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/delete.ts#L35)
+Defined in: [attachments/delete.ts:35](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/delete.ts#L35)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md
index 1afa5679161d9..a1c177bad8a09 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md
@@ -22,7 +22,7 @@ Parameters for deleting a single attachment of a case or sub case.
The attachment ID to delete
-Defined in: [attachments/delete.ts:49](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/delete.ts#L49)
+Defined in: [attachments/delete.ts:49](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/delete.ts#L49)
___
@@ -32,7 +32,7 @@ ___
The case ID to delete an attachment from
-Defined in: [attachments/delete.ts:45](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/delete.ts#L45)
+Defined in: [attachments/delete.ts:45](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/delete.ts#L45)
___
@@ -42,4 +42,4 @@ ___
If specified the caseID will be ignored and this value will be used to find a sub case for deleting the attachment
-Defined in: [attachments/delete.ts:53](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/delete.ts#L53)
+Defined in: [attachments/delete.ts:53](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/delete.ts#L53)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md
index dc0da295b26d2..dcd4deb28b687 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md
@@ -21,7 +21,7 @@ Parameters for finding attachments of a case
The case ID for finding associated attachments
-Defined in: [attachments/get.ts:47](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L47)
+Defined in: [attachments/get.ts:47](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L47)
___
@@ -48,4 +48,4 @@ Optional parameters for filtering the returned attachments
| `sortOrder` | *undefined* \| ``"desc"`` \| ``"asc"`` |
| `subCaseId` | *undefined* \| *string* |
-Defined in: [attachments/get.ts:51](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L51)
+Defined in: [attachments/get.ts:51](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L51)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallalertsattachtocase.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallalertsattachtocase.md
index 541d1cf8f1d80..d935823054b03 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallalertsattachtocase.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallalertsattachtocase.md
@@ -18,4 +18,4 @@
The ID of the case to retrieve the alerts from
-Defined in: [attachments/get.ts:87](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L87)
+Defined in: [attachments/get.ts:87](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L87)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md
index ae67f85e96fc0..9577e89b46074 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md
@@ -22,7 +22,7 @@ Parameters for retrieving all attachments of a case
The case ID to retrieve all attachments for
-Defined in: [attachments/get.ts:61](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L61)
+Defined in: [attachments/get.ts:61](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L61)
___
@@ -32,7 +32,7 @@ ___
Optionally include the attachments associated with a sub case
-Defined in: [attachments/get.ts:65](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L65)
+Defined in: [attachments/get.ts:65](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L65)
___
@@ -42,4 +42,4 @@ ___
If included the case ID will be ignored and the attachments will be retrieved from the specified ID of the sub case
-Defined in: [attachments/get.ts:69](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L69)
+Defined in: [attachments/get.ts:69](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L69)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md
index 2fc569985f980..5530ad8bd936e 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md
@@ -19,7 +19,7 @@
The ID of the attachment to retrieve
-Defined in: [attachments/get.ts:80](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L80)
+Defined in: [attachments/get.ts:80](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L80)
___
@@ -29,4 +29,4 @@ ___
The ID of the case to retrieve an attachment from
-Defined in: [attachments/get.ts:76](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/get.ts#L76)
+Defined in: [attachments/get.ts:76](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/get.ts#L76)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md
index 4b2dd7b404e7a..ce586a6bfdfbd 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md
@@ -22,7 +22,7 @@ Parameters for updating a single attachment
The ID of the case that is associated with this attachment
-Defined in: [attachments/update.ts:32](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/update.ts#L32)
+Defined in: [attachments/update.ts:32](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/update.ts#L32)
___
@@ -32,7 +32,7 @@ ___
The ID of a sub case, if specified a sub case will be searched for to perform the attachment update instead of on a case
-Defined in: [attachments/update.ts:40](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/update.ts#L40)
+Defined in: [attachments/update.ts:40](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/update.ts#L40)
___
@@ -42,4 +42,4 @@ ___
The full attachment request with the fields updated with appropriate values
-Defined in: [attachments/update.ts:36](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/attachments/update.ts#L36)
+Defined in: [attachments/update.ts:36](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/attachments/update.ts#L36)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md
index d86308720cb95..52cf2fbaf1ef1 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md
@@ -14,7 +14,7 @@ API for interacting with the cases entities.
- [delete](cases_client.casessubclient.md#delete)
- [find](cases_client.casessubclient.md#find)
- [get](cases_client.casessubclient.md#get)
-- [getCaseIDsByAlertID](cases_client.casessubclient.md#getcaseidsbyalertid)
+- [getCasesByAlertID](cases_client.casessubclient.md#getcasesbyalertid)
- [getReporters](cases_client.casessubclient.md#getreporters)
- [getTags](cases_client.casessubclient.md#gettags)
- [push](cases_client.casessubclient.md#push)
@@ -36,7 +36,7 @@ Creates a case.
**Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\>
-Defined in: [cases/client.ts:48](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L48)
+Defined in: [cases/client.ts:49](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L49)
___
@@ -56,7 +56,7 @@ Delete a case and all its comments.
**Returns:** *Promise*
-Defined in: [cases/client.ts:72](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L72)
+Defined in: [cases/client.ts:73](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L73)
___
@@ -76,7 +76,7 @@ If the `owner` field is left empty then all the cases that the user has access t
**Returns:** *Promise*<[*ICasesFindResponse*](typedoc_interfaces.icasesfindresponse.md)\>
-Defined in: [cases/client.ts:54](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L54)
+Defined in: [cases/client.ts:55](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L55)
___
@@ -94,25 +94,25 @@ Retrieves a single case with the specified ID.
**Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\>
-Defined in: [cases/client.ts:58](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L58)
+Defined in: [cases/client.ts:59](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L59)
___
-### getCaseIDsByAlertID
+### getCasesByAlertID
-▸ **getCaseIDsByAlertID**(`params`: [*CaseIDsByAlertIDParams*](cases_get.caseidsbyalertidparams.md)): *Promise*
+▸ **getCasesByAlertID**(`params`: [*CasesByAlertIDParams*](cases_get.casesbyalertidparams.md)): *Promise*<{ `id`: *string* ; `title`: *string* }[]\>
-Retrieves the case IDs given a single alert ID
+Retrieves the cases ID and title that have the requested alert attached to them
#### Parameters
| Name | Type |
| :------ | :------ |
-| `params` | [*CaseIDsByAlertIDParams*](cases_get.caseidsbyalertidparams.md) |
+| `params` | [*CasesByAlertIDParams*](cases_get.casesbyalertidparams.md) |
-**Returns:** *Promise*
+**Returns:** *Promise*<{ `id`: *string* ; `title`: *string* }[]\>
-Defined in: [cases/client.ts:84](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L84)
+Defined in: [cases/client.ts:85](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L85)
___
@@ -131,7 +131,7 @@ Retrieves all the reporters across all accessible cases.
**Returns:** *Promise*<{ `email`: *undefined* \| ``null`` \| *string* ; `full_name`: *undefined* \| ``null`` \| *string* ; `username`: *undefined* \| ``null`` \| *string* }[]\>
-Defined in: [cases/client.ts:80](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L80)
+Defined in: [cases/client.ts:81](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L81)
___
@@ -150,7 +150,7 @@ Retrieves all the tags across all cases the user making the request has access t
**Returns:** *Promise*
-Defined in: [cases/client.ts:76](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L76)
+Defined in: [cases/client.ts:77](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L77)
___
@@ -168,7 +168,7 @@ Pushes a specific case to an external system.
**Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\>
-Defined in: [cases/client.ts:62](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L62)
+Defined in: [cases/client.ts:63](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L63)
___
@@ -186,4 +186,4 @@ Update the specified cases with the passed in values.
**Returns:** *Promise*<[*ICasesResponse*](typedoc_interfaces.icasesresponse.md)\>
-Defined in: [cases/client.ts:66](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/client.ts#L66)
+Defined in: [cases/client.ts:67](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/client.ts#L67)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md
deleted file mode 100644
index 274b7a8f2d431..0000000000000
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md
+++ /dev/null
@@ -1,40 +0,0 @@
-[Cases Client API Interface](../cases_client_api.md) / [cases/get](../modules/cases_get.md) / CaseIDsByAlertIDParams
-
-# Interface: CaseIDsByAlertIDParams
-
-[cases/get](../modules/cases_get.md).CaseIDsByAlertIDParams
-
-Parameters for finding cases IDs using an alert ID
-
-## Table of contents
-
-### Properties
-
-- [alertID](cases_get.caseidsbyalertidparams.md#alertid)
-- [options](cases_get.caseidsbyalertidparams.md#options)
-
-## Properties
-
-### alertID
-
-• **alertID**: *string*
-
-The alert ID to search for
-
-Defined in: [cases/get.ts:42](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L42)
-
-___
-
-### options
-
-• **options**: *object*
-
-The filtering options when searching for associated cases.
-
-#### Type declaration
-
-| Name | Type |
-| :------ | :------ |
-| `owner` | *undefined* \| *string* \| *string*[] |
-
-Defined in: [cases/get.ts:46](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L46)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.casesbyalertidparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.casesbyalertidparams.md
new file mode 100644
index 0000000000000..4992ed035721b
--- /dev/null
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.casesbyalertidparams.md
@@ -0,0 +1,40 @@
+[Cases Client API Interface](../cases_client_api.md) / [cases/get](../modules/cases_get.md) / CasesByAlertIDParams
+
+# Interface: CasesByAlertIDParams
+
+[cases/get](../modules/cases_get.md).CasesByAlertIDParams
+
+Parameters for finding cases IDs using an alert ID
+
+## Table of contents
+
+### Properties
+
+- [alertID](cases_get.casesbyalertidparams.md#alertid)
+- [options](cases_get.casesbyalertidparams.md#options)
+
+## Properties
+
+### alertID
+
+• **alertID**: *string*
+
+The alert ID to search for
+
+Defined in: [cases/get.ts:44](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L44)
+
+___
+
+### options
+
+• **options**: *object*
+
+The filtering options when searching for associated cases.
+
+#### Type declaration
+
+| Name | Type |
+| :------ | :------ |
+| `owner` | *undefined* \| *string* \| *string*[] |
+
+Defined in: [cases/get.ts:48](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L48)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md
index a528b7ce6256d..a4dfc7301e543 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md
@@ -22,7 +22,7 @@ The parameters for retrieving a case
Case ID
-Defined in: [cases/get.ts:110](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L110)
+Defined in: [cases/get.ts:145](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L145)
___
@@ -32,7 +32,7 @@ ___
Whether to include the attachments for a case in the response
-Defined in: [cases/get.ts:114](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L114)
+Defined in: [cases/get.ts:149](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L149)
___
@@ -42,4 +42,4 @@ ___
Whether to include the attachments for all children of a case in the response
-Defined in: [cases/get.ts:118](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L118)
+Defined in: [cases/get.ts:153](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L153)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md
index 979e30cb31d3f..0ed510700af8a 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md
@@ -21,7 +21,7 @@ Parameters for pushing a case to an external system
The ID of a case
-Defined in: [cases/push.ts:53](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/push.ts#L53)
+Defined in: [cases/push.ts:53](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/push.ts#L53)
___
@@ -31,4 +31,4 @@ ___
The ID of an external system to push to
-Defined in: [cases/push.ts:57](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/push.ts#L57)
+Defined in: [cases/push.ts:57](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/push.ts#L57)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md
index cf69b101ce2bc..98a6c3a2fcbbf 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md
@@ -31,7 +31,7 @@ Creates a configuration if one does not already exist. If one exists it is delet
**Returns:** *Promise*<[*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\>
-Defined in: [configure/client.ts:98](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/configure/client.ts#L98)
+Defined in: [configure/client.ts:98](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/configure/client.ts#L98)
___
@@ -50,7 +50,7 @@ Retrieves the external connector configuration for a particular case owner.
**Returns:** *Promise*<{} \| [*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\>
-Defined in: [configure/client.ts:80](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/configure/client.ts#L80)
+Defined in: [configure/client.ts:80](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/configure/client.ts#L80)
___
@@ -62,7 +62,7 @@ Retrieves the valid external connectors supported by the cases plugin.
**Returns:** *Promise*
-Defined in: [configure/client.ts:84](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/configure/client.ts#L84)
+Defined in: [configure/client.ts:84](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/configure/client.ts#L84)
___
@@ -81,4 +81,4 @@ Updates a particular configuration with new values.
**Returns:** *Promise*<[*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\>
-Defined in: [configure/client.ts:91](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/configure/client.ts#L91)
+Defined in: [configure/client.ts:91](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/configure/client.ts#L91)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md
index 761b34b5205ec..cc0f30055597d 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md
@@ -29,4 +29,4 @@ Retrieves the total number of open, closed, and in-progress cases.
**Returns:** *Promise*<{ `count_closed_cases`: *number* ; `count_in_progress_cases`: *number* ; `count_open_cases`: *number* }\>
-Defined in: [stats/client.ts:34](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/stats/client.ts#L34)
+Defined in: [stats/client.ts:34](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/stats/client.ts#L34)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md
index c83c68620e8ac..5c0369709c0f0 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md
@@ -31,7 +31,7 @@ Deletes the specified entities and their attachments.
**Returns:** *Promise*
-Defined in: [sub_cases/client.ts:68](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/sub_cases/client.ts#L68)
+Defined in: [sub_cases/client.ts:68](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/sub_cases/client.ts#L68)
___
@@ -49,7 +49,7 @@ Retrieves the sub cases matching the search criteria.
**Returns:** *Promise*<[*ISubCasesFindResponse*](typedoc_interfaces.isubcasesfindresponse.md)\>
-Defined in: [sub_cases/client.ts:72](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/sub_cases/client.ts#L72)
+Defined in: [sub_cases/client.ts:72](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/sub_cases/client.ts#L72)
___
@@ -67,7 +67,7 @@ Retrieves a single sub case.
**Returns:** *Promise*<[*ISubCaseResponse*](typedoc_interfaces.isubcaseresponse.md)\>
-Defined in: [sub_cases/client.ts:76](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/sub_cases/client.ts#L76)
+Defined in: [sub_cases/client.ts:76](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/sub_cases/client.ts#L76)
___
@@ -86,4 +86,4 @@ Updates the specified sub cases to the new values included in the request.
**Returns:** *Promise*<[*ISubCasesResponse*](typedoc_interfaces.isubcasesresponse.md)\>
-Defined in: [sub_cases/client.ts:80](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/sub_cases/client.ts#L80)
+Defined in: [sub_cases/client.ts:80](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/sub_cases/client.ts#L80)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md
index f992a4116c800..5f0cc89239fd8 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md
@@ -21,7 +21,7 @@ Parameters for retrieving user actions for a particular case
The ID of the case
-Defined in: [user_actions/client.ts:19](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/user_actions/client.ts#L19)
+Defined in: [user_actions/client.ts:19](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/user_actions/client.ts#L19)
___
@@ -31,4 +31,4 @@ ___
If specified then a sub case will be used for finding all the user actions
-Defined in: [user_actions/client.ts:23](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/user_actions/client.ts#L23)
+Defined in: [user_actions/client.ts:23](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/user_actions/client.ts#L23)
diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md
index e838a72159bef..df2641adf5a8c 100644
--- a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md
+++ b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md
@@ -28,4 +28,4 @@ Retrieves all user actions for a particular case.
**Returns:** *Promise*<[*ICaseUserActionsResponse*](typedoc_interfaces.icaseuseractionsresponse.md)\>
-Defined in: [user_actions/client.ts:33](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/user_actions/client.ts#L33)
+Defined in: [user_actions/client.ts:33](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/user_actions/client.ts#L33)
diff --git a/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md b/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md
index acfa0b918aa9a..d4ca13501294a 100644
--- a/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md
+++ b/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md
@@ -6,7 +6,7 @@
### Interfaces
-- [CaseIDsByAlertIDParams](../interfaces/cases_get.caseidsbyalertidparams.md)
+- [CasesByAlertIDParams](../interfaces/cases_get.casesbyalertidparams.md)
- [GetParams](../interfaces/cases_get.getparams.md)
### Functions
@@ -31,7 +31,7 @@ Retrieves the reporters from all the cases.
**Returns:** *Promise*
-Defined in: [cases/get.ts:255](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L255)
+Defined in: [cases/get.ts:290](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L290)
___
@@ -50,4 +50,4 @@ Retrieves the tags from all the cases.
**Returns:** *Promise*
-Defined in: [cases/get.ts:205](https://github.com/jonathan-buttner/kibana/blob/0e98e105663/x-pack/plugins/cases/server/client/cases/get.ts#L205)
+Defined in: [cases/get.ts:240](https://github.com/jonathan-buttner/kibana/blob/b65ed845242/x-pack/plugins/cases/server/client/cases/get.ts#L240)
diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts
index 8a17ff9bd0ec1..0932308c2e269 100644
--- a/x-pack/plugins/cases/server/client/cases/client.ts
+++ b/x-pack/plugins/cases/server/client/cases/client.ts
@@ -12,6 +12,7 @@ import {
User,
AllTagsFindRequest,
AllReportersFindRequest,
+ CasesByAlertId,
} from '../../../common';
import { CasesClient } from '../client';
import { CasesClientInternal } from '../client_internal';
@@ -28,9 +29,9 @@ import { create } from './create';
import { deleteCases } from './delete';
import { find } from './find';
import {
- CaseIDsByAlertIDParams,
+ CasesByAlertIDParams,
get,
- getCaseIDsByAlertID,
+ getCasesByAlertID,
GetParams,
getReporters,
getTags,
@@ -79,9 +80,9 @@ export interface CasesSubClient {
*/
getReporters(params: AllReportersFindRequest): Promise;
/**
- * Retrieves the case IDs given a single alert ID
+ * Retrieves the cases ID and title that have the requested alert attached to them
*/
- getCaseIDsByAlertID(params: CaseIDsByAlertIDParams): Promise;
+ getCasesByAlertID(params: CasesByAlertIDParams): Promise;
}
/**
@@ -103,8 +104,7 @@ export const createCasesSubClient = (
delete: (ids: string[]) => deleteCases(ids, clientArgs),
getTags: (params: AllTagsFindRequest) => getTags(params, clientArgs),
getReporters: (params: AllReportersFindRequest) => getReporters(params, clientArgs),
- getCaseIDsByAlertID: (params: CaseIDsByAlertIDParams) =>
- getCaseIDsByAlertID(params, clientArgs),
+ getCasesByAlertID: (params: CasesByAlertIDParams) => getCasesByAlertID(params, clientArgs),
};
return Object.freeze(casesSubClient);
diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts
index f908a8f091ef3..3df1891391c75 100644
--- a/x-pack/plugins/cases/server/client/cases/get.ts
+++ b/x-pack/plugins/cases/server/client/cases/get.ts
@@ -25,6 +25,8 @@ import {
CasesByAlertIDRequest,
CasesByAlertIDRequestRt,
ENABLE_CASE_CONNECTOR,
+ CasesByAlertId,
+ CasesByAlertIdRt,
} from '../../../common';
import { countAlertsForID, createCaseError, flattenCaseSavedObject } from '../../common';
import { CasesClientArgs } from '..';
@@ -35,7 +37,7 @@ import { CasesService } from '../../services';
/**
* Parameters for finding cases IDs using an alert ID
*/
-export interface CaseIDsByAlertIDParams {
+export interface CasesByAlertIDParams {
/**
* The alert ID to search for
*/
@@ -47,15 +49,15 @@ export interface CaseIDsByAlertIDParams {
}
/**
- * Case Client wrapper function for retrieving the case IDs that have a particular alert ID
+ * Case Client wrapper function for retrieving the case IDs and titles that have a particular alert ID
* attached to them. This handles RBAC before calling the saved object API.
*
* @ignore
*/
-export const getCaseIDsByAlertID = async (
- { alertID, options }: CaseIDsByAlertIDParams,
+export const getCasesByAlertID = async (
+ { alertID, options }: CasesByAlertIDParams,
clientArgs: CasesClientArgs
-): Promise => {
+): Promise => {
const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs;
try {
@@ -75,12 +77,15 @@ export const getCaseIDsByAlertID = async (
Operations.getCaseIDsByAlertID.savedObjectType
);
+ // This will likely only return one comment saved object, the response aggregation will contain
+ // the keys we need to retrieve the cases
const commentsWithAlert = await caseService.getCaseIdsByAlertId({
unsecuredSavedObjectsClient,
alertId: alertID,
filter,
});
+ // make sure the comments returned have the right owner
ensureSavedObjectsAreAuthorized(
commentsWithAlert.saved_objects.map((comment) => ({
owner: comment.attributes.owner,
@@ -88,7 +93,37 @@ export const getCaseIDsByAlertID = async (
}))
);
- return CasesService.getCaseIDsFromAlertAggs(commentsWithAlert);
+ const caseIds = CasesService.getCaseIDsFromAlertAggs(commentsWithAlert);
+
+ // if we didn't find any case IDs then let's return early because there's nothing to request
+ if (caseIds.length <= 0) {
+ return [];
+ }
+
+ const casesInfo = await caseService.getCases({
+ unsecuredSavedObjectsClient,
+ caseIds,
+ });
+
+ // if there was an error retrieving one of the cases (maybe it was deleted, but the alert comment still existed)
+ // just ignore it
+ const validCasesInfo = casesInfo.saved_objects.filter(
+ (caseInfo) => caseInfo.error === undefined
+ );
+
+ ensureSavedObjectsAreAuthorized(
+ validCasesInfo.map((caseInfo) => ({
+ owner: caseInfo.attributes.owner,
+ id: caseInfo.id,
+ }))
+ );
+
+ return CasesByAlertIdRt.encode(
+ validCasesInfo.map((caseInfo) => ({
+ id: caseInfo.id,
+ title: caseInfo.attributes.title,
+ }))
+ );
} catch (error) {
throw createCaseError({
message: `Failed to get case IDs using alert ID: ${alertID} options: ${JSON.stringify(
diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts
index f6a36369c0b03..f7c27166ee910 100644
--- a/x-pack/plugins/cases/server/client/mocks.ts
+++ b/x-pack/plugins/cases/server/client/mocks.ts
@@ -28,7 +28,7 @@ const createCasesSubClientMock = (): CasesSubClientMock => {
delete: jest.fn(),
getTags: jest.fn(),
getReporters: jest.fn(),
- getCaseIDsByAlertID: jest.fn(),
+ getCasesByAlertID: jest.fn(),
};
};
diff --git a/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts
index f4b53a921ef88..3471c1dec6208 100644
--- a/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts
+++ b/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts
@@ -12,7 +12,7 @@ import { RouteDeps } from '../../types';
import { escapeHatch, wrapError } from '../../utils';
import { CASE_ALERTS_URL, CasesByAlertIDRequest } from '../../../../../common';
-export function initGetCaseIdsByAlertIdApi({ router, logger }: RouteDeps) {
+export function initGetCasesByAlertIdApi({ router, logger }: RouteDeps) {
router.get(
{
path: CASE_ALERTS_URL,
@@ -33,7 +33,7 @@ export function initGetCaseIdsByAlertIdApi({ router, logger }: RouteDeps) {
const options = request.query as CasesByAlertIDRequest;
return response.ok({
- body: await casesClient.cases.getCaseIDsByAlertID({ alertID, options }),
+ body: await casesClient.cases.getCasesByAlertID({ alertID, options }),
});
} catch (error) {
logger.error(
diff --git a/x-pack/plugins/cases/server/routes/api/index.ts b/x-pack/plugins/cases/server/routes/api/index.ts
index 011464a73396f..266ea9ddb0f18 100644
--- a/x-pack/plugins/cases/server/routes/api/index.ts
+++ b/x-pack/plugins/cases/server/routes/api/index.ts
@@ -38,7 +38,7 @@ import { initPatchSubCasesApi } from './sub_case/patch_sub_cases';
import { initFindSubCasesApi } from './sub_case/find_sub_cases';
import { initDeleteSubCasesApi } from './sub_case/delete_sub_cases';
import { ENABLE_CASE_CONNECTOR } from '../../../common';
-import { initGetCaseIdsByAlertIdApi } from './cases/alerts/get_cases';
+import { initGetCasesByAlertIdApi } from './cases/alerts/get_cases';
import { initGetAllAlertsAttachToCaseApi } from './comments/get_alerts';
/**
@@ -89,6 +89,6 @@ export function initCaseApi(deps: RouteDeps) {
// Tags
initGetTagsApi(deps);
// Alerts
- initGetCaseIdsByAlertIdApi(deps);
+ initGetCasesByAlertIdApi(deps);
initGetAllAlertsAttachToCaseApi(deps);
}
diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx
index 42d53f97d478b..ef311a7ca43b1 100644
--- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx
@@ -41,27 +41,27 @@ export const HostIsolationPanel = React.memo(
return findAlertId ? findAlertId[0] : '';
}, [details]);
- const { caseIds } = useCasesFromAlerts({ alertId });
+ const { casesInfo } = useCasesFromAlerts({ alertId });
// Cases related components to be used in both isolate and unisolate actions from the alert details flyout entry point
- const caseCount: number = useMemo(() => caseIds.length, [caseIds]);
+ const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]);
const casesList = useMemo(
() =>
- caseIds.map((id, index) => {
+ casesInfo.map((caseInfo, index) => {
return (
-
-
+
+
);
}),
- [caseIds]
+ [casesInfo]
);
const associatedCases = useMemo(() => {
@@ -90,7 +90,7 @@ export const HostIsolationPanel = React.memo(
endpointId={endpointId}
hostName={hostName}
cases={associatedCases}
- caseIds={caseIds}
+ casesInfo={casesInfo}
cancelCallback={cancelCallback}
/>
) : (
@@ -98,7 +98,7 @@ export const HostIsolationPanel = React.memo(
endpointId={endpointId}
hostName={hostName}
cases={associatedCases}
- caseIds={caseIds}
+ casesInfo={casesInfo}
cancelCallback={cancelCallback}
/>
);
diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx
index afc2951e26e1f..b209c2f9c6e24 100644
--- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx
@@ -15,24 +15,29 @@ import {
EndpointIsolateForm,
EndpointIsolateSuccess,
} from '../../../common/components/endpoint/host_isolation';
+import { CasesFromAlertsResponse } from '../../containers/detection_engine/alerts/types';
export const IsolateHost = React.memo(
({
endpointId,
hostName,
cases,
- caseIds,
+ casesInfo,
cancelCallback,
}: {
endpointId: string;
hostName: string;
cases: ReactNode;
- caseIds: string[];
+ casesInfo: CasesFromAlertsResponse;
cancelCallback: () => void;
}) => {
const [comment, setComment] = useState('');
const [isIsolated, setIsIsolated] = useState(false);
+ const caseIds: string[] = casesInfo.map((caseInfo): string => {
+ return caseInfo.id;
+ });
+
const { loading, isolateHost } = useHostIsolation({ endpointId, comment, caseIds });
const confirmHostIsolation = useCallback(async () => {
@@ -47,7 +52,7 @@ export const IsolateHost = React.memo(
[]
);
- const caseCount: number = useMemo(() => caseIds.length, [caseIds]);
+ const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]);
const hostIsolatedSuccess = useMemo(() => {
return (
diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx
index 71f7cadda2f68..ad8e8eaddb39e 100644
--- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx
@@ -15,24 +15,29 @@ import {
EndpointUnisolateForm,
} from '../../../common/components/endpoint/host_isolation';
import { useHostUnisolation } from '../../containers/detection_engine/alerts/use_host_unisolation';
+import { CasesFromAlertsResponse } from '../../containers/detection_engine/alerts/types';
export const UnisolateHost = React.memo(
({
endpointId,
hostName,
cases,
- caseIds,
+ casesInfo,
cancelCallback,
}: {
endpointId: string;
hostName: string;
cases: ReactNode;
- caseIds: string[];
+ casesInfo: CasesFromAlertsResponse;
cancelCallback: () => void;
}) => {
const [comment, setComment] = useState('');
const [isUnIsolated, setIsUnIsolated] = useState(false);
+ const caseIds: string[] = casesInfo.map((caseInfo): string => {
+ return caseInfo.id;
+ });
+
const { loading, unIsolateHost } = useHostUnisolation({ endpointId, comment, caseIds });
const confirmHostUnIsolation = useCallback(async () => {
@@ -47,7 +52,7 @@ export const UnisolateHost = React.memo(
[]
);
- const caseCount: number = useMemo(() => caseIds.length, [caseIds]);
+ const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]);
const hostUnisolatedSuccess = useMemo(() => {
return (
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
index 69358958a395c..e4bddfba8278b 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
@@ -1046,6 +1046,6 @@ export const mockHostIsolation: HostIsolationResponse = {
};
export const mockCaseIdsFromAlertId: CasesFromAlertsResponse = [
- '818601a0-b26b-11eb-8759-6b318e8cf4bc',
- '8a774850-b26b-11eb-8759-6b318e8cf4bc',
+ { id: '818601a0-b26b-11eb-8759-6b318e8cf4bc', title: 'Case 1' },
+ { id: '8a774850-b26b-11eb-8759-6b318e8cf4bc', title: 'Case 2' },
];
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
index 52b477d95076b..54d4b6fdcbafd 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
@@ -48,7 +48,7 @@ export interface AlertsIndex {
index_mapping_outdated: boolean;
}
-export type CasesFromAlertsResponse = string[];
+export type CasesFromAlertsResponse = Array<{ id: string; title: string }>;
export interface Privilege {
username: string;
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx
index 0867fb001051a..00aa7c9baa9ac 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx
@@ -35,7 +35,7 @@ describe('useCasesFromAlerts hook', () => {
expect(spyOnCases).toHaveBeenCalledTimes(1);
expect(result.current).toEqual({
loading: false,
- caseIds: mockCaseIdsFromAlertId,
+ casesInfo: mockCaseIdsFromAlertId,
});
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx
index 85b80a588e88d..eeb7968d6b2f2 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx
@@ -15,7 +15,7 @@ import { CasesFromAlertsResponse } from './types';
interface CasesFromAlertsStatus {
loading: boolean;
- caseIds: CasesFromAlertsResponse;
+ casesInfo: CasesFromAlertsResponse;
}
export const useCasesFromAlerts = ({ alertId }: { alertId: string }): CasesFromAlertsStatus => {
@@ -48,5 +48,5 @@ export const useCasesFromAlerts = ({ alertId }: { alertId: string }): CasesFromA
isMounted = false;
};
}, [alertId, addError]);
- return { loading, caseIds: cases };
+ return { loading, casesInfo: cases };
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts
index c54c12981c771..50fe2ffe2cea9 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts
@@ -10,6 +10,7 @@ import { RequestHandler } from 'src/core/server';
import uuid from 'uuid';
import { TypeOf } from '@kbn/config-schema';
import { CommentType } from '../../../../../cases/common';
+import { CasesByAlertId } from '../../../../../cases/common/api/cases/case';
import { HostIsolationRequestSchema } from '../../../../common/endpoint/schema/actions';
import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE } from '../../../../common/endpoint/constants';
import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common';
@@ -103,12 +104,17 @@ export const isolationRequestHandler = function (
let caseIDs: string[] = req.body.case_ids?.slice() || [];
if (req.body.alert_ids && req.body.alert_ids.length > 0) {
const newIDs: string[][] = await Promise.all(
- req.body.alert_ids.map(async (a: string) =>
- (await endpointContext.service.getCasesClient(req)).cases.getCaseIDsByAlertID({
+ req.body.alert_ids.map(async (a: string) => {
+ const cases: CasesByAlertId = await (
+ await endpointContext.service.getCasesClient(req)
+ ).cases.getCasesByAlertID({
alertID: a,
options: { owner: APP_ID },
- })
- )
+ });
+ return cases.map((caseInfo): string => {
+ return caseInfo.id;
+ });
+ })
);
caseIDs = caseIDs.concat(...newIDs);
}
diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts
index 63be1736405fc..921589b2341dd 100644
--- a/x-pack/test/case_api_integration/common/lib/utils.ts
+++ b/x-pack/test/case_api_integration/common/lib/utils.ts
@@ -46,6 +46,7 @@ import {
CasesConfigurationsResponse,
CaseUserActionsResponse,
AlertResponse,
+ CasesByAlertId,
} from '../../../../plugins/cases/common/api';
import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock';
import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers';
@@ -1017,7 +1018,7 @@ export const findCases = async ({
return res;
};
-export const getCaseIDsByAlert = async ({
+export const getCasesByAlert = async ({
supertest,
alertID,
query = {},
@@ -1029,7 +1030,7 @@ export const getCaseIDsByAlert = async ({
query?: Record;
expectedHttpCode?: number;
auth?: { user: User; space: string | null };
-}): Promise => {
+}): Promise => {
const { body: res } = await supertest
.get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/alerts/${alertID}`)
.auth(auth.user.username, auth.user.password)
diff --git a/x-pack/test/case_api_integration/common/lib/validation.ts b/x-pack/test/case_api_integration/common/lib/validation.ts
new file mode 100644
index 0000000000000..8b1c8ca124149
--- /dev/null
+++ b/x-pack/test/case_api_integration/common/lib/validation.ts
@@ -0,0 +1,27 @@
+/*
+ * 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 expect from '@kbn/expect';
+
+import { CaseResponse, CasesByAlertId } from '../../../../plugins/cases/common';
+
+/**
+ * Ensure that the result of the alerts API request matches with the cases created for the test.
+ */
+export function validateCasesFromAlertIDResponse(
+ casesFromAPIResponse: CasesByAlertId,
+ createdCasesForTest: CaseResponse[]
+) {
+ const idToTitle = new Map(
+ createdCasesForTest.map((caseInfo) => [caseInfo.id, caseInfo.title])
+ );
+
+ for (const apiResCase of casesFromAPIResponse) {
+ // check that the title in the api response matches the title in the map from the created cases
+ expect(apiResCase.title).to.be(idToTitle.get(apiResCase.id));
+ }
+}
diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts
index e34f879e3aff8..136e52d08f46a 100644
--- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts
+++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts
@@ -13,9 +13,10 @@ import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/
import {
createCase,
createComment,
- getCaseIDsByAlert,
+ getCasesByAlert,
deleteAllCaseItems,
} from '../../../../common/lib/utils';
+import { validateCasesFromAlertIDResponse } from '../../../../common/lib/validation';
import { CaseResponse } from '../../../../../../plugins/cases/common';
import {
globalRead,
@@ -41,9 +42,9 @@ export default ({ getService }: FtrProviderContext): void => {
it('should return all cases with the same alert ID attached to them', async () => {
const [case1, case2, case3] = await Promise.all([
- createCase(supertest, getPostCaseRequest()),
- createCase(supertest, getPostCaseRequest()),
- createCase(supertest, getPostCaseRequest()),
+ createCase(supertest, getPostCaseRequest({ title: 'a' })),
+ createCase(supertest, getPostCaseRequest({ title: 'b' })),
+ createCase(supertest, getPostCaseRequest({ title: 'c' })),
]);
await Promise.all([
@@ -52,12 +53,10 @@ export default ({ getService }: FtrProviderContext): void => {
createComment({ supertest, caseId: case3.id, params: postCommentAlertReq }),
]);
- const caseIDsWithAlert = await getCaseIDsByAlert({ supertest, alertID: 'test-id' });
+ const caseIDsWithAlert = await getCasesByAlert({ supertest, alertID: 'test-id' });
expect(caseIDsWithAlert.length).to.eql(3);
- expect(caseIDsWithAlert).to.contain(case1.id);
- expect(caseIDsWithAlert).to.contain(case2.id);
- expect(caseIDsWithAlert).to.contain(case3.id);
+ validateCasesFromAlertIDResponse(caseIDsWithAlert, [case1, case2, case3]);
});
it('should return all cases with the same alert ID when more than 100 cases', async () => {
@@ -80,13 +79,11 @@ export default ({ getService }: FtrProviderContext): void => {
await Promise.all(commentPromises);
- const caseIDsWithAlert = await getCaseIDsByAlert({ supertest, alertID: 'test-id' });
+ const caseIDsWithAlert = await getCasesByAlert({ supertest, alertID: 'test-id' });
expect(caseIDsWithAlert.length).to.eql(numCases);
- for (const caseInfo of cases) {
- expect(caseIDsWithAlert).to.contain(caseInfo.id);
- }
+ validateCasesFromAlertIDResponse(caseIDsWithAlert, cases);
});
it('should return no cases when the alert ID is not found', async () => {
@@ -102,7 +99,7 @@ export default ({ getService }: FtrProviderContext): void => {
createComment({ supertest, caseId: case3.id, params: postCommentAlertReq }),
]);
- const caseIDsWithAlert = await getCaseIDsByAlert({ supertest, alertID: 'test-id100' });
+ const caseIDsWithAlert = await getCasesByAlert({ supertest, alertID: 'test-id100' });
expect(caseIDsWithAlert.length).to.eql(0);
});
@@ -120,7 +117,7 @@ export default ({ getService }: FtrProviderContext): void => {
createComment({ supertest, caseId: case3.id, params: postCommentAlertReq }),
]);
- const caseIDsWithAlert = await getCaseIDsByAlert({
+ const caseIDsWithAlert = await getCasesByAlert({
supertest,
alertID: 'test-id',
query: { owner: 'not-real' },
@@ -137,7 +134,7 @@ export default ({ getService }: FtrProviderContext): void => {
describe('rbac', () => {
const supertestWithoutAuth = getService('supertestWithoutAuth');
- it('should return the correct case IDs', async () => {
+ it('should return the correct cases info', async () => {
const secOnlyAuth = { user: secOnly, space: 'space1' };
const obsOnlyAuth = { user: obsOnly, space: 'space1' };
@@ -176,20 +173,20 @@ export default ({ getService }: FtrProviderContext): void => {
for (const scenario of [
{
user: globalRead,
- caseIDs: [case1.id, case2.id, case3.id],
+ cases: [case1, case2, case3],
},
{
user: superUser,
- caseIDs: [case1.id, case2.id, case3.id],
+ cases: [case1, case2, case3],
},
- { user: secOnlyRead, caseIDs: [case1.id, case2.id] },
- { user: obsOnlyRead, caseIDs: [case3.id] },
+ { user: secOnlyRead, cases: [case1, case2] },
+ { user: obsOnlyRead, cases: [case3] },
{
user: obsSecRead,
- caseIDs: [case1.id, case2.id, case3.id],
+ cases: [case1, case2, case3],
},
]) {
- const res = await getCaseIDsByAlert({
+ const res = await getCasesByAlert({
supertest: supertestWithoutAuth,
// cast because the official type is string | string[] but the ids will always be a single value in the tests
alertID: postCommentAlertReq.alertId as string,
@@ -198,10 +195,9 @@ export default ({ getService }: FtrProviderContext): void => {
space: 'space1',
},
});
- expect(res.length).to.eql(scenario.caseIDs.length);
- for (const caseID of scenario.caseIDs) {
- expect(res).to.contain(caseID);
- }
+ expect(res.length).to.eql(scenario.cases.length);
+
+ validateCasesFromAlertIDResponse(res, scenario.cases);
}
});
@@ -224,7 +220,7 @@ export default ({ getService }: FtrProviderContext): void => {
auth: { user: superUser, space: scenario.space },
});
- await getCaseIDsByAlert({
+ await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth: { user: scenario.user, space: scenario.space },
@@ -260,17 +256,17 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- const res = await getCaseIDsByAlert({
+ const res = await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth,
query: { owner: 'securitySolutionFixture' },
});
- expect(res).to.eql([case1.id]);
+ expect(res).to.eql([{ id: case1.id, title: case1.title }]);
});
- it('should return the correct case IDs when the owner query parameter contains unprivileged values', async () => {
+ it('should return the correct cases info when the owner query parameter contains unprivileged values', async () => {
const auth = { user: obsSec, space: 'space1' };
const [case1, case2] = await Promise.all([
createCase(supertestWithoutAuth, getPostCaseRequest(), 200, auth),
@@ -297,7 +293,7 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- const res = await getCaseIDsByAlert({
+ const res = await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth: { user: secOnly, space: 'space1' },
@@ -305,7 +301,7 @@ export default ({ getService }: FtrProviderContext): void => {
query: { owner: ['securitySolutionFixture', 'observabilityFixture'] },
});
- expect(res).to.eql([case1.id]);
+ expect(res).to.eql([{ id: case1.id, title: case1.title }]);
});
});
});
diff --git a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts
index 9575bd99112f6..f55427d13b32b 100644
--- a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts
+++ b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts
@@ -12,7 +12,7 @@ import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/
import {
createCase,
createComment,
- getCaseIDsByAlert,
+ getCasesByAlert,
deleteAllCaseItems,
} from '../../../../common/lib/utils';
import {
@@ -30,6 +30,7 @@ import {
superUserDefaultSpaceAuth,
obsSecDefaultSpaceAuth,
} from '../../../utils';
+import { validateCasesFromAlertIDResponse } from '../../../../common/lib/validation';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
@@ -43,7 +44,7 @@ export default ({ getService }: FtrProviderContext): void => {
const supertestWithoutAuth = getService('supertestWithoutAuth');
- it('should return the correct case IDs', async () => {
+ it('should return the correct cases info', async () => {
const [case1, case2, case3] = await Promise.all([
createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth),
createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth),
@@ -79,20 +80,20 @@ export default ({ getService }: FtrProviderContext): void => {
for (const scenario of [
{
user: globalRead,
- caseIDs: [case1.id, case2.id, case3.id],
+ cases: [case1, case2, case3],
},
{
user: superUser,
- caseIDs: [case1.id, case2.id, case3.id],
+ cases: [case1, case2, case3],
},
- { user: secOnlyReadSpacesAll, caseIDs: [case1.id, case2.id] },
- { user: obsOnlyReadSpacesAll, caseIDs: [case3.id] },
+ { user: secOnlyReadSpacesAll, cases: [case1, case2] },
+ { user: obsOnlyReadSpacesAll, cases: [case3] },
{
user: obsSecReadSpacesAll,
- caseIDs: [case1.id, case2.id, case3.id],
+ cases: [case1, case2, case3],
},
]) {
- const res = await getCaseIDsByAlert({
+ const cases = await getCasesByAlert({
supertest: supertestWithoutAuth,
// cast because the official type is string | string[] but the ids will always be a single value in the tests
alertID: postCommentAlertReq.alertId as string,
@@ -101,10 +102,9 @@ export default ({ getService }: FtrProviderContext): void => {
space: null,
},
});
- expect(res.length).to.eql(scenario.caseIDs.length);
- for (const caseID of scenario.caseIDs) {
- expect(res).to.contain(caseID);
- }
+
+ expect(cases.length).to.eql(scenario.cases.length);
+ validateCasesFromAlertIDResponse(cases, scenario.cases);
}
});
@@ -123,7 +123,7 @@ export default ({ getService }: FtrProviderContext): void => {
auth: superUserDefaultSpaceAuth,
});
- await getCaseIDsByAlert({
+ await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth: { user: noKibanaPrivileges, space: null },
@@ -157,7 +157,7 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- await getCaseIDsByAlert({
+ await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth: { user: obsSecSpacesAll, space: 'space1' },
@@ -192,17 +192,17 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- const res = await getCaseIDsByAlert({
+ const cases = await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth: obsSecDefaultSpaceAuth,
query: { owner: 'securitySolutionFixture' },
});
- expect(res).to.eql([case1.id]);
+ expect(cases).to.eql([{ id: case1.id, title: case1.title }]);
});
- it('should return the correct case IDs when the owner query parameter contains unprivileged values', async () => {
+ it('should return the correct cases info when the owner query parameter contains unprivileged values', async () => {
const [case1, case2] = await Promise.all([
createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth),
createCase(
@@ -228,7 +228,7 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- const res = await getCaseIDsByAlert({
+ const cases = await getCasesByAlert({
supertest: supertestWithoutAuth,
alertID: postCommentAlertReq.alertId as string,
auth: secOnlyDefaultSpaceAuth,
@@ -236,7 +236,7 @@ export default ({ getService }: FtrProviderContext): void => {
query: { owner: ['securitySolutionFixture', 'observabilityFixture'] },
});
- expect(res).to.eql([case1.id]);
+ expect(cases).to.eql([{ id: case1.id, title: case1.title }]);
});
});
};
diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts
index 9587502fb642c..739f8e5ec0892 100644
--- a/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts
+++ b/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts
@@ -12,10 +12,11 @@ import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/
import {
createCase,
createComment,
- getCaseIDsByAlert,
+ getCasesByAlert,
deleteAllCaseItems,
getAuthWithSuperUser,
} from '../../../../common/lib/utils';
+import { validateCasesFromAlertIDResponse } from '../../../../common/lib/validation';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
@@ -57,16 +58,14 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- const caseIDsWithAlert = await getCaseIDsByAlert({
+ const cases = await getCasesByAlert({
supertest,
alertID: 'test-id',
auth: authSpace1,
});
- expect(caseIDsWithAlert.length).to.eql(3);
- expect(caseIDsWithAlert).to.contain(case1.id);
- expect(caseIDsWithAlert).to.contain(case2.id);
- expect(caseIDsWithAlert).to.contain(case3.id);
+ expect(cases.length).to.eql(3);
+ validateCasesFromAlertIDResponse(cases, [case1, case2, case3]);
});
it('should return 1 case in space2 when 2 cases were created in space1 and 1 in space2', async () => {
@@ -97,14 +96,14 @@ export default ({ getService }: FtrProviderContext): void => {
}),
]);
- const caseIDsWithAlert = await getCaseIDsByAlert({
+ const casesByAlert = await getCasesByAlert({
supertest,
alertID: 'test-id',
auth: authSpace2,
});
- expect(caseIDsWithAlert.length).to.eql(1);
- expect(caseIDsWithAlert).to.eql([case3.id]);
+ expect(casesByAlert.length).to.eql(1);
+ expect(casesByAlert).to.eql([{ id: case3.id, title: case3.title }]);
});
});
};
From c33138e5cb2bec40830537e4009ddf4a75ab8bb1 Mon Sep 17 00:00:00 2001
From: Alison Goryachev
Date: Tue, 22 Jun 2021 14:13:48 -0400
Subject: [PATCH 087/163] [Rollups] Migrate to new page layout (#102268)
---
...-plugin-core-public.doclinksstart.links.md | 1 +
...kibana-plugin-core-public.doclinksstart.md | 2 +-
.../public/doc_links/doc_links_service.ts | 2 +
src/core/public/public.api.md | 1 +
.../rollup/public/crud_app/_crud_app.scss | 8 -
.../sections/job_create/job_create.js | 57 ++---
.../job_list/detail_panel/detail_panel.js | 2 +-
.../detail_panel/detail_panel.test.js | 2 +-
.../crud_app/sections/job_list/job_list.js | 214 +++++++++---------
.../sections/job_list/job_list.test.js | 26 ++-
.../sections/job_list/job_table/job_table.js | 23 +-
.../job_list/job_table/job_table.test.js | 8 +
.../crud_app/store/actions/load_jobs.js | 13 +-
.../plugins/rollup/public/shared_imports.ts | 6 +-
.../test/client_integration/job_list.test.js | 5 +-
.../client_integration/job_list_clone.test.js | 9 +
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
18 files changed, 198 insertions(+), 183 deletions(-)
diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
index d3d76079cdc2a..ae433e3db14c6 100644
--- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
+++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
@@ -116,6 +116,7 @@ readonly links: {
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
+ readonly rollupJobs: string;
readonly elasticsearch: Record;
readonly siem: {
readonly guide: string;
diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
index 34279cef198bf..b0800c7dfc65e 100644
--- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
+++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md
@@ -17,5 +17,5 @@ export interface DocLinksStart
| --- | --- | --- |
| [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string
| |
| [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string
| |
-| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
}
| |
+| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
}
| |
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 95091a761639b..8c52d09f82159 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -137,6 +137,7 @@ export class DocLinksService {
addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`,
kibana: `${KIBANA_DOCS}index.html`,
upgradeAssistant: `${KIBANA_DOCS}upgrade-assistant.html`,
+ rollupJobs: `${KIBANA_DOCS}data-rollups.html`,
elasticsearch: {
docsBase: `${ELASTICSEARCH_DOCS}`,
asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`,
@@ -532,6 +533,7 @@ export interface DocLinksStart {
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
+ readonly rollupJobs: string;
readonly elasticsearch: Record;
readonly siem: {
readonly guide: string;
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 6cc2b3f321fb7..27569935bcc65 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -595,6 +595,7 @@ export interface DocLinksStart {
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
+ readonly rollupJobs: string;
readonly elasticsearch: Record;
readonly siem: {
readonly guide: string;
diff --git a/x-pack/plugins/rollup/public/crud_app/_crud_app.scss b/x-pack/plugins/rollup/public/crud_app/_crud_app.scss
index 9e3bd491115ce..ddf69167145f1 100644
--- a/x-pack/plugins/rollup/public/crud_app/_crud_app.scss
+++ b/x-pack/plugins/rollup/public/crud_app/_crud_app.scss
@@ -4,11 +4,3 @@
.rollupJobWizardStepActions {
align-items: flex-end; /* 1 */
}
-
-/**
- * 1. Ensure panel fills width of parent when search input yields no matching rollup jobs.
- */
-.rollupJobsListPanel {
- // sass-lint:disable-block no-important
- flex-grow: 1 !important; /* 1 */
-}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
index fa3ce260424f2..6f22345dc1cec 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { cloneDeep, debounce, first, mapValues } from 'lodash';
@@ -18,11 +18,10 @@ import {
EuiCallOut,
EuiLoadingKibana,
EuiOverlayMask,
- EuiPageContent,
- EuiPageContentHeader,
+ EuiPageContentBody,
+ EuiPageHeader,
EuiSpacer,
EuiStepsHorizontal,
- EuiTitle,
} from '@elastic/eui';
import {
@@ -522,44 +521,46 @@ export class JobCreateUi extends Component {
}
saveErrorFeedback = (
-
+ <>
+
+
{errorBody}
-
+ >
);
}
return (
-
-
-
-
-
-
-
-
-
-
- {saveErrorFeedback}
-
-
+
+
+ }
+ />
-
+
+
+
+
+ {saveErrorFeedback}
+
+
+
+ {this.renderCurrentStep()}
- {this.renderCurrentStep()}
+
-
+ {this.renderNavigation()}
- {this.renderNavigation()}
-
{savingFeedback}
-
+
);
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js
index 4fe1674e8c643..5e97ff5e2980d 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js
@@ -195,7 +195,7 @@ export class DetailPanel extends Component {
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js
index 16919b8388e2e..e1f9ec2b3a315 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js
@@ -70,7 +70,7 @@ describe('
', () => {
({ component, find, exists } = initTestBed({ isLoading: true }));
const loading = find('rollupJobDetailLoading');
expect(loading.length).toBeTruthy();
- expect(loading.text()).toEqual('Loading rollup job...');
+ expect(loading.text()).toEqual('Loading rollup job…');
// Make sure the title and the tabs are visible
expect(exists('detailPanelTabSelected')).toBeTruthy();
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
index 589546a11ef38..b2448eb610774 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js
@@ -12,24 +12,19 @@ import { i18n } from '@kbn/i18n';
import {
EuiButton,
+ EuiButtonEmpty,
EuiEmptyPrompt,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
+ EuiPageHeader,
EuiPageContent,
- EuiPageContentHeader,
- EuiPageContentHeaderSection,
EuiSpacer,
- EuiText,
- EuiTextColor,
- EuiTitle,
- EuiCallOut,
} from '@elastic/eui';
import { withKibana } from '../../../../../../../src/plugins/kibana_react/public';
-import { extractQueryParams } from '../../../shared_imports';
+import { extractQueryParams, SectionLoading } from '../../../shared_imports';
import { getRouterLinkProps, listBreadcrumb } from '../../services';
+import { documentationLinks } from '../../services/documentation_links';
+
import { JobTable } from './job_table';
import { DetailPanel } from './detail_panel';
@@ -87,38 +82,26 @@ export class JobListUi extends Component {
this.props.closeDetailPanel();
}
- getHeaderSection() {
- return (
-
-
-
-
-
-
-
- );
- }
-
renderNoPermission() {
const title = i18n.translate('xpack.rollupJobs.jobList.noPermissionTitle', {
defaultMessage: 'Permission error',
});
return (
-
- {this.getHeaderSection()}
-
-
+
-
-
-
+ iconType="alert"
+ title={
{title} }
+ body={
+
+
+
+ }
+ />
+
);
}
@@ -130,101 +113,110 @@ export class JobListUi extends Component {
const title = i18n.translate('xpack.rollupJobs.jobList.loadingErrorTitle', {
defaultMessage: 'Error loading rollup jobs',
});
+
return (
-
- {this.getHeaderSection()}
-
-
- {statusCode} {errorString}
-
-
+
+ {title}}
+ body={
+
+ {statusCode} {errorString}
+
+ }
+ />
+
);
}
renderEmpty() {
return (
-
-
-
- }
- body={
-
-
+
+
+
+ }
+ body={
+
+
+
+
+
+ }
+ actions={
+
+
-
-
- }
- actions={
-
-
-
- }
- />
+
+ }
+ />
+
);
}
renderLoading() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
}
renderList() {
- const { isLoading } = this.props;
-
return (
-
-
- {this.getHeaderSection()}
-
-
-
+ <>
+
+
+
+ }
+ rightSideItems={[
+
-
-
-
+ ,
+ ]}
+ />
- {isLoading ? this.renderLoading() : }
+
+
+
-
+ >
);
}
@@ -241,15 +233,13 @@ export class JobListUi extends Component {
}
} else if (!isLoading && !hasJobs) {
content = this.renderEmpty();
+ } else if (isLoading) {
+ content = this.renderLoading();
} else {
content = this.renderList();
}
- return (
-
- {content}
-
- );
+ return content;
}
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js
index 3283f4f521fc0..b2c738a033b3c 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js
@@ -22,6 +22,15 @@ jest.mock('../../services', () => {
};
});
+jest.mock('../../services/documentation_links', () => {
+ const coreMocks = jest.requireActual('../../../../../../../src/core/public/mocks');
+
+ return {
+ init: jest.fn(),
+ documentationLinks: coreMocks.docLinksServiceMock.createStartContract().links,
+ };
+});
+
const defaultProps = {
history: { location: {} },
loadJobs: () => {},
@@ -52,14 +61,14 @@ describe(' ', () => {
it('should display a loading message when loading the jobs', () => {
const { component, exists } = initTestBed({ isLoading: true });
- expect(exists('jobListLoading')).toBeTruthy();
+ expect(exists('sectionLoading')).toBeTruthy();
expect(component.find('JobTable').length).toBeFalsy();
});
it('should display the when there are jobs', () => {
const { component, exists } = initTestBed({ hasJobs: true });
- expect(exists('jobListLoading')).toBeFalsy();
+ expect(exists('sectionLoading')).toBeFalsy();
expect(component.find('JobTable').length).toBeTruthy();
});
@@ -71,21 +80,20 @@ describe(' ', () => {
},
});
- it('should display a callout with the status and the message', () => {
+ it('should display an error with the status and the message', () => {
expect(exists('jobListError')).toBeTruthy();
expect(find('jobListError').find('EuiText').text()).toEqual('400 Houston we got a problem.');
});
});
describe('when the user does not have the permission to access it', () => {
- const { exists } = initTestBed({ jobLoadError: { status: 403 } });
+ const { exists, find } = initTestBed({ jobLoadError: { status: 403 } });
- it('should render a callout message', () => {
+ it('should render an error message', () => {
expect(exists('jobListNoPermission')).toBeTruthy();
- });
-
- it('should display the page header', () => {
- expect(exists('jobListPageHeader')).toBeTruthy();
+ expect(find('jobListNoPermission').find('EuiText').text()).toEqual(
+ 'You do not have permission to view or add rollup jobs.'
+ );
});
});
});
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js
index fe3d2cbd4cbe0..83135cf219f35 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -28,10 +28,11 @@ import {
EuiTableRowCellCheckbox,
EuiText,
EuiToolTip,
+ EuiButton,
} from '@elastic/eui';
import { UIM_SHOW_DETAILS_CLICK } from '../../../../../common';
-import { METRIC_TYPE } from '../../../services';
+import { METRIC_TYPE, getRouterLinkProps } from '../../../services';
import { trackUiMetric } from '../../../../kibana_services';
import { JobActionMenu, JobStatus } from '../../components';
@@ -346,9 +347,9 @@ export class JobTable extends Component {
const atLeastOneItemSelected = Object.keys(idToSelectedJobMap).length > 0;
return (
-
-
- {atLeastOneItemSelected ? (
+
+
+ {atLeastOneItemSelected && (
- ) : null}
+ )}
+
+
+
+
+
@@ -409,7 +418,7 @@ export class JobTable extends Component {
{jobs.length > 0 ? this.renderPager() : null}
-
+
);
}
}
diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js
index 3fa879923c40a..d52f3fa35a544 100644
--- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js
+++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js
@@ -20,6 +20,14 @@ jest.mock('../../../../kibana_services', () => {
};
});
+jest.mock('../../../services', () => {
+ const services = jest.requireActual('../../../services');
+ return {
+ ...services,
+ getRouterLinkProps: (link) => ({ href: link }),
+ };
+});
+
const defaultProps = {
jobs: [],
pager: new Pager(20, 10, 1),
diff --git a/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js b/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js
index 0dc3a02d3c077..c63d01f3c200d 100644
--- a/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js
+++ b/x-pack/plugins/rollup/public/crud_app/store/actions/load_jobs.js
@@ -5,9 +5,7 @@
* 2.0.
*/
-import { i18n } from '@kbn/i18n';
-
-import { loadJobs as sendLoadJobsRequest, deserializeJobs, showApiError } from '../../services';
+import { loadJobs as sendLoadJobsRequest, deserializeJobs } from '../../services';
import { LOAD_JOBS_START, LOAD_JOBS_SUCCESS, LOAD_JOBS_FAILURE } from '../action_types';
export const loadJobs = () => async (dispatch) => {
@@ -19,17 +17,10 @@ export const loadJobs = () => async (dispatch) => {
try {
jobs = await sendLoadJobsRequest();
} catch (error) {
- dispatch({
+ return dispatch({
type: LOAD_JOBS_FAILURE,
payload: { error },
});
-
- return showApiError(
- error,
- i18n.translate('xpack.rollupJobs.loadAction.errorTitle', {
- defaultMessage: 'Error loading rollup jobs',
- })
- );
}
dispatch({
diff --git a/x-pack/plugins/rollup/public/shared_imports.ts b/x-pack/plugins/rollup/public/shared_imports.ts
index fd28175318666..c8d7f1d9f13f3 100644
--- a/x-pack/plugins/rollup/public/shared_imports.ts
+++ b/x-pack/plugins/rollup/public/shared_imports.ts
@@ -5,4 +5,8 @@
* 2.0.
*/
-export { extractQueryParams, indices } from '../../../../src/plugins/es_ui_shared/public';
+export {
+ extractQueryParams,
+ indices,
+ SectionLoading,
+} from '../../../../src/plugins/es_ui_shared/public';
diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js
index fa1a786bc8a71..46ddfbcfc2de5 100644
--- a/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js
+++ b/x-pack/plugins/rollup/public/test/client_integration/job_list.test.js
@@ -5,10 +5,10 @@
* 2.0.
*/
-import { getRouter, setHttp } from '../../crud_app/services';
+import { getRouter, setHttp, init as initDocumentation } from '../../crud_app/services';
import { mockHttpRequest, pageHelpers, nextTick } from './helpers';
import { JOBS } from './helpers/constants';
-import { coreMock } from '../../../../../../src/core/public/mocks';
+import { coreMock, docLinksServiceMock } from '../../../../../../src/core/public/mocks';
jest.mock('../../crud_app/services', () => {
const services = jest.requireActual('../../crud_app/services');
@@ -38,6 +38,7 @@ describe(' ', () => {
beforeAll(() => {
startMock = coreMock.createStart();
setHttp(startMock.http);
+ initDocumentation(docLinksServiceMock.createStartContract());
});
beforeEach(async () => {
diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js
index cfb63893ee423..3987e18538e57 100644
--- a/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js
+++ b/x-pack/plugins/rollup/public/test/client_integration/job_list_clone.test.js
@@ -24,6 +24,15 @@ jest.mock('../../kibana_services', () => {
};
});
+jest.mock('../../crud_app/services/documentation_links', () => {
+ const coreMocks = jest.requireActual('../../../../../../src/core/public/mocks');
+
+ return {
+ init: jest.fn(),
+ documentationLinks: coreMocks.docLinksServiceMock.createStartContract().links,
+ };
+});
+
const { setup } = pageHelpers.jobList;
describe('Smoke test cloning an existing rollup job from job list', () => {
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 9520c1ad0d9c1..91277403d9e05 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -18006,7 +18006,6 @@
"xpack.rollupJobs.jobTable.selectRow": "この行 {id} を選択",
"xpack.rollupJobs.licenseCheckErrorMessage": "ライセンス確認失敗",
"xpack.rollupJobs.listBreadcrumbTitle": "ロールアップジョブ",
- "xpack.rollupJobs.loadAction.errorTitle": "ロールアップジョブを読み込み中にエラーが発生",
"xpack.rollupJobs.refreshAction.errorTitle": "ロールアップジョブの更新中にエラーが発生",
"xpack.rollupJobs.rollupIndexPatternsDescription": "ロールアップインデックスを捕捉するインデックスパターンの作成を有効にします。\n それによりロールアップデータに基づくビジュアライゼーションが可能になります。",
"xpack.rollupJobs.rollupIndexPatternsTitle": "ロールアップインデックスパターンを有効にする",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index f74d27eb8b214..632c502d4ef55 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -18250,7 +18250,6 @@
"xpack.rollupJobs.jobTable.selectRow": "选择行 {id}",
"xpack.rollupJobs.licenseCheckErrorMessage": "许可证检查失败",
"xpack.rollupJobs.listBreadcrumbTitle": "汇总/打包作业",
- "xpack.rollupJobs.loadAction.errorTitle": "加载汇总/打包作业时出错",
"xpack.rollupJobs.refreshAction.errorTitle": "刷新汇总/打包作业时出错",
"xpack.rollupJobs.rollupIndexPatternsDescription": "启用用于捕获汇总/打包索引的索引模式的创建,\n 汇总/打包索引反过来基于汇总/打包数据启用可视化。",
"xpack.rollupJobs.rollupIndexPatternsTitle": "启用汇总索引模式",
From 953a464e94ad1791c228fc1705430d11964da909 Mon Sep 17 00:00:00 2001
From: Lisa Cawley
Date: Tue, 22 Jun 2021 11:21:19 -0700
Subject: [PATCH 088/163] [Monitoring] Update Kibana rules/alerts language in
setup mode (#102441)
---
x-pack/plugins/monitoring/public/alerts/badge.tsx | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx
index 8b4075ba67cdc..44af8b3327975 100644
--- a/x-pack/plugins/monitoring/public/alerts/badge.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx
@@ -19,13 +19,18 @@ import { getAlertPanelsByCategory } from './lib/get_alert_panels_by_category';
import { getAlertPanelsByNode } from './lib/get_alert_panels_by_node';
export const numberOfAlertsLabel = (count: number) => `${count} alert${count > 1 ? 's' : ''}`;
+export const numberOfRulesLabel = (count: number) => `${count} rule${count > 1 ? 's' : ''}`;
const MAX_TO_SHOW_BY_CATEGORY = 8;
-const PANEL_TITLE = i18n.translate('xpack.monitoring.alerts.badge.panelTitle', {
+const PANEL_TITLE_ALERTS = i18n.translate('xpack.monitoring.alerts.badge.panelTitle', {
defaultMessage: 'Alerts',
});
+const PANEL_TITLE_RULES = i18n.translate('xpack.monitoring.rules.badge.panelTitle', {
+ defaultMessage: 'Rules',
+});
+
const GROUP_BY_NODE = i18n.translate('xpack.monitoring.alerts.badge.groupByNode', {
defaultMessage: 'Group by node',
});
@@ -54,6 +59,7 @@ export const AlertsBadge: React.FC = (props: Props) => {
const [showByNode, setShowByNode] = React.useState(
!inSetupMode && alertCount > MAX_TO_SHOW_BY_CATEGORY
);
+ const PANEL_TITLE = inSetupMode ? PANEL_TITLE_RULES : PANEL_TITLE_ALERTS;
React.useEffect(() => {
if (inSetupMode && showByNode) {
@@ -93,10 +99,12 @@ export const AlertsBadge: React.FC = (props: Props) => {
setShowPopover(true)}
>
- {numberOfAlertsLabel(alertCount)}
+ {inSetupMode ? numberOfRulesLabel(alertCount) : numberOfAlertsLabel(alertCount)}
);
From 00a6bdd4010b2419229e3b9e98c738b10659df52 Mon Sep 17 00:00:00 2001
From: Joe Portner <5295965+jportner@users.noreply.github.com>
Date: Tue, 22 Jun 2021 14:36:04 -0400
Subject: [PATCH 089/163] Allow initialNamespaces to be used for isolated types
(#102585)
---
docs/api/saved-objects/bulk_create.asciidoc | 5 +
docs/api/saved-objects/create.asciidoc | 5 +
...jectsbulkcreateobject.initialnamespaces.md | 2 +-
...ore-server.savedobjectsbulkcreateobject.md | 2 +-
...dobjectscreateoptions.initialnamespaces.md | 2 +-
...n-core-server.savedobjectscreateoptions.md | 2 +-
.../service/lib/repository.test.js | 145 +++++++++++++-----
.../saved_objects/service/lib/repository.ts | 74 +++++----
.../service/saved_objects_client.ts | 12 +-
src/core/server/server.api.md | 2 +-
.../common/lib/saved_object_test_utils.ts | 6 +-
.../common/suites/bulk_create.ts | 22 ++-
.../common/suites/create.ts | 22 ++-
.../security_and_spaces/apis/bulk_create.ts | 19 ++-
.../security_and_spaces/apis/create.ts | 19 ++-
.../security_only/apis/bulk_create.ts | 18 ++-
.../security_only/apis/create.ts | 18 ++-
.../spaces_only/apis/bulk_create.ts | 18 ++-
.../spaces_only/apis/create.ts | 18 ++-
19 files changed, 307 insertions(+), 104 deletions(-)
diff --git a/docs/api/saved-objects/bulk_create.asciidoc b/docs/api/saved-objects/bulk_create.asciidoc
index 267ab3891d700..5bd3a7587dde9 100644
--- a/docs/api/saved-objects/bulk_create.asciidoc
+++ b/docs/api/saved-objects/bulk_create.asciidoc
@@ -45,6 +45,11 @@ experimental[] Create multiple {kib} saved objects.
(Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the
object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space
(default behavior).
+* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including
+the "All spaces" identifier (`'*'`).
+* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be
+used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
`version`::
(Optional, number) Specifies the version.
diff --git a/docs/api/saved-objects/create.asciidoc b/docs/api/saved-objects/create.asciidoc
index d7a368034ef07..e7e25c7d3bba6 100644
--- a/docs/api/saved-objects/create.asciidoc
+++ b/docs/api/saved-objects/create.asciidoc
@@ -52,6 +52,11 @@ any data that you send to the API is properly formed.
(Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the
object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space
(default behavior).
+* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including
+the "All spaces" identifier (`'*'`).
+* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be
+used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+* For global object types (registered with `namespaceType: 'agnostic'): this option cannot be used.
[[saved-objects-api-create-request-codes]]
==== Response code
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md
index 3db8bbadfbd6b..4d094ecde7a96 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md
@@ -6,7 +6,7 @@
Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).
-Note: this can only be used for multi-namespace object types.
+\* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including the "All spaces" identifier (`'*'`). \* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. \* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
Signature:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
index 6fc01212a2e41..463c3fe81b702 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md
@@ -18,7 +18,7 @@ export interface SavedObjectsBulkCreateObject
| [attributes](./kibana-plugin-core-server.savedobjectsbulkcreateobject.attributes.md) | T
| |
| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md) | string
| A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. |
| [id](./kibana-plugin-core-server.savedobjectsbulkcreateobject.id.md) | string
| |
-| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. |
+| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'
): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'
). \* For isolated object types (registered with namespaceType: 'single'
or namespaceType: 'multiple-isolated'
): this option can only be used to specify a single space, and the "All spaces" identifier ('*'
) is not allowed. \* For global object types (registered with namespaceType: 'agnostic'
): this option cannot be used. |
| [migrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion
| Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [originId](./kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md) | string
| Optional ID of the original saved object, if this object's id
was regenerated |
| [references](./kibana-plugin-core-server.savedobjectsbulkcreateobject.references.md) | SavedObjectReference[]
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md
index 262b0997cb905..43489b8d2e8a2 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md
@@ -6,7 +6,7 @@
Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).
-Note: this can only be used for multi-namespace object types.
+\* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including the "All spaces" identifier (`'*'`). \* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. \* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
Signature:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
index 1805f389d4e7f..7eaa9c51f5c82 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md
@@ -17,7 +17,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions
| --- | --- | --- |
| [coreMigrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md) | string
| A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. |
| [id](./kibana-plugin-core-server.savedobjectscreateoptions.id.md) | string
| (not recommended) Specify an id for the document |
-| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. |
+| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[]
| Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'
): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'
). \* For isolated object types (registered with namespaceType: 'single'
or namespaceType: 'multiple-isolated'
): this option can only be used to specify a single space, and the "All spaces" identifier ('*'
) is not allowed. \* For global object types (registered with namespaceType: 'agnostic'
): this option cannot be used. |
| [migrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion
| Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. |
| [originId](./kibana-plugin-core-server.savedobjectscreateoptions.originid.md) | string
| Optional ID of the original saved object, if this object's id
was regenerated |
| [overwrite](./kibana-plugin-core-server.savedobjectscreateoptions.overwrite.md) | boolean
| Overwrite existing documents (defaults to false) |
diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js
index 22c40a547f419..4456784fdbc0b 100644
--- a/src/core/server/saved_objects/service/lib/repository.test.js
+++ b/src/core/server/saved_objects/service/lib/repository.test.js
@@ -525,15 +525,22 @@ describe('SavedObjectsRepository', () => {
const ns2 = 'bar-namespace';
const ns3 = 'baz-namespace';
const objects = [
- { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2] },
- { ...obj2, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns3] },
+ { ...obj1, type: 'dashboard', initialNamespaces: [ns2] },
+ { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, initialNamespaces: [ns2] },
+ { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2, ns3] },
];
await bulkCreateSuccess(objects, { namespace, overwrite: true });
const body = [
- expect.any(Object),
+ { index: expect.objectContaining({ _id: `${ns2}:dashboard:${obj1.id}` }) },
+ expect.objectContaining({ namespace: ns2 }),
+ {
+ index: expect.objectContaining({
+ _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${obj1.id}`,
+ }),
+ },
expect.objectContaining({ namespaces: [ns2] }),
- expect.any(Object),
- expect.objectContaining({ namespaces: [ns3] }),
+ { index: expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj1.id}` }) },
+ expect.objectContaining({ namespaces: [ns2, ns3] }),
];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
@@ -649,24 +656,19 @@ describe('SavedObjectsRepository', () => {
).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"'));
});
- it(`returns error when initialNamespaces is used with a non-shareable object`, async () => {
- const test = async (objType) => {
- const obj = { ...obj3, type: objType, initialNamespaces: [] };
- await bulkCreateError(
+ it(`returns error when initialNamespaces is used with a space-agnostic object`, async () => {
+ const obj = { ...obj3, type: NAMESPACE_AGNOSTIC_TYPE, initialNamespaces: [] };
+ await bulkCreateError(
+ obj,
+ undefined,
+ expectErrorResult(
obj,
- undefined,
- expectErrorResult(
- obj,
- createBadRequestError('"initialNamespaces" can only be used on multi-namespace types')
- )
- );
- };
- await test('dashboard');
- await test(NAMESPACE_AGNOSTIC_TYPE);
- await test(MULTI_NAMESPACE_ISOLATED_TYPE);
+ createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types')
+ )
+ );
});
- it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => {
+ it(`returns error when initialNamespaces is empty`, async () => {
const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] };
await bulkCreateError(
obj,
@@ -678,6 +680,26 @@ describe('SavedObjectsRepository', () => {
);
});
+ it(`returns error when initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => {
+ const doTest = async (objType, initialNamespaces) => {
+ const obj = { ...obj3, type: objType, initialNamespaces };
+ await bulkCreateError(
+ obj,
+ undefined,
+ expectErrorResult(
+ obj,
+ createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ )
+ )
+ );
+ };
+ await doTest('dashboard', ['spacex', 'spacey']);
+ await doTest('dashboard', ['*']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']);
+ });
+
it(`returns error when type is invalid`, async () => {
const obj = { ...obj3, type: 'unknownType' };
await bulkCreateError(obj, undefined, expectErrorInvalidType(obj));
@@ -1865,12 +1887,46 @@ describe('SavedObjectsRepository', () => {
});
it(`adds initialNamespaces instead of namespace`, async () => {
- const options = { id, namespace, initialNamespaces: ['bar-namespace', 'baz-namespace'] };
- await createSuccess(MULTI_NAMESPACE_TYPE, attributes, options);
- expect(client.create).toHaveBeenCalledWith(
+ const ns2 = 'bar-namespace';
+ const ns3 = 'baz-namespace';
+ await savedObjectsRepository.create('dashboard', attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2],
+ });
+ await savedObjectsRepository.create(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2],
+ });
+ await savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, {
+ id,
+ namespace,
+ initialNamespaces: [ns2, ns3],
+ });
+
+ expect(client.create).toHaveBeenCalledTimes(3);
+ expect(client.create).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ id: `${ns2}:dashboard:${id}`,
+ body: expect.objectContaining({ namespace: ns2 }),
+ }),
+ expect.anything()
+ );
+ expect(client.create).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`,
+ body: expect.objectContaining({ namespaces: [ns2] }),
+ }),
+ expect.anything()
+ );
+ expect(client.create).toHaveBeenNthCalledWith(
+ 3,
expect.objectContaining({
id: `${MULTI_NAMESPACE_TYPE}:${id}`,
- body: expect.objectContaining({ namespaces: options.initialNamespaces }),
+ body: expect.objectContaining({ namespaces: [ns2, ns3] }),
}),
expect.anything()
);
@@ -1892,29 +1948,40 @@ describe('SavedObjectsRepository', () => {
});
describe('errors', () => {
- it(`throws when options.initialNamespaces is used with a non-shareable object`, async () => {
- const test = async (objType) => {
- await expect(
- savedObjectsRepository.create(objType, attributes, { initialNamespaces: [namespace] })
- ).rejects.toThrowError(
- createBadRequestError(
- '"options.initialNamespaces" can only be used on multi-namespace types'
- )
- );
- };
- await test('dashboard');
- await test(MULTI_NAMESPACE_ISOLATED_TYPE);
- await test(NAMESPACE_AGNOSTIC_TYPE);
+ it(`throws when options.initialNamespaces is used with a space-agnostic object`, async () => {
+ await expect(
+ savedObjectsRepository.create(NAMESPACE_AGNOSTIC_TYPE, attributes, {
+ initialNamespaces: [namespace],
+ })
+ ).rejects.toThrowError(
+ createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types')
+ );
});
- it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => {
+ it(`throws when options.initialNamespaces is empty`, async () => {
await expect(
savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { initialNamespaces: [] })
).rejects.toThrowError(
- createBadRequestError('"options.initialNamespaces" must be a non-empty array of strings')
+ createBadRequestError('"initialNamespaces" must be a non-empty array of strings')
);
});
+ it(`throws when options.initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => {
+ const doTest = async (objType, initialNamespaces) => {
+ await expect(
+ savedObjectsRepository.create(objType, attributes, { initialNamespaces })
+ ).rejects.toThrowError(
+ createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ )
+ );
+ };
+ await doTest('dashboard', ['spacex', 'spacey']);
+ await doTest('dashboard', ['*']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']);
+ await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']);
+ });
+
it(`throws when options.namespace is '*'`, async () => {
await expect(
savedObjectsRepository.create(type, attributes, { namespace: ALL_NAMESPACES_STRING })
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index 1577f773434b9..c9fa50da55df1 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -283,28 +283,18 @@ export class SavedObjectsRepository {
} = options;
const namespace = normalizeNamespace(options.namespace);
- if (initialNamespaces) {
- if (!this._registry.isShareable(type)) {
- throw SavedObjectsErrorHelpers.createBadRequestError(
- '"options.initialNamespaces" can only be used on multi-namespace types'
- );
- } else if (!initialNamespaces.length) {
- throw SavedObjectsErrorHelpers.createBadRequestError(
- '"options.initialNamespaces" must be a non-empty array of strings'
- );
- }
- }
+ this.validateInitialNamespaces(type, initialNamespaces);
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
}
const time = this._getCurrentTime();
- let savedObjectNamespace;
+ let savedObjectNamespace: string | undefined;
let savedObjectNamespaces: string[] | undefined;
- if (this._registry.isSingleNamespace(type) && namespace) {
- savedObjectNamespace = namespace;
+ if (this._registry.isSingleNamespace(type)) {
+ savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace;
} else if (this._registry.isMultiNamespace(type)) {
if (id && overwrite) {
// we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces
@@ -369,32 +359,29 @@ export class SavedObjectsRepository {
let bulkGetRequestIndexCounter = 0;
const expectedResults: Either[] = objects.map((object) => {
+ const { type, id, initialNamespaces } = object;
let error: DecoratedError | undefined;
- if (!this._allowedTypes.includes(object.type)) {
- error = SavedObjectsErrorHelpers.createUnsupportedTypeError(object.type);
- } else if (object.initialNamespaces) {
- if (!this._registry.isShareable(object.type)) {
- error = SavedObjectsErrorHelpers.createBadRequestError(
- '"initialNamespaces" can only be used on multi-namespace types'
- );
- } else if (!object.initialNamespaces.length) {
- error = SavedObjectsErrorHelpers.createBadRequestError(
- '"initialNamespaces" must be a non-empty array of strings'
- );
+ if (!this._allowedTypes.includes(type)) {
+ error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
+ } else {
+ try {
+ this.validateInitialNamespaces(type, initialNamespaces);
+ } catch (e) {
+ error = e;
}
}
if (error) {
return {
tag: 'Left' as 'Left',
- error: { id: object.id, type: object.type, error: errorContent(error) },
+ error: { id, type, error: errorContent(error) },
};
}
- const method = object.id && overwrite ? 'index' : 'create';
- const requiresNamespacesCheck = object.id && this._registry.isMultiNamespace(object.type);
+ const method = id && overwrite ? 'index' : 'create';
+ const requiresNamespacesCheck = id && this._registry.isMultiNamespace(type);
- if (object.id == null) {
+ if (id == null) {
object.id = SavedObjectsUtils.generateId();
}
@@ -434,8 +421,8 @@ export class SavedObjectsRepository {
return expectedBulkGetResult;
}
- let savedObjectNamespace;
- let savedObjectNamespaces;
+ let savedObjectNamespace: string | undefined;
+ let savedObjectNamespaces: string[] | undefined;
let versionProperties;
const {
esRequestIndex,
@@ -469,7 +456,7 @@ export class SavedObjectsRepository {
versionProperties = getExpectedVersionProperties(version, actualResult);
} else {
if (this._registry.isSingleNamespace(object.type)) {
- savedObjectNamespace = namespace;
+ savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace;
} else if (this._registry.isMultiNamespace(object.type)) {
savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace);
}
@@ -2080,6 +2067,29 @@ export class SavedObjectsRepository {
const object = await this.get(type, id, options);
return { saved_object: object, outcome: 'exactMatch' };
}
+
+ private validateInitialNamespaces(type: string, initialNamespaces: string[] | undefined) {
+ if (!initialNamespaces) {
+ return;
+ }
+
+ if (this._registry.isNamespaceAgnostic(type)) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" cannot be used on space-agnostic types'
+ );
+ } else if (!initialNamespaces.length) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" must be a non-empty array of strings'
+ );
+ } else if (
+ !this._registry.isShareable(type) &&
+ (initialNamespaces.length > 1 || initialNamespaces.includes(ALL_NAMESPACES_STRING))
+ ) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ '"initialNamespaces" can only specify a single space when used with space-isolated types'
+ );
+ }
+ }
}
/**
diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts
index af682cfb81296..1423050145695 100644
--- a/src/core/server/saved_objects/service/saved_objects_client.ts
+++ b/src/core/server/saved_objects/service/saved_objects_client.ts
@@ -63,7 +63,11 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions {
* Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in
* {@link SavedObjectsCreateOptions}.
*
- * Note: this can only be used for multi-namespace object types.
+ * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces,
+ * including the "All spaces" identifier (`'*'`).
+ * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only
+ * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+ * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
}
@@ -96,7 +100,11 @@ export interface SavedObjectsBulkCreateObject {
* Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in
* {@link SavedObjectsCreateOptions}.
*
- * Note: this can only be used for multi-namespace object types.
+ * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces,
+ * including the "All spaces" identifier (`'*'`).
+ * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only
+ * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed.
+ * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used.
*/
initialNamespaces?: string[];
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 9e7721fde90e7..fcecf39f7e53a 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -2901,7 +2901,7 @@ export class SavedObjectsRepository {
resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>;
update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>;
updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise;
-}
+ }
// @public
export interface SavedObjectsRepositoryFactory {
diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts
index b712c2882ee0f..eb0c161049cf0 100644
--- a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts
+++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts
@@ -154,12 +154,14 @@ export const expectResponses = {
// bulk request error
expect(object.type).to.eql(type);
expect(object.id).to.eql(id);
- expect(object.error).to.eql(error.output.payload);
+ expect(object.error.error).to.eql(error.output.payload.error);
+ expect(object.error.statusCode).to.eql(error.output.payload.statusCode);
+ // ignore the error.message, because it can vary for decorated errors
} else {
// non-bulk request error
expect(object.error).to.eql(error.output.payload.error);
expect(object.statusCode).to.eql(error.output.payload.statusCode);
- // ignore the error.message, because it can vary for decorated non-bulk errors (e.g., conflict)
+ // ignore the error.message, because it can vary for decorated errors
}
} else {
// fall back to default behavior of testing the success outcome
diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
index 5860ec1f193b2..06758da1ebad2 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
@@ -41,13 +41,25 @@ const EACH_SPACE = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID];
const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' });
const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' });
-const NEW_EACH_SPACE_OBJ = Object.freeze({
+const INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE = Object.freeze({
+ type: 'isolatedtype',
+ id: 'new-other-space-id',
+ expectedNamespaces: ['other-space'], // expected namespaces of resulting object
+ initialNamespaces: ['other-space'], // args passed to the bulkCreate method
+});
+const INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE = Object.freeze({
+ type: 'sharecapabletype',
+ id: 'new-other-space-id',
+ expectedNamespaces: ['other-space'], // expected namespaces of resulting object
+ initialNamespaces: ['other-space'], // args passed to the bulkCreate method
+});
+const INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE = Object.freeze({
type: 'sharedtype',
id: 'new-each-space-id',
expectedNamespaces: EACH_SPACE, // expected namespaces of resulting object
initialNamespaces: EACH_SPACE, // args passed to the bulkCreate method
});
-const NEW_ALL_SPACES_OBJ = Object.freeze({
+const INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES = Object.freeze({
type: 'sharedtype',
id: 'new-all-spaces-id',
expectedNamespaces: [ALL_SPACES_ID], // expected namespaces of resulting object
@@ -58,8 +70,10 @@ export const TEST_CASES: Record = Object.freeze({
...CASES,
NEW_SINGLE_NAMESPACE_OBJ,
NEW_MULTI_NAMESPACE_OBJ,
- NEW_EACH_SPACE_OBJ,
- NEW_ALL_SPACES_OBJ,
+ INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
NEW_NAMESPACE_AGNOSTIC_OBJ,
});
diff --git a/x-pack/test/saved_object_api_integration/common/suites/create.ts b/x-pack/test/saved_object_api_integration/common/suites/create.ts
index ff2bfdefb4c08..298e1a9807175 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/create.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/create.ts
@@ -41,13 +41,25 @@ const EACH_SPACE = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID];
// we could create six separate test cases to test every permutation, but there's no real value in doing so
const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: '' });
const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' });
-const NEW_EACH_SPACE_OBJ = Object.freeze({
+const INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE = Object.freeze({
+ type: 'isolatedtype',
+ id: 'new-other-space-id',
+ expectedNamespaces: ['other-space'], // expected namespaces of resulting object
+ initialNamespaces: ['other-space'], // args passed to the bulkCreate method
+});
+const INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE = Object.freeze({
+ type: 'sharecapabletype',
+ id: 'new-other-space-id',
+ expectedNamespaces: ['other-space'], // expected namespaces of resulting object
+ initialNamespaces: ['other-space'], // args passed to the bulkCreate method
+});
+const INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE = Object.freeze({
type: 'sharedtype',
id: 'new-each-space-id',
expectedNamespaces: EACH_SPACE, // expected namespaces of resulting object
initialNamespaces: EACH_SPACE, // args passed to the bulkCreate method
});
-const NEW_ALL_SPACES_OBJ = Object.freeze({
+const INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES = Object.freeze({
type: 'sharedtype',
id: 'new-all-spaces-id',
expectedNamespaces: [ALL_SPACES_ID], // expected namespaces of resulting object
@@ -58,8 +70,10 @@ export const TEST_CASES: Record = Object.freeze({
...CASES,
NEW_SINGLE_NAMESPACE_OBJ,
NEW_MULTI_NAMESPACE_OBJ,
- NEW_EACH_SPACE_OBJ,
- NEW_ALL_SPACES_OBJ,
+ INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
NEW_NAMESPACE_AGNOSTIC_OBJ,
});
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts
index 1fa24c6d6e2d6..e048a4abc8ccc 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { SPACES } from '../../common/lib/spaces';
+import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { TestUser } from '../../common/lib/types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@@ -75,7 +75,22 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
{ ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces },
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
];
- const crossNamespace = [CASES.NEW_EACH_SPACE_OBJ, CASES.NEW_ALL_SPACES_OBJ];
+ const crossNamespace = [
+ {
+ ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ initialNamespaces: ['x', 'y'],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ {
+ ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ initialNamespaces: [ALL_SPACES_ID],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
+ ];
const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }];
const allTypes = normalTypes.concat(hiddenType);
return { normalTypes, crossNamespace, hiddenType, allTypes };
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts
index 3553ae0e5b538..8215c991a9287 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { SPACES } from '../../common/lib/spaces';
+import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { TestUser } from '../../common/lib/types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@@ -62,7 +62,22 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
{ ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces },
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
];
- const crossNamespace = [CASES.NEW_EACH_SPACE_OBJ, CASES.NEW_ALL_SPACES_OBJ];
+ const crossNamespace = [
+ {
+ ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ initialNamespaces: ['x', 'y'],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ {
+ ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ initialNamespaces: [ALL_SPACES_ID],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
+ ];
const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }];
const allTypes = normalTypes.concat(crossNamespace, hiddenType);
return { normalTypes, crossNamespace, hiddenType, allTypes };
diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts
index 7487466f4b38c..f9423d77c5bb5 100644
--- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { SPACES } from '../../common/lib/spaces';
+import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { TestUser } from '../../common/lib/types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@@ -39,8 +39,20 @@ const createTestCases = (overwrite: boolean) => {
{ ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces },
{ ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces },
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
- CASES.NEW_EACH_SPACE_OBJ,
- CASES.NEW_ALL_SPACES_OBJ,
+ {
+ ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ initialNamespaces: ['x', 'y'],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ {
+ ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ initialNamespaces: [ALL_SPACES_ID],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
];
const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }];
const allTypes = normalTypes.concat(hiddenType);
diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts
index 7eda7f5283448..67195637f0c0a 100644
--- a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts
+++ b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { SPACES } from '../../common/lib/spaces';
+import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { TestUser } from '../../common/lib/types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@@ -38,8 +38,20 @@ const createTestCases = (overwrite: boolean) => {
{ ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces },
{ ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces },
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
- CASES.NEW_EACH_SPACE_OBJ,
- CASES.NEW_ALL_SPACES_OBJ,
+ {
+ ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ initialNamespaces: ['x', 'y'],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ {
+ ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ initialNamespaces: [ALL_SPACES_ID],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
];
const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }];
const allTypes = normalTypes.concat(hiddenType);
diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts
index 5812aaf43060d..c448d73ce7bf8 100644
--- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { SPACES } from '../../common/lib/spaces';
+import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { bulkCreateTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/bulk_create';
@@ -70,8 +70,20 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
{ ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces },
{ ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces },
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
- CASES.NEW_EACH_SPACE_OBJ,
- CASES.NEW_ALL_SPACES_OBJ,
+ {
+ ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ initialNamespaces: ['x', 'y'],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ {
+ ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ initialNamespaces: [ALL_SPACES_ID],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
];
};
diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts
index 4c91781b6ab2c..7c8726896c18a 100644
--- a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts
+++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { SPACES } from '../../common/lib/spaces';
+import { SPACES, ALL_SPACES_ID } from '../../common/lib/spaces';
import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { createTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/create';
@@ -57,8 +57,20 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
{ ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces },
{ ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces },
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
- CASES.NEW_EACH_SPACE_OBJ,
- CASES.NEW_ALL_SPACES_OBJ,
+ {
+ ...CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE,
+ initialNamespaces: ['x', 'y'],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_SINGLE_NAMESPACE_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ {
+ ...CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE,
+ initialNamespaces: [ALL_SPACES_ID],
+ ...fail400(), // cannot be created in multiple spaces
+ },
+ CASES.INITIAL_NS_MULTI_NAMESPACE_ISOLATED_OBJ_OTHER_SPACE, // second try creates it in a single other space, which is valid
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_EACH_SPACE,
+ CASES.INITIAL_NS_MULTI_NAMESPACE_OBJ_ALL_SPACES,
];
};
From f422cbdcf17f5e598e715e88bd9bdd52d2f1d72b Mon Sep 17 00:00:00 2001
From: Constance
Date: Tue, 22 Jun 2021 11:40:10 -0700
Subject: [PATCH 090/163] [App Search] Convert API Logs page to new page
template + empty state polish (#102820)
* Convert API Logs noItemsMessage to its own empty state prompt
- Will be used by new page template
* Convert API Logs view to new page template
+ use new empty state
+ add tests clarifying loading UX
* Update router
* Fix i18n ID
---
.../components/api_logs/api_logs.test.tsx | 24 +++---
.../components/api_logs/api_logs.tsx | 73 ++++++++-----------
.../components/api_logs_table.test.tsx | 10 +--
.../api_logs/components/api_logs_table.tsx | 20 -----
.../api_logs/components/empty_state.test.tsx | 27 +++++++
.../api_logs/components/empty_state.tsx | 45 ++++++++++++
.../components/api_logs/components/index.ts | 1 +
.../components/engine/engine_router.tsx | 10 +--
8 files changed, 124 insertions(+), 86 deletions(-)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx
index c2a11ec06fa6a..5b082ce8d26ba 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx
@@ -13,10 +13,7 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPageHeader } from '@elastic/eui';
-
-import { Loading } from '../../../shared/loading';
-import { rerender } from '../../../test_helpers';
+import { rerender, getPageTitle } from '../../../test_helpers';
import { LogRetentionCallout, LogRetentionTooltip } from '../log_retention';
import { ApiLogsTable, NewApiEventsPrompt } from './components';
@@ -42,7 +39,7 @@ describe('ApiLogs', () => {
it('renders', () => {
const wrapper = shallow( );
- expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('API Logs');
+ expect(getPageTitle(wrapper)).toEqual('API Logs');
expect(wrapper.find(ApiLogsTable)).toHaveLength(1);
expect(wrapper.find(NewApiEventsPrompt)).toHaveLength(1);
@@ -50,11 +47,20 @@ describe('ApiLogs', () => {
expect(wrapper.find(LogRetentionTooltip).prop('type')).toEqual('api');
});
- it('renders a loading screen', () => {
- setMockValues({ ...values, dataLoading: true, apiLogs: [] });
- const wrapper = shallow( );
+ describe('loading state', () => {
+ it('renders a full-page loading state on initial page load (no logs exist yet)', () => {
+ setMockValues({ ...values, dataLoading: true, apiLogs: [] });
+ const wrapper = shallow( );
+
+ expect(wrapper.prop('isLoading')).toEqual(true);
+ });
+
+ it('does not re-render a full-page loading state after initial page load (uses component-level loading state instead)', () => {
+ setMockValues({ ...values, dataLoading: true, apiLogs: [{}] });
+ const wrapper = shallow( );
- expect(wrapper.find(Loading)).toHaveLength(1);
+ expect(wrapper.prop('isLoading')).toEqual(false);
+ });
});
describe('effects', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx
index b8179163c93f9..d3eef77db21f0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx
@@ -9,25 +9,14 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
-import {
- EuiPageHeader,
- EuiTitle,
- EuiPageContent,
- EuiPageContentBody,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
-} from '@elastic/eui';
-
-import { FlashMessages } from '../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
-import { Loading } from '../../../shared/loading';
+import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
import { getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import { LogRetentionCallout, LogRetentionTooltip, LogRetentionOptions } from '../log_retention';
import { ApiLogFlyout } from './api_log';
-import { ApiLogsTable, NewApiEventsPrompt } from './components';
+import { ApiLogsTable, NewApiEventsPrompt, EmptyState } from './components';
import { API_LOGS_TITLE, RECENT_API_EVENTS } from './constants';
import { ApiLogsLogic } from './';
@@ -44,38 +33,36 @@ export const ApiLogs: React.FC = () => {
pollForApiLogs();
}, []);
- if (dataLoading && !apiLogs.length) return ;
-
return (
- <>
-
-
-
-
+ }
+ >
-
-
-
-
-
- {RECENT_API_EVENTS}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ {RECENT_API_EVENTS}
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
- >
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx
index 2a00cc6eb42bb..82d3d4715cbc5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx
@@ -18,7 +18,7 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiBasicTable, EuiBadge, EuiHealth, EuiButtonEmpty, EuiEmptyPrompt } from '@elastic/eui';
+import { EuiBasicTable, EuiBadge, EuiHealth, EuiButtonEmpty } from '@elastic/eui';
import { DEFAULT_META } from '../../../../shared/constants';
import { mountWithIntl } from '../../../../test_helpers';
@@ -91,14 +91,6 @@ describe('ApiLogsTable', () => {
expect(actions.openFlyout).toHaveBeenCalled();
});
- it('renders an empty prompt if no items are passed', () => {
- setMockValues({ ...values, apiLogs: [] });
- const wrapper = mountWithIntl( );
- const promptContent = wrapper.find(EuiEmptyPrompt).text();
-
- expect(promptContent).toContain('Perform your first API call');
- });
-
describe('hasPagination', () => {
it('does not render with pagination by default', () => {
const wrapper = shallow( );
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx
index bb1327ce2da30..1b5a8084f5b59 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx
@@ -15,7 +15,6 @@ import {
EuiBadge,
EuiHealth,
EuiButtonEmpty,
- EuiEmptyPrompt,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedRelative } from '@kbn/i18n/react';
@@ -109,25 +108,6 @@ export const ApiLogsTable: React.FC = ({ hasPagination }) => {
items={apiLogs}
responsive
loading={dataLoading}
- noItemsMessage={
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', {
- defaultMessage: 'Perform your first API call',
- })}
-
- }
- body={
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', {
- defaultMessage: "Check back after you've performed some API calls.",
- })}
-
- }
- />
- }
{...paginationProps}
/>
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx
new file mode 100644
index 0000000000000..3ad22ceac5840
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx
@@ -0,0 +1,27 @@
+/*
+ * 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 { shallow } from 'enzyme';
+
+import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+
+import { EmptyState } from './';
+
+describe('EmptyState', () => {
+ it('renders', () => {
+ const wrapper = shallow( )
+ .find(EuiEmptyPrompt)
+ .dive();
+
+ expect(wrapper.find('h2').text()).toEqual('Perform your first API call');
+ expect(wrapper.find(EuiButton).prop('href')).toEqual(
+ expect.stringContaining('/api-reference.html')
+ );
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx
new file mode 100644
index 0000000000000..3f6f44adefc71
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { DOCS_PREFIX } from '../../../routes';
+
+export const EmptyState: React.FC = () => (
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', {
+ defaultMessage: 'Perform your first API call',
+ })}
+
+ }
+ body={
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', {
+ defaultMessage: "Check back after you've performed some API calls.",
+ })}
+
+ }
+ actions={
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.empty.buttonLabel', {
+ defaultMessage: 'View the API reference',
+ })}
+
+ }
+ />
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts
index c0edc51d06228..863216554a540 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts
@@ -7,3 +7,4 @@
export { ApiLogsTable } from './api_logs_table';
export { NewApiEventsPrompt } from './new_api_events_prompt';
+export { EmptyState } from './empty_state';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
index 3e18c9e680de2..fc057858426d2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
@@ -114,6 +114,11 @@ export const EngineRouter: React.FC = () => {
)}
+ {canViewEngineApiLogs && (
+
+
+
+ )}
{/* TODO: Remove layout once page template migration is over */}
}>
{canViewEngineSchema && (
@@ -141,11 +146,6 @@ export const EngineRouter: React.FC = () => {
)}
- {canViewEngineApiLogs && (
-
-
-
- )}
{canViewMetaEngineSourceEngines && (
From 2b0f1256ddd2e65db8f29c198000db4d7cb3af61 Mon Sep 17 00:00:00 2001
From: Clint Andrew Hall
Date: Tue, 22 Jun 2021 14:11:15 -0500
Subject: [PATCH 091/163] [canvas] New Home Page (#102446)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/canvas/i18n/components.ts | 221 -------
x-pack/plugins/canvas/i18n/errors.ts | 53 +-
.../public/components/home/home.component.tsx | 67 +++
.../public/components/home/home.stories.tsx | 30 +
.../canvas/public/components/home/home.tsx | 33 +
.../public/components/home/hooks/index.ts | 15 +
.../home/hooks/use_clone_workpad.ts | 60 ++
.../home/hooks/use_create_from_template.ts | 32 +
.../home/hooks/use_create_workpad.ts | 46 ++
.../home/hooks/use_delete_workpad.ts | 63 ++
.../home/hooks/use_download_workpad.ts | 12 +
.../home/hooks/use_find_templates.ts | 38 ++
.../components/home/hooks/use_find_workpad.ts | 57 ++
.../home/hooks/use_upload_workpad.ts | 100 ++++
.../index.js => home/index.ts} | 2 +-
.../home/my_workpads/empty_prompt.stories.tsx | 19 +
.../home/my_workpads/empty_prompt.tsx | 65 ++
.../components/home/my_workpads/index.ts | 10 +
.../components/home/my_workpads/loading.tsx | 17 +
.../my_workpads/my_workpads.component.tsx | 38 ++
.../home/my_workpads/my_workpads.stories.tsx | 56 ++
.../home/my_workpads/my_workpads.tsx | 42 ++
.../my_workpads/upload_dropzone.component.tsx | 30 +
.../home/my_workpads/upload_dropzone.scss | 8 +
.../home/my_workpads/upload_dropzone.tsx | 55 ++
.../my_workpads/workpad_import.component.tsx | 40 ++
.../home/my_workpads/workpad_import.tsx | 35 ++
.../my_workpads/workpad_table.component.tsx | 203 +++++++
.../my_workpads/workpad_table.stories.tsx | 83 +++
.../home/my_workpads/workpad_table.tsx | 38 ++
.../workpad_table_tools.component.tsx | 160 +++++
.../home/my_workpads/workpad_table_tools.tsx | 51 ++
.../home/workpad_create.component.tsx | 37 ++
.../public/components/home/workpad_create.tsx | 31 +
.../home/workpad_templates/index.ts | 10 +
.../workpad_templates.component.tsx | 157 +++++
.../workpad_templates.stories.tsx | 62 ++
.../workpad_templates/workpad_templates.tsx | 35 ++
.../home_app/home_app.component.tsx | 18 +-
.../toolbar/__stories__/toolbar.stories.tsx | 2 -
.../components/toolbar/toolbar.component.tsx | 36 +-
.../components/workpad_loader/index.tsx | 173 ------
.../workpad_loader/upload_workpad.js | 52 --
.../workpad_loader/workpad_create.js | 31 -
.../workpad_loader/workpad_dropzone/index.js | 31 -
.../workpad_dropzone/workpad_dropzone.js | 31 -
.../workpad_dropzone/workpad_dropzone.scss | 22 -
.../workpad_loader/workpad_loader.js | 426 -------------
.../workpad_loader/workpad_loader.scss | 25 -
.../workpad_loader/workpad_search.js | 44 --
.../workpad_manager/workpad_manager.js | 69 ---
.../workpad_templates.stories.storyshot | 564 ------------------
.../examples/workpad_templates.stories.tsx | 45 --
.../components/workpad_templates/index.tsx | 86 ---
.../workpad_templates/workpad_templates.tsx | 215 -------
.../canvas/public/lib/get_tags_filter.tsx | 39 --
.../plugins/canvas/public/services/index.ts | 2 +-
.../canvas/public/services/stubs/platform.ts | 8 +-
.../canvas/public/services/stubs/workpad.ts | 96 ++-
.../plugins/canvas/public/services/workpad.ts | 21 +-
x-pack/plugins/canvas/public/style/index.scss | 2 -
.../canvas/storybook/decorators/index.ts | 3 +-
.../storybook/decorators/redux_decorator.tsx | 2 +-
.../decorators/services_decorator.tsx | 40 +-
x-pack/plugins/canvas/storybook/index.ts | 5 +
x-pack/plugins/canvas/storybook/main.ts | 5 +
.../empty_prompt.stories.storyshot | 65 ++
.../canvas/storybook/storyshots.test.tsx | 7 +-
.../translations/translations/ja-JP.json | 81 ++-
.../translations/translations/zh-CN.json | 81 ++-
x-pack/test/accessibility/apps/canvas.ts | 2 +-
.../test/functional/apps/canvas/smoke_test.js | 2 +-
.../functional/page_objects/canvas_page.ts | 2 +-
73 files changed, 2156 insertions(+), 2288 deletions(-)
create mode 100644 x-pack/plugins/canvas/public/components/home/home.component.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/home.stories.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/home.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/index.ts
create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts
create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts
create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts
create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts
create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts
create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts
create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts
create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts
rename x-pack/plugins/canvas/public/components/{workpad_manager/index.js => home/index.ts} (83%)
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/index.ts
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_create.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts
create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx
create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/index.tsx
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_templates/index.tsx
delete mode 100644 x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx
delete mode 100644 x-pack/plugins/canvas/public/lib/get_tags_filter.tsx
create mode 100644 x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot
diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts
index 7a23137e7ef60..6f011bb73e3b0 100644
--- a/x-pack/plugins/canvas/i18n/components.ts
+++ b/x-pack/plugins/canvas/i18n/components.ts
@@ -1166,12 +1166,6 @@ export const ComponentStrings = {
description: 'This is referring to the dimensions of U.S. standard letter paper.',
}),
},
- WorkpadCreate: {
- getWorkpadCreateButtonLabel: () =>
- i18n.translate('xpack.canvas.workpadCreate.createButtonLabel', {
- defaultMessage: 'Create workpad',
- }),
- },
WorkpadHeader: {
getAddElementButtonLabel: () =>
i18n.translate('xpack.canvas.workpadHeader.addElementButtonLabel', {
@@ -1546,219 +1540,4 @@ export const ComponentStrings = {
defaultMessage: 'Reset',
}),
},
- WorkpadLoader: {
- getClonedWorkpadName: (workpadName: string) =>
- i18n.translate('xpack.canvas.workpadLoader.clonedWorkpadName', {
- defaultMessage: 'Copy of {workpadName}',
- values: {
- workpadName,
- },
- description:
- 'This suffix is added to the end of the name of a cloned workpad to indicate that this ' +
- 'new workpad is a copy of the original workpad. Example: "Copy of Sales Pitch"',
- }),
- getCloneToolTip: () =>
- i18n.translate('xpack.canvas.workpadLoader.cloneTooltip', {
- defaultMessage: 'Clone workpad',
- }),
- getCreateWorkpadLoadingDescription: () =>
- i18n.translate('xpack.canvas.workpadLoader.createWorkpadLoadingDescription', {
- defaultMessage: 'Creating workpad...',
- description:
- 'This message appears while the user is waiting for a new workpad to be created',
- }),
- getDeleteButtonAriaLabel: (numberOfWorkpads: number) =>
- i18n.translate('xpack.canvas.workpadLoader.deleteButtonAriaLabel', {
- defaultMessage: 'Delete {numberOfWorkpads} workpads',
- values: {
- numberOfWorkpads,
- },
- }),
- getDeleteButtonLabel: (numberOfWorkpads: number) =>
- i18n.translate('xpack.canvas.workpadLoader.deleteButtonLabel', {
- defaultMessage: 'Delete ({numberOfWorkpads})',
- values: {
- numberOfWorkpads,
- },
- }),
- getDeleteModalConfirmButtonLabel: () =>
- i18n.translate('xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel', {
- defaultMessage: 'Delete',
- }),
- getDeleteModalDescription: () =>
- i18n.translate('xpack.canvas.workpadLoader.deleteModalDescription', {
- defaultMessage: `You can't recover deleted workpads.`,
- }),
- getDeleteMultipleWorkpadModalTitle: (numberOfWorkpads: string) =>
- i18n.translate('xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle', {
- defaultMessage: 'Delete {numberOfWorkpads} workpads?',
- values: {
- numberOfWorkpads,
- },
- }),
- getDeleteSingleWorkpadModalTitle: (workpadName: string) =>
- i18n.translate('xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle', {
- defaultMessage: `Delete workpad '{workpadName}'?`,
- values: {
- workpadName,
- },
- }),
- getEmptyPromptGettingStartedDescription: () =>
- i18n.translate('xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription', {
- defaultMessage:
- 'Create a new workpad, start from a template, or import a workpad {JSON} file by dropping it here.',
- values: {
- JSON,
- },
- }),
- getEmptyPromptNewUserDescription: () =>
- i18n.translate('xpack.canvas.workpadLoader.emptyPromptNewUserDescription', {
- defaultMessage: 'New to {CANVAS}?',
- values: {
- CANVAS,
- },
- }),
- getEmptyPromptTitle: () =>
- i18n.translate('xpack.canvas.workpadLoader.emptyPromptTitle', {
- defaultMessage: 'Add your first workpad',
- }),
- getExportButtonAriaLabel: (numberOfWorkpads: number) =>
- i18n.translate('xpack.canvas.workpadLoader.exportButtonAriaLabel', {
- defaultMessage: 'Export {numberOfWorkpads} workpads',
- values: {
- numberOfWorkpads,
- },
- }),
- getExportButtonLabel: (numberOfWorkpads: number) =>
- i18n.translate('xpack.canvas.workpadLoader.exportButtonLabel', {
- defaultMessage: 'Export ({numberOfWorkpads})',
- values: {
- numberOfWorkpads,
- },
- }),
- getExportToolTip: () =>
- i18n.translate('xpack.canvas.workpadLoader.exportTooltip', {
- defaultMessage: 'Export workpad',
- }),
- getFetchLoadingDescription: () =>
- i18n.translate('xpack.canvas.workpadLoader.fetchLoadingDescription', {
- defaultMessage: 'Fetching workpads...',
- description:
- 'This message appears while the user is waiting for their list of workpads to load',
- }),
- getFilePickerPlaceholder: () =>
- i18n.translate('xpack.canvas.workpadLoader.filePickerPlaceholder', {
- defaultMessage: 'Import workpad {JSON} file',
- values: {
- JSON,
- },
- }),
- getLoadWorkpadArialLabel: (workpadName: string) =>
- i18n.translate('xpack.canvas.workpadLoader.loadWorkpadArialLabel', {
- defaultMessage: `Load workpad '{workpadName}'`,
- values: {
- workpadName,
- },
- }),
- getNoPermissionToCloneToolTip: () =>
- i18n.translate('xpack.canvas.workpadLoader.noPermissionToCloneToolTip', {
- defaultMessage: `You don't have permission to clone workpads`,
- }),
- getNoPermissionToCreateToolTip: () =>
- i18n.translate('xpack.canvas.workpadLoader.noPermissionToCreateToolTip', {
- defaultMessage: `You don't have permission to create workpads`,
- }),
- getNoPermissionToDeleteToolTip: () =>
- i18n.translate('xpack.canvas.workpadLoader.noPermissionToDeleteToolTip', {
- defaultMessage: `You don't have permission to delete workpads`,
- }),
- getNoPermissionToUploadToolTip: () =>
- i18n.translate('xpack.canvas.workpadLoader.noPermissionToUploadToolTip', {
- defaultMessage: `You don't have permission to upload workpads`,
- }),
- getSampleDataLinkLabel: () =>
- i18n.translate('xpack.canvas.workpadLoader.sampleDataLinkLabel', {
- defaultMessage: 'Add your first workpad',
- }),
- getTableCreatedColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadLoader.table.createdColumnTitle', {
- defaultMessage: 'Created',
- description: 'This column in the table contains the date/time the workpad was created.',
- }),
- getTableNameColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadLoader.table.nameColumnTitle', {
- defaultMessage: 'Workpad name',
- }),
- getTableUpdatedColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadLoader.table.updatedColumnTitle', {
- defaultMessage: 'Updated',
- description:
- 'This column in the table contains the date/time the workpad was last updated.',
- }),
- getTableActionsColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadLoader.table.actionsColumnTitle', {
- defaultMessage: 'Actions',
- description:
- 'This column in the table contains the actions that can be taken on a workpad.',
- }),
- },
- WorkpadManager: {
- getModalTitle: () =>
- i18n.translate('xpack.canvas.workpadManager.modalTitle', {
- defaultMessage: '{CANVAS} workpads',
- values: {
- CANVAS,
- },
- }),
- getMyWorkpadsTabLabel: () =>
- i18n.translate('xpack.canvas.workpadManager.myWorkpadsTabLabel', {
- defaultMessage: 'My workpads',
- }),
- getWorkpadTemplatesTabLabel: () =>
- i18n.translate('xpack.canvas.workpadManager.workpadTemplatesTabLabel', {
- defaultMessage: 'Templates',
- description: 'The label for the tab that displays a list of designed workpad templates.',
- }),
- },
- WorkpadSearch: {
- getWorkpadSearchPlaceholder: () =>
- i18n.translate('xpack.canvas.workpadSearch.searchPlaceholder', {
- defaultMessage: 'Find workpad',
- }),
- },
- WorkpadTemplates: {
- getCloneTemplateLinkAriaLabel: (templateName: string) =>
- i18n.translate('xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel', {
- defaultMessage: `Clone workpad template '{templateName}'`,
- values: {
- templateName,
- },
- }),
- getTableDescriptionColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadTemplates.table.descriptionColumnTitle', {
- defaultMessage: 'Description',
- }),
- getTableNameColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadTemplates.table.nameColumnTitle', {
- defaultMessage: 'Template name',
- }),
- getTableTagsColumnTitle: () =>
- i18n.translate('xpack.canvas.workpadTemplates.table.tagsColumnTitle', {
- defaultMessage: 'Tags',
- description:
- 'This column contains relevant tags that indicate what type of template ' +
- 'is displayed. For example: "report", "presentation", etc.',
- }),
- getTemplateSearchPlaceholder: () =>
- i18n.translate('xpack.canvas.workpadTemplate.searchPlaceholder', {
- defaultMessage: 'Find template',
- }),
- getCreatingTemplateLabel: (templateName: string) =>
- i18n.translate('xpack.canvas.workpadTemplate.creatingTemplateLabel', {
- defaultMessage: `Creating from template '{templateName}'`,
- values: {
- templateName,
- },
- }),
- },
};
diff --git a/x-pack/plugins/canvas/i18n/errors.ts b/x-pack/plugins/canvas/i18n/errors.ts
index 0928045119234..a55762dce2d20 100644
--- a/x-pack/plugins/canvas/i18n/errors.ts
+++ b/x-pack/plugins/canvas/i18n/errors.ts
@@ -6,7 +6,6 @@
*/
import { i18n } from '@kbn/i18n';
-import { CANVAS, JSON } from './constants';
export const ErrorStrings = {
actionsElements: {
@@ -93,54 +92,10 @@ export const ErrorStrings = {
},
}),
},
- WorkpadFileUpload: {
- getAcceptJSONOnlyErrorMessage: () =>
- i18n.translate('xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage', {
- defaultMessage: 'Only {JSON} files are accepted',
- values: {
- JSON,
- },
- }),
- getFileUploadFailureWithFileNameErrorMessage: (fileName: string) =>
- i18n.translate('xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage', {
- defaultMessage: `Couldn't upload '{fileName}'`,
- values: {
- fileName,
- },
- }),
- getFileUploadFailureWithoutFileNameErrorMessage: () =>
- i18n.translate(
- 'xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage',
- {
- defaultMessage: `Couldn't upload file`,
- }
- ),
- getMissingPropertiesErrorMessage: () =>
- i18n.translate('xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage', {
- defaultMessage:
- 'Some properties required for a {CANVAS} workpad are missing. Edit your {JSON} file to provide the correct property values, and try again.',
- values: {
- CANVAS,
- JSON,
- },
- }),
- },
- WorkpadLoader: {
- getCloneFailureErrorMessage: () =>
- i18n.translate('xpack.canvas.error.workpadLoader.cloneFailureErrorMessage', {
- defaultMessage: `Couldn't clone workpad`,
- }),
- getDeleteFailureErrorMessage: () =>
- i18n.translate('xpack.canvas.error.workpadLoader.deleteFailureErrorMessage', {
- defaultMessage: `Couldn't delete all workpads`,
- }),
- getFindFailureErrorMessage: () =>
- i18n.translate('xpack.canvas.error.workpadLoader.findFailureErrorMessage', {
- defaultMessage: `Couldn't find workpad`,
- }),
- getUploadFailureErrorMessage: () =>
- i18n.translate('xpack.canvas.error.workpadLoader.uploadFailureErrorMessage', {
- defaultMessage: `Couldn't upload workpad`,
+ WorkpadDropzone: {
+ getTooManyFilesErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.workpadDropzone.tooManyFilesErrorMessage', {
+ defaultMessage: 'One one file can be uploaded at a time',
}),
},
workpadRoutes: {
diff --git a/x-pack/plugins/canvas/public/components/home/home.component.tsx b/x-pack/plugins/canvas/public/components/home/home.component.tsx
new file mode 100644
index 0000000000000..96a773186da2b
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/home.component.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { KibanaPageTemplate } from '../../../../../../src/plugins/kibana_react/public';
+import { withSuspense } from '../../../../../../src/plugins/presentation_util/public';
+
+import { WorkpadCreate } from './workpad_create';
+import { LazyWorkpadTemplates } from './workpad_templates';
+import { LazyMyWorkpads } from './my_workpads';
+
+export type HomePageTab = 'workpads' | 'templates';
+
+export interface Props {
+ activeTab?: HomePageTab;
+}
+
+const WorkpadTemplates = withSuspense(LazyWorkpadTemplates);
+const MyWorkpads = withSuspense(LazyMyWorkpads);
+
+export const Home = ({ activeTab = 'workpads' }: Props) => {
+ const [tab, setTab] = useState(activeTab);
+
+ return (
+ ],
+ bottomBorder: true,
+ tabs: [
+ {
+ label: strings.getMyWorkpadsTabLabel(),
+ id: 'myWorkpads',
+ isSelected: tab === 'workpads',
+ onClick: () => setTab('workpads'),
+ },
+ {
+ label: strings.getWorkpadTemplatesTabLabel(),
+ id: 'workpadTemplates',
+ 'data-test-subj': 'workpadTemplates',
+ isSelected: tab === 'templates',
+ onClick: () => setTab('templates'),
+ },
+ ],
+ }}
+ >
+ {tab === 'workpads' ? : }
+
+ );
+};
+
+const strings = {
+ getMyWorkpadsTabLabel: () =>
+ i18n.translate('xpack.canvas.home.myWorkpadsTabLabel', {
+ defaultMessage: 'My workpads',
+ }),
+ getWorkpadTemplatesTabLabel: () =>
+ i18n.translate('xpack.canvas.home.workpadTemplatesTabLabel', {
+ defaultMessage: 'Templates',
+ description: 'The label for the tab that displays a list of designed workpad templates.',
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/home.stories.tsx b/x-pack/plugins/canvas/public/components/home/home.stories.tsx
new file mode 100644
index 0000000000000..186b916afa003
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/home.stories.tsx
@@ -0,0 +1,30 @@
+/*
+ * 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 {
+ reduxDecorator,
+ getAddonPanelParameters,
+ servicesContextDecorator,
+ getDisableStoryshotsParameter,
+} from '../../../storybook';
+
+import { Home } from './home.component';
+
+export default {
+ title: 'Home/Home Page',
+ argTypes: {},
+ decorators: [reduxDecorator()],
+ parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() },
+};
+
+export const NoContent = () => ;
+export const HasContent = () => ;
+
+NoContent.decorators = [servicesContextDecorator()];
+HasContent.decorators = [servicesContextDecorator({ findWorkpads: 5, findTemplates: true })];
diff --git a/x-pack/plugins/canvas/public/components/home/home.tsx b/x-pack/plugins/canvas/public/components/home/home.tsx
new file mode 100644
index 0000000000000..6b356ada8681e
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/home.tsx
@@ -0,0 +1,33 @@
+/*
+ * 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, { useEffect, useState } from 'react';
+import { useDispatch } from 'react-redux';
+
+import { getBaseBreadcrumb } from '../../lib/breadcrumbs';
+import { resetWorkpad } from '../../state/actions/workpad';
+import { Home as Component } from './home.component';
+import { usePlatformService } from '../../services';
+
+export const Home = () => {
+ const { setBreadcrumbs } = usePlatformService();
+ const [isMounted, setIsMounted] = useState(false);
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ if (!isMounted) {
+ dispatch(resetWorkpad());
+ setIsMounted(true);
+ }
+ }, [dispatch, isMounted, setIsMounted]);
+
+ useEffect(() => {
+ setBreadcrumbs([getBaseBreadcrumb()]);
+ }, [setBreadcrumbs]);
+
+ return ;
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/index.ts b/x-pack/plugins/canvas/public/components/home/hooks/index.ts
new file mode 100644
index 0000000000000..91e52948a7ba6
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/index.ts
@@ -0,0 +1,15 @@
+/*
+ * 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.
+ */
+
+export { useCloneWorkpad } from './use_clone_workpad';
+export { useCreateWorkpad } from './use_create_workpad';
+export { useDeleteWorkpads } from './use_delete_workpad';
+export { useDownloadWorkpad } from './use_download_workpad';
+export { useFindTemplates, useFindTemplatesOnMount } from './use_find_templates';
+export { useFindWorkpads, useFindWorkpadsOnMount } from './use_find_workpad';
+export { useImportWorkpad } from './use_upload_workpad';
+export { useCreateFromTemplate } from './use_create_from_template';
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts
new file mode 100644
index 0000000000000..001a711a58a72
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts
@@ -0,0 +1,60 @@
+/*
+ * 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 { useCallback } from 'react';
+import { useHistory } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+
+import { useNotifyService, useWorkpadService } from '../../../services';
+import { getId } from '../../../lib/get_id';
+
+export const useCloneWorkpad = () => {
+ const workpadService = useWorkpadService();
+ const notifyService = useNotifyService();
+ const history = useHistory();
+
+ return useCallback(
+ async (workpadId: string) => {
+ try {
+ let workpad = await workpadService.get(workpadId);
+
+ workpad = {
+ ...workpad,
+ name: strings.getClonedWorkpadName(workpad.name),
+ id: getId('workpad'),
+ };
+
+ await workpadService.create(workpad);
+
+ history.push(`/workpad/${workpad.id}/page/1`);
+ } catch (err) {
+ notifyService.error(err, { title: errors.getCloneFailureErrorMessage() });
+ }
+ },
+ [notifyService, workpadService, history]
+ );
+};
+
+const strings = {
+ getClonedWorkpadName: (workpadName: string) =>
+ i18n.translate('xpack.canvas.useCloneWorkpad.clonedWorkpadName', {
+ defaultMessage: 'Copy of {workpadName}',
+ values: {
+ workpadName,
+ },
+ description:
+ 'This suffix is added to the end of the name of a cloned workpad to indicate that this ' +
+ 'new workpad is a copy of the original workpad. Example: "Copy of Sales Pitch"',
+ }),
+};
+
+const errors = {
+ getCloneFailureErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage', {
+ defaultMessage: `Couldn't clone workpad`,
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts
new file mode 100644
index 0000000000000..968f9398ba857
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 { useCallback } from 'react';
+import { useHistory } from 'react-router-dom';
+
+import { CanvasTemplate } from '../../../../types';
+import { useNotifyService, useWorkpadService } from '../../../services';
+
+export const useCreateFromTemplate = () => {
+ const workpadService = useWorkpadService();
+ const notifyService = useNotifyService();
+ const history = useHistory();
+
+ return useCallback(
+ async (template: CanvasTemplate) => {
+ try {
+ const result = await workpadService.createFromTemplate(template.id);
+ history.push(`/workpad/${result.id}/page/1`);
+ } catch (e) {
+ notifyService.error(e, {
+ title: `Couldn't create workpad from template`,
+ });
+ }
+ },
+ [workpadService, notifyService, history]
+ );
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts
new file mode 100644
index 0000000000000..eb87f4720deec
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts
@@ -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 { useCallback } from 'react';
+import { useHistory } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+
+// @ts-expect-error
+import { getDefaultWorkpad } from '../../../state/defaults';
+import { useNotifyService, useWorkpadService } from '../../../services';
+
+import type { CanvasWorkpad } from '../../../../types';
+
+export const useCreateWorkpad = () => {
+ const workpadService = useWorkpadService();
+ const notifyService = useNotifyService();
+ const history = useHistory();
+
+ return useCallback(
+ async (_workpad?: CanvasWorkpad | null) => {
+ const workpad = _workpad || (getDefaultWorkpad() as CanvasWorkpad);
+
+ try {
+ await workpadService.create(workpad);
+ history.push(`/workpad/${workpad.id}/page/1`);
+ } catch (err) {
+ notifyService.error(err, {
+ title: errors.getUploadFailureErrorMessage(),
+ });
+ }
+ return;
+ },
+ [notifyService, history, workpadService]
+ );
+};
+
+const errors = {
+ getUploadFailureErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage', {
+ defaultMessage: `Couldn't upload workpad`,
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts
new file mode 100644
index 0000000000000..722ddae7411c9
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts
@@ -0,0 +1,63 @@
+/*
+ * 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 { useCallback } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { useNotifyService, useWorkpadService } from '../../../services';
+
+export const useDeleteWorkpads = () => {
+ const workpadService = useWorkpadService();
+ const notifyService = useNotifyService();
+
+ return useCallback(
+ async (workpadIds: string[]) => {
+ const removedWorkpads = workpadIds.map(async (id) => {
+ try {
+ await workpadService.remove(id);
+ return { id, err: null };
+ } catch (err) {
+ return { id, err };
+ }
+ });
+
+ return Promise.all(removedWorkpads).then((results) => {
+ const [passes, errored] = results.reduce<[string[], string[]]>(
+ ([passesArr, errorsArr], result) => {
+ if (result.err) {
+ errorsArr.push(result.id);
+ } else {
+ passesArr.push(result.id);
+ }
+
+ return [passesArr, errorsArr];
+ },
+ [[], []]
+ );
+
+ const removedIds = workpadIds.filter((id) => passes.includes(id));
+
+ if (errored.length > 0) {
+ notifyService.error(errors.getDeleteFailureErrorMessage());
+ }
+
+ return {
+ removedIds,
+ errored,
+ };
+ });
+ },
+ [workpadService, notifyService]
+ );
+};
+
+const errors = {
+ getDeleteFailureErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage', {
+ defaultMessage: `Couldn't delete all workpads`,
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts
new file mode 100644
index 0000000000000..b875e08c2a230
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { useCallback } from 'react';
+import { downloadWorkpad as downloadWorkpadFn } from '../../../lib/download_workpad';
+
+export const useDownloadWorkpad = () =>
+ useCallback((workpadId: string) => downloadWorkpadFn(workpadId), []);
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts
new file mode 100644
index 0000000000000..13ee289fe9867
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts
@@ -0,0 +1,38 @@
+/*
+ * 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 { useState, useCallback } from 'react';
+import useMount from 'react-use/lib/useMount';
+
+import { useWorkpadService } from '../../../services';
+import { TemplateFindResponse } from '../../../services/workpad';
+
+const emptyResponse = { templates: [] };
+
+export const useFindTemplates = () => {
+ const workpadService = useWorkpadService();
+ return useCallback(async () => await workpadService.findTemplates(), [workpadService]);
+};
+
+export const useFindTemplatesOnMount = (): [boolean, TemplateFindResponse] => {
+ const [isMounted, setIsMounted] = useState(false);
+ const findTemplates = useFindTemplates();
+ const [templateResponse, setTemplateResponse] = useState(emptyResponse);
+
+ const fetchTemplates = useCallback(async () => {
+ const foundTemplates = await findTemplates();
+ setTemplateResponse(foundTemplates || emptyResponse);
+ setIsMounted(true);
+ }, [findTemplates]);
+
+ useMount(() => {
+ fetchTemplates();
+ return () => setIsMounted(false);
+ });
+
+ return [isMounted, templateResponse];
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts
new file mode 100644
index 0000000000000..3f8b0e6f630f5
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 { useState, useCallback } from 'react';
+import useMount from 'react-use/lib/useMount';
+import { i18n } from '@kbn/i18n';
+
+import { WorkpadFindResponse } from '../../../services/workpad';
+
+import { useNotifyService, useWorkpadService } from '../../../services';
+const emptyResponse = { total: 0, workpads: [] };
+
+export const useFindWorkpads = () => {
+ const workpadService = useWorkpadService();
+ const notifyService = useNotifyService();
+
+ return useCallback(
+ async (text = '') => {
+ try {
+ return await workpadService.find(text);
+ } catch (err) {
+ notifyService.error(err, { title: errors.getFindFailureErrorMessage() });
+ }
+ },
+ [notifyService, workpadService]
+ );
+};
+
+export const useFindWorkpadsOnMount = (): [boolean, WorkpadFindResponse] => {
+ const [isMounted, setIsMounted] = useState(false);
+ const findWorkpads = useFindWorkpads();
+ const [workpadResponse, setWorkpadResponse] = useState(emptyResponse);
+
+ const fetchWorkpads = useCallback(async () => {
+ const foundWorkpads = await findWorkpads();
+ setWorkpadResponse(foundWorkpads || emptyResponse);
+ setIsMounted(true);
+ }, [findWorkpads]);
+
+ useMount(() => {
+ fetchWorkpads();
+ return () => setIsMounted(false);
+ });
+
+ return [isMounted, workpadResponse];
+};
+
+const errors = {
+ getFindFailureErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.useFindWorkpads.findFailureErrorMessage', {
+ defaultMessage: `Couldn't find workpad`,
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts
new file mode 100644
index 0000000000000..7934a469bb7a2
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts
@@ -0,0 +1,100 @@
+/*
+ * 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 { useCallback } from 'react';
+import { get } from 'lodash';
+import { i18n } from '@kbn/i18n';
+
+import { CANVAS, JSON as JSONString } from '../../../../i18n/constants';
+import { useNotifyService } from '../../../services';
+import { getId } from '../../../lib/get_id';
+import type { CanvasWorkpad } from '../../../../types';
+
+export const useImportWorkpad = () => {
+ const notifyService = useNotifyService();
+
+ return useCallback(
+ (file?: File, onComplete: (workpad?: CanvasWorkpad) => void = () => {}) => {
+ if (!file) {
+ onComplete();
+ return;
+ }
+
+ if (get(file, 'type') !== 'application/json') {
+ notifyService.warning(errors.getAcceptJSONOnlyErrorMessage(), {
+ title: file.name
+ ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
+ : errors.getFileUploadFailureWithoutFileNameErrorMessage(),
+ });
+ onComplete();
+ }
+
+ // TODO: Clean up this file, this loading stuff can, and should be, abstracted
+ const reader = new FileReader();
+
+ // handle reading the uploaded file
+ reader.onload = () => {
+ try {
+ const workpad = JSON.parse(reader.result as string); // Type-casting because we catch below.
+ workpad.id = getId('workpad');
+
+ // sanity check for workpad object
+ if (!Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets) {
+ onComplete();
+ throw new Error(errors.getMissingPropertiesErrorMessage());
+ }
+
+ onComplete(workpad);
+ } catch (e) {
+ notifyService.error(e, {
+ title: file.name
+ ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
+ : errors.getFileUploadFailureWithoutFileNameErrorMessage(),
+ });
+ onComplete();
+ }
+ };
+
+ // read the uploaded file
+ reader.readAsText(file);
+ },
+ [notifyService]
+ );
+};
+
+const errors = {
+ getFileUploadFailureWithoutFileNameErrorMessage: () =>
+ i18n.translate(
+ 'xpack.canvas.error.useImportWorkpad.fileUploadFailureWithoutFileNameErrorMessage',
+ {
+ defaultMessage: `Couldn't upload file`,
+ }
+ ),
+ getFileUploadFailureWithFileNameErrorMessage: (fileName: string) =>
+ i18n.translate('xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage', {
+ defaultMessage: `Couldn't upload '{fileName}'`,
+ values: {
+ fileName,
+ },
+ }),
+ getMissingPropertiesErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage', {
+ defaultMessage:
+ 'Some properties required for a {CANVAS} workpad are missing. Edit your {JSON} file to provide the correct property values, and try again.',
+ values: {
+ CANVAS,
+ JSON: JSONString,
+ },
+ }),
+ getAcceptJSONOnlyErrorMessage: () =>
+ i18n.translate('xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage', {
+ defaultMessage: 'Only {JSON} files are accepted',
+ values: {
+ JSON: JSONString,
+ },
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/workpad_manager/index.js b/x-pack/plugins/canvas/public/components/home/index.ts
similarity index 83%
rename from x-pack/plugins/canvas/public/components/workpad_manager/index.js
rename to x-pack/plugins/canvas/public/components/home/index.ts
index e1f5855e762af..aeb62c3a8de78 100644
--- a/x-pack/plugins/canvas/public/components/workpad_manager/index.js
+++ b/x-pack/plugins/canvas/public/components/home/index.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export { WorkpadManager } from './workpad_manager';
+export { Home } from './home';
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx
new file mode 100644
index 0000000000000..aef1b0625b585
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx
@@ -0,0 +1,19 @@
+/*
+ * 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 { HomeEmptyPrompt } from './empty_prompt';
+import { getDisableStoryshotsParameter } from '../../../../storybook';
+
+export default {
+ title: 'Home/Empty Prompt',
+ argTypes: {},
+ parameters: { ...getDisableStoryshotsParameter() },
+};
+
+export const EmptyPrompt = () => ;
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx
new file mode 100644
index 0000000000000..797f50ac112d0
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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, { Fragment } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiEmptyPrompt, EuiLink, EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+import { CANVAS, JSON } from '../../../../i18n/constants';
+
+export const HomeEmptyPrompt = () => (
+
+
+
+ {strings.getEmptyPromptTitle()}}
+ titleSize="m"
+ body={
+
+ {strings.getEmptyPromptGettingStartedDescription()}
+
+ {strings.getEmptyPromptNewUserDescription()}{' '}
+
+ {strings.getSampleDataLinkLabel()}
+
+ .
+
+
+ }
+ />
+
+
+
+);
+
+const strings = {
+ getEmptyPromptGettingStartedDescription: () =>
+ i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptGettingStartedDescription', {
+ defaultMessage:
+ 'Create a new workpad, start from a template, or import a workpad {JSON} file by dropping it here.',
+ values: {
+ JSON,
+ },
+ }),
+ getEmptyPromptNewUserDescription: () =>
+ i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptNewUserDescription', {
+ defaultMessage: 'New to {CANVAS}?',
+ values: {
+ CANVAS,
+ },
+ }),
+ getEmptyPromptTitle: () =>
+ i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptTitle', {
+ defaultMessage: 'Add your first workpad',
+ }),
+ getSampleDataLinkLabel: () =>
+ i18n.translate('xpack.canvas.homeEmptyPrompt.sampleDataLinkLabel', {
+ defaultMessage: 'Add your first workpad',
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/index.ts b/x-pack/plugins/canvas/public/components/home/my_workpads/index.ts
new file mode 100644
index 0000000000000..79b1519df90fe
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/index.ts
@@ -0,0 +1,10 @@
+/*
+ * 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';
+
+export const LazyMyWorkpads = React.lazy(() => import('./my_workpads'));
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx
new file mode 100644
index 0000000000000..28edfea7c36ca
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx
@@ -0,0 +1,17 @@
+/*
+ * 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 { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+export const Loading = () => (
+
+
+
+
+
+);
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx
new file mode 100644
index 0000000000000..d9e3f0e4e2c99
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx
@@ -0,0 +1,38 @@
+/*
+ * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+import { FoundWorkpad } from '../../../services/workpad';
+import { UploadDropzone } from './upload_dropzone';
+import { HomeEmptyPrompt } from './empty_prompt';
+import { WorkpadTable } from './workpad_table';
+
+export interface Props {
+ workpads: FoundWorkpad[];
+}
+
+export const MyWorkpads = ({ workpads }: Props) => {
+ if (workpads.length === 0) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx
new file mode 100644
index 0000000000000..0d5d6ca16f614
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx
@@ -0,0 +1,56 @@
+/*
+ * 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, { useState } from 'react';
+import { EuiPanel } from '@elastic/eui';
+
+import {
+ reduxDecorator,
+ getAddonPanelParameters,
+ servicesContextDecorator,
+ getDisableStoryshotsParameter,
+} from '../../../../storybook';
+import { getSomeWorkpads } from '../../../services/stubs/workpad';
+
+import { MyWorkpads, WorkpadsContext } from './my_workpads';
+import { MyWorkpads as MyWorkpadsComponent } from './my_workpads.component';
+
+export default {
+ title: 'Home/My Workpads',
+ argTypes: {},
+ decorators: [reduxDecorator()],
+ parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() },
+};
+
+export const NoWorkpads = () => {
+ return ;
+};
+
+export const HasWorkpads = () => {
+ return (
+
+
+
+ );
+};
+
+NoWorkpads.decorators = [servicesContextDecorator()];
+HasWorkpads.decorators = [servicesContextDecorator({ findWorkpads: 5 })];
+
+export const Component = ({ workpadCount }: { workpadCount: number }) => {
+ const [workpads, setWorkpads] = useState(getSomeWorkpads(workpadCount));
+
+ return (
+
+
+
+
+
+ );
+};
+
+Component.args = { workpadCount: 5 };
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx
new file mode 100644
index 0000000000000..4242e2e9d130f
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx
@@ -0,0 +1,42 @@
+/*
+ * 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, { useState, useEffect, createContext, Dispatch, SetStateAction } from 'react';
+import { useFindWorkpadsOnMount } from './../hooks';
+import { FoundWorkpad } from '../../../services/workpad';
+import { Loading } from './loading';
+import { MyWorkpads as Component } from './my_workpads.component';
+
+interface Context {
+ workpads: FoundWorkpad[];
+ setWorkpads: Dispatch>;
+}
+
+export const WorkpadsContext = createContext(null);
+
+export const MyWorkpads = () => {
+ const [isMounted, workpadResponse] = useFindWorkpadsOnMount();
+ const [workpads, setWorkpads] = useState(workpadResponse.workpads);
+
+ useEffect(() => {
+ setWorkpads(workpadResponse.workpads);
+ }, [workpadResponse]);
+
+ if (!isMounted) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+};
+
+// required for dynamic import using React.lazy()
+// eslint-disable-next-line import/no-default-export
+export default MyWorkpads;
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx
new file mode 100644
index 0000000000000..603f4679a9e95
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx
@@ -0,0 +1,30 @@
+/*
+ * 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, { FC } from 'react';
+// @ts-expect-error untyped library
+import Dropzone from 'react-dropzone';
+
+import './upload_dropzone.scss';
+
+export interface Props {
+ disabled?: boolean;
+ onDrop?: (files: FileList) => void;
+}
+
+export const UploadDropzone: FC = ({ onDrop = () => {}, disabled, children }) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss
new file mode 100644
index 0000000000000..e4ee284c72dee
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss
@@ -0,0 +1,8 @@
+.canvasWorkpad__dropzone {
+ border: 2px dashed transparent;
+}
+
+.canvasWorkpad__dropzone--active {
+ background-color: $euiColorLightestShade;
+ border-color: $euiColorLightShade;
+}
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx
new file mode 100644
index 0000000000000..8ee0ae108392e
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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, { FC, useState } from 'react';
+// @ts-expect-error untyped library
+import Dropzone from 'react-dropzone';
+
+import { useNotifyService } from '../../../services';
+import { ErrorStrings } from '../../../../i18n';
+import { useImportWorkpad, useCreateWorkpad } from '../hooks';
+import { CanvasWorkpad } from '../../../../types';
+
+import { UploadDropzone as Component } from './upload_dropzone.component';
+
+const { WorkpadDropzone: errors } = ErrorStrings;
+
+export const UploadDropzone: FC = ({ children }) => {
+ const notify = useNotifyService();
+ const uploadWorkpad = useImportWorkpad();
+ const createWorkpad = useCreateWorkpad();
+ const [isDisabled, setIsDisabled] = useState(false);
+
+ const onComplete = async (workpad?: CanvasWorkpad) => {
+ if (!workpad) {
+ setIsDisabled(false);
+ return;
+ }
+
+ await createWorkpad(workpad);
+ };
+
+ const onDrop = (files: FileList) => {
+ if (!files) {
+ return;
+ }
+
+ if (files.length > 1) {
+ notify.warning(errors.getTooManyFilesErrorMessage());
+ return;
+ }
+
+ setIsDisabled(true);
+ uploadWorkpad(files[0], onComplete);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx
new file mode 100644
index 0000000000000..28e2aa0449d46
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { EuiFilePicker, EuiFilePickerProps } from '@elastic/eui';
+
+import { JSON } from '../../../../i18n/constants';
+export interface Props {
+ canUserWrite: boolean;
+ onImportWorkpad?: EuiFilePickerProps['onChange'];
+ uniqueKey?: string | number;
+}
+
+export const WorkpadImport = ({ uniqueKey, canUserWrite, onImportWorkpad = () => {} }: Props) => (
+
+);
+
+const strings = {
+ getFilePickerPlaceholder: () =>
+ i18n.translate('xpack.canvas.workpadImport.filePickerPlaceholder', {
+ defaultMessage: 'Import workpad {JSON} file',
+ values: {
+ JSON,
+ },
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx
new file mode 100644
index 0000000000000..0f1ba621e14d7
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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, { useState } from 'react';
+import { useSelector } from 'react-redux';
+
+import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app';
+import type { State } from '../../../../types';
+
+import { useImportWorkpad } from '../hooks';
+import { WorkpadImport as Component, Props as ComponentProps } from './workpad_import.component';
+
+type Props = Omit;
+
+export const WorkpadImport = (props: Props) => {
+ const importWorkpad = useImportWorkpad();
+ const [uniqueKey, setUniqueKey] = useState(Date.now());
+
+ const { canUserWrite } = useSelector((state: State) => ({
+ canUserWrite: canUserWriteSelector(state),
+ }));
+
+ const onImportWorkpad: ComponentProps['onImportWorkpad'] = (files) => {
+ if (files) {
+ importWorkpad(files[0]);
+ }
+ setUniqueKey(Date.now());
+ };
+
+ return ;
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx
new file mode 100644
index 0000000000000..5301a88844369
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx
@@ -0,0 +1,203 @@
+/*
+ * 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, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiInMemoryTable,
+ EuiInMemoryTableProps,
+ EuiTableActionsColumnType,
+ EuiBasicTableColumn,
+ EuiToolTip,
+ EuiButtonIcon,
+ EuiTableSelectionType,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+import moment from 'moment';
+
+import { RoutingLink } from '../../routing';
+import { FoundWorkpad } from '../../../services/workpad';
+import { WorkpadTableTools } from './workpad_table_tools';
+import { WorkpadImport } from './workpad_import';
+
+export interface Props {
+ workpads: FoundWorkpad[];
+ canUserWrite: boolean;
+ dateFormat: string;
+ onExportWorkpad: (ids: string) => void;
+ onCloneWorkpad: (id: string) => void;
+}
+
+const getDisplayName = (name: string, workpadId: string, loadedWorkpadId?: string) => {
+ const workpadName = name.length ? {name} : {workpadId} ;
+ return workpadId === loadedWorkpadId ? {workpadName} : workpadName;
+};
+
+export const WorkpadTable = ({
+ workpads,
+ canUserWrite,
+ dateFormat,
+ onExportWorkpad: onExport,
+ onCloneWorkpad,
+}: Props) => {
+ const [selectedIds, setSelectedIds] = useState([]);
+ const formatDate = (date: string) => date && moment(date).format(dateFormat);
+
+ const selection: EuiTableSelectionType = {
+ onSelectionChange: (selectedWorkpads) => {
+ setSelectedIds(selectedWorkpads.map((workpad) => workpad.id).filter((id) => !!id));
+ },
+ };
+
+ const actions: EuiTableActionsColumnType['actions'] = [
+ {
+ render: (workpad: FoundWorkpad) => (
+
+
+
+ onExport(workpad.id)}
+ aria-label={strings.getExportToolTip()}
+ />
+
+
+
+
+ onCloneWorkpad(workpad.id)}
+ aria-label={strings.getCloneToolTip()}
+ disabled={!canUserWrite}
+ />
+
+
+
+ ),
+ },
+ ];
+
+ const search: EuiInMemoryTableProps['search'] = {
+ toolsLeft:
+ selectedIds.length > 0 ? : undefined,
+ toolsRight: ,
+ box: {
+ schema: true,
+ incremental: true,
+ placeholder: strings.getWorkpadSearchPlaceholder(),
+ 'data-test-subj': 'tableListSearchBox',
+ },
+ };
+
+ const columns: Array> = [
+ {
+ field: 'name',
+ name: strings.getTableNameColumnTitle(),
+ sortable: true,
+ dataType: 'string',
+ render: (name, workpad) => (
+
+ {getDisplayName(name, workpad.id)}
+
+ ),
+ },
+ {
+ field: '@created',
+ name: strings.getTableCreatedColumnTitle(),
+ sortable: true,
+ dataType: 'date',
+ width: '20%',
+ render: (date: string) => formatDate(date),
+ },
+ {
+ field: '@timestamp',
+ name: strings.getTableUpdatedColumnTitle(),
+ sortable: true,
+ dataType: 'date',
+ width: '20%',
+ render: (date: string) => formatDate(date),
+ },
+ { name: strings.getTableActionsColumnTitle(), actions, width: '100px' },
+ ];
+
+ return (
+
+ );
+};
+
+const strings = {
+ getCloneToolTip: () =>
+ i18n.translate('xpack.canvas.workpadTable.cloneTooltip', {
+ defaultMessage: 'Clone workpad',
+ }),
+ getExportToolTip: () =>
+ i18n.translate('xpack.canvas.workpadTable.exportTooltip', {
+ defaultMessage: 'Export workpad',
+ }),
+ getLoadWorkpadArialLabel: (workpadName: string) =>
+ i18n.translate('xpack.canvas.workpadTable.loadWorkpadArialLabel', {
+ defaultMessage: `Load workpad '{workpadName}'`,
+ values: {
+ workpadName,
+ },
+ }),
+ getNoPermissionToCloneToolTip: () =>
+ i18n.translate('xpack.canvas.workpadTable.noPermissionToCloneToolTip', {
+ defaultMessage: `You don't have permission to clone workpads`,
+ }),
+ getNoWorkpadsFoundMessage: () =>
+ i18n.translate('xpack.canvas.workpadTable.noWorkpadsFoundMessage', {
+ defaultMessage: 'No workpads matched your search.',
+ }),
+ getWorkpadSearchPlaceholder: () =>
+ i18n.translate('xpack.canvas.workpadTable.searchPlaceholder', {
+ defaultMessage: 'Find workpad',
+ }),
+ getTableCreatedColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTable.table.createdColumnTitle', {
+ defaultMessage: 'Created',
+ description: 'This column in the table contains the date/time the workpad was created.',
+ }),
+ getTableNameColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTable.table.nameColumnTitle', {
+ defaultMessage: 'Workpad name',
+ }),
+ getTableUpdatedColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTable.table.updatedColumnTitle', {
+ defaultMessage: 'Updated',
+ description: 'This column in the table contains the date/time the workpad was last updated.',
+ }),
+ getTableActionsColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTable.table.actionsColumnTitle', {
+ defaultMessage: 'Actions',
+ description: 'This column in the table contains the actions that can be taken on a workpad.',
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx
new file mode 100644
index 0000000000000..501a0a76a8589
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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, { useState, useEffect } from 'react';
+import { EuiPanel } from '@elastic/eui';
+
+import { action } from '@storybook/addon-actions';
+import {
+ reduxDecorator,
+ getAddonPanelParameters,
+ getDisableStoryshotsParameter,
+} from '../../../../storybook';
+import { getSomeWorkpads } from '../../../services/stubs/workpad';
+
+import { WorkpadTable } from './workpad_table';
+import { WorkpadTable as WorkpadTableComponent } from './workpad_table.component';
+import { WorkpadsContext } from './my_workpads';
+
+export default {
+ title: 'Home/Workpad Table',
+ argTypes: {},
+ decorators: [reduxDecorator()],
+ parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() },
+};
+
+export const NoWorkpads = () => {
+ const [workpads, setWorkpads] = useState(getSomeWorkpads(0));
+
+ return (
+
+
+
+
+
+ );
+};
+
+export const HasWorkpads = () => {
+ const [workpads, setWorkpads] = useState(getSomeWorkpads(5));
+
+ return (
+
+
+
+
+
+ );
+};
+
+export const Component = ({
+ workpadCount,
+ canUserWrite,
+ dateFormat,
+}: {
+ workpadCount: number;
+ canUserWrite: boolean;
+ dateFormat: string;
+}) => {
+ const [workpads, setWorkpads] = useState(getSomeWorkpads(workpadCount));
+
+ useEffect(() => {
+ setWorkpads(getSomeWorkpads(workpadCount));
+ }, [workpadCount]);
+
+ return (
+
+
+
+
+
+ );
+};
+
+Component.args = { workpadCount: 5, canUserWrite: true, dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS' };
+Component.argTypes = {};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx
new file mode 100644
index 0000000000000..e5d83039a87eb
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx
@@ -0,0 +1,38 @@
+/*
+ * 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, { useContext } from 'react';
+import { useSelector } from 'react-redux';
+
+import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app';
+import type { State } from '../../../../types';
+import { usePlatformService } from '../../../services';
+import { useCloneWorkpad, useDownloadWorkpad } from '../hooks';
+
+import { WorkpadTable as Component } from './workpad_table.component';
+import { WorkpadsContext } from './my_workpads';
+
+export const WorkpadTable = () => {
+ const platformService = usePlatformService();
+ const onCloneWorkpad = useCloneWorkpad();
+ const onExportWorkpad = useDownloadWorkpad();
+ const context = useContext(WorkpadsContext);
+
+ const { canUserWrite } = useSelector((state: State) => ({
+ canUserWrite: canUserWriteSelector(state),
+ }));
+
+ if (!context) {
+ return null;
+ }
+
+ const { workpads } = context;
+
+ const dateFormat = platformService.getUISetting('dateFormat');
+
+ return ;
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx
new file mode 100644
index 0000000000000..ae6ff9c3cc910
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx
@@ -0,0 +1,160 @@
+/*
+ * 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, { useState, Fragment } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiButton, EuiToolTip, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
+
+import { ConfirmModal } from '../../confirm_modal';
+import { FoundWorkpad } from '../../../services/workpad';
+
+export interface Props {
+ workpads: FoundWorkpad[];
+ canUserWrite: boolean;
+ selectedWorkpadIds: string[];
+ onDeleteWorkpads: (ids: string[]) => void;
+ onExportWorkpads: (ids: string[]) => void;
+}
+
+export const WorkpadTableTools = ({
+ workpads,
+ canUserWrite,
+ selectedWorkpadIds,
+ onDeleteWorkpads,
+ onExportWorkpads,
+}: Props) => {
+ const [isDeletePending, setIsDeletePending] = useState(false);
+
+ const openRemoveConfirm = () => setIsDeletePending(true);
+ const closeRemoveConfirm = () => setIsDeletePending(false);
+
+ let deleteButton = (
+
+ {strings.getDeleteButtonLabel(selectedWorkpadIds.length)}
+
+ );
+
+ const downloadButton = (
+ onExportWorkpads(selectedWorkpadIds)}
+ iconType="exportAction"
+ aria-label={strings.getExportButtonAriaLabel(selectedWorkpadIds.length)}
+ >
+ {strings.getExportButtonLabel(selectedWorkpadIds.length)}
+
+ );
+
+ if (!canUserWrite) {
+ deleteButton = (
+ {deleteButton}
+ );
+ }
+
+ const modalTitle =
+ selectedWorkpadIds.length === 1
+ ? strings.getDeleteSingleWorkpadModalTitle(
+ workpads.find((workpad) => workpad.id === selectedWorkpadIds[0])?.name || ''
+ )
+ : strings.getDeleteMultipleWorkpadModalTitle(selectedWorkpadIds.length + '');
+
+ const confirmModal = (
+ {
+ onDeleteWorkpads(selectedWorkpadIds);
+ closeRemoveConfirm();
+ }}
+ onCancel={closeRemoveConfirm}
+ />
+ );
+
+ return (
+
+
+ {downloadButton}
+ {deleteButton}
+
+ {confirmModal}
+
+ );
+};
+
+const strings = {
+ getDeleteButtonAriaLabel: (numberOfWorkpads: number) =>
+ i18n.translate('xpack.canvas.workpadTableTools.deleteButtonAriaLabel', {
+ defaultMessage: 'Delete {numberOfWorkpads} workpads',
+ values: {
+ numberOfWorkpads,
+ },
+ }),
+ getDeleteButtonLabel: (numberOfWorkpads: number) =>
+ i18n.translate('xpack.canvas.workpadTableTools.deleteButtonLabel', {
+ defaultMessage: 'Delete ({numberOfWorkpads})',
+ values: {
+ numberOfWorkpads,
+ },
+ }),
+ getDeleteModalConfirmButtonLabel: () =>
+ i18n.translate('xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel', {
+ defaultMessage: 'Delete',
+ }),
+ getDeleteModalDescription: () =>
+ i18n.translate('xpack.canvas.workpadTableTools.deleteModalDescription', {
+ defaultMessage: `You can't recover deleted workpads.`,
+ }),
+ getDeleteMultipleWorkpadModalTitle: (numberOfWorkpads: string) =>
+ i18n.translate('xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle', {
+ defaultMessage: 'Delete {numberOfWorkpads} workpads?',
+ values: {
+ numberOfWorkpads,
+ },
+ }),
+ getDeleteSingleWorkpadModalTitle: (workpadName: string) =>
+ i18n.translate('xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle', {
+ defaultMessage: `Delete workpad '{workpadName}'?`,
+ values: {
+ workpadName,
+ },
+ }),
+ getExportButtonAriaLabel: (numberOfWorkpads: number) =>
+ i18n.translate('xpack.canvas.workpadTableTools.exportButtonAriaLabel', {
+ defaultMessage: 'Export {numberOfWorkpads} workpads',
+ values: {
+ numberOfWorkpads,
+ },
+ }),
+ getExportButtonLabel: (numberOfWorkpads: number) =>
+ i18n.translate('xpack.canvas.workpadTableTools.exportButtonLabel', {
+ defaultMessage: 'Export ({numberOfWorkpads})',
+ values: {
+ numberOfWorkpads,
+ },
+ }),
+ getNoPermissionToCreateToolTip: () =>
+ i18n.translate('xpack.canvas.workpadTableTools.noPermissionToCreateToolTip', {
+ defaultMessage: `You don't have permission to create workpads`,
+ }),
+ getNoPermissionToDeleteToolTip: () =>
+ i18n.translate('xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip', {
+ defaultMessage: `You don't have permission to delete workpads`,
+ }),
+ getNoPermissionToUploadToolTip: () =>
+ i18n.translate('xpack.canvas.workpadTableTools.noPermissionToUploadToolTip', {
+ defaultMessage: `You don't have permission to upload workpads`,
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx
new file mode 100644
index 0000000000000..62d84adfc2649
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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, { useContext } from 'react';
+import { useSelector } from 'react-redux';
+
+import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app';
+import type { State } from '../../../../types';
+import { useDeleteWorkpads, useDownloadWorkpad } from '../hooks';
+
+import {
+ WorkpadTableTools as Component,
+ Props as ComponentProps,
+} from './workpad_table_tools.component';
+import { WorkpadsContext } from './my_workpads';
+
+export type Props = Pick;
+
+export const WorkpadTableTools = ({ selectedWorkpadIds }: Props) => {
+ const deleteWorkpads = useDeleteWorkpads();
+ const downloadWorkpad = useDownloadWorkpad();
+ const context = useContext(WorkpadsContext);
+
+ const { canUserWrite } = useSelector((state: State) => ({
+ canUserWrite: canUserWriteSelector(state),
+ }));
+
+ if (context === null || selectedWorkpadIds.length <= 0) {
+ return null;
+ }
+
+ const { workpads, setWorkpads } = context;
+
+ const onExport = () => selectedWorkpadIds.map((id) => downloadWorkpad(id));
+ const onDelete = async () => {
+ const { removedIds } = await deleteWorkpads(selectedWorkpadIds);
+ setWorkpads(workpads.filter((workpad) => !removedIds.includes(workpad.id)));
+ };
+
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx b/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx
new file mode 100644
index 0000000000000..18bdb97683194
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { EuiButton } from '@elastic/eui';
+import { EuiButtonPropsForButton } from '@elastic/eui/src/components/button/button';
+
+export interface Props
+ extends Omit {
+ canUserWrite: boolean;
+}
+
+export const WorkpadCreate = ({ canUserWrite, disabled, ...rest }: Props) => {
+ return (
+
+ {strings.getWorkpadCreateButtonLabel()}
+
+ );
+};
+
+const strings = {
+ getWorkpadCreateButtonLabel: () =>
+ i18n.translate('xpack.canvas.workpadCreate.createButtonLabel', {
+ defaultMessage: 'Create workpad',
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/workpad_create.tsx b/x-pack/plugins/canvas/public/components/home/workpad_create.tsx
new file mode 100644
index 0000000000000..adb73a6bb8896
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/workpad_create.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { useSelector } from 'react-redux';
+
+import { canUserWrite as canUserWriteSelector } from '../../state/selectors/app';
+import type { State } from '../../../types';
+
+import { useCreateWorkpad } from './hooks';
+import { WorkpadCreate as Component, Props as ComponentProps } from './workpad_create.component';
+
+type Props = Omit;
+
+export const WorkpadCreate = (props: Props) => {
+ const createWorkpad = useCreateWorkpad();
+
+ const { canUserWrite } = useSelector((state: State) => ({
+ canUserWrite: canUserWriteSelector(state),
+ }));
+
+ const onClick: ComponentProps['onClick'] = async () => {
+ await createWorkpad();
+ };
+
+ return ;
+};
diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts b/x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts
new file mode 100644
index 0000000000000..4c45dbff38377
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts
@@ -0,0 +1,10 @@
+/*
+ * 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';
+
+export const LazyWorkpadTemplates = React.lazy(() => import('./workpad_templates'));
diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx
new file mode 100644
index 0000000000000..d974c70b05cf2
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx
@@ -0,0 +1,157 @@
+/*
+ * 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 { uniq } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiInMemoryTable,
+ EuiBasicTableColumn,
+ EuiButtonEmpty,
+ EuiSearchBarProps,
+ SearchFilterConfig,
+} from '@elastic/eui';
+
+import { CanvasTemplate } from '../../../../types';
+import { tagsRegistry } from '../../../lib/tags_registry';
+import { TagList } from '../../tag_list';
+
+export interface Props {
+ templates: CanvasTemplate[];
+ onCreateWorkpad: (template: CanvasTemplate) => void;
+}
+
+export const WorkpadTemplates = ({ templates, onCreateWorkpad }: Props) => {
+ const columns: Array> = [
+ {
+ field: 'name',
+ name: strings.getTableNameColumnTitle(),
+ sortable: true,
+ width: '30%',
+ dataType: 'string',
+ render: (name: string, template) => {
+ const templateName = name.length ? name : 'Unnamed Template';
+
+ return (
+ onCreateWorkpad(template)}
+ aria-label={strings.getCloneTemplateLinkAriaLabel(templateName)}
+ type="button"
+ >
+ {templateName}
+
+ );
+ },
+ },
+ {
+ field: 'help',
+ name: strings.getTableDescriptionColumnTitle(),
+ sortable: false,
+ dataType: 'string',
+ width: '30%',
+ },
+ {
+ field: 'tags',
+ name: strings.getTableTagsColumnTitle(),
+ sortable: false,
+ dataType: 'string',
+ width: '30%',
+ render: (tags: string[]) => ,
+ },
+ ];
+
+ let uniqueTagNames: string[] = [];
+
+ templates.forEach((template) => {
+ const { tags } = template;
+ tags.forEach((tag) => uniqueTagNames.push(tag));
+ uniqueTagNames = uniq(uniqueTagNames);
+ });
+
+ const uniqueTags = uniqueTagNames.map(
+ (name) =>
+ tagsRegistry.get(name) || {
+ color: undefined,
+ name,
+ }
+ );
+
+ const filters: SearchFilterConfig[] = [
+ {
+ type: 'field_value_selection',
+ field: 'tags',
+ name: 'Tags',
+ multiSelect: true,
+ options: uniqueTags.map((tag) => ({
+ value: tag.name,
+ name: tag.name,
+ view: ,
+ })),
+ },
+ ];
+
+ const search: EuiSearchBarProps = {
+ box: {
+ incremental: true,
+ schema: true,
+ },
+ filters,
+ };
+
+ return (
+
+ );
+};
+
+const strings = {
+ getCloneTemplateLinkAriaLabel: (templateName: string) =>
+ i18n.translate('xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel', {
+ defaultMessage: `Clone workpad template '{templateName}'`,
+ values: {
+ templateName,
+ },
+ }),
+ getTableDescriptionColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTemplates.table.descriptionColumnTitle', {
+ defaultMessage: 'Description',
+ }),
+ getTableNameColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTemplates.table.nameColumnTitle', {
+ defaultMessage: 'Template name',
+ }),
+ getTableTagsColumnTitle: () =>
+ i18n.translate('xpack.canvas.workpadTemplates.table.tagsColumnTitle', {
+ defaultMessage: 'Tags',
+ description:
+ 'This column contains relevant tags that indicate what type of template ' +
+ 'is displayed. For example: "report", "presentation", etc.',
+ }),
+ getTemplateSearchPlaceholder: () =>
+ i18n.translate('xpack.canvas.workpadTemplates.searchPlaceholder', {
+ defaultMessage: 'Find template',
+ }),
+ getCreatingTemplateLabel: (templateName: string) =>
+ i18n.translate('xpack.canvas.workpadTemplates.creatingTemplateLabel', {
+ defaultMessage: `Creating from template '{templateName}'`,
+ values: {
+ templateName,
+ },
+ }),
+};
diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx
new file mode 100644
index 0000000000000..cb2b872ea15f9
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 { EuiPanel } from '@elastic/eui';
+import { action } from '@storybook/addon-actions';
+import React from 'react';
+
+import {
+ reduxDecorator,
+ getAddonPanelParameters,
+ servicesContextDecorator,
+ getDisableStoryshotsParameter,
+} from '../../../../storybook';
+import { getSomeTemplates } from '../../../services/stubs/workpad';
+
+import { WorkpadTemplates } from './workpad_templates';
+import { WorkpadTemplates as WorkpadTemplatesComponent } from './workpad_templates.component';
+
+export default {
+ title: 'Home/Workpad Templates',
+ argTypes: {},
+ decorators: [reduxDecorator()],
+ parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() },
+};
+
+export const NoTemplates = () => {
+ return (
+
+
+
+ );
+};
+
+export const HasTemplates = () => {
+ return (
+
+
+
+ );
+};
+
+NoTemplates.decorators = [servicesContextDecorator()];
+HasTemplates.decorators = [servicesContextDecorator({ findTemplates: true })];
+
+export const Component = ({ hasTemplates }: { hasTemplates: boolean }) => {
+ return (
+
+
+
+ );
+};
+
+Component.args = {
+ hasTemplates: true,
+};
diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx
new file mode 100644
index 0000000000000..352285e66424b
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
+
+import { useCreateFromTemplate, useFindTemplatesOnMount } from '../hooks';
+
+import { WorkpadTemplates as Component } from './workpad_templates.component';
+
+export const WorkpadTemplates = () => {
+ const [isMounted, templateResponse] = useFindTemplatesOnMount();
+ const onCreateWorkpad = useCreateFromTemplate();
+
+ if (!isMounted) {
+ return (
+
+
+
+
+
+ );
+ }
+ const { templates } = templateResponse;
+
+ return ;
+};
+
+// required for dynamic import using React.lazy()
+// eslint-disable-next-line import/no-default-export
+export default WorkpadTemplates;
diff --git a/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx b/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx
index 712b06cb39299..2e3e826cc32b5 100644
--- a/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx
+++ b/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx
@@ -6,9 +6,7 @@
*/
import React, { FC } from 'react';
-import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui';
-// @ts-expect-error untyped local
-import { WorkpadManager } from '../workpad_manager';
+import { Home } from '../home';
// @ts-expect-error untyped local
import { setDocTitle } from '../../lib/doc_title';
@@ -19,17 +17,5 @@ export interface Props {
export const HomeApp: FC = ({ onLoad = () => {} }) => {
onLoad();
setDocTitle('Canvas');
- return (
-
-
-
- {}} />
-
-
-
- );
+ return ;
};
diff --git a/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx
index e4f297446701c..bd47bb52e0030 100644
--- a/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx
+++ b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx
@@ -18,7 +18,6 @@ storiesOf('components/Toolbar', module)
isWriteable={true}
selectedPageNumber={1}
totalPages={1}
- workpadId={'abc'}
workpadName={'My Canvas Workpad'}
/>
))
@@ -28,7 +27,6 @@ storiesOf('components/Toolbar', module)
selectedElement={getDefaultElement()}
selectedPageNumber={1}
totalPages={1}
- workpadId={'abc'}
workpadName={'My Canvas Workpad'}
/>
));
diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx
index baafbdafcc549..9e89ad4c4f27b 100644
--- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx
+++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx
@@ -7,17 +7,8 @@
import React, { FC, useState, useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiModal,
- EuiModalFooter,
- EuiButton,
-} from '@elastic/eui';
+import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-// @ts-expect-error untyped local
-import { WorkpadManager } from '../workpad_manager';
import { PageManager } from '../page_manager';
import { Expression } from '../expression';
import { Tray } from './tray';
@@ -37,7 +28,6 @@ export interface Props {
selectedElement?: CanvasElement;
selectedPageNumber: number;
totalPages: number;
- workpadId: string;
workpadName: string;
}
@@ -46,11 +36,9 @@ export const Toolbar: FC = ({
selectedElement,
selectedPageNumber,
totalPages,
- workpadId,
workpadName,
}) => {
const [activeTray, setActiveTray] = useState(null);
- const [showWorkpadManager, setShowWorkpadManager] = useState(false);
const { getUrl, previousPage } = useContext(WorkpadRoutingContext);
// While the tray doesn't get activated if the workpad isn't writeable,
@@ -75,20 +63,6 @@ export const Toolbar: FC = ({
}
};
- const closeWorkpadManager = () => setShowWorkpadManager(false);
- const openWorkpadManager = () => setShowWorkpadManager(true);
-
- const workpadManager = (
-
-
-
-
- {strings.getWorkpadManagerCloseButtonLabel()}
-
-
-
- );
-
const trays = {
pageManager: ,
expression: !elementIsSelected ? null : setActiveTray(null)} />,
@@ -99,12 +73,6 @@ export const Toolbar: FC = ({
{activeTray !== null && setActiveTray(null)}>{trays[activeTray]} }
-
- openWorkpadManager()}>
- {workpadName}
-
-
-
= ({
)}
- {showWorkpadManager && workpadManager}
);
};
@@ -153,6 +120,5 @@ Toolbar.propTypes = {
selectedElement: PropTypes.object,
selectedPageNumber: PropTypes.number.isRequired,
totalPages: PropTypes.number.isRequired,
- workpadId: PropTypes.string.isRequired,
workpadName: PropTypes.string.isRequired,
};
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx b/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx
deleted file mode 100644
index 2afd5fe70abe1..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx
+++ /dev/null
@@ -1,173 +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 React, { FC, useState, useCallback } from 'react';
-import { useHistory } from 'react-router-dom';
-import { useSelector } from 'react-redux';
-import moment from 'moment';
-// @ts-expect-error
-import { getDefaultWorkpad } from '../../state/defaults';
-import { canUserWrite as canUserWriteSelector } from '../../state/selectors/app';
-import { getWorkpad } from '../../state/selectors/workpad';
-import { getId } from '../../lib/get_id';
-import { downloadWorkpad } from '../../lib/download_workpad';
-import { ComponentStrings, ErrorStrings } from '../../../i18n';
-import { State, CanvasWorkpad } from '../../../types';
-import { useNotifyService, useWorkpadService, usePlatformService } from '../../services';
-// @ts-expect-error
-import { WorkpadLoader as Component } from './workpad_loader';
-
-const { WorkpadLoader: strings } = ComponentStrings;
-const { WorkpadLoader: errors } = ErrorStrings;
-
-type WorkpadStatePromise = ReturnType['find']>;
-type WorkpadState = WorkpadStatePromise extends PromiseLike ? U : never;
-
-export const WorkpadLoader: FC<{ onClose: () => void }> = ({ onClose }) => {
- const fromState = useSelector((state: State) => ({
- workpadId: getWorkpad(state).id,
- canUserWrite: canUserWriteSelector(state),
- }));
-
- const [workpadsState, setWorkpadsState] = useState(null);
- const workpadService = useWorkpadService();
- const notifyService = useNotifyService();
- const platformService = usePlatformService();
- const history = useHistory();
-
- const createWorkpad = useCallback(
- async (_workpad: CanvasWorkpad | null | undefined) => {
- const workpad = _workpad || getDefaultWorkpad();
- if (workpad != null) {
- try {
- await workpadService.create(workpad);
- history.push(`/workpad/${workpad.id}/page/1`);
- } catch (err) {
- notifyService.error(err, {
- title: errors.getUploadFailureErrorMessage(),
- });
- }
- return;
- }
- },
- [workpadService, notifyService, history]
- );
-
- const findWorkpads = useCallback(
- async (text) => {
- try {
- const fetchedWorkpads = await workpadService.find(text);
- setWorkpadsState(fetchedWorkpads);
- } catch (err) {
- notifyService.error(err, { title: errors.getFindFailureErrorMessage() });
- }
- },
- [notifyService, workpadService]
- );
-
- const onDownloadWorkpad = useCallback((workpadId: string) => downloadWorkpad(workpadId), []);
-
- const cloneWorkpad = useCallback(
- async (workpadId: string) => {
- try {
- const workpad = await workpadService.get(workpadId);
- workpad.name = strings.getClonedWorkpadName(workpad.name);
- workpad.id = getId('workpad');
- await workpadService.create(workpad);
- history.push(`/workpad/${workpad.id}/page/1`);
- } catch (err) {
- notifyService.error(err, { title: errors.getCloneFailureErrorMessage() });
- }
- },
- [notifyService, workpadService, history]
- );
-
- const removeWorkpads = useCallback(
- (workpadIds: string[]) => {
- if (workpadsState === null) {
- return;
- }
-
- const removedWorkpads = workpadIds.map(async (id) => {
- try {
- await workpadService.remove(id);
- return { id, err: null };
- } catch (err) {
- return { id, err };
- }
- });
-
- return Promise.all(removedWorkpads).then((results) => {
- let redirectHome = false;
-
- const [passes, errored] = results.reduce<[string[], string[]]>(
- ([passesArr, errorsArr], result) => {
- if (result.id === fromState.workpadId && !result.err) {
- redirectHome = true;
- }
-
- if (result.err) {
- errorsArr.push(result.id);
- } else {
- passesArr.push(result.id);
- }
-
- return [passesArr, errorsArr];
- },
- [[], []]
- );
-
- const remainingWorkpads = workpadsState.workpads.filter(({ id }) => !passes.includes(id));
-
- const workpadState = {
- total: remainingWorkpads.length,
- workpads: remainingWorkpads,
- };
-
- if (errored.length > 0) {
- notifyService.error(errors.getDeleteFailureErrorMessage());
- }
-
- setWorkpadsState(workpadState);
-
- if (redirectHome) {
- history.push('/');
- }
-
- return errored;
- });
- },
- [history, workpadService, fromState.workpadId, workpadsState, notifyService]
- );
-
- const formatDate = useCallback(
- (date: any) => {
- const dateFormat = platformService.getUISetting('dateFormat');
- return date && moment(date).format(dateFormat);
- },
- [platformService]
- );
-
- const { workpadId, canUserWrite } = fromState;
-
- return (
-
- );
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js b/x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js
deleted file mode 100644
index 24a694268e4ee..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js
+++ /dev/null
@@ -1,52 +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 { get } from 'lodash';
-import { getId } from '../../lib/get_id';
-import { ErrorStrings } from '../../../i18n';
-
-const { WorkpadFileUpload: errors } = ErrorStrings;
-
-export const uploadWorkpad = (file, onUpload, notify) => {
- if (!file) {
- return;
- }
-
- if (get(file, 'type') !== 'application/json') {
- return notify.warning(errors.getAcceptJSONOnlyErrorMessage(), {
- title: file.name
- ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
- : errors.getFileUploadFailureWithoutFileNameErrorMessage(),
- });
- }
- // TODO: Clean up this file, this loading stuff can, and should be, abstracted
- const reader = new FileReader();
-
- // handle reading the uploaded file
- reader.onload = () => {
- try {
- const workpad = JSON.parse(reader.result);
- workpad.id = getId('workpad');
-
- // sanity check for workpad object
- if (!Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets) {
- throw new Error(errors.getMissingPropertiesErrorMessage());
- }
-
- onUpload(workpad);
- } catch (e) {
- notify.error(e, {
- title: file.name
- ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
- : errors.getFileUploadFailureWithoutFileNameErrorMessage(),
- });
- }
- };
-
- // read the uploaded file
- reader.readAsText(file);
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js
deleted file mode 100644
index 51733dad5b377..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js
+++ /dev/null
@@ -1,31 +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 React from 'react';
-import PropTypes from 'prop-types';
-import { EuiButton } from '@elastic/eui';
-import { ComponentStrings } from '../../../i18n';
-
-const { WorkpadCreate: strings } = ComponentStrings;
-
-export const WorkpadCreate = ({ createPending, onCreate, ...rest }) => (
-
- {strings.getWorkpadCreateButtonLabel()}
-
-);
-
-WorkpadCreate.propTypes = {
- onCreate: PropTypes.func.isRequired,
- createPending: PropTypes.bool,
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js
deleted file mode 100644
index 7c34837771c6f..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js
+++ /dev/null
@@ -1,31 +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 PropTypes from 'prop-types';
-import { compose, withHandlers } from 'recompose';
-import { uploadWorkpad } from '../upload_workpad';
-import { ErrorStrings } from '../../../../i18n';
-import { WorkpadDropzone as Component } from './workpad_dropzone';
-
-const { WorkpadFileUpload: errors } = ErrorStrings;
-
-export const WorkpadDropzone = compose(
- withHandlers(({ notify }) => ({
- onDropAccepted: ({ onUpload }) => ([file]) => uploadWorkpad(file, onUpload),
- onDropRejected: () => ([file]) => {
- notify.warning(errors.getAcceptJSONOnlyErrorMessage(), {
- title: file.name
- ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
- : errors.getFileUploadFailureWithoutFileNameErrorMessage(),
- });
- },
- }))
-)(Component);
-
-WorkpadDropzone.propTypes = {
- onUpload: PropTypes.func.isRequired,
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js
deleted file mode 100644
index f77929e1feb76..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js
+++ /dev/null
@@ -1,31 +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 React from 'react';
-import PropTypes from 'prop-types';
-import Dropzone from 'react-dropzone';
-
-export const WorkpadDropzone = ({ onDropAccepted, onDropRejected, disabled, children }) => (
-
- {children}
-
-);
-
-WorkpadDropzone.propTypes = {
- onDropAccepted: PropTypes.func.isRequired,
- onDropRejected: PropTypes.func.isRequired,
- disabled: PropTypes.bool.isRequired,
- children: PropTypes.node.isRequired,
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss
deleted file mode 100644
index ac6838da97fbd..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-.canvasWorkpad__dropzone {
- border: 2px dashed transparent;
-}
-
-.canvasWorkpad__dropzone--active {
- background-color: $euiColorLightestShade;
- border-color: $euiColorLightShade;
-}
-
-.canvasWorkpad__dropzoneTable .euiTable {
- background-color: transparent;
-}
-
-.canvasWorkpad__dropzoneTable--tags {
- .euiTableCellContent {
- flex-wrap: wrap;
- }
-
- .euiHealth {
- width: 100%;
- }
-}
\ No newline at end of file
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js
deleted file mode 100644
index 9c232ab43ec8d..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js
+++ /dev/null
@@ -1,426 +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 React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiBasicTable,
- EuiButtonIcon,
- EuiPagination,
- EuiSpacer,
- EuiButton,
- EuiToolTip,
- EuiEmptyPrompt,
- EuiFilePicker,
- EuiLink,
-} from '@elastic/eui';
-import { orderBy } from 'lodash';
-import { ConfirmModal } from '../confirm_modal';
-import { RoutingLink } from '../routing';
-import { Paginate } from '../paginate';
-import { ComponentStrings } from '../../../i18n';
-import { WorkpadDropzone } from './workpad_dropzone';
-import { WorkpadCreate } from './workpad_create';
-import { WorkpadSearch } from './workpad_search';
-import { uploadWorkpad } from './upload_workpad';
-
-const { WorkpadLoader: strings } = ComponentStrings;
-
-const getDisplayName = (name, workpad, loadedWorkpad) => {
- const workpadName = name.length ? name : {workpad.id} ;
- return workpad.id === loadedWorkpad ? {workpadName} : workpadName;
-};
-
-export class WorkpadLoader extends React.PureComponent {
- static propTypes = {
- workpadId: PropTypes.string.isRequired,
- canUserWrite: PropTypes.bool.isRequired,
- createWorkpad: PropTypes.func.isRequired,
- findWorkpads: PropTypes.func.isRequired,
- downloadWorkpad: PropTypes.func.isRequired,
- cloneWorkpad: PropTypes.func.isRequired,
- removeWorkpads: PropTypes.func.isRequired,
- onClose: PropTypes.func.isRequired,
- workpads: PropTypes.object,
- formatDate: PropTypes.func.isRequired,
- };
-
- state = {
- createPending: false,
- deletingWorkpad: false,
- sortField: '@timestamp',
- sortDirection: 'desc',
- selectedWorkpads: [],
- pageSize: 10,
- };
-
- async componentDidMount() {
- // on component load, kick off the workpad search
- this.props.findWorkpads();
-
- // keep track of whether or not the component is mounted, to prevent rogue setState calls
- this._isMounted = true;
- }
-
- UNSAFE_componentWillReceiveProps(newProps) {
- // the workpadId prop will change when a is created or loaded, close the toolbar when it does
- const { workpadId, onClose } = this.props;
- if (workpadId !== newProps.workpadId) {
- onClose();
- }
- }
-
- componentWillUnmount() {
- this._isMounted = false;
- }
-
- // create new empty workpad
- createWorkpad = async () => {
- this.setState({ createPending: true });
- await this.props.createWorkpad();
- this._isMounted && this.setState({ createPending: false });
- };
-
- // create new workpad from uploaded JSON
- onUpload = async (workpad) => {
- this.setState({ createPending: true });
- await this.props.createWorkpad(workpad);
- this._isMounted && this.setState({ createPending: false });
- };
-
- // clone existing workpad
- cloneWorkpad = async (workpad) => {
- this.setState({ createPending: true });
- await this.props.cloneWorkpad(workpad.id);
- this._isMounted && this.setState({ createPending: false });
- };
-
- // Workpad remove methods
- openRemoveConfirm = () => this.setState({ deletingWorkpad: true });
-
- closeRemoveConfirm = () => this.setState({ deletingWorkpad: false });
-
- removeWorkpads = () => {
- const { selectedWorkpads } = this.state;
-
- this.props.removeWorkpads(selectedWorkpads.map(({ id }) => id)).then((remainingIds) => {
- const remainingWorkpads =
- remainingIds.length > 0
- ? selectedWorkpads.filter(({ id }) => remainingIds.includes(id))
- : [];
-
- this._isMounted &&
- this.setState({
- deletingWorkpad: false,
- selectedWorkpads: remainingWorkpads,
- });
- });
- };
-
- // downloads selected workpads as JSON files
- downloadWorkpads = () => {
- this.state.selectedWorkpads.forEach(({ id }) => this.props.downloadWorkpad(id));
- };
-
- onSelectionChange = (selectedWorkpads) => {
- this.setState({ selectedWorkpads });
- };
-
- onTableChange = ({ sort = {} }) => {
- const { field: sortField, direction: sortDirection } = sort;
- this.setState({
- sortField,
- sortDirection,
- });
- };
-
- renderWorkpadTable = ({ rows, pageNumber, totalPages, setPage }) => {
- const { sortField, sortDirection } = this.state;
- const { canUserWrite, createPending, workpadId: loadedWorkpad } = this.props;
-
- const actions = [
- {
- render: (workpad) => (
-
-
-
- this.props.downloadWorkpad(workpad.id)}
- aria-label={strings.getExportToolTip()}
- />
-
-
-
-
- this.cloneWorkpad(workpad)}
- aria-label={strings.getCloneToolTip()}
- disabled={!canUserWrite}
- />
-
-
-
- ),
- },
- ];
-
- const columns = [
- {
- field: 'name',
- name: strings.getTableNameColumnTitle(),
- sortable: true,
- dataType: 'string',
- render: (name, workpad) => {
- const workpadName = getDisplayName(name, workpad, loadedWorkpad);
-
- return (
-
- {workpadName}
-
- );
- },
- },
- {
- field: '@created',
- name: strings.getTableCreatedColumnTitle(),
- sortable: true,
- dataType: 'date',
- width: '20%',
- render: (date) => this.props.formatDate(date),
- },
- {
- field: '@timestamp',
- name: strings.getTableUpdatedColumnTitle(),
- sortable: true,
- dataType: 'date',
- width: '20%',
- render: (date) => this.props.formatDate(date),
- },
- { name: strings.getTableActionsColumnTitle(), actions, width: '100px' },
- ];
-
- const sorting = {
- sort: {
- field: sortField,
- direction: sortDirection,
- },
- };
-
- const selection = {
- itemId: 'id',
- onSelectionChange: this.onSelectionChange,
- };
-
- const emptyTable = (
- {strings.getEmptyPromptTitle()}}
- titleSize="s"
- body={
-
- {strings.getEmptyPromptGettingStartedDescription()}
-
- {strings.getEmptyPromptNewUserDescription()}{' '}
-
- {strings.getSampleDataLinkLabel()}
-
- .
-
-
- }
- />
- );
-
- return (
-
-
-
-
- {rows.length > 0 && (
-
-
-
-
-
- )}
-
-
- );
- };
-
- render() {
- const {
- deletingWorkpad,
- createPending,
- selectedWorkpads,
- sortField,
- sortDirection,
- } = this.state;
- const { canUserWrite } = this.props;
- const isLoading = this.props.workpads == null;
-
- let createButton = (
-
- );
-
- let deleteButton = (
-
- {strings.getDeleteButtonLabel(selectedWorkpads.length)}
-
- );
-
- const downloadButton = (
-
- {strings.getExportButtonLabel(selectedWorkpads.length)}
-
- );
-
- let uploadButton = (
- uploadWorkpad(file, this.onUpload, this.props.notify)}
- accept="application/json"
- disabled={createPending || !canUserWrite}
- />
- );
-
- if (!canUserWrite) {
- createButton = (
- {createButton}
- );
- deleteButton = (
- {deleteButton}
- );
- uploadButton = (
- {uploadButton}
- );
- }
-
- const modalTitle =
- selectedWorkpads.length === 1
- ? strings.getDeleteSingleWorkpadModalTitle(selectedWorkpads[0].name)
- : strings.getDeleteMultipleWorkpadModalTitle(selectedWorkpads.length);
-
- const confirmModal = (
-
- );
-
- let sortedWorkpads = [];
-
- if (!createPending && !isLoading) {
- const { workpads } = this.props.workpads;
- sortedWorkpads = orderBy(workpads, [sortField, '@timestamp'], [sortDirection, 'desc']);
- }
-
- return (
-
- {(pagination) => (
-
-
-
-
- {selectedWorkpads.length > 0 && (
-
- {downloadButton}
- {deleteButton}
-
- )}
-
- {
- pagination.setPage(0);
- this.props.findWorkpads(text);
- }}
- />
-
-
-
-
-
- {uploadButton}
- {createButton}
-
-
-
-
-
-
- {createPending && (
- {strings.getCreateWorkpadLoadingDescription()}
- )}
-
- {!createPending && isLoading && (
- {strings.getFetchLoadingDescription()}
- )}
-
- {!createPending && !isLoading && this.renderWorkpadTable(pagination)}
-
- {confirmModal}
-
- )}
-
- );
- }
-}
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss
deleted file mode 100644
index 3b2c8eae9e542..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss
+++ /dev/null
@@ -1,25 +0,0 @@
-.canvasWorkpad__upload--compressed {
-
- &.euiFilePicker--compressed.euiFilePicker {
- .euiFilePicker__prompt {
- height: $euiSizeXXL;
- padding: $euiSizeM;
- padding-left: $euiSizeXXL;
- }
-
- .euiFilePicker__icon {
- top: $euiSizeM;
- }
- }
-
- // The file picker input is being used moreso as a button, outside of a form,
- // and thus the need to override the default max-width of form inputs.
- // An issue has been opened in EUI to consider creating a button
- // version of the file picker - https://github.com/elastic/eui/issues/1987
-
- .euiFilePicker__wrap {
- @include euiBreakpoint('xs', 's') {
- max-width: none;
- }
- }
-}
\ No newline at end of file
diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js
deleted file mode 100644
index 8bf8bbae8ced4..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js
+++ /dev/null
@@ -1,44 +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 React from 'react';
-import PropTypes from 'prop-types';
-import { EuiFieldSearch } from '@elastic/eui';
-import { debounce } from 'lodash';
-import { ComponentStrings } from '../../../i18n';
-
-const { WorkpadSearch: strings } = ComponentStrings;
-export class WorkpadSearch extends React.PureComponent {
- static propTypes = {
- onChange: PropTypes.func.isRequired,
- initialText: PropTypes.string,
- };
-
- state = {
- searchText: this.props.initialText || '',
- };
-
- triggerChange = debounce(this.props.onChange, 150);
-
- setSearchText = (ev) => {
- const text = ev.target.value;
- this.setState({ searchText: text });
- this.triggerChange(text);
- };
-
- render() {
- return (
-
- );
- }
-}
diff --git a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js b/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js
deleted file mode 100644
index 8055be32ac481..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js
+++ /dev/null
@@ -1,69 +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 React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import {
- EuiTabbedContent,
- EuiModalHeader,
- EuiModalHeaderTitle,
- EuiModalBody,
- EuiSpacer,
- EuiFlexGroup,
- EuiFlexItem,
-} from '@elastic/eui';
-import { WorkpadLoader } from '../workpad_loader';
-import { WorkpadTemplates } from '../workpad_templates';
-import { ComponentStrings } from '../../../i18n';
-
-const { WorkpadManager: strings } = ComponentStrings;
-
-export const WorkpadManager = ({ onClose }) => {
- const tabs = [
- {
- id: 'workpadLoader',
- name: strings.getMyWorkpadsTabLabel(),
- content: (
-
-
-
-
- ),
- },
- {
- id: 'workpadTemplates',
- name: strings.getWorkpadTemplatesTabLabel(),
- 'data-test-subj': 'workpadTemplates',
- content: (
-
-
-
-
- ),
- },
- ];
- return (
-
-
-
-
-
- {strings.getModalTitle()}
-
-
-
-
-
-
-
-
- );
-};
-
-WorkpadManager.propTypes = {
- onClose: PropTypes.func,
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot
deleted file mode 100644
index cab6e8fd9b5f5..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot
+++ /dev/null
@@ -1,564 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Storyshots components/WorkpadTemplates default 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
- Tags
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sorting
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Template name
-
-
-
-
-
-
-
-
- Description
-
-
-
-
-
-
- Tags
-
-
-
-
-
-
-
-
-
- Template name
-
-
-
-
-
- test1
-
-
-
-
-
-
-
- Description
-
-
-
- This is a test template
-
-
-
-
-
- Tags
-
-
-
-
-
-
-
- Template name
-
-
-
-
-
- test2
-
-
-
-
-
-
-
- Description
-
-
-
- This is a second test template
-
-
-
-
-
- Tags
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx
deleted file mode 100644
index 8e6c055478ca2..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx
+++ /dev/null
@@ -1,45 +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 React from 'react';
-import { storiesOf } from '@storybook/react';
-import { action } from '@storybook/addon-actions';
-import { WorkpadTemplates } from '../workpad_templates';
-import { CanvasTemplate } from '../../../../types';
-
-const templates: Record = {
- test1: {
- id: 'test1-id',
- name: 'test1',
- help: 'This is a test template',
- tags: ['tag1', 'tag2'],
- template_key: 'test1-key',
- },
- test2: {
- id: 'test2-id',
- name: 'test2',
- help: 'This is a second test template',
- tags: ['tag2', 'tag3'],
- template_key: 'test2-key',
- },
-};
-
-storiesOf('components/WorkpadTemplates', module)
- .addDecorator((story) => {story()}
)
- .add('default', () => {
- const onCreateFromTemplateAction = action('onCreateFromTemplate');
- return (
- {
- onCreateFromTemplateAction(template);
- return Promise.resolve();
- }}
- />
- );
- });
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx
deleted file mode 100644
index 7e007b1253464..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx
+++ /dev/null
@@ -1,86 +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 React, { useCallback, useState, useEffect, FunctionComponent } from 'react';
-import { EuiLoadingSpinner } from '@elastic/eui';
-import { useHistory } from 'react-router-dom';
-
-import { ComponentStrings } from '../../../i18n/components';
-// @ts-expect-error
-import * as workpadService from '../../lib/workpad_service';
-import { WorkpadTemplates as Component } from './workpad_templates';
-import { CanvasTemplate } from '../../../types';
-import { list } from '../../lib/template_service';
-import { applyTemplateStrings } from '../../../i18n/templates/apply_strings';
-import { useNotifyService, useServices } from '../../services';
-
-interface WorkpadTemplatesProps {
- onClose: () => void;
-}
-
-const Creating: FunctionComponent<{ name: string }> = ({ name }) => (
-
- {' '}
- {ComponentStrings.WorkpadTemplates.getCreatingTemplateLabel(name)}
-
-);
-export const WorkpadTemplates: FunctionComponent = ({ onClose }) => {
- const history = useHistory();
- const services = useServices();
-
- const [templates, setTemplates] = useState(undefined);
- const [creatingFromTemplateName, setCreatingFromTemplateName] = useState(
- undefined
- );
- const { error } = useNotifyService();
-
- useEffect(() => {
- if (!templates) {
- (async () => {
- const fetchedTemplates = await list();
- setTemplates(applyTemplateStrings(fetchedTemplates));
- })();
- }
- }, [templates]);
-
- let templateProp: Record = {};
-
- if (templates) {
- templateProp = templates.reduce>((reduction, template) => {
- reduction[template.name] = template;
- return reduction;
- }, {});
- }
-
- const createFromTemplate = useCallback(
- async (template: CanvasTemplate) => {
- setCreatingFromTemplateName(template.name);
- try {
- const result = await services.workpad.createFromTemplate(template.id);
- history.push(`/workpad/${result.id}/page/1`);
- } catch (e) {
- setCreatingFromTemplateName(undefined);
- error(e, {
- title: `Couldn't create workpad from template`,
- });
- }
- },
- [services.workpad, error, history]
- );
-
- if (creatingFromTemplateName) {
- return ;
- }
-
- return (
-
- );
-};
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx
deleted file mode 100644
index 72871b93c1735..0000000000000
--- a/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx
+++ /dev/null
@@ -1,215 +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 React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiBasicTable,
- EuiPagination,
- EuiSpacer,
- EuiButtonEmpty,
- EuiSearchBar,
- EuiTableSortingType,
- Direction,
- SortDirection,
-} from '@elastic/eui';
-import { orderBy } from 'lodash';
-// @ts-ignore untyped local
-import { EuiBasicTableColumn } from '@elastic/eui';
-import { Paginate, PaginateChildProps } from '../paginate';
-import { TagList } from '../tag_list';
-import { getTagsFilter } from '../../lib/get_tags_filter';
-// @ts-expect-error
-import { extractSearch } from '../../lib/extract_search';
-import { ComponentStrings } from '../../../i18n';
-import { CanvasTemplate } from '../../../types';
-
-interface TableChange {
- page?: {
- index: number;
- size: number;
- };
- sort?: {
- field: keyof T;
- direction: Direction;
- };
-}
-
-const { WorkpadTemplates: strings } = ComponentStrings;
-
-interface WorkpadTemplatesProps {
- onCreateFromTemplate: (template: CanvasTemplate) => Promise;
- onClose: () => void;
- templates: Record;
-}
-
-interface WorkpadTemplatesState {
- sortField: string;
- sortDirection: Direction;
- pageSize: number;
- searchTerm: string;
- filterTags: string[];
-}
-
-export class WorkpadTemplates extends React.PureComponent<
- WorkpadTemplatesProps,
- WorkpadTemplatesState
-> {
- static propTypes = {
- onCreateFromTemplate: PropTypes.func.isRequired,
- onClose: PropTypes.func.isRequired,
- templates: PropTypes.object,
- };
-
- state = {
- sortField: 'name',
- sortDirection: SortDirection.ASC,
- pageSize: 10,
- searchTerm: '',
- filterTags: [],
- };
-
- tagType: 'health' = 'health';
-
- onTableChange = (tableChange: TableChange) => {
- if (tableChange.sort) {
- const { field: sortField, direction: sortDirection } = tableChange.sort;
- this.setState({
- sortField,
- sortDirection,
- });
- }
- };
-
- onSearch = ({ queryText = '' }) => this.setState(extractSearch(queryText));
-
- cloneTemplate = (template: CanvasTemplate) =>
- this.props.onCreateFromTemplate(template).then(() => this.props.onClose());
-
- renderWorkpadTable = ({ rows, pageNumber, totalPages, setPage }: PaginateChildProps) => {
- const { sortField, sortDirection } = this.state;
-
- const columns: Array> = [
- {
- field: 'name',
- name: strings.getTableNameColumnTitle(),
- sortable: true,
- width: '30%',
- dataType: 'string',
- render: (name: string, template) => {
- const templateName = name.length ? name : 'Unnamed Template';
-
- return (
- this.cloneTemplate(template)}
- aria-label={strings.getCloneTemplateLinkAriaLabel(templateName)}
- type="button"
- >
- {templateName}
-
- );
- },
- },
- {
- field: 'help',
- name: strings.getTableDescriptionColumnTitle(),
- sortable: false,
- dataType: 'string',
- width: '30%',
- },
- {
- field: 'tags',
- name: strings.getTableTagsColumnTitle(),
- sortable: false,
- dataType: 'string',
- width: '30%',
- render: (tags: string[]) => ,
- },
- ];
-
- const sorting: EuiTableSortingType = {
- sort: {
- field: sortField,
- direction: sortDirection,
- },
- };
-
- return (
-
-
-
- {rows.length > 0 && (
-
-
-
-
-
- )}
-
- );
- };
-
- renderSearch = () => {
- const { searchTerm } = this.state;
- const filters = [getTagsFilter(this.tagType)];
-
- return (
-
- );
- };
-
- render() {
- const { templates } = this.props;
- const { sortField, sortDirection, searchTerm, filterTags } = this.state;
- const sortedTemplates = orderBy(templates, [sortField, 'name'], [sortDirection, 'asc']);
-
- const filteredTemplates = sortedTemplates.filter(({ name = '', help = '', tags = [] }) => {
- const tagMatch = filterTags.length
- ? filterTags.every((filterTag) => tags.indexOf(filterTag) > -1)
- : true;
-
- const lowercaseSearch = searchTerm.toLowerCase();
- const textMatch = lowercaseSearch
- ? name.toLowerCase().indexOf(lowercaseSearch) > -1 ||
- help.toLowerCase().indexOf(lowercaseSearch) > -1
- : true;
-
- return tagMatch && textMatch;
- });
-
- return (
-
- {(pagination: PaginateChildProps) => (
-
- {this.renderSearch()}
-
- {this.renderWorkpadTable(pagination)}
-
- )}
-
- );
- }
-}
diff --git a/x-pack/plugins/canvas/public/lib/get_tags_filter.tsx b/x-pack/plugins/canvas/public/lib/get_tags_filter.tsx
deleted file mode 100644
index 12d77c9c7f0c0..0000000000000
--- a/x-pack/plugins/canvas/public/lib/get_tags_filter.tsx
+++ /dev/null
@@ -1,39 +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 React from 'react';
-import { sortBy } from 'lodash';
-import { SearchFilterConfig } from '@elastic/eui';
-import { Tag } from '../components/tag';
-import { getId } from './get_id';
-import { tagsRegistry } from './tags_registry';
-import { ComponentStrings } from '../../i18n';
-
-const { WorkpadTemplates: strings } = ComponentStrings;
-
-// EUI helper function
-// generates the FieldValueSelectionFilter object for EuiSearchBar for tag filtering
-export const getTagsFilter = (type: 'health' | 'badge'): SearchFilterConfig => {
- const uniqueTags = sortBy(Object.values(tagsRegistry.toJS()), 'name');
- const filterType = 'field_value_selection';
-
- return {
- type: filterType,
- field: 'tag',
- name: strings.getTableTagsColumnTitle(),
- multiSelect: true,
- options: uniqueTags.map(({ name, color }) => ({
- value: name,
- name,
- view: (
-
-
-
- ),
- })),
- };
-};
diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts
index 6c039660c64c7..3f8f58367171a 100644
--- a/x-pack/plugins/canvas/public/services/index.ts
+++ b/x-pack/plugins/canvas/public/services/index.ts
@@ -34,7 +34,7 @@ export type CanvasServiceFactory = (
appUpdater: BehaviorSubject
) => Service | Promise;
-class CanvasServiceProvider {
+export class CanvasServiceProvider {
private factory: CanvasServiceFactory;
private service: Service | undefined;
diff --git a/x-pack/plugins/canvas/public/services/stubs/platform.ts b/x-pack/plugins/canvas/public/services/stubs/platform.ts
index ea80a5a7c26b9..5776a1d0d6983 100644
--- a/x-pack/plugins/canvas/public/services/stubs/platform.ts
+++ b/x-pack/plugins/canvas/public/services/stubs/platform.ts
@@ -9,13 +9,19 @@ import { PlatformService } from '../platform';
const noop = (..._args: any[]): any => {};
+const uiSettings: Record = {
+ dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
+};
+
+const getUISetting = (setting: string) => uiSettings[setting];
+
export const platformService: PlatformService = {
getBasePath: () => '/base/path',
getBasePathInterface: noop,
getDocLinkVersion: () => 'dockLinkVersion',
getElasticWebsiteUrl: () => 'https://elastic.co',
getHasWriteAccess: () => true,
- getUISetting: noop,
+ getUISetting,
setBreadcrumbs: noop,
setRecentlyAccessed: noop,
getSavedObjects: noop,
diff --git a/x-pack/plugins/canvas/public/services/stubs/workpad.ts b/x-pack/plugins/canvas/public/services/stubs/workpad.ts
index 857831c92a8a6..4e3612feb67c8 100644
--- a/x-pack/plugins/canvas/public/services/stubs/workpad.ts
+++ b/x-pack/plugins/canvas/public/services/stubs/workpad.ts
@@ -5,17 +5,95 @@
* 2.0.
*/
+import moment from 'moment';
+
+// @ts-expect-error
+import { getDefaultWorkpad } from '../../state/defaults';
import { WorkpadService } from '../workpad';
-import { CanvasWorkpad } from '../../../types';
+import { getId } from '../../lib/get_id';
+import { CanvasTemplate } from '../../../types';
-export const workpadService: WorkpadService = {
- get: (id: string) => Promise.resolve({} as CanvasWorkpad),
- create: (workpad) => Promise.resolve({} as CanvasWorkpad),
- createFromTemplate: (templateId: string) => Promise.resolve({} as CanvasWorkpad),
- find: (term: string) =>
- Promise.resolve({
+const TIMEOUT = 500;
+
+const promiseTimeout = (time: number) => () => new Promise((resolve) => setTimeout(resolve, time));
+const getName = () => {
+ const lorem = 'Lorem ipsum dolor sit amet consectetur adipiscing elit Fusce lobortis aliquet arcu ut turpis duis'.split(
+ ' '
+ );
+ return [1, 2, 3].map(() => lorem[Math.floor(Math.random() * lorem.length)]).join(' ');
+};
+
+const randomDate = (
+ start: Date = moment().toDate(),
+ end: Date = moment().subtract(7, 'days').toDate()
+) => new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toISOString();
+
+const templates: CanvasTemplate[] = [
+ {
+ id: 'test1-id',
+ name: 'test1',
+ help: 'This is a test template',
+ tags: ['tag1', 'tag2'],
+ template_key: 'test1-key',
+ },
+ {
+ id: 'test2-id',
+ name: 'test2',
+ help: 'This is a second test template',
+ tags: ['tag2', 'tag3'],
+ template_key: 'test2-key',
+ },
+];
+
+export const getSomeWorkpads = (count = 3) =>
+ Array.from({ length: count }, () => ({
+ '@created': randomDate(
+ moment().subtract(3, 'days').toDate(),
+ moment().subtract(10, 'days').toDate()
+ ),
+ '@timestamp': randomDate(),
+ id: getId('workpad'),
+ name: getName(),
+ }));
+
+export const findSomeWorkpads = (count = 3, timeout = TIMEOUT) => (_term: string) => {
+ return Promise.resolve()
+ .then(promiseTimeout(timeout))
+ .then(() => ({
+ total: count,
+ workpads: getSomeWorkpads(count),
+ }));
+};
+
+export const findNoWorkpads = (timeout = TIMEOUT) => (_term: string) => {
+ return Promise.resolve()
+ .then(promiseTimeout(timeout))
+ .then(() => ({
total: 0,
workpads: [],
- }),
- remove: (id: string) => Promise.resolve(undefined),
+ }));
+};
+
+export const findSomeTemplates = (timeout = TIMEOUT) => () => {
+ return Promise.resolve()
+ .then(promiseTimeout(timeout))
+ .then(() => getSomeTemplates());
+};
+
+export const findNoTemplates = (timeout = TIMEOUT) => () => {
+ return Promise.resolve()
+ .then(promiseTimeout(timeout))
+ .then(() => getNoTemplates());
+};
+
+export const getNoTemplates = () => ({ templates: [] });
+export const getSomeTemplates = () => ({ templates });
+
+export const workpadService: WorkpadService = {
+ get: (id: string) => Promise.resolve({ ...getDefaultWorkpad(), id }),
+ findTemplates: findNoTemplates(),
+ create: (workpad) => Promise.resolve(workpad),
+ createFromTemplate: (_templateId: string) => Promise.resolve(getDefaultWorkpad()),
+ find: findNoWorkpads(),
+ remove: (id: string) => Promise.resolve(),
};
diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts
index 11690ca4c0c45..7d2f1550a312f 100644
--- a/x-pack/plugins/canvas/public/services/workpad.ts
+++ b/x-pack/plugins/canvas/public/services/workpad.ts
@@ -5,8 +5,12 @@
* 2.0.
*/
-import { API_ROUTE_WORKPAD, DEFAULT_WORKPAD_CSS } from '../../common/lib/constants';
-import { CanvasWorkpad } from '../../types';
+import {
+ API_ROUTE_WORKPAD,
+ DEFAULT_WORKPAD_CSS,
+ API_ROUTE_TEMPLATES,
+} from '../../common/lib/constants';
+import { CanvasWorkpad, CanvasTemplate } from '../../types';
import { CanvasServiceFactory } from './';
/*
@@ -40,9 +44,15 @@ const sanitizeWorkpad = function (workpad: CanvasWorkpad) {
return workpad;
};
-interface WorkpadFindResponse {
+export type FoundWorkpads = Array>;
+export type FoundWorkpad = FoundWorkpads[number];
+export interface WorkpadFindResponse {
total: number;
- workpads: Array>;
+ workpads: FoundWorkpads;
+}
+
+export interface TemplateFindResponse {
+ templates: CanvasTemplate[];
}
export interface WorkpadService {
@@ -51,6 +61,7 @@ export interface WorkpadService {
createFromTemplate: (templateId: string) => Promise;
find: (term: string) => Promise;
remove: (id: string) => Promise;
+ findTemplates: () => Promise;
}
export const workpadServiceFactory: CanvasServiceFactory = (
@@ -82,7 +93,9 @@ export const workpadServiceFactory: CanvasServiceFactory = (
body: JSON.stringify({ templateId }),
});
},
+ findTemplates: async () => coreStart.http.get(API_ROUTE_TEMPLATES),
find: (searchTerm: string) => {
+ // TODO: this shouldn't be necessary. Check for usage.
const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0;
return coreStart.http.get(`${getApiPath()}/find`, {
diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss
index a79e07a7d0016..d9592d5c0be5f 100644
--- a/x-pack/plugins/canvas/public/style/index.scss
+++ b/x-pack/plugins/canvas/public/style/index.scss
@@ -40,8 +40,6 @@
@import '../components/workpad_header/element_menu/element_menu';
@import '../components/workpad_header/share_menu/share_menu';
@import '../components/workpad_header/view_menu/view_menu';
-@import '../components/workpad_loader/workpad_loader';
-@import '../components/workpad_loader/workpad_dropzone/workpad_dropzone';
@import '../components/workpad_page/workpad_page';
@import '../components/workpad_page/workpad_interactive_page/workpad_interactive_page';
@import '../components/workpad_page/workpad_static_page/workpad_static_page';
diff --git a/x-pack/plugins/canvas/storybook/decorators/index.ts b/x-pack/plugins/canvas/storybook/decorators/index.ts
index a674eaad576a7..598a2333be554 100644
--- a/x-pack/plugins/canvas/storybook/decorators/index.ts
+++ b/x-pack/plugins/canvas/storybook/decorators/index.ts
@@ -11,6 +11,7 @@ import { kibanaContextDecorator } from './kibana_decorator';
import { servicesContextDecorator } from './services_decorator';
export { reduxDecorator } from './redux_decorator';
+export { servicesContextDecorator } from './services_decorator';
export const addDecorators = () => {
if (process.env.NODE_ENV === 'test') {
@@ -20,5 +21,5 @@ export const addDecorators = () => {
addDecorator(kibanaContextDecorator);
addDecorator(routerContextDecorator);
- addDecorator(servicesContextDecorator);
+ addDecorator(servicesContextDecorator());
};
diff --git a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx
index 01d96cb0c70e6..289171f136ab5 100644
--- a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx
+++ b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx
@@ -25,7 +25,7 @@ elementsRegistry.register(image);
import { getInitialState, getReducer, getMiddleware, patchDispatch } from '../addon/src/state';
export { ADDON_ID, ACTIONS_PANEL_ID } from '../addon/src/constants';
-interface Params {
+export interface Params {
workpad?: CanvasWorkpad;
elements?: CanvasElement[];
assets?: CanvasAsset[];
diff --git a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx
index a11492387ea7f..def5a5681a8c4 100644
--- a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx
+++ b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx
@@ -7,8 +7,40 @@
import React from 'react';
-import { ServicesProvider } from '../../public/services';
+import {
+ CanvasServiceFactory,
+ CanvasServiceProvider,
+ ServicesProvider,
+} from '../../public/services';
+import {
+ findNoWorkpads,
+ findSomeWorkpads,
+ workpadService,
+ findSomeTemplates,
+ findNoTemplates,
+} from '../../public/services/stubs/workpad';
+import { WorkpadService } from '../../public/services/workpad';
-export const servicesContextDecorator = (story: Function) => (
- {story()}
-);
+interface Params {
+ findWorkpads?: number;
+ findTemplates?: boolean;
+}
+
+export const servicesContextDecorator = ({
+ findWorkpads = 0,
+ findTemplates: findTemplatesOption = false,
+}: Params = {}) => {
+ const workpadServiceFactory: CanvasServiceFactory = (): WorkpadService => ({
+ ...workpadService,
+ find: findWorkpads > 0 ? findSomeWorkpads(findWorkpads) : findNoWorkpads(),
+ findTemplates: findTemplatesOption ? findSomeTemplates() : findNoTemplates(),
+ });
+
+ const workpad = new CanvasServiceProvider(workpadServiceFactory);
+ // @ts-expect-error This is a hack at the moment, until we can get Canvas moved over to the new services architecture.
+ workpad.start();
+
+ return (story: Function) => (
+ {story()}
+ );
+};
diff --git a/x-pack/plugins/canvas/storybook/index.ts b/x-pack/plugins/canvas/storybook/index.ts
index 148af337d7720..ff60b84c88a69 100644
--- a/x-pack/plugins/canvas/storybook/index.ts
+++ b/x-pack/plugins/canvas/storybook/index.ts
@@ -10,3 +10,8 @@ import { ACTIONS_PANEL_ID } from './addon/src/constants';
export * from './decorators';
export { ACTIONS_PANEL_ID } from './addon/src/constants';
export const getAddonPanelParameters = () => ({ options: { selectedPanel: ACTIONS_PANEL_ID } });
+export const getDisableStoryshotsParameter = () => ({
+ storyshots: {
+ disable: true,
+ },
+});
diff --git a/x-pack/plugins/canvas/storybook/main.ts b/x-pack/plugins/canvas/storybook/main.ts
index 80a8aeb14a804..69c05322cf3f0 100644
--- a/x-pack/plugins/canvas/storybook/main.ts
+++ b/x-pack/plugins/canvas/storybook/main.ts
@@ -53,6 +53,11 @@ const canvasWebpack = {
},
],
},
+ resolve: {
+ alias: {
+ 'src/plugins': resolve(KIBANA_ROOT, 'src/plugins'),
+ },
+ },
};
module.exports = {
diff --git a/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot b/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot
new file mode 100644
index 0000000000000..39ec1e234ead5
--- /dev/null
+++ b/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot
@@ -0,0 +1,65 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Home/Empty Prompt Empty Prompt 1`] = `
+
+
+
+
+
+
+
+
+ Add your first workpad
+
+
+
+
+ Create a new workpad, start from a template, or import a workpad JSON file by dropping it here.
+
+
+ New to Canvas?
+
+
+ Add your first workpad
+
+ .
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx
index 0c3765812066e..7f0ea077c7569 100644
--- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx
+++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx
@@ -90,6 +90,11 @@ import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer'
jest.mock('@elastic/eui/test-env/components/observer/observer');
EuiObserver.mockImplementation(() => 'EuiObserver');
+// @ts-expect-error untyped library
+import Dropzone from 'react-dropzone';
+jest.mock('react-dropzone');
+Dropzone.mockImplementation(() => 'Dropzone');
+
// This element uses a `ref` and cannot be rendered by Jest snapshots.
import { RenderedElement } from '../shareable_runtime/components/rendered_element';
jest.mock('../shareable_runtime/components/rendered_element');
@@ -111,7 +116,7 @@ addSerializer(styleSheetSerializer);
// Initialize Storyshots and build the Jest Snapshots
initStoryshots({
- configPath: path.resolve(__dirname, './../storybook'),
+ configPath: path.resolve(__dirname),
framework: 'react',
test: multiSnapshotWithOptions({}),
// Don't snapshot tests that start with 'redux'
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 91277403d9e05..bc8318e803c8f 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -6107,18 +6107,18 @@
"xpack.canvas.error.esService.indicesFetchErrorMessage": "Elasticsearch インデックスを取得できませんでした",
"xpack.canvas.error.RenderWithFn.renderErrorMessage": "「{functionName}」のレンダリングが失敗しました",
"xpack.canvas.error.repeatImage.missingMaxArgument": "{emptyImageArgument} を指定する場合は、{maxArgument} を設定する必要があります",
- "xpack.canvas.error.workpadLoader.cloneFailureErrorMessage": "ワークパッドのクローンを作成できませんでした",
- "xpack.canvas.error.workpadLoader.deleteFailureErrorMessage": "すべてのワークパッドを削除できませんでした",
- "xpack.canvas.error.workpadLoader.findFailureErrorMessage": "ワークパッドが見つかりませんでした",
- "xpack.canvas.error.workpadLoader.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした",
+ "xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "ワークパッドのクローンを作成できませんでした",
+ "xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした",
+ "xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "すべてのワークパッドを削除できませんでした",
+ "xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "ワークパッドが見つかりませんでした",
+ "xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "{JSON} 個のファイルしか受け付けられませんでした",
+ "xpack.canvas.error.useImportWorkpad.fileUploadFailureWithoutFileNameErrorMessage": "ファイルをアップロードできませんでした",
+ "xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage": "{CANVAS} ワークパッドに必要なプロパティの一部が欠けています。 {JSON} ファイルを編集して正しいプロパティ値を入力し、再試行してください。",
"xpack.canvas.error.workpadRoutes.createFailureErrorMessage": "ワークパッドを作成できませんでした",
"xpack.canvas.error.workpadRoutes.loadFailureErrorMessage": "ID でワークパッドを読み込めませんでした",
- "xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage": "{JSON} 個のファイルしか受け付けられませんでした",
- "xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage": "ファイルをアップロードできませんでした",
- "xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage": "{CANVAS} ワークパッドに必要なプロパティの一部が欠けています。 {JSON} ファイルを編集して正しいプロパティ値を入力し、再試行してください。",
"xpack.canvas.errorComponent.description": "表現が失敗し次のメッセージが返されました:",
"xpack.canvas.errorComponent.title": "おっと!表現が失敗しました",
- "xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage": "「{fileName}」をアップロードできませんでした",
+ "xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage": "「{fileName}」をアップロードできませんでした",
"xpack.canvas.expression.cancelButtonLabel": "キャンセル",
"xpack.canvas.expression.closeButtonLabel": "閉じる",
"xpack.canvas.expression.learnLinkText": "表現構文の詳細",
@@ -6452,6 +6452,12 @@
"xpack.canvas.helpMenu.description": "{CANVAS} に関する情報",
"xpack.canvas.helpMenu.documentationLinkLabel": "{CANVAS} ドキュメント",
"xpack.canvas.helpMenu.keyboardShortcutsLinkLabel": "キーボードショートカット",
+ "xpack.canvas.home.myWorkpadsTabLabel": "マイワークパッド",
+ "xpack.canvas.home.workpadTemplatesTabLabel": "テンプレート",
+ "xpack.canvas.homeEmptyPrompt.emptyPromptGettingStartedDescription": "新規ワークパッドを作成、テンプレートで開始、またはワークパッド {JSON} ファイルをここにドロップしてインポートします。",
+ "xpack.canvas.homeEmptyPrompt.emptyPromptNewUserDescription": "{CANVAS} を初めて使用する場合",
+ "xpack.canvas.homeEmptyPrompt.emptyPromptTitle": "初の’ワークパッドを追加しましょう",
+ "xpack.canvas.homeEmptyPrompt.sampleDataLinkLabel": "初の’ワークパッドを追加しましょう",
"xpack.canvas.keyboardShortcuts.bringFowardShortcutHelpText": "前に移動",
"xpack.canvas.keyboardShortcuts.bringToFrontShortcutHelpText": "表面に移動",
"xpack.canvas.keyboardShortcuts.cloneShortcutHelpText": "クローンを作成",
@@ -6898,6 +6904,7 @@
"xpack.canvas.units.quickRange.last90Days": "過去90日間",
"xpack.canvas.units.quickRange.today": "今日",
"xpack.canvas.units.quickRange.yesterday": "昨日",
+ "xpack.canvas.useCloneWorkpad.clonedWorkpadName": "{workpadName} のコピー",
"xpack.canvas.varConfig.addButtonLabel": "変数の追加",
"xpack.canvas.varConfig.addTooltipLabel": "変数の追加",
"xpack.canvas.varConfig.copyActionButtonLabel": "スニペットをコピー",
@@ -7024,40 +7031,30 @@
"xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "ズーム",
"xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "リセット",
"xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%",
- "xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} のコピー",
- "xpack.canvas.workpadLoader.cloneTooltip": "ワークパッドのクローンを作成します",
- "xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "ワークパッドを作成中...",
- "xpack.canvas.workpadLoader.deleteButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドを削除",
- "xpack.canvas.workpadLoader.deleteButtonLabel": " ({numberOfWorkpads}) ワークパッドを削除",
- "xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel": "削除",
- "xpack.canvas.workpadLoader.deleteModalDescription": "削除されたワークパッドは復元できません。",
- "xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle": "{numberOfWorkpads} 個のワークパッドを削除しますか?",
- "xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle": "ワークパッド「{workpadName}」削除しますか?",
- "xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription": "新規ワークパッドを作成、テンプレートで開始、またはワークパッド {JSON} ファイルをここにドロップしてインポートします。",
- "xpack.canvas.workpadLoader.emptyPromptNewUserDescription": "{CANVAS} を初めて使用する場合",
- "xpack.canvas.workpadLoader.emptyPromptTitle": "初の’ワークパッドを追加しましょう",
- "xpack.canvas.workpadLoader.exportButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドをエクスポート",
- "xpack.canvas.workpadLoader.exportButtonLabel": "エクスポート ({numberOfWorkpads}) ",
- "xpack.canvas.workpadLoader.exportTooltip": "ワークパッドをエクスポート",
- "xpack.canvas.workpadLoader.fetchLoadingDescription": "ワークパッドを取得中...",
- "xpack.canvas.workpadLoader.filePickerPlaceholder": "ワークパッド {JSON} ファイルをインポート",
- "xpack.canvas.workpadLoader.loadWorkpadArialLabel": "ワークパッド「{workpadName}」を読み込む",
- "xpack.canvas.workpadLoader.noPermissionToCloneToolTip": "ワークパッドのクローンを作成するパーミッションがありません",
- "xpack.canvas.workpadLoader.noPermissionToCreateToolTip": "ワークパッドを作成するパーミッションがありません",
- "xpack.canvas.workpadLoader.noPermissionToDeleteToolTip": "ワークパッドを削除するパーミッションがありません",
- "xpack.canvas.workpadLoader.noPermissionToUploadToolTip": "ワークパッドを更新するパーミッションがありません",
- "xpack.canvas.workpadLoader.sampleDataLinkLabel": "初の’ワークパッドを追加しましょう",
- "xpack.canvas.workpadLoader.table.actionsColumnTitle": "アクション",
- "xpack.canvas.workpadLoader.table.createdColumnTitle": "作成済み",
- "xpack.canvas.workpadLoader.table.nameColumnTitle": "ワークパッド名",
- "xpack.canvas.workpadLoader.table.updatedColumnTitle": "更新しました",
- "xpack.canvas.workpadManager.modalTitle": "{CANVAS} ワークパッド",
- "xpack.canvas.workpadManager.myWorkpadsTabLabel": "マイワークパッド",
- "xpack.canvas.workpadManager.workpadTemplatesTabLabel": "テンプレート",
- "xpack.canvas.workpadSearch.searchPlaceholder": "ワークパッドを検索",
- "xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel": "ワークパッドテンプレート「{templateName}」のクローンを作成",
- "xpack.canvas.workpadTemplate.creatingTemplateLabel": "テンプレート「{templateName}」から作成しています",
- "xpack.canvas.workpadTemplate.searchPlaceholder": "テンプレートを検索",
+ "xpack.canvas.workpadImport.filePickerPlaceholder": "ワークパッド {JSON} ファイルをインポート",
+ "xpack.canvas.workpadTable.cloneTooltip": "ワークパッドのクローンを作成します",
+ "xpack.canvas.workpadTable.exportTooltip": "ワークパッドをエクスポート",
+ "xpack.canvas.workpadTable.loadWorkpadArialLabel": "ワークパッド「{workpadName}」を読み込む",
+ "xpack.canvas.workpadTable.noPermissionToCloneToolTip": "ワークパッドのクローンを作成するパーミッションがありません",
+ "xpack.canvas.workpadTable.searchPlaceholder": "ワークパッドを検索",
+ "xpack.canvas.workpadTable.table.actionsColumnTitle": "アクション",
+ "xpack.canvas.workpadTable.table.createdColumnTitle": "作成済み",
+ "xpack.canvas.workpadTable.table.nameColumnTitle": "ワークパッド名",
+ "xpack.canvas.workpadTable.table.updatedColumnTitle": "更新しました",
+ "xpack.canvas.workpadTableTools.deleteButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドを削除",
+ "xpack.canvas.workpadTableTools.deleteButtonLabel": " ({numberOfWorkpads}) ワークパッドを削除",
+ "xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel": "削除",
+ "xpack.canvas.workpadTableTools.deleteModalDescription": "削除されたワークパッドは復元できません。",
+ "xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle": "{numberOfWorkpads} 個のワークパッドを削除しますか?",
+ "xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle": "ワークパッド「{workpadName}」削除しますか?",
+ "xpack.canvas.workpadTableTools.exportButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドをエクスポート",
+ "xpack.canvas.workpadTableTools.exportButtonLabel": "エクスポート ({numberOfWorkpads}) ",
+ "xpack.canvas.workpadTableTools.noPermissionToCreateToolTip": "ワークパッドを作成するパーミッションがありません",
+ "xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip": "ワークパッドを削除するパーミッションがありません",
+ "xpack.canvas.workpadTableTools.noPermissionToUploadToolTip": "ワークパッドを更新するパーミッションがありません",
+ "xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel": "ワークパッドテンプレート「{templateName}」のクローンを作成",
+ "xpack.canvas.workpadTemplates.creatingTemplateLabel": "テンプレート「{templateName}」から作成しています",
+ "xpack.canvas.workpadTemplates.searchPlaceholder": "テンプレートを検索",
"xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "説明",
"xpack.canvas.workpadTemplates.table.nameColumnTitle": "テンプレート名",
"xpack.canvas.workpadTemplates.table.tagsColumnTitle": "タグ",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 632c502d4ef55..f867407ff2d9b 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -6146,18 +6146,18 @@
"xpack.canvas.error.esService.indicesFetchErrorMessage": "无法提取 Elasticsearch 索引",
"xpack.canvas.error.RenderWithFn.renderErrorMessage": "呈现“{functionName}”失败。",
"xpack.canvas.error.repeatImage.missingMaxArgument": "如果提供 {emptyImageArgument},则必须设置 {maxArgument}",
- "xpack.canvas.error.workpadLoader.cloneFailureErrorMessage": "无法克隆 Workpad",
- "xpack.canvas.error.workpadLoader.deleteFailureErrorMessage": "无法删除所有 Workpad",
- "xpack.canvas.error.workpadLoader.findFailureErrorMessage": "无法查找 Workpad",
- "xpack.canvas.error.workpadLoader.uploadFailureErrorMessage": "无法上传 Workpad",
+ "xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "无法克隆 Workpad",
+ "xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "无法上传 Workpad",
+ "xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "无法删除所有 Workpad",
+ "xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "无法查找 Workpad",
+ "xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "仅接受 {JSON} 文件",
+ "xpack.canvas.error.useImportWorkpad.fileUploadFailureWithoutFileNameErrorMessage": "无法上传文件",
+ "xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage": "{CANVAS} Workpad 所需的某些属性缺失。 编辑 {JSON} 文件以提供正确的属性值,然后重试。",
"xpack.canvas.error.workpadRoutes.createFailureErrorMessage": "无法创建 Workpad",
"xpack.canvas.error.workpadRoutes.loadFailureErrorMessage": "无法加载具有以下 ID 的 Workpad",
- "xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage": "仅接受 {JSON} 文件",
- "xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage": "无法上传文件",
- "xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage": "{CANVAS} Workpad 所需的某些属性缺失。 编辑 {JSON} 文件以提供正确的属性值,然后重试。",
"xpack.canvas.errorComponent.description": "表达式失败,并显示消息:",
"xpack.canvas.errorComponent.title": "哎哟!表达式失败",
- "xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage": "无法上传“{fileName}”",
+ "xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage": "无法上传“{fileName}”",
"xpack.canvas.expression.cancelButtonLabel": "取消",
"xpack.canvas.expression.closeButtonLabel": "关闭",
"xpack.canvas.expression.learnLinkText": "学习表达式语法",
@@ -6492,6 +6492,12 @@
"xpack.canvas.helpMenu.description": "有关 {CANVAS} 特定信息",
"xpack.canvas.helpMenu.documentationLinkLabel": "{CANVAS} 文档",
"xpack.canvas.helpMenu.keyboardShortcutsLinkLabel": "快捷键",
+ "xpack.canvas.home.myWorkpadsTabLabel": "我的 Workpad",
+ "xpack.canvas.home.workpadTemplatesTabLabel": "模板",
+ "xpack.canvas.homeEmptyPrompt.emptyPromptGettingStartedDescription": "创建新的 Workpad、从模板入手或通过将 Workpad {JSON} 文件拖放到此处来导入。",
+ "xpack.canvas.homeEmptyPrompt.emptyPromptNewUserDescription": "{CANVAS} 新手?",
+ "xpack.canvas.homeEmptyPrompt.emptyPromptTitle": "添加您的首个 Workpad",
+ "xpack.canvas.homeEmptyPrompt.sampleDataLinkLabel": "添加您的首个 Workpad",
"xpack.canvas.keyboardShortcuts.bringFowardShortcutHelpText": "前移",
"xpack.canvas.keyboardShortcuts.bringToFrontShortcutHelpText": "置前",
"xpack.canvas.keyboardShortcuts.cloneShortcutHelpText": "克隆",
@@ -6942,6 +6948,7 @@
"xpack.canvas.units.time.hours": "{hours, plural, other {# 小时}}",
"xpack.canvas.units.time.minutes": "{minutes, plural, other {# 分钟}}",
"xpack.canvas.units.time.seconds": "{seconds, plural, other {# 秒}}",
+ "xpack.canvas.useCloneWorkpad.clonedWorkpadName": "{workpadName} 副本",
"xpack.canvas.varConfig.addButtonLabel": "添加变量",
"xpack.canvas.varConfig.addTooltipLabel": "添加变量",
"xpack.canvas.varConfig.copyActionButtonLabel": "复制代码片段",
@@ -7072,40 +7079,30 @@
"xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "缩放",
"xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "重置",
"xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%",
- "xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} 副本",
- "xpack.canvas.workpadLoader.cloneTooltip": "克隆 Workpad",
- "xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "正在创建 Workpad......",
- "xpack.canvas.workpadLoader.deleteButtonAriaLabel": "删除 {numberOfWorkpads} 个 Workpad",
- "xpack.canvas.workpadLoader.deleteButtonLabel": "删除 ({numberOfWorkpads})",
- "xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel": "删除",
- "xpack.canvas.workpadLoader.deleteModalDescription": "您无法恢复删除的 Workpad。",
- "xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle": "删除 {numberOfWorkpads} 个 Workpad?",
- "xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle": "删除 Workpad“{workpadName}”?",
- "xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription": "创建新的 Workpad、从模板入手或通过将 Workpad {JSON} 文件拖放到此处来导入。",
- "xpack.canvas.workpadLoader.emptyPromptNewUserDescription": "{CANVAS} 新手?",
- "xpack.canvas.workpadLoader.emptyPromptTitle": "添加您的首个 Workpad",
- "xpack.canvas.workpadLoader.exportButtonAriaLabel": "导出 {numberOfWorkpads} 个 Workpad",
- "xpack.canvas.workpadLoader.exportButtonLabel": "导出 ({numberOfWorkpads})",
- "xpack.canvas.workpadLoader.exportTooltip": "导出 Workpad",
- "xpack.canvas.workpadLoader.fetchLoadingDescription": "正在获取 Workpad......",
- "xpack.canvas.workpadLoader.filePickerPlaceholder": "导入 Workpad {JSON} 文件",
- "xpack.canvas.workpadLoader.loadWorkpadArialLabel": "加载 Workpad“{workpadName}”",
- "xpack.canvas.workpadLoader.noPermissionToCloneToolTip": "您无权克隆 Workpad",
- "xpack.canvas.workpadLoader.noPermissionToCreateToolTip": "您无权创建 Workpad",
- "xpack.canvas.workpadLoader.noPermissionToDeleteToolTip": "您无权删除 Workpad",
- "xpack.canvas.workpadLoader.noPermissionToUploadToolTip": "您无权上传 Workpad",
- "xpack.canvas.workpadLoader.sampleDataLinkLabel": "添加您的首个 Workpad",
- "xpack.canvas.workpadLoader.table.actionsColumnTitle": "操作",
- "xpack.canvas.workpadLoader.table.createdColumnTitle": "创建时间",
- "xpack.canvas.workpadLoader.table.nameColumnTitle": "Workpad 名称",
- "xpack.canvas.workpadLoader.table.updatedColumnTitle": "更新时间",
- "xpack.canvas.workpadManager.modalTitle": "{CANVAS} Workpad",
- "xpack.canvas.workpadManager.myWorkpadsTabLabel": "我的 Workpad",
- "xpack.canvas.workpadManager.workpadTemplatesTabLabel": "模板",
- "xpack.canvas.workpadSearch.searchPlaceholder": "查找 Workpad",
- "xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel": "克隆 Workpad 模板“{templateName}”",
- "xpack.canvas.workpadTemplate.creatingTemplateLabel": "正在从模板“{templateName}”创建",
- "xpack.canvas.workpadTemplate.searchPlaceholder": "查找模板",
+ "xpack.canvas.workpadImport.filePickerPlaceholder": "导入 Workpad {JSON} 文件",
+ "xpack.canvas.workpadTable.searchPlaceholder": "查找 Workpad",
+ "xpack.canvas.workpadTable.cloneTooltip": "克隆 Workpad",
+ "xpack.canvas.workpadTable.exportTooltip": "导出 Workpad",
+ "xpack.canvas.workpadTable.loadWorkpadArialLabel": "加载 Workpad“{workpadName}”",
+ "xpack.canvas.workpadTable.noPermissionToCloneToolTip": "您无权克隆 Workpad",
+ "xpack.canvas.workpadTable.table.actionsColumnTitle": "操作",
+ "xpack.canvas.workpadTable.table.createdColumnTitle": "创建时间",
+ "xpack.canvas.workpadTable.table.nameColumnTitle": "Workpad 名称",
+ "xpack.canvas.workpadTable.table.updatedColumnTitle": "更新时间",
+ "xpack.canvas.workpadTableTools.deleteButtonAriaLabel": "删除 {numberOfWorkpads} 个 Workpad",
+ "xpack.canvas.workpadTableTools.deleteButtonLabel": "删除 ({numberOfWorkpads})",
+ "xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel": "删除",
+ "xpack.canvas.workpadTableTools.deleteModalDescription": "您无法恢复删除的 Workpad。",
+ "xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle": "删除 {numberOfWorkpads} 个 Workpad?",
+ "xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle": "删除 Workpad“{workpadName}”?",
+ "xpack.canvas.workpadTableTools.exportButtonAriaLabel": "导出 {numberOfWorkpads} 个 Workpad",
+ "xpack.canvas.workpadTableTools.exportButtonLabel": "导出 ({numberOfWorkpads})",
+ "xpack.canvas.workpadTableTools.noPermissionToCreateToolTip": "您无权创建 Workpad",
+ "xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip": "您无权删除 Workpad",
+ "xpack.canvas.workpadTableTools.noPermissionToUploadToolTip": "您无权上传 Workpad",
+ "xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel": "克隆 Workpad 模板“{templateName}”",
+ "xpack.canvas.workpadTemplates.creatingTemplateLabel": "正在从模板“{templateName}”创建",
+ "xpack.canvas.workpadTemplates.searchPlaceholder": "查找模板",
"xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "描述",
"xpack.canvas.workpadTemplates.table.nameColumnTitle": "模板名称",
"xpack.canvas.workpadTemplates.table.tagsColumnTitle": "标签",
diff --git a/x-pack/test/accessibility/apps/canvas.ts b/x-pack/test/accessibility/apps/canvas.ts
index a79fb7b60e76a..609c8bf5bb1ae 100644
--- a/x-pack/test/accessibility/apps/canvas.ts
+++ b/x-pack/test/accessibility/apps/canvas.ts
@@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('loads workpads', async function () {
await retry.waitFor(
'canvas workpads visible',
- async () => await testSubjects.exists('canvasWorkpadLoaderTable')
+ async () => await testSubjects.exists('canvasWorkpadTable')
);
await a11y.testAppSnapshot();
});
diff --git a/x-pack/test/functional/apps/canvas/smoke_test.js b/x-pack/test/functional/apps/canvas/smoke_test.js
index 5280ad0118fba..fcc04aafdbcd8 100644
--- a/x-pack/test/functional/apps/canvas/smoke_test.js
+++ b/x-pack/test/functional/apps/canvas/smoke_test.js
@@ -17,7 +17,7 @@ export default function canvasSmokeTest({ getService, getPageObjects }) {
describe('smoke test', function () {
this.tags('includeFirefox');
- const workpadListSelector = 'canvasWorkpadLoaderTable > canvasWorkpadLoaderWorkpad';
+ const workpadListSelector = 'canvasWorkpadTable > canvasWorkpadTableWorkpad';
const testWorkpadId = 'workpad-1705f884-6224-47de-ba49-ca224fe6ec31';
before(async () => {
diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts
index 0e0203046fd16..df92c1c398d93 100644
--- a/x-pack/test/functional/page_objects/canvas_page.ts
+++ b/x-pack/test/functional/page_objects/canvas_page.ts
@@ -39,7 +39,7 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo
* to load the workpad. Resolves once the workpad is in the DOM
*/
async loadFirstWorkpad(workpadName: string) {
- const elem = await testSubjects.find('canvasWorkpadLoaderWorkpad');
+ const elem = await testSubjects.find('canvasWorkpadTableWorkpad');
const text = await elem.getVisibleText();
expect(text).to.be(workpadName);
await elem.click();
From 86fb2cc90e38e5e9c783c94b9904387c6b06dea6 Mon Sep 17 00:00:00 2001
From: Patrick Mueller
Date: Tue, 22 Jun 2021 15:18:35 -0400
Subject: [PATCH 092/163] [actions] add rule saved object reference to action
execution event log doc (#101526)
resolves https://github.com/elastic/kibana/issues/99225
Prior to this PR, when an alerting connection action was executed, the event
log document generated did not contain a reference to the originating rule.
This makes it difficult to diagnose problems with connector errors, since
the error is often in the parameters specified in the actions in the alert.
In this PR, a reference to the alerting rule is added to the saved_objects
field in the event document for these events.
---
.../actions/server/actions_client.test.ts | 64 ++++++++++++++
.../plugins/actions/server/actions_client.ts | 9 +-
.../server/create_execute_function.test.ts | 56 ++++++++++++
.../actions/server/create_execute_function.ts | 5 +-
.../actions/server/lib/action_executor.ts | 13 +++
.../server/lib/related_saved_objects.test.ts | 86 +++++++++++++++++++
.../server/lib/related_saved_objects.ts | 31 +++++++
.../server/lib/task_runner_factory.test.ts | 76 ++++++++++++++++
.../actions/server/lib/task_runner_factory.ts | 4 +-
.../actions/server/routes/execute.test.ts | 2 +
.../plugins/actions/server/routes/execute.ts | 1 +
.../server/routes/legacy/execute.test.ts | 2 +
.../actions/server/routes/legacy/execute.ts | 1 +
.../server/saved_objects/mappings.json | 4 +
.../create_execution_handler.test.ts | 32 +++++++
.../task_runner/create_execution_handler.ts | 12 ++-
.../server/task_runner/task_runner.test.ts | 32 +++++++
x-pack/plugins/event_log/README.md | 29 ++++---
18 files changed, 442 insertions(+), 17 deletions(-)
create mode 100644 x-pack/plugins/actions/server/lib/related_saved_objects.test.ts
create mode 100644 x-pack/plugins/actions/server/lib/related_saved_objects.ts
diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts
index 3b91b07eb30f4..16388b2faf52e 100644
--- a/x-pack/plugins/actions/server/actions_client.test.ts
+++ b/x-pack/plugins/actions/server/actions_client.test.ts
@@ -1676,6 +1676,70 @@ describe('execute()', () => {
name: 'my name',
},
});
+
+ await expect(
+ actionsClient.execute({
+ actionId,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ },
+ ],
+ })
+ ).resolves.toMatchObject({ status: 'ok', actionId });
+
+ expect(actionExecutor.execute).toHaveBeenCalledWith({
+ actionId,
+ request,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ },
+ ],
+ });
+
+ await expect(
+ actionsClient.execute({
+ actionId,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ namespace: 'some-namespace',
+ },
+ ],
+ })
+ ).resolves.toMatchObject({ status: 'ok', actionId });
+
+ expect(actionExecutor.execute).toHaveBeenCalledWith({
+ actionId,
+ request,
+ params: {
+ name: 'my name',
+ },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ typeId: 'some-type-id',
+ type: 'some-type',
+ namespace: 'some-namespace',
+ },
+ ],
+ });
});
});
diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts
index 449d218ed5ae0..f8d13cdafa755 100644
--- a/x-pack/plugins/actions/server/actions_client.ts
+++ b/x-pack/plugins/actions/server/actions_client.ts
@@ -469,6 +469,7 @@ export class ActionsClient {
actionId,
params,
source,
+ relatedSavedObjects,
}: Omit): Promise> {
if (
(await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) ===
@@ -476,7 +477,13 @@ export class ActionsClient {
) {
await this.authorization.ensureAuthorized('execute');
}
- return this.actionExecutor.execute({ actionId, params, source, request: this.request });
+ return this.actionExecutor.execute({
+ actionId,
+ params,
+ source,
+ request: this.request,
+ relatedSavedObjects,
+ });
}
public async enqueueExecution(options: EnqueueExecutionOptions): Promise {
diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts
index 4cacba6dc880a..ee8064d2aadc5 100644
--- a/x-pack/plugins/actions/server/create_execute_function.test.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.test.ts
@@ -83,6 +83,62 @@ describe('execute()', () => {
});
});
+ test('schedules the action with all given parameters and relatedSavedObjects', async () => {
+ const actionTypeRegistry = actionTypeRegistryMock.create();
+ const executeFn = createExecutionEnqueuerFunction({
+ taskManager: mockTaskManager,
+ actionTypeRegistry,
+ isESOCanEncrypt: true,
+ preconfiguredActions: [],
+ });
+ savedObjectsClient.get.mockResolvedValueOnce({
+ id: '123',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'mock-action',
+ },
+ references: [],
+ });
+ savedObjectsClient.create.mockResolvedValueOnce({
+ id: '234',
+ type: 'action_task_params',
+ attributes: {},
+ references: [],
+ });
+ await executeFn(savedObjectsClient, {
+ id: '123',
+ params: { baz: false },
+ spaceId: 'default',
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ source: asHttpRequestExecutionSource(request),
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ namespace: 'some-namespace',
+ type: 'some-type',
+ typeId: 'some-typeId',
+ },
+ ],
+ });
+ expect(savedObjectsClient.create).toHaveBeenCalledWith(
+ 'action_task_params',
+ {
+ actionId: '123',
+ params: { baz: false },
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ namespace: 'some-namespace',
+ type: 'some-type',
+ typeId: 'some-typeId',
+ },
+ ],
+ },
+ {}
+ );
+ });
+
test('schedules the action with all given parameters with a preconfigured action', async () => {
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts
index 4f3ffbef36c6e..7dcd66c711bdd 100644
--- a/x-pack/plugins/actions/server/create_execute_function.ts
+++ b/x-pack/plugins/actions/server/create_execute_function.ts
@@ -11,6 +11,7 @@ import { RawAction, ActionTypeRegistryContract, PreConfiguredAction } from './ty
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects';
import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor';
import { isSavedObjectExecutionSource } from './lib';
+import { RelatedSavedObjects } from './lib/related_saved_objects';
interface CreateExecuteFunctionOptions {
taskManager: TaskManagerStartContract;
@@ -23,6 +24,7 @@ export interface ExecuteOptions extends Pick {
request: KibanaRequest;
params: Record;
source?: ActionExecutionSource;
+ relatedSavedObjects?: RelatedSavedObjects;
}
export type ActionExecutorContract = PublicMethodsOf;
@@ -68,6 +70,7 @@ export class ActionExecutor {
params,
request,
source,
+ relatedSavedObjects,
}: ExecuteOptions): Promise> {
if (!this.isInitialized) {
throw new Error('ActionExecutor not initialized');
@@ -154,6 +157,16 @@ export class ActionExecutor {
},
};
+ for (const relatedSavedObject of relatedSavedObjects || []) {
+ event.kibana?.saved_objects?.push({
+ rel: SAVED_OBJECT_REL_PRIMARY,
+ type: relatedSavedObject.type,
+ id: relatedSavedObject.id,
+ type_id: relatedSavedObject.typeId,
+ namespace: relatedSavedObject.namespace,
+ });
+ }
+
eventLogger.startTiming(event);
let rawResult: ActionTypeExecutorResult;
try {
diff --git a/x-pack/plugins/actions/server/lib/related_saved_objects.test.ts b/x-pack/plugins/actions/server/lib/related_saved_objects.test.ts
new file mode 100644
index 0000000000000..8fd13d1375697
--- /dev/null
+++ b/x-pack/plugins/actions/server/lib/related_saved_objects.test.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 { validatedRelatedSavedObjects } from './related_saved_objects';
+import { loggingSystemMock } from '../../../../../src/core/server/mocks';
+import { Logger } from '../../../../../src/core/server';
+
+const loggerMock = loggingSystemMock.createLogger();
+
+describe('related_saved_objects', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('validates valid objects', () => {
+ ensureValid(loggerMock, undefined);
+ ensureValid(loggerMock, []);
+ ensureValid(loggerMock, [
+ {
+ id: 'some-id',
+ type: 'some-type',
+ },
+ ]);
+ ensureValid(loggerMock, [
+ {
+ id: 'some-id',
+ type: 'some-type',
+ typeId: 'some-type-id',
+ },
+ ]);
+ ensureValid(loggerMock, [
+ {
+ id: 'some-id',
+ type: 'some-type',
+ namespace: 'some-namespace',
+ },
+ ]);
+ ensureValid(loggerMock, [
+ {
+ id: 'some-id',
+ type: 'some-type',
+ typeId: 'some-type-id',
+ namespace: 'some-namespace',
+ },
+ ]);
+ ensureValid(loggerMock, [
+ {
+ id: 'some-id',
+ type: 'some-type',
+ },
+ {
+ id: 'some-id-2',
+ type: 'some-type-2',
+ },
+ ]);
+ });
+});
+
+it('handles invalid objects', () => {
+ ensureInvalid(loggerMock, 42);
+ ensureInvalid(loggerMock, {});
+ ensureInvalid(loggerMock, [{}]);
+ ensureInvalid(loggerMock, [{ id: 'some-id' }]);
+ ensureInvalid(loggerMock, [{ id: 42 }]);
+ ensureInvalid(loggerMock, [{ id: 'some-id', type: 'some-type', x: 42 }]);
+});
+
+function ensureValid(logger: Logger, savedObjects: unknown) {
+ const result = validatedRelatedSavedObjects(logger, savedObjects);
+ expect(result).toEqual(savedObjects === undefined ? [] : savedObjects);
+ expect(loggerMock.warn).not.toHaveBeenCalled();
+}
+
+function ensureInvalid(logger: Logger, savedObjects: unknown) {
+ const result = validatedRelatedSavedObjects(logger, savedObjects);
+ expect(result).toEqual([]);
+
+ const message = loggerMock.warn.mock.calls[0][0];
+ expect(message).toMatch(
+ /ignoring invalid related saved objects: expected value of type \[array\] but got/
+ );
+}
diff --git a/x-pack/plugins/actions/server/lib/related_saved_objects.ts b/x-pack/plugins/actions/server/lib/related_saved_objects.ts
new file mode 100644
index 0000000000000..160587a3a9a8b
--- /dev/null
+++ b/x-pack/plugins/actions/server/lib/related_saved_objects.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { schema, TypeOf } from '@kbn/config-schema';
+import { Logger } from '../../../../../src/core/server';
+
+export type RelatedSavedObjects = TypeOf;
+
+const RelatedSavedObjectsSchema = schema.arrayOf(
+ schema.object({
+ namespace: schema.maybe(schema.string({ minLength: 1 })),
+ id: schema.string({ minLength: 1 }),
+ type: schema.string({ minLength: 1 }),
+ // optional; for SO types like action/alert that have type id's
+ typeId: schema.maybe(schema.string({ minLength: 1 })),
+ }),
+ { defaultValue: [] }
+);
+
+export function validatedRelatedSavedObjects(logger: Logger, data: unknown): RelatedSavedObjects {
+ try {
+ return RelatedSavedObjectsSchema.validate(data);
+ } catch (err) {
+ logger.warn(`ignoring invalid related saved objects: ${err.message}`);
+ return [];
+ }
+}
diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
index 229324c1f0df3..2292994e3ccfd 100644
--- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
+++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts
@@ -126,6 +126,7 @@ test('executes the task by calling the executor with proper parameters', async (
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
actionId: '2',
params: { baz: true },
+ relatedSavedObjects: [],
request: expect.objectContaining({
headers: {
// base64 encoded "123:abc"
@@ -247,6 +248,7 @@ test('uses API key when provided', async () => {
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
actionId: '2',
params: { baz: true },
+ relatedSavedObjects: [],
request: expect.objectContaining({
headers: {
// base64 encoded "123:abc"
@@ -262,6 +264,79 @@ test('uses API key when provided', async () => {
);
});
+test('uses relatedSavedObjects when provided', async () => {
+ const taskRunner = taskRunnerFactory.create({
+ taskInstance: mockedTaskInstance,
+ });
+
+ mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' });
+ spaceIdToNamespace.mockReturnValueOnce('namespace-test');
+ mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
+ id: '3',
+ type: 'action_task_params',
+ attributes: {
+ actionId: '2',
+ params: { baz: true },
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ relatedSavedObjects: [{ id: 'some-id', type: 'some-type' }],
+ },
+ references: [],
+ });
+
+ await taskRunner.run();
+
+ expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
+ actionId: '2',
+ params: { baz: true },
+ relatedSavedObjects: [
+ {
+ id: 'some-id',
+ type: 'some-type',
+ },
+ ],
+ request: expect.objectContaining({
+ headers: {
+ // base64 encoded "123:abc"
+ authorization: 'ApiKey MTIzOmFiYw==',
+ },
+ }),
+ });
+});
+
+test('sanitizes invalid relatedSavedObjects when provided', async () => {
+ const taskRunner = taskRunnerFactory.create({
+ taskInstance: mockedTaskInstance,
+ });
+
+ mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' });
+ spaceIdToNamespace.mockReturnValueOnce('namespace-test');
+ mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
+ id: '3',
+ type: 'action_task_params',
+ attributes: {
+ actionId: '2',
+ params: { baz: true },
+ apiKey: Buffer.from('123:abc').toString('base64'),
+ relatedSavedObjects: [{ Xid: 'some-id', type: 'some-type' }],
+ },
+ references: [],
+ });
+
+ await taskRunner.run();
+
+ expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
+ actionId: '2',
+ params: { baz: true },
+ relatedSavedObjects: [],
+ request: expect.objectContaining({
+ headers: {
+ // base64 encoded "123:abc"
+ authorization: 'ApiKey MTIzOmFiYw==',
+ },
+ }),
+ });
+});
+
test(`doesn't use API key when not provided`, async () => {
const factory = new TaskRunnerFactory(mockedActionExecutor);
factory.initialize(taskRunnerFactoryInitializerParams);
@@ -284,6 +359,7 @@ test(`doesn't use API key when not provided`, async () => {
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
actionId: '2',
params: { baz: true },
+ relatedSavedObjects: [],
request: expect.objectContaining({
headers: {},
}),
diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts
index cf4b1576f2778..0515963ab82f4 100644
--- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts
+++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts
@@ -30,6 +30,7 @@ import {
} from '../types';
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../constants/saved_objects';
import { asSavedObjectExecutionSource } from './action_execution_source';
+import { validatedRelatedSavedObjects } from './related_saved_objects';
export interface TaskRunnerContext {
logger: Logger;
@@ -77,7 +78,7 @@ export class TaskRunnerFactory {
const namespace = spaceIdToNamespace(spaceId);
const {
- attributes: { actionId, params, apiKey },
+ attributes: { actionId, params, apiKey, relatedSavedObjects },
references,
} = await encryptedSavedObjectsClient.getDecryptedAsInternalUser(
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
@@ -117,6 +118,7 @@ export class TaskRunnerFactory {
actionId,
request: fakeRequest,
...getSourceFromReferences(references),
+ relatedSavedObjects: validatedRelatedSavedObjects(logger, relatedSavedObjects),
});
} catch (e) {
if (e instanceof ActionTypeDisabledError) {
diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts
index 4b12bf3111c1f..54e10698e5af9 100644
--- a/x-pack/plugins/actions/server/routes/execute.test.ts
+++ b/x-pack/plugins/actions/server/routes/execute.test.ts
@@ -65,6 +65,7 @@ describe('executeActionRoute', () => {
someData: 'data',
},
source: asHttpRequestExecutionSource(req),
+ relatedSavedObjects: [],
});
expect(res.ok).toHaveBeenCalled();
@@ -101,6 +102,7 @@ describe('executeActionRoute', () => {
expect(actionsClient.execute).toHaveBeenCalledWith({
actionId: '1',
params: {},
+ relatedSavedObjects: [],
source: asHttpRequestExecutionSource(req),
});
diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts
index 377fe1215b3fb..7e8110365e87a 100644
--- a/x-pack/plugins/actions/server/routes/execute.ts
+++ b/x-pack/plugins/actions/server/routes/execute.ts
@@ -53,6 +53,7 @@ export const executeActionRoute = (
params,
actionId: id,
source: asHttpRequestExecutionSource(req),
+ relatedSavedObjects: [],
});
return body
? res.ok({
diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.test.ts b/x-pack/plugins/actions/server/routes/legacy/execute.test.ts
index 2ac53ddaaedf6..05b71819911a3 100644
--- a/x-pack/plugins/actions/server/routes/legacy/execute.test.ts
+++ b/x-pack/plugins/actions/server/routes/legacy/execute.test.ts
@@ -63,6 +63,7 @@ describe('executeActionRoute', () => {
someData: 'data',
},
source: asHttpRequestExecutionSource(req),
+ relatedSavedObjects: [],
});
expect(res.ok).toHaveBeenCalled();
@@ -100,6 +101,7 @@ describe('executeActionRoute', () => {
actionId: '1',
params: {},
source: asHttpRequestExecutionSource(req),
+ relatedSavedObjects: [],
});
expect(res.ok).not.toHaveBeenCalled();
diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.ts b/x-pack/plugins/actions/server/routes/legacy/execute.ts
index f6ddec1d01c20..d7ed8d2e15604 100644
--- a/x-pack/plugins/actions/server/routes/legacy/execute.ts
+++ b/x-pack/plugins/actions/server/routes/legacy/execute.ts
@@ -48,6 +48,7 @@ export const executeActionRoute = (
params,
actionId: id,
source: asHttpRequestExecutionSource(req),
+ relatedSavedObjects: [],
});
return body
? res.ok({
diff --git a/x-pack/plugins/actions/server/saved_objects/mappings.json b/x-pack/plugins/actions/server/saved_objects/mappings.json
index c598b96ba2451..57f801ae9a075 100644
--- a/x-pack/plugins/actions/server/saved_objects/mappings.json
+++ b/x-pack/plugins/actions/server/saved_objects/mappings.json
@@ -35,6 +35,10 @@
},
"apiKey": {
"type": "binary"
+ },
+ "relatedSavedObjects": {
+ "enabled": false,
+ "type": "object"
}
}
}
diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts
index 25f0656163f5d..033ffcceb6a0a 100644
--- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts
@@ -135,6 +135,14 @@ test('enqueues execution per selected action', async () => {
"foo": true,
"stateVal": "My goes here",
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": "test1",
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
@@ -247,6 +255,14 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () =>
id: '1',
type: 'alert',
}),
+ relatedSavedObjects: [
+ {
+ id: '1',
+ namespace: 'test1',
+ type: 'alert',
+ typeId: 'test',
+ },
+ ],
spaceId: 'test1',
apiKey: createExecutionHandlerParams.apiKey,
});
@@ -327,6 +343,14 @@ test('context attribute gets parameterized', async () => {
"foo": true,
"stateVal": "My goes here",
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": "test1",
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
@@ -360,6 +384,14 @@ test('state attribute gets parameterized', async () => {
"foo": true,
"stateVal": "My state-val goes here",
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": "test1",
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts
index c3a36297c217a..968fff540dc03 100644
--- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts
+++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts
@@ -157,6 +157,8 @@ export function createExecutionHandler<
continue;
}
+ const namespace = spaceId === 'default' ? {} : { namespace: spaceId };
+
// TODO would be nice to add the action name here, but it's not available
const actionLabel = `${action.actionTypeId}:${action.id}`;
const actionsClient = await actionsPlugin.getActionsClientWithRequest(request);
@@ -169,10 +171,16 @@ export function createExecutionHandler<
id: alertId,
type: 'alert',
}),
+ relatedSavedObjects: [
+ {
+ id: alertId,
+ type: 'alert',
+ namespace: namespace.namespace,
+ typeId: alertType.id,
+ },
+ ],
});
- const namespace = spaceId === 'default' ? {} : { namespace: spaceId };
-
const event: IEvent = {
event: {
action: EVENT_LOG_ACTIONS.executeAction,
diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
index 39a45584631d2..8ab267a5610d3 100644
--- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
@@ -352,6 +352,14 @@ describe('Task Runner', () => {
"params": Object {
"foo": true,
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": undefined,
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
@@ -1098,6 +1106,14 @@ describe('Task Runner', () => {
"params": Object {
"foo": true,
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": undefined,
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
@@ -1634,6 +1650,14 @@ describe('Task Runner', () => {
"params": Object {
"isResolved": true,
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": undefined,
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
@@ -1826,6 +1850,14 @@ describe('Task Runner', () => {
"params": Object {
"isResolved": true,
},
+ "relatedSavedObjects": Array [
+ Object {
+ "id": "1",
+ "namespace": undefined,
+ "type": "alert",
+ "typeId": "test",
+ },
+ ],
"source": Object {
"source": Object {
"id": "1",
diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md
index 032f77543acb9..ffbd20dd6f2be 100644
--- a/x-pack/plugins/event_log/README.md
+++ b/x-pack/plugins/event_log/README.md
@@ -131,7 +131,7 @@ Below is a document in the expected structure, with descriptions of the fields:
instance_id: "alert instance id, for relevant documents",
action_group_id: "alert action group, for relevant documents",
action_subgroup: "alert action subgroup, for relevant documents",
- status: "overall alert status, after alert execution",
+ status: "overall alert status, after rule execution",
},
saved_objects: [
{
@@ -160,21 +160,26 @@ plugins:
- `action: execute-via-http` - generated when an action is executed via HTTP request
- `provider: alerting`
- - `action: execute` - generated when an alert executor runs
- - `action: execute-action` - generated when an alert schedules an action to run
- - `action: new-instance` - generated when an alert has a new instance id that is active
- - `action: recovered-instance` - generated when an alert has a previously active instance id that is no longer active
- - `action: active-instance` - generated when an alert determines an instance id is active
+ - `action: execute` - generated when a rule executor runs
+ - `action: execute-action` - generated when a rule schedules an action to run
+ - `action: new-instance` - generated when a rule has a new instance id that is active
+ - `action: recovered-instance` - generated when a rule has a previously active instance id that is no longer active
+ - `action: active-instance` - generated when a rule determines an instance id is active
For the `saved_objects` array elements, these are references to saved objects
-associated with the event. For the `alerting` provider, those are alert saved
-ojects and for the `actions` provider those are action saved objects. The
-`alerts:execute-action` event includes both the alert and action saved object
-references. For that event, only the alert reference has the optional `rel`
+associated with the event. For the `alerting` provider, those are rule saved
+ojects and for the `actions` provider those are connector saved objects. The
+`alerts:execute-action` event includes both the rule and connector saved object
+references. For that event, only the rule reference has the optional `rel`
property with a `primary` value. This property is used when searching the
event log to indicate which saved objects should be directly searchable via
-saved object references. For the `alerts:execute-action` event, searching
-only via the alert saved object reference will return the event.
+saved object references. For the `alerts:execute-action` event, only searching
+via the rule saved object reference will return the event; searching via the
+connector save object reference will **NOT** return the event. The
+`actions:execute` event also includes both the rule and connector saved object
+references, and both of them have the `rel` property with a `primary` value,
+allowing those events to be returned in searches of either the rule or
+connector.
## Event Log index - associated resources
From b386ce149a8d2175005fbe5045ee48bf8c56f977 Mon Sep 17 00:00:00 2001
From: Constance
Date: Tue, 22 Jun 2021 12:27:27 -0700
Subject: [PATCH 093/163] [App Search] Convert Schema pages to new page
template (#102846)
* Convert Schema page to new page template
+ update empty state - remove panel wrapper, add create schema field modal
* Convert ReindexJob view to new page template
+ remove breadcrumb prop
* Convert Meta Engine Schema view to new page template
* Update routers
* [Polish] Misc Davey Schema UI tweaks
- see https://github.com/elastic/kibana/pull/101958/files
+ change color away from secondary, since that's going away in EUI at some point
* [UX] Fix SchemaAddFieldModal stuttering on first new schema field add
- With the new template, transitioning from the empty state to the filled schema state causes the modal to stutter due to the component rerender
- Changing the page to not instantly react/update `hasSchema` when local schema state changes but instead to wait for the server call to finish and for cachedSchema to update fixes the UX problem
* [UI polish] Revert button color change per Davey's feedback
---
.../components/engine/engine_router.tsx | 10 +-
.../schema/components/empty_state.test.tsx | 11 ++
.../schema/components/empty_state.tsx | 16 ++-
.../schema/reindex_job/reindex_job.test.tsx | 17 +--
.../schema/reindex_job/reindex_job.tsx | 61 ++++----
.../components/schema/schema_logic.test.ts | 8 +-
.../components/schema/schema_logic.ts | 5 +-
.../components/schema/schema_router.tsx | 12 +-
.../schema/views/meta_engine_schema.test.tsx | 10 +-
.../schema/views/meta_engine_schema.tsx | 131 +++++++++---------
.../components/schema/views/schema.test.tsx | 28 +---
.../components/schema/views/schema.tsx | 50 +++----
.../shared/schema/add_field_modal/index.tsx | 10 +-
13 files changed, 172 insertions(+), 197 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
index fc057858426d2..91a21847107a9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
@@ -109,6 +109,11 @@ export const EngineRouter: React.FC = () => {
)}
+ {canViewEngineSchema && (
+
+
+
+ )}
{canManageEngineSearchUi && (
@@ -121,11 +126,6 @@ export const EngineRouter: React.FC = () => {
)}
{/* TODO: Remove layout once page template migration is over */}
}>
- {canViewEngineSchema && (
-
-
-
- )}
{canManageEngineCurations && (
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx
index ea658c741b8a0..1b353f17855d2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.test.tsx
@@ -5,12 +5,16 @@
* 2.0.
*/
+import { setMockValues } from '../../../../__mocks__/kea_logic';
+
import React from 'react';
import { shallow } from 'enzyme';
import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+import { SchemaAddFieldModal } from '../../../../shared/schema';
+
import { EmptyState } from './';
describe('EmptyState', () => {
@@ -24,4 +28,11 @@ describe('EmptyState', () => {
expect.stringContaining('#indexing-documents-guide-schema')
);
});
+
+ it('renders a modal that lets a user add a new schema field', () => {
+ setMockValues({ isModalOpen: true });
+ const wrapper = shallow( );
+
+ expect(wrapper.find(SchemaAddFieldModal)).toHaveLength(1);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx
index 6d7dd198d5eef..ad9285c7b8fef 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/empty_state.tsx
@@ -7,14 +7,21 @@
import React from 'react';
-import { EuiPanel, EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+import { useValues, useActions } from 'kea';
+
+import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { SchemaAddFieldModal } from '../../../../shared/schema';
import { DOCS_PREFIX } from '../../../routes';
+import { SchemaLogic } from '../schema_logic';
export const EmptyState: React.FC = () => {
+ const { isModalOpen } = useValues(SchemaLogic);
+ const { addSchemaField, closeModal } = useActions(SchemaLogic);
+
return (
-
+ <>
{
}
/>
-
+ {isModalOpen && (
+
+ )}
+ >
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx
index e76ab60005231..4dd7a869ca27e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.test.tsx
@@ -14,15 +14,11 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { Loading } from '../../../../shared/loading';
import { SchemaErrorsAccordion } from '../../../../shared/schema';
import { ReindexJob } from './';
describe('ReindexJob', () => {
- const props = {
- schemaBreadcrumb: ['Engines', 'some-engine', 'Schema'],
- };
const values = {
dataLoading: false,
fieldCoercionErrors: {},
@@ -43,27 +39,20 @@ describe('ReindexJob', () => {
});
it('renders', () => {
- const wrapper = shallow( );
+ const wrapper = shallow( );
expect(wrapper.find(SchemaErrorsAccordion)).toHaveLength(1);
expect(wrapper.find(SchemaErrorsAccordion).prop('generateViewPath')).toHaveLength(1);
});
it('calls loadReindexJob on page load', () => {
- shallow( );
+ shallow( );
expect(actions.loadReindexJob).toHaveBeenCalledWith('abc1234567890');
});
- it('renders a loading state', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
it('renders schema errors with links to document pages', () => {
- const wrapper = shallow( );
+ const wrapper = shallow( );
const generateViewPath = wrapper
.find(SchemaErrorsAccordion)
.prop('generateViewPath') as Function;
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx
index 576b4ae11603b..b0a8cbd25f8b0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/reindex_job/reindex_job.tsx
@@ -10,25 +10,17 @@ import { useParams } from 'react-router-dom';
import { useActions, useValues } from 'kea';
-import { EuiPageHeader, EuiPageContentBody } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../../shared/flash_messages';
-import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome';
-import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs';
-import { Loading } from '../../../../shared/loading';
import { SchemaErrorsAccordion } from '../../../../shared/schema';
-
import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../../routes';
-import { EngineLogic, generateEnginePath } from '../../engine';
+import { EngineLogic, generateEnginePath, getEngineBreadcrumbs } from '../../engine';
+import { AppSearchPageTemplate } from '../../layout';
+import { SCHEMA_TITLE } from '../constants';
import { ReindexJobLogic } from './reindex_job_logic';
-interface Props {
- schemaBreadcrumb: BreadcrumbTrail;
-}
-
-export const ReindexJob: React.FC = ({ schemaBreadcrumb }) => {
+export const ReindexJob: React.FC = () => {
const { reindexJobId } = useParams() as { reindexJobId: string };
const { loadReindexJob } = useActions(ReindexJobLogic);
const { dataLoading, fieldCoercionErrors } = useValues(ReindexJobLogic);
@@ -40,34 +32,29 @@ export const ReindexJob: React.FC = ({ schemaBreadcrumb }) => {
loadReindexJob(reindexJobId);
}, [reindexJobId]);
- if (dataLoading) return ;
-
return (
- <>
-
-
+
+ generateEnginePath(ENGINE_DOCUMENT_DETAIL_PATH, { documentId })
+ }
/>
-
-
-
- generateEnginePath(ENGINE_DOCUMENT_DETAIL_PATH, { documentId })
- }
- />
-
- >
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.test.ts
index 7687296cf9f83..dcc5747b0d32f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.test.ts
@@ -140,13 +140,13 @@ describe('SchemaLogic', () => {
describe('selectors', () => {
describe('hasSchema', () => {
- it('returns true when the schema obj has items', () => {
- mountAndSetSchema({ schema: { test: SchemaType.Text } });
+ it('returns true when the cached server schema obj has items', () => {
+ mount({ cachedSchema: { test: SchemaType.Text } });
expect(SchemaLogic.values.hasSchema).toEqual(true);
});
- it('returns false when the schema obj is empty', () => {
- mountAndSetSchema({ schema: {} });
+ it('returns false when the cached server schema obj is empty', () => {
+ mount({ schema: {} });
expect(SchemaLogic.values.hasSchema).toEqual(false);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.ts
index 3215a46c8e299..3dcafd6782afd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_logic.ts
@@ -108,7 +108,10 @@ export const SchemaLogic = kea>({
],
},
selectors: {
- hasSchema: [(selectors) => [selectors.schema], (schema) => Object.keys(schema).length > 0],
+ hasSchema: [
+ (selectors) => [selectors.cachedSchema],
+ (cachedSchema) => Object.keys(cachedSchema).length > 0,
+ ],
hasSchemaChanged: [
(selectors) => [selectors.schema, selectors.cachedSchema],
(schema, cachedSchema) => !isEqual(schema, cachedSchema),
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx
index bfa346fee468b..d358c489593c5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_router.tsx
@@ -10,27 +10,21 @@ import { Route, Switch } from 'react-router-dom';
import { useValues } from 'kea';
-import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { ENGINE_REINDEX_JOB_PATH } from '../../routes';
-import { EngineLogic, getEngineBreadcrumbs } from '../engine';
+import { EngineLogic } from '../engine';
-import { SCHEMA_TITLE } from './constants';
import { ReindexJob } from './reindex_job';
import { Schema, MetaEngineSchema } from './views';
export const SchemaRouter: React.FC = () => {
const { isMetaEngine } = useValues(EngineLogic);
- const schemaBreadcrumb = getEngineBreadcrumbs([SCHEMA_TITLE]);
return (
-
-
-
-
- {isMetaEngine ? : }
+
+ {isMetaEngine ? : }
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx
index 1d677ad08db43..60a0513b774fd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx
@@ -7,6 +7,7 @@
import { setMockValues, setMockActions } from '../../../../__mocks__/kea_logic';
import '../../../../__mocks__/shallow_useeffect.mock';
+import '../../../__mocks__/engine_logic.mock';
import React from 'react';
@@ -14,8 +15,6 @@ import { shallow } from 'enzyme';
import { EuiCallOut } from '@elastic/eui';
-import { Loading } from '../../../../shared/loading';
-
import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components';
import { MetaEngineSchema } from './';
@@ -46,13 +45,6 @@ describe('MetaEngineSchema', () => {
expect(actions.loadSchema).toHaveBeenCalled();
});
- it('renders a loading state', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
it('renders an inactive fields callout & table when source engines have schema conflicts', () => {
setMockValues({ ...values, hasConflicts: true, conflictingFieldsCount: 5 });
const wrapper = shallow( );
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx
index 4c0235cf81129..2eb8bac00a040 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx
@@ -9,14 +9,15 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
-import { EuiPageHeader, EuiPageContentBody, EuiCallOut, EuiSpacer } from '@elastic/eui';
+import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../../shared/flash_messages';
-import { Loading } from '../../../../shared/loading';
import { DataPanel } from '../../data_panel';
+import { getEngineBreadcrumbs } from '../../engine';
+import { AppSearchPageTemplate } from '../../layout';
import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components';
+import { SCHEMA_TITLE } from '../constants';
import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic';
export const MetaEngineSchema: React.FC = () => {
@@ -27,90 +28,88 @@ export const MetaEngineSchema: React.FC = () => {
loadSchema();
}, []);
- if (dataLoading) return ;
-
return (
- <>
-
-
-
- {hasConflicts && (
- <>
-
+ {hasConflicts && (
+ <>
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.conflictsCalloutDescription',
{
defaultMessage:
- '{conflictingFieldsCount, plural, one {# field is} other {# fields are}} not searchable',
- values: { conflictingFieldsCount },
+ 'The field(s) have an inconsistent field-type across the source engines that make up this meta engine. Apply a consistent field-type from the source engines to make these fields searchable.',
}
)}
- >
-
- {i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.conflictsCalloutDescription',
- {
- defaultMessage:
- 'The field(s) have an inconsistent field-type across the source engines that make up this meta engine. Apply a consistent field-type from the source engines to make these fields searchable.',
- }
- )}
-
-
-
- >
+
+
+
+ >
+ )}
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle',
+ { defaultMessage: 'Active fields' }
+ )}
+
+ }
+ subtitle={i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription',
+ { defaultMessage: 'Fields which belong to one or more engine.' }
)}
+ >
+
+
+
+ {hasConflicts && (
{i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle',
- { defaultMessage: 'Active fields' }
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle',
+ { defaultMessage: 'Inactive fields' }
)}
}
subtitle={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription',
- { defaultMessage: 'Fields which belong to one or more engine.' }
+ 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription',
+ {
+ defaultMessage:
+ 'These fields have type conflicts. To activate these fields, change types in the source engines to match.',
+ }
)}
>
-
+
-
- {hasConflicts && (
-
- {i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle',
- { defaultMessage: 'Inactive fields' }
- )}
-
- }
- subtitle={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription',
- {
- defaultMessage:
- 'These fields have type conflicts. To activate these fields, change types in the source engines to match.',
- }
- )}
- >
-
-
- )}
-
- >
+ )}
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx
index 91ec8eda55fc3..cae16d70592fa 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.test.tsx
@@ -7,17 +7,18 @@
import { setMockValues, setMockActions } from '../../../../__mocks__/kea_logic';
import '../../../../__mocks__/shallow_useeffect.mock';
+import '../../../__mocks__/engine_logic.mock';
import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPageHeader, EuiButton } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
-import { Loading } from '../../../../shared/loading';
import { SchemaAddFieldModal } from '../../../../shared/schema';
+import { getPageHeaderActions } from '../../../../test_helpers';
-import { SchemaCallouts, SchemaTable, EmptyState } from '../components';
+import { SchemaCallouts, SchemaTable } from '../components';
import { Schema } from './';
@@ -56,27 +57,8 @@ describe('Schema', () => {
expect(actions.loadSchema).toHaveBeenCalled();
});
- it('renders a loading state', () => {
- setMockValues({ ...values, dataLoading: true });
- const wrapper = shallow( );
-
- expect(wrapper.find(Loading)).toHaveLength(1);
- });
-
- it('renders an empty state', () => {
- setMockValues({ ...values, hasSchema: false });
- const wrapper = shallow( );
-
- expect(wrapper.find(EmptyState)).toHaveLength(1);
- });
-
describe('page action buttons', () => {
- const subject = () =>
- shallow( )
- .find(EuiPageHeader)
- .dive()
- .children()
- .dive();
+ const subject = () => getPageHeaderActions(shallow( ));
it('renders', () => {
const wrapper = subject();
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx
index 7bc995b16468a..d2a760e8accff 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/schema.tsx
@@ -9,14 +9,15 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
-import { EuiPageHeader, EuiButton, EuiPageContentBody } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FlashMessages } from '../../../../shared/flash_messages';
-import { Loading } from '../../../../shared/loading';
import { SchemaAddFieldModal } from '../../../../shared/schema';
+import { getEngineBreadcrumbs } from '../../engine';
+import { AppSearchPageTemplate } from '../../layout';
import { SchemaCallouts, SchemaTable, EmptyState } from '../components';
+import { SCHEMA_TITLE } from '../constants';
import { SchemaLogic } from '../schema_logic';
export const Schema: React.FC = () => {
@@ -31,19 +32,18 @@ export const Schema: React.FC = () => {
loadSchema();
}, []);
- if (dataLoading) return ;
-
return (
- <>
- {
>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.schema.updateSchemaButtonLabel',
- { defaultMessage: 'Update types' }
+ { defaultMessage: 'Save changes' }
)}
,
{
{ defaultMessage: 'Create a schema field' }
)}
,
- ]}
- />
-
-
-
- {hasSchema ? : }
- {isModalOpen && (
-
- )}
-
- >
+ ],
+ }}
+ isLoading={dataLoading}
+ isEmptyState={!hasSchema}
+ emptyState={ }
+ >
+
+
+ {isModalOpen && (
+
+ )}
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx
index 902417d02665e..ba9da900c0145 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/add_field_modal/index.tsx
@@ -10,6 +10,7 @@ import React, { ChangeEvent, FormEvent, useEffect, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
+ EuiCallOut,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
@@ -83,8 +84,13 @@ export const SchemaAddFieldModal: React.FC = ({
{ADD_FIELD_MODAL_TITLE}
- {ADD_FIELD_MODAL_DESCRIPTION}
-
+ {ADD_FIELD_MODAL_DESCRIPTION}}
+ />
+
From dec77cfafb0cd557eaf7a6d2ab72280b9801fb6d Mon Sep 17 00:00:00 2001
From: Chris Roberson
Date: Tue, 22 Jun 2021 16:01:43 -0400
Subject: [PATCH 094/163] [Alerting] Add event log entry when an action starts
executing (#102370)
* First steps for adding action execution to event log
* Fix tests
* Move the event to the actions plugin
* Update functional tests
* Fix tests
* Fix types
---
.../actions/server/constants/event_log.ts | 1 +
.../server/lib/action_executor.test.ts | 47 ++++++++++-
.../actions/server/lib/action_executor.ts | 12 +++
.../tests/actions/execute.ts | 78 +++++++++++++++----
.../spaces_only/tests/actions/execute.ts | 50 ++++++++----
5 files changed, 156 insertions(+), 32 deletions(-)
diff --git a/x-pack/plugins/actions/server/constants/event_log.ts b/x-pack/plugins/actions/server/constants/event_log.ts
index 508709c8783ab..9163a0d105ce8 100644
--- a/x-pack/plugins/actions/server/constants/event_log.ts
+++ b/x-pack/plugins/actions/server/constants/event_log.ts
@@ -8,5 +8,6 @@
export const EVENT_LOG_PROVIDER = 'actions';
export const EVENT_LOG_ACTIONS = {
execute: 'execute',
+ executeStart: 'execute-start',
executeViaHttp: 'execute-via-http',
};
diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts
index 8ec94c4d4a552..37d461d6b2a50 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.test.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts
@@ -23,6 +23,7 @@ const services = actionsMock.createServices();
const actionsClient = actionsClientMock.create();
const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient();
const actionTypeRegistry = actionTypeRegistryMock.create();
+const eventLogger = eventLoggerMock.create();
const executeParams = {
actionId: '1',
@@ -42,7 +43,7 @@ actionExecutor.initialize({
getActionsClientWithRequest,
actionTypeRegistry,
encryptedSavedObjectsClient,
- eventLogger: eventLoggerMock.create(),
+ eventLogger,
preconfiguredActions: [],
});
@@ -379,6 +380,50 @@ test('logs a warning when alert executor returns invalid status', async () => {
);
});
+test('writes to event log for execute and execute start', async () => {
+ const executorMock = setupActionExecutorMock();
+ executorMock.mockResolvedValue({
+ actionId: '1',
+ status: 'ok',
+ });
+ await actionExecutor.execute(executeParams);
+ expect(eventLogger.logEvent).toHaveBeenCalledTimes(2);
+ expect(eventLogger.logEvent.mock.calls[0][0]).toMatchObject({
+ event: {
+ action: 'execute-start',
+ },
+ kibana: {
+ saved_objects: [
+ {
+ rel: 'primary',
+ type: 'action',
+ id: '1',
+ type_id: 'test',
+ namespace: 'some-namespace',
+ },
+ ],
+ },
+ message: 'action started: test:1: action-1',
+ });
+ expect(eventLogger.logEvent.mock.calls[1][0]).toMatchObject({
+ event: {
+ action: 'execute',
+ },
+ kibana: {
+ saved_objects: [
+ {
+ rel: 'primary',
+ type: 'action',
+ id: '1',
+ type_id: 'test',
+ namespace: 'some-namespace',
+ },
+ ],
+ },
+ message: 'action executed: test:1: action-1',
+ });
+});
+
function setupActionExecutorMock() {
const actionType: jest.Mocked = {
id: 'test',
diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts
index 9d2b937734fb0..e9e7b17288611 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.ts
@@ -7,6 +7,7 @@
import type { PublicMethodsOf } from '@kbn/utility-types';
import { Logger, KibanaRequest } from 'src/core/server';
+import { cloneDeep } from 'lodash';
import { withSpan } from '@kbn/apm-utils';
import { validateParams, validateConfig, validateSecrets } from './validate_with_schema';
import {
@@ -168,6 +169,17 @@ export class ActionExecutor {
}
eventLogger.startTiming(event);
+
+ const startEvent = cloneDeep({
+ ...event,
+ event: {
+ ...event.event,
+ action: EVENT_LOG_ACTIONS.executeStart,
+ },
+ message: `action started: ${actionLabel}`,
+ });
+ eventLogger.logEvent(startEvent);
+
let rawResult: ActionTypeExecutorResult;
try {
rawResult = await actionType.executor({
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts
index f7d7c1df8fd46..5c578d2d08dae 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts
@@ -519,47 +519,93 @@ export default function ({ getService }: FtrProviderContext) {
type: 'action',
id: connectorId,
provider: 'actions',
- actions: new Map([['execute', { equal: 1 }]]),
- filter: 'event.action:(execute)',
+ actions: new Map([
+ ['execute-start', { equal: 1 }],
+ ['execute', { equal: 1 }],
+ ]),
+ // filter: 'event.action:(execute)',
});
});
- const event = events[0];
+ const startExecuteEvent = events[0];
+ const executeEvent = events[1];
- const duration = event?.event?.duration;
- const eventStart = Date.parse(event?.event?.start || 'undefined');
- const eventEnd = Date.parse(event?.event?.end || 'undefined');
+ const duration = executeEvent?.event?.duration;
+ const executeEventStart = Date.parse(executeEvent?.event?.start || 'undefined');
+ const startExecuteEventStart = Date.parse(startExecuteEvent?.event?.start || 'undefined');
+ const executeEventEnd = Date.parse(executeEvent?.event?.end || 'undefined');
const dateNow = Date.now();
expect(typeof duration).to.be('number');
- expect(eventStart).to.be.ok();
- expect(eventEnd).to.be.ok();
+ expect(executeEventStart).to.be.ok();
+ expect(startExecuteEventStart).to.equal(executeEventStart);
+ expect(executeEventEnd).to.be.ok();
const durationDiff = Math.abs(
- Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart)
+ Math.round(duration! / NANOS_IN_MILLIS) - (executeEventEnd - executeEventStart)
);
// account for rounding errors
expect(durationDiff < 1).to.equal(true);
- expect(eventStart <= eventEnd).to.equal(true);
- expect(eventEnd <= dateNow).to.equal(true);
+ expect(executeEventStart <= executeEventEnd).to.equal(true);
+ expect(executeEventEnd <= dateNow).to.equal(true);
- expect(event?.event?.outcome).to.equal(outcome);
+ expect(executeEvent?.event?.outcome).to.equal(outcome);
- expect(event?.kibana?.saved_objects).to.eql([
+ expect(executeEvent?.kibana?.saved_objects).to.eql([
{
rel: 'primary',
type: 'action',
id: connectorId,
+ namespace: 'space1',
type_id: actionTypeId,
- namespace: spaceId,
},
]);
+ expect(startExecuteEvent?.kibana?.saved_objects).to.eql(executeEvent?.kibana?.saved_objects);
- expect(event?.message).to.eql(message);
+ expect(executeEvent?.message).to.eql(message);
+ expect(startExecuteEvent?.message).to.eql(message.replace('executed', 'started'));
if (errorMessage) {
- expect(event?.error?.message).to.eql(errorMessage);
+ expect(executeEvent?.error?.message).to.eql(errorMessage);
}
+
+ // const event = events[0];
+
+ // const duration = event?.event?.duration;
+ // const eventStart = Date.parse(event?.event?.start || 'undefined');
+ // const eventEnd = Date.parse(event?.event?.end || 'undefined');
+ // const dateNow = Date.now();
+
+ // expect(typeof duration).to.be('number');
+ // expect(eventStart).to.be.ok();
+ // expect(eventEnd).to.be.ok();
+
+ // const durationDiff = Math.abs(
+ // Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart)
+ // );
+
+ // // account for rounding errors
+ // expect(durationDiff < 1).to.equal(true);
+ // expect(eventStart <= eventEnd).to.equal(true);
+ // expect(eventEnd <= dateNow).to.equal(true);
+
+ // expect(event?.event?.outcome).to.equal(outcome);
+
+ // expect(event?.kibana?.saved_objects).to.eql([
+ // {
+ // rel: 'primary',
+ // type: 'action',
+ // id: connectorId,
+ // type_id: actionTypeId,
+ // namespace: spaceId,
+ // },
+ // ]);
+
+ // expect(event?.message).to.eql(message);
+
+ // if (errorMessage) {
+ // expect(event?.error?.message).to.eql(errorMessage);
+ // }
}
}
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts
index 147b6abfb88d1..d494c99c80e8f 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts
@@ -100,6 +100,7 @@ export default function ({ getService }: FtrProviderContext) {
actionTypeId: 'test.index-record',
outcome: 'success',
message: `action executed: test.index-record:${createdAction.id}: My action`,
+ startMessage: `action started: test.index-record:${createdAction.id}: My action`,
});
});
@@ -336,10 +337,19 @@ export default function ({ getService }: FtrProviderContext) {
outcome: string;
message: string;
errorMessage?: string;
+ startMessage?: string;
}
async function validateEventLog(params: ValidateEventLogParams): Promise {
- const { spaceId, actionId, actionTypeId, outcome, message, errorMessage } = params;
+ const {
+ spaceId,
+ actionId,
+ actionTypeId,
+ outcome,
+ message,
+ startMessage,
+ errorMessage,
+ } = params;
const events: IValidatedEvent[] = await retry.try(async () => {
return await getEventLog({
@@ -348,33 +358,39 @@ export default function ({ getService }: FtrProviderContext) {
type: 'action',
id: actionId,
provider: 'actions',
- actions: new Map([['execute', { equal: 1 }]]),
+ actions: new Map([
+ ['execute-start', { equal: 1 }],
+ ['execute', { equal: 1 }],
+ ]),
});
});
- const event = events[0];
+ const startExecuteEvent = events[0];
+ const executeEvent = events[1];
- const duration = event?.event?.duration;
- const eventStart = Date.parse(event?.event?.start || 'undefined');
- const eventEnd = Date.parse(event?.event?.end || 'undefined');
+ const duration = executeEvent?.event?.duration;
+ const executeEventStart = Date.parse(executeEvent?.event?.start || 'undefined');
+ const startExecuteEventStart = Date.parse(startExecuteEvent?.event?.start || 'undefined');
+ const executeEventEnd = Date.parse(executeEvent?.event?.end || 'undefined');
const dateNow = Date.now();
expect(typeof duration).to.be('number');
- expect(eventStart).to.be.ok();
- expect(eventEnd).to.be.ok();
+ expect(executeEventStart).to.be.ok();
+ expect(startExecuteEventStart).to.equal(executeEventStart);
+ expect(executeEventEnd).to.be.ok();
const durationDiff = Math.abs(
- Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart)
+ Math.round(duration! / NANOS_IN_MILLIS) - (executeEventEnd - executeEventStart)
);
// account for rounding errors
expect(durationDiff < 1).to.equal(true);
- expect(eventStart <= eventEnd).to.equal(true);
- expect(eventEnd <= dateNow).to.equal(true);
+ expect(executeEventStart <= executeEventEnd).to.equal(true);
+ expect(executeEventEnd <= dateNow).to.equal(true);
- expect(event?.event?.outcome).to.equal(outcome);
+ expect(executeEvent?.event?.outcome).to.equal(outcome);
- expect(event?.kibana?.saved_objects).to.eql([
+ expect(executeEvent?.kibana?.saved_objects).to.eql([
{
rel: 'primary',
type: 'action',
@@ -383,11 +399,15 @@ export default function ({ getService }: FtrProviderContext) {
type_id: actionTypeId,
},
]);
+ expect(startExecuteEvent?.kibana?.saved_objects).to.eql(executeEvent?.kibana?.saved_objects);
- expect(event?.message).to.eql(message);
+ expect(executeEvent?.message).to.eql(message);
+ if (startMessage) {
+ expect(startExecuteEvent?.message).to.eql(startMessage);
+ }
if (errorMessage) {
- expect(event?.error?.message).to.eql(errorMessage);
+ expect(executeEvent?.error?.message).to.eql(errorMessage);
}
}
}
From b161bf03be07d6bc9fe688c03be8909fc26bae5f Mon Sep 17 00:00:00 2001
From: Melissa Alvarez
Date: Tue, 22 Jun 2021 16:58:18 -0400
Subject: [PATCH 095/163] [ML] Anomaly Detection: Visualize delayed - data Part
2 (#102270)
* add link in datafeed tab.remove interval
* add annotation overlay to chart
* adds annotations checkbox
* ensure annotation with same start/end time show up in chart
* update annotations time format
* move time format to client
* adds info tooltip to modal title
* adds model snapshots to datafeed chart
---
x-pack/plugins/ml/common/types/results.ts | 4 +
.../annotations_table/annotations_table.js | 10 +-
.../components/datafeed_modal/constants.ts | 2 +-
.../datafeed_modal/datafeed_modal.tsx | 213 ++++++++++++++----
.../datafeed_modal/get_interval_options.ts | 118 ----------
.../components/job_details/job_details.js | 76 +++++--
.../job_details/job_details_pane.js | 13 +-
.../services/ml_api_service/results.ts | 7 +-
.../models/results_service/results_service.ts | 66 +++++-
9 files changed, 308 insertions(+), 201 deletions(-)
delete mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/get_interval_options.ts
diff --git a/x-pack/plugins/ml/common/types/results.ts b/x-pack/plugins/ml/common/types/results.ts
index fa40cefcaed48..74d3286438588 100644
--- a/x-pack/plugins/ml/common/types/results.ts
+++ b/x-pack/plugins/ml/common/types/results.ts
@@ -6,6 +6,7 @@
*/
import { estypes } from '@elastic/elasticsearch';
+import { LineAnnotationDatum, RectAnnotationDatum } from '@elastic/charts';
export interface GetStoppedPartitionResult {
jobs: string[] | Record;
@@ -13,6 +14,9 @@ export interface GetStoppedPartitionResult {
export interface GetDatafeedResultsChartDataResult {
bucketResults: number[][];
datafeedResults: number[][];
+ annotationResultsRect: RectAnnotationDatum[];
+ annotationResultsLine: LineAnnotationDatum[];
+ modelSnapshotResultsLine: LineAnnotationDatum[];
}
export interface DatafeedResultsChartDataParams {
diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
index afed7e79ff757..b68e64a5d9f6a 100644
--- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
+++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
@@ -494,13 +494,13 @@ class AnnotationsTableUI extends Component {
render: (annotation) => {
const viewDataFeedText = (
);
const viewDataFeedTooltipAriaLabelText = i18n.translate(
- 'xpack.ml.annotationsTable.viewDatafeedTooltipAriaLabel',
- { defaultMessage: 'View datafeed' }
+ 'xpack.ml.annotationsTable.datafeedChartTooltipAriaLabel',
+ { defaultMessage: 'Datafeed chart' }
);
return (
) : null}
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts
index 71f3795518bc9..b3b9487523196 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts
@@ -15,7 +15,7 @@ export const CHART_DIRECTION = {
export type ChartDirectionType = typeof CHART_DIRECTION[keyof typeof CHART_DIRECTION];
// [width, height]
-export const CHART_SIZE: ChartSizeArray = ['100%', 300];
+export const CHART_SIZE: ChartSizeArray = ['100%', 380];
export const TAB_IDS = {
CHART: 'chart',
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx
index cf547a49cac4c..2dece82e6f5c7 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx
@@ -11,25 +11,35 @@ import { i18n } from '@kbn/i18n';
import moment from 'moment';
import {
EuiButtonEmpty,
+ EuiCheckbox,
EuiDatePicker,
EuiFlexGroup,
EuiFlexItem,
+ EuiIcon,
+ EuiIconTip,
EuiLoadingChart,
EuiModal,
EuiModalHeader,
EuiModalBody,
- EuiSelect,
EuiSpacer,
EuiTabs,
EuiTab,
+ EuiText,
+ EuiTitle,
EuiToolTip,
+ htmlIdGenerator,
} from '@elastic/eui';
import {
+ AnnotationDomainType,
Axis,
Chart,
CurveType,
+ LineAnnotation,
LineSeries,
+ LineAnnotationDatum,
Position,
+ RectAnnotation,
+ RectAnnotationDatum,
ScaleType,
Settings,
timeFormatter,
@@ -42,7 +52,6 @@ import { useMlApiContext } from '../../../../contexts/kibana';
import { useCurrentEuiTheme } from '../../../../components/color_range_legend';
import { JobMessagesPane } from '../job_details/job_messages_pane';
import { EditQueryDelay } from './edit_query_delay';
-import { getIntervalOptions } from './get_interval_options';
import {
CHART_DIRECTION,
ChartDirectionType,
@@ -53,12 +62,18 @@ import {
} from './constants';
import { loadFullJob } from '../utils';
-const dateFormatter = timeFormatter('MM-DD HH:mm');
+const dateFormatter = timeFormatter('MM-DD HH:mm:ss');
+const MAX_CHART_POINTS = 480;
interface DatafeedModalProps {
jobId: string;
end: number;
- onClose: (deletionApproved?: boolean) => void;
+ onClose: () => void;
+}
+
+function setLineAnnotationHeader(lineDatum: LineAnnotationDatum) {
+ lineDatum.header = dateFormatter(lineDatum.dataValue);
+ return lineDatum;
}
export const DatafeedModal: FC = ({ jobId, end, onClose }) => {
@@ -68,11 +83,17 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
isInitialized: boolean;
}>({ datafeedConfig: undefined, bucketSpan: undefined, isInitialized: false });
const [endDate, setEndDate] = useState(moment(end));
- const [interval, setInterval] = useState();
const [selectedTabId, setSelectedTabId] = useState(TAB_IDS.CHART);
const [isLoadingChartData, setIsLoadingChartData] = useState(false);
const [bucketData, setBucketData] = useState([]);
+ const [annotationData, setAnnotationData] = useState<{
+ rect: RectAnnotationDatum[];
+ line: LineAnnotationDatum[];
+ }>({ rect: [], line: [] });
+ const [modelSnapshotData, setModelSnapshotData] = useState([]);
const [sourceData, setSourceData] = useState([]);
+ const [showAnnotations, setShowAnnotations] = useState(true);
+ const [showModelSnapshots, setShowModelSnapshots] = useState(true);
const {
results: { getDatafeedResultChartData },
@@ -102,25 +123,30 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
const handleChange = (date: moment.Moment) => setEndDate(date);
const handleEndDateChange = (direction: ChartDirectionType) => {
- if (interval === undefined) return;
+ if (data.bucketSpan === undefined) return;
const newEndDate = endDate.clone();
- const [count, type] = interval.split(' ');
+ const unitMatch = data.bucketSpan.match(/[d | h| m | s]/g)!;
+ const unit = unitMatch[0];
+ const count = Number(data.bucketSpan.replace(/[^0-9]/g, ''));
if (direction === CHART_DIRECTION.FORWARD) {
- newEndDate.add(Number(count), type);
+ newEndDate.add(MAX_CHART_POINTS * count, unit);
} else {
- newEndDate.subtract(Number(count), type);
+ newEndDate.subtract(MAX_CHART_POINTS * count, unit);
}
setEndDate(newEndDate);
};
const getChartData = useCallback(async () => {
- if (interval === undefined) return;
+ if (data.bucketSpan === undefined) return;
const endTimestamp = moment(endDate).valueOf();
- const [count, type] = interval.split(' ');
- const startMoment = endDate.clone().subtract(Number(count), type);
+ const unitMatch = data.bucketSpan.match(/[d | h| m | s]/g)!;
+ const unit = unitMatch[0];
+ const count = Number(data.bucketSpan.replace(/[^0-9]/g, ''));
+ // STARTTIME = ENDTIME - (BucketSpan * MAX_CHART_POINTS)
+ const startMoment = endDate.clone().subtract(MAX_CHART_POINTS * count, unit);
const startTimestamp = moment(startMoment).valueOf();
try {
@@ -128,6 +154,11 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
setSourceData(chartData.datafeedResults);
setBucketData(chartData.bucketResults);
+ setAnnotationData({
+ rect: chartData.annotationResultsRect,
+ line: chartData.annotationResultsLine.map(setLineAnnotationHeader),
+ });
+ setModelSnapshotData(chartData.modelSnapshotResultsLine.map(setLineAnnotationHeader));
} catch (error) {
const title = i18n.translate('xpack.ml.jobsList.datafeedModal.errorToastTitle', {
defaultMessage: 'Error fetching data',
@@ -135,7 +166,7 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
displayErrorToast(error, title);
}
setIsLoadingChartData(false);
- }, [endDate, interval]);
+ }, [endDate, data.bucketSpan]);
const getJobData = async () => {
try {
@@ -145,11 +176,6 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
bucketSpan: job.analysis_config.bucket_span,
isInitialized: true,
});
- const intervalOptions = getIntervalOptions(job.analysis_config.bucket_span);
- const initialInterval = intervalOptions.length
- ? intervalOptions[intervalOptions.length - 1]
- : undefined;
- setInterval(initialInterval?.value || '72 hours');
} catch (error) {
displayErrorToast(error);
}
@@ -161,20 +187,17 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
useEffect(
function loadChartData() {
- if (interval !== undefined) {
+ if (data.bucketSpan !== undefined) {
setIsLoadingChartData(true);
getChartData();
}
},
- [endDate, interval]
+ [endDate, data.bucketSpan]
);
const { datafeedConfig, bucketSpan, isInitialized } = data;
-
- const intervalOptions = useMemo(() => {
- if (bucketSpan === undefined) return [];
- return getIntervalOptions(bucketSpan);
- }, [bucketSpan]);
+ const checkboxIdAnnotation = useMemo(() => htmlIdGenerator()(), []);
+ const checkboxIdModelSnapshot = useMemo(() => htmlIdGenerator()(), []);
return (
= ({ jobId, end, onClose }) =
-
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
= ({ jobId, end, onClose }) =
-
- setInterval(e.target.value)}
- aria-label={i18n.translate(
- 'xpack.ml.jobsList.datafeedModal.intervalSelection',
- {
- defaultMessage: 'Datafeed modal chart interval selection',
- }
- )}
- />
-
= ({ jobId, end, onClose }) =
isEnabled={datafeedConfig.state === DATAFEED_STATE.STOPPED}
/>
+
+
+
+
+
+
+ }
+ checked={showAnnotations}
+ onChange={() => setShowAnnotations(!showAnnotations)}
+ />
+
+
+
+
+
+ }
+ checked={showModelSnapshots}
+ onChange={() => setShowModelSnapshots(!showModelSnapshots)}
+ />
+
+
+
@@ -298,7 +362,65 @@ export const DatafeedModal: FC = ({ jobId, end, onClose }) =
})}
position={Position.Left}
/>
+ {showModelSnapshots ? (
+ }
+ markerPosition={Position.Top}
+ style={{
+ line: {
+ strokeWidth: 3,
+ stroke: euiTheme.euiColorVis1,
+ opacity: 0.5,
+ },
+ }}
+ />
+ ) : null}
+ {showAnnotations ? (
+ <>
+ }
+ markerPosition={Position.Top}
+ style={{
+ line: {
+ strokeWidth: 3,
+ stroke: euiTheme.euiColorDangerText,
+ opacity: 0.5,
+ },
+ }}
+ />
+
+ >
+ ) : null}
= ({ jobId, end, onClose }) =
curve={CurveType.LINEAR}
/>
{
- const unitMatch = bucketSpan.match(/[d | h| m | s]/g)!;
- const unit = unitMatch[0];
- const count = Number(bucketSpan.replace(/[^0-9]/g, ''));
-
- const intervalOptions = [];
-
- if (['s', 'ms', 'micros', 'nanos'].includes(unit)) {
- intervalOptions.push(
- {
- value: '1 hour',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.1hourOption', {
- defaultMessage: '{count} hour',
- values: { count: 1 },
- }),
- },
- {
- value: '2 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.2hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 2 },
- }),
- }
- );
- }
-
- if ((unit === 'm' && count <= 4) || unit === 'h') {
- intervalOptions.push(
- {
- value: '3 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.3hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 3 },
- }),
- },
- {
- value: '8 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.8hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 8 },
- }),
- },
- {
- value: '12 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.12hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 12 },
- }),
- },
- {
- value: '24 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.24hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 24 },
- }),
- }
- );
- }
-
- if ((unit === 'm' && count >= 5 && count <= 15) || unit === 'h') {
- intervalOptions.push(
- {
- value: '48 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.48hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 48 },
- }),
- },
- {
- value: '72 hours',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.72hourOption', {
- defaultMessage: '{count} hours',
- values: { count: 72 },
- }),
- }
- );
- }
-
- if ((unit === 'm' && count >= 10 && count <= 15) || unit === 'h' || unit === 'd') {
- intervalOptions.push(
- {
- value: '5 days',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.5daysOption', {
- defaultMessage: '{count} days',
- values: { count: 5 },
- }),
- },
- {
- value: '7 days',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.7daysOption', {
- defaultMessage: '{count} days',
- values: { count: 7 },
- }),
- }
- );
- }
-
- if (unit === 'h' || unit === 'd') {
- intervalOptions.push({
- value: '14 days',
- text: i18n.translate('xpack.ml.jobsList.datafeedModal.14DaysOption', {
- defaultMessage: '{count} days',
- values: { count: 14 },
- }),
- });
- }
-
- return intervalOptions;
-};
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
index b514c8433daf4..d3856e6afa398 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js
@@ -7,26 +7,29 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
-
-import { EuiTabbedContent, EuiLoadingSpinner } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { EuiButtonIcon, EuiTabbedContent, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
import { extractJobDetails } from './extract_job_details';
import { JsonPane } from './json_tab';
import { DatafeedPreviewPane } from './datafeed_preview_tab';
import { AnnotationsTable } from '../../../../components/annotations/annotations_table';
+import { DatafeedModal } from '../datafeed_modal';
import { AnnotationFlyout } from '../../../../components/annotations/annotation_flyout';
import { ModelSnapshotTable } from '../../../../components/model_snapshots';
import { ForecastsTable } from './forecasts_table';
import { JobDetailsPane } from './job_details_pane';
import { JobMessagesPane } from './job_messages_pane';
-import { i18n } from '@kbn/i18n';
import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
export class JobDetailsUI extends Component {
constructor(props) {
super(props);
- this.state = {};
+ this.state = {
+ datafeedModalVisible: false,
+ };
if (this.props.addYourself) {
this.props.addYourself(props.jobId, (j) => this.updateJob(j));
}
@@ -77,6 +80,30 @@ export class JobDetailsUI extends Component {
alertRules,
} = extractJobDetails(job, basePath, refreshJobList);
+ datafeed.titleAction = (
+
+ }
+ >
+
+ this.setState({
+ datafeedModalVisible: true,
+ })
+ }
+ />
+
+ );
+
const tabs = [
{
id: 'job-settings',
@@ -105,6 +132,32 @@ export class JobDetailsUI extends Component {
/>
),
},
+ {
+ id: 'datafeed',
+ 'data-test-subj': 'mlJobListTab-datafeed',
+ name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', {
+ defaultMessage: 'Datafeed',
+ }),
+ content: (
+ <>
+
+ {this.props.jobId && this.state.datafeedModalVisible ? (
+ {
+ this.setState({
+ datafeedModalVisible: false,
+ });
+ }}
+ end={job.data_counts.latest_bucket_timestamp}
+ jobId={this.props.jobId}
+ />
+ ) : null}
+ >
+ ),
+ },
{
id: 'counts',
'data-test-subj': 'mlJobListTab-counts',
@@ -137,21 +190,6 @@ export class JobDetailsUI extends Component {
];
if (showFullDetails && datafeed.items.length) {
- // Datafeed should be at index 2 in tabs array for full details
- tabs.splice(2, 0, {
- id: 'datafeed',
- 'data-test-subj': 'mlJobListTab-datafeed',
- name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', {
- defaultMessage: 'Datafeed',
- }),
- content: (
-
- ),
- });
-
tabs.push(
{
id: 'datafeed-preview',
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js
index 49d9bcde49052..4046f4d5d8071 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js
@@ -9,6 +9,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import {
+ EuiFlexGroup,
+ EuiFlexItem,
EuiTitle,
EuiTable,
EuiTableBody,
@@ -42,9 +44,14 @@ function Section({ section }) {
return (
-
- {section.title}
-
+
+
+
+ {section.title}
+
+
+ {section.titleAction}
+
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
index 19ba5aa304bf0..25ef36782207f 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
@@ -6,7 +6,10 @@
*/
// Service for obtaining data for the ML Results dashboards.
-import { GetStoppedPartitionResult } from '../../../../common/types/results';
+import {
+ GetStoppedPartitionResult,
+ GetDatafeedResultsChartDataResult,
+} from '../../../../common/types/results';
import { HttpService } from '../http_service';
import { basePath } from './index';
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
@@ -148,7 +151,7 @@ export const resultsApiProvider = (httpService: HttpService) => ({
start,
end,
});
- return httpService.http({
+ return httpService.http({
path: `${basePath()}/results/datafeed_results_chart`,
method: 'POST',
body,
diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts
index 9413ee00184d2..81ee394b99704 100644
--- a/x-pack/plugins/ml/server/models/results_service/results_service.ts
+++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts
@@ -27,6 +27,7 @@ import {
import { MlJobsResponse } from '../../../common/types/job_service';
import type { MlClient } from '../../lib/ml_client';
import { datafeedsProvider } from '../job_service/datafeeds';
+import { annotationServiceProvider } from '../annotation_service';
// Service for carrying out Elasticsearch queries to obtain data for the
// ML Results dashboards.
@@ -620,13 +621,19 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
const finalResults: GetDatafeedResultsChartDataResult = {
bucketResults: [],
datafeedResults: [],
+ annotationResultsRect: [],
+ annotationResultsLine: [],
+ modelSnapshotResultsLine: [],
};
const { getDatafeedByJobId } = datafeedsProvider(client!, mlClient);
- const datafeedConfig = await getDatafeedByJobId(jobId);
- const { body: jobsResponse } = await mlClient.getJobs({ job_id: jobId });
- if (jobsResponse.count === 0 || jobsResponse.jobs === undefined) {
+ const [datafeedConfig, { body: jobsResponse }] = await Promise.all([
+ getDatafeedByJobId(jobId),
+ mlClient.getJobs({ job_id: jobId }),
+ ]);
+
+ if (jobsResponse && (jobsResponse.count === 0 || jobsResponse.jobs === undefined)) {
throw Boom.notFound(`Job with the id "${jobId}" not found`);
}
@@ -696,10 +703,25 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
]) || [];
}
- const bucketResp = await mlClient.getBuckets({
- job_id: jobId,
- body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } },
- });
+ const { getAnnotations } = annotationServiceProvider(client!);
+
+ const [bucketResp, annotationResp, { body: modelSnapshotsResp }] = await Promise.all([
+ mlClient.getBuckets({
+ job_id: jobId,
+ body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } },
+ }),
+ getAnnotations({
+ jobIds: [jobId],
+ earliestMs: start,
+ latestMs: end,
+ maxAnnotations: 1000,
+ }),
+ mlClient.getModelSnapshots({
+ job_id: jobId,
+ start: String(start),
+ end: String(end),
+ }),
+ ]);
const bucketResults = bucketResp?.body?.buckets ?? [];
bucketResults.forEach((dataForTime) => {
@@ -708,6 +730,36 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
finalResults.bucketResults.push([timestamp, eventCount]);
});
+ const annotationResults = annotationResp.annotations[jobId] || [];
+ annotationResults.forEach((annotation) => {
+ const timestamp = Number(annotation?.timestamp);
+ const endTimestamp = Number(annotation?.end_timestamp);
+ if (timestamp === endTimestamp) {
+ finalResults.annotationResultsLine.push({
+ dataValue: timestamp,
+ details: annotation.annotation,
+ });
+ } else {
+ finalResults.annotationResultsRect.push({
+ coordinates: {
+ x0: timestamp,
+ x1: endTimestamp,
+ },
+ details: annotation.annotation,
+ });
+ }
+ });
+
+ const modelSnapshots = modelSnapshotsResp?.model_snapshots ?? [];
+ modelSnapshots.forEach((modelSnapshot) => {
+ const timestamp = Number(modelSnapshot?.timestamp);
+
+ finalResults.modelSnapshotResultsLine.push({
+ dataValue: timestamp,
+ details: modelSnapshot.description,
+ });
+ });
+
return finalResults;
}
From e580d5a1e2936bb9357d8ec5f12f7c50653937d8 Mon Sep 17 00:00:00 2001
From: Constance
Date: Tue, 22 Jun 2021 14:43:54 -0700
Subject: [PATCH 096/163] [App Search] Convert Result Settings & Relevance
Tuning pages to new page template (#102845)
* Convert Result Settings page to new page template
+ remove wrapper around empty state (auto handled by new page template)
+ update tests w/ new test helpers
* Convert Relevance Tuning page to new page template
- Remove old relevance_tuning_layout (which handled breadcrumbs, page header, flash messages, and callouts) in favor of simply using the new templtate + callouts (yay DRYing)
- Remove panel wrapper around empty state (handled by new page template)
* Update router
* [Polish] Spacing & icon polish from Davey
see https://github.com/elastic/kibana/pull/101958/files
---
.../components/engine/engine_router.tsx | 20 ++--
.../components/empty_state.tsx | 62 ++++++-----
.../relevance_tuning.test.tsx | 57 +++++-----
.../relevance_tuning/relevance_tuning.tsx | 74 +++++++++----
.../relevance_tuning_form.tsx | 2 +-
.../relevance_tuning_layout.test.tsx | 64 -----------
.../relevance_tuning_layout.tsx | 73 -------------
.../relevance_tuning_preview.tsx | 1 +
.../components/empty_state.tsx | 62 ++++++-----
.../result_settings/result_settings.test.tsx | 56 ++++------
.../result_settings/result_settings.tsx | 101 +++++++++---------
11 files changed, 224 insertions(+), 348 deletions(-)
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
index 91a21847107a9..04e252e44270b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx
@@ -114,6 +114,16 @@ export const EngineRouter: React.FC = () => {
)}
+ {canManageEngineRelevanceTuning && (
+
+
+
+ )}
+ {canManageEngineResultSettings && (
+
+
+
+ )}
{canManageEngineSearchUi && (
@@ -131,21 +141,11 @@ export const EngineRouter: React.FC = () => {
)}
- {canManageEngineRelevanceTuning && (
-
-
-
- )}
{canManageEngineSynonyms && (
)}
- {canManageEngineResultSettings && (
-
-
-
- )}
{canViewMetaEngineSourceEngines && (
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx
index e6a14d7b5cd72..df29010bd682f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx
@@ -7,42 +7,40 @@
import React from 'react';
-import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui';
+import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DOCS_PREFIX } from '../../../routes';
export const EmptyState: React.FC = () => (
-
-
- {i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.title', {
- defaultMessage: 'Add documents to tune relevance',
- })}
-
+
+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.title', {
+ defaultMessage: 'Add documents to tune relevance',
+ })}
+
+ }
+ body={i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.description',
+ {
+ defaultMessage:
+ 'A schema will be automatically created for you after you index some documents.',
}
- body={i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.description',
- {
- defaultMessage:
- 'A schema will be automatically created for you after you index some documents.',
- }
- )}
- actions={
-
- {i18n.translate(
- 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.buttonLabel',
- { defaultMessage: 'Read the relevance tuning guide' }
- )}
-
- }
- />
-
+ )}
+ actions={
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.buttonLabel',
+ { defaultMessage: 'Read the relevance tuning guide' }
+ )}
+
+ }
+ />
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx
index 092740ac5d3cc..48b536a954ed5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx
@@ -13,14 +13,14 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiEmptyPrompt } from '@elastic/eui';
-
-import { Loading } from '../../../shared/loading';
import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt';
+import { getPageHeaderActions } from '../../../test_helpers';
-import { EmptyState } from './components';
import { RelevanceTuning } from './relevance_tuning';
+
+import { RelevanceTuningCallouts } from './relevance_tuning_callouts';
import { RelevanceTuningForm } from './relevance_tuning_form';
+import { RelevanceTuningPreview } from './relevance_tuning_preview';
describe('RelevanceTuning', () => {
const values = {
@@ -50,9 +50,9 @@ describe('RelevanceTuning', () => {
it('renders', () => {
const wrapper = subject();
+ expect(wrapper.find(RelevanceTuningCallouts).exists()).toBe(true);
expect(wrapper.find(RelevanceTuningForm).exists()).toBe(true);
- expect(wrapper.find(Loading).exists()).toBe(false);
- expect(wrapper.find(EmptyState).exists()).toBe(false);
+ expect(wrapper.find(RelevanceTuningPreview).exists()).toBe(true);
});
it('initializes relevance tuning data', () => {
@@ -60,33 +60,38 @@ describe('RelevanceTuning', () => {
expect(actions.initializeRelevanceTuning).toHaveBeenCalled();
});
- it('will render an empty message when the engine has no schema', () => {
+ it('will prevent user from leaving the page if there are unsaved changes', () => {
setMockValues({
...values,
- engineHasSchemaFields: false,
+ unsavedChanges: true,
});
- const wrapper = subject();
- expect(wrapper.find(EmptyState).dive().find(EuiEmptyPrompt).exists()).toBe(true);
- expect(wrapper.find(Loading).exists()).toBe(false);
- expect(wrapper.find(RelevanceTuningForm).exists()).toBe(false);
+ expect(subject().find(UnsavedChangesPrompt).prop('hasUnsavedChanges')).toBe(true);
});
- it('will show a loading message if data is loading', () => {
- setMockValues({
- ...values,
- dataLoading: true,
+ describe('header actions', () => {
+ it('renders a Save button that will save the current changes', () => {
+ const buttons = getPageHeaderActions(subject());
+ expect(buttons.children().length).toBe(2);
+ const saveButton = buttons.find('[data-test-subj="SaveRelevanceTuning"]');
+ saveButton.simulate('click');
+ expect(actions.updateSearchSettings).toHaveBeenCalled();
});
- const wrapper = subject();
- expect(wrapper.find(Loading).exists()).toBe(true);
- expect(wrapper.find(EmptyState).exists()).toBe(false);
- expect(wrapper.find(RelevanceTuningForm).exists()).toBe(false);
- });
- it('will prevent user from leaving the page if there are unsaved changes', () => {
- setMockValues({
- ...values,
- unsavedChanges: true,
+ it('renders a Reset button that will remove all weights and boosts', () => {
+ const buttons = getPageHeaderActions(subject());
+ expect(buttons.children().length).toBe(2);
+ const resetButton = buttons.find('[data-test-subj="ResetRelevanceTuning"]');
+ resetButton.simulate('click');
+ expect(actions.resetSearchSettings).toHaveBeenCalled();
+ });
+
+ it('will not render buttons if the engine has no schema', () => {
+ setMockValues({
+ ...values,
+ engineHasSchemaFields: false,
+ });
+ const buttons = getPageHeaderActions(subject());
+ expect(buttons.children().length).toBe(0);
});
- expect(subject().find(UnsavedChangesPrompt).prop('hasUnsavedChanges')).toBe(true);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx
index b98541a963890..2e87d6836199b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx
@@ -9,43 +9,77 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
-import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-import { Loading } from '../../../shared/loading';
+import { SAVE_BUTTON_LABEL } from '../../../shared/constants';
import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt';
+import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../constants';
+import { getEngineBreadcrumbs } from '../engine';
+import { AppSearchPageTemplate } from '../layout';
import { EmptyState } from './components';
+import { RELEVANCE_TUNING_TITLE } from './constants';
+import { RelevanceTuningCallouts } from './relevance_tuning_callouts';
import { RelevanceTuningForm } from './relevance_tuning_form';
-import { RelevanceTuningLayout } from './relevance_tuning_layout';
import { RelevanceTuningPreview } from './relevance_tuning_preview';
import { RelevanceTuningLogic } from '.';
export const RelevanceTuning: React.FC = () => {
const { dataLoading, engineHasSchemaFields, unsavedChanges } = useValues(RelevanceTuningLogic);
- const { initializeRelevanceTuning } = useActions(RelevanceTuningLogic);
+ const { initializeRelevanceTuning, resetSearchSettings, updateSearchSettings } = useActions(
+ RelevanceTuningLogic
+ );
useEffect(() => {
initializeRelevanceTuning();
}, []);
- if (dataLoading) return ;
-
return (
-
+
+ {SAVE_BUTTON_LABEL}
+ ,
+
+ {RESTORE_DEFAULTS_BUTTON_LABEL}
+ ,
+ ]
+ : [],
+ }}
+ isLoading={dataLoading}
+ isEmptyState={!engineHasSchemaFields}
+ emptyState={ }
+ >
- {engineHasSchemaFields ? (
-
-
-
-
-
-
-
-
- ) : (
-
- )}
-
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx
index 5cbd291f85deb..c35cd280c7a05 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx
@@ -42,7 +42,7 @@ export const RelevanceTuningForm: React.FC = () => {
return (
+ >
);
}
From 4fa3dc46cb14f041d21b1b6a06961eb490602701 Mon Sep 17 00:00:00 2001
From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Date: Tue, 22 Jun 2021 18:56:33 -0400
Subject: [PATCH 099/163] [RAC] T-Grid is moving to a new home (#100265)
* wip
* First pass at standalone and embedded redux stores and usage
* wip
* First pass at standalone and embedded redux stores and usage
* wip
* clean up
* wip
* refact(NA): remove extra pkg_npm target and add specific target folders on @kbn/i18n
* cleanup
* - fixes type errors in tests
* WIP remove use_manage_timeline
* wip add query + selector
* finishing integrating timeline manage context from redux
* integrating t-grid in security solution
* fix RowRender type
* WIP begin to move components from package to plugin
* integration of t-grid inside of security solution
* wip to make redux work
* little trick to make it render
* - fixes a few type errors
* better integration betwen tgrid and security solutions
* bringing back tsconfig on timeline
* wip integration t-grid in observability
* fix types
* fix type in security solutions
* add type to import + trie dto get the bundle size as small as possible
* fix type in integration test
* fix type in integration test
* - fix tests
* clean up to use technical fields
* - fixes unit tests
* - mocks the `useDateFormat` function of the `useKibana` service to fix unit tests
* fix t-grid settings vs create timeline + fix inspect button
* fix last suites test
* Update unit tests, snapshots and lint
* Fix bad merge
* fix plugin export
* Fix some failing tests
* fix unit tets in timelines plugins
* fix latest test
* fix i18n
* free obs from t-grid
* Fix timeline functional plugin types
* fix store provider
* Update failing defaultHeader test
* Fix i18n usage in security solution
* Fix remaining i18n errors in timelines plugin
* Dedupe common shared types
* move drag and drop utils in package to avoid duplication
* More shared type cleanup
* add feature flag
* review I
* fix merge with master
* fix i18n translation
* More type deduping
* Use @kbn/common-utils, fix remaining types
* fix types
* fix tests
* missing type
* fix cypress tests
Co-authored-by: Kevin Qualters
Co-authored-by: Tiago Costa
Co-authored-by: Andrew Goldstein
---
.eslintrc.js | 12 +-
package.json | 4 +
packages/BUILD.bazel | 3 +-
packages/kbn-optimizer/limits.yml | 4 +-
.../kbn-securitysolution-t-grid/BUILD.bazel | 125 +
.../kbn-securitysolution-t-grid/README.md | 3 +
.../babel.config.js | 19 +
.../jest.config.js | 13 +
.../kbn-securitysolution-t-grid/package.json | 10 +
.../react/package.json | 5 +
.../src/constants/index.ts | 26 +
.../kbn-securitysolution-t-grid/src/index.ts | 11 +
.../src/mock/index.ts | 9 +
.../src/mock}/mock_event_details.ts | 5 +-
.../src/utils/api/index.ts | 42 +
.../src/utils/drag_and_drop/index.ts | 133 +
.../src/utils/index.ts | 10 +
.../tsconfig.browser.json | 23 +
.../kbn-securitysolution-t-grid/tsconfig.json | 19 +
packages/kbn-test/jest-preset.js | 2 +-
tsconfig.json | 1 -
tsconfig.refs.json | 1 +
.../common/experimental_features.ts | 1 +
.../plugins/security_solution/common/index.ts | 4 +
.../common/search_strategy/common/index.ts | 68 +-
.../common/search_strategy/index.ts | 1 +
.../timeline/events/all/index.ts | 41 +-
.../timeline/events/common/index.ts | 24 +-
.../timeline/events/details/index.ts | 29 +-
.../timeline/events/eql/index.ts | 47 +-
.../timeline/events/last_event_time/index.ts | 43 +-
.../common/search_strategy/timeline/index.ts | 26 +-
.../security_solution/common/types/index.ts | 8 +
.../common/types/timeline/actions/index.ts | 14 +
.../common/types/timeline/cells/index.ts | 8 +
.../common/types/timeline/columns/index.ts | 13 +
.../types/timeline/data_provider/index.ts | 15 +
.../common/types/timeline/index.ts | 12 +
.../common/types/timeline/rows/index.ts | 7 +
.../common/types/timeline/store.ts | 97 +
.../common/utils/field_formatters.test.ts | 2 +-
.../integration/overview/overview.spec.ts | 2 +-
.../cypress/support/commands.js | 2 +-
x-pack/plugins/security_solution/kibana.json | 1 +
.../security_solution/public/app/app.tsx | 33 +-
.../common/components/accessibility/index.ts | 8 +
.../tooltip_with_keyboard_shortcut/index.tsx | 4 +-
.../components/alerts_viewer/alerts_table.tsx | 30 +-
.../alerts_viewer/default_headers.ts | 4 +-
.../charts/draggable_legend.test.tsx | 2 +
.../charts/draggable_legend_item.test.tsx | 2 +
.../drag_drop_context_wrapper.test.tsx | 2 +
.../drag_drop_context_wrapper.tsx | 17 +-
.../drag_and_drop/draggable_wrapper.test.tsx | 2 +
.../drag_and_drop/draggable_wrapper.tsx | 8 +-
.../draggable_wrapper_hover_content.test.tsx | 85 +-
.../draggable_wrapper_hover_content.tsx | 27 +-
.../drag_and_drop/droppable_wrapper.test.tsx | 2 +
.../components/drag_and_drop/helpers.test.ts | 2 +-
.../components/drag_and_drop/helpers.ts | 364 +-
.../components/draggables/index.test.tsx | 2 +
.../event_details/alert_summary_view.test.tsx | 2 +
.../components/event_details/columns.tsx | 4 +-
.../event_details/event_details.test.tsx | 2 +
.../event_fields_browser.test.tsx | 2 +
.../event_details/event_fields_browser.tsx | 8 +-
.../components/event_details/helpers.tsx | 4 +-
.../events_viewer/default_headers.tsx | 2 +-
.../events_viewer/events_viewer.test.tsx | 13 +-
.../events_viewer/events_viewer.tsx | 34 +-
.../components/events_viewer/index.test.tsx | 6 +-
.../common/components/events_viewer/index.tsx | 102 +-
.../components/events_viewer/translations.ts | 7 -
.../components/header_page/title.test.tsx | 2 +
.../common/components/inspect/index.tsx | 4 +-
.../components/ml/entity_draggable.test.tsx | 2 +
.../ml/score/anomaly_score.test.tsx | 2 +
.../ml/score/anomaly_scores.test.tsx | 2 +
.../get_anomalies_host_table_columns.test.tsx | 2 +
...t_anomalies_network_table_columns.test.tsx | 2 +
.../common/components/tables/helpers.test.tsx | 3 +
.../common/components/toasters/utils.ts | 2 +-
.../common/components/top_n/index.test.tsx | 38 +-
.../components/with_hover_actions/index.tsx | 8 +-
.../events/last_event_time/index.ts | 2 +-
.../public/common/containers/source/index.tsx | 4 +-
.../common/hooks/use_app_toasts.test.ts | 3 +-
.../public/common/hooks/use_app_toasts.ts | 8 +-
.../lib/clipboard/with_copy_to_clipboard.tsx | 3 +-
.../common/lib/kibana/__mocks__/index.ts | 16 +-
.../public/common/mock/global_state.ts | 1 +
.../public/common/mock/header.ts | 2 +-
.../mock/mock_timeline_control_columns.tsx | 2 +-
.../public/common/mock/utils.ts | 32 +-
.../public/common/store/inputs/model.ts | 1 +
.../public/common/store/types.ts | 12 -
.../components/alerts_table/actions.tsx | 4 +-
.../alerts_utility_bar/index.test.tsx | 1 +
.../alerts_table/default_config.tsx | 4 +-
.../components/alerts_table/index.tsx | 76 +-
.../examples/observablity_alerts/columns.ts | 3 +-
.../render_cell_value.test.tsx | 4 +-
.../examples/security_solution_rac/columns.ts | 3 +-
.../render_cell_value.test.tsx | 4 +-
.../security_solution_detections/columns.ts | 2 +-
.../render_cell_value.test.tsx | 4 +-
.../alerts/use_signal_index.tsx | 2 +-
.../lists/use_lists_index.tsx | 3 +-
.../rules/use_rule_status.tsx | 2 +-
.../rules/use_rule_with_fallback.test.tsx | 2 +-
.../rules/use_rule_with_fallback.tsx | 2 +-
.../detection_engine/detection_engine.tsx | 2 +-
.../all/exceptions/exceptions_table.test.tsx | 3 +
.../rules/all/exceptions/exceptions_table.tsx | 5 +-
.../rules/all/rules_tables.tsx | 12 +-
.../detection_engine/rules/details/index.tsx | 4 +-
.../components/hosts_table/index.test.tsx | 2 +
.../uncommon_process_table/index.test.tsx | 2 +
.../hosts/pages/details/details_tabs.test.tsx | 2 +
.../public/hosts/pages/hosts.tsx | 2 +-
.../navigation/events_query_tab_body.tsx | 15 +-
.../plugins/security_solution/public/index.ts | 1 +
.../network/components/ip/index.test.tsx | 2 +
.../network_dns_table/index.test.tsx | 2 +
.../network_http_table/index.test.tsx | 1 +
.../index.test.tsx | 2 +
.../network_top_n_flow_table/index.test.tsx | 1 +
.../network/components/port/index.test.tsx | 2 +
.../source_destination/index.test.tsx | 2 +
.../source_destination_ip.test.tsx | 2 +
.../components/tls_table/index.test.tsx | 2 +
.../components/users_table/index.test.tsx | 2 +
.../public/network/pages/network.tsx | 2 +-
.../endpoint_overview/index.test.tsx | 2 +
.../security_solution/public/plugin.tsx | 19 +-
.../certificate_fingerprint/index.test.tsx | 2 +
.../components/duration/index.test.tsx | 2 +
.../field_renderers/field_renderers.test.tsx | 2 +
.../fields_browser/categories_pane.tsx | 2 +-
.../fields_browser/category.test.tsx | 3 +
.../components/fields_browser/category.tsx | 4 +-
.../fields_browser/category_columns.tsx | 12 +-
.../fields_browser/category_title.test.tsx | 43 +-
.../fields_browser/field_browser.tsx | 8 +-
.../fields_browser/field_items.test.tsx | 4 +-
.../components/fields_browser/field_items.tsx | 9 +-
.../fields_browser/field_name.test.tsx | 2 +
.../components/fields_browser/field_name.tsx | 2 +-
.../fields_browser/fields_pane.test.tsx | 2 +
.../components/fields_browser/fields_pane.tsx | 2 +-
.../components/fields_browser/header.test.tsx | 3 +-
.../components/fields_browser/header.tsx | 13 +-
.../components/fields_browser/helpers.tsx | 2 +-
.../components/fields_browser/index.test.tsx | 2 +
.../components/fields_browser/types.ts | 2 +-
.../components/flyout/bottom_bar/index.tsx | 2 +-
.../components/ja3_fingerprint/index.test.tsx | 2 +
.../components/manage_timeline/index.test.tsx | 125 -
.../components/manage_timeline/index.tsx | 212 -
.../components/netflow/index.test.tsx | 2 +
.../components/notes/note_cards/index.tsx | 2 +-
.../components/open_timeline/helpers.test.ts | 2 +-
.../components/open_timeline/helpers.ts | 3 +-
.../timeline/body/actions/header_actions.tsx | 25 +-
.../timeline/body/actions/index.test.tsx | 4 -
.../timeline/body/actions/index.tsx | 65 +-
.../body/column_headers/actions/index.tsx | 2 +-
.../body/column_headers/column_header.tsx | 14 +-
.../body/column_headers/default_headers.ts | 3 +-
.../body/column_headers/filter/index.tsx | 2 +-
.../column_headers/header/header_content.tsx | 2 +-
.../body/column_headers/header/helpers.ts | 7 +-
.../body/column_headers/header/index.test.tsx | 12 +-
.../body/column_headers/header/index.tsx | 20 +-
.../header_tooltip_content/index.test.tsx | 3 +-
.../header_tooltip_content/index.tsx | 2 +-
.../timeline/body/column_headers/helpers.ts | 2 +-
.../body/column_headers/index.test.tsx | 2 +
.../timeline/body/column_headers/index.tsx | 11 +-
.../timeline/body/control_columns/index.tsx | 45 +-
.../body/data_driven_columns/index.test.tsx | 2 -
.../body/data_driven_columns/index.tsx | 29 +-
.../stateful_cell.test.tsx | 8 +-
.../data_driven_columns/stateful_cell.tsx | 8 +-
.../body/events/event_column_view.test.tsx | 12 -
.../body/events/event_column_view.tsx | 25 +-
.../components/timeline/body/events/index.tsx | 12 +-
.../timeline/body/events/stateful_event.tsx | 26 +-
.../events/stateful_row_renderer/index.tsx | 6 +-
.../events/use_stateful_event_focus/index.tsx | 2 +-
.../components/timeline/body/index.test.tsx | 8 +-
.../components/timeline/body/index.tsx | 30 +-
.../timeline/body/renderers/args.test.tsx | 2 +
.../renderers/auditd/generic_details.test.tsx | 2 +
.../auditd/generic_file_details.test.tsx | 2 +
.../auditd/generic_row_renderer.test.tsx | 4 +-
.../renderers/auditd/generic_row_renderer.tsx | 4 +-
.../primary_secondary_user_info.test.tsx | 2 +
.../session_user_host_working_dir.test.tsx | 2 +
.../body/renderers/bytes/index.test.tsx | 2 +
.../body/renderers/column_renderer.ts | 2 +-
.../renderers/cti/threat_match_row.test.tsx | 2 +
.../cti/threat_match_row_renderer.tsx | 3 +-
.../body/renderers/cti/threat_match_rows.tsx | 3 +-
.../dns/dns_request_event_details.test.tsx | 2 +
.../dns_request_event_details_line.test.tsx | 2 +
.../renderers/empty_column_renderer.test.tsx | 2 +
.../body/renderers/empty_column_renderer.tsx | 3 +-
.../endgame_security_event_details.test.tsx | 2 +
...dgame_security_event_details_line.test.tsx | 2 +
.../renderers/exit_code_draggable.test.tsx | 2 +
.../body/renderers/file_draggable.test.tsx | 2 +
.../body/renderers/file_hash.test.tsx | 2 +
.../renderers/get_column_renderer.test.tsx | 2 +
.../body/renderers/get_row_renderer.test.tsx | 2 +
.../body/renderers/get_row_renderer.ts | 2 +-
.../body/renderers/host_working_dir.test.tsx | 2 +
.../timeline/body/renderers/index.ts | 2 +-
.../netflow/netflow_row_renderer.test.tsx | 2 +
.../netflow/netflow_row_renderer.tsx | 4 +-
.../parent_process_draggable.test.tsx | 2 +
.../renderers/plain_column_renderer.test.tsx | 2 +
.../body/renderers/plain_column_renderer.tsx | 2 +-
.../body/renderers/plain_row_renderer.tsx | 4 +-
.../body/renderers/process_draggable.test.tsx | 2 +
.../body/renderers/process_hash.test.tsx | 2 +
.../registry/registry_event_details.test.tsx | 2 +
.../registry_event_details_line.test.tsx | 2 +
.../timeline/body/renderers/row_renderer.tsx | 18 -
.../suricata/suricata_details.test.tsx | 2 +
.../suricata/suricata_row_renderer.test.tsx | 2 +
.../suricata/suricata_row_renderer.tsx | 4 +-
.../suricata/suricata_signature.test.tsx | 2 +
.../renderers/system/generic_details.test.tsx | 2 +
.../system/generic_file_details.test.tsx | 2 +
.../system/generic_row_renderer.test.tsx | 4 +-
.../renderers/system/generic_row_renderer.tsx | 4 +-
.../body/renderers/system/package.test.tsx | 2 +
.../renderers/user_host_working_dir.test.tsx | 2 +
.../body/renderers/zeek/zeek_details.test.tsx | 2 +
.../renderers/zeek/zeek_row_renderer.test.tsx | 2 +
.../body/renderers/zeek/zeek_row_renderer.tsx | 4 +-
.../renderers/zeek/zeek_signature.test.tsx | 2 +
.../components/timeline/body/sort/index.ts | 12 +-
.../timeline/body/sort/sort_indicator.tsx | 2 +-
.../default_cell_renderer.test.tsx | 2 +
.../timeline/cell_rendering/index.tsx | 14 +-
.../timeline/data_providers/index.test.tsx | 35 +-
.../timeline/data_providers/index.tsx | 14 +-
.../data_providers/provider_item_badge.tsx | 15 +-
.../data_providers/providers.test.tsx | 85 +-
.../timeline/data_providers/providers.tsx | 11 +-
.../timeline/eql_tab_content/index.test.tsx | 5 +
.../timeline/eql_tab_content/index.tsx | 38 +-
.../timelines/components/timeline/events.ts | 49 +-
.../components/timeline/footer/index.test.tsx | 2 +
.../components/timeline/footer/index.tsx | 36 +-
.../timeline/footer/translations.ts | 7 +
.../timelines/components/timeline/helpers.tsx | 2 +-
.../timelines/components/timeline/index.tsx | 5 +-
.../pinned_tab_content/index.test.tsx | 5 +
.../timeline/pinned_tab_content/index.tsx | 11 +-
.../components/timeline/query_bar/index.tsx | 2 +-
.../timeline/query_tab_content/index.test.tsx | 9 +
.../timeline/query_tab_content/index.tsx | 47 +-
.../timeline/search_or_filter/index.tsx | 8 +-
.../search_or_filter/search_or_filter.tsx | 2 +-
.../timeline/tabs_content/index.tsx | 8 +-
.../timelines/containers/details/index.tsx | 2 +-
.../public/timelines/containers/index.tsx | 8 +-
.../timelines/containers/kpis/index.tsx | 2 +-
.../containers/local_storage/index.tsx | 4 +-
.../timelines/store/timeline/actions.ts | 135 +-
.../timelines/store/timeline/defaults.ts | 3 +-
.../public/timelines/store/timeline/epic.ts | 12 +-
.../timelines/store/timeline/helpers.ts | 57 +-
.../public/timelines/store/timeline/model.ts | 78 +-
.../timelines/store/timeline/reducer.test.ts | 3 +-
.../timelines/store/timeline/reducer.ts | 209 +-
.../timelines/store/timeline/selectors.ts | 3 +
.../plugins/security_solution/public/types.ts | 2 +
.../reference_rules/__mocks__/eql.ts | 791 +
.../reference_rules/eql.test.ts | 4 +-
.../timelines/pick_saved_timeline.test.ts | 27 +-
.../timelines/pick_saved_timeline.ts | 5 +-
.../security_solution/server/plugin.ts | 23 -
.../factory/hosts/details/index.test.tsx | 1 +
.../factory/events/all/helpers.test.ts | 1515 -
.../plugins/security_solution/tsconfig.json | 1 +
x-pack/plugins/timelines/README.md | 4 +-
x-pack/plugins/timelines/common/constants.ts | 8 +
.../timelines/common/ecs/agent/index.ts | 10 +
.../timelines/common/ecs/auditd/index.ts | 46 +
.../timelines/common/ecs/cloud/index.ts | 21 +
.../timelines/common/ecs/destination/index.ts | 22 +
.../plugins/timelines/common/ecs/dns/index.ts | 20 +
.../common/ecs/ecs_fields/extend_map.test.ts | 57 +
.../common/ecs/ecs_fields/extend_map.ts | 15 +
.../timelines/common/ecs/ecs_fields/index.ts | 359 +
.../timelines/common/ecs/endgame/index.ts | 22 +
.../timelines/common/ecs/event/index.ts | 46 +
.../timelines/common/ecs/file/index.ts | 61 +
.../plugins/timelines/common/ecs/geo/index.ts | 21 +
.../timelines/common/ecs/host/index.ts | 36 +
.../timelines/common/ecs/http/index.ts | 38 +
x-pack/plugins/timelines/common/ecs/index.ts | 66 +
.../timelines/common/ecs/network/index.ts | 15 +
.../timelines/common/ecs/process/index.ts | 40 +
.../timelines/common/ecs/ransomware/index.ts | 30 +
.../timelines/common/ecs/registry/index.ts | 13 +
.../timelines/common/ecs/rule/index.ts | 43 +
.../timelines/common/ecs/signal/index.ts | 18 +
.../timelines/common/ecs/source/index.ts | 17 +
.../timelines/common/ecs/suricata/index.ts | 24 +
.../timelines/common/ecs/system/index.ts | 40 +
.../timelines/common/ecs/threat/index.ts | 25 +
.../plugins/timelines/common/ecs/tls/index.ts | 34 +
.../plugins/timelines/common/ecs/url/index.ts | 16 +
.../timelines/common/ecs/user/index.ts | 22 +
.../timelines/common/ecs/winlog/index.ts | 10 +
.../timelines/common/ecs/zeek/index.ts | 134 +
x-pack/plugins/timelines/common/index.ts | 4 +
.../common/search_strategy/common/index.ts | 80 +
.../common/search_strategy/eql/index.ts | 45 +
.../eql/validation/helpers.mock.ts | 70 +
.../eql/validation/helpers.test.ts | 59 +
.../search_strategy/eql/validation/helpers.ts | 35 +
.../search_strategy/eql/validation/index.ts | 8 +
.../timelines/common/search_strategy/index.ts | 11 +
.../search_strategy/index_fields/index.ts | 89 +
.../timeline/events/all/index.ts | 42 +
.../timeline/events/common/index.ts | 26 +
.../timeline/events/details/index.ts | 31 +
.../timeline/events/eql/index.ts | 46 +
.../search_strategy/timeline/events/index.ts | 18 +
.../timeline/events/last_event_time/index.ts | 42 +
.../common/search_strategy/timeline/index.ts | 197 +
x-pack/plugins/timelines/common/typed_json.ts | 57 +
.../plugins/timelines/common/types/index.ts | 8 +
.../common/types/timeline/actions/index.ts | 92 +
.../common/types/timeline/cells/index.ts | 21 +
.../common/types/timeline/columns/index.ts | 54 +
.../types/timeline/data_provider/index.ts | 65 +
.../timelines/common/types/timeline/index.ts | 744 +
.../common/types/timeline/note/index.ts | 127 +
.../types/timeline/pinned_event/index.ts | 85 +
.../common/types/timeline/rows/index.ts | 24 +
.../timelines/common/types/timeline/store.ts | 98 +
.../plugins/timelines/common/utility_types.ts | 53 +
.../utils}/accessibility/helpers.test.tsx | 0
.../common/utils}/accessibility/helpers.ts | 8 +-
.../common/utils/accessibility/index.ts | 8 +
.../common/utils/api.ts} | 0
.../common/utils/field_formatters.test.ts | 196 +
.../common/utils/field_formatters.ts | 153 +
.../timelines/common/utils/to_array.ts | 87 +
x-pack/plugins/timelines/jest.config.js | 12 +
x-pack/plugins/timelines/kibana.json | 3 +-
.../draggable_keyboard_wrapper_hook/index.tsx | 8 +-
.../components/drag_and_drop/helpers.ts | 211 +
.../public/components/drag_and_drop/index.tsx | 93 +
.../draggables/field_badge/index.tsx | 48 +
.../draggables/field_badge/translations.ts | 34 +
.../public/components/draggables/index.tsx | 8 +
.../exit_full_screen/index.test.tsx | 60 +
.../components/exit_full_screen/index.tsx | 64 +
.../exit_full_screen/translations.ts | 12 +
.../timelines/public/components/index.tsx | 57 +-
.../public/components/inspect/index.test.tsx | 105 +
.../public/components/inspect/index.tsx | 114 +
.../public/components/inspect/modal.test.tsx | 282 +
.../public/components/inspect/modal.tsx | 253 +
.../public/components/inspect/translations.ts | 64 +
.../components/last_updated/index.test.tsx | 2 +-
.../public}/components/last_updated/index.tsx | 5 +-
.../components/last_updated/translations.ts | 4 +-
.../public/components/loading/index.tsx | 98 +
.../__snapshots__/index.test.tsx.snap | 526 +
.../body/column_headers/actions/index.tsx | 69 +
.../body/column_headers/column_header.tsx | 310 +
.../common/dragging_container.tsx | 25 +
.../body/column_headers/common/styles.tsx | 19 +
.../body/column_headers/default_headers.ts | 58 +
.../header/__snapshots__/index.test.tsx.snap | 51 +
.../column_headers/header/header_content.tsx | 85 +
.../body/column_headers/header/helpers.ts | 55 +
.../body/column_headers/header/index.test.tsx | 331 +
.../body/column_headers/header/index.tsx | 94 +
.../__snapshots__/index.test.tsx.snap | 66 +
.../header_tooltip_content/index.test.tsx | 72 +
.../header_tooltip_content/index.tsx | 81 +
.../body/column_headers/helpers.test.ts | 116 +
.../t_grid/body/column_headers/helpers.ts | 57 +
.../t_grid/body/column_headers/index.test.tsx | 316 +
.../t_grid/body/column_headers/index.tsx | 295 +
.../body/column_headers/translations.ts | 51 +
.../components/t_grid/body/constants.ts | 29 +
.../__snapshots__/index.test.tsx.snap | 967 +
.../body/data_driven_columns/index.test.tsx | 57 +
.../t_grid/body/data_driven_columns/index.tsx | 394 +
.../stateful_cell.test.tsx | 173 +
.../data_driven_columns/stateful_cell.tsx | 65 +
.../body/data_driven_columns/translations.ts | 28 +
.../body/events/event_column_view.test.tsx | 115 +
.../t_grid/body/events/event_column_view.tsx | 182 +
.../components/t_grid/body/events/index.tsx | 100 +
.../t_grid/body/events/stateful_event.tsx | 207 +
.../body/events/stateful_event_context.tsx | 17 +
.../events/stateful_row_renderer/index.tsx | 104 +
.../t_grid/body/events/translations.ts | 15 +
.../events/use_stateful_event_focus/index.tsx | 96 +
.../components/t_grid/body/helpers.test.ts | 178 +
.../public/components/t_grid/body/helpers.tsx | 64 +
.../components/t_grid/body/index.test.tsx | 132 +
.../public/components/t_grid/body/index.tsx | 334 +
.../plain_row_renderer.test.tsx.snap | 3 +
.../body/renderers/get_column_renderer.ts | 24 +
.../t_grid/body/renderers/get_row_renderer.ts | 12 +
.../renderers/plain_row_renderer.test.tsx | 45 +
.../body/renderers/plain_row_renderer.tsx | 22 +
.../t_grid/body/renderers/row_renderer.tsx | 21 +
.../sort_indicator.test.tsx.snap | 18 +
.../components/t_grid/body/sort/index.ts | 16 +
.../t_grid/body/sort/sort_indicator.test.tsx | 85 +
.../t_grid/body/sort/sort_indicator.tsx | 68 +
.../t_grid/body/sort/sort_number.tsx | 27 +
.../components/t_grid/body/translations.ts | 229 +
.../components/t_grid/footer/index.test.tsx | 259 +
.../public/components/t_grid/footer/index.tsx | 394 +
.../components/t_grid/footer/translations.ts | 39 +
.../__snapshots__/index.test.tsx.snap | 35 +
.../t_grid/header_section/index.test.tsx | 159 +
.../t_grid/header_section/index.tsx | 106 +
.../public/components/t_grid/helpers.test.tsx | 578 +
.../public/components/t_grid/helpers.tsx | 314 +
.../components/t_grid/integrated/index.tsx | 355 +
.../t_grid/integrated/translations.ts | 36 +
.../components/t_grid/standalone/index.tsx | 339 +
.../t_grid/standalone/translations.ts | 36 +
.../public/components/t_grid/styles.tsx | 460 +
.../__snapshots__/index.test.tsx.snap | 11 +
.../components/t_grid/subtitle/index.test.tsx | 71 +
.../components/t_grid/subtitle/index.tsx | 72 +
.../public/components/t_grid/translations.ts | 20 +
.../public/components/t_grid/types.ts | 17 +
.../timelines/public/components/tgrid.tsx | 25 +
.../__snapshots__/index.test.tsx.snap | 19 +
.../truncatable_text/index.test.tsx | 36 +
.../components/truncatable_text/index.tsx | 28 +
.../public/components/utils/helpers.ts | 28 +
.../components/utils/keury/index.test.ts | 65 +
.../public/components/utils/keury/index.ts | 99 +
.../components/utils/use_mount_appended.ts | 31 +
.../timelines/public/container/index.tsx | 346 +
.../public/container/translations.ts | 22 +
.../public/hooks/use_add_to_timeline.ts} | 47 +-
.../timelines/public/hooks/use_app_toasts.ts | 241 +
.../timelines/public/hooks/use_selector.tsx | 20 +
x-pack/plugins/timelines/public/index.scss | 0
x-pack/plugins/timelines/public/index.ts | 47 +-
.../timelines/public/methods/index.tsx | 33 +-
.../timelines/public/mock/browser_fields.ts | 737 +
.../timelines/public/mock/cell_renderer.tsx | 20 +
.../timelines/public/mock/global_state.ts | 53 +
.../plugins/timelines/public/mock/header.ts | 133 +
x-pack/plugins/timelines/public/mock/index.ts | 16 +
.../timelines/public/mock/index_pattern.ts | 112 +
.../public/mock/kibana_react.mock.ts | 36 +
.../public/mock/mock_and_providers.tsx | 93 +
.../public/mock/mock_data_providers.tsx | 59 +
.../public/mock/mock_local_storage.ts | 35 +
.../mock/mock_timeline_control_columns.tsx | 117 +
.../public/mock/mock_timeline_data.ts | 1511 +
.../timelines/public/mock/plugin_mock.tsx | 27 +
.../timelines/public/mock/test_providers.tsx | 57 +
x-pack/plugins/timelines/public/plugin.ts | 62 +-
.../timelines/public/store/t_grid/actions.ts | 103 +
.../timelines/public/store/t_grid/defaults.ts | 103 +
.../timelines/public/store/t_grid/helpers.ts | 424 +
.../timelines/public/store/t_grid/index.ts | 65 +
.../timelines/public/store/t_grid/inputs.ts | 13 +
.../timelines/public/store/t_grid/model.ts | 128 +
.../timelines/public/store/t_grid/reducer.ts | 212 +
.../public/store/t_grid/selectors.ts | 48 +
.../public/store/t_grid/translations.ts | 32 +
.../timelines/public/store/t_grid/types.ts | 67 +
x-pack/plugins/timelines/public/types.ts | 45 +-
x-pack/plugins/timelines/server/config.ts | 2 +-
x-pack/plugins/timelines/server/index.ts | 2 +-
x-pack/plugins/timelines/server/plugin.ts | 24 +-
.../index_fields/index.test.ts | 6 +-
.../search_strategy/index_fields/index.ts | 4 +-
.../search_strategy/index_fields/mock.ts | 0
.../timeline/eql/__mocks__/index.ts | 2 +-
.../timeline/eql/helpers.test.ts | 0
.../search_strategy/timeline/eql/helpers.ts | 4 +-
.../search_strategy/timeline/eql/index.ts | 4 +-
.../timeline/factory/events/all/constants.ts | 35 +-
.../factory/events/all/helpers.test.ts | 570 +
.../timeline/factory/events/all/helpers.ts | 7 +-
.../timeline/factory/events/all/index.ts | 30 +-
.../events/all/query.events_all.dsl.ts | 2 +-
.../timeline/factory/events/details/index.ts | 6 +-
.../details/query.events_details.dsl.test.ts | 0
.../details/query.events_details.dsl.ts | 0
.../timeline/factory/events/index.ts | 4 +-
.../timeline/factory/events/kpi/index.ts | 4 +-
.../factory/events/kpi/query.kpi.dsl.ts | 2 +-
.../factory/events/last_event_time/index.ts | 4 +-
.../query.events_last_event_time.dsl.ts | 0
.../search_strategy/timeline/factory/index.ts | 6 +-
.../search_strategy/timeline/factory/types.ts | 2 +-
.../server/search_strategy/timeline/index.ts | 9 +-
x-pack/plugins/timelines/server/types.ts | 13 +-
.../server/utils/beat_schema/fields.ts | 36119 ++++++++++++++++
.../timelines/server/utils/build_query.ts | 21 +
.../plugins/timelines/server/utils/filters.ts | 12 +
x-pack/plugins/timelines/tsconfig.json | 46 +-
.../translations/translations/ja-JP.json | 4 -
.../translations/translations/zh-CN.json | 4 -
.../apis/security_solution/events.ts | 4 +-
.../apis/security_solution/sources.ts | 14 +-
.../security_solution/timeline_details.ts | 4 +-
.../applications/timelines_test/index.tsx | 30 +-
.../plugins/timelines_test/public/plugin.ts | 23 +-
.../test_suites/timelines/index.ts | 2 +-
x-pack/yarn.lock | 31 +
yarn.lock | 30 +-
528 files changed, 60238 insertions(+), 3994 deletions(-)
create mode 100644 packages/kbn-securitysolution-t-grid/BUILD.bazel
create mode 100644 packages/kbn-securitysolution-t-grid/README.md
create mode 100644 packages/kbn-securitysolution-t-grid/babel.config.js
create mode 100644 packages/kbn-securitysolution-t-grid/jest.config.js
create mode 100644 packages/kbn-securitysolution-t-grid/package.json
create mode 100644 packages/kbn-securitysolution-t-grid/react/package.json
create mode 100644 packages/kbn-securitysolution-t-grid/src/constants/index.ts
create mode 100644 packages/kbn-securitysolution-t-grid/src/index.ts
create mode 100644 packages/kbn-securitysolution-t-grid/src/mock/index.ts
rename {x-pack/plugins/security_solution/common/utils => packages/kbn-securitysolution-t-grid/src/mock}/mock_event_details.ts (97%)
create mode 100644 packages/kbn-securitysolution-t-grid/src/utils/api/index.ts
create mode 100644 packages/kbn-securitysolution-t-grid/src/utils/drag_and_drop/index.ts
create mode 100644 packages/kbn-securitysolution-t-grid/src/utils/index.ts
create mode 100644 packages/kbn-securitysolution-t-grid/tsconfig.browser.json
create mode 100644 packages/kbn-securitysolution-t-grid/tsconfig.json
create mode 100644 x-pack/plugins/security_solution/common/types/index.ts
create mode 100644 x-pack/plugins/security_solution/common/types/timeline/actions/index.ts
create mode 100644 x-pack/plugins/security_solution/common/types/timeline/cells/index.ts
create mode 100644 x-pack/plugins/security_solution/common/types/timeline/columns/index.ts
create mode 100644 x-pack/plugins/security_solution/common/types/timeline/data_provider/index.ts
create mode 100644 x-pack/plugins/security_solution/common/types/timeline/rows/index.ts
create mode 100644 x-pack/plugins/security_solution/common/types/timeline/store.ts
create mode 100644 x-pack/plugins/security_solution/public/common/components/accessibility/index.ts
delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx
delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx
create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/eql.ts
delete mode 100644 x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts
create mode 100644 x-pack/plugins/timelines/common/constants.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/agent/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/auditd/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/cloud/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/destination/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/dns/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.test.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/endgame/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/event/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/file/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/geo/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/host/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/http/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/network/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/process/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/ransomware/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/registry/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/rule/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/signal/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/source/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/suricata/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/system/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/threat/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/tls/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/url/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/user/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/winlog/index.ts
create mode 100644 x-pack/plugins/timelines/common/ecs/zeek/index.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/common/index.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/eql/index.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.mock.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.test.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/eql/validation/index.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/index.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/timeline/events/common/index.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts
create mode 100644 x-pack/plugins/timelines/common/search_strategy/timeline/index.ts
create mode 100644 x-pack/plugins/timelines/common/typed_json.ts
create mode 100644 x-pack/plugins/timelines/common/types/index.ts
create mode 100644 x-pack/plugins/timelines/common/types/timeline/actions/index.ts
create mode 100644 x-pack/plugins/timelines/common/types/timeline/cells/index.ts
create mode 100644 x-pack/plugins/timelines/common/types/timeline/columns/index.ts
create mode 100644 x-pack/plugins/timelines/common/types/timeline/data_provider/index.ts
create mode 100644 x-pack/plugins/timelines/common/types/timeline/index.ts
create mode 100644 x-pack/plugins/timelines/common/types/timeline/note/index.ts
create mode 100644 x-pack/plugins/timelines/common/types/timeline/pinned_event/index.ts
create mode 100644 x-pack/plugins/timelines/common/types/timeline/rows/index.ts
create mode 100644 x-pack/plugins/timelines/common/types/timeline/store.ts
create mode 100644 x-pack/plugins/timelines/common/utility_types.ts
rename x-pack/plugins/{security_solution/public/common/components => timelines/common/utils}/accessibility/helpers.test.tsx (100%)
rename x-pack/plugins/{security_solution/public/common/components => timelines/common/utils}/accessibility/helpers.ts (99%)
create mode 100644 x-pack/plugins/timelines/common/utils/accessibility/index.ts
rename x-pack/plugins/{security_solution/public/common/utils/api/index.ts => timelines/common/utils/api.ts} (100%)
create mode 100644 x-pack/plugins/timelines/common/utils/field_formatters.test.ts
create mode 100644 x-pack/plugins/timelines/common/utils/field_formatters.ts
create mode 100644 x-pack/plugins/timelines/common/utils/to_array.ts
create mode 100644 x-pack/plugins/timelines/jest.config.js
rename x-pack/plugins/{security_solution/public/common => timelines/public}/components/drag_and_drop/draggable_keyboard_wrapper_hook/index.tsx (92%)
create mode 100644 x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts
create mode 100644 x-pack/plugins/timelines/public/components/drag_and_drop/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/draggables/field_badge/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/draggables/field_badge/translations.ts
create mode 100644 x-pack/plugins/timelines/public/components/draggables/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/exit_full_screen/index.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/exit_full_screen/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/exit_full_screen/translations.ts
create mode 100644 x-pack/plugins/timelines/public/components/inspect/index.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/inspect/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/inspect/modal.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/inspect/modal.tsx
create mode 100644 x-pack/plugins/timelines/public/components/inspect/translations.ts
rename x-pack/plugins/{security_solution/public/common => timelines/public}/components/last_updated/index.test.tsx (100%)
rename x-pack/plugins/{security_solution/public/common => timelines/public}/components/last_updated/index.tsx (94%)
rename x-pack/plugins/{security_solution/public/common => timelines/public}/components/last_updated/translations.ts (67%)
create mode 100644 x-pack/plugins/timelines/public/components/loading/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/__snapshots__/index.test.tsx.snap
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/actions/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/column_header.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/dragging_container.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/styles.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/default_headers.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/__snapshots__/index.test.tsx.snap
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/header_content.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/helpers.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/constants.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/__snapshots__/index.test.tsx.snap
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/translations.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/events/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event_context.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/events/translations.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/events/use_stateful_event_focus/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_column_renderer.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_row_renderer.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/renderers/row_renderer.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/sort/__snapshots__/sort_indicator.test.tsx.snap
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/sort/index.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_number.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/translations.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/footer/index.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/footer/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/footer/translations.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/header_section/__snapshots__/index.test.tsx.snap
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/header_section/index.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/header_section/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/helpers.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/helpers.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/integrated/translations.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/standalone/translations.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/styles.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/subtitle/__snapshots__/index.test.tsx.snap
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/subtitle/index.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/subtitle/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/translations.ts
create mode 100644 x-pack/plugins/timelines/public/components/t_grid/types.ts
create mode 100644 x-pack/plugins/timelines/public/components/tgrid.tsx
create mode 100644 x-pack/plugins/timelines/public/components/truncatable_text/__snapshots__/index.test.tsx.snap
create mode 100644 x-pack/plugins/timelines/public/components/truncatable_text/index.test.tsx
create mode 100644 x-pack/plugins/timelines/public/components/truncatable_text/index.tsx
create mode 100644 x-pack/plugins/timelines/public/components/utils/helpers.ts
create mode 100644 x-pack/plugins/timelines/public/components/utils/keury/index.test.ts
create mode 100644 x-pack/plugins/timelines/public/components/utils/keury/index.ts
create mode 100644 x-pack/plugins/timelines/public/components/utils/use_mount_appended.ts
create mode 100644 x-pack/plugins/timelines/public/container/index.tsx
create mode 100644 x-pack/plugins/timelines/public/container/translations.ts
rename x-pack/plugins/{security_solution/public/common/hooks/use_add_to_timeline.tsx => timelines/public/hooks/use_add_to_timeline.ts} (90%)
create mode 100644 x-pack/plugins/timelines/public/hooks/use_app_toasts.ts
create mode 100644 x-pack/plugins/timelines/public/hooks/use_selector.tsx
delete mode 100644 x-pack/plugins/timelines/public/index.scss
create mode 100644 x-pack/plugins/timelines/public/mock/browser_fields.ts
create mode 100644 x-pack/plugins/timelines/public/mock/cell_renderer.tsx
create mode 100644 x-pack/plugins/timelines/public/mock/global_state.ts
create mode 100644 x-pack/plugins/timelines/public/mock/header.ts
create mode 100644 x-pack/plugins/timelines/public/mock/index.ts
create mode 100644 x-pack/plugins/timelines/public/mock/index_pattern.ts
create mode 100644 x-pack/plugins/timelines/public/mock/kibana_react.mock.ts
create mode 100644 x-pack/plugins/timelines/public/mock/mock_and_providers.tsx
create mode 100644 x-pack/plugins/timelines/public/mock/mock_data_providers.tsx
create mode 100644 x-pack/plugins/timelines/public/mock/mock_local_storage.ts
create mode 100644 x-pack/plugins/timelines/public/mock/mock_timeline_control_columns.tsx
create mode 100644 x-pack/plugins/timelines/public/mock/mock_timeline_data.ts
create mode 100644 x-pack/plugins/timelines/public/mock/plugin_mock.tsx
create mode 100644 x-pack/plugins/timelines/public/mock/test_providers.tsx
create mode 100644 x-pack/plugins/timelines/public/store/t_grid/actions.ts
create mode 100644 x-pack/plugins/timelines/public/store/t_grid/defaults.ts
create mode 100644 x-pack/plugins/timelines/public/store/t_grid/helpers.ts
create mode 100644 x-pack/plugins/timelines/public/store/t_grid/index.ts
create mode 100644 x-pack/plugins/timelines/public/store/t_grid/inputs.ts
create mode 100644 x-pack/plugins/timelines/public/store/t_grid/model.ts
create mode 100644 x-pack/plugins/timelines/public/store/t_grid/reducer.ts
create mode 100644 x-pack/plugins/timelines/public/store/t_grid/selectors.ts
create mode 100644 x-pack/plugins/timelines/public/store/t_grid/translations.ts
create mode 100644 x-pack/plugins/timelines/public/store/t_grid/types.ts
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/index_fields/index.test.ts (99%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/index_fields/index.ts (99%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/index_fields/mock.ts (100%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/eql/__mocks__/index.ts (99%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/eql/helpers.test.ts (100%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/eql/helpers.ts (96%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/eql/index.ts (91%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/events/all/constants.ts (78%)
create mode 100644 x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/helpers.test.ts
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/events/all/helpers.ts (96%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/events/all/index.ts (70%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts (96%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/events/details/index.ts (89%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts (100%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts (100%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/events/index.ts (87%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/events/kpi/index.ts (90%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/events/kpi/query.kpi.dsl.ts (96%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/events/last_event_time/index.ts (89%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts (100%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/index.ts (72%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/factory/types.ts (88%)
rename x-pack/plugins/{security_solution => timelines}/server/search_strategy/timeline/index.ts (81%)
create mode 100644 x-pack/plugins/timelines/server/utils/beat_schema/fields.ts
create mode 100644 x-pack/plugins/timelines/server/utils/build_query.ts
create mode 100644 x-pack/plugins/timelines/server/utils/filters.ts
create mode 100644 x-pack/yarn.lock
diff --git a/.eslintrc.js b/.eslintrc.js
index 40dd6a55a2a3f..c64f03a8398e5 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -893,6 +893,8 @@ module.exports = {
files: [
'x-pack/plugins/security_solution/public/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/security_solution/common/**/*.{js,mjs,ts,tsx}',
+ 'x-pack/plugins/timelines/public/**/*.{js,mjs,ts,tsx}',
+ 'x-pack/plugins/timelines/common/**/*.{js,mjs,ts,tsx}',
],
rules: {
'import/no-nodejs-modules': 'error',
@@ -907,7 +909,10 @@ module.exports = {
},
{
// typescript only for front and back end
- files: ['x-pack/plugins/security_solution/**/*.{ts,tsx}'],
+ files: [
+ 'x-pack/plugins/security_solution/**/*.{ts,tsx}',
+ 'x-pack/plugins/timelines/**/*.{ts,tsx}',
+ ],
rules: {
'@typescript-eslint/no-this-alias': 'error',
'@typescript-eslint/no-explicit-any': 'error',
@@ -917,7 +922,10 @@ module.exports = {
},
{
// typescript and javascript for front and back end
- files: ['x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}'],
+ files: [
+ 'x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}',
+ 'x-pack/plugins/timelines/**/*.{js,mjs,ts,tsx}',
+ ],
plugins: ['eslint-plugin-node', 'react'],
env: {
jest: true,
diff --git a/package.json b/package.json
index 36fa086657adf..9fc62dd69f1cf 100644
--- a/package.json
+++ b/package.json
@@ -149,6 +149,7 @@
"@kbn/securitysolution-list-api": "link:bazel-bin/packages/kbn-securitysolution-list-api",
"@kbn/securitysolution-list-hooks": "link:bazel-bin/packages/kbn-securitysolution-list-hooks",
"@kbn/securitysolution-list-utils": "link:bazel-bin/packages/kbn-securitysolution-list-utils",
+ "@kbn/securitysolution-t-grid": "link:bazel-bin/packages/kbn-securitysolution-t-grid",
"@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils",
"@kbn/server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools",
"@kbn/server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository",
@@ -217,6 +218,8 @@
"cytoscape-dagre": "^2.2.2",
"d3": "3.5.17",
"d3-array": "1.2.4",
+ "d3-cloud": "1.2.5",
+ "d3-interpolate": "^3.0.1",
"d3-scale": "1.0.7",
"d3-shape": "^1.1.0",
"d3-time": "^1.1.0",
@@ -511,6 +514,7 @@
"@types/cytoscape": "^3.14.0",
"@types/d3": "^3.5.43",
"@types/d3-array": "^1.2.7",
+ "@types/d3-interpolate": "^2.0.0",
"@types/d3-scale": "^2.1.1",
"@types/d3-shape": "^1.3.1",
"@types/d3-time": "^1.0.10",
diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel
index b1c3f580c6baf..801f7cdd7f8dc 100644
--- a/packages/BUILD.bazel
+++ b/packages/BUILD.bazel
@@ -3,7 +3,7 @@
filegroup(
name = "build",
srcs = [
- "//packages/elastic-datemath:build",
+ "//packages/elastic-datemath:build",
"//packages/elastic-eslint-config-kibana:build",
"//packages/elastic-safer-lodash-set:build",
"//packages/kbn-ace:build",
@@ -41,6 +41,7 @@ filegroup(
"//packages/kbn-securitysolution-list-utils:build",
"//packages/kbn-securitysolution-utils:build",
"//packages/kbn-securitysolution-es-utils:build",
+ "//packages/kbn-securitysolution-t-grid:build",
"//packages/kbn-securitysolution-hook-utils:build",
"//packages/kbn-server-http-tools:build",
"//packages/kbn-server-route-repository:build",
diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml
index f9127e4629f43..c6960621359c7 100644
--- a/packages/kbn-optimizer/limits.yml
+++ b/packages/kbn-optimizer/limits.yml
@@ -67,7 +67,7 @@ pageLoadAssetSize:
searchprofiler: 67080
security: 95864
securityOss: 30806
- securitySolution: 76000
+ securitySolution: 217673
share: 99061
snapshotRestore: 79032
spaces: 57868
@@ -107,7 +107,7 @@ pageLoadAssetSize:
dataVisualizer: 27530
banners: 17946
mapsEms: 26072
- timelines: 28613
+ timelines: 230410
screenshotMode: 17856
visTypePie: 35583
cases: 144442
diff --git a/packages/kbn-securitysolution-t-grid/BUILD.bazel b/packages/kbn-securitysolution-t-grid/BUILD.bazel
new file mode 100644
index 0000000000000..5cf1081bdd32e
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/BUILD.bazel
@@ -0,0 +1,125 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
+
+PKG_BASE_NAME = "kbn-securitysolution-t-grid"
+
+PKG_REQUIRE_NAME = "@kbn/securitysolution-t-grid"
+
+SOURCE_FILES = glob(
+ [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ ],
+ exclude = [
+ "**/*.test.*",
+ "**/*.mock.*",
+ ],
+)
+
+SRCS = SOURCE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "react/package.json",
+ "package.json",
+ "README.md",
+]
+
+SRC_DEPS = [
+ "//packages/kbn-babel-preset",
+ "//packages/kbn-dev-utils",
+ "//packages/kbn-i18n",
+ "@npm//@babel/core",
+ "@npm//babel-loader",
+ "@npm//enzyme",
+ "@npm//jest",
+ "@npm//lodash",
+ "@npm//react",
+ "@npm//react-beautiful-dnd",
+ "@npm//tslib",
+]
+
+TYPES_DEPS = [
+ "@npm//typescript",
+ "@npm//@types/enzyme",
+ "@npm//@types/jest",
+ "@npm//@types/lodash",
+ "@npm//@types/node",
+ "@npm//@types/react",
+ "@npm//@types/react-beautiful-dnd",
+]
+
+DEPS = SRC_DEPS + TYPES_DEPS
+
+ts_config(
+ name = "tsconfig",
+ src = "tsconfig.json",
+ deps = [
+ "//:tsconfig.base.json",
+ ],
+)
+
+ts_config(
+ name = "tsconfig_browser",
+ src = "tsconfig.browser.json",
+ deps = [
+ "//:tsconfig.base.json",
+ "//:tsconfig.browser.json",
+ ],
+)
+
+ts_project(
+ name = "tsc",
+ args = ["--pretty"],
+ srcs = SRCS,
+ deps = DEPS,
+ declaration = True,
+ declaration_dir = "target_types",
+ declaration_map = True,
+ incremental = True,
+ out_dir = "target_node",
+ root_dir = "src",
+ source_map = True,
+ tsconfig = ":tsconfig",
+)
+
+ts_project(
+ name = "tsc_browser",
+ args = ['--pretty'],
+ srcs = SRCS,
+ deps = DEPS,
+ allow_js = True,
+ declaration = False,
+ incremental = True,
+ out_dir = "target_web",
+ source_map = True,
+ root_dir = "src",
+ tsconfig = ":tsconfig_browser",
+)
+
+js_library(
+ name = PKG_BASE_NAME,
+ package_name = PKG_REQUIRE_NAME,
+ srcs = NPM_MODULE_EXTRA_FILES,
+ visibility = ["//visibility:public"],
+ deps = [":tsc", ":tsc_browser"] + DEPS,
+)
+
+pkg_npm(
+ name = "npm_module",
+ deps = [
+ ":%s" % PKG_BASE_NAME,
+ ],
+)
+
+filegroup(
+ name = "build",
+ srcs = [
+ ":npm_module",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/packages/kbn-securitysolution-t-grid/README.md b/packages/kbn-securitysolution-t-grid/README.md
new file mode 100644
index 0000000000000..a49669c81689a
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/README.md
@@ -0,0 +1,3 @@
+# kbn-securitysolution-t-grid
+
+We do not want to create circular dependencies between security_solution and timelines plugins. Therefore , we will use this packages to share components between these two plugins.
diff --git a/packages/kbn-securitysolution-t-grid/babel.config.js b/packages/kbn-securitysolution-t-grid/babel.config.js
new file mode 100644
index 0000000000000..b4a118df51af5
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/babel.config.js
@@ -0,0 +1,19 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ env: {
+ web: {
+ presets: ['@kbn/babel-preset/webpack_preset'],
+ },
+ node: {
+ presets: ['@kbn/babel-preset/node_preset'],
+ },
+ },
+ ignore: ['**/*.test.ts', '**/*.test.tsx'],
+};
diff --git a/packages/kbn-securitysolution-t-grid/jest.config.js b/packages/kbn-securitysolution-t-grid/jest.config.js
new file mode 100644
index 0000000000000..21e7d2d71b61a
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/jest.config.js
@@ -0,0 +1,13 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../..',
+ roots: ['/packages/kbn-securitysolution-t-grid'],
+};
diff --git a/packages/kbn-securitysolution-t-grid/package.json b/packages/kbn-securitysolution-t-grid/package.json
new file mode 100644
index 0000000000000..68d3a8c71e7ca
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@kbn/securitysolution-t-grid",
+ "version": "1.0.0",
+ "description": "security solution t-grid packages will allow sharing components between timelines and security_solution plugin until we transfer all functionality to timelines plugin",
+ "license": "SSPL-1.0 OR Elastic License 2.0",
+ "browser": "./target_web/browser.js",
+ "main": "./target_node/index.js",
+ "types": "./target_types/index.d.ts",
+ "private": true
+}
diff --git a/packages/kbn-securitysolution-t-grid/react/package.json b/packages/kbn-securitysolution-t-grid/react/package.json
new file mode 100644
index 0000000000000..c29ddd45f084d
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/react/package.json
@@ -0,0 +1,5 @@
+{
+ "browser": "../target_web/react",
+ "main": "../target_node/react",
+ "types": "../target_types/react/index.d.ts"
+}
\ No newline at end of file
diff --git a/packages/kbn-securitysolution-t-grid/src/constants/index.ts b/packages/kbn-securitysolution-t-grid/src/constants/index.ts
new file mode 100644
index 0000000000000..c03c0093d9839
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/constants/index.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export const HIGHLIGHTED_DROP_TARGET_CLASS_NAME = 'highlighted-drop-target';
+export const EMPTY_PROVIDERS_GROUP_CLASS_NAME = 'empty-providers-group';
+
+/** The draggable will move this many pixels via the keyboard when the arrow key is pressed */
+export const KEYBOARD_DRAG_OFFSET = 20;
+
+export const DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME = 'draggable-keyboard-wrapper';
+
+export const ROW_RENDERER_CLASS_NAME = 'row-renderer';
+
+export const NOTES_CONTAINER_CLASS_NAME = 'notes-container';
+
+export const NOTE_CONTENT_CLASS_NAME = 'note-content';
+
+/** This class is added to the document body while dragging */
+export const IS_DRAGGING_CLASS_NAME = 'is-dragging';
+
+export const HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME = 'hover-actions-always-show';
diff --git a/packages/kbn-securitysolution-t-grid/src/index.ts b/packages/kbn-securitysolution-t-grid/src/index.ts
new file mode 100644
index 0000000000000..0c2e9a7dbea8b
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/index.ts
@@ -0,0 +1,11 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './constants';
+export * from './utils';
+export * from './mock';
diff --git a/packages/kbn-securitysolution-t-grid/src/mock/index.ts b/packages/kbn-securitysolution-t-grid/src/mock/index.ts
new file mode 100644
index 0000000000000..dc1b63dfc33b0
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/mock/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './mock_event_details';
diff --git a/x-pack/plugins/security_solution/common/utils/mock_event_details.ts b/packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts
similarity index 97%
rename from x-pack/plugins/security_solution/common/utils/mock_event_details.ts
rename to packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts
index 7dc257ebb3fef..167fc9dd17a2a 100644
--- a/x-pack/plugins/security_solution/common/utils/mock_event_details.ts
+++ b/packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
export const eventHit = {
diff --git a/packages/kbn-securitysolution-t-grid/src/utils/api/index.ts b/packages/kbn-securitysolution-t-grid/src/utils/api/index.ts
new file mode 100644
index 0000000000000..34e448419693b
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/utils/api/index.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { has } from 'lodash/fp';
+
+export interface AppError extends Error {
+ body: {
+ message: string;
+ };
+}
+
+export interface KibanaError extends AppError {
+ body: {
+ message: string;
+ statusCode: number;
+ };
+}
+
+export interface SecurityAppError extends AppError {
+ body: {
+ message: string;
+ status_code: number;
+ };
+}
+
+export const isKibanaError = (error: unknown): error is KibanaError =>
+ has('message', error) && has('body.message', error) && has('body.statusCode', error);
+
+export const isSecurityAppError = (error: unknown): error is SecurityAppError =>
+ has('message', error) && has('body.message', error) && has('body.status_code', error);
+
+export const isAppError = (error: unknown): error is AppError =>
+ isKibanaError(error) || isSecurityAppError(error);
+
+export const isNotFoundError = (error: unknown) =>
+ (isKibanaError(error) && error.body.statusCode === 404) ||
+ (isSecurityAppError(error) && error.body.status_code === 404);
diff --git a/packages/kbn-securitysolution-t-grid/src/utils/drag_and_drop/index.ts b/packages/kbn-securitysolution-t-grid/src/utils/drag_and_drop/index.ts
new file mode 100644
index 0000000000000..91b2e88d97358
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/utils/drag_and_drop/index.ts
@@ -0,0 +1,133 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { DropResult } from 'react-beautiful-dnd';
+
+export const draggableIdPrefix = 'draggableId';
+
+export const droppableIdPrefix = 'droppableId';
+
+export const draggableContentPrefix = `${draggableIdPrefix}.content.`;
+
+export const draggableTimelineProvidersPrefix = `${draggableIdPrefix}.timelineProviders.`;
+
+export const draggableFieldPrefix = `${draggableIdPrefix}.field.`;
+
+export const droppableContentPrefix = `${droppableIdPrefix}.content.`;
+
+export const droppableFieldPrefix = `${droppableIdPrefix}.field.`;
+
+export const droppableTimelineProvidersPrefix = `${droppableIdPrefix}.timelineProviders.`;
+
+export const droppableTimelineColumnsPrefix = `${droppableIdPrefix}.timelineColumns.`;
+
+export const droppableTimelineFlyoutBottomBarPrefix = `${droppableIdPrefix}.flyoutButton.`;
+
+export const getDraggableId = (dataProviderId: string): string =>
+ `${draggableContentPrefix}${dataProviderId}`;
+
+export const getDraggableFieldId = ({
+ contextId,
+ fieldId,
+}: {
+ contextId: string;
+ fieldId: string;
+}): string => `${draggableFieldPrefix}${escapeContextId(contextId)}.${escapeFieldId(fieldId)}`;
+
+export const getTimelineProviderDroppableId = ({
+ groupIndex,
+ timelineId,
+}: {
+ groupIndex: number;
+ timelineId: string;
+}): string => `${droppableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}`;
+
+export const getTimelineProviderDraggableId = ({
+ dataProviderId,
+ groupIndex,
+ timelineId,
+}: {
+ dataProviderId: string;
+ groupIndex: number;
+ timelineId: string;
+}): string =>
+ `${draggableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}.${dataProviderId}`;
+
+export const getDroppableId = (visualizationPlaceholderId: string): string =>
+ `${droppableContentPrefix}${visualizationPlaceholderId}`;
+
+export const sourceIsContent = (result: DropResult): boolean =>
+ result.source.droppableId.startsWith(droppableContentPrefix);
+
+export const sourceAndDestinationAreSameTimelineProviders = (result: DropResult): boolean => {
+ const regex = /^droppableId\.timelineProviders\.(\S+)\./;
+ const sourceMatches = result.source.droppableId.match(regex) || [];
+ const destinationMatches =
+ (result.destination && result.destination.droppableId.match(regex)) || [];
+
+ return (
+ sourceMatches.length >= 2 &&
+ destinationMatches.length >= 2 &&
+ sourceMatches[1] === destinationMatches[1]
+ );
+};
+
+export const draggableIsContent = (result: DropResult | { draggableId: string }): boolean =>
+ result.draggableId.startsWith(draggableContentPrefix);
+
+export const draggableIsField = (result: DropResult | { draggableId: string }): boolean =>
+ result.draggableId.startsWith(draggableFieldPrefix);
+
+export const reasonIsDrop = (result: DropResult): boolean => result.reason === 'DROP';
+
+export const destinationIsTimelineProviders = (result: DropResult): boolean =>
+ result.destination != null &&
+ result.destination.droppableId.startsWith(droppableTimelineProvidersPrefix);
+
+export const destinationIsTimelineColumns = (result: DropResult): boolean =>
+ result.destination != null &&
+ result.destination.droppableId.startsWith(droppableTimelineColumnsPrefix);
+
+export const destinationIsTimelineButton = (result: DropResult): boolean =>
+ result.destination != null &&
+ result.destination.droppableId.startsWith(droppableTimelineFlyoutBottomBarPrefix);
+
+export const getProviderIdFromDraggable = (result: DropResult): string =>
+ result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1);
+
+export const getFieldIdFromDraggable = (result: DropResult): string =>
+ unEscapeFieldId(result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1));
+
+export const escapeDataProviderId = (path: string) => path.replace(/\./g, '_');
+
+export const escapeContextId = (path: string) => path.replace(/\./g, '_');
+
+export const escapeFieldId = (path: string) => path.replace(/\./g, '!!!DOT!!!');
+
+export const unEscapeFieldId = (path: string) => path.replace(/!!!DOT!!!/g, '.');
+
+export const providerWasDroppedOnTimeline = (result: DropResult): boolean =>
+ reasonIsDrop(result) &&
+ draggableIsContent(result) &&
+ sourceIsContent(result) &&
+ destinationIsTimelineProviders(result);
+
+export const userIsReArrangingProviders = (result: DropResult): boolean =>
+ reasonIsDrop(result) && sourceAndDestinationAreSameTimelineProviders(result);
+
+export const fieldWasDroppedOnTimelineColumns = (result: DropResult): boolean =>
+ reasonIsDrop(result) && draggableIsField(result) && destinationIsTimelineColumns(result);
+
+/**
+ * Prevents fields from being dragged or dropped to any area other than column
+ * header drop zone in the timeline
+ */
+export const DRAG_TYPE_FIELD = 'drag-type-field';
+
+/** This class is added to the document body while timeline field dragging */
+export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging';
diff --git a/packages/kbn-securitysolution-t-grid/src/utils/index.ts b/packages/kbn-securitysolution-t-grid/src/utils/index.ts
new file mode 100644
index 0000000000000..39629a990c539
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/src/utils/index.ts
@@ -0,0 +1,10 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './api';
+export * from './drag_and_drop';
diff --git a/packages/kbn-securitysolution-t-grid/tsconfig.browser.json b/packages/kbn-securitysolution-t-grid/tsconfig.browser.json
new file mode 100644
index 0000000000000..a5183ba4fd457
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/tsconfig.browser.json
@@ -0,0 +1,23 @@
+{
+ "extends": "../../tsconfig.browser.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "incremental": true,
+ "outDir": "./target_web",
+ "declaration": false,
+ "isolatedModules": true,
+ "sourceMap": true,
+ "sourceRoot": "../../../../../packages/kbn-securitysolution-t-grid/src",
+ "types": [
+ "jest",
+ "node"
+ ],
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ ],
+ "exclude": [
+ "**/__fixtures__/**/*"
+ ]
+}
diff --git a/packages/kbn-securitysolution-t-grid/tsconfig.json b/packages/kbn-securitysolution-t-grid/tsconfig.json
new file mode 100644
index 0000000000000..8cda578edede4
--- /dev/null
+++ b/packages/kbn-securitysolution-t-grid/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "incremental": true,
+ "outDir": "target",
+ "rootDir": "src",
+ "sourceMap": true,
+ "sourceRoot": "../../../../packages/kbn-securitysolution-t-grid/src",
+ "types": [
+ "jest",
+ "node"
+ ]
+ },
+ "include": [
+ "src/**/*"
+ ]
+}
diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js
index 225f93d487823..5baff607704c7 100644
--- a/packages/kbn-test/jest-preset.js
+++ b/packages/kbn-test/jest-preset.js
@@ -94,7 +94,7 @@ module.exports = {
transformIgnorePatterns: [
// ignore all node_modules except monaco-editor and react-monaco-editor which requires babel transforms to handle dynamic import()
// since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842)
- '[/\\\\]node_modules(?![\\/\\\\](monaco-editor|react-monaco-editor))[/\\\\].+\\.js$',
+ '[/\\\\]node_modules(?![\\/\\\\](monaco-editor|react-monaco-editor|d3-interpolate|d3-color))[/\\\\].+\\.js$',
'packages/kbn-pm/dist/index.js',
],
diff --git a/tsconfig.json b/tsconfig.json
index c91f7b768a5c4..f6df8fcbb6406 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -70,7 +70,6 @@
{ "path": "./src/plugins/visualize/tsconfig.json" },
{ "path": "./src/plugins/index_pattern_management/tsconfig.json" },
{ "path": "./src/plugins/index_pattern_field_editor/tsconfig.json" },
-
{ "path": "./x-pack/plugins/actions/tsconfig.json" },
{ "path": "./x-pack/plugins/alerting/tsconfig.json" },
{ "path": "./x-pack/plugins/apm/tsconfig.json" },
diff --git a/tsconfig.refs.json b/tsconfig.refs.json
index 3baf5c323ef81..e08b50cc055c1 100644
--- a/tsconfig.refs.json
+++ b/tsconfig.refs.json
@@ -105,6 +105,7 @@
{ "path": "./x-pack/plugins/stack_alerts/tsconfig.json" },
{ "path": "./x-pack/plugins/task_manager/tsconfig.json" },
{ "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" },
+ { "path": "./x-pack/plugins/timelines/tsconfig.json" },
{ "path": "./x-pack/plugins/transform/tsconfig.json" },
{ "path": "./x-pack/plugins/translations/tsconfig.json" },
{ "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" },
diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts
index b20b1501eecc5..a9a81aa285af7 100644
--- a/x-pack/plugins/security_solution/common/experimental_features.ts
+++ b/x-pack/plugins/security_solution/common/experimental_features.ts
@@ -15,6 +15,7 @@ const allowedExperimentalValues = Object.freeze({
trustedAppsByPolicyEnabled: false,
metricsEntitiesEnabled: false,
ruleRegistryEnabled: false,
+ tGridEnabled: false,
});
type ExperimentalConfigKeys = Array;
diff --git a/x-pack/plugins/security_solution/common/index.ts b/x-pack/plugins/security_solution/common/index.ts
index 1fec1c76430eb..e6d7bcc9bd506 100644
--- a/x-pack/plugins/security_solution/common/index.ts
+++ b/x-pack/plugins/security_solution/common/index.ts
@@ -4,3 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
+export * from './types';
+export * from './search_strategy';
+export * from './utility_types';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
index 4fcfbdac3c1b4..095ba4ca20afc 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
@@ -4,52 +4,27 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import type { estypes } from '@elastic/elasticsearch';
import { IEsSearchResponse } from '../../../../../../src/plugins/data/common';
+export type {
+ Inspect,
+ SortField,
+ TimerangeInput,
+ PaginationInputPaginated,
+ DocValueFields,
+ CursorType,
+ TotalValue,
+} from '../../../../timelines/common';
+export { Direction } from '../../../../timelines/common';
export type Maybe = T | null;
export type SearchHit = IEsSearchResponse['rawResponse']['hits']['hits'][0];
-export interface TotalValue {
- value: number;
- relation: string;
-}
-
-export interface Inspect {
- dsl: string[];
-}
-
export interface PageInfoPaginated {
activePage: number;
fakeTotalCount: number;
showMorePagesIndicator: boolean;
}
-
-export interface CursorType {
- value?: Maybe;
- tiebreaker?: Maybe;
-}
-
-export enum Direction {
- asc = 'asc',
- desc = 'desc',
-}
-
-export interface SortField {
- field: Field;
- direction: Direction;
-}
-
-export interface TimerangeInput {
- /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */
- interval: string;
- /** The end of the timerange */
- to: string;
- /** The beginning of the timerange */
- from: string;
-}
-
export interface PaginationInput {
/** The limit parameter allows you to configure the maximum amount of items to be returned */
limit: number;
@@ -59,19 +34,6 @@ export interface PaginationInput {
tiebreaker?: Maybe;
}
-export interface PaginationInputPaginated {
- /** The activePage parameter defines the page of results you want to fetch */
- activePage: number;
- /** The cursorStart parameter defines the start of the results to be displayed */
- cursorStart: number;
- /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */
- fakePossibleCount: number;
- /** The querySize parameter is the number of items to be returned */
- querySize: number;
-}
-
-export type DocValueFields = estypes.SearchDocValueField;
-
export interface Explanation {
value: number;
description: string;
@@ -111,13 +73,3 @@ export interface GenericBuckets {
}
export type StringOrNumber = string | number;
-
-export interface TimerangeFilter {
- range: {
- [timestamp: string]: {
- gte: string;
- lte: string;
- format: string;
- };
- };
-}
diff --git a/x-pack/plugins/security_solution/common/search_strategy/index.ts b/x-pack/plugins/security_solution/common/search_strategy/index.ts
index 575256b991d16..e3d6736878063 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/index.ts
@@ -8,3 +8,4 @@
export * from './common';
export * from './security_solution';
export * from './timeline';
+export * from './index_fields';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts
index d747758640fab..4e5f8af41a2ef 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts
@@ -5,37 +5,10 @@
* 2.0.
*/
-import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
-import { Ecs } from '../../../../ecs';
-import { CursorType, Inspect, Maybe, PaginationInputPaginated } from '../../../common';
-import { TimelineRequestOptionsPaginated } from '../..';
-
-export interface TimelineEdges {
- node: TimelineItem;
- cursor: CursorType;
-}
-
-export interface TimelineItem {
- _id: string;
- _index?: Maybe;
- data: TimelineNonEcsData[];
- ecs: Ecs;
-}
-
-export interface TimelineNonEcsData {
- field: string;
- value?: Maybe;
-}
-
-export interface TimelineEventsAllStrategyResponse extends IEsSearchResponse {
- edges: TimelineEdges[];
- totalCount: number;
- pageInfo: Pick;
- inspect?: Maybe;
-}
-
-export interface TimelineEventsAllRequestOptions extends TimelineRequestOptionsPaginated {
- fields: string[] | Array<{ field: string; include_unmapped: boolean }>;
- fieldRequested: string[];
- language: 'eql' | 'kuery' | 'lucene';
-}
+export type {
+ TimelineEdges,
+ TimelineItem,
+ TimelineNonEcsData,
+ TimelineEventsAllStrategyResponse,
+ TimelineEventsAllRequestOptions,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts
index 4a5bd2c99a0eb..e4d2ea52ffdff 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts
@@ -5,22 +5,8 @@
* 2.0.
*/
-import { Ecs } from '../../../../ecs';
-import { CursorType, Maybe } from '../../../common';
-
-export interface TimelineEdges {
- node: TimelineItem;
- cursor: CursorType;
-}
-
-export interface TimelineItem {
- _id: string;
- _index?: Maybe;
- data: TimelineNonEcsData[];
- ecs: Ecs;
-}
-
-export interface TimelineNonEcsData {
- field: string;
- value?: Maybe;
-}
+export type {
+ TimelineEdges,
+ TimelineItem,
+ TimelineNonEcsData,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
index 1f9820f8e5c2b..3fd13e56cc7e7 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
@@ -5,27 +5,8 @@
* 2.0.
*/
-import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
-import { Inspect, Maybe } from '../../../common';
-import { TimelineRequestOptionsPaginated } from '../..';
-
-export interface TimelineEventsDetailsItem {
- ariaRowindex?: Maybe;
- category?: string;
- field: string;
- values?: Maybe;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- originalValue?: Maybe;
- isObjectArray: boolean;
-}
-
-export interface TimelineEventsDetailsStrategyResponse extends IEsSearchResponse {
- data?: Maybe;
- inspect?: Maybe;
-}
-
-export interface TimelineEventsDetailsRequestOptions
- extends Partial {
- indexName: string;
- eventId: string;
-}
+export type {
+ TimelineEventsDetailsItem,
+ TimelineEventsDetailsStrategyResponse,
+ TimelineEventsDetailsRequestOptions,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts
index c508876032fca..10e9bbd7670cd 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts
@@ -5,43 +5,10 @@
* 2.0.
*/
-import { EuiComboBoxOptionOption } from '@elastic/eui';
-import {
- EqlSearchStrategyRequest,
- EqlSearchStrategyResponse,
-} from '../../../../../../../../src/plugins/data/common';
-import { Inspect, Maybe, PaginationInputPaginated } from '../../..';
-import { TimelineEdges, TimelineEventsAllRequestOptions } from '../..';
-import { EqlSearchResponse } from '../../../../detection_engine/types';
-
-export interface TimelineEqlRequestOptions
- extends EqlSearchStrategyRequest,
- Omit {
- eventCategoryField?: string;
- tiebreakerField?: string;
- timestampField?: string;
- size?: number;
-}
-
-export interface TimelineEqlResponse extends EqlSearchStrategyResponse> {
- edges: TimelineEdges[];
- totalCount: number;
- pageInfo: Pick;
- inspect: Maybe;
-}
-
-export interface EqlOptionsData {
- keywordFields: EuiComboBoxOptionOption[];
- dateFields: EuiComboBoxOptionOption[];
- nonDateFields: EuiComboBoxOptionOption[];
-}
-
-export interface EqlOptionsSelected {
- eventCategoryField?: string;
- tiebreakerField?: string;
- timestampField?: string;
- query?: string;
- size?: number;
-}
-
-export type FieldsEqlOptions = keyof EqlOptionsSelected;
+export type {
+ TimelineEqlRequestOptions,
+ TimelineEqlResponse,
+ EqlOptionsData,
+ EqlOptionsSelected,
+ FieldsEqlOptions,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts
index f29dc4a3c7450..39f23a63c8afe 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts
@@ -5,38 +5,11 @@
* 2.0.
*/
-import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
-import { Inspect, Maybe } from '../../../common';
-import { TimelineRequestBasicOptions } from '../..';
-
-export enum LastEventIndexKey {
- hostDetails = 'hostDetails',
- hosts = 'hosts',
- ipDetails = 'ipDetails',
- network = 'network',
-}
-
-export interface LastTimeDetails {
- hostName?: Maybe;
- ip?: Maybe;
-}
-
-export interface TimelineEventsLastEventTimeStrategyResponse extends IEsSearchResponse {
- lastSeen: Maybe;
- inspect?: Maybe;
-}
-
-export interface TimelineKpiStrategyResponse extends IEsSearchResponse {
- destinationIpCount: number;
- inspect?: Maybe;
- hostCount: number;
- processCount: number;
- sourceIpCount: number;
- userCount: number;
-}
-
-export interface TimelineEventsLastEventTimeRequestOptions
- extends Omit {
- indexKey: LastEventIndexKey;
- details: LastTimeDetails;
-}
+export { LastEventIndexKey } from '../../../../../../timelines/common';
+
+export type {
+ LastTimeDetails,
+ TimelineEventsLastEventTimeStrategyResponse,
+ TimelineKpiStrategyResponse,
+ TimelineEventsLastEventTimeRequestOptions,
+} from '../../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts
index 9c2c23eb334a3..7064ef033fc5a 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts
@@ -24,7 +24,12 @@ import {
SortField,
Maybe,
} from '../common';
-import { DataProviderType, TimelineType, TimelineStatus } from '../../types/timeline';
+import {
+ DataProviderType,
+ TimelineType,
+ TimelineStatus,
+ RowRendererId,
+} from '../../types/timeline';
export * from './events';
@@ -165,25 +170,6 @@ export interface SortTimelineInput {
sortDirection?: Maybe;
}
-export enum RowRendererId {
- alerts = 'alerts',
- auditd = 'auditd',
- auditd_file = 'auditd_file',
- library = 'library',
- netflow = 'netflow',
- plain = 'plain',
- registry = 'registry',
- suricata = 'suricata',
- system = 'system',
- system_dns = 'system_dns',
- system_endgame_process = 'system_endgame_process',
- system_file = 'system_file',
- system_fim = 'system_fim',
- system_security_event = 'system_security_event',
- system_socket = 'system_socket',
- zeek = 'zeek',
-}
-
export interface TimelineInput {
columns?: Maybe;
dataProviders?: Maybe;
diff --git a/x-pack/plugins/security_solution/common/types/index.ts b/x-pack/plugins/security_solution/common/types/index.ts
new file mode 100644
index 0000000000000..9464a33082a49
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/index.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export * from './timeline';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/actions/index.ts b/x-pack/plugins/security_solution/common/types/timeline/actions/index.ts
new file mode 100644
index 0000000000000..782af107417c2
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/actions/index.ts
@@ -0,0 +1,14 @@
+/*
+ * 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.
+ */
+export type {
+ ActionProps,
+ HeaderActionProps,
+ GenericActionRowCellRenderProps,
+ HeaderCellRender,
+ RowCellRender,
+ ControlColumnProps,
+} from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts
new file mode 100644
index 0000000000000..83b0ced332a62
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export type { CellValueElementProps } from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts b/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts
new file mode 100644
index 0000000000000..ee4d621e35d6c
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts
@@ -0,0 +1,13 @@
+/*
+ * 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.
+ */
+
+export type {
+ ColumnHeaderType,
+ ColumnId,
+ ColumnHeaderOptions,
+ ColumnRenderer,
+} from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/data_provider/index.ts b/x-pack/plugins/security_solution/common/types/timeline/data_provider/index.ts
new file mode 100644
index 0000000000000..f363176ac0a88
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/data_provider/index.ts
@@ -0,0 +1,15 @@
+/*
+ * 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.
+ */
+export { IS_OPERATOR, EXISTS_OPERATOR } from '../../../../../timelines/common';
+
+export type {
+ QueryOperator,
+ DataProviderType,
+ QueryMatch,
+ DataProvider,
+ DataProvidersAnd,
+} from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts
index 7ae52a3990ff7..05cf99195774b 100644
--- a/x-pack/plugins/security_solution/common/types/timeline/index.ts
+++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts
@@ -23,6 +23,13 @@ import { FlowTarget } from '../../search_strategy/security_solution/network';
import { errorSchema } from '../../detection_engine/schemas/response/error_schema';
import { Direction, Maybe } from '../../search_strategy';
+export * from './actions';
+export * from './cells';
+export * from './columns';
+export * from './data_provider';
+export * from './rows';
+export * from './store';
+
/*
* ColumnHeader Types
*/
@@ -492,6 +499,11 @@ export type TimelineExpandedDetail = {
[tab in TimelineTabs]?: TimelineExpandedDetailType;
};
+export type ToggleDetailPanel = TimelineExpandedDetailType & {
+ tabType?: TimelineTabs;
+ timelineId: string;
+};
+
export const pageInfoTimeline = runtimeTypes.type({
pageIndex: runtimeTypes.number,
pageSize: runtimeTypes.number,
diff --git a/x-pack/plugins/security_solution/common/types/timeline/rows/index.ts b/x-pack/plugins/security_solution/common/types/timeline/rows/index.ts
new file mode 100644
index 0000000000000..ae2d19a5e2ca8
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/rows/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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.
+ */
+export type { RowRenderer } from '../../../../../timelines/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/store.ts b/x-pack/plugins/security_solution/common/types/timeline/store.ts
new file mode 100644
index 0000000000000..01fc9db7c8e1d
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/types/timeline/store.ts
@@ -0,0 +1,97 @@
+/*
+ * 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 {
+ ColumnHeaderOptions,
+ ColumnId,
+ RowRendererId,
+ TimelineExpandedDetail,
+ TimelineTypeLiteral,
+} from '.';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { Filter } from '../../../../../../src/plugins/data/public';
+
+import { Direction } from '../../search_strategy';
+import { DataProvider } from './data_provider';
+
+export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql';
+
+export interface KueryFilterQuery {
+ kind: KueryFilterQueryKind;
+ expression: string;
+}
+
+export interface SerializedFilterQuery {
+ kuery: KueryFilterQuery | null;
+ serializedQuery: string;
+}
+
+export type SortDirection = 'none' | 'asc' | 'desc' | Direction;
+export interface SortColumnTimeline {
+ columnId: string;
+ columnType: string;
+ sortDirection: SortDirection;
+}
+
+export interface TimelinePersistInput {
+ id: string;
+ dataProviders?: DataProvider[];
+ dateRange?: {
+ start: string;
+ end: string;
+ };
+ excludedRowRendererIds?: RowRendererId[];
+ expandedDetail?: TimelineExpandedDetail;
+ filters?: Filter[];
+ columns: ColumnHeaderOptions[];
+ itemsPerPage?: number;
+ indexNames: string[];
+ kqlQuery?: {
+ filterQuery: SerializedFilterQuery | null;
+ };
+ show?: boolean;
+ sort?: SortColumnTimeline[];
+ showCheckboxes?: boolean;
+ timelineType?: TimelineTypeLiteral;
+ templateTimelineId?: string | null;
+ templateTimelineVersion?: number | null;
+}
+
+/** Invoked when a column is sorted */
+export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void;
+
+export type OnColumnsSorted = (
+ sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }>
+) => void;
+
+export type OnColumnRemoved = (columnId: ColumnId) => void;
+
+export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void;
+
+/** Invoked when a user clicks to load more item */
+export type OnChangePage = (nextPage: number) => void;
+
+/** Invoked when a user checks/un-checks a row */
+export type OnRowSelected = ({
+ eventIds,
+ isSelected,
+}: {
+ eventIds: string[];
+ isSelected: boolean;
+}) => void;
+
+/** Invoked when a user checks/un-checks the select all checkbox */
+export type OnSelectAll = ({ isSelected }: { isSelected: boolean }) => void;
+
+/** Invoked when columns are updated */
+export type OnUpdateColumns = (columns: ColumnHeaderOptions[]) => void;
+
+/** Invoked when a user pins an event */
+export type OnPinEvent = (eventId: string) => void;
+
+/** Invoked when a user unpins an event */
+export type OnUnPinEvent = (eventId: string) => void;
diff --git a/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts b/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts
index b724c0f672b50..64d4f2986903a 100644
--- a/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts
+++ b/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts
@@ -7,7 +7,7 @@
import { EventHit, EventSource } from '../search_strategy';
import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './field_formatters';
-import { eventDetailsFormattedFields, eventHit } from './mock_event_details';
+import { eventDetailsFormattedFields, eventHit } from '@kbn/securitysolution-t-grid';
describe('Events Details Helpers', () => {
const fields: EventHit['fields'] = eventHit.fields;
diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts
index 78ee3fdcdcdd5..3ff036fa0107f 100644
--- a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts
@@ -45,7 +45,7 @@ describe('Overview Page', () => {
describe('with no data', () => {
it('Splash screen should be here', () => {
- cy.stubSearchStrategyApi(emptyInstance, undefined, 'securitySolutionIndexFields');
+ cy.stubSearchStrategyApi(emptyInstance, undefined, 'indexFields');
loginAndWaitForPage(OVERVIEW_URL);
cy.get(OVERVIEW_EMPTY_PAGE).should('be.visible');
});
diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js
index 90eb9a38d7509..e74d06cd621fb 100644
--- a/x-pack/plugins/security_solution/cypress/support/commands.js
+++ b/x-pack/plugins/security_solution/cypress/support/commands.js
@@ -35,7 +35,7 @@ Cypress.Commands.add(
'stubSearchStrategyApi',
function (stubObject, factoryQueryType, searchStrategyName = 'securitySolutionSearchStrategy') {
cy.intercept('POST', '/internal/bsearch', (req) => {
- if (searchStrategyName === 'securitySolutionIndexFields') {
+ if (searchStrategyName === 'indexFields') {
req.reply(stubObject.rawResponse);
} else if (factoryQueryType === 'overviewHost') {
req.reply(stubObject.overviewHost);
diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json
index 02dbc56bd3397..e26f0d9b65bfa 100644
--- a/x-pack/plugins/security_solution/kibana.json
+++ b/x-pack/plugins/security_solution/kibana.json
@@ -17,6 +17,7 @@
"inspector",
"licensing",
"maps",
+ "timelines",
"triggersActionsUi",
"uiActions"
],
diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx
index cfb25c4436db3..2dc7f632c8482 100644
--- a/x-pack/plugins/security_solution/public/app/app.tsx
+++ b/x-pack/plugins/security_solution/public/app/app.tsx
@@ -21,7 +21,6 @@ import { GlobalToaster, ManageGlobalToaster } from '../common/components/toaster
import { KibanaContextProvider, useKibana, useUiSetting$ } from '../common/lib/kibana';
import { State } from '../common/store';
-import { ManageGlobalTimeline } from '../timelines/components/manage_timeline';
import { StartServices } from '../types';
import { PageRouter } from './routes';
import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common';
@@ -42,23 +41,21 @@ const StartAppComponent: FC = ({ children, history, onAppLeav
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/security_solution/public/common/components/accessibility/index.ts b/x-pack/plugins/security_solution/public/common/components/accessibility/index.ts
new file mode 100644
index 0000000000000..f05644c85e536
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/accessibility/index.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export * from './tooltip_with_keyboard_shortcut';
diff --git a/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx b/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx
index 97922ecdc5b61..2d66b4e93e4dc 100644
--- a/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx
@@ -10,7 +10,7 @@ import React from 'react';
import * as i18n from './translations';
-interface Props {
+export interface TooltipWithKeyboardShortcutProps {
additionalScreenReaderOnlyContext?: string;
content: React.ReactNode;
shortcut: string;
@@ -22,7 +22,7 @@ const TooltipWithKeyboardShortcutComponent = ({
content,
shortcut,
showShortcut,
-}: Props) => (
+}: TooltipWithKeyboardShortcutProps) => (
<>
{content}
{additionalScreenReaderOnlyContext !== '' && (
diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx
index 43d5c66655808..58cca7bcbd121 100644
--- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx
@@ -6,12 +6,12 @@
*/
import React, { useEffect, useMemo } from 'react';
-
+import { useDispatch } from 'react-redux';
+import { timelineActions } from '../../../timelines/store/timeline';
import { Filter } from '../../../../../../../src/plugins/data/public';
import { TimelineIdLiteral } from '../../../../common/types/timeline';
import { StatefulEventsViewer } from '../events_viewer';
import { alertsDefaultModel } from './default_headers';
-import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import * as i18n from './translations';
@@ -70,22 +70,24 @@ const AlertsTableComponent: React.FC = ({
startDate,
pageFilters = [],
}) => {
+ const dispatch = useDispatch();
const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]);
const { filterManager } = useKibana().services.data.query;
- const { initializeTimeline } = useManageTimeline();
useEffect(() => {
- initializeTimeline({
- id: timelineId,
- documentType: i18n.ALERTS_DOCUMENT_TYPE,
- filterManager,
- defaultModel: alertsDefaultModel,
- footerText: i18n.TOTAL_COUNT_OF_ALERTS,
- title: i18n.ALERTS_TABLE_TITLE,
- unit: i18n.UNIT,
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ dispatch(
+ timelineActions.initializeTGridSettings({
+ id: timelineId,
+ documentType: i18n.ALERTS_DOCUMENT_TYPE,
+ filterManager,
+ defaultColumns: alertsDefaultModel.columns,
+ excludedRowRendererIds: alertsDefaultModel.excludedRowRendererIds,
+ footerText: i18n.TOTAL_COUNT_OF_ALERTS,
+ title: i18n.ALERTS_TABLE_TITLE,
+ // TODO: avoid passing this through the store
+ })
+ );
+ }, [dispatch, filterManager, timelineId]);
return (
{
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx
index 4958f6bae4a30..175239fcaebe7 100644
--- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx
@@ -14,6 +14,8 @@ import { TestProviders } from '../../mock';
import { DraggableLegendItem, LegendItem } from './draggable_legend_item';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx
index dc0e24fcba8f5..bc3b9c3eaa1c6 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx
@@ -13,6 +13,8 @@ import { TestProviders } from '../../mock';
import { DragDropContextWrapper } from './drag_drop_context_wrapper';
+jest.mock('../../lib/kibana');
+
describe('DragDropContextWrapper', () => {
describe('rendering', () => {
test('it renders against the snapshot', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx
index 1073ed57dee19..1ab19c44e29b2 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx
@@ -11,6 +11,7 @@ import { DropResult, DragDropContext } from 'react-beautiful-dnd';
import { useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import deepEqual from 'fast-deep-equal';
+import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { BeforeCapture } from './drag_drop_context';
import { BrowserFields } from '../../containers/source';
@@ -23,22 +24,24 @@ import {
ADDED_TO_TIMELINE_MESSAGE,
ADDED_TO_TIMELINE_TEMPLATE_MESSAGE,
} from '../../hooks/translations';
-import { useAddToTimelineSensor } from '../../hooks/use_add_to_timeline';
import { displaySuccessToast, useStateToaster } from '../toasters';
import { TimelineId, TimelineType } from '../../../../common/types/timeline';
import {
- addFieldToTimelineColumns,
addProviderToTimeline,
fieldWasDroppedOnTimelineColumns,
- getTimelineIdFromColumnDroppableId,
- IS_DRAGGING_CLASS_NAME,
IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME,
providerWasDroppedOnTimeline,
draggableIsField,
userIsReArrangingProviders,
} from './helpers';
import { useDeepEqualSelector } from '../../hooks/use_selector';
+import { useKibana } from '../../lib/kibana';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
+import {
+ addFieldToTimelineColumns,
+ getTimelineIdFromColumnDroppableId,
+} from '../../../../../timelines/public';
+import { alertsHeaders } from '../alerts_viewer/default_headers';
// @ts-expect-error
window['__react-beautiful-dnd-disable-dev-warnings'] = true;
@@ -85,6 +88,7 @@ const onDragEndHandler = ({
} else if (fieldWasDroppedOnTimelineColumns(result)) {
addFieldToTimelineColumns({
browserFields,
+ defaultsHeader: alertsHeaders,
dispatch,
result,
timelineId: getTimelineIdFromColumnDroppableId(result.destination?.droppableId ?? ''),
@@ -92,8 +96,6 @@ const onDragEndHandler = ({
}
};
-const sensors = [useAddToTimelineSensor];
-
/**
* DragDropContextWrapperComponent handles all drag end events
*/
@@ -101,7 +103,8 @@ export const DragDropContextWrapperComponent: React.FC = ({ browserFields
const dispatch = useDispatch();
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const getDataProviders = useMemo(() => dragAndDropSelectors.getDataProvidersSelector(), []);
-
+ const { timelines } = useKibana().services;
+ const sensors = [timelines.getUseAddToTimelineSensor()];
const {
dataProviders: activeTimelineDataProviders,
timelineType,
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx
index 0d8011ee8b65d..bdc5545880e1c 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx
@@ -17,6 +17,8 @@ import { DragDropContextWrapper } from './drag_drop_context_wrapper';
import { ConditionalPortal, DraggableWrapper, getStyle } from './draggable_wrapper';
import { useMountAppended } from '../../utils/use_mount_appended';
+jest.mock('../../lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx
index 0cb030862a389..9db5b3899d8bc 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx
@@ -6,6 +6,7 @@
*/
import { EuiScreenReaderOnly } from '@elastic/eui';
+import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import {
Draggable,
@@ -24,12 +25,12 @@ import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/com
import { TruncatableText } from '../truncatable_text';
import { WithHoverActions } from '../with_hover_actions';
-import { useDraggableKeyboardWrapper } from './draggable_keyboard_wrapper_hook';
import { DraggableWrapperHoverContent, useGetTimelineId } from './draggable_wrapper_hover_content';
-import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, getDraggableId, getDroppableId } from './helpers';
+import { getDraggableId, getDroppableId } from './helpers';
import { ProviderContainer } from './provider_container';
import * as i18n from './translations';
+import { useKibana } from '../../lib/kibana';
// As right now, we do not know what we want there, we will keep it as a placeholder
export const DragEffects = styled.div``;
@@ -142,6 +143,7 @@ const DraggableWrapperComponent: React.FC = ({
const isDisabled = dataProvider.id.includes(`-${ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID}-`);
const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(false);
const dispatch = useDispatch();
+ const { timelines } = useKibana().services;
const handleClosePopOverTrigger = useCallback(() => {
setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger);
@@ -297,7 +299,7 @@ const DraggableWrapperComponent: React.FC = ({
setHoverActionsOwnFocus(true);
}, []);
- const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
+ const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({
closePopover: handleClosePopOverTrigger,
draggableId: getDraggableId(dataProvider.id),
fieldName: dataProvider.queryMatch.field,
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx
index 0d688bd805999..400b178c167f6 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx
@@ -17,14 +17,10 @@ import { TestProviders } from '../../mock';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
import { useSourcererScope } from '../../containers/sourcerer';
import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content';
-import {
- ManageGlobalTimeline,
- getTimelineDefaults,
-} from '../../../timelines/components/manage_timeline';
import { TimelineId } from '../../../../common/types/timeline';
+import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
jest.mock('../link_to');
-
jest.mock('../../lib/kibana');
jest.mock('../../containers/sourcerer', () => {
const original = jest.requireActual('../../containers/sourcerer');
@@ -42,29 +38,18 @@ jest.mock('uuid', () => {
};
});
const mockStartDragToTimeline = jest.fn();
-jest.mock('../../hooks/use_add_to_timeline', () => {
- const original = jest.requireActual('../../hooks/use_add_to_timeline');
+jest.mock('../../../../../timelines/public/hooks/use_add_to_timeline', () => {
+ const original = jest.requireActual('../../../../../timelines/public/hooks/use_add_to_timeline');
return {
...original,
useAddToTimeline: () => ({ startDragToTimeline: mockStartDragToTimeline }),
};
});
const mockAddFilters = jest.fn();
-const mockGetTimelineFilterManager = jest.fn().mockReturnValue({
- addFilters: mockAddFilters,
-});
-jest.mock('../../../timelines/components/manage_timeline', () => {
- const original = jest.requireActual('../../../timelines/components/manage_timeline');
-
- return {
- ...original,
- useManageTimeline: () => ({
- getManageTimelineById: jest.fn().mockReturnValue({ indexToAdd: [] }),
- getTimelineFilterManager: mockGetTimelineFilterManager,
- isManagedTimeline: jest.fn().mockReturnValue(false),
- }),
- };
-});
+jest.mock('../../../common/hooks/use_selector', () => ({
+ useShallowEqualSelector: jest.fn(),
+ useDeepEqualSelector: jest.fn(),
+}));
const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
const timelineId = TimelineId.active;
@@ -85,6 +70,9 @@ const defaultProps = {
describe('DraggableWrapperHoverContent', () => {
beforeAll(() => {
mockStartDragToTimeline.mockReset();
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({
+ filterManager: { addFilters: mockAddFilters },
+ });
(useSourcererScope as jest.Mock).mockReturnValue({
browserFields: mockBrowserFields,
selectedPatterns: [],
@@ -144,15 +132,10 @@ describe('DraggableWrapperHoverContent', () => {
beforeEach(() => {
onFilterAdded = jest.fn();
- const manageTimelineForTesting = {
- [timelineId]: getTimelineDefaults(timelineId),
- };
wrapper = mount(
-
-
-
+
);
});
@@ -237,18 +220,9 @@ describe('DraggableWrapperHoverContent', () => {
filterManager.addFilters = jest.fn();
onFilterAdded = jest.fn();
- const manageTimelineForTesting = {
- [timelineId]: {
- ...getTimelineDefaults(timelineId),
- filterManager,
- },
- };
-
wrapper = mount(
-
-
-
+
);
});
@@ -586,39 +560,4 @@ describe('DraggableWrapperHoverContent', () => {
expect(wrapper.find(`[data-test-subj="copy-to-clipboard"]`).first().exists()).toBe(false);
});
});
-
- describe('Filter Manager', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
- test('filter manager, not active timeline', () => {
- mount(
-
-
-
- );
-
- expect(mockGetTimelineFilterManager).not.toBeCalled();
- });
- test('filter manager, active timeline', () => {
- mount(
-
-
-
- );
-
- expect(mockGetTimelineFilterManager).toBeCalled();
- });
- test('filter manager, active timeline in draggableId', () => {
- mount(
-
-
-
- );
-
- expect(mockGetTimelineFilterManager).toBeCalled();
- });
- });
});
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx
index 880f0b4e18aca..71c3114015a03 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx
@@ -12,14 +12,12 @@ import {
EuiScreenReaderOnly,
EuiToolTip,
} from '@elastic/eui';
+
import React, { useCallback, useEffect, useRef, useMemo, useState } from 'react';
import { DraggableId } from 'react-beautiful-dnd';
import styled from 'styled-components';
-import { stopPropagationAndPreventDefault } from '../accessibility/helpers';
-import { TooltipWithKeyboardShortcut } from '../accessibility/tooltip_with_keyboard_shortcut';
import { getAllFieldsByName } from '../../containers/source';
-import { useAddToTimeline } from '../../hooks/use_add_to_timeline';
import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../lib/clipboard/clipboard';
import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard';
import { useKibana } from '../../lib/kibana';
@@ -28,11 +26,14 @@ import { StatefulTopN } from '../top_n';
import { allowTopN } from './helpers';
import * as i18n from './translations';
-import { useManageTimeline } from '../../../timelines/components/manage_timeline';
+import { useDeepEqualSelector } from '../../hooks/use_selector';
import { TimelineId } from '../../../../common/types/timeline';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useSourcererScope } from '../../containers/sourcerer';
+import { timelineSelectors } from '../../../timelines/store/timeline';
+import { stopPropagationAndPreventDefault } from '../../../../../timelines/public';
+import { TooltipWithKeyboardShortcut } from '../accessibility';
export const AdditionalContent = styled.div`
padding: 2px;
@@ -102,21 +103,25 @@ const DraggableWrapperHoverContentComponent: React.FC = ({
toggleTopN,
value,
}) => {
- const { startDragToTimeline } = useAddToTimeline({ draggableId, fieldName: field });
const kibana = useKibana();
+ const { timelines } = kibana.services;
+ const { startDragToTimeline } = timelines.getUseAddToTimeline()({
+ draggableId,
+ fieldName: field,
+ });
const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [
kibana.services.data.query.filterManager,
]);
- const { getTimelineFilterManager } = useManageTimeline();
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { filterManager: activeFilterMananager } = useDeepEqualSelector((state) =>
+ getManageTimeline(state, timelineId ?? '')
+ );
const defaultFocusedButtonRef = useRef(null);
const panelRef = useRef(null);
const filterManager = useMemo(
- () =>
- timelineId === TimelineId.active
- ? getTimelineFilterManager(TimelineId.active)
- : filterManagerBackup,
- [timelineId, getTimelineFilterManager, filterManagerBackup]
+ () => (timelineId === TimelineId.active ? activeFilterMananager : filterManagerBackup),
+ [timelineId, activeFilterMananager, filterManagerBackup]
);
// Regarding data from useManageTimeline:
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx
index 42f70e9d296b3..73a732b5d6458 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx
@@ -15,6 +15,8 @@ import { DragDropContextWrapper } from './drag_drop_context_wrapper';
import { DroppableWrapper } from './droppable_wrapper';
import { useMountAppended } from '../../utils/use_mount_appended';
+jest.mock('../../lib/kibana');
+
describe('DroppableWrapper', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts
index 58d2e0e7dc70f..a14a44cd9a68b 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts
@@ -7,6 +7,7 @@
import { omit } from 'lodash/fp';
import { DropResult } from 'react-beautiful-dnd';
+import { getTimelineIdFromColumnDroppableId } from '../../../../../timelines/public';
import { IdToDataProvider } from '../../store/drag_and_drop/model';
@@ -33,7 +34,6 @@ import {
getDroppableId,
getFieldIdFromDraggable,
getProviderIdFromDraggable,
- getTimelineIdFromColumnDroppableId,
getTimelineProviderDraggableId,
getTimelineProviderDroppableId,
providerWasDroppedOnTimeline,
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts
index e2e506e6e1a3f..9717e1e1eda91 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts
@@ -4,138 +4,53 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
-import { isString } from 'lodash/fp';
-import { DropResult, FluidDragActions, Position } from 'react-beautiful-dnd';
+import { DropResult } from 'react-beautiful-dnd';
import { Dispatch } from 'redux';
import { ActionCreator } from 'typescript-fsa';
+import { getProviderIdFromDraggable } from '@kbn/securitysolution-t-grid';
-import { stopPropagationAndPreventDefault } from '../accessibility/helpers';
-import { alertsHeaders } from '../alerts_viewer/default_headers';
-import { BrowserField, BrowserFields, getAllFieldsByName } from '../../containers/source';
+import { BrowserField } from '../../containers/source';
import { dragAndDropActions } from '../../store/actions';
import { IdToDataProvider } from '../../store/drag_and_drop/model';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
-import { timelineActions } from '../../../timelines/store/timeline';
-import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
import { addContentToTimeline } from '../../../timelines/components/timeline/data_providers/helpers';
import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider';
-import { TimelineId } from '../../../../common/types/timeline';
-
-export const draggableIdPrefix = 'draggableId';
-
-export const droppableIdPrefix = 'droppableId';
-
-export const draggableContentPrefix = `${draggableIdPrefix}.content.`;
-
-export const draggableTimelineProvidersPrefix = `${draggableIdPrefix}.timelineProviders.`;
-
-export const draggableFieldPrefix = `${draggableIdPrefix}.field.`;
-
-export const droppableContentPrefix = `${droppableIdPrefix}.content.`;
-
-export const droppableFieldPrefix = `${droppableIdPrefix}.field.`;
-
-export const droppableTimelineProvidersPrefix = `${droppableIdPrefix}.timelineProviders.`;
-
-export const droppableTimelineColumnsPrefix = `${droppableIdPrefix}.timelineColumns.`;
-
-export const droppableTimelineFlyoutBottomBarPrefix = `${droppableIdPrefix}.flyoutButton.`;
-
-export const getDraggableId = (dataProviderId: string): string =>
- `${draggableContentPrefix}${dataProviderId}`;
-
-export const getDraggableFieldId = ({
- contextId,
- fieldId,
-}: {
- contextId: string;
- fieldId: string;
-}): string => `${draggableFieldPrefix}${escapeContextId(contextId)}.${escapeFieldId(fieldId)}`;
-
-export const getTimelineProviderDroppableId = ({
- groupIndex,
- timelineId,
-}: {
- groupIndex: number;
- timelineId: string;
-}): string => `${droppableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}`;
-
-export const getTimelineProviderDraggableId = ({
- dataProviderId,
- groupIndex,
- timelineId,
-}: {
- dataProviderId: string;
- groupIndex: number;
- timelineId: string;
-}): string =>
- `${draggableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}.${dataProviderId}`;
-
-export const getDroppableId = (visualizationPlaceholderId: string): string =>
- `${droppableContentPrefix}${visualizationPlaceholderId}`;
-
-export const sourceIsContent = (result: DropResult): boolean =>
- result.source.droppableId.startsWith(droppableContentPrefix);
-
-export const sourceAndDestinationAreSameTimelineProviders = (result: DropResult): boolean => {
- const regex = /^droppableId\.timelineProviders\.(\S+)\./;
- const sourceMatches = result.source.droppableId.match(regex) ?? [];
- const destinationMatches = result.destination?.droppableId.match(regex) ?? [];
-
- return (
- sourceMatches.length >= 2 &&
- destinationMatches.length >= 2 &&
- sourceMatches[1] === destinationMatches[1]
- );
-};
-
-export const draggableIsContent = (result: DropResult | { draggableId: string }): boolean =>
- result.draggableId.startsWith(draggableContentPrefix);
-
-export const draggableIsField = (result: DropResult | { draggableId: string }): boolean =>
- result.draggableId.startsWith(draggableFieldPrefix);
-
-export const reasonIsDrop = (result: DropResult): boolean => result.reason === 'DROP';
-
-export const destinationIsTimelineProviders = (result: DropResult): boolean =>
- result.destination != null &&
- result.destination.droppableId.startsWith(droppableTimelineProvidersPrefix);
-
-export const destinationIsTimelineColumns = (result: DropResult): boolean =>
- result.destination != null &&
- result.destination.droppableId.startsWith(droppableTimelineColumnsPrefix);
-
-export const destinationIsTimelineButton = (result: DropResult): boolean =>
- result.destination != null &&
- result.destination.droppableId.startsWith(droppableTimelineFlyoutBottomBarPrefix);
-
-export const getProviderIdFromDraggable = (result: DropResult): string =>
- result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1);
-
-export const getFieldIdFromDraggable = (result: DropResult): string =>
- unEscapeFieldId(result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1));
-
-export const escapeDataProviderId = (path: string) => path.replace(/\./g, '_');
-
-export const escapeContextId = (path: string) => path.replace(/\./g, '_');
-
-export const escapeFieldId = (path: string) => path.replace(/\./g, '!!!DOT!!!');
-
-export const unEscapeFieldId = (path: string) => path.replace(/!!!DOT!!!/g, '.');
-
-export const providerWasDroppedOnTimeline = (result: DropResult): boolean =>
- reasonIsDrop(result) &&
- draggableIsContent(result) &&
- sourceIsContent(result) &&
- destinationIsTimelineProviders(result);
-
-export const userIsReArrangingProviders = (result: DropResult): boolean =>
- reasonIsDrop(result) && sourceAndDestinationAreSameTimelineProviders(result);
-
-export const fieldWasDroppedOnTimelineColumns = (result: DropResult): boolean =>
- reasonIsDrop(result) && draggableIsField(result) && destinationIsTimelineColumns(result);
+export {
+ draggableIdPrefix,
+ droppableIdPrefix,
+ draggableContentPrefix,
+ draggableTimelineProvidersPrefix,
+ draggableFieldPrefix,
+ draggableIsField,
+ droppableContentPrefix,
+ droppableFieldPrefix,
+ droppableTimelineProvidersPrefix,
+ droppableTimelineColumnsPrefix,
+ droppableTimelineFlyoutBottomBarPrefix,
+ getDraggableId,
+ getDraggableFieldId,
+ getTimelineProviderDroppableId,
+ getTimelineProviderDraggableId,
+ getDroppableId,
+ sourceIsContent,
+ sourceAndDestinationAreSameTimelineProviders,
+ draggableIsContent,
+ reasonIsDrop,
+ destinationIsTimelineProviders,
+ destinationIsTimelineColumns,
+ destinationIsTimelineButton,
+ getProviderIdFromDraggable,
+ getFieldIdFromDraggable,
+ escapeDataProviderId,
+ escapeContextId,
+ escapeFieldId,
+ unEscapeFieldId,
+ providerWasDroppedOnTimeline,
+ userIsReArrangingProviders,
+ fieldWasDroppedOnTimelineColumns,
+ DRAG_TYPE_FIELD,
+ IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME,
+} from '@kbn/securitysolution-t-grid';
interface AddProviderToTimelineParams {
activeTimelineDataProviders: DataProvider[];
dataProviders: IdToDataProvider;
@@ -148,18 +63,6 @@ interface AddProviderToTimelineParams {
timelineId: string;
}
-interface AddFieldToTimelineColumnsParams {
- upsertColumn?: ActionCreator<{
- column: ColumnHeaderOptions;
- id: string;
- index: number;
- }>;
- browserFields: BrowserFields;
- dispatch: Dispatch;
- result: DropResult;
- timelineId: string;
-}
-
export const addProviderToTimeline = ({
activeTimelineDataProviders,
dataProviders,
@@ -186,73 +89,6 @@ export const addProviderToTimeline = ({
}
};
-const linkFields: Record = {
- 'signal.rule.name': 'signal.rule.id',
- 'event.module': 'rule.reference',
-};
-
-export const addFieldToTimelineColumns = ({
- upsertColumn = timelineActions.upsertColumn,
- browserFields,
- dispatch,
- result,
- timelineId,
-}: AddFieldToTimelineColumnsParams): void => {
- const fieldId = getFieldIdFromDraggable(result);
- const allColumns = getAllFieldsByName(browserFields);
- const column = allColumns[fieldId];
- const initColumnHeader =
- timelineId === TimelineId.detectionsPage || timelineId === TimelineId.detectionsRulesDetailsPage
- ? alertsHeaders.find((c) => c.id === fieldId) ?? {}
- : {};
-
- if (column != null) {
- dispatch(
- upsertColumn({
- column: {
- category: column.category,
- columnHeaderType: 'not-filtered',
- description: isString(column.description) ? column.description : undefined,
- example: isString(column.example) ? column.example : undefined,
- id: fieldId,
- linkField: linkFields[fieldId] ?? undefined,
- type: column.type,
- aggregatable: column.aggregatable,
- initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
- ...initColumnHeader,
- },
- id: timelineId,
- index: result.destination != null ? result.destination.index : 0,
- })
- );
- } else {
- // create a column definition, because it doesn't exist in the browserFields:
- dispatch(
- upsertColumn({
- column: {
- columnHeaderType: 'not-filtered',
- id: fieldId,
- initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
- },
- id: timelineId,
- index: result.destination != null ? result.destination.index : 0,
- })
- );
- }
-};
-
-/**
- * Prevents fields from being dragged or dropped to any area other than column
- * header drop zone in the timeline
- */
-export const DRAG_TYPE_FIELD = 'drag-type-field';
-
-/** This class is added to the document body while dragging */
-export const IS_DRAGGING_CLASS_NAME = 'is-dragging';
-
-/** This class is added to the document body while timeline field dragging */
-export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging';
-
export const allowTopN = ({
browserField,
fieldName,
@@ -347,125 +183,3 @@ export const allowTopN = ({
return isAllowlistedNonBrowserField || (isAggregatable && isAllowedType);
};
-
-export const getTimelineIdFromColumnDroppableId = (droppableId: string) =>
- droppableId.slice(droppableId.lastIndexOf('.') + 1);
-
-/** The draggable will move this many pixes via the keyboard when the arrow key is pressed */
-export const KEYBOARD_DRAG_OFFSET = 20;
-
-export const DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME = 'draggable-keyboard-wrapper';
-
-/**
- * Temporarily disables tab focus on child links of the draggable to work
- * around an issue where tab focus becomes stuck on the interactive children
- *
- * NOTE: This function is (intentionally) only effective when used in a key
- * event handler, because it automatically restores focus capabilities on
- * the next tick.
- */
-export const temporarilyDisableInteractiveChildTabIndexes = (draggableElement: HTMLDivElement) => {
- const interactiveChildren = draggableElement.querySelectorAll('a, button');
- interactiveChildren.forEach((interactiveChild) => {
- interactiveChild.setAttribute('tabindex', '-1'); // DOM mutation
- });
-
- // restore the default tabindexs on the next tick:
- setTimeout(() => {
- interactiveChildren.forEach((interactiveChild) => {
- interactiveChild.setAttribute('tabindex', '0'); // DOM mutation
- });
- }, 0);
-};
-
-export const draggableKeyDownHandler = ({
- beginDrag,
- cancelDragActions,
- closePopover,
- draggableElement,
- dragActions,
- dragToLocation,
- endDrag,
- keyboardEvent,
- openPopover,
- setDragActions,
-}: {
- beginDrag: () => FluidDragActions | null;
- cancelDragActions: () => void;
- closePopover?: () => void;
- draggableElement: HTMLDivElement;
- dragActions: FluidDragActions | null;
- dragToLocation: ({
- // eslint-disable-next-line @typescript-eslint/no-shadow
- dragActions,
- position,
- }: {
- dragActions: FluidDragActions | null;
- position: Position;
- }) => void;
- keyboardEvent: React.KeyboardEvent;
- endDrag: (dragActions: FluidDragActions | null) => void;
- openPopover?: () => void;
- setDragActions: (value: React.SetStateAction) => void;
-}) => {
- let currentPosition: DOMRect | null = null;
-
- switch (keyboardEvent.key) {
- case ' ':
- if (!dragActions) {
- // start dragging, because space was pressed
- if (closePopover != null) {
- closePopover();
- }
- setDragActions(beginDrag());
- } else {
- // end dragging, because space was pressed
- endDrag(dragActions);
- setDragActions(null);
- }
- break;
- case 'Escape':
- cancelDragActions();
- break;
- case 'Tab':
- // IMPORTANT: we do NOT want to stop propagation and prevent default when Tab is pressed
- temporarilyDisableInteractiveChildTabIndexes(draggableElement);
- break;
- case 'ArrowUp':
- currentPosition = draggableElement.getBoundingClientRect();
- dragToLocation({
- dragActions,
- position: { x: currentPosition.x, y: currentPosition.y - KEYBOARD_DRAG_OFFSET },
- });
- break;
- case 'ArrowDown':
- currentPosition = draggableElement.getBoundingClientRect();
- dragToLocation({
- dragActions,
- position: { x: currentPosition.x, y: currentPosition.y + KEYBOARD_DRAG_OFFSET },
- });
- break;
- case 'ArrowLeft':
- currentPosition = draggableElement.getBoundingClientRect();
- dragToLocation({
- dragActions,
- position: { x: currentPosition.x - KEYBOARD_DRAG_OFFSET, y: currentPosition.y },
- });
- break;
- case 'ArrowRight':
- currentPosition = draggableElement.getBoundingClientRect();
- dragToLocation({
- dragActions,
- position: { x: currentPosition.x + KEYBOARD_DRAG_OFFSET, y: currentPosition.y },
- });
- break;
- case 'Enter':
- stopPropagationAndPreventDefault(keyboardEvent); // prevents the first item in the popover from getting an errant ENTER
- if (!dragActions && openPopover != null) {
- openPopover();
- }
- break;
- default:
- break;
- }
-};
diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx
index 9c6b8c485986e..f77bf0f347f79 100644
--- a/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx
@@ -21,6 +21,8 @@ import {
tooltipContentIsExplicitlyNull,
} from '.';
+jest.mock('../../lib/kibana');
+
describe('draggables', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx
index b8f29996d603b..c782804b0592b 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx
@@ -17,6 +17,8 @@ import { TestProviders } from '../../mock';
import { mockBrowserFields } from '../../containers/source/mock';
import { useMountAppended } from '../../utils/use_mount_appended';
+jest.mock('../../lib/kibana');
+
jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => {
return {
useRuleAsync: jest.fn(),
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx
index 204b8c088304b..1be05cc560552 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx
@@ -21,9 +21,8 @@ import { get, isEmpty } from 'lodash';
import memoizeOne from 'memoize-one';
import React from 'react';
import styled from 'styled-components';
-import { onFocusReFocusDraggable } from '../accessibility/helpers';
+import { onFocusReFocusDraggable } from '../../../../../timelines/public';
import { BrowserFields } from '../../containers/source';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { DragEffects } from '../drag_and_drop/draggable_wrapper';
import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper';
import { DRAG_TYPE_FIELD, getDroppableId } from '../drag_and_drop/helpers';
@@ -38,6 +37,7 @@ import { OnUpdateColumns } from '../../../timelines/components/timeline/events';
import { getIconFromType, getExampleText } from './helpers';
import * as i18n from './translations';
import { EventFieldsData } from './types';
+import { ColumnHeaderOptions } from '../../../../common';
const HoverActionsContainer = styled(EuiPanel)`
align-items: center;
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx
index 0c7515fe75d86..6aff259d8220e 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx
@@ -20,6 +20,8 @@ import { mockAlertDetailsData } from './__mocks__';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
import { TimelineTabs } from '../../../../common/types/timeline';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('../link_to');
describe('EventDetails', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx
index f0865e1b8e083..555b67da953d6 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx
@@ -16,6 +16,8 @@ import { mockBrowserFields } from '../../containers/source/mock';
import { useMountAppended } from '../../utils/use_mount_appended';
import { TimelineTabs } from '../../../../common/types/timeline';
+jest.mock('../../lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx
index 93d0e6ccfbe3c..3ad7e9aef19dc 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx
@@ -11,26 +11,24 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { rgba } from 'polished';
import styled from 'styled-components';
-
import {
arrayIndexToAriaIndex,
DATA_COLINDEX_ATTRIBUTE,
DATA_ROWINDEX_ATTRIBUTE,
isTab,
onKeyDownFocusHandler,
-} from '../accessibility/helpers';
+} from '../../../../../timelines/public';
+
import { ADD_TIMELINE_BUTTON_CLASS_NAME } from '../../../timelines/components/flyout/add_timeline_button';
import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { BrowserFields, getAllFieldsByName } from '../../containers/source';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
import { getColumnHeaders } from '../../../timelines/components/timeline/body/column_headers/helpers';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
-
import { getColumns } from './columns';
import { EVENT_FIELDS_TABLE_CLASS_NAME, onEventDetailsTabKeyPressed, search } from './helpers';
import { useDeepEqualSelector } from '../../hooks/use_selector';
-import { TimelineTabs } from '../../../../common/types/timeline';
+import { ColumnHeaderOptions, TimelineTabs } from '../../../../common/types/timeline';
interface Props {
browserFields: BrowserFields;
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx
index 1f12c2de5e24f..8392be420a2c5 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx
@@ -15,15 +15,15 @@ import {
getTableSkipFocus,
handleSkipFocus,
stopPropagationAndPreventDefault,
-} from '../accessibility/helpers';
+} from '../../../../../timelines/public';
import { BrowserField, BrowserFields } from '../../containers/source';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import {
DEFAULT_DATE_COLUMN_MIN_WIDTH,
DEFAULT_COLUMN_MIN_WIDTH,
} from '../../../timelines/components/timeline/body/constants';
import * as i18n from './translations';
+import { ColumnHeaderOptions } from '../../../../common';
/**
* Defines the behavior of the search input that appears above the table of data
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx
index 7c84a325cb667..5051b39fe6093 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../common';
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
index 36986f5f8d353..90a4e67d76b99 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
@@ -21,9 +21,8 @@ import { mockBrowserFields, mockDocValueFields } from '../../containers/source/m
import { eventsDefaultModel } from './default_model';
import { useMountAppended } from '../../utils/use_mount_appended';
import { inputsModel } from '../../store/inputs';
-import { TimelineId } from '../../../../common/types/timeline';
+import { TimelineId, SortDirection } from '../../../../common/types/timeline';
import { KqlMode } from '../../../timelines/store/timeline/model';
-import { SortDirection } from '../../../timelines/components/timeline/body/sort';
import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
@@ -31,6 +30,8 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell
import { useTimelineEvents } from '../../../timelines/containers';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
+jest.mock('../../lib/kibana');
+
jest.mock('../../hooks/use_experimental_features');
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
@@ -144,18 +145,18 @@ describe('EventsViewer', () => {
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponseWithEvents]);
});
- test('call the right reduce action to show event details', async () => {
+ test('call the right reduce action to show event details', () => {
const wrapper = mount(
);
- await act(async () => {
+ act(() => {
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
});
- await waitFor(() => {
+ waitFor(() => {
expect(mockDispatch).toBeCalledTimes(2);
expect(mockDispatch.mock.calls[1][0]).toEqual({
payload: {
@@ -197,7 +198,7 @@ describe('EventsViewer', () => {
);
expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true);
});
- // TO DO sourcerer @X
+
test('it renders the footer containing the pagination', () => {
const wrapper = mount(
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
index c99275ec49ab3..8326cdaaaf995 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
@@ -10,11 +10,12 @@ import React, { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
+import { useDispatch } from 'react-redux';
import { Direction } from '../../../../common/search_strategy';
import { BrowserFields, DocValueFields } from '../../containers/source';
import { useTimelineEvents } from '../../../timelines/containers';
import { useKibana } from '../../lib/kibana';
-import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model';
+import { KqlMode } from '../../../timelines/store/timeline/model';
import { HeaderSection } from '../header_section';
import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import { Sort } from '../../../timelines/components/timeline/body/sort';
@@ -36,18 +37,21 @@ import {
Query,
} from '../../../../../../../src/plugins/data/public';
import { inputsModel } from '../../store';
-import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { ExitFullScreen } from '../exit_full_screen';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
-import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
-import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer';
+import {
+ ColumnHeaderOptions,
+ ControlColumnProps,
+ RowRenderer,
+ TimelineId,
+ TimelineTabs,
+} from '../../../../common/types/timeline';
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles';
-import {
- defaultControlColumn,
- ControlColumnProps,
-} from '../../../timelines/components/timeline/body/control_columns';
+import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
+import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
+import { useDeepEqualSelector } from '../../hooks/use_selector';
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
const UTILITY_BAR_HEIGHT = 19; // px
@@ -162,21 +166,19 @@ const EventsViewerComponent: React.FC = ({
utilityBar,
graphEventId,
}) => {
+ const dispatch = useDispatch();
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
const kibana = useKibana();
const [isQueryLoading, setIsQueryLoading] = useState(false);
- const { getManageTimelineById, setIsTimelineLoading } = useManageTimeline();
-
useEffect(() => {
- setIsTimelineLoading({ id, isLoading: isQueryLoading });
- }, [id, isQueryLoading, setIsTimelineLoading]);
+ dispatch(timelineActions.updateIsLoading({ id, isLoading: isQueryLoading }));
+ }, [dispatch, id, isQueryLoading]);
- const { queryFields, title, unit } = useMemo(() => getManageTimelineById(id), [
- getManageTimelineById,
- id,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const unit = useMemo(() => (n: number) => i18n.UNIT(n), []);
+ const { queryFields, title } = useDeepEqualSelector((state) => getManageTimeline(state, id));
const justTitle = useMemo(() => {title} , [title]);
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx
index cd27177643b44..571e04a106cf0 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx
@@ -22,6 +22,8 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell
import { useTimelineEvents } from '../../../timelines/containers';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('../../../timelines/containers', () => ({
useTimelineEvents: jest.fn(),
}));
@@ -60,7 +62,9 @@ describe('StatefulEventsViewer', () => {
await waitFor(() => {
wrapper.update();
- expect(wrapper.find('[data-test-subj="events-viewer-panel"]').first().exists()).toBe(true);
+ expect(wrapper.text()).toMatchInlineSnapshot(
+ `"Showing: 12 events1 fields sorted@timestamp1event.severityevent.categoryevent.actionhost.namesource.ipdestination.ipdestination.bytesuser.name_idmessage0 of 12 events123"`
+ );
});
});
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
index b58aa2236d292..c0a75bdd3edd2 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
@@ -12,18 +12,20 @@ import styled from 'styled-components';
import { inputsModel, inputsSelectors, State } from '../../store';
import { inputsActions } from '../../store/actions';
-import { TimelineId } from '../../../../common/types/timeline';
+import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline';
import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
import { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model';
import { Filter } from '../../../../../../../src/plugins/data/public';
-import { EventsViewer } from './events_viewer';
import { InspectButtonContainer } from '../inspect';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
+import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useSourcererScope } from '../../containers/sourcerer';
import { DetailsPanel } from '../../../timelines/components/side_panel';
-import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
+import { useKibana } from '../../lib/kibana';
+import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
+import { EventsViewer } from './events_viewer';
const DEFAULT_EVENTS_VIEWER_HEIGHT = 652;
@@ -83,6 +85,7 @@ const StatefulEventsViewerComponent: React.FC = ({
// If truthy, the graph viewer (Resolver) is showing
graphEventId,
}) => {
+ const { timelines: timelinesUi } = useKibana().services;
const {
browserFields,
docValueFields,
@@ -90,8 +93,9 @@ const StatefulEventsViewerComponent: React.FC = ({
selectedPatterns,
loading: isLoadingIndexPattern,
} = useSourcererScope(scopeId);
- const { globalFullScreen } = useGlobalFullScreen();
-
+ const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
+ // TODO: Once we are past experimental phase this code should be removed
+ const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
useEffect(() => {
if (createTimeline != null) {
createTimeline({
@@ -111,37 +115,73 @@ const StatefulEventsViewerComponent: React.FC = ({
}, []);
const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]);
+ const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn];
+ const trailingControlColumns: ControlColumnProps[] = [];
return (
<>
-
+ {tGridEnabled ? (
+ timelinesUi.getTGrid<'embedded'>({
+ type: 'embedded',
+ browserFields,
+ columns,
+ dataProviders: dataProviders!,
+ deletedEventIds,
+ docValueFields,
+ end,
+ filters: globalFilters,
+ globalFullScreen,
+ headerFilterGroup,
+ id,
+ indexNames: selectedPatterns,
+ indexPattern,
+ isLive,
+ isLoadingIndexPattern,
+ itemsPerPage,
+ itemsPerPageOptions: itemsPerPageOptions!,
+ kqlMode,
+ query,
+ onRuleChange,
+ renderCellValue,
+ rowRenderers,
+ setGlobalFullScreen,
+ start,
+ sort,
+ utilityBar,
+ graphEventId,
+ leadingControlColumns,
+ trailingControlColumns,
+ })
+ ) : (
+
+ )}
i18n.translate('xpack.securitySolution.eventsViewer.unit', {
values: { totalCount },
diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx
index 7ad9de29431c9..d21adbd00cc20 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx
@@ -13,6 +13,8 @@ import { TestProviders } from '../../mock';
import { Title } from './title';
import { useMountAppended } from '../../utils/use_mount_appended';
+jest.mock('../../lib/kibana');
+
describe('Title', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx
index ddbcf710aff30..a0e2ff266ad28 100644
--- a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx
@@ -131,7 +131,7 @@ const InspectButtonComponent: React.FC = ({
color="text"
iconSide="left"
iconType="inspect"
- isDisabled={loading || isDisabled}
+ isDisabled={loading || isDisabled || false}
isLoading={loading}
onClick={handleClick}
>
@@ -145,7 +145,7 @@ const InspectButtonComponent: React.FC = ({
data-test-subj="inspect-icon-button"
iconSize="m"
iconType="inspect"
- isDisabled={loading || isDisabled}
+ isDisabled={loading || isDisabled || false}
title={i18n.INSPECT}
onClick={handleClick}
/>
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx
index 115fb65dc7011..f08edb114b9a9 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx
@@ -13,6 +13,8 @@ import { EntityDraggableComponent } from './entity_draggable';
import { TestProviders } from '../../mock/test_providers';
import { useMountAppended } from '../../utils/use_mount_appended';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx
index 6ad2bd30283d2..0d9b4001c17aa 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx
@@ -17,6 +17,8 @@ import { useMountAppended } from '../../../utils/use_mount_appended';
import { Anomalies } from '../types';
import { waitFor } from '@testing-library/dom';
+jest.mock('../../../lib/kibana');
+
const startDate: string = '2020-07-07T08:20:18.966Z';
const endDate: string = '3000-01-01T00:00:00.000Z';
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx
index 6b569a67cfebf..5eb0751404872 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx
@@ -18,6 +18,8 @@ import { Anomalies } from '../types';
import { useMountAppended } from '../../../utils/use_mount_appended';
import { waitFor } from '@testing-library/dom';
+jest.mock('../../../lib/kibana');
+
const startDate: string = '2020-07-07T08:20:18.966Z';
const endDate: string = '3000-01-01T00:00:00.000Z';
const narrowDateRange = jest.fn();
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx
index ae6ef4e680ffa..2ecda8482e340 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx
@@ -16,6 +16,8 @@ import { Columns } from '../../paginated_table';
import { TestProviders } from '../../../mock';
import { useMountAppended } from '../../../utils/use_mount_appended';
+jest.mock('../../../lib/kibana');
+
const startDate = new Date(2001).toISOString();
const endDate = new Date(3000).toISOString();
const interval = 'days';
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx
index b8a8ab88a74fd..48c2ec3ee38d8 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx
@@ -15,6 +15,8 @@ import React from 'react';
import { TestProviders } from '../../../mock';
import { useMountAppended } from '../../../utils/use_mount_appended';
+jest.mock('../../../../common/lib/kibana');
+
const startDate = new Date(2001).toISOString();
const endDate = new Date(3000).toISOString();
describe('get_anomalies_network_table_columns', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx
index 8c2b97a4b8b38..c122138f9547a 100644
--- a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx
@@ -18,6 +18,9 @@ import {
import { TestProviders } from '../../mock';
import { getEmptyValue } from '../empty_value';
import { useMountAppended } from '../../utils/use_mount_appended';
+
+jest.mock('../../lib/kibana');
+
describe('Table Helpers', () => {
const items = ['item1', 'item2', 'item3'];
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts
index 70e095c88576f..04ceafde7ef74 100644
--- a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts
+++ b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts
@@ -8,10 +8,10 @@
import type React from 'react';
import uuid from 'uuid';
import { isError } from 'lodash/fp';
+import { isAppError } from '@kbn/securitysolution-t-grid';
import { AppToast, ActionToaster } from './';
import { isToasterError } from './errors';
-import { isAppError } from '../../utils/api';
/**
* Displays an error toast for the provided title and message
diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx
index 005602738f376..4f6834e84d83a 100644
--- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx
@@ -18,17 +18,11 @@ import {
createSecuritySolutionStorageMock,
mockIndexPattern,
} from '../../mock';
-import { FilterManager } from '../../../../../../../src/plugins/data/public';
import { createStore, State } from '../../store';
import { Props } from './top_n';
import { StatefulTopN } from '.';
-import {
- ManageGlobalTimeline,
- getTimelineDefaults,
-} from '../../../timelines/components/manage_timeline';
import { TimelineId } from '../../../../common/types/timeline';
-import { coreMock } from '../../../../../../../src/core/public/mocks';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
@@ -45,8 +39,6 @@ jest.mock('../link_to');
jest.mock('../../lib/kibana');
jest.mock('../../../timelines/store/timeline/actions');
-const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
-
const field = 'process.name';
const value = 'nice';
@@ -175,9 +167,7 @@ describe('StatefulTopN', () => {
beforeEach(() => {
wrapper = mount(
-
-
-
+
);
});
@@ -244,26 +234,16 @@ describe('StatefulTopN', () => {
});
describe('rendering in a timeline context', () => {
- let filterManager: FilterManager;
let wrapper: ReactWrapper;
beforeEach(() => {
- filterManager = new FilterManager(mockUiSettingsForFilterManager);
- const manageTimelineForTesting = {
- [TimelineId.active]: {
- ...getTimelineDefaults(TimelineId.active),
- filterManager,
- },
- };
testProps = {
...testProps,
timelineId: TimelineId.active,
};
wrapper = mount(
-
-
-
+
);
});
@@ -320,25 +300,13 @@ describe('StatefulTopN', () => {
});
describe('rendering in a NON-active timeline context', () => {
test(`defaults to the 'Alert events' option when rendering in a NON-active timeline context (e.g. the Alerts table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'alerts'`, async () => {
- const filterManager = new FilterManager(mockUiSettingsForFilterManager);
-
- const manageTimelineForTesting = {
- [TimelineId.active]: {
- ...getTimelineDefaults(TimelineId.active),
- filterManager,
- documentType: 'alerts',
- },
- };
-
testProps = {
...testProps,
timelineId: TimelineId.detectionsPage,
};
const wrapper = mount(
-
-
-
+
);
await waitFor(() => {
diff --git a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx
index a8868436d9689..c867862e690bd 100644
--- a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx
@@ -6,13 +6,13 @@
*/
import { EuiPopover } from '@elastic/eui';
+import {
+ HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME,
+ IS_DRAGGING_CLASS_NAME,
+} from '@kbn/securitysolution-t-grid';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
-import { IS_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers';
-
-export const HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME = 'hover-actions-always-show';
-
/**
* To avoid expensive changes to the DOM, delay showing the popover menu
*/
diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts
index 3e690e50b04b1..4f558412576b4 100644
--- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts
+++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts
@@ -83,7 +83,7 @@ export const useTimelineLastEventTime = ({
TimelineEventsLastEventTimeRequestOptions,
TimelineEventsLastEventTimeStrategyResponse
>(request, {
- strategy: 'securitySolutionTimelineSearchStrategy',
+ strategy: 'timelineSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx
index 1c17f95bb6ba0..3bc92dafd351f 100644
--- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx
@@ -151,7 +151,7 @@ export const useFetchIndex = (
{ indices: iNames, onlyCheckIfIndicesExist },
{
abortSignal: abortCtrl.current.signal,
- strategy: 'securitySolutionIndexFields',
+ strategy: 'indexFields',
}
)
.subscribe({
@@ -235,7 +235,7 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => {
{ indices: indicesName, onlyCheckIfIndicesExist: false },
{
abortSignal: abortCtrl.current.signal,
- strategy: 'securitySolutionIndexFields',
+ strategy: 'indexFields',
}
)
.subscribe({
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts
index da6b41080c1c7..6c5caa25a1f96 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts
@@ -7,9 +7,10 @@
import { renderHook } from '@testing-library/react-hooks';
import { IEsError } from 'src/plugins/data/public';
+import { KibanaError, SecurityAppError } from '@kbn/securitysolution-t-grid';
import { useToasts } from '../lib/kibana';
-import { KibanaError, SecurityAppError } from '../utils/api';
+
import {
appErrorToErrorStack,
convertErrorToEnumerable,
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
index 61b20e137f870..0c2721e6ad416 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
@@ -7,11 +7,17 @@
import { useCallback, useRef } from 'react';
import { isString } from 'lodash/fp';
+import {
+ AppError,
+ isAppError,
+ isKibanaError,
+ isSecurityAppError,
+} from '@kbn/securitysolution-t-grid';
+
import { IEsError, isEsError } from '../../../../../../src/plugins/data/public';
import { ErrorToastOptions, ToastsStart, Toast } from '../../../../../../src/core/public';
import { useToasts } from '../lib/kibana';
-import { AppError, isAppError, isKibanaError, isSecurityAppError } from '../utils/api';
export type UseAppToasts = Pick & {
api: ToastsStart;
diff --git a/x-pack/plugins/security_solution/public/common/lib/clipboard/with_copy_to_clipboard.tsx b/x-pack/plugins/security_solution/public/common/lib/clipboard/with_copy_to_clipboard.tsx
index 1baa57166de3f..2f5afc8a44489 100644
--- a/x-pack/plugins/security_solution/public/common/lib/clipboard/with_copy_to_clipboard.tsx
+++ b/x-pack/plugins/security_solution/public/common/lib/clipboard/with_copy_to_clipboard.tsx
@@ -6,9 +6,10 @@
*/
import { EuiToolTip } from '@elastic/eui';
+
import React from 'react';
-import { TooltipWithKeyboardShortcut } from '../../components/accessibility/tooltip_with_keyboard_shortcut';
+import { TooltipWithKeyboardShortcut } from '../../components/accessibility';
import * as i18n from '../../components/drag_and_drop/translations';
import { Clipboard } from './clipboard';
diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts
index eb0ae1ae1dee9..09c3d2537e272 100644
--- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts
@@ -6,6 +6,10 @@
*/
import { notificationServiceMock } from '../../../../../../../../src/core/public/mocks';
+
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { createTGridMocks } from '../../../../../../timelines/public/mock';
+
import {
createKibanaContextProviderMock,
createUseUiSettingMock,
@@ -30,14 +34,24 @@ export const useKibana = jest.fn().mockReturnValue({
})),
})),
},
+ query: {
+ ...mockStartServicesMock.data.query,
+ filterManager: {
+ addFilters: jest.fn(),
+ getFilters: jest.fn(),
+ getUpdates$: jest.fn().mockReturnValue({ subscribe: jest.fn() }),
+ setAppFilters: jest.fn(),
+ },
+ },
},
+ timelines: createTGridMocks(),
},
});
export const useUiSetting = jest.fn(createUseUiSettingMock());
export const useUiSetting$ = jest.fn(createUseUiSetting$Mock());
export const useHttp = jest.fn().mockReturnValue(createStartServicesMock().http);
export const useTimeZone = jest.fn();
-export const useDateFormat = jest.fn();
+export const useDateFormat = jest.fn().mockReturnValue('MMM D, YYYY @ HH:mm:ss.SSS');
export const useBasePath = jest.fn(() => '/test/base/path');
export const useToasts = jest
.fn()
diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts
index 557c04e4e8a47..316f8b6214d1e 100644
--- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts
@@ -43,6 +43,7 @@ export const mockGlobalState: State = {
trustedAppsByPolicyEnabled: false,
metricsEntitiesEnabled: false,
ruleRegistryEnabled: false,
+ tGridEnabled: false,
},
},
hosts: {
diff --git a/x-pack/plugins/security_solution/public/common/mock/header.ts b/x-pack/plugins/security_solution/public/common/mock/header.ts
index ae7d3c9e576a8..029ddb00d1832 100644
--- a/x-pack/plugins/security_solution/public/common/mock/header.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/header.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { ColumnHeaderOptions } from '../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../common';
import { defaultColumnHeaderType } from '../../timelines/components/timeline/body/column_headers/default_headers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_control_columns.tsx b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_control_columns.tsx
index 7604732f90203..7dae3e671d271 100644
--- a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_control_columns.tsx
+++ b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_control_columns.tsx
@@ -15,7 +15,7 @@ import {
EuiPopoverTitle,
EuiSpacer,
} from '@elastic/eui';
-import { ControlColumnProps } from '../../timelines/components/timeline/body/control_columns';
+import { ControlColumnProps } from '../../../common/types/timeline';
const SelectionHeaderCell = () => {
return (
diff --git a/x-pack/plugins/security_solution/public/common/mock/utils.ts b/x-pack/plugins/security_solution/public/common/mock/utils.ts
index 30951b81611db..e0f8e651a5821 100644
--- a/x-pack/plugins/security_solution/public/common/mock/utils.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/utils.ts
@@ -5,12 +5,20 @@
* 2.0.
*/
+import { AnyAction, Reducer } from 'redux';
+import reduceReducers from 'reduce-reducers';
+
+import { tGridReducer } from '../../../../timelines/public';
+
import { hostsReducer } from '../../hosts/store';
import { networkReducer } from '../../network/store';
import { timelineReducer } from '../../timelines/store/timeline/reducer';
import { managementReducer } from '../../management/store/reducer';
import { ManagementPluginReducer } from '../../management';
import { SubPluginsInitReducer } from '../store';
+import { mockGlobalState } from './global_state';
+import { TimelineState } from '../../timelines/store/timeline/types';
+import { defaultHeaders } from '../../timelines/components/timeline/body/column_headers/default_headers';
interface Global extends NodeJS.Global {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -19,10 +27,32 @@ interface Global extends NodeJS.Global {
export const globalNode: Global = global;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const combineTimelineReducer = reduceReducers(
+ {
+ ...mockGlobalState.timeline,
+ timelineById: {
+ ...mockGlobalState.timeline.timelineById,
+ test: {
+ ...mockGlobalState.timeline.timelineById.test,
+ defaultColumns: defaultHeaders,
+ loadingText: 'events',
+ footerText: 'events',
+ documentType: '',
+ selectAll: false,
+ queryFields: [],
+ unit: (n: number) => n,
+ },
+ },
+ },
+ tGridReducer,
+ timelineReducer
+) as Reducer;
+
export const SUB_PLUGINS_REDUCER: SubPluginsInitReducer = {
hosts: hostsReducer,
network: networkReducer,
- timeline: timelineReducer,
+ timeline: combineTimelineReducer,
/**
* These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture,
* they are cast to mutable versions here.
diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts
index e784f6cebae17..5791a4940cbed 100644
--- a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts
+++ b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts
@@ -60,6 +60,7 @@ export interface GlobalGenericQuery {
isInspected: boolean;
loading: boolean;
selectedInspectIndex: number;
+ invalidKqlQuery?: Error;
}
export interface GlobalGraphqlQuery extends GlobalGenericQuery {
diff --git a/x-pack/plugins/security_solution/public/common/store/types.ts b/x-pack/plugins/security_solution/public/common/store/types.ts
index fbf4caad9793d..21e833abe1f9b 100644
--- a/x-pack/plugins/security_solution/public/common/store/types.ts
+++ b/x-pack/plugins/security_solution/public/common/store/types.ts
@@ -37,18 +37,6 @@ export type StoreState = HostsPluginState &
*/
export type State = CombinedState;
-export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql';
-
-export interface KueryFilterQuery {
- kind: KueryFilterQueryKind;
- expression: string;
-}
-
-export interface SerializedFilterQuery {
- kuery: KueryFilterQuery | null;
- serializedQuery: string;
-}
-
/**
* like redux's `MiddlewareAPI` but `getState` returns an `Immutable` version of
* state and `dispatch` accepts `Immutable` versions of actions.
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
index e5cefca66d0fd..601e0509009ce 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
@@ -14,6 +14,7 @@ import { i18n } from '@kbn/i18n';
import type { Filter } from '../../../../../../../src/plugins/data/common/es_query/filters';
import {
+ KueryFilterQueryKind,
TimelineId,
TimelineResult,
TimelineStatus,
@@ -44,7 +45,6 @@ import {
replaceTemplateFieldFromMatchFilters,
replaceTemplateFieldFromDataProviders,
} from './helpers';
-import { KueryFilterQueryKind } from '../../../common/store';
import {
DataProvider,
QueryOperator,
@@ -399,7 +399,7 @@ export const sendAlertToTimelineAction = async ({
factoryQueryType: TimelineEventsQueries.details,
},
{
- strategy: 'securitySolutionTimelineSearchStrategy',
+ strategy: 'timelineSearchStrategy',
}
)
.toPromise(),
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx
index 4ca2980dc74e5..a3d3bf4834376 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx
@@ -11,6 +11,7 @@ import { shallow, mount } from 'enzyme';
import { AlertsUtilityBar, AlertsUtilityBarProps } from './index';
import { TestProviders } from '../../../../common/mock/test_providers';
+jest.useFakeTimers();
jest.mock('../../../../common/lib/kibana');
describe('AlertsUtilityBar', () => {
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx
index 02a815bc59f3b..9a142f6cba247 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx
@@ -6,11 +6,11 @@
*/
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
-import { RowRendererId } from '../../../../common/types/timeline';
+import { ColumnHeaderOptions, RowRendererId } from '../../../../common/types/timeline';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
import { Filter } from '../../../../../../../src/plugins/data/common/es_query';
-import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model';
+import { SubsetTimelineModel } from '../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { columns } from '../../configurations/security_solution_detections/columns';
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
index f20754fc446d6..a27368cc61c3a 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
@@ -8,11 +8,11 @@
import { EuiPanel, EuiLoadingContent } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { connect, ConnectedProps } from 'react-redux';
+import { connect, ConnectedProps, useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
import { Filter, esQuery } from '../../../../../../../src/plugins/data/public';
-import { TimelineIdLiteral } from '../../../../common/types/timeline';
+import { RowRendererId, TimelineIdLiteral } from '../../../../common/types/timeline';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import { StatefulEventsViewer } from '../../../common/components/events_viewer';
import { HeaderSection } from '../../../common/components/header_section';
@@ -23,8 +23,6 @@ import { inputsSelectors, State, inputsModel } from '../../../common/store';
import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline';
import { TimelineModel } from '../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
-import { useManageTimeline } from '../../../timelines/components/manage_timeline';
-
import { updateAlertStatusAction } from './actions';
import {
requiredFieldsForActions,
@@ -95,6 +93,7 @@ export const AlertsTableComponent: React.FC = ({
timelineId,
to,
}) => {
+ const dispatch = useDispatch();
const [showClearSelectionAction, setShowClearSelectionAction] = useState(false);
const [filterGroup, setFilterGroup] = useState(FILTER_OPEN);
const {
@@ -106,7 +105,6 @@ export const AlertsTableComponent: React.FC = ({
const kibana = useKibana();
const [, dispatchToaster] = useStateToaster();
const { addWarning } = useAppToasts();
- const { initializeTimeline, setSelectAll } = useManageTimeline();
// TODO: Once we are past experimental phase this code should be removed
const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled');
@@ -195,14 +193,16 @@ export const AlertsTableComponent: React.FC = ({
// Catches state change isSelectAllChecked->false upon user selection change to reset utility bar
useEffect(() => {
if (isSelectAllChecked) {
- setSelectAll({
- id: timelineId,
- selectAll: false,
- });
+ dispatch(
+ timelineActions.setTGridSelectAll({
+ id: timelineId,
+ selectAll: false,
+ })
+ );
} else {
setShowClearSelectionAction(false);
}
- }, [isSelectAllChecked, setSelectAll, timelineId]);
+ }, [dispatch, isSelectAllChecked, timelineId]);
// Callback for when open/closed filter changes
const onFilterGroupChangedCallback = useCallback(
@@ -218,23 +218,27 @@ export const AlertsTableComponent: React.FC = ({
// Callback for clearing entire selection from utility bar
const clearSelectionCallback = useCallback(() => {
clearSelected!({ id: timelineId });
- setSelectAll({
- id: timelineId,
- selectAll: false,
- });
+ dispatch(
+ timelineActions.setTGridSelectAll({
+ id: timelineId,
+ selectAll: false,
+ })
+ );
setShowClearSelectionAction(false);
- }, [clearSelected, setSelectAll, setShowClearSelectionAction, timelineId]);
+ }, [clearSelected, dispatch, timelineId]);
// Callback for selecting all events on all pages from utility bar
// Dispatches to stateful_body's selectAll via TimelineTypeContext props
// as scope of response data required to actually set selectedEvents
const selectAllOnAllPagesCallback = useCallback(() => {
- setSelectAll({
- id: timelineId,
- selectAll: true,
- });
+ dispatch(
+ timelineActions.setTGridSelectAll({
+ id: timelineId,
+ selectAll: true,
+ })
+ );
setShowClearSelectionAction(true);
- }, [setSelectAll, setShowClearSelectionAction, timelineId]);
+ }, [dispatch, timelineId]);
const updateAlertsStatusCallback: UpdateAlertsStatusCallback = useCallback(
async (
@@ -330,22 +334,22 @@ export const AlertsTableComponent: React.FC = ({
: alertsDefaultModel;
useEffect(() => {
- initializeTimeline({
- defaultModel: {
- ...defaultTimelineModel,
- columns,
- },
- documentType: i18n.ALERTS_DOCUMENT_TYPE,
- filterManager,
- footerText: i18n.TOTAL_COUNT_OF_ALERTS,
- id: timelineId,
- loadingText: i18n.LOADING_ALERTS,
- selectAll: false,
- queryFields: requiredFieldsForActions,
- title: '',
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ dispatch(
+ timelineActions.initializeTGridSettings({
+ defaultColumns: columns,
+ documentType: i18n.ALERTS_DOCUMENT_TYPE,
+ excludedRowRendererIds: defaultTimelineModel.excludedRowRendererIds as RowRendererId[],
+ filterManager,
+ footerText: i18n.TOTAL_COUNT_OF_ALERTS,
+ id: timelineId,
+ loadingText: i18n.LOADING_ALERTS,
+ selectAll: false,
+ queryFields: requiredFieldsForActions,
+ title: '',
+ showCheckboxes: true,
+ })
+ );
+ }, [dispatch, defaultTimelineModel, filterManager, timelineId]);
const headerFilterGroup = useMemo(
() => ,
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/columns.ts
index 8cbb532501a2c..70d2237a535eb 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/columns.ts
+++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/columns.ts
@@ -6,10 +6,9 @@
*/
import { EuiDataGridColumn } from '@elastic/eui';
-
+import { ColumnHeaderOptions } from '../../../../../common';
import { defaultColumnHeaderType } from '../../../../timelines/components/timeline/body/column_headers/default_headers';
import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../../../timelines/components/timeline/body/constants';
-import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model';
import * as i18n from '../../../components/alerts_table/translations';
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx
index 9c2114a4ef085..7db75d3a73d90 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx
@@ -15,10 +15,12 @@ import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../com
import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline';
import { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering';
import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
-import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../../common';
import { RenderCellValue } from '.';
+jest.mock('../../../../common/lib/kibana/');
+
describe('RenderCellValue', () => {
const columnId = '@timestamp';
const eventId = '_id-123';
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/columns.ts
index 96d2d870b1270..3365ce5432940 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/columns.ts
+++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/columns.ts
@@ -6,10 +6,9 @@
*/
import { EuiDataGridColumn } from '@elastic/eui';
-
+import { ColumnHeaderOptions } from '../../../../../common';
import { defaultColumnHeaderType } from '../../../../timelines/components/timeline/body/column_headers/default_headers';
import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../../../timelines/components/timeline/body/constants';
-import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model';
import * as i18n from '../../../components/alerts_table/translations';
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx
index aa4eb543a3d9b..a8f295df2540d 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx
@@ -15,9 +15,11 @@ import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../com
import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline';
import { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering';
import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
-import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model';
import { RenderCellValue } from '.';
+import { ColumnHeaderOptions } from '../../../../../common';
+
+jest.mock('../../../../common/lib/kibana/');
describe('RenderCellValue', () => {
const columnId = '@timestamp';
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts
index 23a0740294e84..7f46c839ffe62 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts
+++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts
@@ -6,13 +6,13 @@
*/
import { EuiDataGridColumn } from '@elastic/eui';
+import { ColumnHeaderOptions } from '../../../../common';
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_DATE_COLUMN_MIN_WIDTH,
} from '../../../timelines/components/timeline/body/constants';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import * as i18n from '../../components/alerts_table/translations';
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx
index 18350c102c049..965ee913a1daa 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx
@@ -9,16 +9,18 @@ import { mount } from 'enzyme';
import { cloneDeep } from 'lodash/fp';
import React from 'react';
+import { ColumnHeaderOptions } from '../../../../common';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { DragDropContextWrapper } from '../../../common/components/drag_and_drop/drag_drop_context_wrapper';
import { defaultHeaders, mockTimelineData, TestProviders } from '../../../common/mock';
import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { RenderCellValue } from '.';
+jest.mock('../../../common/lib/kibana');
+
describe('RenderCellValue', () => {
const columnId = '@timestamp';
const eventId = '_id-123';
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
index 84eaf8e3aa93c..6f8d938dd987e 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx
@@ -6,13 +6,13 @@
*/
import { useEffect, useState } from 'react';
+import { isSecurityAppError } from '@kbn/securitysolution-t-grid';
import { DEFAULT_ALERTS_INDEX } from '../../../../../common/constants';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { createSignalIndex, getSignalIndex } from './api';
import * as i18n from './translations';
-import { isSecurityAppError } from '../../../../common/utils/api';
import { useAlertsPrivileges } from './use_alerts_privileges';
type Func = () => Promise;
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx
index 8e231f0d1fdbb..d55d171708963 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx
@@ -6,10 +6,9 @@
*/
import { useEffect, useState, useCallback } from 'react';
-
+import { isSecurityAppError } from '@kbn/securitysolution-t-grid';
import { useReadListIndex, useCreateListIndex } from '@kbn/securitysolution-list-hooks';
import { useHttp, useKibana } from '../../../../common/lib/kibana';
-import { isSecurityAppError } from '../../../../common/utils/api';
import * as i18n from './translations';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useListsPrivileges } from './use_lists_privileges';
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx
index f848b71cf7bd3..4f524886935cd 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx
@@ -6,8 +6,8 @@
*/
import { useEffect, useRef, useState } from 'react';
+import { isNotFoundError } from '@kbn/securitysolution-t-grid';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
-import { isNotFoundError } from '../../../../common/utils/api';
import { RuleStatusRowItemType } from '../../../pages/detection_engine/rules/all/columns';
import { getRuleStatusById, getRulesStatusByIds } from './api';
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx
index 4a39e486b6fd5..abd5a2781c8a7 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx
@@ -6,11 +6,11 @@
*/
import { renderHook, act } from '@testing-library/react-hooks';
+import { SecurityAppError } from '@kbn/securitysolution-t-grid';
import { useRuleWithFallback } from './use_rule_with_fallback';
import * as api from './api';
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
-import { SecurityAppError } from '../../../../common/utils/api';
jest.mock('./api');
jest.mock('../alerts/api');
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx
index 11c30547848c3..da56275280f65 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx
@@ -6,9 +6,9 @@
*/
import { useCallback, useEffect, useMemo } from 'react';
+import { isNotFoundError } from '@kbn/securitysolution-t-grid';
import { useAsync, withOptionalSignal } from '@kbn/securitysolution-hook-utils';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
-import { isNotFoundError } from '../../../../common/utils/api';
import { useQueryAlerts } from '../alerts/use_query';
import { fetchRuleById } from './api';
import { transformInput } from './transforms';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
index 8ae7e4fb2852b..1c31dfd3b8907 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
@@ -11,13 +11,13 @@ import { noop } from 'lodash/fp';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
+import { isTab } from '../../../../../timelines/public';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { SecurityPageName } from '../../../app/types';
import { TimelineId } from '../../../../common/types/timeline';
import { useGlobalTime } from '../../../common/containers/use_global_time';
-import { isTab } from '../../../common/components/accessibility/helpers';
import { UpdateDateRange } from '../../../common/components/charts/common';
import { FiltersGlobal } from '../../../common/components/filters_global';
import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx
index dd3549ea20d36..8cc3113a5706a 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx
@@ -42,6 +42,9 @@ describe('ExceptionListsTable', () => {
addError: jest.fn(),
},
},
+ timelines: {
+ getLastUpdated: () => null,
+ },
},
});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
index 7f734b10fd020..35404f4486bc3 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
@@ -26,7 +26,6 @@ import { Loader } from '../../../../../../common/components/loader';
import { Panel } from '../../../../../../common/components/panel';
import * as i18n from './translations';
import { AllRulesUtilityBar } from '../utility_bar';
-import { LastUpdatedAt } from '../../../../../../common/components/last_updated';
import { AllExceptionListsColumns, getAllExceptionListsColumns } from './columns';
import { useAllExceptionLists } from './use_all_exception_lists';
import { ReferenceErrorModal } from '../../../../../components/value_lists_management_modal/reference_error_modal';
@@ -62,7 +61,7 @@ const exceptionReferenceModalInitialState: ReferenceModalState = {
export const ExceptionListsTable = React.memo(
({ formatUrl, history, hasPermissions, loading }) => {
const {
- services: { http, notifications },
+ services: { http, notifications, timelines },
} = useKibana();
const { exportExceptionList, deleteExceptionList } = useApi(http);
@@ -344,7 +343,7 @@ export const ExceptionListsTable = React.memo(
}
+ subtitle={timelines.getLastUpdated({ showUpdating: loading, updatedAt: lastUpdated })}
>
{!initLoading && }
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx
index 8fd82a495e52f..2ec34aaece60b 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx
@@ -47,7 +47,6 @@ import { hasMlAdminPermissions } from '../../../../../../common/machine_learning
import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license';
import { isBoolean } from '../../../../../common/utils/privileges';
import { AllRulesUtilityBar } from './utility_bar';
-import { LastUpdatedAt } from '../../../../../common/components/last_updated';
import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../common/constants';
import { AllRulesTabs } from '.';
import { useValueChanged } from '../../../../../common/hooks/use_value_changed';
@@ -104,6 +103,7 @@ export const RulesTables = React.memo(
application: {
capabilities: { actions },
},
+ timelines,
},
} = useKibana();
@@ -473,12 +473,10 @@ export const RulesTables = React.memo(
split
growLeftSplit={false}
title={i18n.ALL_RULES}
- subtitle={
-
- }
+ subtitle={timelines.getLastUpdated({
+ showUpdating: loading || isLoadingRules || isLoadingRulesStatuses,
+ updatedAt: lastUpdated,
+ })}
>
{shouldShowRulesTable && (
({
diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx
index 751a2bf5a2055..2cd4ed1f57f84 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx
@@ -20,6 +20,8 @@ import { mockData } from './mock';
import { HostsType } from '../../store/model';
import * as i18n from './translations';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx
index 2333d5e9b127c..b51e20b801f40 100644
--- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx
@@ -19,6 +19,8 @@ import { type } from './utils';
import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { getHostDetailsPageFilters } from './helpers';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('../../../common/components/url_state/normalize_time_range.ts');
jest.mock('../../../common/containers/source', () => ({
diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx
index 57cded85d67cc..ce0385b532fd5 100644
--- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx
@@ -11,6 +11,7 @@ import { noop } from 'lodash/fp';
import React, { useCallback, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
+import { isTab } from '../../../../timelines/public';
import { SecurityPageName } from '../../app/types';
import { UpdateDateRange } from '../../common/components/charts/common';
@@ -42,7 +43,6 @@ import * as i18n from './translations';
import { filterHostData } from './navigation';
import { hostsModel } from '../store';
import { HostsTableType } from '../store/model';
-import { isTab } from '../../common/components/accessibility/helpers';
import {
onTimelineTabKeyPressed,
resetKeyboardFocus,
diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx
index f88709e6e95ac..973dbc41925da 100644
--- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx
@@ -10,6 +10,7 @@ import { useDispatch } from 'react-redux';
import { TimelineId } from '../../../../common/types/timeline';
import { StatefulEventsViewer } from '../../../common/components/events_viewer';
+import { timelineActions } from '../../../timelines/store/timeline';
import { HostsComponentsQueryProps } from './types';
import { eventsDefaultModel } from '../../../common/components/events_viewer/default_model';
import {
@@ -20,7 +21,6 @@ import { MatrixHistogram } from '../../../common/components/matrix_histogram';
import { useGlobalFullScreen } from '../../../common/containers/use_full_screen';
import * as i18n from '../translations';
import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution';
-import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
@@ -64,14 +64,15 @@ const EventsQueryTabBodyComponent: React.FC = ({
startDate,
}) => {
const dispatch = useDispatch();
- const { initializeTimeline } = useManageTimeline();
const { globalFullScreen } = useGlobalFullScreen();
useEffect(() => {
- initializeTimeline({
- id: TimelineId.hostsPageEvents,
- defaultModel: eventsDefaultModel,
- });
- }, [dispatch, initializeTimeline]);
+ dispatch(
+ timelineActions.initializeTGridSettings({
+ id: TimelineId.hostsPageEvents,
+ defaultColumns: eventsDefaultModel.columns,
+ })
+ );
+ }, [dispatch]);
useEffect(() => {
return () => {
diff --git a/x-pack/plugins/security_solution/public/index.ts b/x-pack/plugins/security_solution/public/index.ts
index 55262fe039b4e..3d2412b326b54 100644
--- a/x-pack/plugins/security_solution/public/index.ts
+++ b/x-pack/plugins/security_solution/public/index.ts
@@ -8,6 +8,7 @@
import { PluginInitializerContext } from '../../../../src/core/public';
import { Plugin } from './plugin';
import { PluginSetup } from './types';
+export type { TimelineModel } from './timelines/store/timeline/model';
export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context);
diff --git a/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx
index a3fd32008062c..63971ae508d5c 100644
--- a/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx
@@ -14,6 +14,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { Ip } from '.';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx
index 7ec18c078c73d..a811f5c92c37a 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx
@@ -25,6 +25,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { NetworkDnsTable } from '.';
import { mockData } from './mock';
+jest.mock('../../../common/lib/kibana');
+
describe('NetworkTopNFlow Table Component', () => {
const loadPage = jest.fn();
const state: State = mockGlobalState;
diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx
index f7f75d9f0a365..f05372c76b36f 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx
@@ -25,6 +25,7 @@ import { networkModel } from '../../store';
import { NetworkHttpTable } from '.';
import { mockData } from './mock';
+jest.mock('../../../common/lib/kibana');
jest.mock('../../../common/components/link_to');
describe('NetworkHttp Table Component', () => {
diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx
index 1501f56882290..a0727fad65f18 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx
@@ -27,6 +27,8 @@ import { networkModel } from '../../store';
import { NetworkTopCountriesTable } from '.';
import { mockData } from './mock';
+jest.mock('../../../common/lib/kibana');
+
describe('NetworkTopCountries Table Component', () => {
const loadPage = jest.fn();
const state: State = mockGlobalState;
diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx
index cd8c8c6543299..e2b9447b58806 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx
@@ -25,6 +25,7 @@ import { NetworkTopNFlowTable } from '.';
import { mockData } from './mock';
import { FlowTargetSourceDest } from '../../../../common/search_strategy';
+jest.mock('../../../common/lib/kibana');
jest.mock('../../../common/components/link_to');
describe('NetworkTopNFlow Table Component', () => {
diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx
index ef1039bfc92e3..dd7ad20d2384a 100644
--- a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx
@@ -15,6 +15,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { Port } from '.';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx
index 01065ad5bf15f..b59eb25cbfe25 100644
--- a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx
@@ -49,6 +49,8 @@ import {
NETWORK_TRANSPORT_FIELD_NAME,
} from './field_names';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx
index f767e793c8f21..91f7ea3d7ac7a 100644
--- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx
@@ -38,6 +38,8 @@ import {
SOURCE_GEO_REGION_NAME_FIELD_NAME,
} from './geo_fields';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('../../../common/components/link_to');
describe('SourceDestinationIp', () => {
diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx
index 4b6c31f5b6176..8f2c7a098a045 100644
--- a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx
@@ -24,6 +24,8 @@ import { networkModel } from '../../store';
import { TlsTable } from '.';
import { mockTlsData } from './mock';
+jest.mock('../../../common/lib/kibana');
+
describe('Tls Table Component', () => {
const loadPage = jest.fn();
const state: State = mockGlobalState;
diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx
index 4b613e79a1d1a..69027ad9bd9f8 100644
--- a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx
@@ -26,6 +26,8 @@ import { UsersTable } from '.';
import { mockUsersData } from './mock';
import { FlowTarget } from '../../../../common/search_strategy';
+jest.mock('../../../common/lib/kibana');
+
describe('Users Table Component', () => {
const loadPage = jest.fn();
const state: State = mockGlobalState;
diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx
index 2bcc72d932a9b..dbfb250095ee2 100644
--- a/x-pack/plugins/security_solution/public/network/pages/network.tsx
+++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx
@@ -12,6 +12,7 @@ import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
import styled from 'styled-components';
+import { isTab } from '../../../../timelines/public';
import { esQuery } from '../../../../../../src/plugins/data/public';
import { SecurityPageName } from '../../app/types';
import { UpdateDateRange } from '../../common/components/charts/common';
@@ -46,7 +47,6 @@ import {
showGlobalFilters,
} from '../../timelines/components/timeline/helpers';
import { timelineSelectors } from '../../timelines/store/timeline';
-import { isTab } from '../../common/components/accessibility/helpers';
import { TimelineId } from '../../../common/types/timeline';
import { timelineDefaults } from '../../timelines/store/timeline/defaults';
import { useSourcererScope } from '../../common/containers/sourcerer';
diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx
index b43d5af029ec4..45898427ee60b 100644
--- a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx
@@ -15,6 +15,8 @@ import { TestProviders } from '../../../../common/mock';
import { EndpointOverview } from './index';
import { HostPolicyResponseActionStatus } from '../../../../../common/search_strategy/security_solution/hosts';
+jest.mock('../../../../common/lib/kibana');
+
describe('EndpointOverview Component', () => {
test('it renders with endpoint data', () => {
const endpointData = {
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index 781ed8ffdaa54..5a44faa58414a 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -6,8 +6,10 @@
*/
import { i18n } from '@kbn/i18n';
+import reduceReducers from 'reduce-reducers';
import { BehaviorSubject, Subject, Subscription } from 'rxjs';
import { pluck } from 'rxjs/operators';
+import { AnyAction, Reducer } from 'redux';
import {
PluginSetup,
PluginStart,
@@ -72,6 +74,7 @@ import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/vi
import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension';
import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension';
import { parseExperimentalConfigValue } from '../common/experimental_features';
+import type { TimelineState } from '../../timelines/public';
export class Plugin implements IPlugin {
private kibanaVersion: string;
@@ -471,7 +474,7 @@ export class Plugin implements IPlugin(
{ indices: defaultIndicesName, onlyCheckIfIndicesExist: true },
{
- strategy: 'securitySolutionIndexFields',
+ strategy: 'indexFields',
}
)
.toPromise(),
@@ -500,7 +503,6 @@ export class Plugin implements IPlugin;
+
this._store = createStore(
createInitialState(
{
@@ -531,13 +540,17 @@ export class Plugin implements IPlugin {
const mount = useMountAppended();
test('renders the expected label', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx
index 4c90d3738a198..ea8317346cd99 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx
@@ -14,6 +14,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { Duration } from '.';
+jest.mock('../../../common/lib/kibana');
+
describe('Duration', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx
index 5becf7ea8bc6b..e2194156ecf4d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx
@@ -29,6 +29,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { AutonomousSystem, FlowTarget } from '../../../../common/search_strategy';
import { HostEcs } from '../../../../common/ecs/host';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx
index 77a8d0082bf23..da2ff248d9a5d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx
@@ -14,7 +14,7 @@ import {
DATA_COLINDEX_ATTRIBUTE,
DATA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
-} from '../../../common/components/accessibility/helpers';
+} from '../../../../../timelines/public';
import { BrowserFields } from '../../../common/containers/source';
import { getCategoryColumns } from './category_columns';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx
index c3c55206f8d53..c95463dea5b27 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx
@@ -17,6 +17,9 @@ import { TestProviders } from '../../../common/mock';
import { useMountAppended } from '../../../common/utils/use_mount_appended';
import * as i18n from './translations';
+
+jest.mock('../../../common/lib/kibana');
+
describe('Category', () => {
const timelineId = 'test';
const selectedCategoryId = 'client';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx
index 636ebf022cffb..deafda95ceab2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx
@@ -9,13 +9,13 @@ import { EuiInMemoryTable } from '@elastic/eui';
import { noop } from 'lodash/fp';
import React, { useCallback, useMemo, useRef } from 'react';
import styled from 'styled-components';
-
import {
arrayIndexToAriaIndex,
DATA_COLINDEX_ATTRIBUTE,
DATA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
-} from '../../../common/components/accessibility/helpers';
+} from '../../../../../timelines/public';
+
import { BrowserFields } from '../../../common/containers/source';
import { OnUpdateColumns } from '../timeline/events';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx
index 15164cd151574..528791328fdb9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx
@@ -18,6 +18,7 @@ import {
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
+import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
import { BrowserFields } from '../../../common/containers/source';
import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers';
import { CountBadge } from '../../../common/components/page';
@@ -29,7 +30,7 @@ import {
VIEW_ALL_BUTTON_CLASS_NAME,
} from './helpers';
import * as i18n from './translations';
-import { useManageTimeline } from '../manage_timeline';
+import { timelineSelectors } from '../../store/timeline';
const CategoryName = styled.span<{ bold: boolean }>`
.euiText {
@@ -67,11 +68,10 @@ interface ViewAllButtonProps {
export const ViewAllButton = React.memo(
({ categoryId, browserFields, onUpdateColumns, timelineId }) => {
- const { getManageTimelineById } = useManageTimeline();
- const { isLoading } = useMemo(() => getManageTimelineById(timelineId) ?? { isLoading: false }, [
- getManageTimelineById,
- timelineId,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { isLoading } = useDeepEqualSelector((state) =>
+ getManageTimeline(state, timelineId ?? '')
+ );
const handleClick = useCallback(() => {
onUpdateColumns(
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx
index 70cc535cb59a9..6af4b5c5c312e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx
@@ -9,6 +9,7 @@ import { mount } from 'enzyme';
import React from 'react';
import { mockBrowserFields } from '../../../common/containers/source/mock';
+import { TestProviders } from '../../../common/mock';
import { CategoryTitle } from './category_title';
import { getFieldCount } from './helpers';
@@ -19,12 +20,14 @@ describe('CategoryTitle', () => {
test('it renders the category id as the value of the title', () => {
const categoryId = 'client';
const wrapper = mount(
-
+
+
+
);
expect(wrapper.find('[data-test-subj="selected-category-title"]').first().text()).toEqual(
@@ -35,12 +38,14 @@ describe('CategoryTitle', () => {
test('when `categoryId` specifies a valid category in `filteredBrowserFields`, a count of the field is displayed in the badge', () => {
const validCategoryId = 'client';
const wrapper = mount(
-
+
+
+
);
expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual(
@@ -51,12 +56,14 @@ describe('CategoryTitle', () => {
test('when `categoryId` specifies an INVALID category in `filteredBrowserFields`, a count of zero is displayed in the badge', () => {
const invalidCategoryId = 'this.is.not.happening';
const wrapper = mount(
-
+
+
+
);
expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual(
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx
index c4f76c639c7c1..0496b9d7c8886 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx
@@ -19,13 +19,8 @@ import { noop } from 'lodash/fp';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
-import {
- isEscape,
- isTab,
- stopPropagationAndPreventDefault,
-} from '../../../common/components/accessibility/helpers';
+import { isEscape, isTab, stopPropagationAndPreventDefault } from '../../../../../timelines/public';
import { BrowserFields } from '../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { CategoriesPane } from './categories_pane';
import { FieldsPane } from './fields_pane';
import { Header } from './header';
@@ -42,6 +37,7 @@ import { FieldBrowserProps, OnHideFieldBrowser } from './types';
import { timelineActions } from '../../store/timeline';
import * as i18n from './translations';
+import { ColumnHeaderOptions } from '../../../../common';
const FieldsBrowserContainer = styled.div<{ width: number }>`
background-color: ${({ theme }) => theme.eui.euiColorLightestShade};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx
index 07911541bb2fe..e40807dc85dc7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx
@@ -12,7 +12,6 @@ import { waitFor } from '@testing-library/react';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { TestProviders } from '../../../common/mock';
import '../../../common/mock/match_media';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers';
import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../timeline/body/constants';
@@ -20,6 +19,9 @@ import { Category } from './category';
import { getFieldColumns, getFieldItems } from './field_items';
import { FIELDS_PANE_WIDTH } from './helpers';
import { useMountAppended } from '../../../common/utils/use_mount_appended';
+import { ColumnHeaderOptions } from '../../../../common';
+
+jest.mock('../../../common/lib/kibana');
const selectedCategoryId = 'base';
const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx
index a2db284e51790..89a91ee6da305 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx
@@ -18,14 +18,12 @@ import React, { useCallback, useRef, useState } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import styled from 'styled-components';
+import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { BrowserField, BrowserFields } from '../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
-import { useDraggableKeyboardWrapper } from '../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook';
import { DragEffects } from '../../../common/components/drag_and_drop/draggable_wrapper';
import { DroppableWrapper } from '../../../common/components/drag_and_drop/droppable_wrapper';
import {
DRAG_TYPE_FIELD,
- DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME,
getDraggableFieldId,
getDroppableId,
} from '../../../common/components/drag_and_drop/helpers';
@@ -43,6 +41,8 @@ import { TruncatableText } from '../../../common/components/truncatable_text';
import { FieldName } from './field_name';
import * as i18n from './translations';
import { getAlertColumnHeader } from './helpers';
+import { ColumnHeaderOptions } from '../../../../common';
+import { useKibana } from '../../../common/lib/kibana';
const TypeIcon = styled(EuiIcon)`
margin: 0 4px;
@@ -92,6 +92,7 @@ const DraggableFieldsBrowserFieldComponent = ({
const keyboardHandlerRef = useRef(null);
const [closePopOverTrigger, setClosePopOverTrigger] = useState(false);
const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(false);
+ const { timelines } = useKibana().services;
const handleClosePopOverTrigger = useCallback(() => {
setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger);
@@ -115,7 +116,7 @@ const DraggableFieldsBrowserFieldComponent = ({
setHoverActionsOwnFocus(true);
}, [setHoverActionsOwnFocus]);
- const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
+ const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({
closePopover: handleClosePopOverTrigger,
draggableId: getDraggableFieldId({
contextId: `field-browser-field-items-field-draggable-${timelineId}-${categoryId}-${fieldName}`,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx
index 493f2e44263e3..5014a198e8bd5 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx
@@ -15,6 +15,8 @@ import { getColumnsWithTimestamp } from '../../../common/components/event_detail
import { FieldName } from './field_name';
+jest.mock('../../../common/lib/kibana');
+
const categoryId = 'base';
const timestampFieldId = '@timestamp';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx
index 09bd18ef62fb1..2e76e43227506 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx
@@ -9,13 +9,13 @@ import { EuiHighlight, EuiText } from '@elastic/eui';
import React, { useCallback, useState, useMemo, useRef } from 'react';
import styled from 'styled-components';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { OnUpdateColumns } from '../timeline/events';
import { WithHoverActions } from '../../../common/components/with_hover_actions';
import {
DraggableWrapperHoverContent,
useGetTimelineId,
} from '../../../common/components/drag_and_drop/draggable_wrapper_hover_content';
+import { ColumnHeaderOptions } from '../../../../common';
/**
* The name of a (draggable) field
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx
index 3f1b0300ad70d..6d17f148aa1dc 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx
@@ -15,6 +15,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { FIELDS_PANE_WIDTH } from './helpers';
import { FieldsPane } from './fields_pane';
+jest.mock('../../../common/lib/kibana');
+
const timelineId = 'test';
describe('FieldsPane', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx
index 15df232a1a454..dfb4edad17414 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx
@@ -11,7 +11,6 @@ import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { BrowserFields } from '../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { timelineActions } from '../../../timelines/store/timeline';
import { OnUpdateColumns } from '../timeline/events';
import { Category } from './category';
@@ -20,6 +19,7 @@ import { getFieldItems } from './field_items';
import { FIELDS_PANE_WIDTH, TABLE_HEIGHT } from './helpers';
import * as i18n from './translations';
+import { ColumnHeaderOptions } from '../../../../common';
const NoFieldsPanel = styled.div`
background-color: ${(props) => props.theme.eui.euiColorLightestShade};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx
index aa53b1922f3a3..89b361e86422e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx
@@ -9,7 +9,6 @@ import { mount } from 'enzyme';
import React from 'react';
import { mockBrowserFields } from '../../../common/containers/source/mock';
import { TestProviders } from '../../../common/mock';
-import { defaultHeaders } from '../timeline/body/column_headers/default_headers';
import { Header } from './header';
const timelineId = 'test';
@@ -72,7 +71,7 @@ describe('Header', () => {
wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click');
- expect(onUpdateColumns).toBeCalledWith(defaultHeaders);
+ expect(onUpdateColumns).toBeCalled();
});
test('it invokes onOutsideClick when the user clicks the Reset Fields button', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx
index 120a82a4046e3..b52c6cd672ac7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx
@@ -13,10 +13,12 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
-import React, { useCallback } from 'react';
+import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { BrowserFields } from '../../../common/containers/source';
+import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
+import { timelineSelectors } from '../../store/timeline';
import { OnUpdateColumns } from '../timeline/events';
import {
@@ -27,7 +29,6 @@ import {
} from './helpers';
import * as i18n from './translations';
-import { useManageTimeline } from '../manage_timeline';
const CountsFlexGroup = styled(EuiFlexGroup)`
margin-top: 5px;
@@ -101,13 +102,13 @@ const TitleRow = React.memo<{
onOutsideClick: () => void;
onUpdateColumns: OnUpdateColumns;
}>(({ id, onOutsideClick, onUpdateColumns }) => {
- const { getManageTimelineById } = useManageTimeline();
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { defaultColumns } = useDeepEqualSelector((state) => getManageTimeline(state, id));
const handleResetColumns = useCallback(() => {
- const timeline = getManageTimelineById(id);
- onUpdateColumns(timeline.defaultModel.columns);
+ onUpdateColumns(defaultColumns);
onOutsideClick();
- }, [id, onUpdateColumns, onOutsideClick, getManageTimelineById]);
+ }, [onUpdateColumns, onOutsideClick, defaultColumns]);
return (
{
const timelineId = 'test';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts
index 4d912f73c7ef2..ea71a8860ab01 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts
@@ -5,8 +5,8 @@
* 2.0.
*/
+import { ColumnHeaderOptions } from '../../../../common';
import { BrowserFields } from '../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
export type OnFieldSelected = (fieldId: string) => void;
export type OnHideFieldBrowser = () => void;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx
index 7b43fb9c7194c..feaf7b7513bc1 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx
@@ -10,7 +10,7 @@ import { rgba } from 'polished';
import React from 'react';
import styled from 'styled-components';
-import { IS_DRAGGING_CLASS_NAME } from '../../../../common/components/drag_and_drop/helpers';
+import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { DataProvider } from '../../timeline/data_providers/data_provider';
import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers';
import { DataProviders } from '../../timeline/data_providers';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx
index 802dd74c1892b..31f2fec942490 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx
@@ -14,6 +14,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended';
import { Ja3Fingerprint } from '.';
+jest.mock('../../../common/lib/kibana');
+
describe('Ja3Fingerprint', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx
deleted file mode 100644
index ed299c3a4ef1a..0000000000000
--- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx
+++ /dev/null
@@ -1,125 +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 { renderHook, act } from '@testing-library/react-hooks';
-import { getTimelineDefaults, useTimelineManager, UseTimelineManager } from './';
-import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager';
-import { coreMock } from '../../../../../../../src/core/public/mocks';
-
-const isStringifiedComparisonEqual = (a: {}, b: {}): boolean =>
- JSON.stringify(a) === JSON.stringify(b);
-
-describe('useTimelineManager', () => {
- const setupMock = coreMock.createSetup();
- const testId = 'coolness';
- const timelineDefaults = getTimelineDefaults(testId);
- const mockFilterManager = new FilterManager(setupMock.uiSettings);
-
- beforeEach(() => {
- jest.clearAllMocks();
- jest.restoreAllMocks();
- });
-
- it('initializes an undefined timeline', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useTimelineManager()
- );
- await waitForNextUpdate();
- const uninitializedTimeline = result.current.getManageTimelineById(testId);
- expect(isStringifiedComparisonEqual(uninitializedTimeline, timelineDefaults)).toBeTruthy();
- });
- });
- // TO DO sourcerer
- // it('getIndexToAddById', async () => {
- // await act(async () => {
- // const { result, waitForNextUpdate } = renderHook(() =>
- // useTimelineManager()
- // );
- // await waitForNextUpdate();
- // const data = result.current.getIndexToAddById(testId);
- // expect(data).toEqual(timelineDefaults.indexToAdd);
- // });
- // });
- //
- // it('setIndexToAdd', async () => {
- // await act(async () => {
- // const indexToAddArgs = { id: testId, indexToAdd: ['example'] };
- // const { result, waitForNextUpdate } = renderHook(() =>
- // useTimelineManager()
- // );
- // await waitForNextUpdate();
- // result.current.initializeTimeline({
- // id: testId,
- // });
- // result.current.setIndexToAdd(indexToAddArgs);
- // const data = result.current.getIndexToAddById(testId);
- // expect(data).toEqual(indexToAddArgs.indexToAdd);
- // });
- // });
-
- it('setIsTimelineLoading', async () => {
- await act(async () => {
- const isLoadingArgs = { id: testId, isLoading: true };
- const { result, waitForNextUpdate } = renderHook(() =>
- useTimelineManager()
- );
- await waitForNextUpdate();
- result.current.initializeTimeline({
- id: testId,
- });
- let timeline = result.current.getManageTimelineById(testId);
- expect(timeline.isLoading).toBeFalsy();
- result.current.setIsTimelineLoading(isLoadingArgs);
- timeline = result.current.getManageTimelineById(testId);
- expect(timeline.isLoading).toBeTruthy();
- });
- });
-
- it('getTimelineFilterManager undefined on uninitialized', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useTimelineManager()
- );
- await waitForNextUpdate();
- const data = result.current.getTimelineFilterManager(testId);
- expect(data).toEqual(undefined);
- });
- });
-
- it('getTimelineFilterManager defined at initialize', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useTimelineManager()
- );
- await waitForNextUpdate();
- result.current.initializeTimeline({
- id: testId,
- filterManager: mockFilterManager,
- });
- const data = result.current.getTimelineFilterManager(testId);
- expect(data).toEqual(mockFilterManager);
- });
- });
-
- it('isManagedTimeline returns false when unset and then true when set', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useTimelineManager()
- );
- await waitForNextUpdate();
- let data = result.current.isManagedTimeline(testId);
- expect(data).toBeFalsy();
- result.current.initializeTimeline({
- id: testId,
- filterManager: mockFilterManager,
- });
- data = result.current.isManagedTimeline(testId);
- expect(data).toBeTruthy();
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx
deleted file mode 100644
index 1f215ee8f2141..0000000000000
--- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx
+++ /dev/null
@@ -1,212 +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 React, { createContext, useCallback, useContext, useReducer } from 'react';
-import { noop } from 'lodash/fp';
-
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager';
-import { SubsetTimelineModel } from '../../store/timeline/model';
-import * as i18n from '../../../common/components/events_viewer/translations';
-import * as i18nF from '../timeline/footer/translations';
-import { timelineDefaults as timelineDefaultModel } from '../../store/timeline/defaults';
-
-interface ManageTimelineInit {
- documentType?: string;
- defaultModel?: SubsetTimelineModel;
- filterManager?: FilterManager;
- footerText?: string;
- id: string;
- loadingText?: string;
- selectAll?: boolean;
- queryFields?: string[];
- title?: string;
- unit?: (totalCount: number) => string;
-}
-
-interface ManageTimeline {
- documentType: string;
- defaultModel: SubsetTimelineModel;
- filterManager?: FilterManager;
- footerText: string;
- id: string;
- isLoading: boolean;
- loadingText: string;
- queryFields: string[];
- selectAll: boolean;
- title: string;
- unit: (totalCount: number) => string;
-}
-
-interface ManageTimelineById {
- [id: string]: ManageTimeline;
-}
-const initManageTimeline: ManageTimelineById = {};
-type ActionManageTimeline =
- | {
- type: 'INITIALIZE_TIMELINE';
- id: string;
- payload: ManageTimelineInit;
- }
- | {
- type: 'SET_IS_LOADING';
- id: string;
- payload: boolean;
- }
- | {
- type: 'SET_SELECT_ALL';
- id: string;
- payload: boolean;
- };
-
-export const getTimelineDefaults = (id: string) => ({
- defaultModel: timelineDefaultModel,
- loadingText: i18n.LOADING_EVENTS,
- footerText: i18nF.TOTAL_COUNT_OF_EVENTS,
- documentType: i18nF.TOTAL_COUNT_OF_EVENTS,
- selectAll: false,
- id,
- isLoading: false,
- queryFields: [],
- title: i18n.EVENTS,
- unit: (n: number) => i18n.UNIT(n),
-});
-const reducerManageTimeline = (
- state: ManageTimelineById,
- action: ActionManageTimeline
-): ManageTimelineById => {
- switch (action.type) {
- case 'INITIALIZE_TIMELINE':
- return {
- ...state,
- [action.id]: {
- ...getTimelineDefaults(action.id),
- ...state[action.id],
- ...action.payload,
- },
- } as ManageTimelineById;
- case 'SET_SELECT_ALL':
- return {
- ...state,
- [action.id]: {
- ...state[action.id],
- selectAll: action.payload,
- },
- } as ManageTimelineById;
-
- case 'SET_IS_LOADING':
- return {
- ...state,
- [action.id]: {
- ...state[action.id],
- isLoading: action.payload,
- },
- } as ManageTimelineById;
- default:
- return state;
- }
-};
-
-export interface UseTimelineManager {
- getManageTimelineById: (id: string) => ManageTimeline;
- getTimelineFilterManager: (id: string) => FilterManager | undefined;
- initializeTimeline: (newTimeline: ManageTimelineInit) => void;
- isManagedTimeline: (id: string) => boolean;
- setIsTimelineLoading: (isLoadingArgs: { id: string; isLoading: boolean }) => void;
- setSelectAll: (selectAllArgs: { id: string; selectAll: boolean }) => void;
-}
-
-export const useTimelineManager = (
- manageTimelineForTesting?: ManageTimelineById
-): UseTimelineManager => {
- const [state, dispatch] = useReducer<
- (state: ManageTimelineById, action: ActionManageTimeline) => ManageTimelineById
- >(reducerManageTimeline, manageTimelineForTesting ?? initManageTimeline);
-
- const initializeTimeline = useCallback((newTimeline: ManageTimelineInit) => {
- dispatch({
- type: 'INITIALIZE_TIMELINE',
- id: newTimeline.id,
- payload: newTimeline,
- });
- }, []);
-
- const setIsTimelineLoading = useCallback(
- ({ id, isLoading }: { id: string; isLoading: boolean }) => {
- dispatch({
- type: 'SET_IS_LOADING',
- id,
- payload: isLoading,
- });
- },
- []
- );
-
- const setSelectAll = useCallback(({ id, selectAll }: { id: string; selectAll: boolean }) => {
- dispatch({
- type: 'SET_SELECT_ALL',
- id,
- payload: selectAll,
- });
- }, []);
-
- const getTimelineFilterManager = useCallback(
- (id: string): FilterManager | undefined => state[id]?.filterManager,
- [state]
- );
- const getManageTimelineById = useCallback(
- (id: string): ManageTimeline => {
- if (state[id] != null) {
- return state[id];
- }
- initializeTimeline({ id });
- return getTimelineDefaults(id);
- },
- [initializeTimeline, state]
- );
- const isManagedTimeline = useCallback((id: string): boolean => state[id] != null, [state]);
-
- return {
- getManageTimelineById,
- getTimelineFilterManager,
- initializeTimeline,
- isManagedTimeline,
- setIsTimelineLoading,
- setSelectAll,
- };
-};
-
-const init = {
- getManageTimelineById: (id: string) => getTimelineDefaults(id),
- getTimelineFilterManager: () => undefined,
- initializeTimeline: () => noop,
- isManagedTimeline: () => false,
- setIsTimelineLoading: () => noop,
- setSelectAll: () => noop,
-};
-
-const ManageTimelineContext = createContext(init);
-
-export const useManageTimeline = () => useContext(ManageTimelineContext);
-
-interface ManageGlobalTimelineProps {
- children: React.ReactNode;
- manageTimelineForTesting?: ManageTimelineById;
-}
-
-export const ManageGlobalTimeline = ({
- children,
- manageTimelineForTesting,
-}: ManageGlobalTimelineProps) => {
- const timelineManager = useTimelineManager(manageTimelineForTesting);
-
- return (
-
- {children}
-
- );
-};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx
index e2c8b8854504a..c73e372b4a71c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx
@@ -62,6 +62,8 @@ import {
} from '../../../network/components/source_destination/field_names';
import { useMountAppended } from '../../../common/utils/use_mount_appended';
+jest.mock('../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx
index 0544b00a79227..00d2a7b35483e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx
@@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiPanel, EuiScreenReaderOnly } from '@elastic/eui';
import React, { useState, useCallback } from 'react';
import styled from 'styled-components';
-import { getNotesContainerClassName } from '../../../../common/components/accessibility/helpers';
+import { getNotesContainerClassName } from '../../../../../../timelines/public';
import { AddNote } from '../add_note';
import { AssociateNote } from '../helpers';
import { NotePreviews, NotePreviewsContainer } from '../../open_timeline/note_previews';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts
index c06c3f076e097..c0fea1f210a8a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts
@@ -36,7 +36,6 @@ import {
formatTimelineResultToModel,
} from './helpers';
import { OpenTimelineResult, DispatchUpdateTimeline } from './types';
-import { KueryFilterQueryKind } from '../../../common/store';
import { Note } from '../../../common/lib/note';
import moment from 'moment';
import sinon from 'sinon';
@@ -45,6 +44,7 @@ import {
TimelineType,
TimelineStatus,
TimelineTabs,
+ KueryFilterQueryKind,
} from '../../../../common/types/timeline';
import {
mockTimeline as mockSelectedTimeline,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
index e45a1a117769b..03ac0b3d14342 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
@@ -13,6 +13,7 @@ import { Dispatch } from 'redux';
import deepMerge from 'deepmerge';
import {
+ ColumnHeaderOptions,
DataProviderType,
TimelineId,
TimelineStatus,
@@ -37,7 +38,7 @@ import {
addTimeline as dispatchAddTimeline,
addNote as dispatchAddGlobalTimelineNote,
} from '../../../timelines/store/timeline/actions';
-import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model';
+import { TimelineModel } from '../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx
index 9887563c0fef6..2daebdf37e77f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx
@@ -16,41 +16,28 @@ import {
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
-import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
+import {
+ HeaderActionProps,
+ SortDirection,
+ TimelineId,
+ TimelineTabs,
+} from '../../../../../../common/types/timeline';
import { EXIT_FULL_SCREEN } from '../../../../../common/components/exit_full_screen/translations';
import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../../../common/constants';
import {
useGlobalFullScreen,
useTimelineFullScreen,
} from '../../../../../common/containers/use_full_screen';
-import { BrowserFields } from '../../../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
-import { OnSelectAll } from '../../events';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
import { StatefulFieldsBrowser } from '../../../fields_browser';
import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser';
import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers';
import { EventsTh, EventsThContent } from '../../styles';
-import { Sort, SortDirection } from '../sort';
import { EventsSelect } from '../column_headers/events_select';
import * as i18n from '../column_headers/translations';
import { timelineActions } from '../../../../store/timeline';
import { isFullScreen } from '../column_headers';
-export interface HeaderActionProps {
- width: number;
- browserFields: BrowserFields;
- columnHeaders: ColumnHeaderOptions[];
- isEventViewer?: boolean;
- isSelectAllChecked: boolean;
- onSelectAll: OnSelectAll;
- showEventsSelect: boolean;
- showSelectAllCheckbox: boolean;
- sort: Sort[];
- tabType: TimelineTabs;
- timelineId: string;
-}
-
const SortingColumnsContainer = styled.div`
button {
color: ${({ theme }) => theme.eui.euiColorPrimary};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
index a186b324cc03a..82d593e80bc44 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
@@ -41,8 +41,6 @@ describe('Actions', () => {
eventId="abc"
loadingEventIds={[]}
onEventDetailsPanelOpened={jest.fn()}
- onPinEvent={jest.fn()}
- onUnPinEvent={jest.fn()}
onRowSelected={jest.fn()}
showNotes={false}
isEventPinned={false}
@@ -74,8 +72,6 @@ describe('Actions', () => {
toggleShowNotes={jest.fn()}
timelineId={'test'}
refetch={jest.fn()}
- onPinEvent={jest.fn()}
- onUnPinEvent={jest.fn()}
columnId={''}
index={2}
eventId="abc"
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
index 2053b9a0da942..0a3a1cd88accc 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
@@ -6,7 +6,9 @@
*/
import React, { useCallback, useMemo } from 'react';
+import { useDispatch } from 'react-redux';
import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
+import { noop } from 'lodash/fp';
import {
eventHasNotes,
getEventType,
@@ -22,45 +24,9 @@ import * as i18n from '../translations';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
import { AddToCaseAction } from '../../../../../cases/components/timeline_actions/add_to_case_action';
-import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
-import { timelineSelectors } from '../../../../store/timeline';
+import { TimelineId, ActionProps, OnPinEvent } from '../../../../../../common/types/timeline';
+import { timelineActions, timelineSelectors } from '../../../../store/timeline';
import { timelineDefaults } from '../../../../store/timeline/defaults';
-import { Ecs } from '../../../../../../common/ecs';
-import { inputsModel } from '../../../../../common/store';
-import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events';
-import { RowCellRender } from '../control_columns';
-
-interface Props {
- ariaRowindex: number;
- action?: RowCellRender;
- width?: number;
- columnId: string;
- columnValues: string;
- checked: boolean;
- onRowSelected: OnRowSelected;
- eventId: string;
- loadingEventIds: Readonly;
- onEventDetailsPanelOpened: () => void;
- showCheckboxes: boolean;
- data: TimelineNonEcsData[];
- ecsData: Ecs;
- index: number;
- eventIdToNoteIds: Readonly>;
- isEventPinned: boolean;
- isEventViewer?: boolean;
- onPinEvent: OnPinEvent;
- onUnPinEvent: OnUnPinEvent;
- refetch: inputsModel.Refetch;
- rowIndex: number;
- onRuleChange?: () => void;
- showNotes: boolean;
- tabType?: TimelineTabs;
- timelineId: string;
- toggleShowNotes: () => void;
-}
-
-export type ActionProps = Props;
const ActionsComponent: React.FC = ({
ariaRowindex,
@@ -75,9 +41,7 @@ const ActionsComponent: React.FC = ({
isEventViewer = false,
loadingEventIds,
onEventDetailsPanelOpened,
- onPinEvent,
onRowSelected,
- onUnPinEvent,
refetch,
onRuleChange,
showCheckboxes,
@@ -85,9 +49,20 @@ const ActionsComponent: React.FC = ({
timelineId,
toggleShowNotes,
}) => {
+ const dispatch = useDispatch();
const emptyNotes: string[] = [];
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
+ const onPinEvent: OnPinEvent = useCallback(
+ (evtId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId: evtId })),
+ [dispatch, timelineId]
+ );
+
+ const onUnPinEvent: OnPinEvent = useCallback(
+ (evtId) => dispatch(timelineActions.unPinEvent({ id: timelineId, eventId: evtId })),
+ [dispatch, timelineId]
+ );
+
const handleSelectEvent = useCallback(
(event: React.ChangeEvent) =>
onRowSelected({
@@ -99,7 +74,7 @@ const ActionsComponent: React.FC = ({
const handlePinClicked = useCallback(
() =>
getPinOnClick({
- allowUnpinning: !eventHasNotes(eventIdToNoteIds[eventId]),
+ allowUnpinning: eventIdToNoteIds ? !eventHasNotes(eventIdToNoteIds[eventId]) : true,
eventId,
onPinEvent,
onUnPinEvent,
@@ -164,12 +139,12 @@ const ActionsComponent: React.FC = ({
/>
)}
- {!isEventViewer && (
+ {!isEventViewer && toggleShowNotes && (
<>
@@ -177,7 +152,7 @@ const ActionsComponent: React.FC = ({
ariaLabel={i18n.PIN_EVENT_FOR_ROW({ ariaRowindex, columnValues, isEventPinned })}
key="pin-event"
onPinClicked={handlePinClicked}
- noteIds={eventIdToNoteIds[eventId] || emptyNotes}
+ noteIds={eventIdToNoteIds ? eventIdToNoteIds[eventId] || emptyNotes : emptyNotes}
eventIsPinned={isEventPinned}
timelineType={timelineType}
/>
@@ -200,7 +175,7 @@ const ActionsComponent: React.FC = ({
ecsRowData={ecsData}
timelineId={timelineId}
disabled={eventType !== 'signal' && !isEventContextMenuEnabled}
- refetch={refetch}
+ refetch={refetch ?? noop}
onRuleChange={onRuleChange}
/>
>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx
index f9eda55c237ae..8795255dfcfd4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx
@@ -8,7 +8,7 @@
import { EuiButtonIcon } from '@elastic/eui';
import React, { useCallback } from 'react';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../../../../common';
import { OnColumnRemoved } from '../../../events';
import { EventsHeadingExtra, EventsLoading } from '../../../styles';
import { Sort } from '../../sort';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx
index 3ab4d564391f3..74593e40ddf4c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx
@@ -12,16 +12,12 @@ import { Resizable, ResizeCallback } from 're-resizable';
import deepEqual from 'fast-deep-equal';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
+import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
-import { useDraggableKeyboardWrapper } from '../../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../constants';
-import {
- DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME,
- getDraggableFieldId,
-} from '../../../../../common/components/drag_and_drop/helpers';
-import { TimelineTabs } from '../../../../../../common/types/timeline';
+import { getDraggableFieldId } from '../../../../../common/components/drag_and_drop/helpers';
+import { ColumnHeaderOptions, TimelineTabs } from '../../../../../../common/types/timeline';
import { Direction } from '../../../../../../common/search_strategy';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
import { OnFilterChange } from '../../events';
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles';
@@ -31,6 +27,7 @@ import { Header } from './header';
import { timelineActions } from '../../../../store/timeline';
import * as i18n from './translations';
+import { useKibana } from '../../../../../common/lib/kibana';
const ContextMenu = styled(EuiContextMenu)`
width: 115px;
@@ -75,6 +72,7 @@ const ColumnHeaderComponent: React.FC = ({
const restoreFocus = useCallback(() => keyboardHandlerRef.current?.focus(), []);
const dispatch = useDispatch();
+ const { timelines } = useKibana().services;
const resizableSize = useMemo(
() => ({
width: header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH,
@@ -247,7 +245,7 @@ const ColumnHeaderComponent: React.FC = ({
setHoverActionsOwnFocus(true);
}, []);
- const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
+ const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({
closePopover: handleClosePopOverTrigger,
draggableId,
fieldName: header.id,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts
index fea65d0499a13..7eb98b7475952 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts
@@ -5,7 +5,8 @@
* 2.0.
*/
-import { ColumnHeaderOptions, ColumnHeaderType } from '../../../../store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../../../common';
+import { ColumnHeaderType } from '../../../../store/timeline/model';
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx
index bdf4cc42fa794..828b8d8701188 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx
@@ -8,9 +8,9 @@
import { noop } from 'lodash/fp';
import React from 'react';
+import { ColumnHeaderOptions } from '../../../../../../../common';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../constants';
import { OnFilterChange } from '../../../events';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
import { TextFilter } from '../text_filter';
interface Props {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx
index 484cb78417c2f..ffab38b64bef8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx
@@ -8,8 +8,8 @@
import { EuiToolTip } from '@elastic/eui';
import { noop } from 'lodash/fp';
import React from 'react';
+import { ColumnHeaderOptions } from '../../../../../../../common/types/timeline';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
import { TruncatableText } from '../../../../../../common/components/truncatable_text';
import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from '../../../styles';
import { Sort } from '../../sort';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts
index b52fa292413df..257b88944c14e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts
@@ -6,9 +6,8 @@
*/
import { Direction } from '../../../../../../../common/search_strategy';
-import { assertUnreachable } from '../../../../../../../common/utility_types';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
-import { Sort, SortDirection } from '../../sort';
+import { ColumnHeaderOptions, SortDirection } from '../../../../../../../common/types/timeline';
+import { Sort } from '../../sort';
interface GetNewSortDirectionOnClickParams {
clickedHeader: ColumnHeaderOptions;
@@ -35,7 +34,7 @@ export const getNextSortDirection = (currentSort: Sort): Direction => {
case 'none':
return Direction.desc;
default:
- return assertUnreachable(currentSort.sortDirection, 'Unhandled sort direction');
+ return Direction.desc;
}
};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx
index f2496484c25ea..4fa72fa5da424 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx
@@ -18,6 +18,7 @@ import { defaultHeaders } from '../default_headers';
import { HeaderComponent } from '.';
import { getNewSortDirectionOnClick, getNextSortDirection, getSortDirection } from './helpers';
import { Direction } from '../../../../../../../common/search_strategy';
+import { useDeepEqualSelector } from '../../../../../../common/hooks/use_selector';
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
@@ -30,6 +31,11 @@ jest.mock('react-redux', () => {
};
});
+jest.mock('../../../../../../common/hooks/use_selector', () => ({
+ useShallowEqualSelector: jest.fn(),
+ useDeepEqualSelector: jest.fn(),
+}));
+
const filteredColumnHeader: ColumnHeaderType = 'text-filter';
describe('Header', () => {
@@ -41,7 +47,11 @@ describe('Header', () => {
sortDirection: Direction.desc,
},
];
- const timelineId = 'fakeId';
+ const timelineId = 'test';
+
+ beforeEach(() => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: false });
+ });
test('renders correctly against snapshot', () => {
const wrapper = shallow(
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx
index ece28faedb951..60a241a340d99 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx
@@ -9,16 +9,18 @@ import { noop } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
-import { useShallowEqualSelector } from '../../../../../../common/hooks/use_selector';
-import { timelineActions } from '../../../../../store/timeline';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../../../../common';
+import {
+ useDeepEqualSelector,
+ useShallowEqualSelector,
+} from '../../../../../../common/hooks/use_selector';
+import { timelineActions, timelineSelectors } from '../../../../../store/timeline';
import { OnFilterChange } from '../../../events';
import { Sort } from '../../sort';
import { Actions } from '../actions';
import { Filter } from '../filter';
import { getNewSortDirectionOnClick } from './helpers';
import { HeaderContent } from './header_content';
-import { useManageTimeline } from '../../../../manage_timeline';
import { isEqlOnSelector } from './selectors';
interface Props {
@@ -80,12 +82,10 @@ export const HeaderComponent: React.FC = ({
[dispatch, timelineId]
);
- const { getManageTimelineById } = useManageTimeline();
-
- const isLoading = useMemo(() => getManageTimelineById(timelineId).isLoading, [
- getManageTimelineById,
- timelineId,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { isLoading } = useDeepEqualSelector(
+ (state) => getManageTimeline(state, timelineId) || { isLoading: false }
+ );
const showSortingCapability = !isEqlOn && !(header.subType && header.subType.nested);
return (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx
index 5b5a8b10591d4..b33e47dd27b96 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx
@@ -9,9 +9,8 @@ import { mount, shallow } from 'enzyme';
import { cloneDeep } from 'lodash/fp';
import React from 'react';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../../../../common';
import { defaultHeaders } from '../../../../../../common/mock';
-
import { HeaderToolTipContent } from '.';
describe('HeaderToolTipContent', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx
index f4e7b6459bd14..0ae8dbb537fb8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx
@@ -10,7 +10,7 @@ import { isEmpty } from 'lodash/fp';
import React from 'react';
import styled from 'styled-components';
-import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model';
+import { ColumnHeaderOptions } from '../../../../../../../common';
import { getIconFromType } from '../../../../../../common/components/event_details/helpers';
import * as i18n from '../translations';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
index d19c5689ab049..c49d088d6241d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts
@@ -6,9 +6,9 @@
*/
import { get } from 'lodash/fp';
+import { ColumnHeaderOptions } from '../../../../../../common';
import { BrowserFields } from '../../../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
import {
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_DATE_COLUMN_MIN_WIDTH,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx
index 41f9db3f1c25b..378f7fce250fe 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx
@@ -24,6 +24,8 @@ import { Direction } from '../../../../../../common/search_strategy';
import { defaultControlColumn } from '../control_columns';
import { testTrailingControlColumns } from '../../../../../common/mock/mock_timeline_control_columns';
+jest.mock('../../../../../common/lib/kibana');
+
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx
index 3b0b935bfcff4..25aefd513f806 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx
@@ -11,12 +11,17 @@ import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd';
import { DragEffects } from '../../../../../common/components/drag_and_drop/draggable_wrapper';
import { DraggableFieldBadge } from '../../../../../common/components/draggables/field_badge';
import { BrowserFields } from '../../../../../common/containers/source';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
import {
DRAG_TYPE_FIELD,
droppableTimelineColumnsPrefix,
} from '../../../../../common/components/drag_and_drop/helpers';
-import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
+import {
+ ColumnHeaderOptions,
+ ControlColumnProps,
+ HeaderActionProps,
+ TimelineId,
+ TimelineTabs,
+} from '../../../../../../common/types/timeline';
import { OnSelectAll } from '../../events';
import {
EventsTh,
@@ -27,8 +32,6 @@ import {
} from '../../styles';
import { Sort } from '../sort';
import { ColumnHeader } from './column_header';
-import { ControlColumnProps } from '../control_columns';
-import { HeaderActionProps } from '../actions/header_actions';
interface Props {
actionsColumnWidth: number;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx
index 8ef69697af1d0..e4f4c26417351 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx
@@ -5,48 +5,9 @@
* 2.0.
*/
-import { ComponentType, JSXElementConstructor } from 'react';
-import { EuiDataGridControlColumn, EuiDataGridCellValueElementProps } from '@elastic/eui';
-import { OnRowSelected } from '../../events';
-import { ActionProps, Actions } from '../actions';
-import { HeaderActions, HeaderActionProps } from '../actions/header_actions';
-
-export type GenericActionRowCellRenderProps = Pick<
- EuiDataGridCellValueElementProps,
- 'rowIndex' | 'columnId'
->;
-
-export type HeaderCellRender = ComponentType | ComponentType;
-export type RowCellRender =
- | JSXElementConstructor
- | ((props: GenericActionRowCellRenderProps) => JSX.Element)
- | JSXElementConstructor
- | ((props: ActionProps) => JSX.Element);
-
-interface AdditionalControlColumnProps {
- ariaRowindex: number;
- actionsColumnWidth: number;
- columnValues: string;
- checked: boolean;
- onRowSelected: OnRowSelected;
- eventId: string;
- id: string;
- columnId: string;
- loadingEventIds: Readonly;
- onEventDetailsPanelOpened: () => void;
- showCheckboxes: boolean;
- // Override these type definitions to support either a generic custom component or the one used in security_solution today.
- headerCellRender: HeaderCellRender;
- rowCellRender: RowCellRender;
- // If not provided, calculated dynamically
- width?: number;
-}
-
-export type ControlColumnProps = Omit<
- EuiDataGridControlColumn,
- keyof AdditionalControlColumnProps
-> &
- Partial;
+import { ControlColumnProps } from '../../../../../../common/types/timeline';
+import { Actions } from '../actions';
+import { HeaderActions } from '../actions/header_actions';
export const defaultControlColumn: ControlColumnProps = {
id: 'default-timeline-control-column',
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx
index ae6307c0a294b..ecacbc51e395a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx
@@ -36,11 +36,9 @@ describe('Columns', () => {
timelineId="test"
columnValues={'abc def'}
showCheckboxes={false}
- onPinEvent={jest.fn()}
selectedEventIds={{}}
loadingEventIds={[]}
onEventDetailsPanelOpened={jest.fn()}
- onUnPinEvent={jest.fn()}
onRowSelected={jest.fn()}
showNotes={false}
isEventPinned={false}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx
index ecabc3eae51c4..11bf88977fe61 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx
@@ -8,17 +8,20 @@
import { EuiScreenReaderOnly } from '@elastic/eui';
import React, { useMemo } from 'react';
import { getOr } from 'lodash/fp';
+import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
-import { CellValueElementProps } from '../../cell_rendering';
-import { ControlColumnProps, RowCellRender } from '../control_columns';
-import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '../../../../../common/components/drag_and_drop/helpers';
import { Ecs } from '../../../../../../common/ecs';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { TimelineTabs } from '../../../../../../common/types/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
+import {
+ ColumnHeaderOptions,
+ CellValueElementProps,
+ ActionProps,
+ ControlColumnProps,
+ TimelineTabs,
+ RowCellRender,
+} from '../../../../../../common/types/timeline';
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
-import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events';
-import { ActionProps } from '../actions';
+import { OnRowSelected } from '../../events';
import { inputsModel } from '../../../../../common/store';
import {
EventsTd,
@@ -60,9 +63,7 @@ interface DataDrivenColumnProps {
loadingEventIds: Readonly;
notesCount: number;
onEventDetailsPanelOpened: () => void;
- onPinEvent: OnPinEvent;
onRowSelected: OnRowSelected;
- onUnPinEvent: OnUnPinEvent;
refetch: inputsModel.Refetch;
onRuleChange?: () => void;
hasRowRenderers: boolean;
@@ -137,9 +138,7 @@ const TgridActionTdCell = ({
loadingEventIds,
notesCount,
onEventDetailsPanelOpened,
- onPinEvent,
onRowSelected,
- onUnPinEvent,
refetch,
rowIndex,
hasRowRenderers,
@@ -193,9 +192,7 @@ const TgridActionTdCell = ({
isEventViewer={isEventViewer}
loadingEventIds={loadingEventIds}
onEventDetailsPanelOpened={onEventDetailsPanelOpened}
- onPinEvent={onPinEvent}
onRowSelected={onRowSelected}
- onUnPinEvent={onUnPinEvent}
refetch={refetch}
rowIndex={rowIndex}
onRuleChange={onRuleChange}
@@ -292,9 +289,7 @@ export const DataDrivenColumns = React.memo(
loadingEventIds,
notesCount,
onEventDetailsPanelOpened,
- onPinEvent,
onRowSelected,
- onUnPinEvent,
refetch,
hasRowRenderers,
onRuleChange,
@@ -345,8 +340,6 @@ export const DataDrivenColumns = React.memo(
isEventPinned={isEventPinned}
isEventViewer={isEventViewer}
notesCount={notesCount}
- onPinEvent={onPinEvent}
- onUnPinEvent={onUnPinEvent}
refetch={refetch}
hasRowRenderers={hasRowRenderers}
onRuleChange={onRuleChange}
@@ -365,7 +358,6 @@ export const DataDrivenColumns = React.memo(
data,
ecsData,
onRowSelected,
- onPinEvent,
isEventPinned,
isEventViewer,
actionsColumnWidth,
@@ -378,7 +370,6 @@ export const DataDrivenColumns = React.memo(
notesCount,
onEventDetailsPanelOpened,
onRuleChange,
- onUnPinEvent,
refetch,
selectedEventIds,
showCheckboxes,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx
index 3c75bc7fb2649..3e22cba208ca2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx
@@ -9,11 +9,13 @@ import { mount } from 'enzyme';
import { cloneDeep } from 'lodash/fp';
import React, { useEffect } from 'react';
-import { CellValueElementProps } from '../../cell_rendering';
import { defaultHeaders, mockTimelineData } from '../../../../../common/mock';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { TimelineTabs } from '../../../../../../common/types/timeline';
-import { ColumnHeaderOptions } from '../../../../store/timeline/model';
+import {
+ ColumnHeaderOptions,
+ CellValueElementProps,
+ TimelineTabs,
+} from '../../../../../../common/types/timeline';
import { StatefulCell } from './stateful_cell';
import { getMappedNonEcsValue } from '.';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx
index a5f8336cc7997..7931e0739aa68 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx
@@ -7,10 +7,12 @@
import React, { HTMLAttributes, useState } from 'react';
-import { CellValueElementProps } from '../../cell_rendering';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { TimelineTabs } from '../../../../../../common/types/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
+import {
+ ColumnHeaderOptions,
+ CellValueElementProps,
+ TimelineTabs,
+} from '../../../../../../common/types/timeline';
export interface CommonProps {
className?: string;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx
index e56171aae003c..17f231c0fdad9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx
@@ -60,9 +60,7 @@ describe('EventColumnView', () => {
loadingEventIds: [],
notesCount: 0,
onEventDetailsPanelOpened: jest.fn(),
- onPinEvent: jest.fn(),
onRowSelected: jest.fn(),
- onUnPinEvent: jest.fn(),
refetch: jest.fn(),
renderCellValue: DefaultCellRenderer,
selectedEventIds: {},
@@ -120,16 +118,6 @@ describe('EventColumnView', () => {
expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false);
});
- test('it invokes onPinClicked when the button for pinning events is clicked', () => {
- const wrapper = mount( , { wrappingComponent: TestProviders });
-
- expect(props.onPinEvent).not.toHaveBeenCalled();
-
- wrapper.find('[data-test-subj="pin"]').first().simulate('click');
-
- expect(props.onPinEvent).toHaveBeenCalled();
- });
-
test('it render AddToCaseAction if timelineId === TimelineId.detectionsPage', () => {
const wrapper = mount( , {
wrappingComponent: TestProviders,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
index 5dc718f90a91a..298ce252ba925 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
@@ -7,16 +7,19 @@
import React, { useMemo } from 'react';
-import { CellValueElementProps } from '../../cell_rendering';
-import { ControlColumnProps, RowCellRender } from '../control_columns';
import { Ecs } from '../../../../../../common/ecs';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
-import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events';
+import { OnRowSelected } from '../../events';
import { EventsTrData, EventsTdGroupActions } from '../../styles';
import { DataDrivenColumns, getMappedNonEcsValue } from '../data_driven_columns';
import { inputsModel } from '../../../../../common/store';
-import { TimelineTabs } from '../../../../../../common/types/timeline';
+import {
+ ColumnHeaderOptions,
+ CellValueElementProps,
+ ControlColumnProps,
+ RowCellRender,
+ TimelineTabs,
+} from '../../../../../../common/types/timeline';
interface Props {
id: string;
@@ -31,9 +34,7 @@ interface Props {
loadingEventIds: Readonly;
notesCount: number;
onEventDetailsPanelOpened: () => void;
- onPinEvent: OnPinEvent;
onRowSelected: OnRowSelected;
- onUnPinEvent: OnUnPinEvent;
refetch: inputsModel.Refetch;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
onRuleChange?: () => void;
@@ -62,9 +63,7 @@ export const EventColumnView = React.memo(
loadingEventIds,
notesCount,
onEventDetailsPanelOpened,
- onPinEvent,
onRowSelected,
- onUnPinEvent,
refetch,
hasRowRenderers,
onRuleChange,
@@ -134,10 +133,8 @@ export const EventColumnView = React.memo(
eventIdToNoteIds={eventIdToNoteIds}
isEventPinned={isEventPinned}
isEventViewer={isEventViewer}
- onPinEvent={onPinEvent}
- onUnPinEvent={onUnPinEvent}
- refetch={refetch}
onRuleChange={onRuleChange}
+ refetch={refetch}
showNotes={showNotes}
tabType={tabType}
timelineId={timelineId}
@@ -161,10 +158,8 @@ export const EventColumnView = React.memo(
leadingControlColumns,
loadingEventIds,
onEventDetailsPanelOpened,
- onPinEvent,
onRowSelected,
onRuleChange,
- onUnPinEvent,
refetch,
selectedEventIds,
showCheckboxes,
@@ -201,8 +196,6 @@ export const EventColumnView = React.memo(
eventIdToNoteIds={eventIdToNoteIds}
isEventPinned={isEventPinned}
isEventViewer={isEventViewer}
- onPinEvent={onPinEvent}
- onUnPinEvent={onUnPinEvent}
refetch={refetch}
onRuleChange={onRuleChange}
selectedEventIds={selectedEventIds}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx
index c3097ad68aba1..c09de87c87f32 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx
@@ -8,19 +8,21 @@
import React from 'react';
import { isEmpty } from 'lodash';
-import { CellValueElementProps } from '../../cell_rendering';
-import { ControlColumnProps } from '../control_columns';
import { inputsModel } from '../../../../../common/store';
import { BrowserFields } from '../../../../../common/containers/source';
import {
TimelineItem,
TimelineNonEcsData,
} from '../../../../../../common/search_strategy/timeline';
-import { TimelineTabs } from '../../../../../../common/types/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
+import {
+ ColumnHeaderOptions,
+ CellValueElementProps,
+ ControlColumnProps,
+ RowRenderer,
+ TimelineTabs,
+} from '../../../../../../common/types/timeline';
import { OnRowSelected } from '../../events';
import { EventsTbody } from '../../styles';
-import { RowRenderer } from '../renderers/row_renderer';
import { StatefulEvent } from './stateful_event';
import { eventIsPinned } from '../helpers';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
index 701dc549467e9..b8840a75cc9b4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
@@ -8,10 +8,12 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
-import { CellValueElementProps } from '../../cell_rendering';
-import { ControlColumnProps } from '../control_columns';
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
import {
+ ColumnHeaderOptions,
+ CellValueElementProps,
+ ControlColumnProps,
+ RowRenderer,
TimelineExpandedDetailType,
TimelineId,
TimelineTabs,
@@ -21,11 +23,9 @@ import {
TimelineItem,
TimelineNonEcsData,
} from '../../../../../../common/search_strategy/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
-import { OnPinEvent, OnRowSelected } from '../../events';
+import { OnRowSelected } from '../../events';
import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers';
import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles';
-import { RowRenderer } from '../renderers/row_renderer';
import { isEventBuildingBlockType, getEventType, isEvenEqlSequence } from '../helpers';
import { NoteCards } from '../../../notes/note_cards';
import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context';
@@ -176,16 +176,6 @@ const StatefulEventComponent: React.FC = ({
});
}, [event]);
- const onPinEvent: OnPinEvent = useCallback(
- (eventId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId })),
- [dispatch, timelineId]
- );
-
- const onUnPinEvent: OnPinEvent = useCallback(
- (eventId) => dispatch(timelineActions.unPinEvent({ id: timelineId, eventId })),
- [dispatch, timelineId]
- );
-
const handleOnEventDetailPanelOpened = useCallback(() => {
const eventId = event._id;
const indexName = event._index!;
@@ -215,10 +205,10 @@ const StatefulEventComponent: React.FC = ({
(noteId: string) => {
dispatch(timelineActions.addNoteToEvent({ eventId: event._id, id: timelineId, noteId }));
if (!isEventPinned) {
- onPinEvent(event._id); // pin the event, because it has notes
+ dispatch(timelineActions.pinEvent({ id: timelineId, eventId: event._id }));
}
},
- [dispatch, event, isEventPinned, onPinEvent, timelineId]
+ [dispatch, event, isEventPinned, timelineId]
);
const RowRendererContent = useMemo(
@@ -273,9 +263,7 @@ const StatefulEventComponent: React.FC = ({
loadingEventIds={loadingEventIds}
notesCount={notes.length}
onEventDetailsPanelOpened={handleOnEventDetailPanelOpened}
- onPinEvent={onPinEvent}
onRowSelected={onRowSelected}
- onUnPinEvent={onUnPinEvent}
refetch={refetch}
renderCellValue={renderCellValue}
onRuleChange={onRuleChange}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx
index 10a25538c1ba3..19abd6841e7e8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx
@@ -9,15 +9,15 @@ import { noop } from 'lodash/fp';
import { EuiFocusTrap, EuiOutsideClickDetector, EuiScreenReaderOnly } from '@elastic/eui';
import React, { useMemo } from 'react';
-import { BrowserFields } from '../../../../../../common/containers/source';
import {
ARIA_COLINDEX_ATTRIBUTE,
ARIA_ROWINDEX_ATTRIBUTE,
getRowRendererClassName,
-} from '../../../../../../common/components/accessibility/helpers';
+} from '../../../../../../../../timelines/public';
+import { RowRenderer } from '../../../../../../../common';
+import { BrowserFields } from '../../../../../../common/containers/source';
import { TimelineItem } from '../../../../../../../common/search_strategy/timeline';
import { getRowRenderer } from '../../renderers/get_row_renderer';
-import { RowRenderer } from '../../renderers/row_renderer';
import { useStatefulEventFocus } from '../use_stateful_event_focus';
import * as i18n from '../translations';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/use_stateful_event_focus/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/use_stateful_event_focus/index.tsx
index 5f3c4dac8b73d..4e8fd7dc48968 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/use_stateful_event_focus/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/use_stateful_event_focus/index.tsx
@@ -13,7 +13,7 @@ import {
isEscape,
focusColumn,
OnColumnFocused,
-} from '../../../../../../common/components/accessibility/helpers';
+} from '../../../../../../../../timelines/public';
type FocusOwnership = 'not-owned' | 'owned';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
index 61601c3921445..19059b5fb4599 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
@@ -23,6 +23,8 @@ import { timelineActions } from '../../../store/timeline';
import { TimelineTabs } from '../../../../../common/types/timeline';
import { defaultRowRenderers } from './renderers';
+jest.mock('../../../../common/lib/kibana');
+
const mockSort: Sort[] = [
{
columnId: '@timestamp',
@@ -255,7 +257,7 @@ describe('Body', () => {
tabType: 'query',
timelineId: 'timeline-test',
},
- type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL',
+ type: 'x-pack/timelines/t-grid/TOGGLE_DETAIL_PANEL',
});
});
@@ -279,7 +281,7 @@ describe('Body', () => {
tabType: 'pinned',
timelineId: 'timeline-test',
},
- type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL',
+ type: 'x-pack/timelines/t-grid/TOGGLE_DETAIL_PANEL',
});
});
@@ -303,7 +305,7 @@ describe('Body', () => {
tabType: 'notes',
timelineId: 'timeline-test',
},
- type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL',
+ type: 'x-pack/timelines/t-grid/TOGGLE_DETAIL_PANEL',
});
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
index 64f61232377e8..fc8bf2086471c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
@@ -11,21 +11,26 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { connect, ConnectedProps } from 'react-redux';
import deepEqual from 'fast-deep-equal';
-import { CellValueElementProps } from '../cell_rendering';
-import { DEFAULT_COLUMN_MIN_WIDTH } from './constants';
-import { ControlColumnProps } from './control_columns';
-import { RowRendererId, TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
import {
FIRST_ARIA_INDEX,
ARIA_COLINDEX_ATTRIBUTE,
ARIA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
-} from '../../../../common/components/accessibility/helpers';
+} from '../../../../../../timelines/public';
+import { CellValueElementProps } from '../cell_rendering';
+import { DEFAULT_COLUMN_MIN_WIDTH } from './constants';
+import {
+ ColumnHeaderOptions,
+ ControlColumnProps,
+ RowRendererId,
+ RowRenderer,
+ TimelineId,
+ TimelineTabs,
+} from '../../../../../common/types/timeline';
import { BrowserFields } from '../../../../common/containers/source';
import { TimelineItem } from '../../../../../common/search_strategy/timeline';
import { inputsModel, State } from '../../../../common/store';
-import { useManageTimeline } from '../../manage_timeline';
-import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model';
+import { TimelineModel } from '../../../store/timeline/model';
import { timelineDefaults } from '../../../store/timeline/defaults';
import { timelineActions, timelineSelectors } from '../../../store/timeline';
import { OnRowSelected, OnSelectAll } from '../events';
@@ -33,11 +38,11 @@ import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helper
import { getEventIdToDataMapping } from './helpers';
import { Sort } from './sort';
import { plainRowRenderer } from './renderers/plain_row_renderer';
-import { RowRenderer } from './renderers/row_renderer';
import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles';
import { ColumnHeaders } from './column_headers';
import { Events } from './events';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
interface OwnProps {
activePage: number;
@@ -99,11 +104,10 @@ export const BodyComponent = React.memo(
trailingControlColumns = [],
}) => {
const containerRef = useRef(null);
- const { getManageTimelineById } = useManageTimeline();
- const { queryFields, selectAll } = useMemo(() => getManageTimelineById(id), [
- getManageTimelineById,
- id,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { queryFields, selectAll } = useDeepEqualSelector((state) =>
+ getManageTimeline(state, id)
+ );
const onRowSelected: OnRowSelected = useCallback(
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx
index 21c44cb26e2e5..d5ec8b6f94862 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx
@@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
import { TestProviders } from '../../../../../common/mock';
import { ArgsComponent } from './args';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx
index f45c049ca137a..2a5764e53756a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx
@@ -15,6 +15,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock';
import { AuditdGenericDetails, AuditdGenericLine } from './generic_details';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx
index 51676c067cd79..009ffecf28f74 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx
@@ -15,6 +15,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock';
import { AuditdGenericFileDetails, AuditdGenericFileLine } from './generic_file_details';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx
index 31fea6fa25e65..74a5ff472b581 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx
@@ -9,17 +9,19 @@ import { shallow } from 'enzyme';
import { cloneDeep } from 'lodash/fp';
import React from 'react';
+import { RowRenderer } from '../../../../../../../common';
import { BrowserFields } from '../../../../../../common/containers/source';
import { mockBrowserFields } from '../../../../../../common/containers/source/mock';
import { Ecs } from '../../../../../../../common/ecs';
import { mockTimelineData, TestProviders } from '../../../../../../common/mock';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
-import { RowRenderer } from '../row_renderer';
import {
createGenericAuditRowRenderer,
createGenericFileRowRenderer,
} from './generic_row_renderer';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx
index 9133e500162bc..765bfd3d21351 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx
@@ -11,9 +11,9 @@ import { IconType } from '@elastic/eui';
import { get } from 'lodash/fp';
import React from 'react';
-import { RowRendererId } from '../../../../../../../common/types/timeline';
+import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline';
-import { RowRenderer, RowRendererContainer } from '../row_renderer';
+import { RowRendererContainer } from '../row_renderer';
import { AuditdGenericDetails } from './generic_details';
import { AuditdGenericFileDetails } from './generic_file_details';
import * as i18n from './translations';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx
index 24b9f8d40eb17..d6037a310dc7e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx
@@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../../common/mock';
import { PrimarySecondaryUserInfo, nilOrUnSet } from './primary_secondary_user_info';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx
index 22cd8446a51c0..fa6eda6bce37d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx
@@ -14,6 +14,8 @@ import { TestProviders } from '../../../../../../common/mock';
import { SessionUserHostWorkingDir } from './session_user_host_working_dir';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx
index 8b4a9f72b1a45..c7da6f758766e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx
@@ -14,6 +14,8 @@ import { useMountAppended } from '../../../../../../common/utils/use_mount_appen
import { Bytes } from '.';
+jest.mock('../../../../../../common/lib/kibana');
+
describe('Bytes', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts
index cb670b53a9679..65bb67458ab2a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts
@@ -6,8 +6,8 @@
*/
import type React from 'react';
+import { ColumnHeaderOptions } from '../../../../../../common';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
export interface ColumnRenderer {
isInstance: (columnName: string, data: TimelineNonEcsData[]) => boolean;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx
index 7f580642130fe..872ca017d7f7d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx
@@ -12,6 +12,8 @@ import { TestProviders } from '../../../../../../common/mock';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
import { ThreatMatchRowProps, ThreatMatchRowView } from './threat_match_row';
+jest.mock('../../../../../../common/lib/kibana');
+
describe('ThreatMatchRowView', () => {
const mount = useMountAppended();
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx
index 2a7e8ce02d79f..16426bf74aba7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx
@@ -5,8 +5,7 @@
* 2.0.
*/
-import { RowRendererId } from '../../../../../../../common/types/timeline';
-import { RowRenderer } from '../row_renderer';
+import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline';
import { hasThreatMatchValue } from './helpers';
import { ThreatMatchRows } from './threat_match_rows';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx
index cc34f9e63b5e2..f6feb6dd1b126 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx
@@ -10,9 +10,10 @@ import { get } from 'lodash';
import React, { Fragment } from 'react';
import styled from 'styled-components';
+import { RowRenderer } from '../../../../../../../common';
import { Fields } from '../../../../../../../common/search_strategy';
import { ID_FIELD_NAME } from '../../../../../../common/components/event_details/event_id';
-import { RowRenderer, RowRendererContainer } from '../row_renderer';
+import { RowRendererContainer } from '../row_renderer';
import { ThreatMatchRow } from './threat_match_row';
const SpacedContainer = styled.div`
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx
index d3e870aa92ef0..9e6c5b819a20b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx
@@ -15,6 +15,8 @@ import { useMountAppended } from '../../../../../../common/utils/use_mount_appen
import { DnsRequestEventDetails } from './dns_request_event_details';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx
index 2809b06c77469..5c0aecf5fbbc7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx
@@ -12,6 +12,8 @@ import '../../../../../../common/mock/match_media';
import { DnsRequestEventDetailsLine } from './dns_request_event_details_line';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx
index 034ade75ef2c0..5144705f26174 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx
@@ -18,6 +18,8 @@ import { getEmptyValue } from '../../../../../common/components/empty_value';
import { deleteItemIdx, findItem } from './helpers';
import { emptyColumnRenderer } from './empty_column_renderer';
+jest.mock('../../../../../common/lib/kibana');
+
describe('empty_column_renderer', () => {
let mockDatum: TimelineNonEcsData[];
const _id = mockTimelineData[0]._id;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx
index 400ccf47201ac..37873df7f4e7b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx
@@ -8,9 +8,8 @@
/* eslint-disable react/display-name */
import React from 'react';
-
+import { ColumnHeaderOptions } from '../../../../../../common';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
import {
DraggableWrapper,
DragEffects,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx
index c1df6d6eb48c8..613d66505601a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx
@@ -20,6 +20,8 @@ import { useMountAppended } from '../../../../../../common/utils/use_mount_appen
import { EndgameSecurityEventDetails } from './endgame_security_event_details';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx
index 5d08898789821..879862d06b250 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx
@@ -13,6 +13,8 @@ import '../../../../../../common/mock/match_media';
import { EndgameSecurityEventDetailsLine } from './endgame_security_event_details_line';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx
index a6f15a9f79f4e..1bf8d1a4a4f51 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx
@@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
import { ExitCodeDraggable } from './exit_code_draggable';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx
index d7274f0774fc5..cf3fce2c25c0b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx
@@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../common/mock';
import { FileDraggable } from './file_draggable';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.test.tsx
index e7e6274942bea..8ebd3ae8a67c2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.test.tsx
@@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
import { FileHash } from './file_hash';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx
index 8e54f13ec9cbf..852331aa021dd 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx
@@ -21,6 +21,8 @@ import { getColumnRenderer } from './get_column_renderer';
import { getValues, findItem, deleteItemIdx } from './helpers';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx
index 56dbc99d47c66..104550f138f16 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx
@@ -20,6 +20,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
import { defaultRowRenderers } from '.';
import { getRowRenderer } from './get_row_renderer';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts
index bfe60a14e042d..2d1be6ee7914a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts
@@ -5,8 +5,8 @@
* 2.0.
*/
+import { RowRenderer } from '../../../../../../common';
import { Ecs } from '../../../../../../common/ecs';
-import { RowRenderer } from './row_renderer';
export const getRowRenderer = (ecs: Ecs, rowRenderers: RowRenderer[]): RowRenderer | null =>
rowRenderers.find((rowRenderer) => rowRenderer.isInstance(ecs)) ?? null;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx
index 9412ecfd364ba..d650710b25cad 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx
@@ -13,6 +13,8 @@ import { mockTimelineData, TestProviders } from '../../../../../common/mock';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
import { HostWorkingDir } from './host_working_dir';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts
index 537a24bbfd953..911dcc8cd2e87 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts
@@ -5,12 +5,12 @@
* 2.0.
*/
+import { RowRenderer } from '../../../../../../common';
import { auditdRowRenderers } from './auditd/generic_row_renderer';
import { ColumnRenderer } from './column_renderer';
import { emptyColumnRenderer } from './empty_column_renderer';
import { netflowRowRenderer } from './netflow/netflow_row_renderer';
import { plainColumnRenderer } from './plain_column_renderer';
-import { RowRenderer } from './row_renderer';
import { suricataRowRenderer } from './suricata/suricata_row_renderer';
import { unknownColumnRenderer } from './unknown_column_renderer';
import { zeekRowRenderer } from './zeek/zeek_row_renderer';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx
index 72e3516827c8a..fc97624dbfc96 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx
@@ -26,6 +26,8 @@ export const justIdAndTimestamp: Ecs = {
timestamp: '2018-11-12T19:03:25.936Z',
};
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('../../../../../../common/components/link_to');
describe('netflowRowRenderer', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx
index 2605670ee8b38..35406dce6ff72 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx
@@ -11,7 +11,7 @@ import { get } from 'lodash/fp';
import React from 'react';
import styled from 'styled-components';
-import { RowRendererId } from '../../../../../../../common/types/timeline';
+import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline';
import { asArrayIfExists } from '../../../../../../common/lib/helpers';
import {
TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME,
@@ -63,7 +63,7 @@ import {
SOURCE_BYTES_FIELD_NAME,
SOURCE_PACKETS_FIELD_NAME,
} from '../../../../../../network/components/source_destination/source_destination_arrows';
-import { RowRenderer, RowRendererContainer } from '../row_renderer';
+import { RowRendererContainer } from '../row_renderer';
const Details = styled.div`
margin: 5px 0;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx
index 2402be88dea18..7c28747cc84ef 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx
@@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../common/mock';
import { ParentProcessDraggable } from './parent_process_draggable';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx
index a56acbe48685c..e970aaad026b1 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx
@@ -18,6 +18,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
import { plainColumnRenderer } from './plain_column_renderer';
import { getValues, deleteItemIdx, findItem } from './helpers';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx
index a2b7750d9bb59..77039ddc4a586 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx
@@ -8,8 +8,8 @@
import { head } from 'lodash/fp';
import React from 'react';
+import { ColumnHeaderOptions } from '../../../../../../common';
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
import { getEmptyTagValue } from '../../../../../common/components/empty_value';
import { ColumnRenderer } from './column_renderer';
import { FormattedFieldValue } from './formatted_field';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx
index 0b5afd579d08c..15620a7fc04b4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx
@@ -7,9 +7,7 @@
import React from 'react';
-import { RowRendererId } from '../../../../../../common/types/timeline';
-
-import { RowRenderer } from './row_renderer';
+import { RowRendererId, RowRenderer } from '../../../../../../common/types/timeline';
const PlainRowRenderer = () => <>>;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx
index 31a1745fa2a6d..6509808fb0c9f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx
@@ -13,6 +13,8 @@ import '../../../../../common/mock/match_media';
import { ProcessDraggable, ProcessDraggableWithNonExistentProcess } from './process_draggable';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx
index 9e90e061e94d5..7135f2a5fed6a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx
@@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
import { ProcessHash } from './process_hash';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.test.tsx
index f37adef7e73cb..e5bb91c532505 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.test.tsx
@@ -18,6 +18,8 @@ import { MODIFIED_REGISTRY_KEY } from '../system/translations';
import { RegistryEventDetails } from './registry_event_details';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.test.tsx
index 6be1529152523..d0287f2b010ae 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.test.tsx
@@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../../common/utils/use_mount_appen
import { RegistryEventDetailsLine } from './registry_event_details_line';
import { MODIFIED_REGISTRY_KEY } from '../system/translations';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx
index 679da28e622bf..9099f76b8305c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx
@@ -7,11 +7,7 @@
import React from 'react';
-import { BrowserFields } from '../../../../../common/containers/source';
-import type { RowRendererId } from '../../../../../../common/types/timeline';
-import { Ecs } from '../../../../../../common/ecs';
import { EventsTrSupplement } from '../../styles';
-
interface RowRendererContainerProps {
children: React.ReactNode;
}
@@ -22,17 +18,3 @@ export const RowRendererContainer = React.memo(({ chi
));
RowRendererContainer.displayName = 'RowRendererContainer';
-
-export interface RowRenderer {
- id: RowRendererId;
- isInstance: (data: Ecs) => boolean;
- renderRow: ({
- browserFields,
- data,
- timelineId,
- }: {
- browserFields: BrowserFields;
- data: Ecs;
- timelineId: string;
- }) => React.ReactNode;
-}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx
index 5960f43174b98..355077ee50066 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx
@@ -16,6 +16,8 @@ import { TestProviders } from '../../../../../../common/mock/test_providers';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
import { SuricataDetails } from './suricata_details';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx
index 098d6775cfaa4..998233b2278c9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx
@@ -18,6 +18,8 @@ import { TestProviders } from '../../../../../../common/mock/test_providers';
import { suricataRowRenderer } from './suricata_row_renderer';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx
index 5a68bc6fe28c8..aa482926bf007 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx
@@ -10,9 +10,9 @@
import { get } from 'lodash/fp';
import React from 'react';
-import { RowRendererId } from '../../../../../../../common/types/timeline';
+import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline';
-import { RowRenderer, RowRendererContainer } from '../row_renderer';
+import { RowRendererContainer } from '../row_renderer';
import { SuricataDetails } from './suricata_details';
export const suricataRowRenderer: RowRenderer = {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx
index 4a727e4e7bc27..b3911f9eded67 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx
@@ -18,6 +18,8 @@ import {
SURICATA_SIGNATURE_ID_FIELD_NAME,
} from './suricata_signature';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx
index 001b7f4b68bab..35872d0093f02 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx
@@ -15,6 +15,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock';
import { SystemGenericDetails, SystemGenericLine } from './generic_details';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx
index b660d823954ee..f5dc4c6fdf599 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx
@@ -16,6 +16,8 @@ import { mockEndgameCreationEvent } from '../../../../../../common/mock/mock_end
import { SystemGenericFileDetails, SystemGenericFileLine } from './generic_file_details';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx
index 8e8ce9cb2f988..6f5b225f0690b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx
@@ -67,7 +67,6 @@ import {
mockEndpointSecurityLogOffEvent,
} from '../../../../../../common/mock/mock_endgame_ecs_data';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
-import { RowRenderer } from '../row_renderer';
import {
createDnsRowRenderer,
createEndgameProcessRowRenderer,
@@ -82,6 +81,9 @@ import {
EndpointAlertCriteria,
} from './generic_row_renderer';
import * as i18n from './translations';
+import { RowRenderer } from '../../../../../../../common';
+
+jest.mock('../../../../../../common/lib/kibana');
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx
index 211fa9152dc8d..c6845d7d672d2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx
@@ -10,13 +10,13 @@
import { get } from 'lodash/fp';
import React from 'react';
-import { RowRendererId } from '../../../../../../../common/types/timeline';
+import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline';
import { DnsRequestEventDetails } from '../dns/dns_request_event_details';
import { EndgameSecurityEventDetails } from '../endgame/endgame_security_event_details';
import { isFileEvent, isNillEmptyOrNotFinite } from '../helpers';
import { RegistryEventDetails } from '../registry/registry_event_details';
-import { RowRenderer, RowRendererContainer } from '../row_renderer';
+import { RowRendererContainer } from '../row_renderer';
import { SystemGenericDetails } from './generic_details';
import { SystemGenericFileDetails } from './generic_file_details';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx
index ac1e4d6748dcd..be11955169bd7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx
@@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../../common/mock';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
import { Package } from './package';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx
index dfb9ae69ac2d4..7cff1166cd0de 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx
@@ -13,6 +13,8 @@ import '../../../../../common/mock/match_media';
import { UserHostWorkingDir } from './user_host_working_dir';
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
+jest.mock('../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx
index 04150163fb4d4..7f0ec8b7b0b79 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx
@@ -14,6 +14,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
import { ZeekDetails } from './zeek_details';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx
index 749e450b36ae4..6b154d4d32707 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx
@@ -17,6 +17,8 @@ import '../../../../../../common/mock/match_media';
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
import { zeekRowRenderer } from './zeek_row_renderer';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx
index 7a8d284d0ec1e..2b6311b8cae83 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx
@@ -10,9 +10,9 @@
import { get } from 'lodash/fp';
import React from 'react';
-import { RowRendererId } from '../../../../../../../common/types/timeline';
+import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline';
-import { RowRenderer, RowRendererContainer } from '../row_renderer';
+import { RowRendererContainer } from '../row_renderer';
import { ZeekDetails } from './zeek_details';
export const zeekRowRenderer: RowRenderer = {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx
index 61155331b1a4b..28034dac8f575 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx
@@ -28,6 +28,8 @@ import {
defaultStringRenderer,
} from './zeek_signature';
+jest.mock('../../../../../../common/lib/kibana');
+
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts
index e7c69b9229d70..bd05bf0656687 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts
@@ -5,15 +5,7 @@
* 2.0.
*/
-import { Direction } from '../../../../../../common/search_strategy';
-import { ColumnId } from '../column_id';
-
-/** Specifies a column's sort direction */
-export type SortDirection = 'none' | Direction;
+import { SortColumnTimeline } from '../../../../../../common/types/timeline';
/** Specifies which column the timeline is sorted on */
-export interface Sort {
- columnId: ColumnId;
- columnType: string;
- sortDirection: SortDirection;
-}
+export type Sort = SortColumnTimeline;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx
index 6af29793f9373..3e610abe79050 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx
@@ -11,8 +11,8 @@ import React from 'react';
import * as i18n from '../translations';
import { SortNumber } from './sort_number';
-import { SortDirection } from '.';
import { Direction } from '../../../../../../common/search_strategy';
+import { SortDirection } from '../../../../../../common/types/timeline';
enum SortDirectionIndicatorEnum {
SORT_UP = 'sortUp',
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx
index 5ac1dcf8805cf..06d8133a24f6e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx
@@ -17,6 +17,8 @@ import { mockBrowserFields } from '../../../../common/containers/source/mock';
import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../common/mock';
import { DefaultCellRenderer } from './default_cell_renderer';
+jest.mock('../../../../common/lib/kibana');
+
jest.mock('../body/renderers/get_column_renderer');
const getColumnRendererMock = getColumnRenderer as jest.Mock;
const mockImplementation = {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx
index 03e444e3a9afd..2848a850a5227 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx
@@ -5,16 +5,4 @@
* 2.0.
*/
-import { EuiDataGridCellValueElementProps } from '@elastic/eui';
-
-import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline';
-import { ColumnHeaderOptions } from '../../../store/timeline/model';
-
-/** The following props are provided to the function called by `renderCellValue` */
-export type CellValueElementProps = EuiDataGridCellValueElementProps & {
- data: TimelineNonEcsData[];
- eventId: string; // _id
- header: ColumnHeaderOptions;
- linkValues: string[] | undefined;
- timelineId: string;
-};
+export { CellValueElementProps } from '../../../../../common/types/timeline';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx
index 35595de646126..ef04c1177dcd6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx
@@ -11,11 +11,6 @@ import { TestProviders } from '../../../../common/mock/test_providers';
import { useMountAppended } from '../../../../common/utils/use_mount_appended';
import { DataProviders } from '.';
-import { ManageGlobalTimeline, getTimelineDefaults } from '../../manage_timeline';
-import { FilterManager } from '../../../../../../../../src/plugins/data/public/query/filter_manager';
-import { coreMock } from '../../../../../../../../src/core/public/mocks';
-
-const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
jest.mock('../../../../common/hooks/use_selector', () => {
const actual = jest.requireActual('../../../../common/hooks/use_selector');
@@ -25,7 +20,6 @@ jest.mock('../../../../common/hooks/use_selector', () => {
};
});
-const filterManager = new FilterManager(mockUiSettingsForFilterManager);
describe('DataProviders', () => {
const mount = useMountAppended();
@@ -33,17 +27,9 @@ describe('DataProviders', () => {
const dropMessage = ['Drop', 'query', 'build', 'here'];
test('renders correctly against snapshot', () => {
- const manageTimelineForTesting = {
- foo: {
- ...getTimelineDefaults('foo'),
- filterManager,
- },
- };
const wrapper = mount(
-
-
-
+
);
expect(wrapper.find(`[data-test-subj="dataProviders-container"]`)).toBeTruthy();
@@ -73,19 +59,10 @@ describe('DataProviders', () => {
});
describe('resizable drop target', () => {
- const manageTimelineForTesting = {
- foo: {
- ...getTimelineDefaults('test'),
- filterManager,
- },
- };
-
test('it may be resized vertically via a resize handle', () => {
const wrapper = mount(
-
-
-
+
);
@@ -98,9 +75,7 @@ describe('DataProviders', () => {
test('it never grows taller than one third (33%) of the view height', () => {
const wrapper = mount(
-
-
-
+
);
@@ -113,9 +88,7 @@ describe('DataProviders', () => {
test('it automatically displays scroll bars when the width or height of the data providers exceeds the drop target', () => {
const wrapper = mount(
-
-
-
+
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx
index bdc0327026488..f642ec35d4306 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx
@@ -9,19 +9,16 @@ import { rgba } from 'polished';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import uuid from 'uuid';
+import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper';
-import {
- droppableTimelineProvidersPrefix,
- IS_DRAGGING_CLASS_NAME,
-} from '../../../../common/components/drag_and_drop/helpers';
+import { droppableTimelineProvidersPrefix } from '../../../../common/components/drag_and_drop/helpers';
import { Empty } from './empty';
import { Providers } from './providers';
-import { useManageTimeline } from '../../manage_timeline';
import { timelineSelectors } from '../../../store/timeline';
import { timelineDefaults } from '../../../store/timeline/defaults';
@@ -89,11 +86,8 @@ const getDroppableId = (id: string): string =>
*/
export const DataProviders = React.memo(({ timelineId }) => {
const { browserFields } = useSourcererScope(SourcererScopeName.timeline);
- const { getManageTimelineById } = useManageTimeline();
- const isLoading = useMemo(() => getManageTimelineById(timelineId).isLoading, [
- getManageTimelineById,
- timelineId,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { isLoading } = useDeepEqualSelector((state) => getManageTimeline(state, timelineId));
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const dataProviders = useDeepEqualSelector(
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).dataProviders
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx
index a3693d5ba2001..e5e5ad5f010fc 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx
@@ -11,7 +11,10 @@ import { useDispatch } from 'react-redux';
import { TimelineType } from '../../../../../common/types/timeline';
import { BrowserFields } from '../../../../common/containers/source';
-import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
+import {
+ useDeepEqualSelector,
+ useShallowEqualSelector,
+} from '../../../../common/hooks/use_selector';
import { timelineSelectors } from '../../../store/timeline';
import { OnDataProviderEdited } from '../events';
@@ -19,7 +22,6 @@ import { ProviderBadge } from './provider_badge';
import { ProviderItemActions } from './provider_item_actions';
import { DataProvidersAnd, DataProviderType, QueryOperator } from './data_provider';
import { dragAndDropActions } from '../../../../common/store/drag_and_drop';
-import { useManageTimeline } from '../../manage_timeline';
interface ProviderItemBadgeProps {
andProviderId?: string;
@@ -75,11 +77,10 @@ export const ProviderItemBadge = React.memo(
return getTimeline(state, timelineId)?.timelineType ?? TimelineType.default;
});
- const { getManageTimelineById } = useManageTimeline();
- const isLoading = useMemo(() => getManageTimelineById(timelineId ?? '').isLoading, [
- getManageTimelineById,
- timelineId,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const { isLoading } = useDeepEqualSelector((state) =>
+ getManageTimeline(state, timelineId ?? '')
+ );
const togglePopover = useCallback(() => {
setIsPopoverOpen(!isPopoverOpen);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx
index 7f2133aca7348..a2a91c206521a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx
@@ -8,36 +8,30 @@
import { shallow } from 'enzyme';
import React from 'react';
-import { coreMock } from '../../../../../../../../src/core/public/mocks';
import { TestProviders } from '../../../../common/mock/test_providers';
import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper';
-import { FilterManager } from '../../../../../../../../src/plugins/data/public';
import { timelineActions } from '../../../store/timeline';
import { mockDataProviders } from './mock/mock_data_providers';
import { Providers } from './providers';
import { DELETE_CLASS_NAME, ENABLE_CLASS_NAME, EXCLUDE_CLASS_NAME } from './provider_item_actions';
import { useMountAppended } from '../../../../common/utils/use_mount_appended';
-import { ManageGlobalTimeline, getTimelineDefaults } from '../../manage_timeline';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
-const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
+jest.mock('../../../../common/lib/kibana');
+
+jest.mock('../../../../common/hooks/use_selector', () => ({
+ useShallowEqualSelector: jest.fn(),
+ useDeepEqualSelector: jest.fn(),
+}));
describe('Providers', () => {
- const isLoading: boolean = true;
const mount = useMountAppended();
- const filterManager = new FilterManager(mockUiSettingsForFilterManager);
const mockOnDataProviderRemoved = jest.spyOn(timelineActions, 'removeProvider');
- const manageTimelineForTesting = {
- test: {
- ...getTimelineDefaults('test'),
- filterManager,
- isLoading,
- },
- };
-
beforeEach(() => {
jest.clearAllMocks();
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: false });
});
describe('rendering', () => {
@@ -82,13 +76,12 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onDataProviderRemoved callback when the close button is clicked', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const wrapper = mount(
-
-
-
-
-
+
+
+
);
@@ -120,13 +113,12 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onDataProviderRemoved callback when you click on the option "Delete" in the provider menu', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const wrapper = mount(
-
-
-
-
-
+
+
+
);
wrapper.find('button[data-test-subj="providerBadge"]').first().simulate('click');
@@ -172,17 +164,16 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const mockOnToggleDataProviderEnabled = jest.spyOn(
timelineActions,
'updateDataProviderEnabled'
);
const wrapper = mount(
-
-
-
-
-
+
+
+
);
@@ -231,6 +222,7 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const mockOnToggleDataProviderExcluded = jest.spyOn(
timelineActions,
'updateDataProviderExcluded'
@@ -238,11 +230,9 @@ describe('Providers', () => {
const wrapper = mount(
-
-
-
-
-
+
+
+
);
@@ -311,16 +301,15 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onDataProviderRemoved callback when you click on the close button is clicked', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const wrapper = mount(
-
-
-
-
-
+
+
+
);
@@ -375,6 +364,7 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const mockOnToggleDataProviderEnabled = jest.spyOn(
@@ -384,11 +374,9 @@ describe('Providers', () => {
const wrapper = mount(
-
-
-
-
-
+
+
+
);
@@ -448,6 +436,7 @@ describe('Providers', () => {
});
test('while loading data, it does NOT invoke the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => {
+ (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true });
const dataProviders = mockDataProviders.slice(0, 1);
dataProviders[0].and = mockDataProviders.slice(1, 3);
const mockOnToggleDataProviderExcluded = jest.spyOn(
@@ -457,11 +446,9 @@ describe('Providers', () => {
const wrapper = mount(
-
-
-
-
-
+
+
+
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx
index d7436d2b891b8..d144a67c27509 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx
@@ -13,17 +13,18 @@ import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import deepEqual from 'fast-deep-equal';
+import {
+ DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME,
+ IS_DRAGGING_CLASS_NAME,
+} from '@kbn/securitysolution-t-grid';
import { timelineActions } from '../../../store/timeline';
import { AndOrBadge } from '../../../../common/components/and_or_badge';
-import { useDraggableKeyboardWrapper } from '../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook';
import { AddDataProviderPopover } from './add_data_provider_popover';
import { BrowserFields } from '../../../../common/containers/source';
import {
- DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME,
getTimelineProviderDraggableId,
getTimelineProviderDroppableId,
- IS_DRAGGING_CLASS_NAME,
} from '../../../../common/components/drag_and_drop/helpers';
import { DataProvider, DataProviderType, DataProvidersAnd, IS_OPERATOR } from './data_provider';
@@ -31,6 +32,7 @@ import { EMPTY_GROUP, flattenIntoAndGroups } from './helpers';
import { ProviderItemBadge } from './provider_item_badge';
import * as i18n from './translations';
+import { useKibana } from '../../../../common/lib/kibana';
export const EMPTY_PROVIDERS_GROUP_CLASS_NAME = 'empty-providers-group';
@@ -159,6 +161,7 @@ export const DataProvidersGroupItem = React.memo(
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [, setClosePopOverTrigger] = useState(false);
const dispatch = useDispatch();
+ const { timelines } = useKibana().services;
const handleClosePopOverTrigger = useCallback(() => {
setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger);
@@ -244,7 +247,7 @@ export const DataProvidersGroupItem = React.memo(
setIsPopoverOpen(true);
}, []);
- const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
+ const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({
closePopover: handleClosePopOverTrigger,
draggableId,
fieldName: dataProvider.queryMatch.field,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx
index e13bed1e2eff6..5f08bf5a016f5 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx
@@ -22,6 +22,7 @@ import { useTimelineEvents } from '../../../containers/index';
import { useTimelineEventsDetails } from '../../../containers/details/index';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks';
+import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../timelines/public/components';
jest.mock('../../../containers/index', () => ({
useTimelineEvents: jest.fn(),
@@ -57,6 +58,10 @@ jest.mock('../../../../common/lib/kibana', () => {
savedObjects: {
client: {},
},
+ timelines: {
+ getLastUpdated: jest.fn(),
+ getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper,
+ },
},
}),
useGetUserSavedObjectPermissions: jest.fn(),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx
index bb2a995ff9fae..b67b9348f51aa 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx
@@ -17,7 +17,7 @@ import { isEmpty } from 'lodash/fp';
import React, { useEffect, useCallback } from 'react';
import styled from 'styled-components';
import { Dispatch } from 'redux';
-import { connect, ConnectedProps } from 'react-redux';
+import { connect, ConnectedProps, useDispatch } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { InPortal } from 'react-reverse-portal';
@@ -27,12 +27,17 @@ import { TimelineItem } from '../../../../../common/search_strategy';
import { useTimelineEvents } from '../../../containers/index';
import { defaultHeaders } from '../body/column_headers/default_headers';
import { StatefulBody } from '../body';
-import { RowRenderer } from '../body/renderers/row_renderer';
import { Footer, footerHeight } from '../footer';
import { calculateTotalPages } from '../helpers';
import { TimelineRefetch } from '../refetch_timeline';
-import { useManageTimeline } from '../../manage_timeline';
-import { TimelineEventsType, TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
+import {
+ ControlColumnProps,
+ RowRenderer,
+ TimelineEventsType,
+ TimelineId,
+ TimelineTabs,
+ ToggleDetailPanel,
+} from '../../../../../common/types/timeline';
import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config';
import { ExitFullScreen } from '../../../../common/components/exit_full_screen';
import { SuperDatePicker } from '../../../../common/components/super_date_picker';
@@ -48,10 +53,9 @@ import { TimelineModel } from '../../../../timelines/store/timeline/model';
import { TimelineDatePickerLock } from '../date_picker_lock';
import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen';
import { activeTimeline } from '../../../containers/active_timeline_context';
-import { ToggleDetailPanel } from '../../../store/timeline/actions';
import { DetailsPanel } from '../../side_panel';
import { EqlQueryBarTimeline } from '../query_bar/eql';
-import { defaultControlColumn, ControlColumnProps } from '../body/control_columns';
+import { defaultControlColumn } from '../body/control_columns';
import { Sort } from '../body/sort';
const TimelineHeaderContainer = styled.div`
@@ -166,6 +170,7 @@ export const EqlTabContentComponent: React.FC = ({
timerangeKind,
updateEventTypeAndIndexesName,
}) => {
+ const dispatch = useDispatch();
const { query: eqlQuery = '', ...restEqlOption } = eqlOptions;
const { portalNode: eqlEventsCountPortalNode } = useEqlEventsCountPortal();
const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen();
@@ -192,12 +197,13 @@ export const EqlTabContentComponent: React.FC = ({
return [...columnFields, ...requiredFieldsForActions];
};
- const { initializeTimeline, setIsTimelineLoading } = useManageTimeline();
useEffect(() => {
- initializeTimeline({
- id: timelineId,
- });
- }, [initializeTimeline, timelineId]);
+ dispatch(
+ timelineActions.initializeTGridSettings({
+ id: timelineId,
+ })
+ );
+ }, [dispatch, timelineId]);
const [
isQueryLoading,
@@ -230,8 +236,13 @@ export const EqlTabContentComponent: React.FC = ({
}, [onEventClosed, timelineId, expandedDetail, showExpandedDetails]);
useEffect(() => {
- setIsTimelineLoading({ id: timelineId, isLoading: isQueryLoading || loadingSourcerer });
- }, [loadingSourcerer, timelineId, isQueryLoading, setIsTimelineLoading]);
+ dispatch(
+ timelineActions.updateIsLoading({
+ id: timelineId,
+ isLoading: isQueryLoading || loadingSourcerer,
+ })
+ );
+ }, [loadingSourcerer, timelineId, isQueryLoading, dispatch]);
const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn];
const trailingControlColumns: ControlColumnProps[] = [];
@@ -385,7 +396,6 @@ const makeMapStateToProps = () => {
};
return mapStateToProps;
};
-
const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({
updateEventTypeAndIndexesName: (newEventType: TimelineEventsType, newIndexNames: string[]) => {
dispatch(timelineActions.updateEventType({ id: timelineId, eventType: newEventType }));
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts
index 21e213b799535..ca7c3596d13bb 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts
@@ -5,10 +5,20 @@
* 2.0.
*/
-import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { ColumnId } from './body/column_id';
-import { SortDirection } from './body/sort';
import { DataProvider, QueryOperator } from './data_providers/data_provider';
+export {
+ OnColumnSorted,
+ OnColumnsSorted,
+ OnColumnRemoved,
+ OnColumnResized,
+ OnChangePage,
+ OnPinEvent,
+ OnRowSelected,
+ OnSelectAll,
+ OnUnPinEvent,
+ OnUpdateColumns,
+} from '../../../../common/types/timeline';
export type OnDataProviderEdited = ({
andProviderId,
@@ -35,38 +45,3 @@ export type OnRangeSelected = (range: string) => void;
/** Invoked when a user updates a column's filter */
export type OnFilterChange = (filter: { columnId: ColumnId; filter: string }) => void;
-
-/** Invoked when a column is sorted */
-export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void;
-
-export type OnColumnsSorted = (
- sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }>
-) => void;
-
-export type OnColumnRemoved = (columnId: ColumnId) => void;
-
-export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void;
-
-/** Invoked when a user clicks to load more item */
-export type OnChangePage = (nextPage: number) => void;
-
-/** Invoked when a user pins an event */
-export type OnPinEvent = (eventId: string) => void;
-
-/** Invoked when a user checks/un-checks a row */
-export type OnRowSelected = ({
- eventIds,
- isSelected,
-}: {
- eventIds: string[];
- isSelected: boolean;
-}) => void;
-
-/** Invoked when a user checks/un-checks the select all checkbox */
-export type OnSelectAll = ({ isSelected }: { isSelected: boolean }) => void;
-
-/** Invoked when columns are updated */
-export type OnUpdateColumns = (columns: ColumnHeaderOptions[]) => void;
-
-/** Invoked when a user unpins an event */
-export type OnUnPinEvent = (eventId: string) => void;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx
index f0a14e990e1cc..cf8d51546a899 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx
@@ -12,6 +12,8 @@ import { TestProviders } from '../../../../common/mock/test_providers';
import { FooterComponent, PagingControlComponent } from './index';
+jest.mock('../../../../common/lib/kibana');
+
describe('Footer Timeline Component', () => {
const loadMore = jest.fn();
const updatedAt = 1546878704036;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx
index 4c5432f686c93..ac6f6e52db1e2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx
@@ -24,15 +24,14 @@ import React, { FC, useCallback, useEffect, useState, useMemo } from 'react';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
-import { LoadingPanel } from '../../loading';
import { OnChangePage } from '../events';
import { EVENTS_COUNT_BUTTON_CLASS_NAME } from '../helpers';
import * as i18n from './translations';
import { useEventDetailsWidthContext } from '../../../../common/components/events_viewer/event_details_width_context';
-import { useManageTimeline } from '../../manage_timeline';
-import { LastUpdatedAt } from '../../../../common/components/last_updated';
-import { timelineActions } from '../../../store/timeline';
+import { timelineActions, timelineSelectors } from '../../../store/timeline';
+import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
+import { useKibana } from '../../../../common/lib/kibana';
export const isCompactFooter = (width: number): boolean => width < 600;
@@ -42,12 +41,13 @@ interface FixedWidthLastUpdatedContainerProps {
const FixedWidthLastUpdatedContainer = React.memo(
({ updatedAt }) => {
+ const { timelines } = useKibana().services;
const width = useEventDetailsWidthContext();
const compact = useMemo(() => isCompactFooter(width), [width]);
return (
-
+ {timelines.getLastUpdated({ updatedAt, compact })}
);
}
@@ -259,14 +259,16 @@ export const FooterComponent = ({
totalCount,
}: FooterProps) => {
const dispatch = useDispatch();
+ const { timelines } = useKibana().services;
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [paginationLoading, setPaginationLoading] = useState(false);
- const { getManageTimelineById } = useManageTimeline();
- const { documentType, loadingText, footerText } = useMemo(() => getManageTimelineById(id), [
- getManageTimelineById,
- id,
- ]);
+ const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
+ const {
+ documentType = i18n.TOTAL_COUNT_OF_EVENTS,
+ loadingText = i18n.LOADING_EVENTS,
+ footerText = i18n.TOTAL_COUNT_OF_EVENTS,
+ } = useDeepEqualSelector((state) => getManageTimeline(state, id));
const handleChangePageClick = useCallback(
(nextPage: number) => {
@@ -322,13 +324,13 @@ export const FooterComponent = ({
if (isLoading && !paginationLoading) {
return (
-
+ {timelines.getLoadingPanel({
+ dataTestSubj: 'LoadingPanelTimeline',
+ height: '35px',
+ showBorder: false,
+ text: loadingText,
+ width: '100%',
+ })}
);
}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts
index fa8a8b743646d..6736573cac293 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts
@@ -43,3 +43,10 @@ export const AUTO_REFRESH_ACTIVE = i18n.translate(
defaultMessage: 'Auto-Refresh Active',
}
);
+
+export const LOADING_EVENTS = i18n.translate(
+ 'xpack.securitySolution.footer.loadingEventsDataLabel',
+ {
+ defaultMessage: 'Loading Events',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
index 0093ce2f95bdd..f2a4071111602 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
@@ -14,7 +14,7 @@ import {
getFocusedAriaColindexCell,
getTableSkipFocus,
stopPropagationAndPreventDefault,
-} from '../../../common/components/accessibility/helpers';
+} from '../../../../../timelines/public';
import { escapeQueryValue, convertToBuildEsQuery } from '../../../common/lib/keury';
import {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
index 5e86bf8d75385..e95efdf754418 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
@@ -11,16 +11,15 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
+import { isTab } from '../../../../../timelines/public';
import { timelineActions, timelineSelectors } from '../../store/timeline';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { defaultHeaders } from './body/column_headers/default_headers';
-import { RowRenderer } from './body/renderers/row_renderer';
import { CellValueElementProps } from './cell_rendering';
-import { isTab } from '../../../common/components/accessibility/helpers';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { FlyoutHeader, FlyoutHeaderPanel } from '../flyout/header';
-import { TimelineType, TimelineId } from '../../../../common/types/timeline';
+import { TimelineType, TimelineId, RowRenderer } from '../../../../common/types/timeline';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { activeTimeline } from '../../containers/active_timeline_context';
import { EVENTS_COUNT_BUTTON_CLASS_NAME, onTimelineTabKeyPressed } from './helpers';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx
index 0f781b0958d02..f4d5570ce40d3 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx
@@ -23,6 +23,7 @@ import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks';
import { PinnedTabContentComponent, Props as PinnedTabContentComponentProps } from '.';
import { Direction } from '../../../../../common/search_strategy';
+import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../timelines/public/components';
jest.mock('../../../containers/index', () => ({
useTimelineEvents: jest.fn(),
@@ -57,6 +58,10 @@ jest.mock('../../../../common/lib/kibana', () => {
savedObjects: {
client: {},
},
+ timelines: {
+ getLastUpdated: jest.fn(),
+ getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper,
+ },
},
}),
useGetUserSavedObjectPermissions: jest.fn(),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx
index c01cf5c8aa0f0..b5e3d853bc81c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx
@@ -19,7 +19,6 @@ import { Direction } from '../../../../../common/search_strategy';
import { useTimelineEvents } from '../../../containers/index';
import { defaultHeaders } from '../body/column_headers/default_headers';
import { StatefulBody } from '../body';
-import { RowRenderer } from '../body/renderers/row_renderer';
import { Footer, footerHeight } from '../footer';
import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config';
import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context';
@@ -29,14 +28,18 @@ import { timelineDefaults } from '../../../store/timeline/defaults';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen';
import { TimelineModel } from '../../../store/timeline/model';
-import { ToggleDetailPanel } from '../../../store/timeline/actions';
import { State } from '../../../../common/store';
import { calculateTotalPages } from '../helpers';
-import { TimelineTabs } from '../../../../../common/types/timeline';
+import {
+ ControlColumnProps,
+ RowRenderer,
+ TimelineTabs,
+ ToggleDetailPanel,
+} from '../../../../../common/types/timeline';
import { DetailsPanel } from '../../side_panel';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { ExitFullScreen } from '../../../../common/components/exit_full_screen';
-import { defaultControlColumn, ControlColumnProps } from '../body/control_columns';
+import { defaultControlColumn } from '../body/control_columns';
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
overflow-y: hidden;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx
index 8790d8c98c161..b2b304e16c4a0 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx
@@ -22,7 +22,6 @@ import {
SavedQueryTimeFilter,
} from '../../../../../../../../src/plugins/data/public';
import { convertKueryToElasticSearchQuery } from '../../../../common/lib/keury';
-import { KueryFilterQuery, KueryFilterQueryKind } from '../../../../common/store';
import { KqlMode } from '../../../../timelines/store/timeline/model';
import { useSavedQueryServices } from '../../../../common/utils/saved_query_services';
import { DispatchUpdateReduxTime } from '../../../../common/components/super_date_picker';
@@ -30,6 +29,7 @@ import { QueryBar } from '../../../../common/components/query_bar';
import { DataProvider } from '../data_providers/data_provider';
import { buildGlobalQuery } from '../helpers';
import { timelineActions } from '../../../store/timeline';
+import { KueryFilterQuery, KueryFilterQueryKind } from '../../../../../common/types/timeline';
export interface QueryBarTimelineComponentProps {
dataProviders: DataProvider[];
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx
index acae8c8c53cd0..9bf7ee28f3934 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx
@@ -59,6 +59,15 @@ jest.mock('../../../../common/lib/kibana', () => {
savedObjects: {
client: {},
},
+ timelines: {
+ getLastUpdated: jest.fn(),
+ getLoadingPanel: jest.fn(),
+ getUseDraggableKeyboardWrapper: () =>
+ jest.fn().mockReturnValue({
+ onBlur: jest.fn(),
+ onKeyDown: jest.fn(),
+ }),
+ },
},
}),
useGetUserSavedObjectPermissions: jest.fn(),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx
index 4298f2ff74517..6f0bbd026cd7b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx
@@ -17,12 +17,11 @@ import { isEmpty } from 'lodash/fp';
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import styled from 'styled-components';
import { Dispatch } from 'redux';
-import { connect, ConnectedProps } from 'react-redux';
+import { connect, ConnectedProps, useDispatch } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { InPortal } from 'react-reverse-portal';
import { timelineActions, timelineSelectors } from '../../../store/timeline';
-import { RowRenderer } from '../body/renderers/row_renderer';
import { CellValueElementProps } from '../cell_rendering';
import { Direction, TimelineItem } from '../../../../../common/search_strategy';
import { useTimelineEvents } from '../../../containers/index';
@@ -34,18 +33,20 @@ import { TimelineHeader } from '../header';
import { calculateTotalPages, combineQueries } from '../helpers';
import { TimelineRefetch } from '../refetch_timeline';
import { esQuery, FilterManager } from '../../../../../../../../src/plugins/data/public';
-import { useManageTimeline } from '../../manage_timeline';
-import { TimelineEventsType, TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
+import {
+ ControlColumnProps,
+ KueryFilterQueryKind,
+ RowRenderer,
+ TimelineEventsType,
+ TimelineId,
+ TimelineTabs,
+ ToggleDetailPanel,
+} from '../../../../../common/types/timeline';
import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config';
import { SuperDatePicker } from '../../../../common/components/super_date_picker';
import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context';
import { PickEventType } from '../search_or_filter/pick_events';
-import {
- inputsModel,
- inputsSelectors,
- KueryFilterQueryKind,
- State,
-} from '../../../../common/store';
+import { inputsModel, inputsSelectors, State } from '../../../../common/store';
import { sourcererActions } from '../../../../common/store/sourcerer';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { timelineDefaults } from '../../../../timelines/store/timeline/defaults';
@@ -55,10 +56,9 @@ import { TimelineModel } from '../../../../timelines/store/timeline/model';
import { TimelineDatePickerLock } from '../date_picker_lock';
import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen';
import { activeTimeline } from '../../../containers/active_timeline_context';
-import { ToggleDetailPanel } from '../../../store/timeline/actions';
import { DetailsPanel } from '../../side_panel';
import { ExitFullScreen } from '../../../../common/components/exit_full_screen';
-import { defaultControlColumn, ControlColumnProps } from '../body/control_columns';
+import { defaultControlColumn } from '../body/control_columns';
const TimelineHeaderContainer = styled.div`
margin-top: 6px;
@@ -180,6 +180,7 @@ export const QueryTabContentComponent: React.FC = ({
timerangeKind,
updateEventTypeAndIndexesName,
}) => {
+ const dispatch = useDispatch();
const { portalNode: timelineEventsCountPortalNode } = useTimelineEventsCountPortal();
const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen();
const {
@@ -231,13 +232,14 @@ export const QueryTabContentComponent: React.FC = ({
type: columnType,
}));
- const { initializeTimeline, setIsTimelineLoading } = useManageTimeline();
useEffect(() => {
- initializeTimeline({
- filterManager,
- id: timelineId,
- });
- }, [initializeTimeline, filterManager, timelineId]);
+ dispatch(
+ timelineActions.initializeTGridSettings({
+ filterManager,
+ id: timelineId,
+ })
+ );
+ }, [filterManager, timelineId, dispatch]);
const [
isQueryLoading,
@@ -270,8 +272,13 @@ export const QueryTabContentComponent: React.FC = ({
}, [onEventClosed, timelineId, expandedDetail, showExpandedDetails]);
useEffect(() => {
- setIsTimelineLoading({ id: timelineId, isLoading: isQueryLoading || loadingSourcerer });
- }, [loadingSourcerer, timelineId, isQueryLoading, setIsTimelineLoading]);
+ dispatch(
+ timelineActions.updateIsLoading({
+ id: timelineId,
+ isLoading: isQueryLoading || loadingSourcerer,
+ })
+ );
+ }, [loadingSourcerer, timelineId, isQueryLoading, dispatch]);
const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn];
const trailingControlColumns: ControlColumnProps[] = [];
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx
index 4ea4f94abff63..33ab2e0049828 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx
@@ -12,17 +12,13 @@ import { Dispatch } from 'redux';
import deepEqual from 'fast-deep-equal';
import { Filter, FilterManager } from '../../../../../../../../src/plugins/data/public';
-import {
- SerializedFilterQuery,
- State,
- inputsModel,
- inputsSelectors,
-} from '../../../../common/store';
+import { State, inputsModel, inputsSelectors } from '../../../../common/store';
import { timelineActions, timelineSelectors } from '../../../store/timeline';
import { KqlMode, TimelineModel } from '../../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../../timelines/store/timeline/defaults';
import { dispatchUpdateReduxTime } from '../../../../common/components/super_date_picker';
import { SearchOrFilter } from './search_or_filter';
+import { SerializedFilterQuery } from '../../../../../common/types/timeline';
interface OwnProps {
filterManager: FilterManager;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx
index 262709ed98e5a..f1c4b7c3ef089 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx
@@ -10,9 +10,9 @@ import React, { useCallback } from 'react';
import styled, { createGlobalStyle } from 'styled-components';
import { Filter, FilterManager } from '../../../../../../../../src/plugins/data/public';
-import { KueryFilterQuery } from '../../../../common/store';
import { KqlMode } from '../../../../timelines/store/timeline/model';
import { DispatchUpdateReduxTime } from '../../../../common/components/super_date_picker';
+import { KueryFilterQuery } from '../../../../../common/types/timeline';
import { DataProvider } from '../data_providers/data_provider';
import { QueryBarTimeline } from '../query_bar';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx
index adaa5f98c88c4..8cdd7722d7fbd 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx
@@ -10,7 +10,12 @@ import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 're
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
-import { TimelineTabs, TimelineId, TimelineType } from '../../../../../common/types/timeline';
+import {
+ RowRenderer,
+ TimelineTabs,
+ TimelineId,
+ TimelineType,
+} from '../../../../../common/types/timeline';
import {
useShallowEqualSelector,
useDeepEqualSelector,
@@ -20,7 +25,6 @@ import {
TimelineEventsCountBadge,
} from '../../../../common/hooks/use_timeline_events_count';
import { timelineActions } from '../../../store/timeline';
-import { RowRenderer } from '../body/renderers/row_renderer';
import { CellValueElementProps } from '../cell_rendering';
import {
getActiveTabSelector,
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx
index 37fdd5a444b2b..86624ba161a83 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx
@@ -69,7 +69,7 @@ export const useTimelineEventsDetails = ({
.search(
request,
{
- strategy: 'securitySolutionTimelineSearchStrategy',
+ strategy: 'timelineSearchStrategy',
abortSignal: abortCtrl.current.signal,
}
)
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx
index 17c107899d85a..00df0146e06d5 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx
@@ -14,7 +14,7 @@ import { Subscription } from 'rxjs';
import { ESQuery } from '../../../common/typed_json';
import { isCompleteResponse, isErrorResponse } from '../../../../../../src/plugins/data/public';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
-import { inputsModel, KueryFilterQueryKind } from '../../common/store';
+import { inputsModel } from '../../common/store';
import { useKibana } from '../../common/lib/kibana';
import { createFilter } from '../../common/containers/helpers';
import { timelineActions } from '../../timelines/store/timeline';
@@ -33,7 +33,7 @@ import {
} from '../../../common/search_strategy';
import { InspectResponse } from '../../types';
import * as i18n from './translations';
-import { TimelineId } from '../../../common/types/timeline';
+import { KueryFilterQueryKind, TimelineId } from '../../../common/types/timeline';
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
import { activeTimeline } from './active_timeline_context';
import {
@@ -214,9 +214,7 @@ export const useTimelineEvents = ({
searchSubscription$.current = data.search
.search, TimelineResponse>(request, {
strategy:
- request.language === 'eql'
- ? 'securitySolutionTimelineEqlSearchStrategy'
- : 'securitySolutionTimelineSearchStrategy',
+ request.language === 'eql' ? 'timelineEqlSearchStrategy' : 'timelineSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx
index 4a6eab13ba4f1..be93a13ab1c6a 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx
@@ -64,7 +64,7 @@ export const useTimelineKpis = ({
searchSubscription$.current = data.search
.search(request, {
- strategy: 'securitySolutionTimelineSearchStrategy',
+ strategy: 'timelineSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx
index 38eb6d3d222f8..99f45c7d9a4b4 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx
@@ -9,8 +9,8 @@ import { isEmpty } from 'lodash/fp';
import { Storage } from '../../../../../../../src/plugins/kibana_utils/public';
import { TimelinesStorage } from './types';
import { useKibana } from '../../../common/lib/kibana';
-import { ColumnHeaderOptions, TimelineModel } from '../../store/timeline/model';
-import { TimelineIdLiteral } from '../../../../common/types/timeline';
+import { TimelineModel } from '../../store/timeline/model';
+import { ColumnHeaderOptions, TimelineIdLiteral } from '../../../../common/types/timeline';
export const LOCAL_STORAGE_TIMELINE_KEY = 'timelines';
const EMPTY_TIMELINE = {} as {
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
index 11e9a625d05d0..a3429c9247ffd 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
@@ -8,25 +8,42 @@
import actionCreatorFactory from 'typescript-fsa';
import { Filter } from '../../../../../../../src/plugins/data/public';
-import { Sort } from '../../../timelines/components/timeline/body/sort';
import {
DataProvider,
DataProviderType,
QueryOperator,
} from '../../../timelines/components/timeline/data_providers/data_provider';
-import { SerializedFilterQuery } from '../../../common/store/types';
-import { KqlMode, TimelineModel, ColumnHeaderOptions } from './model';
-import { FieldsEqlOptions, TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
+import { KqlMode, TimelineModel } from './model';
+import { FieldsEqlOptions } from '../../../../common/search_strategy/timeline';
import {
TimelineEventsType,
- TimelineExpandedDetail,
- TimelineExpandedDetailType,
- TimelineTypeLiteral,
RowRendererId,
TimelineTabs,
+ TimelinePersistInput,
+ SerializedFilterQuery,
} from '../../../../common/types/timeline';
import { InsertTimeline } from './types';
+import { tGridActions } from '../../../../../timelines/public';
+export const {
+ applyDeltaToColumnWidth,
+ clearEventsDeleted,
+ clearEventsLoading,
+ clearSelected,
+ initializeTGridSettings,
+ removeColumn,
+ setEventsDeleted,
+ setEventsLoading,
+ setSelected,
+ setTGridSelectAll,
+ toggleDetailPanel,
+ updateColumns,
+ updateIsLoading,
+ updateItemsPerPage,
+ updateItemsPerPageOptions,
+ updateSort,
+ upsertColumn,
+} = tGridActions;
const actionCreator = actionCreatorFactory('x-pack/security_solution/local/timeline');
@@ -38,62 +55,14 @@ export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventI
'ADD_NOTE_TO_EVENT'
);
-export type ToggleDetailPanel = TimelineExpandedDetailType & {
- tabType?: TimelineTabs;
- timelineId: string;
-};
-
-export const toggleDetailPanel = actionCreator('TOGGLE_DETAIL_PANEL');
-
-export const upsertColumn = actionCreator<{
- column: ColumnHeaderOptions;
- id: string;
- index: number;
-}>('UPSERT_COLUMN');
-
export const addProvider = actionCreator<{ id: string; provider: DataProvider }>('ADD_PROVIDER');
-export const applyDeltaToColumnWidth = actionCreator<{
- id: string;
- columnId: string;
- delta: number;
-}>('APPLY_DELTA_TO_COLUMN_WIDTH');
-
-export interface TimelineInput {
- id: string;
- dataProviders?: DataProvider[];
- dateRange?: {
- start: string;
- end: string;
- };
- excludedRowRendererIds?: RowRendererId[];
- expandedDetail?: TimelineExpandedDetail;
- filters?: Filter[];
- columns: ColumnHeaderOptions[];
- itemsPerPage?: number;
- indexNames: string[];
- kqlQuery?: {
- filterQuery: SerializedFilterQuery | null;
- };
- show?: boolean;
- sort?: Sort[];
- showCheckboxes?: boolean;
- timelineType?: TimelineTypeLiteral;
- templateTimelineId?: string | null;
- templateTimelineVersion?: number | null;
-}
-
-export const saveTimeline = actionCreator('SAVE_TIMELINE');
+export const saveTimeline = actionCreator('SAVE_TIMELINE');
-export const createTimeline = actionCreator('CREATE_TIMELINE');
+export const createTimeline = actionCreator('CREATE_TIMELINE');
export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT');
-export const removeColumn = actionCreator<{
- id: string;
- columnId: string;
-}>('REMOVE_COLUMN');
-
export const removeProvider = actionCreator<{
id: string;
providerId: string;
@@ -129,16 +98,6 @@ export const endTimelineSaving = actionCreator<{
id: string;
}>('END_TIMELINE_SAVING');
-export const updateIsLoading = actionCreator<{
- id: string;
- isLoading: boolean;
-}>('UPDATE_LOADING');
-
-export const updateColumns = actionCreator<{
- id: string;
- columns: ColumnHeaderOptions[];
-}>('UPDATE_COLUMNS');
-
export const updateDataProviderEnabled = actionCreator<{
id: string;
enabled: boolean;
@@ -189,15 +148,6 @@ export const updateIsFavorite = actionCreator<{ id: string; isFavorite: boolean
export const updateIsLive = actionCreator<{ id: string; isLive: boolean }>('UPDATE_IS_LIVE');
-export const updateItemsPerPage = actionCreator<{ id: string; itemsPerPage: number }>(
- 'UPDATE_ITEMS_PER_PAGE'
-);
-
-export const updateItemsPerPageOptions = actionCreator<{
- id: string;
- itemsPerPageOptions: number[];
-}>('UPDATE_ITEMS_PER_PAGE_OPTIONS');
-
export const updateTitleAndDescription = actionCreator<{
description: string;
id: string;
@@ -216,8 +166,6 @@ export const updateRange = actionCreator<{ id: string; start: string; end: strin
'UPDATE_RANGE'
);
-export const updateSort = actionCreator<{ id: string; sort: Sort[] }>('UPDATE_SORT');
-
export const updateAutoSaveMsg = actionCreator<{
timelineId: string | null;
newTimelineModel: TimelineModel | null;
@@ -235,37 +183,6 @@ export const setFilters = actionCreator<{
filters: Filter[];
}>('SET_TIMELINE_FILTERS');
-export const setSelected = actionCreator<{
- id: string;
- eventIds: Readonly>;
- isSelected: boolean;
- isSelectAllChecked: boolean;
-}>('SET_TIMELINE_SELECTED');
-
-export const clearSelected = actionCreator<{
- id: string;
-}>('CLEAR_TIMELINE_SELECTED');
-
-export const setEventsLoading = actionCreator<{
- id: string;
- eventIds: string[];
- isLoading: boolean;
-}>('SET_TIMELINE_EVENTS_LOADING');
-
-export const clearEventsLoading = actionCreator<{
- id: string;
-}>('CLEAR_TIMELINE_EVENTS_LOADING');
-
-export const setEventsDeleted = actionCreator<{
- id: string;
- eventIds: string[];
- isDeleted: boolean;
-}>('SET_TIMELINE_EVENTS_DELETED');
-
-export const clearEventsDeleted = actionCreator<{
- id: string;
-}>('CLEAR_TIMELINE_EVENTS_DELETED');
-
export const updateEventType = actionCreator<{ id: string; eventType: TimelineEventsType }>(
'UPDATE_EVENT_TYPE'
);
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts
index 7e76f6035f8b5..d8fd82005dfbe 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts
@@ -10,7 +10,6 @@ import { TimelineType, TimelineStatus, TimelineTabs } from '../../../../common/t
import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers';
import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range';
import { SubsetTimelineModel, TimelineModel } from './model';
-import { Direction } from '../../../../common/search_strategy';
// normalizeTimeRange uses getTimeRangeSettings which cannot be used outside Kibana context if the uiSettings is not false
const { from: start, to: end } = normalizeTimeRange({ from: '', to: '' }, false);
@@ -66,7 +65,7 @@ export const timelineDefaults: SubsetTimelineModel &
{
columnId: '@timestamp',
columnType: 'number',
- sortDirection: Direction.desc,
+ sortDirection: 'desc',
},
],
status: TimelineStatus.draft,
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
index 5f5d76990b5ff..8f2631dac6769 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
@@ -41,6 +41,7 @@ import {
TimelineType,
ResponseTimeline,
TimelineResult,
+ ColumnHeaderOptions,
} from '../../../../common/types/timeline';
import { inputsModel } from '../../../common/store/inputs';
import { addError } from '../../../common/store/app/actions';
@@ -81,7 +82,7 @@ import {
showCallOutUnauthorizedMsg,
saveTimeline,
} from './actions';
-import { ColumnHeaderOptions, TimelineModel } from './model';
+import { TimelineModel } from './model';
import { epicPersistNote, timelineNoteActionsType } from './epic_note';
import { epicPersistPinnedEvent, timelinePinnedEventActionsType } from './epic_pinned_event';
import { epicPersistTimelineFavorite, timelineFavoriteActionsType } from './epic_favorite';
@@ -96,13 +97,11 @@ const timelineActionsType = [
addProvider.type,
addTimeline.type,
dataProviderEdited.type,
- removeColumn.type,
removeProvider.type,
saveTimeline.type,
setExcludedRowRendererIds.type,
setFilters.type,
setSavedQueryId.type,
- updateColumns.type,
updateDataProviderEnabled.type,
updateDataProviderExcluded.type,
updateDataProviderKqlQuery.type,
@@ -110,10 +109,13 @@ const timelineActionsType = [
updateEqlOptions.type,
updateEventType.type,
updateKqlMode.type,
- updateIndexNames.type,
updateProviders.type,
- updateSort.type,
updateTitleAndDescription.type,
+
+ updateIndexNames.type,
+ removeColumn.type,
+ updateColumns.type,
+ updateSort.type,
updateRange.type,
upsertColumn.type,
];
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
index 2172cf8562c97..610c394614c32 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
@@ -8,7 +8,6 @@
import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp';
import uuid from 'uuid';
-import { ToggleDetailPanel } from './actions';
import { Filter } from '../../../../../../../src/plugins/data/public';
import { Sort } from '../../../timelines/components/timeline/body/sort';
@@ -20,22 +19,24 @@ import {
IS_OPERATOR,
EXISTS_OPERATOR,
} from '../../../timelines/components/timeline/data_providers/data_provider';
-import { SerializedFilterQuery } from '../../../common/store/model';
import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
import {
+ ColumnHeaderOptions,
TimelineEventsType,
- TimelineExpandedDetail,
TimelineTypeLiteral,
TimelineType,
RowRendererId,
TimelineStatus,
TimelineId,
TimelineTabs,
+ SerializedFilterQuery,
+ ToggleDetailPanel,
+ TimelinePersistInput,
} from '../../../../common/types/timeline';
import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range';
import { timelineDefaults } from './defaults';
-import { ColumnHeaderOptions, KqlMode, TimelineModel } from './model';
+import { KqlMode, TimelineModel } from './model';
import { TimelineById } from './types';
import {
DEFAULT_FROM_MOMENT,
@@ -168,47 +169,20 @@ export const addTimelineToStore = ({
};
};
-interface AddNewTimelineParams {
- columns: ColumnHeaderOptions[];
- dataProviders?: DataProvider[];
- dateRange?: {
- start: string;
- end: string;
- };
- excludedRowRendererIds?: RowRendererId[];
- expandedDetail?: TimelineExpandedDetail;
- filters?: Filter[];
- id: string;
- itemsPerPage?: number;
- indexNames: string[];
- kqlQuery?: {
- filterQuery: SerializedFilterQuery | null;
- };
- show?: boolean;
- sort?: Sort[];
- showCheckboxes?: boolean;
+interface AddNewTimelineParams extends TimelinePersistInput {
timelineById: TimelineById;
timelineType: TimelineTypeLiteral;
}
/** Adds a new `Timeline` to the provided collection of `TimelineById` */
export const addNewTimeline = ({
- columns,
- dataProviders = [],
- dateRange: maybeDateRange,
- excludedRowRendererIds = [],
- expandedDetail = {},
- filters = timelineDefaults.filters,
id,
- itemsPerPage = timelineDefaults.itemsPerPage,
- indexNames,
- kqlQuery = { filterQuery: null },
- sort = timelineDefaults.sort,
- show = false,
- showCheckboxes = false,
timelineById,
timelineType,
+ dateRange: maybeDateRange,
+ ...timelineProps
}: AddNewTimelineParams): TimelineById => {
+ const timeline = timelineById[id];
const { from: startDateRange, to: endDateRange } = normalizeTimeRange({ from: '', to: '' });
const dateRange = maybeDateRange ?? { start: startDateRange, end: endDateRange };
const templateTimelineInfo =
@@ -222,23 +196,14 @@ export const addNewTimeline = ({
...timelineById,
[id]: {
id,
+ ...(timeline ? timeline : {}),
...timelineDefaults,
- columns,
- dataProviders,
+ ...timelineProps,
dateRange,
- expandedDetail,
- excludedRowRendererIds,
- filters,
- itemsPerPage,
- indexNames,
- kqlQuery,
- sort,
- show,
savedObjectId: null,
version: null,
isSaving: false,
isLoading: false,
- showCheckboxes,
timelineType,
...templateTimelineInfo,
},
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
index 559cec57dd55c..a68617536c6af 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
@@ -5,63 +5,29 @@
* 2.0.
*/
-import { EuiDataGridColumn } from '@elastic/eui';
-
-import { Filter, IFieldSubType } from '../../../../../../../src/plugins/data/public';
-
import { DataProvider } from '../../components/timeline/data_providers/data_provider';
-import { Sort } from '../../components/timeline/body/sort';
-import {
- EqlOptionsSelected,
- TimelineNonEcsData,
-} from '../../../../common/search_strategy/timeline';
-import { SerializedFilterQuery } from '../../../common/store/types';
+import { EqlOptionsSelected } from '../../../../common/search_strategy/timeline';
import type {
TimelineEventsType,
- TimelineExpandedDetail,
TimelineType,
TimelineStatus,
- RowRendererId,
TimelineTabs,
} from '../../../../common/types/timeline';
import { PinnedEvent } from '../../../../common/types/timeline/pinned_event';
+import type { TGridModelForTimeline } from '../../../../../timelines/public';
export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages
export type KqlMode = 'filter' | 'search';
export type ColumnHeaderType = 'not-filtered' | 'text-filter';
-/** Uniquely identifies a column */
-export type ColumnId = string;
-
-/** The specification of a column header */
-export type ColumnHeaderOptions = Pick<
- EuiDataGridColumn,
- 'display' | 'displayAsText' | 'id' | 'initialWidth'
-> & {
- aggregatable?: boolean;
- category?: string;
- columnHeaderType: ColumnHeaderType;
- description?: string;
- example?: string;
- format?: string;
- linkField?: string;
- placeholder?: string;
- subType?: IFieldSubType;
- type?: string;
-};
-
-export interface TimelineModel {
+export type TimelineModel = TGridModelForTimeline & {
/** The selected tab to displayed in the timeline */
activeTab: TimelineTabs;
prevActiveTab: TimelineTabs;
- /** The columns displayed in the timeline */
- columns: ColumnHeaderOptions[];
/** Timeline saved object owner */
createdBy?: string;
/** The sources of the event data shown in the timeline */
dataProviders: DataProvider[];
- /** Events to not be rendered **/
- deletedEventIds: string[];
/** A summary of the events and notes in this timeline */
description: string;
eqlOptions: EqlOptionsSelected;
@@ -69,40 +35,16 @@ export interface TimelineModel {
eventType?: TimelineEventsType;
/** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */
eventIdToNoteIds: Record;
- /** A list of Ids of excluded Row Renderers */
- excludedRowRendererIds: RowRendererId[];
- /** This holds the view information for the flyout when viewing timeline in a consuming view (i.e. hosts page) or the side panel in the primary timeline view */
- expandedDetail: TimelineExpandedDetail;
- filters?: Filter[];
- /** When non-empty, display a graph view for this event */
- graphEventId?: string;
/** The chronological history of actions related to this timeline */
historyIds: string[];
/** The chronological history of actions related to this timeline */
highlightedDropAndProviderId: string;
- /** Uniquely identifies the timeline */
- id: string;
- /** TO DO sourcerer @X define this */
- indexNames: string[];
- /** If selectAll checkbox in header is checked **/
- isSelectAllChecked: boolean;
- /** Events to be rendered as loading **/
- loadingEventIds: string[];
- savedObjectId: string | null;
/** When true, this timeline was marked as "favorite" by the user */
isFavorite: boolean;
/** When true, the timeline will update as new data arrives */
isLive: boolean;
- /** The number of items to show in a single page of results */
- itemsPerPage: number;
- /** Displays a series of choices that when selected, become the value of `itemsPerPage` */
- itemsPerPageOptions: number[];
/** determines the behavior of the KQL bar */
kqlMode: KqlMode;
- /** the KQL query in the KQL bar */
- kqlQuery: {
- filterQuery: SerializedFilterQuery | null;
- };
/** Title */
title: string;
/** timelineType: default | template */
@@ -116,30 +58,18 @@ export interface TimelineModel {
/** Events pinned to this timeline */
pinnedEventIds: Record;
pinnedEventsSaveObject: Record;
- /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */
- dateRange: {
- start: string;
- end: string;
- };
showSaveModal?: boolean;
savedQueryId?: string | null;
- /** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/
- selectedEventIds: Record;
/** When true, show the timeline flyover */
show: boolean;
- /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/
- showCheckboxes: boolean;
- /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */
- sort: Sort[];
/** status: active | draft */
status: TimelineStatus;
/** updated saved object timestamp */
updated?: number;
/** timeline is saving */
isSaving: boolean;
- isLoading: boolean;
version: string | null;
-}
+};
export type SubsetTimelineModel = Readonly<
Pick<
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
index 1c65c01a0bdfc..8a5c8546d3834 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
@@ -7,6 +7,7 @@
import { cloneDeep } from 'lodash/fp';
import {
+ ColumnHeaderOptions,
TimelineType,
TimelineStatus,
TimelineTabs,
@@ -47,7 +48,7 @@ import {
upsertTimelineColumn,
updateGraphEventId,
} from './helpers';
-import { ColumnHeaderOptions, TimelineModel } from './model';
+import { TimelineModel } from './model';
import { timelineDefaults } from './defaults';
import { TimelineById } from './types';
import { Direction } from '../../../../common/search_strategy';
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
index 80c6d83075719..656784c330e45 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
@@ -13,32 +13,22 @@ import {
addNoteToEvent,
addProvider,
addTimeline,
- applyDeltaToColumnWidth,
applyKqlFilterQuery,
- clearEventsDeleted,
- clearEventsLoading,
- clearSelected,
createTimeline,
dataProviderEdited,
endTimelineSaving,
pinEvent,
- removeColumn,
removeProvider,
- setEventsDeleted,
setActiveTabTimeline,
- setEventsLoading,
setExcludedRowRendererIds,
setFilters,
setInsertTimeline,
setSavedQueryId,
- setSelected,
showCallOutUnauthorizedMsg,
showTimeline,
startTimelineSaving,
- toggleDetailPanel,
unPinEvent,
updateAutoSaveMsg,
- updateColumns,
updateDataProviderEnabled,
updateDataProviderExcluded,
updateDataProviderKqlQuery,
@@ -47,18 +37,13 @@ import {
updateIndexNames,
updateIsFavorite,
updateIsLive,
- updateIsLoading,
- updateItemsPerPage,
- updateItemsPerPageOptions,
updateKqlMode,
updatePageIndex,
updateProviders,
updateRange,
- updateSort,
updateTimeline,
updateTimelineGraphEventId,
updateTitleAndDescription,
- upsertColumn,
toggleModalSaveTimeline,
updateEqlOptions,
} from './actions';
@@ -69,23 +54,15 @@ import {
addTimelineNoteToEvent,
addTimelineProvider,
addTimelineToStore,
- applyDeltaToTimelineColumnWidth,
applyKqlFilterQueryDraft,
pinTimelineEvent,
- removeTimelineColumn,
removeTimelineProvider,
- setDeletedTimelineEvents,
- setLoadingTimelineEvents,
- setSelectedTimelineEvents,
unPinTimelineEvent,
updateExcludedRowRenderersIds,
- updateTimelineColumns,
updateTimelineIsFavorite,
updateTimelineIsLive,
- updateTimelineItemsPerPage,
updateTimelineKqlMode,
updateTimelinePageIndex,
- updateTimelinePerPageOptions,
updateTimelineProviderEnabled,
updateTimelineProviderExcluded,
updateTimelineProviderProperties,
@@ -94,13 +71,10 @@ import {
updateTimelineProviders,
updateTimelineRange,
updateTimelineShowTimeline,
- updateTimelineSort,
updateTimelineTitleAndDescription,
- upsertTimelineColumn,
updateSavedQuery,
updateGraphEventId,
updateFilters,
- updateTimelineDetailsPanel,
updateTimelineEventType,
} from './helpers';
@@ -123,53 +97,17 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: addTimelineToStore({ id, timeline, timelineById: state.timelineById }),
}))
- .case(
- createTimeline,
- (
- state,
- {
+ .case(createTimeline, (state, { id, timelineType = TimelineType.default, ...timelineProps }) => {
+ return {
+ ...state,
+ timelineById: addNewTimeline({
id,
- dataProviders,
- dateRange,
- excludedRowRendererIds,
- expandedDetail = {},
- show,
- columns,
- itemsPerPage,
- indexNames,
- kqlQuery,
- sort,
- showCheckboxes,
- timelineType = TimelineType.default,
- filters,
- }
- ) => {
- return {
- ...state,
- timelineById: addNewTimeline({
- columns,
- dataProviders,
- dateRange,
- excludedRowRendererIds,
- expandedDetail,
- filters,
- id,
- itemsPerPage,
- indexNames,
- kqlQuery,
- sort,
- show,
- showCheckboxes,
- timelineById: state.timelineById,
- timelineType,
- }),
- };
- }
- )
- .case(upsertColumn, (state, { column, id, index }) => ({
- ...state,
- timelineById: upsertTimelineColumn({ column, id, index, timelineById: state.timelineById }),
- }))
+ timelineById: state.timelineById,
+ timelineType,
+ ...timelineProps,
+ }),
+ };
+ })
.case(addHistory, (state, { id, historyId }) => ({
...state,
timelineById: addTimelineHistory({ id, historyId, timelineById: state.timelineById }),
@@ -182,19 +120,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: addTimelineNoteToEvent({ id, noteId, eventId, timelineById: state.timelineById }),
}))
- .case(toggleDetailPanel, (state, action) => ({
- ...state,
- timelineById: {
- ...state.timelineById,
- [action.timelineId]: {
- ...state.timelineById[action.timelineId],
- expandedDetail: {
- ...state.timelineById[action.timelineId].expandedDetail,
- ...updateTimelineDetailsPanel(action),
- },
- },
- },
- }))
.case(addProvider, (state, { id, provider }) => ({
...state,
timelineById: addTimelineProvider({ id, provider, timelineById: state.timelineById }),
@@ -215,27 +140,10 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: updateGraphEventId({ id, graphEventId, timelineById: state.timelineById }),
}))
- .case(applyDeltaToColumnWidth, (state, { id, columnId, delta }) => ({
- ...state,
- timelineById: applyDeltaToTimelineColumnWidth({
- id,
- columnId,
- delta,
- timelineById: state.timelineById,
- }),
- }))
.case(pinEvent, (state, { id, eventId }) => ({
...state,
timelineById: pinTimelineEvent({ id, eventId, timelineById: state.timelineById }),
}))
- .case(removeColumn, (state, { id, columnId }) => ({
- ...state,
- timelineById: removeTimelineColumn({
- id,
- columnId,
- timelineById: state.timelineById,
- }),
- }))
.case(removeProvider, (state, { id, providerId, andProviderId }) => ({
...state,
timelineById: removeTimelineProvider({
@@ -265,44 +173,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
},
},
}))
- .case(setEventsDeleted, (state, { id, eventIds, isDeleted }) => ({
- ...state,
- timelineById: setDeletedTimelineEvents({
- id,
- eventIds,
- timelineById: state.timelineById,
- isDeleted,
- }),
- }))
- .case(clearEventsDeleted, (state, { id }) => ({
- ...state,
- timelineById: {
- ...state.timelineById,
- [id]: {
- ...state.timelineById[id],
- deletedEventIds: [],
- },
- },
- }))
- .case(setEventsLoading, (state, { id, eventIds, isLoading }) => ({
- ...state,
- timelineById: setLoadingTimelineEvents({
- id,
- eventIds,
- timelineById: state.timelineById,
- isLoading,
- }),
- }))
- .case(clearEventsLoading, (state, { id }) => ({
- ...state,
- timelineById: {
- ...state.timelineById,
- [id]: {
- ...state.timelineById[id],
- loadingEventIds: [],
- },
- },
- }))
.case(setExcludedRowRendererIds, (state, { id, excludedRowRendererIds }) => ({
...state,
timelineById: updateExcludedRowRenderersIds({
@@ -311,37 +181,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
timelineById: state.timelineById,
}),
}))
- .case(setSelected, (state, { id, eventIds, isSelected, isSelectAllChecked }) => ({
- ...state,
- timelineById: setSelectedTimelineEvents({
- id,
- eventIds,
- timelineById: state.timelineById,
- isSelected,
- isSelectAllChecked,
- }),
- }))
- .case(clearSelected, (state, { id }) => ({
- ...state,
- timelineById: {
- ...state.timelineById,
- [id]: {
- ...state.timelineById[id],
- selectedEventIds: {},
- isSelectAllChecked: false,
- },
- },
- }))
- .case(updateIsLoading, (state, { id, isLoading }) => ({
- ...state,
- timelineById: {
- ...state.timelineById,
- [id]: {
- ...state.timelineById[id],
- isLoading,
- },
- },
- }))
.case(updateTimeline, (state, { id, timeline }) => ({
...state,
timelineById: {
@@ -353,14 +192,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: unPinTimelineEvent({ id, eventId, timelineById: state.timelineById }),
}))
- .case(updateColumns, (state, { id, columns }) => ({
- ...state,
- timelineById: updateTimelineColumns({
- id,
- columns,
- timelineById: state.timelineById,
- }),
- }))
.case(updateEventType, (state, { id, eventType }) => ({
...state,
timelineById: updateTimelineEventType({ id, eventType, timelineById: state.timelineById }),
@@ -394,10 +225,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: updateTimelineRange({ id, start, end, timelineById: state.timelineById }),
}))
- .case(updateSort, (state, { id, sort }) => ({
- ...state,
- timelineById: updateTimelineSort({ id, sort, timelineById: state.timelineById }),
- }))
.case(updateDataProviderEnabled, (state, { id, enabled, providerId, andProviderId }) => ({
...state,
timelineById: updateTimelineProviderEnabled({
@@ -454,14 +281,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
timelineById: state.timelineById,
}),
}))
- .case(updateItemsPerPage, (state, { id, itemsPerPage }) => ({
- ...state,
- timelineById: updateTimelineItemsPerPage({
- id,
- itemsPerPage,
- timelineById: state.timelineById,
- }),
- }))
.case(updatePageIndex, (state, { id, activePage }) => ({
...state,
timelineById: updateTimelinePageIndex({
@@ -470,14 +289,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
timelineById: state.timelineById,
}),
}))
- .case(updateItemsPerPageOptions, (state, { id, itemsPerPageOptions }) => ({
- ...state,
- timelineById: updateTimelinePerPageOptions({
- id,
- itemsPerPageOptions,
- timelineById: state.timelineById,
- }),
- }))
.case(updateAutoSaveMsg, (state, { timelineId, newTimelineModel }) => ({
...state,
autoSavedWarningMsg: {
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts
index b05e6568be6c3..f46b55bcd3345 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts
@@ -7,11 +7,14 @@
import { createSelector } from 'reselect';
+import { tGridSelectors } from '../../../../../timelines/public';
import { State } from '../../../common/store/types';
import { TimelineModel } from './model';
import { AutoSavedWarningMsg, InsertTimeline, TimelineById } from './types';
+export const { getManageTimelineById } = tGridSelectors;
+
const selectTimelineById = (state: State): TimelineById => state.timeline.timelineById;
const selectAutoSaveMsg = (state: State): AutoSavedWarningMsg => state.timeline.autoSavedWarningMsg;
diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts
index d4e2601554187..aad685f9fb103 100644
--- a/x-pack/plugins/security_solution/public/types.ts
+++ b/x-pack/plugins/security_solution/public/types.ts
@@ -23,6 +23,7 @@ import {
} from '../../triggers_actions_ui/public';
import { CasesUiStart } from '../../cases/public';
import { SecurityPluginSetup } from '../../security/public';
+import { TimelinesUIStart } from '../../timelines/public';
import { ResolverPluginSetup } from './resolver/types';
import { Inspect } from '../common/search_strategy';
import { MlPluginSetup, MlPluginStart } from '../../ml/public';
@@ -56,6 +57,7 @@ export interface StartPlugins {
licensing: LicensingPluginStart;
newsfeed?: NewsfeedPublicPluginStart;
triggersActionsUi: TriggersActionsStart;
+ timelines: TimelinesUIStart;
uiActions: UiActionsStart;
ml?: MlPluginStart;
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/eql.ts
new file mode 100644
index 0000000000000..76389d7376fc8
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/eql.ts
@@ -0,0 +1,791 @@
+/*
+ * 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 { EqlSearchStrategyResponse } from '../../../../../../../../src/plugins/data/common';
+import { EqlSearchResponse } from '../../../../../common/detection_engine/types';
+
+export const sequenceResponse = ({
+ rawResponse: {
+ body: {
+ is_partial: false,
+ is_running: false,
+ took: 527,
+ timed_out: false,
+ hits: {
+ total: {
+ value: 10,
+ relation: 'eq',
+ },
+ sequences: [
+ {
+ join_keys: ['win2019-endpoint-mr-pedro'],
+ events: [
+ {
+ _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005',
+ _id: 'qhymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTIzODAtMTMyNTUwNzg2ODkuOTY1Nzg1NTAw',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU2OC0xMzI1NTA3ODY2Ny4zMjk3MDY2MDA=',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ name: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe',
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw',
+ executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe',
+ },
+ message: 'Endpoint security event',
+ '@timestamp': '2021-02-08T21:50:28.3377092Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.security',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293866,
+ ingested: '2021-02-08T21:57:26.417559711Z',
+ created: '2021-02-08T21:50:28.3377092Z',
+ kind: 'event',
+ module: 'endpoint',
+ action: 'log_on',
+ id: 'LzzWB9jjGmCwGMvk++++FG/O',
+ category: ['authentication', 'session'],
+ type: ['start'],
+ dataset: 'endpoint.events.security',
+ outcome: 'success',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'SYSTEM',
+ },
+ },
+ },
+ {
+ _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005',
+ _id: 'qxymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=',
+ executable: 'C:\\Windows\\System32\\lsass.exe',
+ },
+ message: 'Endpoint security event',
+ '@timestamp': '2021-02-08T21:50:28.3377142Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.security',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293867,
+ ingested: '2021-02-08T21:57:26.417596906Z',
+ created: '2021-02-08T21:50:28.3377142Z',
+ kind: 'event',
+ module: 'endpoint',
+ action: 'log_on',
+ id: 'LzzWB9jjGmCwGMvk++++FG/P',
+ category: ['authentication', 'session'],
+ type: ['start'],
+ dataset: 'endpoint.events.security',
+ outcome: 'success',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'SYSTEM',
+ },
+ },
+ },
+ {
+ _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005',
+ _id: 'rBymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=',
+ executable: 'C:\\Windows\\System32\\lsass.exe',
+ },
+ message: 'Endpoint security event',
+ '@timestamp': '2021-02-08T21:50:28.3381013Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.security',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293868,
+ ingested: '2021-02-08T21:57:26.417632166Z',
+ created: '2021-02-08T21:50:28.3381013Z',
+ kind: 'event',
+ module: 'endpoint',
+ id: 'LzzWB9jjGmCwGMvk++++FG/Q',
+ category: [],
+ type: [],
+ dataset: 'endpoint.events.security',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'SYSTEM',
+ },
+ },
+ },
+ ],
+ },
+ {
+ join_keys: ['win2019-endpoint-mr-pedro'],
+ events: [
+ {
+ _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005',
+ _id: 'qxymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=',
+ executable: 'C:\\Windows\\System32\\lsass.exe',
+ },
+ message: 'Endpoint security event',
+ '@timestamp': '2021-02-08T21:50:28.3377142Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.security',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293867,
+ ingested: '2021-02-08T21:57:26.417596906Z',
+ created: '2021-02-08T21:50:28.3377142Z',
+ kind: 'event',
+ module: 'endpoint',
+ action: 'log_on',
+ id: 'LzzWB9jjGmCwGMvk++++FG/P',
+ category: ['authentication', 'session'],
+ type: ['start'],
+ dataset: 'endpoint.events.security',
+ outcome: 'success',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'SYSTEM',
+ },
+ },
+ },
+ {
+ _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005',
+ _id: 'rBymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=',
+ executable: 'C:\\Windows\\System32\\lsass.exe',
+ },
+ message: 'Endpoint security event',
+ '@timestamp': '2021-02-08T21:50:28.3381013Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.security',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293868,
+ ingested: '2021-02-08T21:57:26.417632166Z',
+ created: '2021-02-08T21:50:28.3381013Z',
+ kind: 'event',
+ module: 'endpoint',
+ id: 'LzzWB9jjGmCwGMvk++++FG/Q',
+ category: [],
+ type: [],
+ dataset: 'endpoint.events.security',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'SYSTEM',
+ },
+ },
+ },
+ {
+ _index: '.ds-logs-endpoint.events.process-default-2021.02.02-000005',
+ _id: 'pxymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTIzODAtMTMyNTUwNzg2ODkuOTY1Nzg1NTAw',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU2OC0xMzI1NTA3ODY2Ny4zMjk3MDY2MDA=',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ code_signature: [
+ {
+ trusted: true,
+ subject_name: 'Microsoft Corporation',
+ exists: true,
+ status: 'trusted',
+ },
+ ],
+ token: {
+ integrity_level_name: 'high',
+ elevation_level: 'default',
+ },
+ },
+ args: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe', '-y'],
+ parent: {
+ args: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe', '-R'],
+ name: 'sshd.exe',
+ pid: 5284,
+ args_count: 2,
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw',
+ command_line: '"C:\\Program Files\\OpenSSH-Win64\\sshd.exe" -R',
+ executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe',
+ },
+ code_signature: {
+ trusted: true,
+ subject_name: 'Microsoft Corporation',
+ exists: true,
+ status: 'trusted',
+ },
+ name: 'sshd.exe',
+ pid: 6368,
+ args_count: 2,
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTYzNjgtMTMyNTcyOTQ2MjguMzQ0NjM1NTAw',
+ command_line: '"C:\\Program Files\\OpenSSH-Win64\\sshd.exe" -y',
+ executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe',
+ hash: {
+ sha1: '631244d731f406394c17c7dfd85203e317c74814',
+ sha256: 'e6a972f9db27de18be225095b3b3141b945be8aadc4014c8704ae5acafe3e8e0',
+ md5: '331ba0e529810ef718dd3efbd1242302',
+ },
+ },
+ message: 'Endpoint process event',
+ '@timestamp': '2021-02-08T21:50:28.3446355Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.process',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293863,
+ ingested: '2021-02-08T21:57:26.417387865Z',
+ created: '2021-02-08T21:50:28.3446355Z',
+ kind: 'event',
+ module: 'endpoint',
+ action: 'start',
+ id: 'LzzWB9jjGmCwGMvk++++FG/K',
+ category: ['process'],
+ type: ['start'],
+ dataset: 'endpoint.events.process',
+ },
+ user: {
+ domain: '',
+ name: '',
+ },
+ },
+ },
+ ],
+ },
+ {
+ join_keys: ['win2019-endpoint-mr-pedro'],
+ events: [
+ {
+ _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005',
+ _id: 'rBymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=',
+ executable: 'C:\\Windows\\System32\\lsass.exe',
+ },
+ message: 'Endpoint security event',
+ '@timestamp': '2021-02-08T21:50:28.3381013Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.security',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293868,
+ ingested: '2021-02-08T21:57:26.417632166Z',
+ created: '2021-02-08T21:50:28.3381013Z',
+ kind: 'event',
+ module: 'endpoint',
+ id: 'LzzWB9jjGmCwGMvk++++FG/Q',
+ category: [],
+ type: [],
+ dataset: 'endpoint.events.security',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'SYSTEM',
+ },
+ },
+ },
+ {
+ _index: '.ds-logs-endpoint.events.process-default-2021.02.02-000005',
+ _id: 'pxymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTIzODAtMTMyNTUwNzg2ODkuOTY1Nzg1NTAw',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU2OC0xMzI1NTA3ODY2Ny4zMjk3MDY2MDA=',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ code_signature: [
+ {
+ trusted: true,
+ subject_name: 'Microsoft Corporation',
+ exists: true,
+ status: 'trusted',
+ },
+ ],
+ token: {
+ integrity_level_name: 'high',
+ elevation_level: 'default',
+ },
+ },
+ args: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe', '-y'],
+ parent: {
+ args: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe', '-R'],
+ name: 'sshd.exe',
+ pid: 5284,
+ args_count: 2,
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw',
+ command_line: '"C:\\Program Files\\OpenSSH-Win64\\sshd.exe" -R',
+ executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe',
+ },
+ code_signature: {
+ trusted: true,
+ subject_name: 'Microsoft Corporation',
+ exists: true,
+ status: 'trusted',
+ },
+ name: 'sshd.exe',
+ pid: 6368,
+ args_count: 2,
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTYzNjgtMTMyNTcyOTQ2MjguMzQ0NjM1NTAw',
+ command_line: '"C:\\Program Files\\OpenSSH-Win64\\sshd.exe" -y',
+ executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe',
+ hash: {
+ sha1: '631244d731f406394c17c7dfd85203e317c74814',
+ sha256: 'e6a972f9db27de18be225095b3b3141b945be8aadc4014c8704ae5acafe3e8e0',
+ md5: '331ba0e529810ef718dd3efbd1242302',
+ },
+ },
+ message: 'Endpoint process event',
+ '@timestamp': '2021-02-08T21:50:28.3446355Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.process',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293863,
+ ingested: '2021-02-08T21:57:26.417387865Z',
+ created: '2021-02-08T21:50:28.3446355Z',
+ kind: 'event',
+ module: 'endpoint',
+ action: 'start',
+ id: 'LzzWB9jjGmCwGMvk++++FG/K',
+ category: ['process'],
+ type: ['start'],
+ dataset: 'endpoint.events.process',
+ },
+ user: {
+ domain: '',
+ name: '',
+ },
+ },
+ },
+ {
+ _index: '.ds-logs-endpoint.events.network-default-2021.02.02-000005',
+ _id: 'qBymg3cBX5UUcOOYP3Ec',
+ _source: {
+ agent: {
+ id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2',
+ type: 'endpoint',
+ version: '7.10.0',
+ },
+ process: {
+ Ext: {
+ ancestry: [
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU2OC0xMzI1NTA3ODY2Ny4zMjk3MDY2MDA=',
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=',
+ ],
+ },
+ name: 'svchost.exe',
+ pid: 968,
+ entity_id:
+ 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTk2OC0xMzI1NTA3ODY3My4yNjQyNDcyMDA=',
+ executable: 'C:\\Windows\\System32\\svchost.exe',
+ },
+ destination: {
+ address: '10.128.0.57',
+ port: 3389,
+ bytes: 1681,
+ ip: '10.128.0.57',
+ },
+ source: {
+ address: '142.202.189.139',
+ port: 16151,
+ bytes: 1224,
+ ip: '142.202.189.139',
+ },
+ message: 'Endpoint network event',
+ network: {
+ transport: 'tcp',
+ type: 'ipv4',
+ direction: 'incoming',
+ },
+ '@timestamp': '2021-02-08T21:50:28.5553532Z',
+ ecs: {
+ version: '1.5.0',
+ },
+ data_stream: {
+ namespace: 'default',
+ type: 'logs',
+ dataset: 'endpoint.events.network',
+ },
+ elastic: {
+ agent: {
+ id: 'f5dec71e-438c-424e-ac9b-0281f10412b9',
+ },
+ },
+ host: {
+ hostname: 'win2019-endpoint-mr-pedro',
+ os: {
+ Ext: {
+ variant: 'Windows Server 2019 Datacenter',
+ },
+ kernel: '1809 (10.0.17763.1697)',
+ name: 'Windows',
+ family: 'windows',
+ version: '1809 (10.0.17763.1697)',
+ platform: 'windows',
+ full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)',
+ },
+ ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'],
+ name: 'win2019-endpoint-mr-pedro',
+ id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d',
+ mac: ['42:01:0a:80:00:39'],
+ architecture: 'x86_64',
+ },
+ event: {
+ sequence: 3293864,
+ ingested: '2021-02-08T21:57:26.417451347Z',
+ created: '2021-02-08T21:50:28.5553532Z',
+ kind: 'event',
+ module: 'endpoint',
+ action: 'disconnect_received',
+ id: 'LzzWB9jjGmCwGMvk++++FG/L',
+ category: ['network'],
+ type: ['end'],
+ dataset: 'endpoint.events.network',
+ },
+ user: {
+ domain: 'NT AUTHORITY',
+ name: 'NETWORK SERVICE',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ statusCode: 200,
+ headers: {},
+ meta: {},
+ hits: {},
+ },
+} as unknown) as EqlSearchStrategyResponse>;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts
index 6529c594dd5a5..da5c89a3102a1 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts
@@ -7,10 +7,8 @@
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
-
-import { sequenceResponse } from '../../../search_strategy/timeline/eql/__mocks__';
-
import { createEqlAlertType } from './eql';
+import { sequenceResponse } from './__mocks__/eql';
import { createRuleTypeMocks } from './__mocks__/rule_type';
describe('EQL alerts', () => {
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts
index 94e70e4eb001b..3a37a49d03dcd 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts
@@ -7,13 +7,20 @@
import { AuthenticatedUser } from '../../../../../../security/common/model';
-import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
+import { TimelineStatus, TimelineType, SavedTimeline } from '../../../../../common/types/timeline';
+import { NoteSavedObject } from '../../../../../common/types/timeline/note';
import { pickSavedTimeline } from './pick_saved_timeline';
describe('pickSavedTimeline', () => {
const mockDateNow = new Date('2020-04-03T23:00:00.000Z').valueOf();
- const getMockSavedTimeline = () => ({
+ const getMockSavedTimeline = (): SavedTimeline & {
+ savedObjectId?: string | null;
+ version?: string;
+ eventNotes?: NoteSavedObject[];
+ globalNotes?: NoteSavedObject[];
+ pinnedEventIds?: [];
+ } => ({
savedObjectId: '7af80430-03f4-11eb-9d9d-ffba20fabba8',
version: 'WzQ0ODgsMV0=',
created: 1601563413330,
@@ -91,7 +98,7 @@ describe('pickSavedTimeline', () => {
test('Updating a timeline', () => {
const savedTimeline = getMockSavedTimeline();
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -113,7 +120,7 @@ describe('pickSavedTimeline', () => {
test('Updating a timeline', () => {
const savedTimeline = getMockSavedTimeline();
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -143,7 +150,7 @@ describe('pickSavedTimeline', () => {
test('Updating a timeline with a new title', () => {
const savedTimeline = getMockSavedTimeline();
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -152,7 +159,7 @@ describe('pickSavedTimeline', () => {
test('Updating a timeline without title', () => {
const savedTimeline = getMockSavedTimeline();
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -161,7 +168,7 @@ describe('pickSavedTimeline', () => {
test('Updating an immutable timeline with a new title', () => {
const savedTimeline = { ...getMockSavedTimeline(), status: TimelineStatus.immutable };
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -192,7 +199,7 @@ describe('pickSavedTimeline', () => {
test('Updating an untitled draft timeline with a title', () => {
const savedTimeline = { ...getMockSavedTimeline(), status: TimelineStatus.draft };
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -201,7 +208,7 @@ describe('pickSavedTimeline', () => {
test('Updating a draft timeline with a new title', () => {
const savedTimeline = { ...getMockSavedTimeline(), status: TimelineStatus.draft };
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
@@ -210,7 +217,7 @@ describe('pickSavedTimeline', () => {
test('Updating a draft timeline without title', () => {
const savedTimeline = { ...getMockSavedTimeline(), status: TimelineStatus.draft };
- const timelineId = savedTimeline.savedObjectId;
+ const timelineId = savedTimeline.savedObjectId ?? null;
const userInfo = { username: 'elastic' } as AuthenticatedUser;
const result = pickSavedTimeline(timelineId, savedTimeline, userInfo);
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts
index a28084cd78154..3e00a33966f17 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts
@@ -12,10 +12,9 @@ import { SavedTimeline, TimelineType, TimelineStatus } from '../../../../../comm
export const pickSavedTimeline = (
timelineId: string | null,
- savedTimeline: SavedTimeline,
+ savedTimeline: SavedTimeline & { savedObjectId?: string | null },
userInfo: AuthenticatedUser | null
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
-): any => {
+): SavedTimeline & { savedObjectId?: string | null } => {
const dateNow = new Date().valueOf();
if (timelineId == null) {
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index ac9d854f18211..4bcbcb71d048c 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -83,8 +83,6 @@ import { initUsageCollectors } from './usage';
import type { SecuritySolutionRequestHandlerContext } from './types';
import { registerTrustedAppsRoutes } from './endpoint/routes/trusted_apps';
import { securitySolutionSearchStrategyProvider } from './search_strategy/security_solution';
-import { securitySolutionIndexFieldsProvider } from './search_strategy/index_fields';
-import { securitySolutionTimelineSearchStrategyProvider } from './search_strategy/timeline';
import { TelemetryEventsSender } from './lib/telemetry/sender';
import {
TelemetryPluginStart,
@@ -92,7 +90,6 @@ import {
} from '../../../../src/plugins/telemetry/server';
import { licenseService } from './lib/license';
import { PolicyWatcher } from './endpoint/lib/policy/license_watch';
-import { securitySolutionTimelineEqlSearchStrategyProvider } from './search_strategy/timeline/eql';
import { parseExperimentalConfigValue } from '../common/experimental_features';
import { migrateArtifactsToFleet } from './endpoint/lib/artifacts/migrate_artifacts_to_fleet';
@@ -451,30 +448,10 @@ export class Plugin implements IPlugin {
- describe('#formatTimelineData', () => {
- it('happy path', async () => {
- const res = await formatTimelineData(
- [
- '@timestamp',
- 'host.name',
- 'destination.ip',
- 'source.ip',
- 'source.geo.location',
- 'threat.indicator.matched.field',
- ],
- TIMELINE_EVENTS_FIELDS,
- eventHit
- );
- expect(res).toEqual({
- cursor: {
- tiebreaker: 'beats-ci-immutable-ubuntu-1804-1605624279743236239',
- value: '1605624488922',
- },
- node: {
- _id: 'tkCt1nUBaEgqnrVSZ8R_',
- _index: 'auditbeat-7.8.0-2020.11.05-000003',
- data: [
- {
- field: '@timestamp',
- value: ['2020-11-17T14:48:08.922Z'],
- },
- {
- field: 'host.name',
- value: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
- },
- {
- field: 'threat.indicator.matched.field',
- value: ['matched_field', 'other_matched_field', 'matched_field_2'],
- },
- {
- field: 'source.geo.location',
- value: [`{"lon":118.7778,"lat":32.0617}`],
- },
- ],
- ecs: {
- '@timestamp': ['2020-11-17T14:48:08.922Z'],
- _id: 'tkCt1nUBaEgqnrVSZ8R_',
- _index: 'auditbeat-7.8.0-2020.11.05-000003',
- agent: {
- type: ['auditbeat'],
- },
- event: {
- action: ['process_started'],
- category: ['process'],
- dataset: ['process'],
- kind: ['event'],
- module: ['system'],
- type: ['start'],
- },
- host: {
- id: ['e59991e835905c65ed3e455b33e13bd6'],
- ip: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'],
- name: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
- os: {
- family: ['debian'],
- },
- },
- message: ['Process go (PID: 4313) by user jenkins STARTED'],
- process: {
- args: ['go', 'vet', './...'],
- entity_id: ['Z59cIkAAIw8ZoK0H'],
- executable: [
- '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go',
- ],
- hash: {
- sha1: ['1eac22336a41e0660fb302add9d97daa2bcc7040'],
- },
- name: ['go'],
- pid: ['4313'],
- ppid: ['3977'],
- working_directory: [
- '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat',
- ],
- },
- timestamp: '2020-11-17T14:48:08.922Z',
- user: {
- name: ['jenkins'],
- },
- threat: {
- indicator: [
- {
- event: {
- dataset: [],
- reference: [],
- },
- matched: {
- atomic: ['matched_atomic'],
- field: ['matched_field', 'other_matched_field'],
- type: [],
- },
- provider: ['yourself'],
- },
- {
- event: {
- dataset: [],
- reference: [],
- },
- matched: {
- atomic: ['matched_atomic_2'],
- field: ['matched_field_2'],
- type: [],
- },
- provider: ['other_you'],
- },
- ],
- },
- },
- },
- });
- });
-
- it('rule signal results', async () => {
- const response: EventHit = {
- _index: '.siem-signals-patrykkopycinski-default-000007',
- _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562',
- _score: 0,
- _source: {
- signal: {
- threshold_result: {
- count: 10000,
- value: '2a990c11-f61b-4c8e-b210-da2574e9f9db',
- },
- parent: {
- depth: 0,
- index:
- 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*',
- id: '0268af90-d8da-576a-9747-2a191519416a',
- type: 'event',
- },
- depth: 1,
- _meta: {
- version: 14,
- },
- rule: {
- note: null,
- throttle: null,
- references: [],
- severity_mapping: [],
- description: 'asdasd',
- created_at: '2021-01-09T11:25:45.046Z',
- language: 'kuery',
- threshold: {
- field: '',
- value: 200,
- },
- building_block_type: null,
- output_index: '.siem-signals-patrykkopycinski-default',
- type: 'threshold',
- rule_name_override: null,
- enabled: true,
- exceptions_list: [],
- updated_at: '2021-01-09T13:36:39.204Z',
- timestamp_override: null,
- from: 'now-360s',
- id: '696c24e0-526d-11eb-836c-e1620268b945',
- timeline_id: null,
- max_signals: 100,
- severity: 'low',
- risk_score: 21,
- risk_score_mapping: [],
- author: [],
- query: '_id :*',
- index: [
- 'apm-*-transaction*',
- 'auditbeat-*',
- 'endgame-*',
- 'filebeat-*',
- 'logs-*',
- 'packetbeat-*',
- 'winlogbeat-*',
- ],
- filters: [
- {
- $state: {
- store: 'appState',
- },
- meta: {
- negate: false,
- alias: null,
- disabled: false,
- type: 'exists',
- value: 'exists',
- key: '_index',
- },
- exists: {
- field: '_index',
- },
- },
- {
- $state: {
- store: 'appState',
- },
- meta: {
- negate: false,
- alias: 'id_exists',
- disabled: false,
- type: 'exists',
- value: 'exists',
- key: '_id',
- },
- exists: {
- field: '_id',
- },
- },
- ],
- created_by: 'patryk_test_user',
- version: 1,
- saved_id: null,
- tags: [],
- rule_id: '2a990c11-f61b-4c8e-b210-da2574e9f9db',
- license: '',
- immutable: false,
- timeline_title: null,
- meta: {
- from: '1m',
- kibana_siem_app_url: 'http://localhost:5601/app/security',
- },
- name: 'Threshold test',
- updated_by: 'patryk_test_user',
- interval: '5m',
- false_positives: [],
- to: 'now',
- threat: [],
- actions: [],
- },
- original_time: '2021-01-09T13:39:32.595Z',
- ancestors: [
- {
- depth: 0,
- index:
- 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*',
- id: '0268af90-d8da-576a-9747-2a191519416a',
- type: 'event',
- },
- ],
- parents: [
- {
- depth: 0,
- index:
- 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*',
- id: '0268af90-d8da-576a-9747-2a191519416a',
- type: 'event',
- },
- ],
- status: 'open',
- },
- },
- fields: {
- 'signal.rule.output_index': ['.siem-signals-patrykkopycinski-default'],
- 'signal.rule.from': ['now-360s'],
- 'signal.rule.language': ['kuery'],
- '@timestamp': ['2021-01-09T13:41:40.517Z'],
- 'signal.rule.query': ['_id :*'],
- 'signal.rule.type': ['threshold'],
- 'signal.rule.id': ['696c24e0-526d-11eb-836c-e1620268b945'],
- 'signal.rule.risk_score': [21],
- 'signal.status': ['open'],
- 'event.kind': ['signal'],
- 'signal.original_time': ['2021-01-09T13:39:32.595Z'],
- 'signal.rule.severity': ['low'],
- 'signal.rule.version': ['1'],
- 'signal.rule.index': [
- 'apm-*-transaction*',
- 'auditbeat-*',
- 'endgame-*',
- 'filebeat-*',
- 'logs-*',
- 'packetbeat-*',
- 'winlogbeat-*',
- ],
- 'signal.rule.name': ['Threshold test'],
- 'signal.rule.to': ['now'],
- },
- _type: '',
- sort: ['1610199700517'],
- aggregations: {},
- };
-
- expect(
- await formatTimelineData(
- ['@timestamp', 'host.name', 'destination.ip', 'source.ip'],
- TIMELINE_EVENTS_FIELDS,
- response
- )
- ).toEqual({
- cursor: {
- tiebreaker: null,
- value: '',
- },
- node: {
- _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562',
- _index: '.siem-signals-patrykkopycinski-default-000007',
- data: [
- {
- field: '@timestamp',
- value: ['2021-01-09T13:41:40.517Z'],
- },
- ],
- ecs: {
- '@timestamp': ['2021-01-09T13:41:40.517Z'],
- timestamp: '2021-01-09T13:41:40.517Z',
- _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562',
- _index: '.siem-signals-patrykkopycinski-default-000007',
- event: {
- kind: ['signal'],
- },
- signal: {
- original_time: ['2021-01-09T13:39:32.595Z'],
- status: ['open'],
- threshold_result: ['{"count":10000,"value":"2a990c11-f61b-4c8e-b210-da2574e9f9db"}'],
- rule: {
- building_block_type: [],
- exceptions_list: [],
- from: ['now-360s'],
- id: ['696c24e0-526d-11eb-836c-e1620268b945'],
- index: [
- 'apm-*-transaction*',
- 'auditbeat-*',
- 'endgame-*',
- 'filebeat-*',
- 'logs-*',
- 'packetbeat-*',
- 'winlogbeat-*',
- ],
- language: ['kuery'],
- name: ['Threshold test'],
- output_index: ['.siem-signals-patrykkopycinski-default'],
- risk_score: ['21'],
- query: ['_id :*'],
- severity: ['low'],
- to: ['now'],
- type: ['threshold'],
- version: ['1'],
- timeline_id: [],
- timeline_title: [],
- saved_id: [],
- note: [],
- threshold: [
- JSON.stringify({
- field: '',
- value: 200,
- }),
- ],
- filters: [
- JSON.stringify({
- $state: {
- store: 'appState',
- },
- meta: {
- negate: false,
- alias: null,
- disabled: false,
- type: 'exists',
- value: 'exists',
- key: '_index',
- },
- exists: {
- field: '_index',
- },
- }),
- JSON.stringify({
- $state: {
- store: 'appState',
- },
- meta: {
- negate: false,
- alias: 'id_exists',
- disabled: false,
- type: 'exists',
- value: 'exists',
- key: '_id',
- },
- exists: {
- field: '_id',
- },
- }),
- ],
- },
- },
- },
- },
- });
- });
- });
-
- describe('#buildObjectForFieldPath', () => {
- it('builds an object from a single non-nested field', () => {
- expect(buildObjectForFieldPath('@timestamp', eventHit)).toEqual({
- '@timestamp': ['2020-11-17T14:48:08.922Z'],
- });
- });
-
- it('builds an object with no fields response', () => {
- const { fields, ...fieldLessHit } = eventHit;
- // @ts-expect-error fieldLessHit is intentionally missing fields
- expect(buildObjectForFieldPath('@timestamp', fieldLessHit)).toEqual({
- '@timestamp': [],
- });
- });
-
- it('does not misinterpret non-nested fields with a common prefix', () => {
- // @ts-expect-error hit is minimal
- const hit: EventHit = {
- fields: {
- 'foo.bar': ['baz'],
- 'foo.barBaz': ['foo'],
- },
- };
-
- expect(buildObjectForFieldPath('foo.barBaz', hit)).toEqual({
- foo: { barBaz: ['foo'] },
- });
- });
-
- it('builds an array of objects from a nested field', () => {
- // @ts-expect-error hit is minimal
- const hit: EventHit = {
- fields: {
- foo: [{ bar: ['baz'] }],
- },
- };
- expect(buildObjectForFieldPath('foo.bar', hit)).toEqual({
- foo: [{ bar: ['baz'] }],
- });
- });
-
- it('builds intermediate objects for nested fields', () => {
- // @ts-expect-error nestedHit is minimal
- const nestedHit: EventHit = {
- fields: {
- 'foo.bar': [
- {
- baz: ['host.name'],
- },
- ],
- },
- };
- expect(buildObjectForFieldPath('foo.bar.baz', nestedHit)).toEqual({
- foo: {
- bar: [
- {
- baz: ['host.name'],
- },
- ],
- },
- });
- });
-
- it('builds intermediate objects at multiple levels', () => {
- expect(buildObjectForFieldPath('threat.indicator.matched.atomic', eventHit)).toEqual({
- threat: {
- indicator: [
- {
- matched: {
- atomic: ['matched_atomic'],
- },
- },
- {
- matched: {
- atomic: ['matched_atomic_2'],
- },
- },
- ],
- },
- });
- });
-
- it('preserves multiple values for a single leaf', () => {
- expect(buildObjectForFieldPath('threat.indicator.matched.field', eventHit)).toEqual({
- threat: {
- indicator: [
- {
- matched: {
- field: ['matched_field', 'other_matched_field'],
- },
- },
- {
- matched: {
- field: ['matched_field_2'],
- },
- },
- ],
- },
- });
- });
-
- describe('multiple levels of nested fields', () => {
- let nestedHit: EventHit;
-
- beforeEach(() => {
- // @ts-expect-error nestedHit is minimal
- nestedHit = {
- fields: {
- 'nested_1.foo': [
- {
- 'nested_2.bar': [
- { leaf: ['leaf_value'], leaf_2: ['leaf_2_value'] },
- { leaf_2: ['leaf_2_value_2', 'leaf_2_value_3'] },
- ],
- },
- {
- 'nested_2.bar': [
- { leaf: ['leaf_value_2'], leaf_2: ['leaf_2_value_4'] },
- { leaf: ['leaf_value_3'], leaf_2: ['leaf_2_value_5'] },
- ],
- },
- ],
- },
- };
- });
-
- it('includes objects without the field', () => {
- expect(buildObjectForFieldPath('nested_1.foo.nested_2.bar.leaf', nestedHit)).toEqual({
- nested_1: {
- foo: [
- {
- nested_2: {
- bar: [{ leaf: ['leaf_value'] }, { leaf: [] }],
- },
- },
- {
- nested_2: {
- bar: [{ leaf: ['leaf_value_2'] }, { leaf: ['leaf_value_3'] }],
- },
- },
- ],
- },
- });
- });
-
- it('groups multiple leaf values', () => {
- expect(buildObjectForFieldPath('nested_1.foo.nested_2.bar.leaf_2', nestedHit)).toEqual({
- nested_1: {
- foo: [
- {
- nested_2: {
- bar: [
- { leaf_2: ['leaf_2_value'] },
- { leaf_2: ['leaf_2_value_2', 'leaf_2_value_3'] },
- ],
- },
- },
- {
- nested_2: {
- bar: [{ leaf_2: ['leaf_2_value_4'] }, { leaf_2: ['leaf_2_value_5'] }],
- },
- },
- ],
- },
- });
- });
- });
- });
-
- describe('#buildFieldsRequest', () => {
- it('happy path', async () => {
- const res = await buildFieldsRequest([
- '@timestamp',
- 'host.name',
- 'destination.ip',
- 'source.ip',
- 'source.geo.location',
- 'threat.indicator.matched.field',
- ]);
- expect(res).toEqual([
- {
- field: '@timestamp',
- include_unmapped: true,
- },
- {
- field: 'host.name',
- include_unmapped: true,
- },
- {
- field: 'destination.ip',
- include_unmapped: true,
- },
- {
- field: 'source.ip',
- include_unmapped: true,
- },
- {
- field: 'source.geo.location',
- include_unmapped: true,
- },
- {
- field: 'threat.indicator.matched.field',
- include_unmapped: true,
- },
- {
- field: 'signal.status',
- include_unmapped: true,
- },
- {
- field: 'signal.group.id',
- include_unmapped: true,
- },
- {
- field: 'signal.original_time',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.filters',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.from',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.language',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.query',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.name',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.to',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.id',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.index',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.type',
- include_unmapped: true,
- },
- {
- field: 'signal.original_event.kind',
- include_unmapped: true,
- },
- {
- field: 'signal.original_event.module',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.version',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.severity',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.risk_score',
- include_unmapped: true,
- },
- {
- field: 'signal.threshold_result',
- include_unmapped: true,
- },
- {
- field: 'event.code',
- include_unmapped: true,
- },
- {
- field: 'event.module',
- include_unmapped: true,
- },
- {
- field: 'event.action',
- include_unmapped: true,
- },
- {
- field: 'event.category',
- include_unmapped: true,
- },
- {
- field: 'user.name',
- include_unmapped: true,
- },
- {
- field: 'message',
- include_unmapped: true,
- },
- {
- field: 'system.auth.ssh.signature',
- include_unmapped: true,
- },
- {
- field: 'system.auth.ssh.method',
- include_unmapped: true,
- },
- {
- field: 'system.audit.package.arch',
- include_unmapped: true,
- },
- {
- field: 'system.audit.package.entity_id',
- include_unmapped: true,
- },
- {
- field: 'system.audit.package.name',
- include_unmapped: true,
- },
- {
- field: 'system.audit.package.size',
- include_unmapped: true,
- },
- {
- field: 'system.audit.package.summary',
- include_unmapped: true,
- },
- {
- field: 'system.audit.package.version',
- include_unmapped: true,
- },
- {
- field: 'event.created',
- include_unmapped: true,
- },
- {
- field: 'event.dataset',
- include_unmapped: true,
- },
- {
- field: 'event.duration',
- include_unmapped: true,
- },
- {
- field: 'event.end',
- include_unmapped: true,
- },
- {
- field: 'event.hash',
- include_unmapped: true,
- },
- {
- field: 'event.id',
- include_unmapped: true,
- },
- {
- field: 'event.kind',
- include_unmapped: true,
- },
- {
- field: 'event.original',
- include_unmapped: true,
- },
- {
- field: 'event.outcome',
- include_unmapped: true,
- },
- {
- field: 'event.risk_score',
- include_unmapped: true,
- },
- {
- field: 'event.risk_score_norm',
- include_unmapped: true,
- },
- {
- field: 'event.severity',
- include_unmapped: true,
- },
- {
- field: 'event.start',
- include_unmapped: true,
- },
- {
- field: 'event.timezone',
- include_unmapped: true,
- },
- {
- field: 'event.type',
- include_unmapped: true,
- },
- {
- field: 'agent.type',
- include_unmapped: true,
- },
- {
- field: 'auditd.result',
- include_unmapped: true,
- },
- {
- field: 'auditd.session',
- include_unmapped: true,
- },
- {
- field: 'auditd.data.acct',
- include_unmapped: true,
- },
- {
- field: 'auditd.data.terminal',
- include_unmapped: true,
- },
- {
- field: 'auditd.data.op',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.actor.primary',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.actor.secondary',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.object.primary',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.object.secondary',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.object.type',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.how',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.message_type',
- include_unmapped: true,
- },
- {
- field: 'auditd.summary.sequence',
- include_unmapped: true,
- },
- {
- field: 'file.Ext.original.path',
- include_unmapped: true,
- },
- {
- field: 'file.name',
- include_unmapped: true,
- },
- {
- field: 'file.target_path',
- include_unmapped: true,
- },
- {
- field: 'file.extension',
- include_unmapped: true,
- },
- {
- field: 'file.type',
- include_unmapped: true,
- },
- {
- field: 'file.device',
- include_unmapped: true,
- },
- {
- field: 'file.inode',
- include_unmapped: true,
- },
- {
- field: 'file.uid',
- include_unmapped: true,
- },
- {
- field: 'file.owner',
- include_unmapped: true,
- },
- {
- field: 'file.gid',
- include_unmapped: true,
- },
- {
- field: 'file.group',
- include_unmapped: true,
- },
- {
- field: 'file.mode',
- include_unmapped: true,
- },
- {
- field: 'file.size',
- include_unmapped: true,
- },
- {
- field: 'file.mtime',
- include_unmapped: true,
- },
- {
- field: 'file.ctime',
- include_unmapped: true,
- },
- {
- field: 'file.path',
- include_unmapped: true,
- },
- {
- field: 'file.Ext.code_signature',
- include_unmapped: true,
- },
- {
- field: 'file.Ext.code_signature.subject_name',
- include_unmapped: true,
- },
- {
- field: 'file.Ext.code_signature.trusted',
- include_unmapped: true,
- },
- {
- field: 'file.hash.sha256',
- include_unmapped: true,
- },
- {
- field: 'host.os.family',
- include_unmapped: true,
- },
- {
- field: 'host.id',
- include_unmapped: true,
- },
- {
- field: 'host.ip',
- include_unmapped: true,
- },
- {
- field: 'registry.key',
- include_unmapped: true,
- },
- {
- field: 'registry.path',
- include_unmapped: true,
- },
- {
- field: 'rule.reference',
- include_unmapped: true,
- },
- {
- field: 'source.bytes',
- include_unmapped: true,
- },
- {
- field: 'source.packets',
- include_unmapped: true,
- },
- {
- field: 'source.port',
- include_unmapped: true,
- },
- {
- field: 'source.geo.continent_name',
- include_unmapped: true,
- },
- {
- field: 'source.geo.country_name',
- include_unmapped: true,
- },
- {
- field: 'source.geo.country_iso_code',
- include_unmapped: true,
- },
- {
- field: 'source.geo.city_name',
- include_unmapped: true,
- },
- {
- field: 'source.geo.region_iso_code',
- include_unmapped: true,
- },
- {
- field: 'source.geo.region_name',
- include_unmapped: true,
- },
- {
- field: 'destination.bytes',
- include_unmapped: true,
- },
- {
- field: 'destination.packets',
- include_unmapped: true,
- },
- {
- field: 'destination.port',
- include_unmapped: true,
- },
- {
- field: 'destination.geo.continent_name',
- include_unmapped: true,
- },
- {
- field: 'destination.geo.country_name',
- include_unmapped: true,
- },
- {
- field: 'destination.geo.country_iso_code',
- include_unmapped: true,
- },
- {
- field: 'destination.geo.city_name',
- include_unmapped: true,
- },
- {
- field: 'destination.geo.region_iso_code',
- include_unmapped: true,
- },
- {
- field: 'destination.geo.region_name',
- include_unmapped: true,
- },
- {
- field: 'dns.question.name',
- include_unmapped: true,
- },
- {
- field: 'dns.question.type',
- include_unmapped: true,
- },
- {
- field: 'dns.resolved_ip',
- include_unmapped: true,
- },
- {
- field: 'dns.response_code',
- include_unmapped: true,
- },
- {
- field: 'endgame.exit_code',
- include_unmapped: true,
- },
- {
- field: 'endgame.file_name',
- include_unmapped: true,
- },
- {
- field: 'endgame.file_path',
- include_unmapped: true,
- },
- {
- field: 'endgame.logon_type',
- include_unmapped: true,
- },
- {
- field: 'endgame.parent_process_name',
- include_unmapped: true,
- },
- {
- field: 'endgame.pid',
- include_unmapped: true,
- },
- {
- field: 'endgame.process_name',
- include_unmapped: true,
- },
- {
- field: 'endgame.subject_domain_name',
- include_unmapped: true,
- },
- {
- field: 'endgame.subject_logon_id',
- include_unmapped: true,
- },
- {
- field: 'endgame.subject_user_name',
- include_unmapped: true,
- },
- {
- field: 'endgame.target_domain_name',
- include_unmapped: true,
- },
- {
- field: 'endgame.target_logon_id',
- include_unmapped: true,
- },
- {
- field: 'endgame.target_user_name',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.saved_id',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.timeline_id',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.timeline_title',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.output_index',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.note',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.threshold',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.exceptions_list',
- include_unmapped: true,
- },
- {
- field: 'signal.rule.building_block_type',
- include_unmapped: true,
- },
- {
- field: 'suricata.eve.proto',
- include_unmapped: true,
- },
- {
- field: 'suricata.eve.flow_id',
- include_unmapped: true,
- },
- {
- field: 'suricata.eve.alert.signature',
- include_unmapped: true,
- },
- {
- field: 'suricata.eve.alert.signature_id',
- include_unmapped: true,
- },
- {
- field: 'network.bytes',
- include_unmapped: true,
- },
- {
- field: 'network.community_id',
- include_unmapped: true,
- },
- {
- field: 'network.direction',
- include_unmapped: true,
- },
- {
- field: 'network.packets',
- include_unmapped: true,
- },
- {
- field: 'network.protocol',
- include_unmapped: true,
- },
- {
- field: 'network.transport',
- include_unmapped: true,
- },
- {
- field: 'http.version',
- include_unmapped: true,
- },
- {
- field: 'http.request.method',
- include_unmapped: true,
- },
- {
- field: 'http.request.body.bytes',
- include_unmapped: true,
- },
- {
- field: 'http.request.body.content',
- include_unmapped: true,
- },
- {
- field: 'http.request.referrer',
- include_unmapped: true,
- },
- {
- field: 'http.response.status_code',
- include_unmapped: true,
- },
- {
- field: 'http.response.body.bytes',
- include_unmapped: true,
- },
- {
- field: 'http.response.body.content',
- include_unmapped: true,
- },
- {
- field: 'tls.client_certificate.fingerprint.sha1',
- include_unmapped: true,
- },
- {
- field: 'tls.fingerprints.ja3.hash',
- include_unmapped: true,
- },
- {
- field: 'tls.server_certificate.fingerprint.sha1',
- include_unmapped: true,
- },
- {
- field: 'user.domain',
- include_unmapped: true,
- },
- {
- field: 'winlog.event_id',
- include_unmapped: true,
- },
- {
- field: 'process.exit_code',
- include_unmapped: true,
- },
- {
- field: 'process.hash.md5',
- include_unmapped: true,
- },
- {
- field: 'process.hash.sha1',
- include_unmapped: true,
- },
- {
- field: 'process.hash.sha256',
- include_unmapped: true,
- },
- {
- field: 'process.parent.name',
- include_unmapped: true,
- },
- {
- field: 'process.parent.pid',
- include_unmapped: true,
- },
- {
- field: 'process.pid',
- include_unmapped: true,
- },
- {
- field: 'process.name',
- include_unmapped: true,
- },
- {
- field: 'process.ppid',
- include_unmapped: true,
- },
- {
- field: 'process.args',
- include_unmapped: true,
- },
- {
- field: 'process.entity_id',
- include_unmapped: true,
- },
- {
- field: 'process.executable',
- include_unmapped: true,
- },
- {
- field: 'process.title',
- include_unmapped: true,
- },
- {
- field: 'process.working_directory',
- include_unmapped: true,
- },
- {
- field: 'zeek.session_id',
- include_unmapped: true,
- },
- {
- field: 'zeek.connection.local_resp',
- include_unmapped: true,
- },
- {
- field: 'zeek.connection.local_orig',
- include_unmapped: true,
- },
- {
- field: 'zeek.connection.missed_bytes',
- include_unmapped: true,
- },
- {
- field: 'zeek.connection.state',
- include_unmapped: true,
- },
- {
- field: 'zeek.connection.history',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.suppress_for',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.msg',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.note',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.sub',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.dst',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.dropped',
- include_unmapped: true,
- },
- {
- field: 'zeek.notice.peer_descr',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.AA',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.qclass_name',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.RD',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.qtype_name',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.qtype',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.query',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.trans_id',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.qclass',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.RA',
- include_unmapped: true,
- },
- {
- field: 'zeek.dns.TC',
- include_unmapped: true,
- },
- {
- field: 'zeek.http.resp_mime_types',
- include_unmapped: true,
- },
- {
- field: 'zeek.http.trans_depth',
- include_unmapped: true,
- },
- {
- field: 'zeek.http.status_msg',
- include_unmapped: true,
- },
- {
- field: 'zeek.http.resp_fuids',
- include_unmapped: true,
- },
- {
- field: 'zeek.http.tags',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.session_ids',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.timedout',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.local_orig',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.tx_host',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.source',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.is_orig',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.overflow_bytes',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.sha1',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.duration',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.depth',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.analyzers',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.mime_type',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.rx_host',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.total_bytes',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.fuid',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.seen_bytes',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.missing_bytes',
- include_unmapped: true,
- },
- {
- field: 'zeek.files.md5',
- include_unmapped: true,
- },
- {
- field: 'zeek.ssl.cipher',
- include_unmapped: true,
- },
- {
- field: 'zeek.ssl.established',
- include_unmapped: true,
- },
- {
- field: 'zeek.ssl.resumed',
- include_unmapped: true,
- },
- {
- field: 'zeek.ssl.version',
- include_unmapped: true,
- },
- {
- field: 'threat.indicator.matched.atomic',
- include_unmapped: true,
- },
- {
- field: 'threat.indicator.matched.type',
- include_unmapped: true,
- },
- {
- field: 'threat.indicator.event.dataset',
- include_unmapped: true,
- },
- {
- field: 'threat.indicator.event.reference',
- include_unmapped: true,
- },
- {
- field: 'threat.indicator.provider',
- include_unmapped: true,
- },
- ]);
- });
-
- it('remove internal attributes starting with _', async () => {
- const res = await buildFieldsRequest([
- '@timestamp',
- '_id',
- 'host.name',
- 'destination.ip',
- 'source.ip',
- 'source.geo.location',
- '_type',
- 'threat.indicator.matched.field',
- ]);
- expect(res.some((f) => f.field === '_id')).toEqual(false);
- expect(res.some((f) => f.field === '_type')).toEqual(false);
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json
index bebfd9ca88c23..0df41b9f988b7 100644
--- a/x-pack/plugins/security_solution/tsconfig.json
+++ b/x-pack/plugins/security_solution/tsconfig.json
@@ -42,5 +42,6 @@
{ "path": "../ml/tsconfig.json" },
{ "path": "../spaces/tsconfig.json" },
{ "path": "../security/tsconfig.json"},
+ { "path": "../timelines/tsconfig.json"},
]
}
diff --git a/x-pack/plugins/timelines/README.md b/x-pack/plugins/timelines/README.md
index 441a505903698..0c14953837d02 100644
--- a/x-pack/plugins/timelines/README.md
+++ b/x-pack/plugins/timelines/README.md
@@ -3,9 +3,9 @@ Timelines is a plugin that provides a grid component with accompanying server si
## Using timelines in another plugin
-- Add `TimelinesPluginSetup` to Kibana plugin `SetupServices` dependencies:
+- Add `TimelinesPluginUI` to Kibana plugin `SetupServices` dependencies:
```ts
-timelines: TimelinesPluginSetup;
+timelines: TimelinesPluginUI;
```
- Once `timelines` is added as a required plugin in the consuming plugin's kibana.json, timeline functionality will be available as any other kibana plugin, ie PluginSetupDependencies.timelines.getTimeline()
diff --git a/x-pack/plugins/timelines/common/constants.ts b/x-pack/plugins/timelines/common/constants.ts
new file mode 100644
index 0000000000000..86ff9d501f148
--- /dev/null
+++ b/x-pack/plugins/timelines/common/constants.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000;
diff --git a/x-pack/plugins/timelines/common/ecs/agent/index.ts b/x-pack/plugins/timelines/common/ecs/agent/index.ts
new file mode 100644
index 0000000000000..2332b60f1a3ca
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/agent/index.ts
@@ -0,0 +1,10 @@
+/*
+ * 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.
+ */
+
+export interface AgentEcs {
+ type?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/auditd/index.ts b/x-pack/plugins/timelines/common/ecs/auditd/index.ts
new file mode 100644
index 0000000000000..f210f8862dc44
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/auditd/index.ts
@@ -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.
+ */
+
+export interface AuditdEcs {
+ result?: string[];
+
+ session?: string[];
+
+ data?: AuditdDataEcs;
+
+ summary?: SummaryEcs;
+
+ sequence?: string[];
+}
+
+export interface AuditdDataEcs {
+ acct?: string[];
+
+ terminal?: string[];
+
+ op?: string[];
+}
+
+export interface SummaryEcs {
+ actor?: PrimarySecondaryEcs;
+
+ object?: PrimarySecondaryEcs;
+
+ how?: string[];
+
+ message_type?: string[];
+
+ sequence?: string[];
+}
+
+export interface PrimarySecondaryEcs {
+ primary?: string[];
+
+ secondary?: string[];
+
+ type?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/cloud/index.ts b/x-pack/plugins/timelines/common/ecs/cloud/index.ts
new file mode 100644
index 0000000000000..a169e5561c6b6
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/cloud/index.ts
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+export interface CloudEcs {
+ instance?: CloudInstanceEcs;
+ machine?: CloudMachineEcs;
+ provider?: string[];
+ region?: string[];
+}
+
+export interface CloudMachineEcs {
+ type?: string[];
+}
+
+export interface CloudInstanceEcs {
+ id?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/destination/index.ts b/x-pack/plugins/timelines/common/ecs/destination/index.ts
new file mode 100644
index 0000000000000..2d3b6154276b9
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/destination/index.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 { GeoEcs } from '../geo';
+
+export interface DestinationEcs {
+ bytes?: number[];
+
+ ip?: string[];
+
+ port?: number[];
+
+ domain?: string[];
+
+ geo?: GeoEcs;
+
+ packets?: number[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/dns/index.ts b/x-pack/plugins/timelines/common/ecs/dns/index.ts
new file mode 100644
index 0000000000000..e0f142d9cf57a
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/dns/index.ts
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+export interface DnsEcs {
+ question?: DnsQuestionEcs;
+
+ resolved_ip?: string[];
+
+ response_code?: string[];
+}
+
+export interface DnsQuestionEcs {
+ name?: string[];
+
+ type?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.test.ts b/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.test.ts
new file mode 100644
index 0000000000000..e27b15f021257
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.test.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 { extendMap } from './extend_map';
+
+describe('ecs_fields test', () => {
+ describe('extendMap', () => {
+ test('it should extend a record', () => {
+ const osFieldsMap: Readonly> = {
+ 'os.platform': 'os.platform',
+ 'os.full': 'os.full',
+ 'os.family': 'os.family',
+ 'os.version': 'os.version',
+ 'os.kernel': 'os.kernel',
+ };
+ const expected: Record = {
+ 'host.os.family': 'host.os.family',
+ 'host.os.full': 'host.os.full',
+ 'host.os.kernel': 'host.os.kernel',
+ 'host.os.platform': 'host.os.platform',
+ 'host.os.version': 'host.os.version',
+ };
+ expect(extendMap('host', osFieldsMap)).toEqual(expected);
+ });
+
+ test('it should extend a sample hosts record', () => {
+ const hostMap: Record = {
+ 'host.id': 'host.id',
+ 'host.ip': 'host.ip',
+ 'host.name': 'host.name',
+ };
+ const osFieldsMap: Readonly> = {
+ 'os.platform': 'os.platform',
+ 'os.full': 'os.full',
+ 'os.family': 'os.family',
+ 'os.version': 'os.version',
+ 'os.kernel': 'os.kernel',
+ };
+ const expected: Record = {
+ 'host.id': 'host.id',
+ 'host.ip': 'host.ip',
+ 'host.name': 'host.name',
+ 'host.os.family': 'host.os.family',
+ 'host.os.full': 'host.os.full',
+ 'host.os.kernel': 'host.os.kernel',
+ 'host.os.platform': 'host.os.platform',
+ 'host.os.version': 'host.os.version',
+ };
+ const output = { ...hostMap, ...extendMap('host', osFieldsMap) };
+ expect(output).toEqual(expected);
+ });
+ });
+});
diff --git a/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.ts b/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.ts
new file mode 100644
index 0000000000000..184e6b4f32566
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.ts
@@ -0,0 +1,15 @@
+/*
+ * 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.
+ */
+
+export const extendMap = (
+ path: string,
+ map: Readonly>
+): Readonly> =>
+ Object.entries(map).reduce>((accum, [key, value]) => {
+ accum[`${path}.${key}`] = `${path}.${value}`;
+ return accum;
+ }, {});
diff --git a/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts b/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts
new file mode 100644
index 0000000000000..292822019fc9c
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts
@@ -0,0 +1,359 @@
+/*
+ * 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 { extendMap } from './extend_map';
+
+export const auditdMap: Readonly> = {
+ 'auditd.result': 'auditd.result',
+ 'auditd.session': 'auditd.session',
+ 'auditd.data.acct': 'auditd.data.acct',
+ 'auditd.data.terminal': 'auditd.data.terminal',
+ 'auditd.data.op': 'auditd.data.op',
+ 'auditd.summary.actor.primary': 'auditd.summary.actor.primary',
+ 'auditd.summary.actor.secondary': 'auditd.summary.actor.secondary',
+ 'auditd.summary.object.primary': 'auditd.summary.object.primary',
+ 'auditd.summary.object.secondary': 'auditd.summary.object.secondary',
+ 'auditd.summary.object.type': 'auditd.summary.object.type',
+ 'auditd.summary.how': 'auditd.summary.how',
+ 'auditd.summary.message_type': 'auditd.summary.message_type',
+ 'auditd.summary.sequence': 'auditd.summary.sequence',
+};
+
+export const cloudFieldsMap: Readonly> = {
+ 'cloud.account.id': 'cloud.account.id',
+ 'cloud.availability_zone': 'cloud.availability_zone',
+ 'cloud.instance.id': 'cloud.instance.id',
+ 'cloud.instance.name': 'cloud.instance.name',
+ 'cloud.machine.type': 'cloud.machine.type',
+ 'cloud.provider': 'cloud.provider',
+ 'cloud.region': 'cloud.region',
+};
+
+export const fileMap: Readonly> = {
+ 'file.name': 'file.name',
+ 'file.path': 'file.path',
+ 'file.target_path': 'file.target_path',
+ 'file.extension': 'file.extension',
+ 'file.type': 'file.type',
+ 'file.device': 'file.device',
+ 'file.inode': 'file.inode',
+ 'file.uid': 'file.uid',
+ 'file.owner': 'file.owner',
+ 'file.gid': 'file.gid',
+ 'file.group': 'file.group',
+ 'file.mode': 'file.mode',
+ 'file.size': 'file.size',
+ 'file.mtime': 'file.mtime',
+ 'file.ctime': 'file.ctime',
+};
+
+export const osFieldsMap: Readonly> = {
+ 'os.platform': 'os.platform',
+ 'os.name': 'os.name',
+ 'os.full': 'os.full',
+ 'os.family': 'os.family',
+ 'os.version': 'os.version',
+ 'os.kernel': 'os.kernel',
+};
+
+export const hostFieldsMap: Readonly> = {
+ 'host.architecture': 'host.architecture',
+ 'host.id': 'host.id',
+ 'host.ip': 'host.ip',
+ 'host.mac': 'host.mac',
+ 'host.name': 'host.name',
+ ...extendMap('host', osFieldsMap),
+};
+
+export const processFieldsMap: Readonly> = {
+ 'process.hash.md5': 'process.hash.md5',
+ 'process.hash.sha1': 'process.hash.sha1',
+ 'process.hash.sha256': 'process.hash.sha256',
+ 'process.pid': 'process.pid',
+ 'process.name': 'process.name',
+ 'process.ppid': 'process.ppid',
+ 'process.args': 'process.args',
+ 'process.entity_id': 'process.entity_id',
+ 'process.executable': 'process.executable',
+ 'process.title': 'process.title',
+ 'process.thread': 'process.thread',
+ 'process.working_directory': 'process.working_directory',
+};
+
+export const agentFieldsMap: Readonly> = {
+ 'agent.type': 'agent.type',
+};
+
+export const userFieldsMap: Readonly> = {
+ 'user.domain': 'user.domain',
+ 'user.id': 'user.id',
+ 'user.name': 'user.name',
+ // NOTE: This field is not tested and available from ECS. Please remove this tag once it is
+ 'user.full_name': 'user.full_name',
+ // NOTE: This field is not tested and available from ECS. Please remove this tag once it is
+ 'user.email': 'user.email',
+ // NOTE: This field is not tested and available from ECS. Please remove this tag once it is
+ 'user.hash': 'user.hash',
+ // NOTE: This field is not tested and available from ECS. Please remove this tag once it is
+ 'user.group': 'user.group',
+};
+
+export const winlogFieldsMap: Readonly> = {
+ 'winlog.event_id': 'winlog.event_id',
+};
+
+export const suricataFieldsMap: Readonly> = {
+ 'suricata.eve.flow_id': 'suricata.eve.flow_id',
+ 'suricata.eve.proto': 'suricata.eve.proto',
+ 'suricata.eve.alert.signature': 'suricata.eve.alert.signature',
+ 'suricata.eve.alert.signature_id': 'suricata.eve.alert.signature_id',
+};
+
+export const tlsFieldsMap: Readonly> = {
+ 'tls.client_certificate.fingerprint.sha1': 'tls.client_certificate.fingerprint.sha1',
+ 'tls.fingerprints.ja3.hash': 'tls.fingerprints.ja3.hash',
+ 'tls.server_certificate.fingerprint.sha1': 'tls.server_certificate.fingerprint.sha1',
+};
+
+export const urlFieldsMap: Readonly> = {
+ 'url.original': 'url.original',
+ 'url.domain': 'url.domain',
+ 'user.username': 'user.username',
+ 'user.password': 'user.password',
+};
+
+export const httpFieldsMap: Readonly> = {
+ 'http.version': 'http.version',
+ 'http.request': 'http.request',
+ 'http.request.method': 'http.request.method',
+ 'http.request.body.bytes': 'http.request.body.bytes',
+ 'http.request.body.content': 'http.request.body.content',
+ 'http.request.referrer': 'http.request.referrer',
+ 'http.response.status_code': 'http.response.status_code',
+ 'http.response.body': 'http.response.body',
+ 'http.response.body.bytes': 'http.response.body.bytes',
+ 'http.response.body.content': 'http.response.body.content',
+};
+
+export const zeekFieldsMap: Readonly> = {
+ 'zeek.session_id': 'zeek.session_id',
+ 'zeek.connection.local_resp': 'zeek.connection.local_resp',
+ 'zeek.connection.local_orig': 'zeek.connection.local_orig',
+ 'zeek.connection.missed_bytes': 'zeek.connection.missed_bytes',
+ 'zeek.connection.state': 'zeek.connection.state',
+ 'zeek.connection.history': 'zeek.connection.history',
+ 'zeek.notice.suppress_for': 'zeek.notice.suppress_for',
+ 'zeek.notice.msg': 'zeek.notice.msg',
+ 'zeek.notice.note': 'zeek.notice.note',
+ 'zeek.notice.sub': 'zeek.notice.sub',
+ 'zeek.notice.dst': 'zeek.notice.dst',
+ 'zeek.notice.dropped': 'zeek.notice.dropped',
+ 'zeek.notice.peer_descr': 'zeek.notice.peer_descr',
+ 'zeek.dns.AA': 'zeek.dns.AA',
+ 'zeek.dns.qclass_name': 'zeek.dns.qclass_name',
+ 'zeek.dns.RD': 'zeek.dns.RD',
+ 'zeek.dns.qtype_name': 'zeek.dns.qtype_name',
+ 'zeek.dns.qtype': 'zeek.dns.qtype',
+ 'zeek.dns.query': 'zeek.dns.query',
+ 'zeek.dns.trans_id': 'zeek.dns.trans_id',
+ 'zeek.dns.qclass': 'zeek.dns.qclass',
+ 'zeek.dns.RA': 'zeek.dns.RA',
+ 'zeek.dns.TC': 'zeek.dns.TC',
+ 'zeek.http.resp_mime_types': 'zeek.http.resp_mime_types',
+ 'zeek.http.trans_depth': 'zeek.http.trans_depth',
+ 'zeek.http.status_msg': 'zeek.http.status_msg',
+ 'zeek.http.resp_fuids': 'zeek.http.resp_fuids',
+ 'zeek.http.tags': 'zeek.http.tags',
+ 'zeek.files.session_ids': 'zeek.files.session_ids',
+ 'zeek.files.timedout': 'zeek.files.timedout',
+ 'zeek.files.local_orig': 'zeek.files.local_orig',
+ 'zeek.files.tx_host': 'zeek.files.tx_host',
+ 'zeek.files.source': 'zeek.files.source',
+ 'zeek.files.is_orig': 'zeek.files.is_orig',
+ 'zeek.files.overflow_bytes': 'zeek.files.overflow_bytes',
+ 'zeek.files.sha1': 'zeek.files.sha1',
+ 'zeek.files.duration': 'zeek.files.duration',
+ 'zeek.files.depth': 'zeek.files.depth',
+ 'zeek.files.analyzers': 'zeek.files.analyzers',
+ 'zeek.files.mime_type': 'zeek.files.mime_type',
+ 'zeek.files.rx_host': 'zeek.files.rx_host',
+ 'zeek.files.total_bytes': 'zeek.files.total_bytes',
+ 'zeek.files.fuid': 'zeek.files.fuid',
+ 'zeek.files.seen_bytes': 'zeek.files.seen_bytes',
+ 'zeek.files.missing_bytes': 'zeek.files.missing_bytes',
+ 'zeek.files.md5': 'zeek.files.md5',
+ 'zeek.ssl.cipher': 'zeek.ssl.cipher',
+ 'zeek.ssl.established': 'zeek.ssl.established',
+ 'zeek.ssl.resumed': 'zeek.ssl.resumed',
+ 'zeek.ssl.version': 'zeek.ssl.version',
+};
+
+export const sourceFieldsMap: Readonly> = {
+ 'source.bytes': 'source.bytes',
+ 'source.ip': 'source.ip',
+ 'source.packets': 'source.packets',
+ 'source.port': 'source.port',
+ 'source.domain': 'source.domain',
+ 'source.geo.continent_name': 'source.geo.continent_name',
+ 'source.geo.country_name': 'source.geo.country_name',
+ 'source.geo.country_iso_code': 'source.geo.country_iso_code',
+ 'source.geo.city_name': 'source.geo.city_name',
+ 'source.geo.region_iso_code': 'source.geo.region_iso_code',
+ 'source.geo.region_name': 'source.geo.region_name',
+};
+
+export const destinationFieldsMap: Readonly> = {
+ 'destination.bytes': 'destination.bytes',
+ 'destination.ip': 'destination.ip',
+ 'destination.packets': 'destination.packets',
+ 'destination.port': 'destination.port',
+ 'destination.domain': 'destination.domain',
+ 'destination.geo.continent_name': 'destination.geo.continent_name',
+ 'destination.geo.country_name': 'destination.geo.country_name',
+ 'destination.geo.country_iso_code': 'destination.geo.country_iso_code',
+ 'destination.geo.city_name': 'destination.geo.city_name',
+ 'destination.geo.region_iso_code': 'destination.geo.region_iso_code',
+ 'destination.geo.region_name': 'destination.geo.region_name',
+};
+
+export const networkFieldsMap: Readonly> = {
+ 'network.bytes': 'network.bytes',
+ 'network.community_id': 'network.community_id',
+ 'network.direction': 'network.direction',
+ 'network.packets': 'network.packets',
+ 'network.protocol': 'network.protocol',
+ 'network.transport': 'network.transport',
+};
+
+export const geoFieldsMap: Readonly> = {
+ 'geo.region_name': 'destination.geo.region_name',
+ 'geo.country_iso_code': 'destination.geo.country_iso_code',
+};
+
+export const dnsFieldsMap: Readonly> = {
+ 'dns.question.name': 'dns.question.name',
+ 'dns.question.type': 'dns.question.type',
+ 'dns.resolved_ip': 'dns.resolved_ip',
+ 'dns.response_code': 'dns.response_code',
+};
+
+export const endgameFieldsMap: Readonly> = {
+ 'endgame.exit_code': 'endgame.exit_code',
+ 'endgame.file_name': 'endgame.file_name',
+ 'endgame.file_path': 'endgame.file_path',
+ 'endgame.logon_type': 'endgame.logon_type',
+ 'endgame.parent_process_name': 'endgame.parent_process_name',
+ 'endgame.pid': 'endgame.pid',
+ 'endgame.process_name': 'endgame.process_name',
+ 'endgame.subject_domain_name': 'endgame.subject_domain_name',
+ 'endgame.subject_logon_id': 'endgame.subject_logon_id',
+ 'endgame.subject_user_name': 'endgame.subject_user_name',
+ 'endgame.target_domain_name': 'endgame.target_domain_name',
+ 'endgame.target_logon_id': 'endgame.target_logon_id',
+ 'endgame.target_user_name': 'endgame.target_user_name',
+};
+
+export const eventBaseFieldsMap: Readonly> = {
+ 'event.action': 'event.action',
+ 'event.category': 'event.category',
+ 'event.code': 'event.code',
+ 'event.created': 'event.created',
+ 'event.dataset': 'event.dataset',
+ 'event.duration': 'event.duration',
+ 'event.end': 'event.end',
+ 'event.hash': 'event.hash',
+ 'event.id': 'event.id',
+ 'event.kind': 'event.kind',
+ 'event.module': 'event.module',
+ 'event.original': 'event.original',
+ 'event.outcome': 'event.outcome',
+ 'event.risk_score': 'event.risk_score',
+ 'event.risk_score_norm': 'event.risk_score_norm',
+ 'event.severity': 'event.severity',
+ 'event.start': 'event.start',
+ 'event.timezone': 'event.timezone',
+ 'event.type': 'event.type',
+};
+
+export const systemFieldsMap: Readonly> = {
+ 'system.audit.package.arch': 'system.audit.package.arch',
+ 'system.audit.package.entity_id': 'system.audit.package.entity_id',
+ 'system.audit.package.name': 'system.audit.package.name',
+ 'system.audit.package.size': 'system.audit.package.size',
+ 'system.audit.package.summary': 'system.audit.package.summary',
+ 'system.audit.package.version': 'system.audit.package.version',
+ 'system.auth.ssh.signature': 'system.auth.ssh.signature',
+ 'system.auth.ssh.method': 'system.auth.ssh.method',
+};
+
+export const signalFieldsMap: Readonly> = {
+ 'signal.original_time': 'signal.original_time',
+ 'signal.rule.id': 'signal.rule.id',
+ 'signal.rule.saved_id': 'signal.rule.saved_id',
+ 'signal.rule.timeline_id': 'signal.rule.timeline_id',
+ 'signal.rule.timeline_title': 'signal.rule.timeline_title',
+ 'signal.rule.output_index': 'signal.rule.output_index',
+ 'signal.rule.from': 'signal.rule.from',
+ 'signal.rule.index': 'signal.rule.index',
+ 'signal.rule.language': 'signal.rule.language',
+ 'signal.rule.query': 'signal.rule.query',
+ 'signal.rule.to': 'signal.rule.to',
+ 'signal.rule.filters': 'signal.rule.filters',
+ 'signal.rule.rule_id': 'signal.rule.rule_id',
+ 'signal.rule.false_positives': 'signal.rule.false_positives',
+ 'signal.rule.max_signals': 'signal.rule.max_signals',
+ 'signal.rule.risk_score': 'signal.rule.risk_score',
+ 'signal.rule.description': 'signal.rule.description',
+ 'signal.rule.name': 'signal.rule.name',
+ 'signal.rule.immutable': 'signal.rule.immutable',
+ 'signal.rule.references': 'signal.rule.references',
+ 'signal.rule.severity': 'signal.rule.severity',
+ 'signal.rule.tags': 'signal.rule.tags',
+ 'signal.rule.threat': 'signal.rule.threat',
+ 'signal.rule.type': 'signal.rule.type',
+ 'signal.rule.size': 'signal.rule.size',
+ 'signal.rule.enabled': 'signal.rule.enabled',
+ 'signal.rule.created_at': 'signal.rule.created_at',
+ 'signal.rule.updated_at': 'signal.rule.updated_at',
+ 'signal.rule.created_by': 'signal.rule.created_by',
+ 'signal.rule.updated_by': 'signal.rule.updated_by',
+ 'signal.rule.version': 'signal.rule.version',
+ 'signal.rule.note': 'signal.rule.note',
+ 'signal.rule.threshold': 'signal.rule.threshold',
+ 'signal.rule.exceptions_list': 'signal.rule.exceptions_list',
+};
+
+export const ruleFieldsMap: Readonly> = {
+ 'rule.reference': 'rule.reference',
+};
+
+export const eventFieldsMap: Readonly> = {
+ timestamp: '@timestamp',
+ '@timestamp': '@timestamp',
+ message: 'message',
+ ...{ ...agentFieldsMap },
+ ...{ ...auditdMap },
+ ...{ ...destinationFieldsMap },
+ ...{ ...dnsFieldsMap },
+ ...{ ...endgameFieldsMap },
+ ...{ ...eventBaseFieldsMap },
+ ...{ ...fileMap },
+ ...{ ...geoFieldsMap },
+ ...{ ...hostFieldsMap },
+ ...{ ...networkFieldsMap },
+ ...{ ...ruleFieldsMap },
+ ...{ ...signalFieldsMap },
+ ...{ ...sourceFieldsMap },
+ ...{ ...suricataFieldsMap },
+ ...{ ...systemFieldsMap },
+ ...{ ...tlsFieldsMap },
+ ...{ ...zeekFieldsMap },
+ ...{ ...httpFieldsMap },
+ ...{ ...userFieldsMap },
+ ...{ ...winlogFieldsMap },
+ ...{ ...processFieldsMap },
+};
diff --git a/x-pack/plugins/timelines/common/ecs/endgame/index.ts b/x-pack/plugins/timelines/common/ecs/endgame/index.ts
new file mode 100644
index 0000000000000..f82a9587c75c3
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/endgame/index.ts
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+export interface EndgameEcs {
+ exit_code?: number[];
+ file_name?: string[];
+ file_path?: string[];
+ logon_type?: number[];
+ parent_process_name?: string[];
+ pid?: number[];
+ process_name?: string[];
+ subject_domain_name?: string[];
+ subject_logon_id?: string[];
+ subject_user_name?: string[];
+ target_domain_name?: string[];
+ target_logon_id?: string[];
+ target_user_name?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/event/index.ts b/x-pack/plugins/timelines/common/ecs/event/index.ts
new file mode 100644
index 0000000000000..4e38bacefd351
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/event/index.ts
@@ -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.
+ */
+
+export interface EventEcs {
+ action?: string[];
+
+ category?: string[];
+
+ code?: string[];
+
+ created?: string[];
+
+ dataset?: string[];
+
+ duration?: number[];
+
+ end?: string[];
+
+ hash?: string[];
+
+ id?: string[];
+
+ kind?: string[];
+
+ module?: string[];
+
+ original?: string[];
+
+ outcome?: string[];
+
+ risk_score?: number[];
+
+ risk_score_norm?: number[];
+
+ severity?: number[];
+
+ start?: string[];
+
+ timezone?: string[];
+
+ type?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/file/index.ts b/x-pack/plugins/timelines/common/ecs/file/index.ts
new file mode 100644
index 0000000000000..5e409b1095cf5
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/file/index.ts
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+
+interface Original {
+ name?: string[];
+ path?: string[];
+}
+
+export interface CodeSignature {
+ subject_name: string[];
+ trusted: string[];
+}
+export interface Ext {
+ code_signature?: CodeSignature[] | CodeSignature;
+ original?: Original;
+}
+export interface Hash {
+ md5?: string[];
+ sha1?: string[];
+ sha256: string[];
+}
+
+export interface FileEcs {
+ name?: string[];
+
+ path?: string[];
+
+ target_path?: string[];
+
+ extension?: string[];
+
+ Ext?: Ext;
+
+ type?: string[];
+
+ device?: string[];
+
+ inode?: string[];
+
+ uid?: string[];
+
+ owner?: string[];
+
+ gid?: string[];
+
+ group?: string[];
+
+ mode?: string[];
+
+ size?: number[];
+
+ mtime?: string[];
+
+ ctime?: string[];
+
+ hash?: Hash;
+}
diff --git a/x-pack/plugins/timelines/common/ecs/geo/index.ts b/x-pack/plugins/timelines/common/ecs/geo/index.ts
new file mode 100644
index 0000000000000..b6bf0f7b8aaad
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/geo/index.ts
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+export interface GeoEcs {
+ city_name?: string[];
+ continent_name?: string[];
+ country_iso_code?: string[];
+ country_name?: string[];
+ location?: Location;
+ region_iso_code?: string[];
+ region_name?: string[];
+}
+
+export interface Location {
+ lon?: number[];
+ lat?: number[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/host/index.ts b/x-pack/plugins/timelines/common/ecs/host/index.ts
new file mode 100644
index 0000000000000..37032c91fc312
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/host/index.ts
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+export interface HostEcs {
+ architecture?: string[];
+
+ id?: string[];
+
+ ip?: string[];
+
+ mac?: string[];
+
+ name?: string[];
+
+ os?: OsEcs;
+
+ type?: string[];
+}
+
+export interface OsEcs {
+ platform?: string[];
+
+ name?: string[];
+
+ full?: string[];
+
+ family?: string[];
+
+ version?: string[];
+
+ kernel?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/http/index.ts b/x-pack/plugins/timelines/common/ecs/http/index.ts
new file mode 100644
index 0000000000000..89ce6b678181b
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/http/index.ts
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+export interface HttpEcs {
+ version?: string[];
+
+ request?: HttpRequestData;
+
+ response?: HttpResponseData;
+}
+
+export interface HttpRequestData {
+ method?: string[];
+
+ body?: HttpBodyData;
+
+ referrer?: string[];
+
+ bytes?: number[];
+}
+
+export interface HttpBodyData {
+ content?: string[];
+
+ bytes?: number[];
+}
+
+export interface HttpResponseData {
+ status_code?: number[];
+
+ body?: HttpBodyData;
+
+ bytes?: number[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/index.ts b/x-pack/plugins/timelines/common/ecs/index.ts
new file mode 100644
index 0000000000000..8054b3c8521db
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/index.ts
@@ -0,0 +1,66 @@
+/*
+ * 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 { AgentEcs } from './agent';
+import { AuditdEcs } from './auditd';
+import { DestinationEcs } from './destination';
+import { DnsEcs } from './dns';
+import { EndgameEcs } from './endgame';
+import { EventEcs } from './event';
+import { FileEcs } from './file';
+import { GeoEcs } from './geo';
+import { HostEcs } from './host';
+import { NetworkEcs } from './network';
+import { RegistryEcs } from './registry';
+import { RuleEcs } from './rule';
+import { SignalEcs } from './signal';
+import { SourceEcs } from './source';
+import { SuricataEcs } from './suricata';
+import { TlsEcs } from './tls';
+import { ZeekEcs } from './zeek';
+import { HttpEcs } from './http';
+import { UrlEcs } from './url';
+import { UserEcs } from './user';
+import { WinlogEcs } from './winlog';
+import { ProcessEcs } from './process';
+import { SystemEcs } from './system';
+import { ThreatEcs } from './threat';
+import { Ransomware } from './ransomware';
+
+export interface Ecs {
+ _id: string;
+ _index?: string;
+ agent?: AgentEcs;
+ auditd?: AuditdEcs;
+ destination?: DestinationEcs;
+ dns?: DnsEcs;
+ endgame?: EndgameEcs;
+ event?: EventEcs;
+ geo?: GeoEcs;
+ host?: HostEcs;
+ network?: NetworkEcs;
+ registry?: RegistryEcs;
+ rule?: RuleEcs;
+ signal?: SignalEcs;
+ source?: SourceEcs;
+ suricata?: SuricataEcs;
+ tls?: TlsEcs;
+ zeek?: ZeekEcs;
+ http?: HttpEcs;
+ url?: UrlEcs;
+ timestamp?: string;
+ message?: string[];
+ user?: UserEcs;
+ winlog?: WinlogEcs;
+ process?: ProcessEcs;
+ file?: FileEcs;
+ system?: SystemEcs;
+ threat?: ThreatEcs;
+ // This should be temporary
+ eql?: { parentId: string; sequenceNumber: string };
+ Ransomware?: Ransomware;
+}
diff --git a/x-pack/plugins/timelines/common/ecs/network/index.ts b/x-pack/plugins/timelines/common/ecs/network/index.ts
new file mode 100644
index 0000000000000..6cc5dacab1e53
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/network/index.ts
@@ -0,0 +1,15 @@
+/*
+ * 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.
+ */
+
+export interface NetworkEcs {
+ bytes?: number[];
+ community_id?: string[];
+ direction?: string[];
+ packets?: number[];
+ protocol?: string[];
+ transport?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/process/index.ts b/x-pack/plugins/timelines/common/ecs/process/index.ts
new file mode 100644
index 0000000000000..820ecc5560e6c
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/process/index.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { Ext } from '../file';
+
+export interface ProcessEcs {
+ Ext?: Ext;
+ entity_id?: string[];
+ exit_code?: number[];
+ hash?: ProcessHashData;
+ parent?: ProcessParentData;
+ pid?: number[];
+ name?: string[];
+ ppid?: number[];
+ args?: string[];
+ executable?: string[];
+ title?: string[];
+ thread?: Thread;
+ working_directory?: string[];
+}
+
+export interface ProcessHashData {
+ md5?: string[];
+ sha1?: string[];
+ sha256?: string[];
+}
+
+export interface ProcessParentData {
+ name?: string[];
+ pid?: number[];
+}
+
+export interface Thread {
+ id?: number[];
+ start?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/ransomware/index.ts b/x-pack/plugins/timelines/common/ecs/ransomware/index.ts
new file mode 100644
index 0000000000000..1724a264f8a4c
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/ransomware/index.ts
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+export interface Ransomware {
+ feature?: string[];
+ score?: string[];
+ version?: number[];
+ child_pids?: string[];
+ files?: RansomwareFiles;
+}
+
+export interface RansomwareFiles {
+ operation?: string[];
+ entropy?: number[];
+ metrics?: string[];
+ extension?: string[];
+ original?: OriginalRansomwareFiles;
+ path?: string[];
+ data?: string[];
+ score?: number[];
+}
+
+export interface OriginalRansomwareFiles {
+ path?: string[];
+ extension?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/registry/index.ts b/x-pack/plugins/timelines/common/ecs/registry/index.ts
new file mode 100644
index 0000000000000..c756fb139199e
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/registry/index.ts
@@ -0,0 +1,13 @@
+/*
+ * 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.
+ */
+
+export interface RegistryEcs {
+ hive?: string[];
+ key?: string[];
+ path?: string[];
+ value?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/rule/index.ts b/x-pack/plugins/timelines/common/ecs/rule/index.ts
new file mode 100644
index 0000000000000..ae7e5064a8ece
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/rule/index.ts
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+export interface RuleEcs {
+ id?: string[];
+ rule_id?: string[];
+ name?: string[];
+ false_positives?: string[];
+ saved_id?: string[];
+ timeline_id?: string[];
+ timeline_title?: string[];
+ max_signals?: number[];
+ risk_score?: string[];
+ output_index?: string[];
+ description?: string[];
+ from?: string[];
+ immutable?: boolean[];
+ index?: string[];
+ interval?: string[];
+ language?: string[];
+ query?: string[];
+ references?: string[];
+ severity?: string[];
+ tags?: string[];
+ threat?: unknown;
+ threshold?: unknown;
+ type?: string[];
+ size?: string[];
+ to?: string[];
+ enabled?: boolean[];
+ filters?: unknown;
+ created_at?: string[];
+ updated_at?: string[];
+ created_by?: string[];
+ updated_by?: string[];
+ version?: string[];
+ note?: string[];
+ building_block_type?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/signal/index.ts b/x-pack/plugins/timelines/common/ecs/signal/index.ts
new file mode 100644
index 0000000000000..45e1f04d2b405
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/signal/index.ts
@@ -0,0 +1,18 @@
+/*
+ * 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 { RuleEcs } from '../rule';
+
+export interface SignalEcs {
+ rule?: RuleEcs;
+ original_time?: string[];
+ status?: string[];
+ group?: {
+ id?: string[];
+ };
+ threshold_result?: unknown;
+}
diff --git a/x-pack/plugins/timelines/common/ecs/source/index.ts b/x-pack/plugins/timelines/common/ecs/source/index.ts
new file mode 100644
index 0000000000000..10a2025eb43ec
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/source/index.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { GeoEcs } from '../geo';
+
+export interface SourceEcs {
+ bytes?: number[];
+ ip?: string[];
+ port?: number[];
+ domain?: string[];
+ geo?: GeoEcs;
+ packets?: number[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/suricata/index.ts b/x-pack/plugins/timelines/common/ecs/suricata/index.ts
new file mode 100644
index 0000000000000..5555a40188432
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/suricata/index.ts
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+export interface SuricataEcs {
+ eve?: SuricataEveData;
+}
+
+export interface SuricataEveData {
+ alert?: SuricataAlertData;
+
+ flow_id?: number[];
+
+ proto?: string[];
+}
+
+export interface SuricataAlertData {
+ signature?: string[];
+
+ signature_id?: number[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/system/index.ts b/x-pack/plugins/timelines/common/ecs/system/index.ts
new file mode 100644
index 0000000000000..f2313c7884511
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/system/index.ts
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+export interface SystemEcs {
+ audit?: AuditEcs;
+
+ auth?: AuthEcs;
+}
+
+export interface AuditEcs {
+ package?: PackageEcs;
+}
+
+export interface PackageEcs {
+ arch?: string[];
+
+ entity_id?: string[];
+
+ name?: string[];
+
+ size?: number[];
+
+ summary?: string[];
+
+ version?: string[];
+}
+
+export interface AuthEcs {
+ ssh?: SshEcs;
+}
+
+export interface SshEcs {
+ method?: string[];
+
+ signature?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/threat/index.ts b/x-pack/plugins/timelines/common/ecs/threat/index.ts
new file mode 100644
index 0000000000000..19923a82dc846
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/threat/index.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 { EventEcs } from '../event';
+
+interface ThreatMatchEcs {
+ atomic?: string[];
+ field?: string[];
+ type?: string[];
+}
+
+export interface ThreatIndicatorEcs {
+ matched?: ThreatMatchEcs;
+ event?: EventEcs & { reference?: string[] };
+ provider?: string[];
+ type?: string[];
+}
+
+export interface ThreatEcs {
+ indicator: ThreatIndicatorEcs[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/tls/index.ts b/x-pack/plugins/timelines/common/ecs/tls/index.ts
new file mode 100644
index 0000000000000..f2e6b3d36653d
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/tls/index.ts
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+export interface TlsEcs {
+ client_certificate?: TlsClientCertificateData;
+
+ fingerprints?: TlsFingerprintsData;
+
+ server_certificate?: TlsServerCertificateData;
+}
+
+export interface TlsClientCertificateData {
+ fingerprint?: FingerprintData;
+}
+
+export interface FingerprintData {
+ sha1?: string[];
+}
+
+export interface TlsFingerprintsData {
+ ja3?: TlsJa3Data;
+}
+
+export interface TlsJa3Data {
+ hash?: string[];
+}
+
+export interface TlsServerCertificateData {
+ fingerprint?: FingerprintData;
+}
diff --git a/x-pack/plugins/timelines/common/ecs/url/index.ts b/x-pack/plugins/timelines/common/ecs/url/index.ts
new file mode 100644
index 0000000000000..ea9dc303108e3
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/url/index.ts
@@ -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.
+ */
+
+export interface UrlEcs {
+ domain?: string[];
+
+ original?: string[];
+
+ username?: string[];
+
+ password?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/user/index.ts b/x-pack/plugins/timelines/common/ecs/user/index.ts
new file mode 100644
index 0000000000000..b03a8e5e96b41
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/user/index.ts
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+export interface UserEcs {
+ domain?: string[];
+
+ id?: string[];
+
+ name?: string[];
+
+ full_name?: string[];
+
+ email?: string[];
+
+ hash?: string[];
+
+ group?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/winlog/index.ts b/x-pack/plugins/timelines/common/ecs/winlog/index.ts
new file mode 100644
index 0000000000000..27757d05ba6ec
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/winlog/index.ts
@@ -0,0 +1,10 @@
+/*
+ * 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.
+ */
+
+export interface WinlogEcs {
+ event_id?: number[];
+}
diff --git a/x-pack/plugins/timelines/common/ecs/zeek/index.ts b/x-pack/plugins/timelines/common/ecs/zeek/index.ts
new file mode 100644
index 0000000000000..b1a3786ae74aa
--- /dev/null
+++ b/x-pack/plugins/timelines/common/ecs/zeek/index.ts
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+
+export interface ZeekEcs {
+ session_id?: string[];
+
+ connection?: ZeekConnectionData;
+
+ notice?: ZeekNoticeData;
+
+ dns?: ZeekDnsData;
+
+ http?: ZeekHttpData;
+
+ files?: ZeekFileData;
+
+ ssl?: ZeekSslData;
+}
+
+export interface ZeekConnectionData {
+ local_resp?: boolean[];
+
+ local_orig?: boolean[];
+
+ missed_bytes?: number[];
+
+ state?: string[];
+
+ history?: string[];
+}
+
+export interface ZeekNoticeData {
+ suppress_for?: number[];
+
+ msg?: string[];
+
+ note?: string[];
+
+ sub?: string[];
+
+ dst?: string[];
+
+ dropped?: boolean[];
+
+ peer_descr?: string[];
+}
+
+export interface ZeekDnsData {
+ AA?: boolean[];
+
+ qclass_name?: string[];
+
+ RD?: boolean[];
+
+ qtype_name?: string[];
+
+ rejected?: boolean[];
+
+ qtype?: string[];
+
+ query?: string[];
+
+ trans_id?: number[];
+
+ qclass?: string[];
+
+ RA?: boolean[];
+
+ TC?: boolean[];
+}
+
+export interface ZeekHttpData {
+ resp_mime_types?: string[];
+
+ trans_depth?: string[];
+
+ status_msg?: string[];
+
+ resp_fuids?: string[];
+
+ tags?: string[];
+}
+
+export interface ZeekFileData {
+ session_ids?: string[];
+
+ timedout?: boolean[];
+
+ local_orig?: boolean[];
+
+ tx_host?: string[];
+
+ source?: string[];
+
+ is_orig?: boolean[];
+
+ overflow_bytes?: number[];
+
+ sha1?: string[];
+
+ duration?: number[];
+
+ depth?: number[];
+
+ analyzers?: string[];
+
+ mime_type?: string[];
+
+ rx_host?: string[];
+
+ total_bytes?: number[];
+
+ fuid?: string[];
+
+ seen_bytes?: number[];
+
+ missing_bytes?: number[];
+
+ md5?: string[];
+}
+
+export interface ZeekSslData {
+ cipher?: string[];
+
+ established?: boolean[];
+
+ resumed?: boolean[];
+
+ version?: string[];
+}
diff --git a/x-pack/plugins/timelines/common/index.ts b/x-pack/plugins/timelines/common/index.ts
index c095b6c89627e..05174235c20db 100644
--- a/x-pack/plugins/timelines/common/index.ts
+++ b/x-pack/plugins/timelines/common/index.ts
@@ -5,5 +5,9 @@
* 2.0.
*/
+export * from './types';
+export * from './search_strategy';
+export * from './utils/accessibility';
+
export const PLUGIN_ID = 'timelines';
export const PLUGIN_NAME = 'timelines';
diff --git a/x-pack/plugins/timelines/common/search_strategy/common/index.ts b/x-pack/plugins/timelines/common/search_strategy/common/index.ts
new file mode 100644
index 0000000000000..62c2187e267fa
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/common/index.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import type { estypes } from '@elastic/elasticsearch';
+
+export type Maybe = T | null;
+
+export interface TotalValue {
+ value: number;
+ relation: string;
+}
+
+export interface CursorType {
+ value?: Maybe;
+ tiebreaker?: Maybe;
+}
+
+export interface Inspect {
+ dsl: string[];
+}
+
+export enum Direction {
+ asc = 'asc',
+ desc = 'desc',
+}
+
+export interface SortField {
+ field: Field;
+ direction: Direction;
+}
+
+export interface TimerangeInput {
+ /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */
+ interval: string;
+ /** The end of the timerange */
+ to: string;
+ /** The beginning of the timerange */
+ from: string;
+}
+
+export interface PaginationInputPaginated {
+ /** The activePage parameter defines the page of results you want to fetch */
+ activePage: number;
+ /** The cursorStart parameter defines the start of the results to be displayed */
+ cursorStart: number;
+ /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */
+ fakePossibleCount: number;
+ /** The querySize parameter is the number of items to be returned */
+ querySize: number;
+}
+
+export type DocValueFields = estypes.SearchDocValueField;
+
+export interface TimerangeFilter {
+ range: {
+ [timestamp: string]: {
+ gte: string;
+ lte: string;
+ format: string;
+ };
+ };
+}
+
+export interface Fields {
+ [x: string]: T | Array>;
+}
+
+export interface EventSource {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ [field: string]: any;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export interface EventHit extends estypes.SearchHit> {
+ sort: string[];
+ fields: Fields;
+}
diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/index.ts b/x-pack/plugins/timelines/common/search_strategy/eql/index.ts
new file mode 100644
index 0000000000000..4a361bed64890
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/eql/index.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { TotalValue } from '../common';
+
+export * from './validation';
+
+export type SearchTypes =
+ | string
+ | string[]
+ | number
+ | number[]
+ | boolean
+ | boolean[]
+ | object
+ | object[]
+ | undefined;
+
+export interface BaseHit {
+ _index: string;
+ _id: string;
+ _source: T;
+ fields?: Record;
+}
+
+export interface EqlSequence {
+ join_keys: SearchTypes[];
+ events: Array>;
+}
+
+export interface EqlSearchResponse {
+ is_partial: boolean;
+ is_running: boolean;
+ took: number;
+ timed_out: boolean;
+ hits: {
+ total: TotalValue;
+ sequences?: Array>;
+ events?: Array>;
+ };
+}
diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.mock.ts b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.mock.ts
new file mode 100644
index 0000000000000..b3a2c9c9a3f62
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.mock.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 { ApiResponse } from '@elastic/elasticsearch';
+import { ErrorResponse } from './helpers';
+
+export const getValidEqlResponse = (): ApiResponse['body'] => ({
+ is_partial: false,
+ is_running: false,
+ took: 162,
+ timed_out: false,
+ hits: {
+ total: {
+ value: 1,
+ relation: 'eq',
+ },
+ sequences: [],
+ },
+});
+
+export const getEqlResponseWithValidationError = (): ErrorResponse => ({
+ error: {
+ root_cause: [
+ {
+ type: 'verification_exception',
+ reason:
+ 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
+ },
+ ],
+ type: 'verification_exception',
+ reason:
+ 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
+ },
+});
+
+export const getEqlResponseWithValidationErrors = (): ErrorResponse => ({
+ error: {
+ root_cause: [
+ {
+ type: 'verification_exception',
+ reason:
+ 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
+ },
+ {
+ type: 'parsing_exception',
+ reason: "line 1:4: mismatched input '' expecting 'where'",
+ },
+ ],
+ type: 'verification_exception',
+ reason:
+ 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
+ },
+});
+
+export const getEqlResponseWithNonValidationError = (): ApiResponse['body'] => ({
+ error: {
+ root_cause: [
+ {
+ type: 'other_error',
+ reason: 'some other reason',
+ },
+ ],
+ type: 'other_error',
+ reason: 'some other reason',
+ },
+});
diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.test.ts b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.test.ts
new file mode 100644
index 0000000000000..de75cf6ac6dc7
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.test.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 { getValidationErrors, isErrorResponse, isValidationErrorResponse } from './helpers';
+import {
+ getEqlResponseWithNonValidationError,
+ getEqlResponseWithValidationError,
+ getEqlResponseWithValidationErrors,
+ getValidEqlResponse,
+} from './helpers.mock';
+
+describe('eql validation helpers', () => {
+ describe('isErrorResponse', () => {
+ it('is false for a regular response', () => {
+ expect(isErrorResponse(getValidEqlResponse())).toEqual(false);
+ });
+
+ it('is true for a response with non-validation errors', () => {
+ expect(isErrorResponse(getEqlResponseWithNonValidationError())).toEqual(true);
+ });
+
+ it('is true for a response with validation errors', () => {
+ expect(isErrorResponse(getEqlResponseWithValidationError())).toEqual(true);
+ });
+ });
+
+ describe('isValidationErrorResponse', () => {
+ it('is false for a regular response', () => {
+ expect(isValidationErrorResponse(getValidEqlResponse())).toEqual(false);
+ });
+
+ it('is false for a response with non-validation errors', () => {
+ expect(isValidationErrorResponse(getEqlResponseWithNonValidationError())).toEqual(false);
+ });
+
+ it('is true for a response with validation errors', () => {
+ expect(isValidationErrorResponse(getEqlResponseWithValidationError())).toEqual(true);
+ });
+ });
+
+ describe('getValidationErrors', () => {
+ it('returns a single error for a single root cause', () => {
+ expect(getValidationErrors(getEqlResponseWithValidationError())).toEqual([
+ 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
+ ]);
+ });
+
+ it('returns multiple errors for multiple root causes', () => {
+ expect(getValidationErrors(getEqlResponseWithValidationErrors())).toEqual([
+ 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
+ "line 1:4: mismatched input '' expecting 'where'",
+ ]);
+ });
+ });
+});
diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.ts b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.ts
new file mode 100644
index 0000000000000..63a812cad759a
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 { get, has } from 'lodash';
+
+const PARSING_ERROR_TYPE = 'parsing_exception';
+const VERIFICATION_ERROR_TYPE = 'verification_exception';
+const MAPPING_ERROR_TYPE = 'mapping_exception';
+
+interface ErrorCause {
+ type: string;
+ reason: string;
+}
+
+export interface ErrorResponse {
+ error: ErrorCause & { root_cause: ErrorCause[] };
+}
+
+const isValidationErrorType = (type: unknown): boolean =>
+ type === PARSING_ERROR_TYPE || type === VERIFICATION_ERROR_TYPE || type === MAPPING_ERROR_TYPE;
+
+export const isErrorResponse = (response: unknown): response is ErrorResponse =>
+ has(response, 'error.type');
+
+export const isValidationErrorResponse = (response: unknown): response is ErrorResponse =>
+ isErrorResponse(response) && isValidationErrorType(get(response, 'error.type'));
+
+export const getValidationErrors = (response: ErrorResponse): string[] =>
+ response.error.root_cause
+ .filter((cause) => isValidationErrorType(cause.type))
+ .map((cause) => cause.reason);
diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/validation/index.ts b/x-pack/plugins/timelines/common/search_strategy/eql/validation/index.ts
new file mode 100644
index 0000000000000..6c315f929b9bb
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/eql/validation/index.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export * from './helpers';
diff --git a/x-pack/plugins/timelines/common/search_strategy/index.ts b/x-pack/plugins/timelines/common/search_strategy/index.ts
new file mode 100644
index 0000000000000..155306327ee0c
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/index.ts
@@ -0,0 +1,11 @@
+/*
+ * 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.
+ */
+
+export * from './common';
+export * from './timeline';
+export * from './index_fields';
+export * from './eql';
diff --git a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts
new file mode 100644
index 0000000000000..76ab48a8243db
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts
@@ -0,0 +1,89 @@
+/*
+ * 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 { IIndexPattern } from 'src/plugins/data/public';
+import {
+ IEsSearchRequest,
+ IEsSearchResponse,
+ IFieldSubType,
+} from '../../../../../../src/plugins/data/common';
+import { DocValueFields, Maybe } from '../common';
+
+export type BeatFieldsFactoryQueryType = 'beatFields';
+
+interface FieldInfo {
+ category: string;
+ description?: string;
+ example?: string | number;
+ format?: string;
+ name: string;
+ type?: string;
+}
+
+export interface IndexField {
+ /** Where the field belong */
+ category: string;
+ /** Example of field's value */
+ example?: Maybe;
+ /** whether the field's belong to an alias index */
+ indexes: Array>;
+ /** The name of the field */
+ name: string;
+ /** The type of the field's values as recognized by Kibana */
+ type: string;
+ /** Whether the field's values can be efficiently searched for */
+ searchable: boolean;
+ /** Whether the field's values can be aggregated */
+ aggregatable: boolean;
+ /** Description of the field */
+ description?: Maybe;
+ format?: Maybe;
+ /** the elastic type as mapped in the index */
+ esTypes?: string[];
+ subType?: IFieldSubType;
+ readFromDocValues: boolean;
+}
+
+export type BeatFields = Record;
+
+export interface IndexFieldsStrategyRequest extends IEsSearchRequest {
+ indices: string[];
+ onlyCheckIfIndicesExist: boolean;
+}
+
+export interface IndexFieldsStrategyResponse extends IEsSearchResponse {
+ indexFields: IndexField[];
+ indicesExist: string[];
+}
+
+export interface BrowserField {
+ aggregatable: boolean;
+ category: string;
+ description: string | null;
+ example: string | number | null;
+ fields: Readonly>>;
+ format: string;
+ indexes: string[];
+ name: string;
+ searchable: boolean;
+ type: string;
+ subType?: {
+ [key: string]: unknown;
+ nested?: {
+ path: string;
+ };
+ };
+}
+
+export type BrowserFields = Readonly>>;
+
+export const EMPTY_BROWSER_FIELDS = {};
+export const EMPTY_DOCVALUE_FIELD: DocValueFields[] = [];
+export const EMPTY_INDEX_PATTERN: IIndexPattern = {
+ fields: [],
+ title: '',
+};
diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts
new file mode 100644
index 0000000000000..94f7bc617e2f2
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 type { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
+import type { Ecs } from '../../../../ecs';
+import type { CursorType, Inspect, Maybe, PaginationInputPaginated } from '../../../common';
+import type { TimelineRequestOptionsPaginated } from '../..';
+
+export interface TimelineEdges {
+ node: TimelineItem;
+ cursor: CursorType;
+}
+
+export interface TimelineItem {
+ _id: string;
+ _index?: Maybe;
+ data: TimelineNonEcsData[];
+ ecs: Ecs;
+}
+
+export interface TimelineNonEcsData {
+ field: string;
+ value?: Maybe;
+}
+
+export interface TimelineEventsAllStrategyResponse extends IEsSearchResponse {
+ edges: TimelineEdges[];
+ totalCount: number;
+ pageInfo: Pick;
+ inspect?: Maybe;
+}
+
+export interface TimelineEventsAllRequestOptions extends TimelineRequestOptionsPaginated {
+ fields: string[] | Array<{ field: string; include_unmapped: boolean }>;
+ fieldRequested: string[];
+ language: 'eql' | 'kuery' | 'lucene';
+ excludeEcsData?: boolean;
+}
diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/common/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/common/index.ts
new file mode 100644
index 0000000000000..4a5bd2c99a0eb
--- /dev/null
+++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/common/index.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { Ecs } from '../../../../ecs';
+import { CursorType, Maybe } from '../../../common';
+
+export interface TimelineEdges {
+ node: TimelineItem;
+ cursor: CursorType;
+}
+
+export interface TimelineItem {
+ _id: string;
+ _index?: Maybe