diff --git a/.editorconfig b/.editorconfig index 7564b3596f043..ec8a51f2314be 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,6 +12,6 @@ insert_final_newline = true [package.json] insert_final_newline = false -[*.{md,asciidoc}] +[*.{md,mdx,asciidoc}] trim_trailing_whitespace = false insert_final_newline = false diff --git a/docs/concepts/images/add-filter-popup.png b/docs/concepts/images/add-filter-popup.png new file mode 100644 index 0000000000000..f1b5b1ff3f6ca Binary files /dev/null and b/docs/concepts/images/add-filter-popup.png differ diff --git a/docs/concepts/images/global-search.png b/docs/concepts/images/global-search.png new file mode 100644 index 0000000000000..a867477938219 Binary files /dev/null and b/docs/concepts/images/global-search.png differ diff --git a/docs/concepts/images/refresh-every.png b/docs/concepts/images/refresh-every.png new file mode 100644 index 0000000000000..a0930a6c56a65 Binary files /dev/null and b/docs/concepts/images/refresh-every.png differ diff --git a/docs/concepts/images/save-icon.png b/docs/concepts/images/save-icon.png new file mode 100644 index 0000000000000..959c7ef8e1bb9 Binary files /dev/null and b/docs/concepts/images/save-icon.png differ diff --git a/docs/concepts/images/top-bar.png b/docs/concepts/images/top-bar.png new file mode 100755 index 0000000000000..73c872c29e6ae Binary files /dev/null and b/docs/concepts/images/top-bar.png differ diff --git a/docs/concepts/index.asciidoc b/docs/concepts/index.asciidoc new file mode 100644 index 0000000000000..70b8a5265ce8a --- /dev/null +++ b/docs/concepts/index.asciidoc @@ -0,0 +1,149 @@ +[[kibana-concepts-analysts]] +== {kib} concepts for analysts +**_Learn the shared concepts for analyzing and visualizing your data_** + +As an analyst, you will use a combination of {kib} apps to analyze and +visualize your data. {kib} contains both general-purpose apps and apps for the +https://www.elastic.co/guide/en/enterprise-search/current/index.html[*Enterprise Search*], +{observability-guide}/observability-introduction.html[*Elastic Observability*], +and {security-guide}/es-overview.html[*Elastic Security*] solutions. +These apps share a common set of concepts. + +[float] +=== Three things to know about {es} + +You don't need to know everything about {es} to use {kib}, but the most important concepts follow: + +* *{es} makes JSON documents searchable and aggregatable.* The documents are +stored in an {ref}/documents-indices.html[index] or {ref}/data-streams.html[data stream], which represent one type of data. + +* **_Searchable_ means that you can filter the documents for conditions.** +For example, you can filter for data "within the last 7 days" or data that "contains the word {kib}". +{kib} provides many ways for you to construct filters, which are also called queries or search terms. + +* **_Aggregatable_ means that you can extract summaries from matching documents.** +The simplest aggregation is *count*, and it is frequently used in combination +with the *date histogram*, to see count over time. The *terms* aggregation shows the most frequent values. + +[float] +=== Finding your apps and objects + +{kib} offers a <> on every page that you can use to find any app or saved object. +Open the search bar using the keyboard shortcut Ctrl+/ on Windows and Linux, Command+/ on MacOS. + +[role="screenshot"] +image:concepts/images/global-search.png["Global search showing matches to apps and saved objects for the word visualize"] + +[float] +=== Accessing data with index patterns + +{kib} requires an index pattern to tell it which {es} data you want to access, +and whether the data is time-based. An index pattern can point to one or more {es} +data streams, indices, or index aliases by name. +For example, `logs-elasticsearch-prod-*` is an index pattern, +and it is time-based with a time field of `@timestamp`. The time field is not editable. + +Index patterns are typically created by an administrator when sending data to {es}. +You can <> in *Stack Management*, or by using a script +that accesses the {kib} API. + +{kib} uses the index pattern to show you a list of fields, such as +`event.duration`. You can customize the display name and format for each field. +For example, you can tell Kibana to display `event.duration` in seconds. +{kib} has <> for strings, +dates, geopoints, +and numbers. + +[float] +=== Searching your data + +{kib} provides you several ways to build search queries, +which will reduce the number of document matches that you get from {es}. +Each app in {kib} provides a time filter, and most apps also include semi-structured search and extra filters. + +[role="screenshot"] +image:concepts/images/top-bar.png["Time filter, semi-structured search, and filters in a {kib} app"] + +If you frequently use any of the search options, you can click the +save icon +image:concepts/images/save-icon.png["save icon"] next to the +semi-structured search to save or load a previously saved query. +The saved query will always contain the semi-structured search query, +and can optionally contain the time filter and extra filters. + +[float] +==== Time filter + +The <> limits the time range of data displayed. +In most cases, the time filter applies to the time field in the index pattern, +but some apps allow you to use a different time field. + +Using the time filter, you can configure a refresh rate to periodically +resubmit your searches. You can also click *Refresh* to resubmit the search. +This might be useful if you use {kib} to monitor the underlying data. + +[role="screenshot"] +image:concepts/images/refresh-every.png["section of time filter where you can configure a refresh rate"] + + +[float] +==== Semi-structured search + +Combine free text search with field-based search using the Kibana Query Language (KQL). +Type a search term to match across all fields, or start typing a field name to +get suggestions for field names and operators that you can use to build a structured query. +The semi-structured search will filter documents for matches, and only return matching documents. + +Following are some example KQL queries. For more detailed examples, refer to <>. + +[cols=2*] +|=== +| Exact phrase query +| `http.response.body.content.text:"quick brown fox"` + +| Terms query +| http.response.status_code:400 401 404 + +| Boolean query +| `response:200 or extension:php` + +| Range query +| `account_number >= 100 and items_sold <= 200` + +| Wildcard query +| `machine.os:win*` +|=== + +[float] +==== Additional filters with AND + +Structured filters are a more interactive way to create {es} queries, +and are commonly used when building dashboards that are shared by multiple analysts. +Each filter can be disabled, inverted, or pinned across all apps. +The structured filters are the only way to use the {es} Query DSL in JSON form, +or to target a specific index pattern for filtering. Each of the structured +filters is combined with AND logic on the rest of the query. + +[role="screenshot"] +image:concepts/images/add-filter-popup.png["Add filter popup"] + +[float] +=== Saving objects +{kib} lets you save objects for your own future use or for sharing with others. +Each <> type has different abilities. For example, you can save +your search queries made with *Discover*, which lets you: + +* Share a link to your search +* Download the full search results in CSV form +* Start an aggregated visualization using the same search query +* Embed the *Discover* search results into a dashboard +* Embed the *Discover* search results into a Canvas workpad + +For organization, every saved object can have a name, <>, and type. +Use the global search to quickly open a saved object. + +[float] +=== What's next? + +* Try the {kib} <>, which shows you how to put these concepts into action. +* Go to <> for instructions on searching your data. diff --git a/docs/concepts/save-query.asciidoc b/docs/concepts/save-query.asciidoc new file mode 100644 index 0000000000000..4f049d121bbef --- /dev/null +++ b/docs/concepts/save-query.asciidoc @@ -0,0 +1,39 @@ +[[save-load-delete-query]] +== Save a query +A saved query is a collection of query text and filters that you can +reuse in any app with a query bar, like <> and <>. Save a query when you want to: + +* Retrieve results from the same query at a later time without having to reenter the query text, add the filters or set the time filter +* View the results of the same query in multiple apps +* Share your query + +Saved queries don't include information specific to *Discover*, +such as the currently selected columns in the document table, the sort order, and the index pattern. +To save your current view of *Discover* for later retrieval and reuse, +create a <> instead. + +NOTE:: + +If you have insufficient privileges to save queries, the *Save current query* +button isn't visible in the saved query management popover. +For more information, see <> + +. Click *#* in the query bar. +. In the popover, click *Save current query*. ++ +[role="screenshot"] +image::discover/images/saved-query-management-component-all-privileges.png["Example of the saved query management popover with a list of saved queries with write access",width="80%"] ++ +. Enter a name, a description, and then select the filter options. +By default, filters are automatically included, but the time filter is not. ++ +[role="screenshot"] +image::discover/images/saved-query-save-form-default-filters.png["Example of the saved query management save form with the filters option included and the time filter option excluded",width="80%"] +. Click *Save*. +. To load a saved query into *Discover* or *Dashboard*, open the *Saved search* popover, and select the query. +. To manage your saved queries, use these actions in the popover: ++ +* Save as new: Save changes to the current query. +* Clear. Clear a query that is currently loaded in an app. +* Delete. You can’t recover a deleted query. +. To import and export saved queries, go to <>. diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index d7e15258bf29b..81ded1e54d8fd 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -2,6 +2,8 @@ include::introduction.asciidoc[] include::whats-new.asciidoc[] +include::{kib-repo-dir}/concepts/index.asciidoc[] + include::{kib-repo-dir}/getting-started/quick-start-guide.asciidoc[] include::setup.asciidoc[] diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 249183d4b1e31..e114e3e930016 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -100,7 +100,7 @@ pageLoadAssetSize: watcher: 43598 runtimeFields: 41752 stackAlerts: 29684 - presentationUtil: 28545 + presentationUtil: 49767 spacesOss: 18817 indexPatternFieldEditor: 90489 osquery: 107090 diff --git a/packages/kbn-plugin-generator/src/render_template.ts b/packages/kbn-plugin-generator/src/render_template.ts index 282a547318d28..1a9716f1f1ba5 100644 --- a/packages/kbn-plugin-generator/src/render_template.ts +++ b/packages/kbn-plugin-generator/src/render_template.ts @@ -84,7 +84,7 @@ export async function renderTemplates({ answers.ui ? [] : 'public/**/*', answers.ui && !answers.internal ? [] : ['translations/**/*', 'i18nrc.json'], answers.server ? [] : 'server/**/*', - !answers.internal ? [] : ['eslintrc.js', 'tsconfig.json', 'package.json', '.gitignore'] + !answers.internal ? [] : ['.eslintrc.js', 'tsconfig.json', 'package.json', '.gitignore'] ) ), diff --git a/src/plugins/dashboard/.storybook/storyshots.test.tsx b/src/plugins/dashboard/.storybook/storyshots.test.tsx deleted file mode 100644 index 80e8aa795ed40..0000000000000 --- a/src/plugins/dashboard/.storybook/storyshots.test.tsx +++ /dev/null @@ -1,76 +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 { ReactChildren } from 'react'; -import path from 'path'; -import moment from 'moment'; -import 'moment-timezone'; -import ReactDOM from 'react-dom'; - -import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'; -// @ts-ignore -import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer'; -import { addSerializer } from 'jest-specific-snapshot'; - -// Set our default timezone to UTC for tests so we can generate predictable snapshots -moment.tz.setDefault('UTC'); - -// Freeze time for the tests for predictable snapshots -const testTime = new Date(Date.UTC(2019, 5, 1)); // June 1 2019 -Date.now = jest.fn(() => testTime.getTime()); - -// Mock React Portal for components that use modals, tooltips, etc -// @ts-expect-error Portal mocks are notoriously difficult to type -ReactDOM.createPortal = jest.fn((element) => element); - -// Mock EUI generated ids to be consistently predictable for snapshots. -jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); - -// Mock react-datepicker dep used by eui to avoid rendering the entire large component -jest.mock('@elastic/eui/packages/react-datepicker', () => { - return { - __esModule: true, - default: 'ReactDatePicker', - }; -}); - -// Mock the EUI HTML ID Generator so elements have a predictable ID in snapshots -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { - return { - htmlIdGenerator: () => () => `generated-id`, - }; -}); - -// To be resolved by EUI team. -// https://github.com/elastic/eui/issues/3712 -jest.mock('@elastic/eui/lib/components/overlay_mask/overlay_mask', () => { - return { - EuiOverlayMask: ({ children }: { children: ReactChildren }) => children, - }; -}); - -// @ts-ignore -import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer'; -jest.mock('@elastic/eui/test-env/components/observer/observer'); -EuiObserver.mockImplementation(() => 'EuiObserver'); - -// Some of the code requires that this directory exists, but the tests don't actually require any css to be present -const cssDir = path.resolve(__dirname, '../../../../built_assets/css'); -if (!fs.existsSync(cssDir)) { - fs.mkdirSync(cssDir, { recursive: true }); -} - -addSerializer(styleSheetSerializer); - -// Initialize Storyshots and build the Jest Snapshots -initStoryshots({ - configPath: path.resolve(__dirname, './../.storybook'), - framework: 'react', - test: multiSnapshotWithOptions({}), -}); diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index 32507cbc5e5f4..41335069461fa 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -23,6 +23,7 @@ "requiredBundles": [ "home", "kibanaReact", - "kibanaUtils" + "kibanaUtils", + "presentationUtil" ] } diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index a82aa78b815ec..ef0cd376df98b 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -11,6 +11,12 @@ import angular from 'angular'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import UseUnmount from 'react-use/lib/useUnmount'; +import { + AddFromLibraryButton, + PrimaryActionButton, + QuickButtonGroup, + SolutionToolbar, +} from '../../../../presentation_util/public'; import { useKibana } from '../../services/kibana_react'; import { IndexPattern, SavedQuery, TimefilterContract } from '../../services/data'; import { @@ -43,9 +49,9 @@ import { showCloneModal } from './show_clone_modal'; import { showOptionsPopover } from './show_options_popover'; import { TopNavIds } from './top_nav_ids'; import { ShowShareModal } from './show_share_modal'; -import { PanelToolbar } from './panel_toolbar'; import { confirmDiscardOrKeepUnsavedChanges } from '../listing/confirm_overlays'; import { OverlayRef } from '../../../../../core/public'; +import { DashboardConstants } from '../../dashboard_constants'; import { getNewDashboardTitle, unsavedChangesBadge } from '../../dashboard_strings'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_panel_storage'; import { DashboardContainer } from '..'; @@ -103,6 +109,8 @@ export function DashboardTopNav({ const [state, setState] = useState({ chromeIsVisible: false }); const [isSaveInProgress, setIsSaveInProgress] = useState(false); + const stateTransferService = embeddable.getStateTransfer(); + useEffect(() => { const visibleSubscription = chrome.getIsVisible$().subscribe((chromeIsVisible) => { setState((s) => ({ ...s, chromeIsVisible })); @@ -147,12 +155,26 @@ export function DashboardTopNav({ const createNew = useCallback(async () => { const type = 'visualization'; const factory = embeddable.getEmbeddableFactory(type); + if (!factory) { throw new EmbeddableFactoryNotFoundError(type); } + await factory.create({} as EmbeddableInput, dashboardContainer); }, [dashboardContainer, embeddable]); + const createNewVisType = useCallback( + (newVisType: string) => async () => { + stateTransferService.navigateToEditor('visualize', { + path: `#/create?type=${encodeURIComponent(newVisType)}`, + state: { + originatingApp: DashboardConstants.DASHBOARDS_ID, + }, + }); + }, + [stateTransferService] + ); + const clearAddPanel = useCallback(() => { if (state.addPanelOverlay) { state.addPanelOverlay.close(); @@ -540,11 +562,51 @@ export function DashboardTopNav({ }; const { TopNavMenu } = navigation.ui; + + const quickButtons = [ + { + iconType: 'visText', + createType: i18n.translate('dashboard.solutionToolbar.markdownQuickButtonLabel', { + defaultMessage: 'Markdown', + }), + onClick: createNewVisType('markdown'), + 'data-test-subj': 'dashboardMarkdownQuickButton', + }, + { + iconType: 'controlsHorizontal', + createType: i18n.translate('dashboard.solutionToolbar.inputControlsQuickButtonLabel', { + defaultMessage: 'Input control', + }), + onClick: createNewVisType('input_control_vis'), + 'data-test-subj': 'dashboardInputControlsQuickButton', + }, + ]; + return ( <> {viewMode !== ViewMode.VIEW ? ( - + + {{ + primaryActionButton: ( + + ), + quickButtonGroup: , + addFromLibraryButton: ( + + ), + }} + ) : null} ); diff --git a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/__snapshots__/panel_toolbar.stories.storyshot b/src/plugins/dashboard/public/application/top_nav/panel_toolbar/__snapshots__/panel_toolbar.stories.storyshot deleted file mode 100644 index afbbecb3935e0..0000000000000 --- a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/__snapshots__/panel_toolbar.stories.storyshot +++ /dev/null @@ -1,71 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots components/PanelToolbar default 1`] = ` -
-
- -
-
- -
-
-`; diff --git a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.tsx b/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.tsx deleted file mode 100644 index 0449fae80186d..0000000000000 --- a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.tsx +++ /dev/null @@ -1,51 +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 './panel_toolbar.scss'; -import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - -interface Props { - /** The click handler for the Add Panel button for creating new panels */ - onAddPanelClick: () => void; - /** The click handler for the Library button for adding existing visualizations/embeddables */ - onLibraryClick: () => void; -} - -export const PanelToolbar: FC = ({ onAddPanelClick, onLibraryClick }) => ( - - - - {i18n.translate('dashboard.panelToolbar.addPanelButtonLabel', { - defaultMessage: 'Create panel', - })} - - - - - {i18n.translate('dashboard.panelToolbar.libraryButtonLabel', { - defaultMessage: 'Add from library', - })} - - - -); diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index dd99119cfb457..12820fc08310d 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -32,5 +32,8 @@ { "path": "../saved_objects/tsconfig.json" }, { "path": "../ui_actions/tsconfig.json" }, { "path": "../spaces_oss/tsconfig.json" }, + { "path": "../charts/tsconfig.json" }, + { "path": "../discover/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, ] } diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts index ebb1946b524cd..6a51c085ebbc9 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts @@ -6,13 +6,12 @@ * Side Public License, v 1. */ -import { Capabilities } from 'kibana/public'; -import { getSharingData, showPublicUrlSwitch } from './get_sharing_data'; -import { IUiSettingsClient } from 'kibana/public'; +import { Capabilities, IUiSettingsClient } from 'kibana/public'; +import { IndexPattern } from 'src/plugins/data/public'; import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; -import { IndexPattern } from 'src/plugins/data/public'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { getSharingData, showPublicUrlSwitch } from './get_sharing_data'; describe('getSharingData', () => { let mockConfig: IUiSettingsClient; @@ -36,6 +35,32 @@ describe('getSharingData', () => { const result = await getSharingData(searchSourceMock, { columns: [] }, mockConfig); expect(result).toMatchInlineSnapshot(` Object { + "columns": Array [], + "searchSource": Object { + "index": "the-index-pattern-id", + "sort": Array [ + Object { + "_score": "desc", + }, + ], + }, + } + `); + }); + + test('returns valid data for sharing when columns are selected', async () => { + const searchSourceMock = createSearchSourceMock({ index: indexPatternMock }); + const result = await getSharingData( + searchSourceMock, + { columns: ['column_a', 'column_b'] }, + mockConfig + ); + expect(result).toMatchInlineSnapshot(` + Object { + "columns": Array [ + "column_a", + "column_b", + ], "searchSource": Object { "index": "the-index-pattern-id", "sort": Array [ @@ -69,16 +94,16 @@ describe('getSharingData', () => { ); expect(result).toMatchInlineSnapshot(` Object { + "columns": Array [ + "cool-timefield", + "cool-field-1", + "cool-field-2", + "cool-field-3", + "cool-field-4", + "cool-field-5", + "cool-field-6", + ], "searchSource": Object { - "fields": Array [ - "cool-timefield", - "cool-field-1", - "cool-field-2", - "cool-field-3", - "cool-field-4", - "cool-field-5", - "cool-field-6", - ], "index": "the-index-pattern-id", "sort": Array [ Object { @@ -120,15 +145,15 @@ describe('getSharingData', () => { ); expect(result).toMatchInlineSnapshot(` Object { + "columns": Array [ + "cool-field-1", + "cool-field-2", + "cool-field-3", + "cool-field-4", + "cool-field-5", + "cool-field-6", + ], "searchSource": Object { - "fields": Array [ - "cool-field-1", - "cool-field-2", - "cool-field-3", - "cool-field-4", - "cool-field-5", - "cool-field-6", - ], "index": "the-index-pattern-id", "sort": Array [ Object { diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.ts index f0e07ccc38deb..47be4b8037152 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.ts @@ -7,11 +7,11 @@ */ import type { Capabilities, IUiSettingsClient } from 'kibana/public'; -import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; -import { getSortForSearchSource } from '../angular/doc_table'; import { ISearchSource } from '../../../../data/common'; -import { AppState } from '../angular/discover_state'; +import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; import type { SavedSearch, SortOrder } from '../../saved_searches/types'; +import { AppState } from '../angular/discover_state'; +import { getSortForSearchSource } from '../angular/doc_table'; /** * Preparing data to share the current state as link or CSV/Report @@ -23,10 +23,6 @@ export async function getSharingData( ) { const searchSource = currentSearchSource.createCopy(); const index = searchSource.getField('index')!; - const fields = { - fields: searchSource.getField('fields'), - fieldsFromSource: searchSource.getField('fieldsFromSource'), - }; searchSource.setField( 'sort', @@ -37,7 +33,7 @@ export async function getSharingData( searchSource.removeField('aggs'); searchSource.removeField('size'); - // fields get re-set to match the saved search columns + // Columns that the user has selected in the saved search let columns = state.columns || []; if (columns && columns.length > 0) { @@ -50,14 +46,11 @@ export async function getSharingData( if (timeFieldName && !columns.includes(timeFieldName)) { columns = [timeFieldName, ...columns]; } - - // if columns were selected in the saved search, use them for the searchSource's fields - const fieldsKey = fields.fieldsFromSource ? 'fieldsFromSource' : 'fields'; - searchSource.setField(fieldsKey, columns); } return { searchSource: searchSource.getSerializedFields(true), + columns, }; } diff --git a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/index.ts b/src/plugins/presentation_util/public/components/solution_toolbar/index.ts similarity index 81% rename from src/plugins/dashboard/public/application/top_nav/panel_toolbar/index.ts rename to src/plugins/presentation_util/public/components/solution_toolbar/index.ts index fd0ce66beb97c..332d60787b8cb 100644 --- a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/index.ts +++ b/src/plugins/presentation_util/public/components/solution_toolbar/index.ts @@ -6,4 +6,5 @@ * Side Public License, v 1. */ -export { PanelToolbar } from './panel_toolbar'; +export { SolutionToolbar } from './solution_toolbar'; +export * from './items'; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/add_from_library.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/add_from_library.tsx new file mode 100644 index 0000000000000..0550de1d069fa --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/add_from_library.tsx @@ -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 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 { ComponentStrings } from '../../../i18n/components'; +import { SolutionToolbarButton, Props as SolutionToolbarButtonProps } from './button'; + +const { SolutionToolbar: strings } = ComponentStrings; + +export type Props = Omit; + +export const AddFromLibraryButton = ({ onClick, ...rest }: Props) => ( + +); diff --git a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss similarity index 73% rename from src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.scss rename to src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss index 9ad6a5257df3e..79c3d4cca7ace 100644 --- a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.scss +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss @@ -1,9 +1,5 @@ -.panelToolbar { - padding: 0 $euiSizeS $euiSizeS; - flex-grow: 0; -} -.panelToolbarButton { +.solutionToolbarButton { line-height: $euiButtonHeight; // Keeps alignment of text and chart icon background-color: $euiColorEmptyShade; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/button.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.tsx new file mode 100644 index 0000000000000..5de8e24ef5f0d --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.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 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 { EuiButton } from '@elastic/eui'; +import { EuiButtonPropsForButton } from '@elastic/eui/src/components/button/button'; + +import './button.scss'; + +export interface Props extends Pick { + label: string; + primary?: boolean; +} + +export const SolutionToolbarButton = ({ label, primary, ...rest }: Props) => ( + + {label} + +); diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/index.ts b/src/plugins/presentation_util/public/components/solution_toolbar/items/index.ts new file mode 100644 index 0000000000000..654831e86d3f6 --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/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 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 { SolutionToolbarButton } from './button'; +export { SolutionToolbarPopover } from './popover'; +export { AddFromLibraryButton } from './add_from_library'; +export { QuickButtonProps, QuickButtonGroup } from './quick_group'; +export { PrimaryActionButton } from './primary_button'; +export { PrimaryActionPopover } from './primary_popover'; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx new file mode 100644 index 0000000000000..fbb34e165190d --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx @@ -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 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, { useState } from 'react'; +import { EuiPopover } from '@elastic/eui'; +import { Props as EuiPopoverProps } from '@elastic/eui/src/components/popover/popover'; + +import { SolutionToolbarButton, Props as ButtonProps } from './button'; + +type AllowedButtonProps = Omit; +type AllowedPopoverProps = Omit< + EuiPopoverProps, + 'button' | 'isOpen' | 'closePopover' | 'anchorPosition' +>; + +export type Props = AllowedButtonProps & AllowedPopoverProps; + +export const SolutionToolbarPopover = ({ label, iconType, primary, ...popover }: Props) => { + const [isOpen, setIsOpen] = useState(false); + + const onButtonClick = () => setIsOpen((status) => !status); + const closePopover = () => setIsOpen(false); + + const button = ( + + ); + + return ( + + ); +}; diff --git a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.stories.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_button.tsx similarity index 53% rename from src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.stories.tsx rename to src/plugins/presentation_util/public/components/solution_toolbar/items/primary_button.tsx index 5525b1cd069ad..e2ef75e45a404 100644 --- a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/panel_toolbar.stories.tsx +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_button.tsx @@ -6,14 +6,10 @@ * Side Public License, v 1. */ -import { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; import React from 'react'; -import { PanelToolbar } from './panel_toolbar'; -storiesOf('components/PanelToolbar', module).add('default', () => ( - -)); +import { SolutionToolbarButton, Props as SolutionToolbarButtonProps } from './button'; + +export const PrimaryActionButton = (props: Omit) => ( + +); diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_popover.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_popover.tsx new file mode 100644 index 0000000000000..164d4c9b4a1a6 --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/primary_popover.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 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 { SolutionToolbarPopover, Props as SolutionToolbarPopoverProps } from './popover'; + +export type Props = Omit; + +export const PrimaryActionPopover = (props: Omit) => ( + +); diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss new file mode 100644 index 0000000000000..639ff5bf2a117 --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss @@ -0,0 +1,5 @@ +.quickButtonGroup { + .quickButtonGroup__button { + background-color: $euiColorEmptyShade; + } +} diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx new file mode 100644 index 0000000000000..58f8bd803b636 --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx @@ -0,0 +1,58 @@ +/* + * 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 { EuiButtonGroup, htmlIdGenerator, EuiButtonGroupOptionProps } from '@elastic/eui'; +import { ComponentStrings } from '../../../i18n/components'; + +const { QuickButtonGroup: strings } = ComponentStrings; + +import './quick_group.scss'; + +export interface QuickButtonProps extends Pick { + createType: string; + onClick: () => void; +} + +export interface Props { + buttons: QuickButtonProps[]; +} + +type Option = EuiButtonGroupOptionProps & Omit; + +export const QuickButtonGroup = ({ buttons }: Props) => { + const buttonGroupOptions: Option[] = buttons.map((button: QuickButtonProps, index) => { + const { createType: label, ...rest } = button; + const title = strings.getAriaButtonLabel(label); + + return { + ...rest, + 'aria-label': title, + className: 'quickButtonGroup__button', + id: `${htmlIdGenerator()()}${index}`, + label, + title, + }; + }); + + const onChangeIconsMulti = (optionId: string) => { + buttonGroupOptions.find((x) => x.id === optionId)?.onClick(); + }; + + return ( + + ); +}; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.scss b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.scss new file mode 100644 index 0000000000000..18160acf191e4 --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.scss @@ -0,0 +1,4 @@ +.solutionToolbar { + padding: 0 $euiSizeS $euiSizeS; + flex-grow: 0; +} diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.stories.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.stories.tsx new file mode 100644 index 0000000000000..fa33f53f9ae4f --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.stories.tsx @@ -0,0 +1,205 @@ +/* + * 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 { Story } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { EuiContextMenu } from '@elastic/eui'; + +import { SolutionToolbar } from './solution_toolbar'; +import { SolutionToolbarPopover } from './items'; +import { AddFromLibraryButton, PrimaryActionButton, QuickButtonGroup } from './items'; + +const quickButtons = [ + { + createType: 'Text', + onClick: action('onTextClick'), + iconType: 'visText', + }, + { + createType: 'Control', + onClick: action('onControlClick'), + iconType: 'controlsHorizontal', + }, + { + createType: 'Link', + onClick: action('onLinkClick'), + iconType: 'link', + }, + { + createType: 'Image', + onClick: action('onImageClick'), + iconType: 'image', + }, + { + createType: 'Markup', + onClick: action('onMarkupClick'), + iconType: 'visVega', + }, +]; + +const primaryButtonConfigs = { + Generic: ( + + ), + Canvas: ( + + + + ), + Dashboard: ( + + ), +}; + +const extraButtonConfigs = { + Generic: undefined, + Canvas: undefined, + Dashboard: [ + + + , + ], +}; + +export default { + title: 'Solution Toolbar', + description: 'A universal toolbar for solutions maintained by the Presentation Team.', + component: SolutionToolbar, + argTypes: { + quickButtonCount: { + defaultValue: 2, + control: { + type: 'number', + min: 0, + max: 5, + step: 1, + }, + }, + showAddFromLibraryButton: { + defaultValue: true, + control: { + type: 'boolean', + }, + }, + solution: { + table: { + disable: true, + }, + }, + }, + // https://github.com/storybookjs/storybook/issues/11543#issuecomment-684130442 + parameters: { + docs: { + source: { + type: 'code', + }, + }, + }, +}; + +const Template: Story<{ + solution: 'Generic' | 'Canvas' | 'Dashboard'; + quickButtonCount: number; + showAddFromLibraryButton: boolean; +}> = ({ quickButtonCount, solution, showAddFromLibraryButton }) => { + const primaryActionButton = primaryButtonConfigs[solution]; + const extraButtons = extraButtonConfigs[solution]; + let quickButtonGroup; + let addFromLibraryButton; + + if (quickButtonCount > 0) { + quickButtonGroup = ; + } + + if (showAddFromLibraryButton) { + addFromLibraryButton = ; + } + + return ( + + {{ + primaryActionButton, + quickButtonGroup, + extraButtons, + addFromLibraryButton, + }} + + ); +}; + +export const Generic = Template.bind({}); +Generic.args = { + ...Template.args, + solution: 'Generic', +}; + +export const Canvas = Template.bind({}); +Canvas.args = { + ...Template.args, + solution: 'Canvas', +}; + +export const Dashboard = Template.bind({}); +Dashboard.args = { + ...Template.args, + solution: 'Dashboard', +}; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.tsx new file mode 100644 index 0000000000000..bb8b04e8b8f09 --- /dev/null +++ b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.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 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, { ReactElement } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { + AddFromLibraryButton, + QuickButtonGroup, + PrimaryActionButton, + SolutionToolbarButton, + PrimaryActionPopover, + SolutionToolbarPopover, +} from './items'; + +import './solution_toolbar.scss'; + +interface NamedSlots { + primaryActionButton: ReactElement; + quickButtonGroup?: ReactElement; + addFromLibraryButton?: ReactElement; + extraButtons?: Array>; +} + +export interface Props { + children: NamedSlots; +} + +export const SolutionToolbar = ({ children }: Props) => { + const { + primaryActionButton, + quickButtonGroup, + addFromLibraryButton, + extraButtons = [], + } = children; + + const extra = extraButtons.map((button, index) => + button ? ( + + {button} + + ) : null + ); + + return ( + + {primaryActionButton} + {quickButtonGroup ? {quickButtonGroup} : null} + {extra} + {addFromLibraryButton ? {addFromLibraryButton} : null} + + ); +}; diff --git a/src/plugins/presentation_util/public/i18n/components.ts b/src/plugins/presentation_util/public/i18n/components.ts new file mode 100644 index 0000000000000..ab0e6d1bdbda0 --- /dev/null +++ b/src/plugins/presentation_util/public/i18n/components.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 { i18n } from '@kbn/i18n'; + +export const ComponentStrings = { + SolutionToolbar: { + getEditorMenuButtonLabel: () => + i18n.translate('presentationUtil.solutionToolbar.editorMenuButtonLabel', { + defaultMessage: 'All editors', + }), + getLibraryButtonLabel: () => + i18n.translate('presentationUtil.solutionToolbar.libraryButtonLabel', { + defaultMessage: 'Add from library', + }), + }, + QuickButtonGroup: { + getAriaButtonLabel: (createType: string) => + i18n.translate('presentationUtil.solutionToolbar.quickButton.ariaButtonLabel', { + defaultMessage: `Create new {createType}`, + values: { + createType, + }, + }), + getLegend: () => + i18n.translate('presentationUtil.solutionToolbar.quickButton.legendLabel', { + defaultMessage: 'Quick create', + }), + }, +}; diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index 1cbf4b5a4f334..9c5f65de40955 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -19,6 +19,16 @@ export { LazySavedObjectSaveModalDashboard, withSuspense, } from './components'; +export { + AddFromLibraryButton, + PrimaryActionButton, + PrimaryActionPopover, + QuickButtonGroup, + QuickButtonProps, + SolutionToolbar, + SolutionToolbarButton, + SolutionToolbarPopover, +} from './components/solution_toolbar'; export function plugin() { return new PresentationUtilPlugin(); diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index 63d136cf9445a..c0fafe8c3aaba 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -15,11 +15,8 @@ "../../../typings/**/*" ], "references": [ - { - "path": "../../core/tsconfig.json" - }, - { - "path": "../saved_objects/tsconfig.json" - }, + { "path": "../../core/tsconfig.json" }, + { "path": "../saved_objects/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, ] } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index c2b9fcd77757a..2b5a611cd946e 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -58,7 +58,7 @@ interface VisualizationAttributes extends SavedObjectAttributes { export interface VisualizeEmbeddableFactoryDeps { start: StartServicesGetter< - Pick + Pick >; } diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 8f1ebe25b5059..901593626a945 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -17,7 +17,6 @@ import { dataPluginMock } from '../../../plugins/data/public/mocks'; import { usageCollectionPluginMock } from '../../../plugins/usage_collection/public/mocks'; import { uiActionsPluginMock } from '../../../plugins/ui_actions/public/mocks'; import { inspectorPluginMock } from '../../../plugins/inspector/public/mocks'; -import { dashboardPluginMock } from '../../../plugins/dashboard/public/mocks'; import { savedObjectsPluginMock } from '../../../plugins/saved_objects/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ @@ -62,7 +61,6 @@ const createInstance = async () => { uiActions: uiActionsPluginMock.createStartContract(), application: applicationServiceMock.createStartContract(), embeddable: embeddablePluginMock.createStartContract(), - dashboard: dashboardPluginMock.createStartContract(), getAttributeService: jest.fn(), savedObjectsClient: coreMock.createStart().savedObjects.client, savedObjects: savedObjectsPluginMock.createStartContract(), diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index d4e7132a1a21e..081f5d65103c2 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -62,7 +62,6 @@ import { convertToSerializedVis, } from './saved_visualizations/_saved_vis'; import { createSavedSearchesLoader } from '../../discover/public'; -import { DashboardStart } from '../../dashboard/public'; import { SavedObjectsStart } from '../../saved_objects/public'; /** @@ -97,7 +96,6 @@ export interface VisualizationsStartDeps { inspector: InspectorStart; uiActions: UiActionsStart; application: ApplicationStart; - dashboard: DashboardStart; getAttributeService: EmbeddableStart['getAttributeService']; savedObjects: SavedObjectsStart; savedObjectsClient: SavedObjectsClientContract; @@ -145,7 +143,7 @@ export class VisualizationsPlugin public start( core: CoreStart, - { data, expressions, uiActions, embeddable, dashboard, savedObjects }: VisualizationsStartDeps + { data, expressions, uiActions, embeddable, savedObjects }: VisualizationsStartDeps ): VisualizationsStart { const types = this.types.start(); setTypes(types); diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index d7c5e6a4b4366..356448aa59771 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -15,7 +15,6 @@ "references": [ { "path": "../../core/tsconfig.json" }, { "path": "../data/tsconfig.json" }, - { "path": "../dashboard/tsconfig.json" }, { "path": "../expressions/tsconfig.json" }, { "path": "../ui_actions/tsconfig.json" }, { "path": "../embeddable/tsconfig.json" }, diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.ts b/test/functional/apps/dashboard/create_and_add_embeddables.ts index f4ee4e9904768..9b8fc4785a671 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.ts +++ b/test/functional/apps/dashboard/create_and_add_embeddables.ts @@ -69,6 +69,36 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); }); + it('adds a markdown visualization via the quick button', async () => { + const originalPanelCount = await PageObjects.dashboard.getPanelCount(); + await PageObjects.dashboard.clickMarkdownQuickButton(); + await PageObjects.visualize.saveVisualizationExpectSuccess( + 'visualization from markdown quick button', + { redirectToOrigin: true } + ); + + await retry.try(async () => { + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(originalPanelCount + 1); + }); + await PageObjects.dashboard.waitForRenderComplete(); + }); + + it('adds an input control visualization via the quick button', async () => { + const originalPanelCount = await PageObjects.dashboard.getPanelCount(); + await PageObjects.dashboard.clickInputControlsQuickButton(); + await PageObjects.visualize.saveVisualizationExpectSuccess( + 'visualization from input control quick button', + { redirectToOrigin: true } + ); + + await retry.try(async () => { + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(originalPanelCount + 1); + }); + await PageObjects.dashboard.waitForRenderComplete(); + }); + it('saves the listing page instead of the visualization to the app link', async () => { await PageObjects.header.clickVisualize(true); const currentUrl = await browser.getCurrentUrl(); diff --git a/test/functional/apps/dashboard/edit_visualizations.js b/test/functional/apps/dashboard/edit_visualizations.js index d5df97881a1d3..ce32f53587e74 100644 --- a/test/functional/apps/dashboard/edit_visualizations.js +++ b/test/functional/apps/dashboard/edit_visualizations.js @@ -15,15 +15,12 @@ export default function ({ getService, getPageObjects }) { const appsMenu = getService('appsMenu'); const kibanaServer = getService('kibanaServer'); const dashboardPanelActions = getService('dashboardPanelActions'); - const dashboardVisualizations = getService('dashboardVisualizations'); const originalMarkdownText = 'Original markdown text'; const modifiedMarkdownText = 'Modified markdown text'; const createMarkdownVis = async (title) => { - await testSubjects.click('dashboardAddNewPanelButton'); - await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); - await PageObjects.visualize.clickMarkdownWidget(); + await PageObjects.dashboard.clickMarkdownQuickButton(); await PageObjects.visEditor.setMarkdownTxt(originalMarkdownText); await PageObjects.visEditor.clickGo(); if (title) { diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 9c12296db138c..34559afdf6ae1 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -413,6 +413,16 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide await testSubjects.click('confirmSaveSavedObjectButton'); } + public async clickMarkdownQuickButton() { + log.debug('Click markdown quick button'); + await testSubjects.click('dashboardMarkdownQuickButton'); + } + + public async clickInputControlsQuickButton() { + log.debug('Click input controls quick button'); + await testSubjects.click('dashboardInputControlsQuickButton'); + } + /** * * @param dashboardTitle {String} diff --git a/x-pack/examples/reporting_example/.eslintrc.js b/x-pack/examples/reporting_example/.eslintrc.js deleted file mode 100644 index b267018448ba6..0000000000000 --- a/x-pack/examples/reporting_example/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - root: true, - extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], - rules: { - '@kbn/eslint/require-license-header': 'off', - }, -}; diff --git a/x-pack/examples/reporting_example/common/index.ts b/x-pack/examples/reporting_example/common/index.ts index e47604bd7b823..f01f2673eff56 100644 --- a/x-pack/examples/reporting_example/common/index.ts +++ b/x-pack/examples/reporting_example/common/index.ts @@ -1,2 +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. + */ + export const PLUGIN_ID = 'reportingExample'; export const PLUGIN_NAME = 'reportingExample'; diff --git a/x-pack/examples/reporting_example/public/application.tsx b/x-pack/examples/reporting_example/public/application.tsx index 1bb944faad3ea..25a1cc767f1f5 100644 --- a/x-pack/examples/reporting_example/public/application.tsx +++ b/x-pack/examples/reporting_example/public/application.tsx @@ -1,3 +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'; import ReactDOM from 'react-dom'; import { AppMountParameters, CoreStart } from '../../../../src/core/public'; diff --git a/x-pack/examples/reporting_example/public/components/app.tsx b/x-pack/examples/reporting_example/public/components/app.tsx index 8f7176675f2c2..fd4a85dd06779 100644 --- a/x-pack/examples/reporting_example/public/components/app.tsx +++ b/x-pack/examples/reporting_example/public/components/app.tsx @@ -1,3 +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 { EuiCard, EuiCode, diff --git a/x-pack/examples/reporting_example/public/index.ts b/x-pack/examples/reporting_example/public/index.ts index a490cf96895be..f9f749e2b0cd0 100644 --- a/x-pack/examples/reporting_example/public/index.ts +++ b/x-pack/examples/reporting_example/public/index.ts @@ -1,3 +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 { ReportingExamplePlugin } from './plugin'; export function plugin() { diff --git a/x-pack/examples/reporting_example/public/plugin.ts b/x-pack/examples/reporting_example/public/plugin.ts index 95b4d917f549a..6ac1cbe01db92 100644 --- a/x-pack/examples/reporting_example/public/plugin.ts +++ b/x-pack/examples/reporting_example/public/plugin.ts @@ -1,3 +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 { AppMountParameters, AppNavLinkStatus, diff --git a/x-pack/examples/reporting_example/public/types.ts b/x-pack/examples/reporting_example/public/types.ts index d574053266fae..56e8c34d9dae4 100644 --- a/x-pack/examples/reporting_example/public/types.ts +++ b/x-pack/examples/reporting_example/public/types.ts @@ -1,3 +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 { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public'; import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; import { ReportingStart } from '../../../plugins/reporting/public'; diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index 439cae4f414f7..82f04d82ea2f8 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -84,7 +84,6 @@ export class DataEnhancedPlugin sessionService: plugins.data.search.session, application: core.application, basePath: core.http.basePath, - timeFilter: plugins.data.query.timefilter.timefilter, storage: this.storage, disableSaveAfterSessionCompletesTimeout: moment .duration(this.config.search.sessions.notTouchedTimeout) diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx index c96d821641dd6..a16557b50700e 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx @@ -60,7 +60,6 @@ test("shouldn't show indicator in case no active search session", async () => { const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService, application, - timeFilter, storage, disableSaveAfterSessionCompletesTimeout, usageCollector, @@ -89,7 +88,6 @@ test("shouldn't show indicator in case app hasn't opt-in", async () => { const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService, application, - timeFilter, storage, disableSaveAfterSessionCompletesTimeout, usageCollector, @@ -120,7 +118,6 @@ test('should show indicator in case there is an active search session', async () const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService: { ...sessionService, state$ }, application, - timeFilter, storage, disableSaveAfterSessionCompletesTimeout, usageCollector, @@ -146,7 +143,6 @@ test('should be disabled in case uiConfig says so ', async () => { const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService: { ...sessionService, state$ }, application, - timeFilter, storage, disableSaveAfterSessionCompletesTimeout, usageCollector, @@ -171,7 +167,6 @@ test('should be disabled in case not enough permissions', async () => { const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService: { ...sessionService, state$, hasAccess: () => false }, application, - timeFilter, storage, disableSaveAfterSessionCompletesTimeout, basePath, @@ -191,38 +186,6 @@ test('should be disabled in case not enough permissions', async () => { expect(screen.getByRole('button', { name: 'Manage sessions' })).toBeDisabled(); }); -test('should be disabled during auto-refresh', async () => { - const state$ = new BehaviorSubject(SearchSessionState.Loading); - - const SearchSessionIndicator = createConnectedSearchSessionIndicator({ - sessionService: { ...sessionService, state$ }, - application, - timeFilter, - storage, - disableSaveAfterSessionCompletesTimeout, - usageCollector, - basePath, - }); - - render( - - - - ); - - await waitFor(() => screen.getByTestId('searchSessionIndicator')); - - await userEvent.click(screen.getByLabelText('Search session loading')); - - expect(screen.getByRole('button', { name: 'Save session' })).not.toBeDisabled(); - - act(() => { - refreshInterval$.next({ value: 0, pause: false }); - }); - - expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled(); -}); - describe('Completed inactivity', () => { beforeEach(() => { jest.useFakeTimers(); @@ -236,7 +199,6 @@ describe('Completed inactivity', () => { const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService: { ...sessionService, state$ }, application, - timeFilter, storage, disableSaveAfterSessionCompletesTimeout, usageCollector, @@ -298,7 +260,6 @@ describe('tour steps', () => { const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService: { ...sessionService, state$ }, application, - timeFilter, storage, disableSaveAfterSessionCompletesTimeout, usageCollector, @@ -340,7 +301,6 @@ describe('tour steps', () => { const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService: { ...sessionService, state$ }, application, - timeFilter, storage, disableSaveAfterSessionCompletesTimeout, usageCollector, @@ -376,7 +336,6 @@ describe('tour steps', () => { const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService: { ...sessionService, state$ }, application, - timeFilter, storage, disableSaveAfterSessionCompletesTimeout, usageCollector, @@ -404,7 +363,6 @@ describe('tour steps', () => { const SearchSessionIndicator = createConnectedSearchSessionIndicator({ sessionService: { ...sessionService, state$ }, application, - timeFilter, storage, disableSaveAfterSessionCompletesTimeout, usageCollector, diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx index 630aea417c84e..603df09e1c4c6 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.tsx @@ -6,7 +6,7 @@ */ import React, { useCallback, useEffect, useState } from 'react'; -import { debounce, distinctUntilChanged, map, mapTo, switchMap, tap } from 'rxjs/operators'; +import { debounce, distinctUntilChanged, mapTo, switchMap, tap } from 'rxjs/operators'; import { merge, of, timer } from 'rxjs'; import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; @@ -14,7 +14,6 @@ import { SearchSessionIndicator, SearchSessionIndicatorRef } from '../search_ses import { ISessionService, SearchSessionState, - TimefilterContract, SearchUsageCollector, } from '../../../../../../../src/plugins/data/public'; import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public'; @@ -24,7 +23,6 @@ import { useSearchSessionTour } from './search_session_tour'; export interface SearchSessionIndicatorDeps { sessionService: ISessionService; - timeFilter: TimefilterContract; application: ApplicationStart; basePath: IBasePath; storage: IStorageWrapper; @@ -39,17 +37,12 @@ export interface SearchSessionIndicatorDeps { export const createConnectedSearchSessionIndicator = ({ sessionService, application, - timeFilter, storage, disableSaveAfterSessionCompletesTimeout, usageCollector, basePath, }: SearchSessionIndicatorDeps): React.FC => { const searchSessionsManagementUrl = basePath.prepend('/app/management/kibana/search_sessions'); - const isAutoRefreshEnabled = () => !timeFilter.getRefreshInterval().pause; - const isAutoRefreshEnabled$ = timeFilter - .getRefreshIntervalUpdate$() - .pipe(map(isAutoRefreshEnabled), distinctUntilChanged()); const debouncedSessionServiceState$ = sessionService.state$.pipe( debounce((_state) => timer(_state === SearchSessionState.None ? 50 : 300)) // switch to None faster to quickly remove indicator when navigating away @@ -69,7 +62,6 @@ export const createConnectedSearchSessionIndicator = ({ return () => { const state = useObservable(debouncedSessionServiceState$, SearchSessionState.None); - const autoRefreshEnabled = useObservable(isAutoRefreshEnabled$, isAutoRefreshEnabled()); const isSaveDisabledByApp = sessionService.getSearchSessionIndicatorUiConfig().isDisabled(); const disableSaveAfterSessionCompleteTimedOut = useObservable( disableSaveAfterSessionCompleteTimedOut$, @@ -91,16 +83,6 @@ export const createConnectedSearchSessionIndicator = ({ let managementDisabled = false; let managementDisabledReasonText: string = ''; - if (autoRefreshEnabled) { - saveDisabled = true; - saveDisabledReasonText = i18n.translate( - 'xpack.data.searchSessionIndicator.disabledDueToAutoRefreshMessage', - { - defaultMessage: 'Saving search session is not available when auto refresh is enabled.', - } - ); - } - if (disableSaveAfterSessionCompleteTimedOut) { saveDisabled = true; saveDisabledReasonText = i18n.translate( diff --git a/x-pack/plugins/graph/public/components/settings/url_template_form.tsx b/x-pack/plugins/graph/public/components/settings/url_template_form.tsx index 51dc310ababa2..e89640ef2dbe2 100644 --- a/x-pack/plugins/graph/public/components/settings/url_template_form.tsx +++ b/x-pack/plugins/graph/public/components/settings/url_template_form.tsx @@ -216,15 +216,18 @@ export function UrlTemplateForm(props: UrlTemplateFormProps) { value={currentTemplate.url} onChange={(e) => { setValue('url', e.target.value); - setAutoformatUrl(false); + if ( + (e.nativeEvent as InputEvent)?.inputType !== 'insertFromPaste' || + !isKibanaUrl(e.target.value) + ) { + setAutoformatUrl(false); + } }} onPaste={(e) => { - e.preventDefault(); const pastedUrl = e.clipboardData.getData('text/plain'); if (isKibanaUrl(pastedUrl)) { setAutoformatUrl(true); } - setValue('url', pastedUrl); }} isInvalid={urlPlaceholderMissing || (touched.url && !currentTemplate.url)} /> diff --git a/x-pack/plugins/lens/public/debounced_component/debounced_component.test.tsx b/x-pack/plugins/lens/public/debounced_component/debounced_component.test.tsx index 43dcefae66ad5..6beb565be098c 100644 --- a/x-pack/plugins/lens/public/debounced_component/debounced_component.test.tsx +++ b/x-pack/plugins/lens/public/debounced_component/debounced_component.test.tsx @@ -26,7 +26,7 @@ describe('debouncedComponent', () => { component.setProps({ title: 'yall' }); expect(component.text()).toEqual('there'); await act(async () => { - await new Promise((r) => setTimeout(r, 1)); + await new Promise((r) => setTimeout(r, 10)); }); expect(component.text()).toEqual('yall'); }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 505673f440ef2..61abf8476c632 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -523,6 +523,9 @@ export const loadEvalData = async ({ [jobType]: { actual_field: dependentVariable, predicted_field: predictedField, + ...(jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION + ? { top_classes_field: `${resultsField}.top_classes` } + : {}), metrics: metrics[jobType as keyof EvaluateMetrics], }, }, diff --git a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts index 70b7632775bf5..d760ff9455a88 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts @@ -159,12 +159,13 @@ export class AnomalyExplorerChartsService { const halfPoints = Math.ceil(plotPoints / 2); const bounds = timeFilter.getActiveBounds(); const boundsMin = bounds?.min ? bounds.min.valueOf() : undefined; + const boundsMax = bounds?.max ? bounds.max.valueOf() : undefined; let chartRange: ChartRange = { min: boundsMin ? Math.max(midpointMs - halfPoints * minBucketSpanMs, boundsMin) : midpointMs - halfPoints * minBucketSpanMs, - max: bounds?.max - ? Math.min(midpointMs + halfPoints * minBucketSpanMs, bounds.max.valueOf()) + max: boundsMax + ? Math.min(midpointMs + halfPoints * minBucketSpanMs, boundsMax) : midpointMs + halfPoints * minBucketSpanMs, }; @@ -210,15 +211,21 @@ export class AnomalyExplorerChartsService { } // Elasticsearch aggregation returns points at start of bucket, - // so align the min to the length of the longest bucket. + // so align the min to the length of the longest bucket, + // and use the start of the latest selected bucket in the check + // for too many selected buckets, respecting the max bounds set in the view. chartRange.min = Math.floor(chartRange.min / maxBucketSpanMs) * maxBucketSpanMs; if (boundsMin !== undefined && chartRange.min < boundsMin) { chartRange.min = chartRange.min + maxBucketSpanMs; } + const selectedLatestBucketStart = boundsMax + ? Math.floor(Math.min(selectedLatestMs, boundsMax) / maxBucketSpanMs) * maxBucketSpanMs + : Math.floor(selectedLatestMs / maxBucketSpanMs) * maxBucketSpanMs; + if ( - (chartRange.min > selectedEarliestMs || chartRange.max < selectedLatestMs) && - chartRange.max - chartRange.min < selectedLatestMs - selectedEarliestMs + (chartRange.min > selectedEarliestMs || chartRange.max < selectedLatestBucketStart) && + chartRange.max - chartRange.min < selectedLatestBucketStart - selectedEarliestMs ) { tooManyBuckets = true; } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/logo.json new file mode 100755 index 0000000000000..862f970b7405d --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/logo.json @@ -0,0 +1,3 @@ +{ + "icon": "logoSecurity" +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json new file mode 100755 index 0000000000000..55f07ab077d40 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json @@ -0,0 +1,63 @@ +{ + "id": "security_network", + "title": "Security: Network", + "description": "Detect anomalous network activity in your ECS-compatible network logs.", + "type": "network data", + "logoFile": "logo.json", + "defaultIndexPattern": [ + "logs-*", + "filebeat-*", + "packetbeat-*" + ], + "query": { + "bool": { + "filter": [ + { + "term": { + "event.category": "network" + } + } + ] + } + }, + "jobs": [ + { + "id": "high_count_by_destination_country", + "file": "high_count_by_destination_country.json" + }, + { + "id": "high_count_network_denies", + "file": "high_count_network_denies.json" + }, + { + "id": "high_count_network_events", + "file": "high_count_network_events.json" + }, + { + "id": "rare_destination_country", + "file": "rare_destination_country.json" + } + ], + "datafeeds": [ + { + "id": "datafeed_high_count_by_destination_country", + "file": "datafeed_high_count_by_destination_country.json", + "job_id": "high_count_by_destination_country" + }, + { + "id": "datafeed_high_count_network_denies", + "file": "datafeed_high_count_network_denies.json", + "job_id": "high_count_network_denies" + }, + { + "id": "datafeed_high_count_network_events", + "file": "datafeed_high_count_network_events.json", + "job_id": "high_count_network_events" + }, + { + "id": "datafeed_rare_destination_country", + "file": "datafeed_rare_destination_country.json", + "job_id": "rare_destination_country" + } + ] +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_by_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_by_destination_country.json new file mode 100755 index 0000000000000..48706c6ea6b5d --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_by_destination_country.json @@ -0,0 +1,25 @@ +{ + "job_id": "high_count_by_destination_country", + "indices": [ + "logs-*", + "filebeat-*", + "packetbeat-*" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "filter": [ + { + "term": { + "event.category": "network" + } + }, + { + "exists": { + "field": "destination.geo.country_name" + } + } + ] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_denies.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_denies.json new file mode 100755 index 0000000000000..a4412a6d732e9 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_denies.json @@ -0,0 +1,25 @@ +{ + "job_id": "high_count_network_denies", + "indices": [ + "logs-*", + "filebeat-*", + "packetbeat-*" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "filter": [ + { + "term": { + "event.category": "network" + } + }, + { + "term": { + "event.outcome": "deny" + } + } + ] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_events.json new file mode 100755 index 0000000000000..1e3bbf92b8aed --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_events.json @@ -0,0 +1,20 @@ +{ + "job_id": "high_count_network_events", + "indices": [ + "logs-*", + "filebeat-*", + "packetbeat-*" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "filter": [ + { + "term": { + "event.category": "network" + } + } + ] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_rare_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_rare_destination_country.json new file mode 100755 index 0000000000000..92431a6912faa --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_rare_destination_country.json @@ -0,0 +1,25 @@ +{ + "job_id": "rare_destination_country", + "indices": [ + "logs-*", + "filebeat-*", + "packetbeat-*" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "filter": [ + { + "term": { + "event.category": "network" + } + }, + { + "exists": { + "field": "destination.geo.country_name" + } + } + ] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json new file mode 100755 index 0000000000000..aaee46d9cf80b --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json @@ -0,0 +1,35 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Network - looks for an unusually large spike in network activity to one destination country in the network logs. This could be due to unusually large amounts of reconnaissance or enumeration traffic. Data exfiltration activity may also produce such a surge in traffic to a destination country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.", + "groups": [ + "security", + "network" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "high_non_zero_count by \"destination.geo.country_name\"", + "function": "high_non_zero_count", + "by_field_name": "destination.geo.country_name", + "detector_index": 0 + } + ], + "influencers": [ + "destination.geo.country_name", + "destination.as.organization.name", + "source.ip", + "destination.ip" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-security-network" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json new file mode 100755 index 0000000000000..bc08aa21f3277 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json @@ -0,0 +1,34 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Network - looks for an unusually large spike in network traffic that was denied by network ACLs or firewall rules. Such a burst of denied traffic is usually either 1) a misconfigured application or firewall or 2) suspicious or malicious activity. Unsuccessful attempts at network transit, in order to connect to command-and-control (C2), or engage in data exfiltration, may produce a burst of failed connections. This could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", + "groups": [ + "security", + "network" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "high_count", + "function": "high_count", + "detector_index": 0 + } + ], + "influencers": [ + "destination.geo.country_name", + "destination.as.organization.name", + "source.ip", + "destination.port" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-security-network" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json new file mode 100755 index 0000000000000..d709eb21d7c6d --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json @@ -0,0 +1,34 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Network - looks for an unusually large spike in network traffic. Such a burst of traffic, if not caused by a surge in business activity, can be due to suspicious or malicious activity. Large-scale data exfiltration may produce a burst of network traffic; this could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", + "groups": [ + "security", + "network" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "high_count", + "function": "high_count", + "detector_index": 0 + } + ], + "influencers": [ + "destination.geo.country_name", + "destination.as.organization.name", + "source.ip", + "destination.ip" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-security-network" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json new file mode 100755 index 0000000000000..15571f89b81af --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json @@ -0,0 +1,35 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Network - looks for an unusual destination country name in the network logs. This can be due to initial access, persistence, command-and-control, or exfiltration activity. For example, when a user clicks on a link in a phishing email or opens a malicious document, a request may be sent to download and run a payload from a server in a country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.", + "groups": [ + "security", + "network" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"destination.geo.country_name\"", + "function": "rare", + "by_field_name": "destination.geo.country_name", + "detector_index": 0 + } + ], + "influencers": [ + "destination.geo.country_name", + "destination.as.organization.name", + "source.ip", + "destination.ip" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-security-network" + } +} diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts index 4e1b9ccd2642f..06d626a4c4044 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts @@ -16,6 +16,7 @@ describe('GetCsvReportPanelAction', () => { let core: any; let context: any; let mockLicense$: any; + let mockSearchSource: any; beforeAll(() => { if (typeof window.URL.revokeObjectURL === 'undefined') { @@ -49,22 +50,19 @@ describe('GetCsvReportPanelAction', () => { }, } as any; + mockSearchSource = { + createCopy: () => mockSearchSource, + removeField: jest.fn(), + setField: jest.fn(), + getField: jest.fn(), + getSerializedFields: jest.fn().mockImplementation(() => ({})), + }; + context = { embeddable: { type: 'search', getSavedSearch: () => { - const searchSource = { - createCopy: () => searchSource, - removeField: jest.fn(), - setField: jest.fn(), - getField: jest.fn().mockImplementation((key: string) => { - if (key === 'index') { - return 'my-test-index-*'; - } - }), - getSerializedFields: jest.fn().mockImplementation(() => ({})), - }; - return { searchSource }; + return { searchSource: mockSearchSource }; }, getTitle: () => `The Dude`, getInspectorAdapters: () => null, @@ -79,6 +77,49 @@ describe('GetCsvReportPanelAction', () => { } as any; }); + it('translates empty embeddable context into job params', async () => { + const panel = new GetCsvReportPanelAction(core, mockLicense$()); + + await panel.execute(context); + + expect(core.http.post).toHaveBeenCalledWith( + '/api/reporting/v1/generate/immediate/csv_searchsource', + { + body: '{"searchSource":{},"columns":[],"browserTimezone":"America/New_York"}', + } + ); + }); + + it('translates embeddable context into job params', async () => { + // setup + mockSearchSource = { + createCopy: () => mockSearchSource, + removeField: jest.fn(), + setField: jest.fn(), + getField: jest.fn(), + getSerializedFields: jest.fn().mockImplementation(() => ({ testData: 'testDataValue' })), + }; + context.embeddable.getSavedSearch = () => { + return { + searchSource: mockSearchSource, + columns: ['column_a', 'column_b'], + }; + }; + + const panel = new GetCsvReportPanelAction(core, mockLicense$()); + + // test + await panel.execute(context); + + expect(core.http.post).toHaveBeenCalledWith( + '/api/reporting/v1/generate/immediate/csv_searchsource', + { + body: + '{"searchSource":{"testData":"testDataValue"},"columns":["column_a","column_b"],"browserTimezone":"America/New_York"}', + } + ); + }); + it('allows downloading for valid licenses', async () => { const panel = new GetCsvReportPanelAction(core, mockLicense$()); diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index d440edc3f3fe9..95d193880975c 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -7,21 +7,19 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; -import { CoreSetup } from 'src/core/public'; +import type { CoreSetup } from 'src/core/public'; +import type { ISearchEmbeddable, SavedSearch } from '../../../../../src/plugins/discover/public'; import { loadSharingDataHelpers, - ISearchEmbeddable, - SavedSearch, SEARCH_EMBEDDABLE_TYPE, } from '../../../../../src/plugins/discover/public'; -import { IEmbeddable, ViewMode } from '../../../../../src/plugins/embeddable/public'; -import { - IncompatibleActionError, - UiActionsActionDefinition as ActionDefinition, -} from '../../../../../src/plugins/ui_actions/public'; -import { LicensingPluginSetup } from '../../../licensing/public'; +import type { IEmbeddable } from '../../../../../src/plugins/embeddable/public'; +import { ViewMode } from '../../../../../src/plugins/embeddable/public'; +import type { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public'; +import { IncompatibleActionError } from '../../../../../src/plugins/ui_actions/public'; +import type { LicensingPluginSetup } from '../../../licensing/public'; import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../common/constants'; -import { JobParamsDownloadCSV } from '../../server/export_types/csv_searchsource_immediate/types'; +import type { JobParamsDownloadCSV } from '../../server/export_types/csv_searchsource_immediate/types'; import { checkLicense } from '../lib/license_check'; function isSavedSearchEmbeddable( @@ -64,14 +62,11 @@ export class GetCsvReportPanelAction implements ActionDefinition public async getSearchSource(savedSearch: SavedSearch, embeddable: ISearchEmbeddable) { const { getSharingData } = await loadSharingDataHelpers(); - const searchSource = savedSearch.searchSource.createCopy(); - const { searchSource: serializedSearchSource } = await getSharingData( - searchSource, + return await getSharingData( + savedSearch.searchSource, savedSearch, // TODO: get unsaved state (using embeddale.searchScope): https://github.com/elastic/kibana/issues/43977 this.core.uiSettings ); - - return serializedSearchSource; } public isCompatible = async (context: ActionContext) => { @@ -96,12 +91,13 @@ export class GetCsvReportPanelAction implements ActionDefinition } const savedSearch = embeddable.getSavedSearch(); - const searchSource = await this.getSearchSource(savedSearch, embeddable); + const { columns, searchSource } = await this.getSearchSource(savedSearch, embeddable); const kibanaTimezone = this.core.uiSettings.get('dateFormat:tz'); const browserTimezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; const immediateJobParams: JobParamsDownloadCSV = { searchSource, + columns, browserTimezone, title: savedSearch.title, }; diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index 97433f7a4f0c1..8995ef4739b09 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -8,14 +8,15 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; import React from 'react'; -import { IUiSettingsClient, ToastsSetup } from 'src/core/public'; -import { ShareContext } from '../../../../../src/plugins/share/public'; -import { LicensingPluginSetup } from '../../../licensing/public'; +import type { IUiSettingsClient, ToastsSetup } from 'src/core/public'; +import type { SearchSourceFields } from 'src/plugins/data/common'; +import type { ShareContext } from '../../../../../src/plugins/share/public'; +import type { LicensingPluginSetup } from '../../../licensing/public'; import { CSV_JOB_TYPE } from '../../common/constants'; -import { JobParamsCSV } from '../../server/export_types/csv_searchsource/types'; +import type { JobParamsCSV } from '../../server/export_types/csv_searchsource/types'; import { ReportingPanelContent } from '../components/reporting_panel_content_lazy'; import { checkLicense } from '../lib/license_check'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; +import type { ReportingAPIClient } from '../lib/reporting_api_client'; interface ReportingProvider { apiClient: ReportingAPIClient; @@ -65,7 +66,8 @@ export const csvReportingProvider = ({ browserTimezone, title: sharingData.title as string, objectType, - searchSource: sharingData.searchSource, + searchSource: sharingData.searchSource as SearchSourceFields, + columns: sharingData.columns as string[] | undefined, }; const getJobParams = () => jobParams; diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index 87011cc918587..00ba167c50ae6 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -8,15 +8,15 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; import React from 'react'; -import { IUiSettingsClient, ToastsSetup } from 'src/core/public'; -import { ShareContext } from '../../../../../src/plugins/share/public'; -import { LicensingPluginSetup } from '../../../licensing/public'; -import { LayoutParams } from '../../common/types'; -import { JobParamsPNG } from '../../server/export_types/png/types'; -import { JobParamsPDF } from '../../server/export_types/printable_pdf/types'; +import type { IUiSettingsClient, ToastsSetup } from 'src/core/public'; +import type { ShareContext } from '../../../../../src/plugins/share/public'; +import type { LicensingPluginSetup } from '../../../licensing/public'; +import type { LayoutParams } from '../../common/types'; +import type { JobParamsPNG } from '../../server/export_types/png/types'; +import type { JobParamsPDF } from '../../server/export_types/printable_pdf/types'; import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content_lazy'; import { checkLicense } from '../lib/license_check'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; +import type { ReportingAPIClient } from '../lib/reporting_api_client'; interface ReportingPDFPNGProvider { apiClient: ReportingAPIClient; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap index 62c9ecff830ff..789b68a25ac42 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap @@ -1,18 +1,36 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`fields cells can be multi-value 1`] = ` +exports[`fields from job.columns (7.13+ generated) cells can be multi-value 1`] = ` +"product,category +coconut,\\"cool, rad\\" +" +`; + +exports[`fields from job.columns (7.13+ generated) columns can be top-level fields such as _id and _index 1`] = ` +"\\"_id\\",\\"_index\\",product,category +\\"my-cool-id\\",\\"my-cool-index\\",coconut,\\"cool, rad\\" +" +`; + +exports[`fields from job.columns (7.13+ generated) empty columns defaults to using searchSource.getFields() 1`] = ` +"product +coconut +" +`; + +exports[`fields from job.searchSource.getFields() (7.12 generated) cells can be multi-value 1`] = ` "\\"_id\\",sku \\"my-cool-id\\",\\"This is a cool SKU., This is also a cool SKU.\\" " `; -exports[`fields provides top-level underscored fields as columns 1`] = ` +exports[`fields from job.searchSource.getFields() (7.12 generated) provides top-level underscored fields as columns 1`] = ` "\\"_id\\",\\"_index\\",date,message \\"my-cool-id\\",\\"my-cool-index\\",\\"2020-12-31T00:14:28.000Z\\",\\"it's nice to see you\\" " `; -exports[`fields sorts the fields when they are to be used as table column names 1`] = ` +exports[`fields from job.searchSource.getFields() (7.12 generated) sorts the fields when they are to be used as table column names 1`] = ` "\\"_id\\",\\"_index\\",\\"_score\\",\\"_type\\",date,\\"message_t\\",\\"message_u\\",\\"message_v\\",\\"message_w\\",\\"message_x\\",\\"message_y\\",\\"message_z\\" \\"my-cool-id\\",\\"my-cool-index\\",\\"'-\\",\\"'-\\",\\"2020-12-31T00:14:28.000Z\\",\\"test field T\\",\\"test field U\\",\\"test field V\\",\\"test field W\\",\\"test field X\\",\\"test field Y\\",\\"test field Z\\" " diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts index 0193eaaff2c8d..8694eddce7967 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts @@ -326,7 +326,7 @@ it('uses the scrollId to page all the data', async () => { expect(csvResult.content).toMatchSnapshot(); }); -describe('fields', () => { +describe('fields from job.searchSource.getFields() (7.12 generated)', () => { it('cells can be multi-value', async () => { searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { if (key === 'fields') { @@ -497,6 +497,140 @@ describe('fields', () => { }); }); +describe('fields from job.columns (7.13+ generated)', () => { + it('cells can be multi-value', async () => { + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: [ + { + _id: 'my-cool-id', + _index: 'my-cool-index', + _version: 4, + fields: { + product: 'coconut', + category: [`cool`, `rad`], + }, + }, + ], + total: 1, + }, + }, + }) + ); + + const generateCsv = new CsvGenerator( + createMockJob({ searchSource: {}, columns: ['product', 'category'] }), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + const csvResult = await generateCsv.generateData(); + + expect(csvResult.content).toMatchSnapshot(); + }); + + it('columns can be top-level fields such as _id and _index', async () => { + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: [ + { + _id: 'my-cool-id', + _index: 'my-cool-index', + _version: 4, + fields: { + product: 'coconut', + category: [`cool`, `rad`], + }, + }, + ], + total: 1, + }, + }, + }) + ); + + const generateCsv = new CsvGenerator( + createMockJob({ searchSource: {}, columns: ['_id', '_index', 'product', 'category'] }), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + const csvResult = await generateCsv.generateData(); + + expect(csvResult.content).toMatchSnapshot(); + }); + + it('empty columns defaults to using searchSource.getFields()', async () => { + searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { + if (key === 'fields') { + return ['product']; + } + return mockSearchSourceGetFieldDefault(key); + }); + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: [ + { + _id: 'my-cool-id', + _index: 'my-cool-index', + _version: 4, + fields: { + product: 'coconut', + category: [`cool`, `rad`], + }, + }, + ], + total: 1, + }, + }, + }) + ); + + const generateCsv = new CsvGenerator( + createMockJob({ searchSource: {}, columns: [] }), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + const csvResult = await generateCsv.generateData(); + + expect(csvResult.content).toMatchSnapshot(); + }); +}); + describe('formulas', () => { const TEST_FORMULA = '=SUM(A1:A2)'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index 01959ed08036d..7517396961c00 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -20,6 +20,7 @@ import { ISearchSource, ISearchStartSearchSource, SearchFieldValue, + SearchSourceFields, tabifyDocs, } from '../../../../../../../src/plugins/data/common'; import { KbnServerError } from '../../../../../../../src/plugins/kibana_utils/server'; @@ -60,7 +61,8 @@ function isPlainStringArray( } export class CsvGenerator { - private _formatters: Record | null = null; + private _columns?: string[]; + private _formatters?: Record; private csvContainsFormulas = false; private maxSizeReached = false; private csvRowCount = 0; @@ -135,27 +137,36 @@ export class CsvGenerator { }; } - // use fields/fieldsFromSource from the searchSource to get the ordering of columns - // otherwise use the table columns as they are - private getFields(searchSource: ISearchSource, table: Datatable): string[] { - const fieldValues: Record = { - fields: searchSource.getField('fields'), - fieldsFromSource: searchSource.getField('fieldsFromSource'), - }; - const fieldSource = fieldValues.fieldsFromSource ? 'fieldsFromSource' : 'fields'; - this.logger.debug(`Getting search source fields from: '${fieldSource}'`); - - const fields = fieldValues[fieldSource]; - // Check if field name values are string[] and if the fields are user-defined - if (isPlainStringArray(fields)) { - return fields; + private getColumns(searchSource: ISearchSource, table: Datatable) { + if (this._columns != null) { + return this._columns; } - // Default to using the table column IDs as the fields - const columnIds = table.columns.map((c) => c.id); - // Fields in the API response don't come sorted - they need to be sorted client-side - columnIds.sort(); - return columnIds; + // if columns is not provided in job params, + // default to use fields/fieldsFromSource from the searchSource to get the ordering of columns + const getFromSearchSource = (): string[] => { + const fieldValues: Pick = { + fields: searchSource.getField('fields'), + fieldsFromSource: searchSource.getField('fieldsFromSource'), + }; + const fieldSource = fieldValues.fieldsFromSource ? 'fieldsFromSource' : 'fields'; + this.logger.debug(`Getting columns from '${fieldSource}' in search source.`); + + const fields = fieldValues[fieldSource]; + // Check if field name values are string[] and if the fields are user-defined + if (isPlainStringArray(fields)) { + return fields; + } + + // Default to using the table column IDs as the fields + const columnIds = table.columns.map((c) => c.id); + // Fields in the API response don't come sorted - they need to be sorted client-side + columnIds.sort(); + return columnIds; + }; + this._columns = this.job.columns?.length ? this.job.columns : getFromSearchSource(); + + return this._columns; } private formatCellValues(formatters: Record) { @@ -202,16 +213,16 @@ export class CsvGenerator { } /* - * Use the list of fields to generate the header row + * Use the list of columns to generate the header row */ private generateHeader( - fields: string[], + columns: string[], table: Datatable, builder: MaxSizeStringBuilder, settings: CsvExportSettings ) { this.logger.debug(`Building CSV header row...`); - const header = fields.map(this.escapeValues(settings)).join(settings.separator) + '\n'; + const header = columns.map(this.escapeValues(settings)).join(settings.separator) + '\n'; if (!builder.tryAppend(header)) { return { @@ -227,7 +238,7 @@ export class CsvGenerator { * Format a Datatable into rows of CSV content */ private generateRows( - fields: string[], + columns: string[], table: Datatable, builder: MaxSizeStringBuilder, formatters: Record, @@ -240,7 +251,7 @@ export class CsvGenerator { } const row = - fields + columns .map((f) => ({ column: f, data: dataTableRow[f] })) .map(this.formatCellValues(formatters)) .map(this.escapeValues(settings)) @@ -338,11 +349,13 @@ export class CsvGenerator { break; } - const fields = this.getFields(searchSource, table); + // If columns exists in the job params, use it to order the CSV columns + // otherwise, get the ordering from the searchSource's fields / fieldsFromSource + const columns = this.getColumns(searchSource, table); if (first) { first = false; - this.generateHeader(fields, table, builder, settings); + this.generateHeader(columns, table, builder, settings); } if (table.rows.length < 1) { @@ -350,7 +363,7 @@ export class CsvGenerator { } const formatters = this.getFormatters(table); - this.generateRows(fields, table, builder, formatters, settings); + this.generateRows(columns, table, builder, formatters, settings); // update iterator currentRecord += table.rows.length; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts index f0ad4e00ebd5c..d2a9e2b5bf783 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts @@ -5,13 +5,15 @@ * 2.0. */ -import { BaseParams, BasePayload } from '../../types'; +import type { SearchSourceFields } from 'src/plugins/data/common'; +import type { BaseParams, BasePayload } from '../../types'; export type RawValue = string | object | null | undefined; interface BaseParamsCSV { browserTimezone: string; - searchSource: any; + searchSource: SearchSourceFields; + columns?: string[]; } export type JobParamsCSV = BaseParamsCSV & BaseParams; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/types.d.ts index 276016dd61233..cb1dd659ee2c2 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/types.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { TimeRangeParams } from '../common'; +import type { SearchSourceFields } from 'src/plugins/data/common'; export interface FakeRequest { headers: Record; @@ -14,7 +14,8 @@ export interface FakeRequest { export interface JobParamsDownloadCSV { browserTimezone: string; title: string; - searchSource: any; + searchSource: SearchSourceFields; + columns?: string[]; } export interface SavedObjectServiceError { diff --git a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts index 55092b5236ce6..5d2b77c082ca5 100644 --- a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts @@ -44,6 +44,7 @@ export function registerGenerateCsvFromSavedObjectImmediate( path: `${API_BASE_GENERATE_V1}/immediate/csv_searchsource`, validate: { body: schema.object({ + columns: schema.maybe(schema.arrayOf(schema.string())), searchSource: schema.object({}, { unknowns: 'allow' }), browserTimezone: schema.string({ defaultValue: 'UTC' }), title: schema.string(), diff --git a/x-pack/plugins/timelines/.eslintrc.js b/x-pack/plugins/timelines/.eslintrc.js deleted file mode 100644 index b267018448ba6..0000000000000 --- a/x-pack/plugins/timelines/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - root: true, - extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], - rules: { - '@kbn/eslint/require-license-header': 'off', - }, -}; diff --git a/x-pack/plugins/timelines/common/index.ts b/x-pack/plugins/timelines/common/index.ts index 2354c513f73b8..c095b6c89627e 100644 --- a/x-pack/plugins/timelines/common/index.ts +++ b/x-pack/plugins/timelines/common/index.ts @@ -1,2 +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. + */ + export const PLUGIN_ID = 'timelines'; export const PLUGIN_NAME = 'timelines'; diff --git a/x-pack/plugins/timelines/public/components/index.tsx b/x-pack/plugins/timelines/public/components/index.tsx index 3388b3c44baff..f44ad8052917f 100644 --- a/x-pack/plugins/timelines/public/components/index.tsx +++ b/x-pack/plugins/timelines/public/components/index.tsx @@ -1,3 +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'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts index b535def809de3..c3d24d49e2401 100644 --- a/x-pack/plugins/timelines/public/index.ts +++ b/x-pack/plugins/timelines/public/index.ts @@ -1,3 +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 './index.scss'; import { PluginInitializerContext } from 'src/core/public'; diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts index 7e90d9467fefd..76a692cf8ed10 100644 --- a/x-pack/plugins/timelines/public/plugin.ts +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -1,3 +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 { CoreSetup, Plugin, PluginInitializerContext } from '../../../../src/core/public'; import { TimelinesPluginSetup, TimelineProps } from './types'; import { getTimelineLazy } from './methods'; diff --git a/x-pack/plugins/timelines/public/types.ts b/x-pack/plugins/timelines/public/types.ts index b199b45902718..1fa6d33a6af60 100644 --- a/x-pack/plugins/timelines/public/types.ts +++ b/x-pack/plugins/timelines/public/types.ts @@ -1,3 +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 { ReactElement } from 'react'; export interface TimelinesPluginSetup { diff --git a/x-pack/plugins/timelines/server/config.ts b/x-pack/plugins/timelines/server/config.ts index 633a95b8f91a7..31be256611803 100644 --- a/x-pack/plugins/timelines/server/config.ts +++ b/x-pack/plugins/timelines/server/config.ts @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. + * 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 { TypeOf, schema } from '@kbn/config-schema'; diff --git a/x-pack/plugins/timelines/server/index.ts b/x-pack/plugins/timelines/server/index.ts index 32de97be2704a..65e2b6494c6f4 100644 --- a/x-pack/plugins/timelines/server/index.ts +++ b/x-pack/plugins/timelines/server/index.ts @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. + * 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 { PluginInitializerContext } from '../../../../src/core/server'; diff --git a/x-pack/plugins/timelines/server/plugin.ts b/x-pack/plugins/timelines/server/plugin.ts index 3e330b19b7fdb..825d42994e096 100644 --- a/x-pack/plugins/timelines/server/plugin.ts +++ b/x-pack/plugins/timelines/server/plugin.ts @@ -1,3 +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 { PluginInitializerContext, CoreSetup, diff --git a/x-pack/plugins/timelines/server/routes/index.ts b/x-pack/plugins/timelines/server/routes/index.ts index edb10c579b30b..1c651469b795a 100644 --- a/x-pack/plugins/timelines/server/routes/index.ts +++ b/x-pack/plugins/timelines/server/routes/index.ts @@ -1,3 +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 { IRouter } from '../../../../../src/core/server'; export function defineRoutes(router: IRouter) { diff --git a/x-pack/plugins/timelines/server/types.ts b/x-pack/plugins/timelines/server/types.ts index cb544562b79b4..5bcc90b48f0b9 100644 --- a/x-pack/plugins/timelines/server/types.ts +++ b/x-pack/plugins/timelines/server/types.ts @@ -1,3 +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. + */ + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface TimelinesPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a0f535e93a8a6..63580981cb320 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -669,8 +669,7 @@ "dashboard.panelStorageError.clearError": "保存されていない変更の消去中にエラーが発生しました。{message}", "dashboard.panelStorageError.getError": "保存されていない変更の取得中にエラーが発生しました。{message}", "dashboard.panelStorageError.setError": "保存されていない変更の設定中にエラーが発生しました。{message}", - "dashboard.panelToolbar.addPanelButtonLabel": "パネルの作成", - "dashboard.panelToolbar.libraryButtonLabel": "ライブラリから追加", + "dashboard.solutionToolbar.addPanelButtonLabel": "パネルの作成", "dashboard.placeholder.factory.displayName": "プレースホルダー", "dashboard.savedDashboard.newDashboardTitle": "新規ダッシュボード", "dashboard.stateManager.timeNotSavedWithDashboardErrorMessage": "このダッシュボードに時刻が保存されていないため、同期できません。", @@ -7344,7 +7343,6 @@ "xpack.data.searchSessionIndicator.canceledTitleText": "検索セッションが停止しました", "xpack.data.searchSessionIndicator.canceledTooltipText": "検索セッションが停止しました", "xpack.data.searchSessionIndicator.continueInBackgroundButtonText": "セッションの保存", - "xpack.data.searchSessionIndicator.disabledDueToAutoRefreshMessage": "自動更新が有効な場合は、検索セッションの保存を使用できません。", "xpack.data.searchSessionIndicator.disabledDueToDisabledGloballyMessage": "検索セッションを管理するアクセス権がありません", "xpack.data.searchSessionIndicator.disabledDueToTimeoutMessage": "検索セッション結果が期限切れです。", "xpack.data.searchSessionIndicator.loadingInTheBackgroundDescriptionText": "管理から完了した結果に戻ることができます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 31bc197f2ea05..77ef19e61030a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -672,8 +672,7 @@ "dashboard.panelStorageError.clearError": "清除未保存更改时遇到错误:{message}", "dashboard.panelStorageError.getError": "获取未保存更改时遇到错误:{message}", "dashboard.panelStorageError.setError": "设置未保存更改时遇到错误:{message}", - "dashboard.panelToolbar.addPanelButtonLabel": "创建面板", - "dashboard.panelToolbar.libraryButtonLabel": "从库中添加", + "dashboard.solutionToolbar.addPanelButtonLabel": "创建面板", "dashboard.placeholder.factory.displayName": "占位符", "dashboard.savedDashboard.newDashboardTitle": "新建仪表板", "dashboard.stateManager.timeNotSavedWithDashboardErrorMessage": "时间未随此仪表板保存,因此无法同步。", @@ -7408,7 +7407,6 @@ "xpack.data.searchSessionIndicator.canceledTitleText": "搜索会话已停止", "xpack.data.searchSessionIndicator.canceledTooltipText": "搜索会话已停止", "xpack.data.searchSessionIndicator.continueInBackgroundButtonText": "保存会话", - "xpack.data.searchSessionIndicator.disabledDueToAutoRefreshMessage": "启用自动刷新时,保存搜索会话不可用。", "xpack.data.searchSessionIndicator.disabledDueToDisabledGloballyMessage": "您无权管理搜索会话", "xpack.data.searchSessionIndicator.disabledDueToTimeoutMessage": "搜索会话结果已过期。", "xpack.data.searchSessionIndicator.loadingInTheBackgroundDescriptionText": "可以从“管理”中返回至完成的结果。", diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index b7df493a1036a..1f6fe310bfa7c 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -74,8 +74,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/reporting_api_integration/reporting_and_security.config.ts'), require.resolve('../test/reporting_api_integration/reporting_without_security.config.ts'), require.resolve('../test/security_solution_endpoint_api_int/config.ts'), - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96515 - // require.resolve('../test/fleet_api_integration/config.ts'), + require.resolve('../test/fleet_api_integration/config.ts'), require.resolve('../test/search_sessions_integration/config.ts'), require.resolve('../test/saved_object_tagging/api_integration/security_and_spaces/config.ts'), require.resolve('../test/saved_object_tagging/api_integration/tagging_api/config.ts'), diff --git a/x-pack/test/api_integration/apis/ml/modules/get_module.ts b/x-pack/test/api_integration/apis/ml/modules/get_module.ts index bd35bdddc3399..aade372374548 100644 --- a/x-pack/test/api_integration/apis/ml/modules/get_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/get_module.ts @@ -27,6 +27,7 @@ const moduleIds = [ 'sample_data_ecommerce', 'sample_data_weblogs', 'security_linux', + 'security_network', 'security_windows', 'siem_auditbeat', 'siem_auditbeat_auth', diff --git a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts index d7ba410dd5dd8..d6020e17801fd 100644 --- a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts @@ -143,7 +143,7 @@ export default ({ getService }: FtrProviderContext) => { user: USER.ML_POWERUSER, expected: { responseCode: 200, - moduleIds: ['security_linux', 'security_windows'], + moduleIds: ['security_linux', 'security_network', 'security_windows'], }, }, ]; diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index 627cb299f0909..5737794eefeab 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -19,11 +19,13 @@ export default function (providerContext: FtrProviderContext) { await esArchiver.load('fleet/empty_fleet_server'); }); beforeEach(async () => { + await esArchiver.unload('fleet/empty_fleet_server'); await esArchiver.load('fleet/agents'); }); setupFleetAndAgents(providerContext); afterEach(async () => { await esArchiver.unload('fleet/agents'); + await esArchiver.load('fleet/empty_fleet_server'); }); after(async () => { await esArchiver.unload('fleet/empty_fleet_server'); diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 41232f73efa5c..008614f075514 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -26,12 +26,15 @@ export default function (providerContext: FtrProviderContext) { describe('fleet upgrade', () => { skipIfNoDockerRegistry(providerContext); before(async () => { - await esArchiver.loadIfNeeded('fleet/agents'); + await esArchiver.load('fleet/agents'); }); setupFleetAndAgents(providerContext); beforeEach(async () => { await esArchiver.load('fleet/agents'); }); + afterEach(async () => { + await esArchiver.unload('fleet/agents'); + }); after(async () => { await esArchiver.unload('fleet/agents'); }); diff --git a/x-pack/test/fleet_api_integration/apis/agents_setup.ts b/x-pack/test/fleet_api_integration/apis/agents_setup.ts index 700a06750d2f4..25b4e16535fda 100644 --- a/x-pack/test/fleet_api_integration/apis/agents_setup.ts +++ b/x-pack/test/fleet_api_integration/apis/agents_setup.ts @@ -24,7 +24,7 @@ export default function (providerContext: FtrProviderContext) { after(async () => { await esArchiver.unload('empty_kibana'); - await esArchiver.load('fleet/empty_fleet_server'); + await esArchiver.unload('fleet/empty_fleet_server'); }); beforeEach(async () => { diff --git a/x-pack/test/fleet_api_integration/apis/epm/list.ts b/x-pack/test/fleet_api_integration/apis/epm/list.ts index 5a991e52bdba4..c482f4012d2e5 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/list.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/list.ts @@ -26,7 +26,7 @@ export default function (providerContext: FtrProviderContext) { }); setupFleetAndAgents(providerContext); after(async () => { - await esArchiver.load('fleet/empty_fleet_server'); + await esArchiver.unload('fleet/empty_fleet_server'); }); describe('list api tests', async () => { diff --git a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts index c9709475d182d..4c16a4fbd1cfa 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts @@ -24,7 +24,7 @@ export default function (providerContext: FtrProviderContext) { after(async () => { await esArchiver.unload('empty_kibana'); - await esArchiver.load('fleet/empty_fleet_server'); + await esArchiver.unload('fleet/empty_fleet_server'); }); beforeEach(async () => { try { diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts index c437cfaa8f5dc..d4a909f6a0474 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts @@ -50,8 +50,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('csvDownloadStarted'); // validate toast panel }; - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96000 - describe.skip('Download CSV', () => { + describe('Download CSV', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await browser.setWindowSize(1600, 850); diff --git a/x-pack/test/functional/es_archives/fleet/agents/mappings.json b/x-pack/test/functional/es_archives/fleet/agents/mappings.json index 5b35fa05a51bf..72a7368e4d0a8 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/mappings.json +++ b/x-pack/test/functional/es_archives/fleet/agents/mappings.json @@ -3089,7 +3089,7 @@ ".fleet-actions": { } }, - "index": ".fleet-actions_1", + "index": ".fleet-actions-7", "mappings": { "_meta": { "migrationHash": "6527beea5a4a2f33acb599585ed4710442ece7f2" @@ -3136,7 +3136,7 @@ ".fleet-agents": { } }, - "index": ".fleet-agents_1", + "index": ".fleet-agent-7", "mappings": { "_meta": { "migrationHash": "87cab95ac988d78a78d0d66bbf05361b65dcbacf" @@ -3373,7 +3373,7 @@ ".fleet-enrollment-api-keys": { } }, - "index": ".fleet-enrollment-api-keys_1", + "index": ".fleet-enrollment-api-keys-7", "mappings": { "_meta": { "migrationHash": "06bef724726f3bea9f474a09be0a7f7881c28d4a" @@ -3422,7 +3422,7 @@ ".fleet-policies": { } }, - "index": ".fleet-policies_1", + "index": ".fleet-policies-7", "mappings": { "_meta": { "migrationHash": "c2c2a49b19562942fa7c1ff1537e66e751cdb4fa" @@ -3466,7 +3466,7 @@ ".fleet-servers": { } }, - "index": ".fleet-servers_1", + "index": ".fleet-servers-7", "mappings": { "_meta": { "migrationHash": "e2782448c7235ec9af66ca7997e867d715ac379c" diff --git a/x-pack/test/functional/es_archives/fleet/empty_fleet_server/mappings.json b/x-pack/test/functional/es_archives/fleet/empty_fleet_server/mappings.json index 73f090b6103dc..a04b7a7dc21c7 100644 --- a/x-pack/test/functional/es_archives/fleet/empty_fleet_server/mappings.json +++ b/x-pack/test/functional/es_archives/fleet/empty_fleet_server/mappings.json @@ -5,7 +5,7 @@ ".fleet-actions": { } }, - "index": ".fleet-actions_1", + "index": ".fleet-actions-7", "mappings": { "_meta": { "migrationHash": "6527beea5a4a2f33acb599585ed4710442ece7f2" @@ -52,7 +52,7 @@ ".fleet-agents": { } }, - "index": ".fleet-agents_1", + "index": ".fleet-agents-7", "mappings": { "_meta": { "migrationHash": "87cab95ac988d78a78d0d66bbf05361b65dcbacf" @@ -289,7 +289,7 @@ ".fleet-enrollment-api-keys": { } }, - "index": ".fleet-enrollment-api-keys_1", + "index": ".fleet-enrollment-api-keys-7", "mappings": { "_meta": { "migrationHash": "06bef724726f3bea9f474a09be0a7f7881c28d4a" @@ -338,7 +338,7 @@ ".fleet-policies": { } }, - "index": ".fleet-policies_1", + "index": ".fleet-policies-7", "mappings": { "_meta": { "migrationHash": "c2c2a49b19562942fa7c1ff1537e66e751cdb4fa" @@ -382,7 +382,7 @@ ".fleet-servers": { } }, - "index": ".fleet-servers_1", + "index": ".fleet-servers-7", "mappings": { "_meta": { "migrationHash": "e2782448c7235ec9af66ca7997e867d715ac379c" diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts b/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts index ebc7badd88f42..f381bc1edd28e 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts @@ -10,9 +10,9 @@ import supertest from 'supertest'; import { JobParamsDownloadCSV } from '../../../plugins/reporting/server/export_types/csv_searchsource_immediate/types'; import { FtrProviderContext } from '../ftr_provider_context'; -const getMockJobParams = (obj: Partial): JobParamsDownloadCSV => ({ +const getMockJobParams = (obj: any): JobParamsDownloadCSV => ({ title: `Mock CSV Title`, - ...(obj as any), + ...obj, }); // eslint-disable-next-line import/no-default-export @@ -31,8 +31,7 @@ export default function ({ getService }: FtrProviderContext) { }, }; - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96000 - describe.skip('CSV Generation from SearchSource', () => { + describe('CSV Generation from SearchSource', () => { before(async () => { await kibanaServer.uiSettings.update({ 'csv:quoteValues': false, @@ -387,9 +386,9 @@ export default function ({ getService }: FtrProviderContext) { version: true, index: '907bc200-a294-11e9-a900-ef10e0ac769e', sort: [{ date: 'desc' }], - fields: ['date', 'message', '_id', '_index'], filter: [], }, + columns: ['date', 'message', '_id', '_index'], }) ); const { status: resStatus, text: resText, type: resType } = res;