From 4fbb6f0baa5eff64a2e63b6da4540b38162bdaf1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Jun 2021 10:49:31 +0200 Subject: [PATCH 01/10] Update dependency @elastic/charts to v30 (master) (#101409) Co-authored-by: Renovate Bot Co-authored-by: nickofthyme Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marco Vettorello --- package.json | 2 +- src/plugins/vis_type_xy/public/config/get_axis.ts | 2 +- .../components/timeline/timeline.tsx | 4 +++- .../pages/components/charts/anomaly_chart/line.tsx | 1 - .../charts/anomaly_chart/model_bounds.tsx | 1 - .../components/charts/anomaly_chart/scatter.tsx | 1 - .../pages/components/charts/common/axes.tsx | 2 +- .../pages/components/charts/common/utils.ts | 11 +++++++++-- .../components/common/charts/duration_chart.tsx | 2 +- .../common/charts/duration_line_series_list.tsx | 1 - x-pack/test/functional/apps/lens/chart_data.ts | 14 +++++++------- yarn.lock | 8 ++++---- 12 files changed, 27 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index e12e431f686940..4614e9293e633a 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "dependencies": { "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "29.2.0", + "@elastic/charts": "30.0.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.4", "@elastic/ems-client": "7.13.0", diff --git a/src/plugins/vis_type_xy/public/config/get_axis.ts b/src/plugins/vis_type_xy/public/config/get_axis.ts index 91647f02e2a1e6..08b17c882eea6b 100644 --- a/src/plugins/vis_type_xy/public/config/get_axis.ts +++ b/src/plugins/vis_type_xy/public/config/get_axis.ts @@ -169,7 +169,7 @@ function getAxisDomain( const { min, max, defaultYExtents, boundsMargin } = scale; const fit = defaultYExtents; - const padding = boundsMargin; + const padding = boundsMargin || undefined; if (!isNil(min) && !isNil(max)) { return { fit, padding, min, max }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx index 0a657b52424273..dd1023e7f0185a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx @@ -19,6 +19,7 @@ import { TooltipValue, niceTimeFormatter, ElementClickListener, + GeometryValue, RectAnnotation, RectAnnotationDatum, } from '@elastic/charts'; @@ -141,7 +142,8 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible const onClickPoint: ElementClickListener = useCallback( ([[geometryValue]]) => { if (!Array.isArray(geometryValue)) { - const { x: timestamp } = geometryValue; + // casting to GeometryValue as we are using cartesian charts + const { x: timestamp } = geometryValue as GeometryValue; jumpToTime(timestamp); stopAutoReload(); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx index 28194e2d453ac3..855c66e1e1c0db 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx @@ -29,7 +29,6 @@ export const Line: FC = ({ chartData }) => { xAccessor={'time'} yAccessors={['value']} data={chartData} - yScaleToDataExtent={false} curve={CurveType.CURVE_MONOTONE_X} lineSeriesStyle={lineSeriesStyle} color={LINE_COLOR} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/model_bounds.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/model_bounds.tsx index a2d2bad5a503e5..fe07c6800c0e83 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/model_bounds.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/model_bounds.tsx @@ -42,7 +42,6 @@ export const ModelBounds: FC = ({ modelData }) => { y0Accessors={['modelLower']} data={model} stackAccessors={['time']} - yScaleToDataExtent={false} curve={CurveType.CURVE_MONOTONE_X} areaSeriesStyle={areaSeriesStyle} color={MODEL_COLOR} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/scatter.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/scatter.tsx index cbe3a99512d1ee..bc6d0065fec8ee 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/scatter.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/scatter.tsx @@ -37,7 +37,6 @@ export const Scatter: FC = ({ chartData }) => { xAccessor={'time'} yAccessors={['value']} data={chartData} - yScaleToDataExtent={false} curve={CurveType.CURVE_MONOTONE_X} lineSeriesStyle={scatterSeriesStyle} color={LINE_COLOR} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/axes.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/axes.tsx index 320faf0bbc615c..bbd700a0ef8e14 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/axes.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/axes.tsx @@ -22,7 +22,7 @@ function tickFormatter(d: number): string { } export const Axes: FC = ({ chartData }) => { - const yDomain = chartData !== undefined ? getYRange(chartData) : undefined; + const yDomain = getYRange(chartData); return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/utils.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/utils.ts index 8623133fa78357..c55bb769b6eb9f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/utils.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/utils.ts @@ -5,9 +5,15 @@ * 2.0. */ -export function getYRange(chartData: any[]) { +export function getYRange(chartData?: any[]) { + const fit = false; + + if (chartData === undefined) { + return { fit }; + } + if (chartData.length === 0) { - return { min: 0, max: 0 }; + return { min: 0, max: 0, fit }; } let max: number = Number.MIN_VALUE; @@ -24,6 +30,7 @@ export function getYRange(chartData: any[]) { return { min, max, + fit, }; } diff --git a/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx index 1f1e80838b6580..c7884683e1aedf 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx @@ -113,7 +113,7 @@ export const DurationChartComponent = ({ tickFormat={timeFormatter(getChartDateLabel(min, max))} /> getTickFormat(d)} diff --git a/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx index a13696a6379b31..4ccbbd37b041b5 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx @@ -29,7 +29,6 @@ export const DurationLineSeriesList = ({ monitorType, lines }: Props) => ( xAccessor={0} xScaleType="time" yAccessors={[1]} - yScaleToDataExtent={false} yScaleType="linear" fit={Fit.Linear} tickFormat={(d) => diff --git a/x-pack/test/functional/apps/lens/chart_data.ts b/x-pack/test/functional/apps/lens/chart_data.ts index 24aaab9807494b..498c722f33c2e7 100644 --- a/x-pack/test/functional/apps/lens/chart_data.ts +++ b/x-pack/test/functional/apps/lens/chart_data.ts @@ -107,13 +107,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // assert legend expect(debugState.legend!.items).to.eql([ - { key: '6000', name: '> 6000', color: '#6092c0' }, - { key: '8000', name: '> 8000', color: '#6092c0' }, - { key: '10000', name: '> 10000', color: '#a8bfda' }, - { key: '12000', name: '> 12000', color: '#ebeff5' }, - { key: '14000', name: '> 14000', color: '#ebeff5' }, - { key: '16000', name: '> 16000', color: '#ecb385' }, - { key: '18000', name: '> 18000', color: '#e7664c' }, + { key: '6000', name: '> 6,000', color: '#6092c0' }, + { key: '8000', name: '> 8,000', color: '#6092c0' }, + { key: '10000', name: '> 10,000', color: '#a8bfda' }, + { key: '12000', name: '> 12,000', color: '#ebeff5' }, + { key: '14000', name: '> 14,000', color: '#ebeff5' }, + { key: '16000', name: '> 16,000', color: '#ecb385' }, + { key: '18000', name: '> 18,000', color: '#e7664c' }, ]); }); diff --git a/yarn.lock b/yarn.lock index 954a9ebfec8718..6d3da3bd879ba1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1366,10 +1366,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@29.2.0": - version "29.2.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-29.2.0.tgz#cd65887c0ca1d420493aee1b570c862ca0df5311" - integrity sha512-gj3Gew9zy8XPNEkAAznOjncO5AF63jy/X1k1VIcNPqdqMi07YYCZwCQjMzUVoS4RN6X4GSzxhrYfGAeyZP8gqg== +"@elastic/charts@30.0.0": + version "30.0.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-30.0.0.tgz#e19ad8b94928aa9bac5d7facc488fa69b683ff1e" + integrity sha512-r22T2dlW3drEmrIx6JNlOOzSp0JCkI/qbIfmvzdMBu8E8hITkJTaXJaLsNN4mz9EvR9jEM8XQQPQXOFKJhWixw== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" From 65251051fdcb948bfb0bc03ad211b851bdb877bd Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 8 Jun 2021 11:27:00 +0200 Subject: [PATCH 02/10] [Discover] Deangularize discover controller (#96766) Co-authored-by: Tim Roes --- .../data/common/search/search_source/mocks.ts | 6 +- .../search/search_source/search_source.ts | 2 +- .../query_string/query_string_manager.mock.ts | 3 +- .../timefilter/timefilter_service.mock.ts | 2 +- .../public/__mocks__/index_pattern.ts | 4 + .../__mocks__/index_pattern_with_timefield.ts | 6 + .../discover/public/__mocks__/saved_search.ts | 3 + .../public/__mocks__/search_session.ts | 27 ++ .../discover/public/__mocks__/services.ts | 63 +++ .../angular/create_discover_directive.ts | 16 + .../create_discover_grid_directive.tsx | 2 +- .../angular/directives/debounce/debounce.js | 57 --- .../directives/debounce/debounce.test.ts | 127 ------ .../public/application/angular/discover.js | 415 +----------------- .../angular/discover_datagrid.html | 22 +- .../application/angular/discover_legacy.html | 19 +- .../angular/doc_table/actions/columns.test.ts | 2 +- .../angular/doc_table/actions/columns.ts | 2 +- .../doc_table/components/row_headers.test.js | 2 +- .../doc_table/create_doc_table_react.tsx | 14 +- .../angular/doc_table/doc_table.test.js | 2 +- .../angular}/get_inner_angular.ts | 36 +- .../application/angular/helpers/index.ts | 1 - .../public/application/angular/index.ts | 1 - .../application/angular/response_handler.js | 109 ----- .../main/components/chart/discover_chart.tsx | 145 ++++++ .../main/components/chart}/histogram.tsx | 6 +- .../main/components/chart/index.ts} | 2 +- .../main/components/chart}/point_series.ts | 16 +- .../components/hits_counter/hits_counter.scss | 0 .../hits_counter/hits_counter.test.tsx | 0 .../components/hits_counter/hits_counter.tsx | 7 +- .../main}/components/hits_counter/index.ts | 0 .../components/layout/discover_layout.scss} | 2 +- .../layout/discover_layout.test.tsx | 76 ++++ .../components/layout/discover_layout.tsx} | 406 +++++++++-------- .../main/components/layout}/index.ts | 3 +- .../apps/main/components/layout/types.ts | 34 ++ .../loading_spinner/loading_spinner.scss | 0 .../loading_spinner/loading_spinner.test.tsx | 0 .../loading_spinner/loading_spinner.tsx | 0 .../components/no_results/_no_results.scss | 0 .../main}/components/no_results/index.ts | 0 .../components/no_results/no_results.test.tsx | 2 +- .../components/no_results/no_results.tsx | 4 +- .../no_results/no_results_helper.tsx | 0 ...iscover_field_details_footer.test.tsx.snap | 0 .../discover_index_pattern.test.tsx.snap | 0 ...ver_index_pattern_management.test.tsx.snap | 0 .../sidebar/change_indexpattern.tsx | 0 .../components/sidebar/discover_field.scss | 0 .../sidebar/discover_field.test.tsx | 13 +- .../components/sidebar/discover_field.tsx | 4 +- .../sidebar/discover_field_bucket.scss | 0 .../sidebar/discover_field_bucket.tsx | 2 +- .../sidebar/discover_field_details.scss | 0 .../sidebar/discover_field_details.test.tsx | 10 +- .../sidebar/discover_field_details.tsx | 2 +- .../discover_field_details_footer.test.tsx | 10 +- .../sidebar/discover_field_details_footer.tsx | 4 +- .../sidebar/discover_field_search.scss | 0 .../sidebar/discover_field_search.test.tsx | 0 .../sidebar/discover_field_search.tsx | 0 .../sidebar/discover_index_pattern.test.tsx | 15 +- .../sidebar/discover_index_pattern.tsx | 55 +-- ...discover_index_pattern_management.test.tsx | 14 +- .../discover_index_pattern_management.tsx | 4 +- .../components/sidebar/discover_sidebar.scss | 0 .../sidebar/discover_sidebar.test.tsx | 57 +-- .../components/sidebar/discover_sidebar.tsx | 20 +- .../discover_sidebar_responsive.test.tsx | 24 +- .../sidebar/discover_sidebar_responsive.tsx | 35 +- .../main}/components/sidebar/index.ts | 0 .../sidebar/lib/field_calculator.js | 0 .../sidebar/lib/field_calculator.test.ts | 10 +- .../sidebar/lib/field_filter.test.ts | 2 +- .../components/sidebar/lib/field_filter.ts | 2 +- .../components/sidebar/lib/get_details.ts | 4 +- .../sidebar/lib/get_field_type_name.ts | 0 .../lib/get_index_pattern_field_list.ts | 2 +- .../components/sidebar/lib/get_warnings.ts | 2 +- .../sidebar/lib/group_fields.test.ts | 2 +- .../components/sidebar/lib/group_fields.tsx | 0 .../sidebar/lib/visualize_trigger_utils.ts | 6 +- .../sidebar/string_progress_bar.tsx | 0 .../main}/components/sidebar/types.ts | 0 .../components/skip_bottom_button/index.ts | 0 .../skip_bottom_button.test.tsx | 0 .../skip_bottom_button/skip_bottom_button.tsx | 0 .../components/timechart_header/index.ts | 0 .../timechart_header/timechart_header.scss | 0 .../timechart_header.test.tsx | 2 +- .../timechart_header/timechart_header.tsx | 14 +- .../open_search_panel.test.tsx.snap | 0 .../top_nav/discover_topnav.test.tsx | 53 +++ .../components/top_nav}/discover_topnav.tsx | 44 +- .../top_nav/get_top_nav_links.test.ts | 9 +- .../components/top_nav/get_top_nav_links.ts | 14 +- .../top_nav/on_save_search.test.tsx | 14 +- .../components/top_nav/on_save_search.tsx | 14 +- .../top_nav/open_options_popover.scss | 0 .../top_nav/open_options_popover.test.tsx | 4 +- .../top_nav/open_options_popover.tsx | 4 +- .../top_nav/open_search_panel.test.tsx | 2 +- .../components/top_nav/open_search_panel.tsx | 4 +- .../top_nav/show_open_search_panel.tsx | 0 .../uninitialized}/uninitialized.tsx | 0 .../apps/main/discover_main_app.test.tsx | 46 ++ .../apps/main/discover_main_app.tsx | 145 ++++++ .../constants.ts => apps/main/index.ts} | 7 +- .../services}/discover_search_session.test.ts | 14 +- .../main/services}/discover_search_session.ts | 6 +- .../main/services}/discover_state.test.ts | 6 +- .../main/services}/discover_state.ts | 14 +- .../main/services/use_discover_state.test.ts | 82 ++++ .../apps/main/services/use_discover_state.ts | 155 +++++++ .../main/services/use_saved_search.test.ts | 91 ++++ .../apps/main/services/use_saved_search.ts | 337 ++++++++++++++ .../main/services/use_search_session.test.ts | 39 ++ .../apps/main/services/use_search_session.ts | 65 +++ .../apps/main/services/use_url.test.ts | 27 ++ .../application/apps/main/services/use_url.ts | 31 ++ .../main/utils}/calc_field_counts.test.ts | 11 +- .../main/utils}/calc_field_counts.ts | 5 +- .../main/utils/get_chart_agg_config.test.ts | 59 +++ .../main/utils/get_chart_agg_configs.ts} | 23 +- .../apps/main/utils/get_dimensions.test.ts | 70 +++ .../main/utils}/get_dimensions.ts | 20 +- .../main/utils}/get_result_state.test.ts | 16 +- .../main/utils}/get_result_state.ts | 10 +- .../main/utils}/get_sharing_data.test.ts | 6 +- .../main/utils}/get_sharing_data.ts | 10 +- .../main/utils}/get_state_defaults.test.ts | 17 +- .../main/utils}/get_state_defaults.ts | 23 +- ...get_switch_index_pattern_app_state.test.ts | 2 +- .../get_switch_index_pattern_app_state.ts | 6 +- .../histogram => apps/main/utils}/index.ts | 2 +- .../main/utils}/nested_fields.ts | 0 .../main/utils}/persist_saved_search.ts | 16 +- .../main/utils}/resolve_index_pattern.test.ts | 6 +- .../main/utils}/resolve_index_pattern.ts | 6 +- .../main/utils}/update_search_source.test.ts | 80 ++-- .../main/utils}/update_search_source.ts | 70 +-- .../apps/main/utils/use_singleton.ts | 26 ++ .../main/utils}/validate_time_range.test.ts | 2 +- .../main/utils}/validate_time_range.ts | 0 .../components/create_discover_directive.ts | 35 -- .../application/components/discover.test.tsx | 106 ----- .../components/discover_topnav.test.tsx | 77 ---- .../apply_aggs_to_search_source.test.ts | 89 ---- .../histogram/get_dimensions.test.ts | 63 --- .../application/components/table/table.tsx | 2 +- .../public/application/components/types.ts | 172 -------- .../helpers/use_data_grid_columns.ts | 2 +- .../discover/public/application/types.ts | 14 + src/plugins/discover/public/build_services.ts | 4 +- src/plugins/discover/public/plugin.tsx | 6 +- src/plugins/discover/public/shared/index.ts | 2 +- test/functional/apps/discover/_discover.ts | 6 +- .../_indexpattern_without_timefield.ts | 20 +- test/functional/apps/discover/_inspector.ts | 4 +- .../apps/discover/_runtime_fields_editor.ts | 19 +- .../index_pattern_without_timefield/data.json | 5 +- .../mappings.json | 7 - .../generate_csv/generate_csv.ts | 2 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../functional/services/search_sessions.ts | 1 + .../tests/apps/discover/async_search.ts | 31 +- 169 files changed, 2298 insertions(+), 2047 deletions(-) create mode 100644 src/plugins/discover/public/__mocks__/search_session.ts create mode 100644 src/plugins/discover/public/__mocks__/services.ts create mode 100644 src/plugins/discover/public/application/angular/create_discover_directive.ts rename src/plugins/discover/public/application/{components => angular}/create_discover_grid_directive.tsx (95%) delete mode 100644 src/plugins/discover/public/application/angular/directives/debounce/debounce.js delete mode 100644 src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts rename src/plugins/discover/public/{ => application/angular}/get_inner_angular.ts (78%) delete mode 100644 src/plugins/discover/public/application/angular/response_handler.js create mode 100644 src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx rename src/plugins/discover/public/application/{angular/directives => apps/main/components/chart}/histogram.tsx (97%) rename src/plugins/discover/public/application/{angular/directives/debounce/index.js => apps/main/components/chart/index.ts} (85%) rename src/plugins/discover/public/application/{angular/helpers => apps/main/components/chart}/point_series.ts (90%) rename src/plugins/discover/public/application/{ => apps/main}/components/hits_counter/hits_counter.scss (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/hits_counter/hits_counter.test.tsx (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/hits_counter/hits_counter.tsx (94%) rename src/plugins/discover/public/application/{ => apps/main}/components/hits_counter/index.ts (100%) rename src/plugins/discover/public/application/{components/discover.scss => apps/main/components/layout/discover_layout.scss} (97%) create mode 100644 src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx rename src/plugins/discover/public/application/{components/discover.tsx => apps/main/components/layout/discover_layout.tsx} (52%) rename src/plugins/discover/public/application/{angular/directives => apps/main/components/layout}/index.ts (77%) create mode 100644 src/plugins/discover/public/application/apps/main/components/layout/types.ts rename src/plugins/discover/public/application/{ => apps/main}/components/loading_spinner/loading_spinner.scss (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/loading_spinner/loading_spinner.test.tsx (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/loading_spinner/loading_spinner.tsx (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/no_results/_no_results.scss (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/no_results/index.ts (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/no_results/no_results.test.tsx (98%) rename src/plugins/discover/public/application/{ => apps/main}/components/no_results/no_results.tsx (94%) rename src/plugins/discover/public/application/{ => apps/main}/components/no_results/no_results_helper.tsx (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/change_indexpattern.tsx (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_field.scss (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_field.test.tsx (91%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_field.tsx (99%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_field_bucket.scss (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_field_bucket.tsx (98%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_field_details.scss (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_field_details.test.tsx (88%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_field_details.tsx (99%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_field_details_footer.test.tsx (87%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_field_details_footer.tsx (91%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_field_search.scss (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_field_search.test.tsx (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_field_search.tsx (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_index_pattern.test.tsx (89%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_index_pattern.tsx (57%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_index_pattern_management.test.tsx (90%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_index_pattern_management.tsx (95%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_sidebar.scss (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_sidebar.test.tsx (71%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_sidebar.tsx (97%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_sidebar_responsive.test.tsx (84%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/discover_sidebar_responsive.tsx (91%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/index.ts (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/lib/field_calculator.js (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/lib/field_calculator.test.ts (94%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/lib/field_filter.test.ts (97%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/lib/field_filter.ts (96%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/lib/get_details.ts (91%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/lib/get_field_type_name.ts (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/lib/get_index_pattern_field_list.ts (95%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/lib/get_warnings.ts (92%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/lib/group_fields.test.ts (98%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/lib/group_fields.tsx (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/lib/visualize_trigger_utils.ts (95%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/string_progress_bar.tsx (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/sidebar/types.ts (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/skip_bottom_button/index.ts (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/skip_bottom_button/skip_bottom_button.test.tsx (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/skip_bottom_button/skip_bottom_button.tsx (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/timechart_header/index.ts (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/timechart_header/timechart_header.scss (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/timechart_header/timechart_header.test.tsx (97%) rename src/plugins/discover/public/application/{ => apps/main}/components/timechart_header/timechart_header.tsx (95%) rename src/plugins/discover/public/application/{ => apps/main}/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap (100%) create mode 100644 src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx rename src/plugins/discover/public/application/{components => apps/main/components/top_nav}/discover_topnav.tsx (60%) rename src/plugins/discover/public/application/{ => apps/main}/components/top_nav/get_top_nav_links.test.ts (87%) rename src/plugins/discover/public/application/{ => apps/main}/components/top_nav/get_top_nav_links.ts (90%) rename src/plugins/discover/public/application/{ => apps/main}/components/top_nav/on_save_search.test.tsx (63%) rename src/plugins/discover/public/application/{ => apps/main}/components/top_nav/on_save_search.tsx (89%) rename src/plugins/discover/public/application/{ => apps/main}/components/top_nav/open_options_popover.scss (100%) rename src/plugins/discover/public/application/{ => apps/main}/components/top_nav/open_options_popover.test.tsx (93%) rename src/plugins/discover/public/application/{ => apps/main}/components/top_nav/open_options_popover.tsx (97%) rename src/plugins/discover/public/application/{ => apps/main}/components/top_nav/open_search_panel.test.tsx (93%) rename src/plugins/discover/public/application/{ => apps/main}/components/top_nav/open_search_panel.tsx (95%) rename src/plugins/discover/public/application/{ => apps/main}/components/top_nav/show_open_search_panel.tsx (100%) rename src/plugins/discover/public/application/{angular/directives => apps/main/components/uninitialized}/uninitialized.tsx (100%) create mode 100644 src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx create mode 100644 src/plugins/discover/public/application/apps/main/discover_main_app.tsx rename src/plugins/discover/public/application/{components/constants.ts => apps/main/index.ts} (72%) rename src/plugins/discover/public/application/{angular => apps/main/services}/discover_search_session.test.ts (83%) rename src/plugins/discover/public/application/{angular => apps/main/services}/discover_search_session.ts (93%) rename src/plugins/discover/public/application/{angular => apps/main/services}/discover_state.test.ts (97%) rename src/plugins/discover/public/application/{angular => apps/main/services}/discover_state.ts (95%) create mode 100644 src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts create mode 100644 src/plugins/discover/public/application/apps/main/services/use_discover_state.ts create mode 100644 src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts create mode 100644 src/plugins/discover/public/application/apps/main/services/use_saved_search.ts create mode 100644 src/plugins/discover/public/application/apps/main/services/use_search_session.test.ts create mode 100644 src/plugins/discover/public/application/apps/main/services/use_search_session.ts create mode 100644 src/plugins/discover/public/application/apps/main/services/use_url.test.ts create mode 100644 src/plugins/discover/public/application/apps/main/services/use_url.ts rename src/plugins/discover/public/application/{helpers => apps/main/utils}/calc_field_counts.test.ts (83%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/calc_field_counts.ts (84%) create mode 100644 src/plugins/discover/public/application/apps/main/utils/get_chart_agg_config.test.ts rename src/plugins/discover/public/application/{components/histogram/apply_aggs_to_search_source.ts => apps/main/utils/get_chart_agg_configs.ts} (60%) create mode 100644 src/plugins/discover/public/application/apps/main/utils/get_dimensions.test.ts rename src/plugins/discover/public/application/{components/histogram => apps/main/utils}/get_dimensions.ts (72%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/get_result_state.test.ts (71%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/get_result_state.ts (73%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/get_sharing_data.test.ts (96%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/get_sharing_data.ts (87%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/get_state_defaults.test.ts (74%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/get_state_defaults.ts (71%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/get_switch_index_pattern_app_state.test.ts (98%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/get_switch_index_pattern_app_state.ts (90%) rename src/plugins/discover/public/application/{components/histogram => apps/main/utils}/index.ts (84%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/nested_fields.ts (100%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/persist_saved_search.ts (75%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/resolve_index_pattern.test.ts (89%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/resolve_index_pattern.ts (95%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/update_search_source.test.ts (66%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/update_search_source.ts (50%) create mode 100644 src/plugins/discover/public/application/apps/main/utils/use_singleton.ts rename src/plugins/discover/public/application/{helpers => apps/main/utils}/validate_time_range.test.ts (94%) rename src/plugins/discover/public/application/{helpers => apps/main/utils}/validate_time_range.ts (100%) delete mode 100644 src/plugins/discover/public/application/components/create_discover_directive.ts delete mode 100644 src/plugins/discover/public/application/components/discover.test.tsx delete mode 100644 src/plugins/discover/public/application/components/discover_topnav.test.tsx delete mode 100644 src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts delete mode 100644 src/plugins/discover/public/application/components/histogram/get_dimensions.test.ts delete mode 100644 src/plugins/discover/public/application/components/types.ts create mode 100644 src/plugins/discover/public/application/types.ts diff --git a/src/plugins/data/common/search/search_source/mocks.ts b/src/plugins/data/common/search/search_source/mocks.ts index 64ed82f36e81bc..c8769141a71d68 100644 --- a/src/plugins/data/common/search/search_source/mocks.ts +++ b/src/plugins/data/common/search/search_source/mocks.ts @@ -45,6 +45,10 @@ export const searchSourceCommonMock: jest.Mocked = { export const createSearchSourceMock = (fields?: SearchSourceFields) => new SearchSource(fields, { getConfig: uiSettingsServiceMock.createStartContract().get, - search: jest.fn(), + search: jest + .fn() + .mockReturnValue( + of({ rawResponse: { hits: { hits: [], total: 0 } }, isPartial: false, isRunning: false }) + ), onResponse: jest.fn().mockImplementation((req, res) => res), }); diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 7633382bb87631..19e80c7a487dcb 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -699,7 +699,7 @@ export class SearchSource { searchRequest.body = searchRequest.body || {}; const { body, index, query, filters, highlightAll } = searchRequest; searchRequest.indexType = this.getIndexType(index); - const metaFields = getConfig(UI_SETTINGS.META_FIELDS); + const metaFields = getConfig(UI_SETTINGS.META_FIELDS) ?? []; // get some special field types from the index pattern const { docvalueFields, scriptFields, storedFields, runtimeFields } = index diff --git a/src/plugins/data/public/query/query_string/query_string_manager.mock.ts b/src/plugins/data/public/query/query_string/query_string_manager.mock.ts index 0038ba0e87e06d..976d3ce13e7de7 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.mock.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.mock.ts @@ -7,12 +7,13 @@ */ import { QueryStringContract } from '.'; +import { Observable } from 'rxjs'; const createSetupContractMock = () => { const queryStringManagerMock: jest.Mocked = { getQuery: jest.fn(), setQuery: jest.fn(), - getUpdates$: jest.fn(), + getUpdates$: jest.fn(() => new Observable()), getDefaultQuery: jest.fn(), formatQuery: jest.fn(), clearQuery: jest.fn(), diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts index c22f62f45a709f..f1f02a010e98c6 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts @@ -21,7 +21,7 @@ const createSetupContractMock = () => { getTimeUpdate$: jest.fn(), getRefreshIntervalUpdate$: jest.fn(), getAutoRefreshFetch$: jest.fn(() => new Observable<() => void>()), - getFetch$: jest.fn(), + getFetch$: jest.fn(() => new Observable<() => void>()), getTime: jest.fn(), setTime: jest.fn(), setRefreshInterval: jest.fn(), diff --git a/src/plugins/discover/public/__mocks__/index_pattern.ts b/src/plugins/discover/public/__mocks__/index_pattern.ts index 4fbeac80b09723..67aac96889424f 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern.ts @@ -65,6 +65,10 @@ fields.getByName = (name: string) => { return fields.find((field) => field.name === name); }; +fields.getAll = () => { + return fields; +}; + const indexPattern = ({ id: 'the-index-pattern-id', title: 'the-index-pattern-title', diff --git a/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts b/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts index ad84518af9de3a..f1c3c537dd5e18 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts @@ -56,6 +56,9 @@ const fields = [ fields.getByName = (name: string) => { return fields.find((field) => field.name === name); }; +fields.getAll = () => { + return fields; +}; const indexPattern = ({ id: 'index-pattern-with-timefield-id', @@ -72,5 +75,8 @@ const indexPattern = ({ indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); indexPattern.isTimeBased = () => !!indexPattern.timeFieldName; +indexPattern.formatField = (hit: Record, fieldName: string) => { + return fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName]; +}; export const indexPatternWithTimefieldMock = indexPattern; diff --git a/src/plugins/discover/public/__mocks__/saved_search.ts b/src/plugins/discover/public/__mocks__/saved_search.ts index 8f07391571987c..6f48c56b0810e4 100644 --- a/src/plugins/discover/public/__mocks__/saved_search.ts +++ b/src/plugins/discover/public/__mocks__/saved_search.ts @@ -7,6 +7,8 @@ */ import { SavedSearch } from '../saved_searches'; +import { createSearchSourceMock } from '../../../data/public/mocks'; +import { indexPatternMock } from './index_pattern'; export const savedSearchMock = ({ id: 'the-saved-search-id', @@ -27,4 +29,5 @@ export const savedSearchMock = ({ ], migrationVersion: { search: '7.5.0' }, error: undefined, + searchSource: createSearchSourceMock({ index: indexPatternMock }), } as unknown) as SavedSearch; diff --git a/src/plugins/discover/public/__mocks__/search_session.ts b/src/plugins/discover/public/__mocks__/search_session.ts new file mode 100644 index 00000000000000..a9037217a303ae --- /dev/null +++ b/src/plugins/discover/public/__mocks__/search_session.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { createMemoryHistory } from 'history'; +import { dataPluginMock } from '../../../data/public/mocks'; +import { DataPublicPluginStart } from '../../../data/public'; +import { DiscoverSearchSessionManager } from '../application/apps/main/services/discover_search_session'; + +export function createSearchSessionMock() { + const history = createMemoryHistory(); + const session = dataPluginMock.createStartContract().search.session as jest.Mocked< + DataPublicPluginStart['search']['session'] + >; + const searchSessionManager = new DiscoverSearchSessionManager({ + history, + session, + }); + return { + history, + session, + searchSessionManager, + }; +} diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts new file mode 100644 index 00000000000000..8a047625303d2a --- /dev/null +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { DiscoverServices } from '../build_services'; +import { dataPluginMock } from '../../../data/public/mocks'; +import { chromeServiceMock, coreMock, docLinksServiceMock } from '../../../../core/public/mocks'; +import { DEFAULT_COLUMNS_SETTING } from '../../common'; +import { savedSearchMock } from './saved_search'; +import { UI_SETTINGS } from '../../../data/common'; +import { TopNavMenu } from '../../../navigation/public'; +const dataPlugin = dataPluginMock.createStartContract(); + +export const discoverServiceMock = ({ + core: coreMock.createStart(), + chrome: chromeServiceMock.createStartContract(), + history: () => ({ + location: { + search: '', + }, + }), + data: dataPlugin, + docLinks: docLinksServiceMock.createStartContract(), + capabilities: { + visualize: { + show: true, + }, + discover: { + save: false, + }, + advancedSettings: { + save: true, + }, + }, + filterManager: dataPlugin.query.filterManager, + uiSettings: { + get: (key: string) => { + if (key === 'fields:popularLimit') { + return 5; + } else if (key === DEFAULT_COLUMNS_SETTING) { + return []; + } else if (key === UI_SETTINGS.META_FIELDS) { + return []; + } + }, + }, + indexPatternFieldEditor: { + openEditor: jest.fn(), + userPermissions: { + editIndexPattern: jest.fn(), + }, + }, + getSavedSearchById: (id?: string) => Promise.resolve(savedSearchMock), + navigation: { + ui: { TopNavMenu }, + }, + metadata: { + branch: 'test', + }, +} as unknown) as DiscoverServices; diff --git a/src/plugins/discover/public/application/angular/create_discover_directive.ts b/src/plugins/discover/public/application/angular/create_discover_directive.ts new file mode 100644 index 00000000000000..ae0d978322bcd0 --- /dev/null +++ b/src/plugins/discover/public/application/angular/create_discover_directive.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { DiscoverMainApp } from '../apps/main'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function createDiscoverDirective(reactDirective: any) { + return reactDirective(DiscoverMainApp, [ + ['indexPattern', { watchDepth: 'reference' }], + ['opts', { watchDepth: 'reference' }], + ]); +} diff --git a/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx b/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx similarity index 95% rename from src/plugins/discover/public/application/components/create_discover_grid_directive.tsx rename to src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx index 39a9df89260238..1fc8edcb4d0659 100644 --- a/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx +++ b/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx @@ -7,7 +7,7 @@ */ import React, { useState } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; -import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid'; +import { DiscoverGrid, DiscoverGridProps } from '../components/discover_grid/discover_grid'; import { getServices } from '../../kibana_services'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; diff --git a/src/plugins/discover/public/application/angular/directives/debounce/debounce.js b/src/plugins/discover/public/application/angular/directives/debounce/debounce.js deleted file mode 100644 index bca1a9c785fd68..00000000000000 --- a/src/plugins/discover/public/application/angular/directives/debounce/debounce.js +++ /dev/null @@ -1,57 +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 _ from 'lodash'; -// Debounce service, angularized version of lodash debounce -// borrowed heavily from https://github.com/shahata/angular-debounce - -export function createDebounceProviderTimeout($timeout) { - return function (func, wait, options) { - let timeout; - let args; - let self; - let result; - options = _.defaults(options || {}, { - leading: false, - trailing: true, - invokeApply: true, - }); - - function debounce() { - self = this; - args = arguments; - - const later = function () { - timeout = null; - if (!options.leading || options.trailing) { - result = func.apply(self, args); - } - }; - - const callNow = options.leading && !timeout; - - if (timeout) { - $timeout.cancel(timeout); - } - timeout = $timeout(later, wait, options.invokeApply); - - if (callNow) { - result = func.apply(self, args); - } - - return result; - } - - debounce.cancel = function () { - $timeout.cancel(timeout); - timeout = null; - }; - - return debounce; - }; -} diff --git a/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts b/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts deleted file mode 100644 index 0c033fd066eabe..00000000000000 --- a/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts +++ /dev/null @@ -1,127 +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 sinon, { SinonSpy } from 'sinon'; -import angular, { auto, ITimeoutService } from 'angular'; -import 'angular-mocks'; -import 'angular-sanitize'; -import 'angular-route'; - -// @ts-expect-error -import { createDebounceProviderTimeout } from './debounce'; -import { coreMock } from '../../../../../../../core/public/mocks'; -import { initializeInnerAngularModule } from '../../../../get_inner_angular'; -import { navigationPluginMock } from '../../../../../../navigation/public/mocks'; -import { dataPluginMock } from '../../../../../../data/public/mocks'; -import { initAngularBootstrap } from '../../../../../../kibana_legacy/public'; - -describe('debounce service', function () { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let debounce: (fn: () => void, timeout: number, options?: any) => any; - let $timeout: ITimeoutService; - let spy: SinonSpy; - - beforeEach(() => { - spy = sinon.spy(); - - initAngularBootstrap(); - - initializeInnerAngularModule( - 'app/discover', - coreMock.createStart(), - navigationPluginMock.createStartContract(), - dataPluginMock.createStartContract() - ); - - angular.mock.module('app/discover'); - - angular.mock.inject(($injector: auto.IInjectorService, _$timeout_: ITimeoutService) => { - $timeout = _$timeout_; - - debounce = createDebounceProviderTimeout($timeout); - }); - }); - - it('should have a cancel method', function () { - const bouncer = debounce(() => {}, 100); - - expect(bouncer).toHaveProperty('cancel'); - }); - - describe('delayed execution', function () { - const sandbox = sinon.createSandbox(); - - beforeEach(() => sandbox.useFakeTimers()); - afterEach(() => sandbox.restore()); - - it('should delay execution', function () { - const bouncer = debounce(spy, 100); - - bouncer(); - sinon.assert.notCalled(spy); - $timeout.flush(); - sinon.assert.calledOnce(spy); - - spy.resetHistory(); - }); - - it('should fire on leading edge', function () { - const bouncer = debounce(spy, 100, { leading: true }); - - bouncer(); - sinon.assert.calledOnce(spy); - $timeout.flush(); - sinon.assert.calledTwice(spy); - - spy.resetHistory(); - }); - - it('should only fire on leading edge', function () { - const bouncer = debounce(spy, 100, { leading: true, trailing: false }); - - bouncer(); - sinon.assert.calledOnce(spy); - $timeout.flush(); - sinon.assert.calledOnce(spy); - - spy.resetHistory(); - }); - - it('should reset delayed execution', function () { - const cancelSpy = sinon.spy($timeout, 'cancel'); - const bouncer = debounce(spy, 100); - - bouncer(); - sandbox.clock.tick(1); - - bouncer(); - sinon.assert.notCalled(spy); - $timeout.flush(); - sinon.assert.calledOnce(spy); - sinon.assert.calledOnce(cancelSpy); - - spy.resetHistory(); - cancelSpy.resetHistory(); - }); - }); - - describe('cancel', function () { - it('should cancel the $timeout', function () { - const cancelSpy = sinon.spy($timeout, 'cancel'); - const bouncer = debounce(spy, 100); - - bouncer(); - bouncer.cancel(); - sinon.assert.calledOnce(cancelSpy); - // throws if pending timeouts - $timeout.verifyNoPendingTasks(); - - cancelSpy.resetHistory(); - }); - }); -}); diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index c66ca19c967437..aa1344a67fbec9 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -6,47 +6,17 @@ * Side Public License, v 1. */ -import _ from 'lodash'; -import { merge, Subject, Subscription } from 'rxjs'; -import { debounceTime, tap, filter } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; -import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state'; -import { RequestAdapter } from '../../../../inspector/public'; -import { - connectToQueryState, - esFilters, - indexPatterns as indexPatternsUtils, - noSearchSessionStorageCapabilityMessage, - syncQueryStateWithUrl, -} from '../../../../data/public'; -import { getSortArray } from './doc_table'; +import { getState } from '../apps/main/services/discover_state'; import indexTemplateLegacy from './discover_legacy.html'; -import { discoverResponseHandler } from './response_handler'; import { getAngularModule, - getHeaderActionMenuMounter, getServices, getUrlTracker, redirectWhenMissing, - subscribeWithScope, - tabifyAggResponse, } from '../../kibana_services'; import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; -import { getStateDefaults } from '../helpers/get_state_defaults'; -import { getResultState } from '../helpers/get_result_state'; -import { validateTimeRange } from '../helpers/validate_time_range'; -import { addFatalError } from '../../../../kibana_legacy/public'; -import { - SAMPLE_SIZE_SETTING, - SEARCH_FIELDS_FROM_SOURCE, - SEARCH_ON_PAGE_LOAD_SETTING, -} from '../../../common'; -import { loadIndexPattern, resolveIndexPattern } from '../helpers/resolve_index_pattern'; -import { updateSearchSource } from '../helpers/update_search_source'; -import { calcFieldCounts } from '../helpers/calc_field_counts'; -import { DiscoverSearchSessionManager } from './discover_search_session'; -import { applyAggsToSearchSource, getDimensions } from '../components/histogram'; -import { fetchStatuses } from '../components/constants'; +import { loadIndexPattern, resolveIndexPattern } from '../apps/main/utils/resolve_index_pattern'; const services = getServices(); @@ -56,8 +26,6 @@ const { chrome, data, history: getHistory, - filterManager, - timefilter, toastNotifications, uiSettings: config, } = getServices(); @@ -147,390 +115,29 @@ app.directive('discoverApp', function () { }); function discoverController($route, $scope) { - const { isDefault: isDefaultType } = indexPatternsUtils; - const subscriptions = new Subscription(); - const refetch$ = new Subject(); - - let isChangingIndexPattern = false; const savedSearch = $route.current.locals.savedObjects.savedSearch; - const persistentSearchSource = savedSearch.searchSource; $scope.indexPattern = resolveIndexPattern( $route.current.locals.savedObjects.ip, - persistentSearchSource, + savedSearch.searchSource, toastNotifications ); - $scope.useNewFieldsApi = !config.get(SEARCH_FIELDS_FROM_SOURCE); - - //used for functional testing - $scope.fetchCounter = 0; - - const getTimeField = () => { - return isDefaultType($scope.indexPattern) ? $scope.indexPattern.timeFieldName : undefined; - }; const history = getHistory(); - const searchSessionManager = new DiscoverSearchSessionManager({ - history, - session: data.search.session, - }); - - const stateContainer = getState({ - getStateDefaults: () => - getStateDefaults({ - config, - data, - indexPattern: $scope.indexPattern, - savedSearch, - searchSource: persistentSearchSource, - }), - storeInSessionStorage: config.get('state:storeInSessionStorage'), - history, - toasts: core.notifications.toasts, - uiSettings: config, - }); - - const { - appStateContainer, - startSync: startStateSync, - stopSync: stopStateSync, - setAppState, - replaceUrlAppState, - kbnUrlStateStorage, - getPreviousAppState, - } = stateContainer; - - if (appStateContainer.getState().index !== $scope.indexPattern.id) { - //used index pattern is different than the given by url/state which is invalid - setAppState({ index: $scope.indexPattern.id }); - } - $scope.state = { ...appStateContainer.getState() }; - - // syncs `_g` portion of url with query services - const { stop: stopSyncingGlobalStateWithUrl } = syncQueryStateWithUrl( - data.query, - kbnUrlStateStorage - ); - - // sync initial app filters from state to filterManager - filterManager.setAppFilters(_.cloneDeep(appStateContainer.getState().filters)); - data.query.queryString.setQuery(appStateContainer.getState().query); - - const stopSyncingQueryAppStateWithStateContainer = connectToQueryState( - data.query, - appStateContainer, - { - filters: esFilters.FilterStateStore.APP_STATE, - query: true, - } - ); - const showUnmappedFields = $scope.useNewFieldsApi; - const updateSearchSourceHelper = () => { - const { indexPattern, useNewFieldsApi } = $scope; - const { columns, sort } = $scope.state; - updateSearchSource({ - persistentSearchSource, - volatileSearchSource: $scope.volatileSearchSource, - indexPattern, - services, - sort, - columns, - useNewFieldsApi, - showUnmappedFields, - }); - }; - - const appStateUnsubscribe = appStateContainer.subscribe(async (newState) => { - const { state: newStatePartial } = splitState(newState); - const { state: oldStatePartial } = splitState(getPreviousAppState()); - - if (!_.isEqual(newStatePartial, oldStatePartial)) { - $scope.$evalAsync(async () => { - // NOTE: this is also called when navigating from discover app to context app - if (newStatePartial.index && oldStatePartial.index !== newStatePartial.index) { - //in case of index pattern switch the route has currently to be reloaded, legacy - isChangingIndexPattern = true; - $route.reload(); - return; - } - - $scope.state = { ...newState }; - - // detect changes that should trigger fetching of new data - const changes = ['interval', 'sort'].filter( - (prop) => !_.isEqual(newStatePartial[prop], oldStatePartial[prop]) - ); - - if (oldStatePartial.hideChart && !newStatePartial.hideChart) { - // in case the histogram is hidden, no data is requested - // so when changing this state data needs to be fetched - changes.push(true); - } - - if (changes.length) { - refetch$.next(); - } - }); - } - }); - - // this listener is waiting for such a path http://localhost:5601/app/discover#/ - // which could be set through pressing "New" button in top nav or go to "Discover" plugin from the sidebar - // to reload the page in a right way - const unlistenHistoryBasePath = history.listen(({ pathname, search, hash }) => { - if (!search && !hash && pathname === '/') { - $route.reload(); - } - }); - - data.search.session.enableStorage( - createSearchSessionRestorationDataProvider({ - appStateContainer, - data, - getSavedSearch: () => savedSearch, - }), - { - isDisabled: () => - capabilities.discover.storeSearchSession - ? { disabled: false } - : { - disabled: true, - reasonText: noSearchSessionStorageCapabilityMessage, - }, - } - ); $scope.opts = { - // number of records to fetch, then paginate through - sampleSize: config.get(SAMPLE_SIZE_SETTING), - timefield: getTimeField(), - savedSearch: savedSearch, + savedSearch, + history, services, indexPatternList: $route.current.locals.savedObjects.ip.list, - config: config, - setHeaderActionMenu: getHeaderActionMenuMounter(), - filterManager, - setAppState, - data, - stateContainer, - searchSessionManager, - refetch$, - }; - - const inspectorAdapters = ($scope.opts.inspectorAdapters = { - requests: new RequestAdapter(), - }); - - const shouldSearchOnPageLoad = () => { - // A saved search is created on every page load, so we check the ID to see if we're loading a - // previously saved search or if it is just transient - return ( - config.get(SEARCH_ON_PAGE_LOAD_SETTING) || - savedSearch.id !== undefined || - timefilter.getRefreshInterval().pause === false || - searchSessionManager.hasSearchSessionIdInURL() - ); + navigateTo: (path) => { + $scope.$evalAsync(() => { + history.push(path); + }); + }, }; - $scope.fetchStatus = fetchStatuses.UNINITIALIZED; - $scope.resultState = shouldSearchOnPageLoad() ? 'loading' : 'uninitialized'; - - let abortController; $scope.$on('$destroy', () => { - if (abortController) abortController.abort(); savedSearch.destroy(); - subscriptions.unsubscribe(); - if (!isChangingIndexPattern) { - // HACK: - // do not clear session when changing index pattern due to how state management around it is setup - // it will be cleared by searchSessionManager on controller reload instead - data.search.session.clear(); - } - appStateUnsubscribe(); - stopStateSync(); - stopSyncingGlobalStateWithUrl(); - stopSyncingQueryAppStateWithStateContainer(); - unlistenHistoryBasePath(); - }); - - $scope.opts.getFieldCounts = async () => { - // the field counts aren't set until we have the data back, - // so we wait for the fetch to be done before proceeding - if ($scope.fetchStatus === fetchStatuses.COMPLETE) { - return $scope.fieldCounts; - } - - return await new Promise((resolve) => { - const unwatch = $scope.$watch('fetchStatus', (newValue) => { - if (newValue === fetchStatuses.COMPLETE) { - unwatch(); - resolve($scope.fieldCounts); - } - }); - }); - }; - $scope.opts.navigateTo = (path) => { - $scope.$evalAsync(() => { - history.push(path); - }); - }; - - persistentSearchSource.setField('index', $scope.indexPattern); - - // searchSource which applies time range - const volatileSearchSource = savedSearch.searchSource.create(); - - if (isDefaultType($scope.indexPattern)) { - volatileSearchSource.setField('filter', () => { - return timefilter.createFilter($scope.indexPattern); - }); - } - - volatileSearchSource.setParent(persistentSearchSource); - $scope.volatileSearchSource = volatileSearchSource; - $scope.state.index = $scope.indexPattern.id; - $scope.state.sort = getSortArray($scope.state.sort, $scope.indexPattern); - - $scope.opts.fetch = $scope.fetch = async function () { - $scope.fetchCounter++; - $scope.fetchError = undefined; - if (!validateTimeRange(timefilter.getTime(), toastNotifications)) { - $scope.resultState = 'none'; - } - - // Abort any in-progress requests before fetching again - if (abortController) abortController.abort(); - abortController = new AbortController(); - - const searchSessionId = searchSessionManager.getNextSearchSessionId(); - updateSearchSourceHelper(); - - $scope.opts.chartAggConfigs = applyAggsToSearchSource( - getTimeField() && !$scope.state.hideChart, - volatileSearchSource, - $scope.state.interval, - $scope.indexPattern, - data - ); - - $scope.fetchStatus = fetchStatuses.LOADING; - $scope.resultState = getResultState($scope.fetchStatus, $scope.rows); - - inspectorAdapters.requests.reset(); - return $scope.volatileSearchSource - .fetch$({ - abortSignal: abortController.signal, - sessionId: searchSessionId, - inspector: { - adapter: inspectorAdapters.requests, - title: i18n.translate('discover.inspectorRequestDataTitle', { - defaultMessage: 'data', - }), - description: i18n.translate('discover.inspectorRequestDescription', { - defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.', - }), - }, - }) - .toPromise() - .then(({ rawResponse }) => onResults(rawResponse)) - .catch((error) => { - // If the request was aborted then no need to surface this error in the UI - if (error instanceof Error && error.name === 'AbortError') return; - - $scope.fetchStatus = fetchStatuses.NO_RESULTS; - $scope.fetchError = error; - data.search.showError(error); - }) - .finally(() => { - $scope.resultState = getResultState($scope.fetchStatus, $scope.rows); - $scope.$apply(); - }); - }; - - function onResults(resp) { - if (getTimeField() && !$scope.state.hideChart) { - const tabifiedData = tabifyAggResponse($scope.opts.chartAggConfigs, resp); - $scope.volatileSearchSource.rawResponse = resp; - const dimensions = getDimensions($scope.opts.chartAggConfigs, data); - if (dimensions) { - $scope.histogramData = discoverResponseHandler(tabifiedData, dimensions); - } - } - - $scope.hits = resp.hits.total; - $scope.rows = resp.hits.hits; - - $scope.fieldCounts = calcFieldCounts( - $scope.fieldCounts || {}, - resp.hits.hits, - $scope.indexPattern - ); - $scope.fetchStatus = fetchStatuses.COMPLETE; - } - - $scope.refreshAppState = async () => { - $scope.hits = []; - $scope.rows = []; - $scope.fieldCounts = {}; - await refetch$.next(); - }; - - $scope.resetQuery = function () { - history.push( - $route.current.params.id ? `/view/${encodeURIComponent($route.current.params.id)}` : '/' - ); - $route.reload(); - }; - - $scope.newQuery = function () { - history.push('/'); - }; - - $scope.unmappedFieldsConfig = { - showUnmappedFields, - }; - - // handler emitted by `timefilter.getAutoRefreshFetch$()` - // to notify when data completed loading and to start a new autorefresh loop - let autoRefreshDoneCb; - const fetch$ = merge( - refetch$, - filterManager.getFetches$(), - timefilter.getFetch$(), - timefilter.getAutoRefreshFetch$().pipe( - tap((done) => { - autoRefreshDoneCb = done; - }), - filter(() => $scope.fetchStatus !== fetchStatuses.LOADING) - ), - data.query.queryString.getUpdates$(), - searchSessionManager.newSearchSessionIdFromURL$ - ).pipe(debounceTime(100)); - - subscriptions.add( - subscribeWithScope( - $scope, - fetch$, - { - next: async () => { - try { - await $scope.fetch(); - } finally { - // if there is a saved `autoRefreshDoneCb`, notify auto refresh service that - // the last fetch is completed so it starts the next auto refresh loop if needed - autoRefreshDoneCb?.(); - autoRefreshDoneCb = undefined; - } - }, - }, - (error) => addFatalError(core.fatalErrors, error) - ) - ); - - // Propagate current app state to url, then start syncing and fetching - replaceUrlAppState().then(() => { - startStateSync(); - if (shouldSearchOnPageLoad()) { - refetch$.next(); - } + data.search.session.clear(); }); } diff --git a/src/plugins/discover/public/application/angular/discover_datagrid.html b/src/plugins/discover/public/application/angular/discover_datagrid.html index 42218568a838db..fa1181fd0d6cd6 100644 --- a/src/plugins/discover/public/application/angular/discover_datagrid.html +++ b/src/plugins/discover/public/application/angular/discover_datagrid.html @@ -1,31 +1,11 @@ diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index fa3656d1529d28..025a4490faf1f2 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -1,22 +1,7 @@ + > - \ No newline at end of file + diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.test.ts b/src/plugins/discover/public/application/angular/doc_table/actions/columns.test.ts index 43044d08f6ac06..e1aa96f4625de5 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.test.ts +++ b/src/plugins/discover/public/application/angular/doc_table/actions/columns.test.ts @@ -11,7 +11,7 @@ import { configMock } from '../../../../__mocks__/config'; import { indexPatternMock } from '../../../../__mocks__/index_pattern'; import { indexPatternsMock } from '../../../../__mocks__/index_patterns'; import { Capabilities } from '../../../../../../../core/types'; -import { AppState } from '../../discover_state'; +import { AppState } from '../../../apps/main/services/discover_state'; function getStateColumnAction(state: {}, setAppState: (state: Partial) => void) { return getStateColumnActions({ diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts index 0907844aa1c54d..9ef5d45947afb0 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts @@ -11,7 +11,7 @@ import { IndexPattern, IndexPatternsContract } from '../../../../kibana_services import { AppState as DiscoverState, GetStateReturn as DiscoverGetStateReturn, -} from '../../discover_state'; +} from '../../../apps/main/services/discover_state'; import { AppState as ContextState, GetStateReturn as ContextGetStateReturn, diff --git a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js index 270f366bddbf24..a087ac86971838 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js +++ b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js @@ -19,7 +19,7 @@ import { setScopedHistory, setServices, setDocViewsRegistry } from '../../../../ import { coreMock } from '../../../../../../../core/public/mocks'; import { dataPluginMock } from '../../../../../../data/public/mocks'; import { navigationPluginMock } from '../../../../../../navigation/public/mocks'; -import { getInnerAngularModule } from '../../../../get_inner_angular'; +import { getInnerAngularModule } from '../../get_inner_angular'; import { createBrowserHistory } from 'history'; const fakeRowVals = { diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx index b8e8bf1fe7d6ce..1135d79b6b69ee 100644 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx @@ -13,8 +13,8 @@ import type { estypes } from '@elastic/elasticsearch'; import { EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { getServices, IIndexPattern } from '../../../kibana_services'; -import { IndexPatternField } from '../../../../../data/common/index_patterns'; -import { SkipBottomButton } from '../../components/skip_bottom_button'; +import { IndexPatternField } from '../../../../../data/common'; +import { SkipBottomButton } from '../../apps/main/components/skip_bottom_button'; export interface DocTableLegacyProps { columns: string[]; @@ -23,7 +23,7 @@ export interface DocTableLegacyProps { onFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; rows: estypes.Hit[]; indexPattern: IIndexPattern; - minimumVisibleRows: number; + minimumVisibleRows?: number; onAddColumn?: (column: string) => void; onBackToTop: () => void; onSort?: (sort: string[][]) => void; @@ -119,11 +119,9 @@ export function DocTableLegacy(renderProps: DocTableLegacyProps) { }, [setMinimumVisibleRows, renderProps.rows]); useEffect(() => { - if (minimumVisibleRows > 50) { - setMinimumVisibleRows(50); - } + setMinimumVisibleRows(50); setRows(renderProps.rows); - }, [renderProps.rows, minimumVisibleRows, setMinimumVisibleRows]); + }, [renderProps.rows, setMinimumVisibleRows]); useEffect(() => { if (ref && ref.current && !scope.current) { @@ -133,7 +131,7 @@ export function DocTableLegacy(renderProps: DocTableLegacyProps) { }); } else if (scope && scope.current) { scope.current.renderProps = { ...renderProps, rows, minimumVisibleRows }; - scope.current.$apply(); + scope.current.$applyAsync(); } }, [renderProps, minimumVisibleRows, rows]); diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js b/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js index 1765bae07eed72..1db35ddf18089e 100644 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js +++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js @@ -18,7 +18,7 @@ import { coreMock } from '../../../../../../core/public/mocks'; import { dataPluginMock } from '../../../../../data/public/mocks'; import { navigationPluginMock } from '../../../../../navigation/public/mocks'; import { setScopedHistory, setServices } from '../../../kibana_services'; -import { getInnerAngularModule } from '../../../get_inner_angular'; +import { getInnerAngularModule } from '../get_inner_angular'; let $parentScope; diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/application/angular/get_inner_angular.ts similarity index 78% rename from src/plugins/discover/public/get_inner_angular.ts rename to src/plugins/discover/public/application/angular/get_inner_angular.ts index c36312a87ce9ad..26d64d5adc8a33 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/application/angular/get_inner_angular.ts @@ -9,29 +9,29 @@ // inner angular imports // these are necessary to bootstrap the local angular. // They can stay even after NP cutover -import './application/index.scss'; +import '../index.scss'; import angular from 'angular'; // required for `ngSanitize` angular module import 'angular-sanitize'; import { EuiIcon } from '@elastic/eui'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, PluginInitializerContext } from 'kibana/public'; -import { DataPublicPluginStart } from '../../data/public'; -import { Storage } from '../../kibana_utils/public'; -import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public'; -import { createDocTableDirective } from './application/angular/doc_table'; -import { createTableHeaderDirective } from './application/angular/doc_table/components/table_header'; +import { DataPublicPluginStart } from '../../../../data/public'; +import { Storage } from '../../../../kibana_utils/public'; +import { NavigationPublicPluginStart as NavigationStart } from '../../../../navigation/public'; +import { createDocTableDirective } from './doc_table'; +import { createTableHeaderDirective } from './doc_table/components/table_header'; import { createToolBarPagerButtonsDirective, createToolBarPagerTextDirective, -} from './application/angular/doc_table/components/pager'; -import { createContextAppLegacy } from './application/components/context_app/context_app_legacy_directive'; -import { createTableRowDirective } from './application/angular/doc_table/components/table_row'; -import { createPagerFactory } from './application/angular/doc_table/lib/pager/pager_factory'; -import { createInfiniteScrollDirective } from './application/angular/doc_table/infinite_scroll'; -import { createDocViewerDirective } from './application/angular/doc_viewer'; -import { createDiscoverGridDirective } from './application/components/create_discover_grid_directive'; -import { createRenderCompleteDirective } from './application/angular/directives/render_complete'; +} from './doc_table/components/pager'; +import { createContextAppLegacy } from '../components/context_app/context_app_legacy_directive'; +import { createTableRowDirective } from './doc_table/components/table_row'; +import { createPagerFactory } from './doc_table/lib/pager/pager_factory'; +import { createInfiniteScrollDirective } from './doc_table/infinite_scroll'; +import { createDocViewerDirective } from './doc_viewer'; +import { createDiscoverGridDirective } from './create_discover_grid_directive'; +import { createRenderCompleteDirective } from './directives/render_complete'; import { initAngularBootstrap, configureAppAngularModule, @@ -39,10 +39,10 @@ import { PromiseServiceCreator, registerListenEventListener, watchMultiDecorator, -} from '../../kibana_legacy/public'; -import { DiscoverStartPlugins } from './plugin'; -import { getScopedHistory } from './kibana_services'; -import { createDiscoverDirective } from './application/components/create_discover_directive'; +} from '../../../../kibana_legacy/public'; +import { DiscoverStartPlugins } from '../../plugin'; +import { getScopedHistory } from '../../kibana_services'; +import { createDiscoverDirective } from './create_discover_directive'; /** * returns the main inner angular module, it contains all the parts of Angular Discover diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover/public/application/angular/helpers/index.ts index 1dd194436cdfb0..3d2c0b1c63b332 100644 --- a/src/plugins/discover/public/application/angular/helpers/index.ts +++ b/src/plugins/discover/public/application/angular/helpers/index.ts @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -export { buildPointSeriesData } from './point_series'; export { formatRow, formatTopLevelObject } from './row_formatter'; export { handleSourceColumnState } from './state_helpers'; diff --git a/src/plugins/discover/public/application/angular/index.ts b/src/plugins/discover/public/application/angular/index.ts index b45357439a1eeb..e75add7910b745 100644 --- a/src/plugins/discover/public/application/angular/index.ts +++ b/src/plugins/discover/public/application/angular/index.ts @@ -16,4 +16,3 @@ import './doc'; import './context'; import './doc_viewer'; import './redirect'; -import './directives'; diff --git a/src/plugins/discover/public/application/angular/response_handler.js b/src/plugins/discover/public/application/angular/response_handler.js deleted file mode 100644 index 301fd98f1e34c2..00000000000000 --- a/src/plugins/discover/public/application/angular/response_handler.js +++ /dev/null @@ -1,109 +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 { getServices } from '../../kibana_services'; -import { buildPointSeriesData } from './helpers'; - -function tableResponseHandler(table, dimensions) { - const converted = { tables: [] }; - const split = dimensions.splitColumn || dimensions.splitRow; - - if (split) { - converted.direction = dimensions.splitRow ? 'row' : 'column'; - const splitColumnIndex = split[0].accessor; - const splitColumnFormatter = getServices().data.fieldFormats.deserialize(split[0].format); - const splitColumn = table.columns[splitColumnIndex]; - const splitMap = {}; - let splitIndex = 0; - - table.rows.forEach((row, rowIndex) => { - const splitValue = row[splitColumn.id]; - - if (!splitMap.hasOwnProperty(splitValue)) { - splitMap[splitValue] = splitIndex++; - const tableGroup = { - $parent: converted, - title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`, - name: splitColumn.name, - key: splitValue, - column: splitColumnIndex, - row: rowIndex, - table, - tables: [], - }; - tableGroup.tables.push({ - $parent: tableGroup, - columns: table.columns, - rows: [], - }); - - converted.tables.push(tableGroup); - } - - const tableIndex = splitMap[splitValue]; - converted.tables[tableIndex].tables[0].rows.push(row); - }); - } else { - converted.tables.push({ - columns: table.columns, - rows: table.rows, - }); - } - - return converted; -} - -function convertTableGroup(tableGroup, convertTable) { - const tables = tableGroup.tables; - - if (!tables.length) return; - - const firstChild = tables[0]; - if (firstChild.columns) { - const chart = convertTable(firstChild); - // if chart is within a split, assign group title to its label - if (tableGroup.$parent) { - chart.label = tableGroup.title; - } - return chart; - } - - const out = {}; - let outList; - - tables.forEach(function (table) { - if (!outList) { - const direction = tableGroup.direction === 'row' ? 'rows' : 'columns'; - outList = out[direction] = []; - } - - let output; - if ((output = convertTableGroup(table, convertTable))) { - outList.push(output); - } - }); - - return out; -} - -export const discoverResponseHandler = (response, dimensions) => { - const tableGroup = tableResponseHandler(response, dimensions); - - let converted = convertTableGroup(tableGroup, (table) => { - return buildPointSeriesData(table, dimensions); - }); - if (!converted) { - // mimic a row of tables that doesn't have any tables - // https://github.com/elastic/kibana/blob/7bfb68cd24ed42b1b257682f93c50cd8d73e2520/src/kibana/components/vislib/components/zero_injection/inject_zeros.js#L32 - converted = { rows: [] }; - } - - converted.hits = response.rows.length; - - return converted; -}; diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx new file mode 100644 index 00000000000000..210313aac53662 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx @@ -0,0 +1,145 @@ +/* + * 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, { useCallback } from 'react'; +import moment from 'moment'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; +import { IUiSettingsClient } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { HitsCounter } from '../hits_counter'; +import { DataPublicPluginStart, IndexPattern, search } from '../../../../../../../data/public'; +import { TimechartHeader } from '../timechart_header'; +import { SavedSearch } from '../../../../../saved_searches'; +import { AppState, GetStateReturn } from '../../services/discover_state'; +import { TimechartBucketInterval } from '../timechart_header/timechart_header'; +import { Chart as IChart } from './point_series'; +import { DiscoverHistogram } from './histogram'; + +const TimechartHeaderMemoized = React.memo(TimechartHeader); +const DiscoverHistogramMemoized = React.memo(DiscoverHistogram); +export function DiscoverChart({ + config, + data, + bucketInterval, + chartData, + hits, + isLegacy, + resetQuery, + savedSearch, + state, + stateContainer, + timefield, +}: { + config: IUiSettingsClient; + data: DataPublicPluginStart; + bucketInterval?: TimechartBucketInterval; + chartData?: IChart; + hits?: number; + indexPattern: IndexPattern; + isLegacy: boolean; + resetQuery: () => void; + savedSearch: SavedSearch; + state: AppState; + stateContainer: GetStateReturn; + timefield?: string; +}) { + const toggleHideChart = useCallback(() => { + stateContainer.setAppState({ hideChart: !state.hideChart }); + }, [state, stateContainer]); + + const onChangeInterval = useCallback( + (interval: string) => { + if (interval) { + stateContainer.setAppState({ interval }); + } + }, + [stateContainer] + ); + + const timefilterUpdateHandler = useCallback( + (ranges: { from: number; to: number }) => { + data.query.timefilter.timefilter.setTime({ + from: moment(ranges.from).toISOString(), + to: moment(ranges.to).toISOString(), + mode: 'absolute', + }); + }, + [data] + ); + + return ( + + + + + + + {!state.hideChart && ( + + + + )} + {timefield && ( + + { + toggleHideChart(); + }} + data-test-subj="discoverChartToggle" + > + {!state.hideChart + ? i18n.translate('discover.hideChart', { + defaultMessage: 'Hide chart', + }) + : i18n.translate('discover.showChart', { + defaultMessage: 'Show chart', + })} + + + )} + + + {!state.hideChart && chartData && ( + +
+
+ +
+
+ +
+ )} +
+ ); +} diff --git a/src/plugins/discover/public/application/angular/directives/histogram.tsx b/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx similarity index 97% rename from src/plugins/discover/public/application/angular/directives/histogram.tsx rename to src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx index 616a0692dbba42..92b212d5739b52 100644 --- a/src/plugins/discover/public/application/angular/directives/histogram.tsx +++ b/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx @@ -27,14 +27,14 @@ import { import { IUiSettingsClient } from 'kibana/public'; import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme'; import { Subscription, combineLatest } from 'rxjs'; -import { getServices } from '../../../kibana_services'; -import { Chart as IChart } from '../helpers/point_series'; +import { getServices } from '../../../../../kibana_services'; +import { Chart as IChart } from './point_series'; import { CurrentTime, Endzones, getAdjustedInterval, renderEndzoneTooltip, -} from '../../../../../charts/public'; +} from '../../../../../../../charts/public'; export interface DiscoverHistogramProps { chartData: IChart; diff --git a/src/plugins/discover/public/application/angular/directives/debounce/index.js b/src/plugins/discover/public/application/apps/main/components/chart/index.ts similarity index 85% rename from src/plugins/discover/public/application/angular/directives/debounce/index.js rename to src/plugins/discover/public/application/apps/main/components/chart/index.ts index 774876af70d4c6..d5d5a85d1d0f2c 100644 --- a/src/plugins/discover/public/application/angular/directives/debounce/index.js +++ b/src/plugins/discover/public/application/apps/main/components/chart/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { createDebounceProviderTimeout } from './debounce'; +export { DiscoverChart } from './discover_chart'; diff --git a/src/plugins/discover/public/application/angular/helpers/point_series.ts b/src/plugins/discover/public/application/apps/main/components/chart/point_series.ts similarity index 90% rename from src/plugins/discover/public/application/angular/helpers/point_series.ts rename to src/plugins/discover/public/application/apps/main/components/chart/point_series.ts index dae70d9b8e55ba..1245b712e6fd7b 100644 --- a/src/plugins/discover/public/application/angular/helpers/point_series.ts +++ b/src/plugins/discover/public/application/apps/main/components/chart/point_series.ts @@ -9,8 +9,7 @@ import { uniq } from 'lodash'; import { Duration, Moment } from 'moment'; import { Unit } from '@elastic/datemath'; - -import { SerializedFieldFormat } from '../../../../../expressions/common/types'; +import { SerializedFieldFormat } from '../../../../../../../expressions/common'; export interface Column { id: string; @@ -26,20 +25,23 @@ export interface Table { rows: Row[]; } +export interface HistogramParamsBounds { + min: Moment; + max: Moment; +} + interface HistogramParams { date: true; interval: Duration; intervalESValue: number; intervalESUnit: Unit; format: string; - bounds: { - min: Moment; - max: Moment; - }; + bounds: HistogramParamsBounds; } export interface Dimension { accessor: 0 | 1; format: SerializedFieldFormat<{ pattern: string }>; + label: string; } export interface Dimensions { @@ -68,7 +70,7 @@ export interface Chart { ordered: Ordered; } -export const buildPointSeriesData = (table: Table, dimensions: Dimensions) => { +export const buildPointSeriesData = (table: Table, dimensions: Dimensions): Chart => { const { x, y } = dimensions; const xAccessor = table.columns[x.accessor].id; const yAccessor = table.columns[y.accessor].id; diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.scss b/src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.scss similarity index 100% rename from src/plugins/discover/public/application/components/hits_counter/hits_counter.scss rename to src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.scss diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx b/src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.test.tsx similarity index 100% rename from src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx rename to src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.test.tsx diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx b/src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.tsx similarity index 94% rename from src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx rename to src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.tsx index 6ce6129f56b5e9..0850a8ff03d967 100644 --- a/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx +++ b/src/plugins/discover/public/application/apps/main/components/hits_counter/hits_counter.tsx @@ -12,13 +12,13 @@ import React from 'react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { formatNumWithCommas } from '../../helpers'; +import { formatNumWithCommas } from '../../../../helpers'; export interface HitsCounterProps { /** * the number of query hits */ - hits: number; + hits?: number; /** * displays the reset button */ @@ -30,6 +30,9 @@ export interface HitsCounterProps { } export function HitsCounter({ hits, showResetButton, onResetQuery }: HitsCounterProps) { + if (typeof hits === 'undefined') { + return null; + } return ( { + return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' }; + }; + + const indexPatternList = ([indexPattern].map((ip) => { + return { ...ip, ...{ attributes: { title: ip.title } } }; + }) as unknown) as Array>; + + const savedSearch$ = new BehaviorSubject({ + state: FetchStatus.COMPLETE, + rows: esHits, + fetchCounter: 1, + fieldCounts: {}, + hits: Number(esHits.length), + }) as SavedSearchDataSubject; + + return { + indexPattern, + indexPatternList, + navigateTo: jest.fn(), + resetQuery: jest.fn(), + savedSearch: savedSearchMock, + savedSearchData$: savedSearch$, + savedSearchRefetch$: new Subject(), + searchSessionManager: {} as DiscoverSearchSessionManager, + searchSource: searchSourceMock, + services, + state: { columns: [] }, + stateContainer: {} as GetStateReturn, + }; +} + +describe('Discover component', () => { + test('selected index pattern without time field displays no chart toggle', () => { + const component = mountWithIntl(); + expect(component.find('[data-test-subj="discoverChartToggle"]').exists()).toBeFalsy(); + }); + test('selected index pattern with time field displays chart toggle', () => { + const component = mountWithIntl( + + ); + expect(component.find('[data-test-subj="discoverChartToggle"]').exists()).toBeTruthy(); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx similarity index 52% rename from src/plugins/discover/public/application/components/discover.tsx rename to src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index f962c56cc4690c..ce987e2870466b 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -5,10 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import './discover.scss'; +import './discover_layout.scss'; import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react'; import { - EuiButtonEmpty, + EuiSpacer, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, @@ -17,99 +17,141 @@ import { EuiPage, EuiPageBody, EuiPageContent, - EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import moment from 'moment'; import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import classNames from 'classnames'; -import { HitsCounter } from './hits_counter'; -import { TimechartHeader } from './timechart_header'; -import { DiscoverHistogram, DiscoverUninitialized } from '../angular/directives'; -import { DiscoverNoResults } from './no_results'; -import { LoadingSpinner } from './loading_spinner/loading_spinner'; -import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; -import { esFilters, IndexPatternField, search } from '../../../../data/public'; -import { DiscoverSidebarResponsive } from './sidebar'; -import { DiscoverProps } from './types'; -import { SortPairArr } from '../angular/doc_table/lib/get_sort'; +import { DiscoverNoResults } from '../no_results'; +import { LoadingSpinner } from '../loading_spinner/loading_spinner'; +import { DocTableLegacy } from '../../../../angular/doc_table/create_doc_table_react'; +import { + esFilters, + IndexPatternField, + indexPatterns as indexPatternsUtils, +} from '../../../../../../../data/public'; +import { DiscoverSidebarResponsive } from '../sidebar'; +import { DiscoverLayoutProps } from './types'; +import { SortPairArr } from '../../../../angular/doc_table/lib/get_sort'; import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY, + MODIFY_COLUMNS_ON_SWITCH, + SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, -} from '../../../common'; -import { popularizeField } from '../helpers/popularize_field'; -import { DocViewFilterFn } from '../doc_views/doc_views_types'; -import { DiscoverGrid } from './discover_grid/discover_grid'; -import { DiscoverTopNav } from './discover_topnav'; -import { ElasticSearchHit } from '../doc_views/doc_views_types'; -import { setBreadcrumbsTitle } from '../helpers/breadcrumbs'; -import { addHelpMenuToAppChrome } from './help_menu/help_menu_util'; -import { InspectorSession } from '../../../../inspector/public'; -import { useDataGridColumns } from '../helpers/use_data_grid_columns'; + SORT_DEFAULT_ORDER_SETTING, +} from '../../../../../../common'; +import { popularizeField } from '../../../../helpers/popularize_field'; +import { DocViewFilterFn } from '../../../../doc_views/doc_views_types'; +import { DiscoverGrid } from '../../../../components/discover_grid/discover_grid'; +import { DiscoverTopNav } from '../top_nav/discover_topnav'; +import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; +import { DiscoverChart } from '../chart'; +import { getResultState } from '../../utils/get_result_state'; +import { InspectorSession } from '../../../../../../../inspector/public'; +import { DiscoverUninitialized } from '../uninitialized/uninitialized'; +import { SavedSearchDataMessage } from '../../services/use_saved_search'; +import { useDataGridColumns } from '../../../../helpers/use_data_grid_columns'; +import { getSwitchIndexPatternAppState } from '../../utils/get_switch_index_pattern_app_state'; +import { FetchStatus } from '../../../../types'; const DocTableLegacyMemoized = React.memo(DocTableLegacy); const SidebarMemoized = React.memo(DiscoverSidebarResponsive); const DataGridMemoized = React.memo(DiscoverGrid); const TopNavMemoized = React.memo(DiscoverTopNav); -const TimechartHeaderMemoized = React.memo(TimechartHeader); -const DiscoverHistogramMemoized = React.memo(DiscoverHistogram); +const DiscoverChartMemoized = React.memo(DiscoverChart); -export function Discover({ - fetch, - fetchCounter, - fetchError, - fieldCounts, - fetchStatus, - histogramData, - hits, +interface DiscoverLayoutFetchState extends SavedSearchDataMessage { + state: FetchStatus; + fetchCounter: number; + fieldCounts: Record; + rows: ElasticSearchHit[]; +} + +export function DiscoverLayout({ indexPattern, - minimumVisibleRows, - opts, + indexPatternList, + navigateTo, + savedSearchRefetch$, resetQuery, - resultState, - rows, + savedSearchData$, + savedSearch, + searchSessionManager, searchSource, + services, state, - unmappedFieldsConfig, - refreshAppState, -}: DiscoverProps) { + stateContainer, +}: DiscoverLayoutProps) { + const { + trackUiMetric, + capabilities, + indexPatterns, + data, + uiSettings: config, + filterManager, + } = services; + + const sampleSize = useMemo(() => config.get(SAMPLE_SIZE_SETTING), [config]); const [expandedDoc, setExpandedDoc] = useState(undefined); const [inspectorSession, setInspectorSession] = useState(undefined); const scrollableDesktop = useRef(null); const collapseIcon = useRef(null); + + const [fetchState, setFetchState] = useState({ + state: savedSearchData$.getValue().state, + fetchCounter: 0, + fieldCounts: {}, + rows: [], + }); + const { state: fetchStatus, fetchCounter, inspectorAdapters, rows } = fetchState; + + useEffect(() => { + const subscription = savedSearchData$.subscribe((next) => { + if ( + (next.state && next.state !== fetchState.state) || + (next.fetchCounter && next.fetchCounter !== fetchState.fetchCounter) || + (next.rows && next.rows !== fetchState.rows) || + (next.chartData && next.chartData !== fetchState.chartData) + ) { + setFetchState({ ...fetchState, ...next }); + } + }); + return () => { + subscription.unsubscribe(); + }; + }, [savedSearchData$, fetchState]); + const isMobile = () => { // collapse icon isn't displayed in mobile view, use it to detect which view is displayed return collapseIcon && !collapseIcon.current; }; - const toggleHideChart = useCallback(() => { - const newState = { ...state, hideChart: !state.hideChart }; - opts.stateContainer.setAppState(newState); - }, [state, opts]); - const hideChart = useMemo(() => state.hideChart, [state]); - const { savedSearch, indexPatternList, config, services, data, setAppState } = opts; - const { trackUiMetric, capabilities, indexPatterns, chrome, docLinks } = services; + const timeField = useMemo(() => { + return indexPatternsUtils.isDefault(indexPattern) ? indexPattern.timeFieldName : undefined; + }, [indexPattern]); const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const bucketInterval = useMemo(() => { - const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; - return bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) - ? bucketAggConfig.buckets?.getInterval() - : undefined; - }, [opts.chartAggConfigs]); + const isLegacy = useMemo(() => services.uiSettings.get(DOC_TABLE_LEGACY), [services]); + const useNewFieldsApi = useMemo(() => !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [ + services, + ]); + + const unmappedFieldsConfig = useMemo( + () => ({ + showUnmappedFields: useNewFieldsApi, + }), + [useNewFieldsApi] + ); + + const resultState = useMemo(() => getResultState(fetchStatus, rows!), [fetchStatus, rows]); - const contentCentered = resultState === 'uninitialized'; - const isLegacy = services.uiSettings.get(DOC_TABLE_LEGACY); - const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); const updateQuery = useCallback( (_payload, isUpdate?: boolean) => { if (isUpdate === false) { - opts.searchSessionManager.removeSearchSessionIdFromURL({ replace: false }); - opts.refetch$.next(); + searchSessionManager.removeSearchSessionIdFromURL({ replace: false }); + savedSearchRefetch$.next(); } }, - [opts] + [savedSearchRefetch$, searchSessionManager] ); const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useDataGridColumns({ @@ -117,27 +159,21 @@ export function Discover({ config, indexPattern, indexPatterns, - setAppState, + setAppState: stateContainer.setAppState, state, useNewFieldsApi, }); - useEffect(() => { - const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : ''; - chrome.docTitle.change(`Discover${pageTitleSuffix}`); - - setBreadcrumbsTitle(savedSearch, chrome); - addHelpMenuToAppChrome(chrome, docLinks); - }, [savedSearch, chrome, docLinks]); - const onOpenInspector = useCallback(() => { // prevent overlapping - setExpandedDoc(undefined); - const session = services.inspector.open(opts.inspectorAdapters, { - title: savedSearch.title, - }); - setInspectorSession(session); - }, [setExpandedDoc, opts.inspectorAdapters, savedSearch, services.inspector]); + if (inspectorAdapters) { + setExpandedDoc(undefined); + const session = services.inspector.open(inspectorAdapters, { + title: savedSearch.title, + }); + setInspectorSession(session); + } + }, [setExpandedDoc, inspectorAdapters, savedSearch, services.inspector]); useEffect(() => { return () => { @@ -150,9 +186,9 @@ export function Discover({ const onSort = useCallback( (sort: string[][]) => { - setAppState({ sort }); + stateContainer.setAppState({ sort }); }, - [setAppState] + [stateContainer] ); const onAddFilter = useCallback( @@ -160,7 +196,7 @@ export function Discover({ const fieldName = typeof field === 'string' ? field : field.name; popularizeField(indexPattern, fieldName, indexPatterns); const newFilters = esFilters.generateFilters( - opts.filterManager, + filterManager, field, values, operation, @@ -169,31 +205,13 @@ export function Discover({ if (trackUiMetric) { trackUiMetric(METRIC_TYPE.CLICK, 'filter_added'); } - return opts.filterManager.addFilters(newFilters); - }, - [opts, indexPattern, indexPatterns, trackUiMetric] - ); - - const onChangeInterval = useCallback( - (interval: string) => { - if (interval) { - setAppState({ interval }); - } - }, - [setAppState] - ); - - const timefilterUpdateHandler = useCallback( - (ranges: { from: number; to: number }) => { - data.query.timefilter.timefilter.setTime({ - from: moment(ranges.from).toISOString(), - to: moment(ranges.to).toISOString(), - mode: 'absolute', - }); + return filterManager.addFilters(newFilters); }, - [data] + [filterManager, indexPattern, indexPatterns, trackUiMetric] ); - + /** + * Legacy function, remove once legacy grid is removed + */ const onBackToTop = useCallback(() => { if (scrollableDesktop && scrollableDesktop.current) { scrollableDesktop.current.focus(); @@ -214,28 +232,69 @@ export function Discover({ width: colSettings.width, }; const newGrid = { ...grid, columns: newColumns }; - opts.setAppState({ grid: newGrid }); + stateContainer.setAppState({ grid: newGrid }); }, - [opts, state] + [stateContainer, state] ); - const onEditRuntimeField = () => { - if (refreshAppState) { - refreshAppState(); - } - }; + const onEditRuntimeField = useCallback(() => { + savedSearchRefetch$.next('reset'); + }, [savedSearchRefetch$]); + + const contentCentered = resultState === 'uninitialized'; + const showTimeCol = useMemo( + () => !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, + [config, indexPattern.timeFieldName] + ); + + const onChangeIndexPattern = useCallback( + async (id: string) => { + const nextIndexPattern = await indexPatterns.get(id); + if (nextIndexPattern && indexPattern) { + /** + * Without resetting the fetch state, e.g. a time column would be displayed when switching + * from a index pattern without to a index pattern with time filter for a brief moment + * That's because appState is updated before savedSearchData$ + * The following line of code catches this, but should be improved + */ + savedSearchData$.next({ rows: [], state: FetchStatus.LOADING, fieldCounts: {} }); + + const nextAppState = getSwitchIndexPatternAppState( + indexPattern, + nextIndexPattern, + state.columns || [], + (state.sort || []) as SortPairArr[], + config.get(MODIFY_COLUMNS_ON_SWITCH), + config.get(SORT_DEFAULT_ORDER_SETTING) + ); + stateContainer.setAppState(nextAppState); + } + }, + [ + config, + indexPattern, + indexPatterns, + savedSearchData$, + state.columns, + state.sort, + stateContainer, + ] + ); return (

@@ -244,18 +303,16 @@ export function Discover({ {resultState === 'none' && ( )} - {resultState === 'uninitialized' && } + {resultState === 'uninitialized' && ( + savedSearchRefetch$.next()} /> + )} {resultState === 'loading' && } {resultState === 'ready' && ( - - - - 0 ? hits : 0} - showResetButton={!!(savedSearch && savedSearch.id)} - onResetQuery={resetQuery} - /> - - {!hideChart && ( - - - - )} - {opts.timefield && ( - - { - toggleHideChart(); - }} - data-test-subj="discoverChartToggle" - > - {!hideChart - ? i18n.translate('discover.hideChart', { - defaultMessage: 'Hide chart', - }) - : i18n.translate('discover.showChart', { - defaultMessage: 'Show chart', - })} - - - )} - + + - {!hideChart && opts.timefield && ( - -
- {opts.chartAggConfigs && histogramData && rows.length !== 0 && ( -
- -
- )} -
- -
- )} - @@ -403,18 +405,17 @@ export function Discover({ )} @@ -428,18 +429,15 @@ export function Discover({ isLoading={fetchStatus === 'loading'} rows={rows} sort={(state.sort as SortPairArr[]) || []} - sampleSize={opts.sampleSize} - searchDescription={opts.savedSearch.description} - searchTitle={opts.savedSearch.lastSavedTitle} + sampleSize={sampleSize} + searchDescription={savedSearch.description} + searchTitle={savedSearch.lastSavedTitle} setExpandedDoc={setExpandedDoc} - showTimeCol={ - !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && - !!indexPattern.timeFieldName - } + showTimeCol={showTimeCol} services={services} settings={state.grid} - onFilter={onAddFilter as DocViewFilterFn} onAddColumn={onAddColumn} + onFilter={onAddFilter as DocViewFilterFn} onRemoveColumn={onRemoveColumn} onSetColumns={onSetColumns} onSort={onSort} diff --git a/src/plugins/discover/public/application/angular/directives/index.ts b/src/plugins/discover/public/application/apps/main/components/layout/index.ts similarity index 77% rename from src/plugins/discover/public/application/angular/directives/index.ts rename to src/plugins/discover/public/application/apps/main/components/layout/index.ts index 4478d8bb319793..592e893cf8ba84 100644 --- a/src/plugins/discover/public/application/angular/directives/index.ts +++ b/src/plugins/discover/public/application/apps/main/components/layout/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export { DiscoverUninitialized } from './uninitialized'; -export { DiscoverHistogram } from './histogram'; +export { DiscoverLayout } from './discover_layout'; diff --git a/src/plugins/discover/public/application/apps/main/components/layout/types.ts b/src/plugins/discover/public/application/apps/main/components/layout/types.ts new file mode 100644 index 00000000000000..d8d6338fbe41a3 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/layout/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { + IndexPattern, + IndexPatternAttributes, + SavedObject, +} from '../../../../../../../data/common'; +import { ISearchSource } from '../../../../../../../data/public'; +import { DiscoverSearchSessionManager } from '../../services/discover_search_session'; +import { AppState, GetStateReturn } from '../../services/discover_state'; +import { SavedSearchRefetchSubject, SavedSearchDataSubject } from '../../services/use_saved_search'; +import { DiscoverServices } from '../../../../../build_services'; +import { SavedSearch } from '../../../../../saved_searches'; + +export interface DiscoverLayoutProps { + indexPattern: IndexPattern; + indexPatternList: Array>; + resetQuery: () => void; + navigateTo: (url: string) => void; + savedSearch: SavedSearch; + savedSearchData$: SavedSearchDataSubject; + savedSearchRefetch$: SavedSearchRefetchSubject; + searchSessionManager: DiscoverSearchSessionManager; + searchSource: ISearchSource; + services: DiscoverServices; + state: AppState; + stateContainer: GetStateReturn; +} diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.scss b/src/plugins/discover/public/application/apps/main/components/loading_spinner/loading_spinner.scss similarity index 100% rename from src/plugins/discover/public/application/components/loading_spinner/loading_spinner.scss rename to src/plugins/discover/public/application/apps/main/components/loading_spinner/loading_spinner.scss diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx b/src/plugins/discover/public/application/apps/main/components/loading_spinner/loading_spinner.test.tsx similarity index 100% rename from src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx rename to src/plugins/discover/public/application/apps/main/components/loading_spinner/loading_spinner.test.tsx diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx b/src/plugins/discover/public/application/apps/main/components/loading_spinner/loading_spinner.tsx similarity index 100% rename from src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx rename to src/plugins/discover/public/application/apps/main/components/loading_spinner/loading_spinner.tsx diff --git a/src/plugins/discover/public/application/components/no_results/_no_results.scss b/src/plugins/discover/public/application/apps/main/components/no_results/_no_results.scss similarity index 100% rename from src/plugins/discover/public/application/components/no_results/_no_results.scss rename to src/plugins/discover/public/application/apps/main/components/no_results/_no_results.scss diff --git a/src/plugins/discover/public/application/components/no_results/index.ts b/src/plugins/discover/public/application/apps/main/components/no_results/index.ts similarity index 100% rename from src/plugins/discover/public/application/components/no_results/index.ts rename to src/plugins/discover/public/application/apps/main/components/no_results/index.ts diff --git a/src/plugins/discover/public/application/components/no_results/no_results.test.tsx b/src/plugins/discover/public/application/apps/main/components/no_results/no_results.test.tsx similarity index 98% rename from src/plugins/discover/public/application/components/no_results/no_results.test.tsx rename to src/plugins/discover/public/application/apps/main/components/no_results/no_results.test.tsx index 9a115fb5a66b62..e7af82cf9504c7 100644 --- a/src/plugins/discover/public/application/components/no_results/no_results.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/no_results/no_results.test.tsx @@ -12,7 +12,7 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { DiscoverNoResults, DiscoverNoResultsProps } from './no_results'; -jest.mock('../../../kibana_services', () => { +jest.mock('../../../../../kibana_services', () => { return { getServices: () => ({ docLinks: { diff --git a/src/plugins/discover/public/application/components/no_results/no_results.tsx b/src/plugins/discover/public/application/apps/main/components/no_results/no_results.tsx similarity index 94% rename from src/plugins/discover/public/application/components/no_results/no_results.tsx rename to src/plugins/discover/public/application/apps/main/components/no_results/no_results.tsx index c23de80ec99373..07deb45d1fda24 100644 --- a/src/plugins/discover/public/application/components/no_results/no_results.tsx +++ b/src/plugins/discover/public/application/apps/main/components/no_results/no_results.tsx @@ -9,8 +9,8 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { getServices } from '../../../kibana_services'; -import { DataPublicPluginStart } from '../../../../../data/public'; +import { getServices } from '../../../../../kibana_services'; +import { DataPublicPluginStart } from '../../../../../../../data/public'; import { getLuceneQueryMessage, getTimeFieldMessage } from './no_results_helper'; import './_no_results.scss'; diff --git a/src/plugins/discover/public/application/components/no_results/no_results_helper.tsx b/src/plugins/discover/public/application/apps/main/components/no_results/no_results_helper.tsx similarity index 100% rename from src/plugins/discover/public/application/components/no_results/no_results_helper.tsx rename to src/plugins/discover/public/application/apps/main/components/no_results/no_results_helper.tsx diff --git a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap rename to src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap diff --git a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap rename to src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap diff --git a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap rename to src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap diff --git a/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.scss b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.scss similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/discover_field.scss rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.scss diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.test.tsx similarity index 91% rename from src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.test.tsx index 2041a8dfdc6379..82e37dd2b427cb 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.test.tsx @@ -8,15 +8,16 @@ import React from 'react'; import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-expect-error -import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; + +// @ts-expect-error +import stubbedLogstashFields from '../../../../../__fixtures__/logstash_fields'; import { DiscoverField } from './discover_field'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { IndexPatternField } from '../../../../../data/public'; -import { getStubIndexPattern } from '../../../../../data/public/test_utils'; +import { coreMock } from '../../../../../../../../core/public/mocks'; +import { IndexPatternField } from '../../../../../../../data/public'; +import { getStubIndexPattern } from '../../../../../../../data/public/test_utils'; -jest.mock('../../../kibana_services', () => ({ +jest.mock('../../../../../kibana_services', () => ({ getServices: () => ({ history: () => ({ location: { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx similarity index 99% rename from src/plugins/discover/public/application/components/sidebar/discover_field.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx index a53809cfa2c7ef..e60dabd1d8d8c7 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx @@ -23,9 +23,9 @@ import { i18n } from '@kbn/i18n'; import { UiCounterMetricType } from '@kbn/analytics'; import classNames from 'classnames'; import { DiscoverFieldDetails } from './discover_field_details'; -import { FieldIcon, FieldButton } from '../../../../../kibana_react/public'; +import { FieldIcon, FieldButton } from '../../../../../../../kibana_react/public'; import { FieldDetails } from './types'; -import { IndexPatternField, IndexPattern } from '../../../../../data/public'; +import { IndexPatternField, IndexPattern } from '../../../../../../../data/public'; import { getFieldTypeName } from './lib/get_field_type_name'; import { DiscoverFieldDetailsFooter } from './discover_field_details_footer'; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.scss b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_bucket.scss similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/discover_field_bucket.scss rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_bucket.scss diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_bucket.tsx similarity index 98% rename from src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_bucket.tsx index e433c3419e24af..7dab8cecf28a96 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_bucket.tsx @@ -11,7 +11,7 @@ import { EuiText, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@e import { i18n } from '@kbn/i18n'; import { StringFieldProgressBar } from './string_progress_bar'; import { Bucket } from './types'; -import { IndexPatternField } from '../../../../../data/public'; +import { IndexPatternField } from '../../../../../../../data/public'; import './discover_field_bucket.scss'; interface Props { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.scss similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/discover_field_details.scss rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.scss diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.test.tsx similarity index 88% rename from src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.test.tsx index f82154af33d1ea..a798abb60b833a 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.test.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-expect-error -import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; +// @ts-expect-error +import stubbedLogstashFields from '../../../../../__fixtures__/logstash_fields'; import { DiscoverFieldDetails } from './discover_field_details'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { IndexPatternField } from '../../../../../data/public'; -import { getStubIndexPattern } from '../../../../../data/public/test_utils'; +import { coreMock } from '../../../../../../../../core/public/mocks'; +import { IndexPatternField } from '../../../../../../../data/public'; +import { getStubIndexPattern } from '../../../../../../../data/public/test_utils'; const indexPattern = getStubIndexPattern( 'logstash-*', diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx similarity index 99% rename from src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx index e2ffb79551aab2..d7008ba3e310f3 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx @@ -18,7 +18,7 @@ import { getVisualizeHref, } from './lib/visualize_trigger_utils'; import { Bucket, FieldDetails } from './types'; -import { IndexPatternField, IndexPattern } from '../../../../../data/public'; +import { IndexPatternField, IndexPattern } from '../../../../../../../data/public'; import './discover_field_details.scss'; import { DiscoverFieldDetailsFooter } from './discover_field_details_footer'; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.test.tsx similarity index 87% rename from src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.test.tsx index 73e906cb62f5fc..aa93b2a6637362 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.test.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-expect-error -import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { IndexPatternField } from '../../../../../data/public'; -import { getStubIndexPattern } from '../../../../../data/public/test_utils'; +// @ts-expect-error +import stubbedLogstashFields from '../../../../../__fixtures__/logstash_fields'; +import { coreMock } from '../../../../../../../../core/public/mocks'; +import { IndexPatternField } from '../../../../../../../data/public'; +import { getStubIndexPattern } from '../../../../../../../data/public/test_utils'; import { DiscoverFieldDetailsFooter } from './discover_field_details_footer'; const indexPattern = getStubIndexPattern( diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.tsx similarity index 91% rename from src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.tsx index bea302087938ab..148dfc67c3e416 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details_footer.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { EuiLink, EuiPopoverFooter, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IndexPatternField } from '../../../../../data/common/index_patterns/fields'; -import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns'; +import { IndexPatternField } from '../../../../../../../data/common/index_patterns/fields'; +import { IndexPattern } from '../../../../../../../data/common/index_patterns/index_patterns'; import { FieldDetails } from './types'; interface DiscoverFieldDetailsFooterProps { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.scss b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.scss similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/discover_field_search.scss rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.scss diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.test.tsx similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.test.tsx diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.test.tsx similarity index 89% rename from src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.test.tsx index f6d577de564ade..45d6faa9606e10 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.test.tsx @@ -15,8 +15,7 @@ import { SavedObject } from 'kibana/server'; import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern'; import { EuiSelectable } from '@elastic/eui'; import { IndexPattern, IndexPatternAttributes } from 'src/plugins/data/public'; -import { configMock } from '../../../__mocks__/config'; -import { indexPatternsMock } from '../../../__mocks__/index_patterns'; +import { indexPatternsMock } from '../../../../../__mocks__/index_patterns'; const indexPattern = { id: 'the-index-pattern-id-first', @@ -38,13 +37,11 @@ const indexPattern2 = { } as SavedObject; const defaultProps = { - config: configMock, indexPatternList: [indexPattern1, indexPattern2], selectedIndexPattern: indexPattern, - state: {}, - setAppState: jest.fn(), useNewFieldsApi: true, indexPatterns: indexPatternsMock, + onChangeIndexPattern: jest.fn(), }; function getIndexPatternPickerList(instance: ShallowWrapper) { @@ -72,7 +69,7 @@ describe('DiscoverIndexPattern', () => { const props = ({ indexPatternList: null, selectedIndexPattern: null, - setIndexPattern: jest.fn(), + onChangeIndexPattern: jest.fn(), } as unknown) as DiscoverIndexPatternProps; expect(shallow()).toMatchSnapshot(`""`); @@ -92,10 +89,6 @@ describe('DiscoverIndexPattern', () => { await act(async () => { selectIndexPatternPickerOption(instance, 'test2 title'); }); - expect(defaultProps.setAppState).toHaveBeenCalledWith({ - index: 'the-index-pattern-id', - columns: [], - sort: [], - }); + expect(defaultProps.onChangeIndexPattern).toHaveBeenCalledWith('the-index-pattern-id'); }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.tsx similarity index 57% rename from src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.tsx index 021d5a0252f7ca..356c86dded97a9 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.tsx @@ -6,58 +6,35 @@ * Side Public License, v 1. */ -import React, { useState, useEffect, useCallback } from 'react'; -import { IUiSettingsClient, SavedObject } from 'kibana/public'; -import { - IndexPattern, - IndexPatternAttributes, - IndexPatternsContract, -} from 'src/plugins/data/public'; +import React, { useState, useEffect } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; +import { SavedObject } from 'kibana/public'; +import { IndexPattern, IndexPatternAttributes } from 'src/plugins/data/public'; import { IndexPatternRef } from './types'; import { ChangeIndexPattern } from './change_indexpattern'; -import { getSwitchIndexPatternAppState } from '../../helpers/get_switch_index_pattern_app_state'; -import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; -import { MODIFY_COLUMNS_ON_SWITCH, SORT_DEFAULT_ORDER_SETTING } from '../../../../common'; -import { AppState } from '../../angular/discover_state'; export interface DiscoverIndexPatternProps { - /** - * Client of uiSettings - */ - config: IUiSettingsClient; /** * list of available index patterns, if length > 1, component offers a "change" link */ indexPatternList: Array>; /** - * Index patterns service + * Callback function when changing an index pattern */ - indexPatterns: IndexPatternsContract; + onChangeIndexPattern: (id: string) => void; /** * currently selected index pattern, due to angular issues it's undefined at first rendering */ selectedIndexPattern: IndexPattern; - /** - * Function to set the current state - */ - setAppState: (state: Partial) => void; - /** - * Discover App state - */ - state: AppState; } /** * Component allows you to select an index pattern in discovers side bar */ export function DiscoverIndexPattern({ - config, indexPatternList, + onChangeIndexPattern, selectedIndexPattern, - indexPatterns, - state, - setAppState, }: DiscoverIndexPatternProps) { const options: IndexPatternRef[] = (indexPatternList || []).map((entity) => ({ id: entity.id, @@ -65,24 +42,6 @@ export function DiscoverIndexPattern({ })); const { id: selectedId, title: selectedTitle } = selectedIndexPattern || {}; - const setIndexPattern = useCallback( - async (id: string) => { - const nextIndexPattern = await indexPatterns.get(id); - if (nextIndexPattern && selectedIndexPattern) { - const nextAppState = getSwitchIndexPatternAppState( - selectedIndexPattern, - nextIndexPattern, - state.columns || [], - (state.sort || []) as SortPairArr[], - config.get(MODIFY_COLUMNS_ON_SWITCH), - config.get(SORT_DEFAULT_ORDER_SETTING) - ); - setAppState(nextAppState); - } - }, - [selectedIndexPattern, state, config, indexPatterns, setAppState] - ); - const [selected, setSelected] = useState({ id: selectedId, title: selectedTitle || '', @@ -108,7 +67,7 @@ export function DiscoverIndexPattern({ onChangeIndexPattern={(id) => { const indexPattern = options.find((pattern) => pattern.id === id); if (indexPattern) { - setIndexPattern(id); + onChangeIndexPattern(id); setSelected(indexPattern); } }} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern_management.test.tsx similarity index 90% rename from src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern_management.test.tsx index 6f9ff63d69782f..d81ecb79a42211 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern_management.test.tsx @@ -6,16 +6,16 @@ * Side Public License, v 1. */ -import { getStubIndexPattern } from '../../../../../data/public/index_patterns/index_pattern.stub'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { DiscoverServices } from '../../../build_services'; -// @ts-expect-error -import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; -import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; +import { mountWithIntl } from '@kbn/test/jest'; import { EuiContextMenuPanel, EuiPopover, EuiContextMenuItem } from '@elastic/eui'; import { findTestSubject } from '@kbn/test/jest'; +import { getStubIndexPattern } from '../../../../../../../data/public/index_patterns/index_pattern.stub'; +import { coreMock } from '../../../../../../../../core/public/mocks'; +import { DiscoverServices } from '../../../../../build_services'; +// @ts-expect-error +import stubbedLogstashFields from '../../../../../__fixtures__/logstash_fields'; +import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; const mockServices = ({ history: () => ({ diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern_management.tsx similarity index 95% rename from src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern_management.tsx index 9a9dfd579b96d5..125172f47b7c42 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern_management.tsx @@ -9,8 +9,8 @@ import React, { useState } from 'react'; import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DiscoverServices } from '../../../build_services'; -import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns'; +import { DiscoverServices } from '../../../../../build_services'; +import { IndexPattern } from '../../../../../../../data/common/index_patterns/index_patterns'; export interface DiscoverIndexPatternManagementProps { /** diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.scss similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.scss diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx similarity index 71% rename from src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx index 35d93095a1da31..304ab05516f92a 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.test.tsx @@ -10,54 +10,23 @@ import { each, cloneDeep } from 'lodash'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-expect-error -import realHits from '../../../__fixtures__/real_hits.js'; +import realHits from '../../../../../__fixtures__/real_hits.js'; // @ts-expect-error -import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; +import stubbedLogstashFields from '../../../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; import { DiscoverSidebarProps } from './discover_sidebar'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { IndexPatternAttributes } from '../../../../../data/common'; -import { getStubIndexPattern } from '../../../../../data/public/test_utils'; -import { SavedObject } from '../../../../../../core/types'; +import { coreMock } from '../../../../../../../../core/public/mocks'; +import { IndexPatternAttributes } from '../../../../../../../data/common'; +import { getStubIndexPattern } from '../../../../../../../data/public/test_utils'; +import { SavedObject } from '../../../../../../../../core/types'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; -import { DiscoverServices } from '../../../build_services'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; -import { configMock } from '../../../__mocks__/config'; -import { indexPatternsMock } from '../../../__mocks__/index_patterns'; +import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; +import { discoverServiceMock as mockDiscoverServices } from '../../../../../__mocks__/services'; -const mockServices = ({ - history: () => ({ - location: { - search: '', - }, - }), - capabilities: { - visualize: { - show: true, - }, - discover: { - save: false, - }, - }, - uiSettings: { - get: (key: string) => { - if (key === 'fields:popularLimit') { - return 5; - } - }, - }, - indexPatternFieldEditor: { - openEditor: jest.fn(), - userPermissions: { - editIndexPattern: jest.fn(), - }, - }, -} as unknown) as DiscoverServices; - -jest.mock('../../../kibana_services', () => ({ - getServices: () => mockServices, +jest.mock('../../../../../kibana_services', () => ({ + getServices: () => mockDiscoverServices, })); jest.mock('./lib/get_index_pattern_field_list', () => ({ @@ -92,22 +61,20 @@ function getCompProps(): DiscoverSidebarProps { } } return { - config: configMock, columns: ['extension'], fieldCounts, hits, indexPatternList, - indexPatterns: indexPatternsMock, + onChangeIndexPattern: jest.fn(), onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, - services: mockServices, + services: mockDiscoverServices, state: {}, trackUiMetric: jest.fn(), fieldFilter: getDefaultFieldFilter(), setFieldFilter: jest.fn(), - setAppState: jest.fn(), onEditRuntimeField: jest.fn(), editField: jest.fn(), }; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx similarity index 97% rename from src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx index aaaf72f7706305..0bebec61657b4a 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx @@ -27,9 +27,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { DiscoverField } from './discover_field'; import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; -import { FIELDS_LIMIT_SETTING } from '../../../../common'; +import { FIELDS_LIMIT_SETTING } from '../../../../../../common'; import { groupFields } from './lib/group_fields'; -import { IndexPatternField } from '../../../../../data/public'; +import { IndexPatternField } from '../../../../../../../data/public'; import { getDetails } from './lib/get_details'; import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; @@ -68,30 +68,28 @@ export interface DiscoverSidebarProps extends DiscoverSidebarResponsiveProps { export function DiscoverSidebar({ alwaysShowActionButtons = false, columns, - config, fieldCounts, fieldFilter, hits, indexPatternList, - indexPatterns, onAddField, onAddFilter, onRemoveField, selectedIndexPattern, services, - setAppState, setFieldFilter, - state, trackUiMetric, useNewFieldsApi = false, useFlyout = false, unmappedFieldsConfig, onEditRuntimeField, + onChangeIndexPattern, setFieldEditorRef, closeFlyout, editField, }: DiscoverSidebarProps) { const [fields, setFields] = useState(null); + const { indexPatternFieldEditor } = services; const indexPatternFieldEditPermission = indexPatternFieldEditor?.userPermissions.editIndexPattern(); const canEditIndexPatternField = !!indexPatternFieldEditPermission && useNewFieldsApi; @@ -282,12 +280,9 @@ export function DiscoverSidebar({ o.attributes.title)} - indexPatterns={indexPatterns} - state={state} - setAppState={setAppState} + onChangeIndexPattern={onChangeIndexPattern} /> @@ -323,12 +318,9 @@ export function DiscoverSidebar({ o.attributes.title)} - indexPatterns={indexPatterns} - state={state} - setAppState={setAppState} + onChangeIndexPattern={onChangeIndexPattern} /> diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx similarity index 84% rename from src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx index caa0e436f40910..2ad75806173eb0 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -10,23 +10,21 @@ import { each, cloneDeep } from 'lodash'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-expect-error -import realHits from '../../../__fixtures__/real_hits.js'; +import realHits from '../../../../../__fixtures__/real_hits.js'; // @ts-expect-error -import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; +import stubbedLogstashFields from '../../../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { IndexPatternAttributes } from '../../../../../data/common'; -import { getStubIndexPattern } from '../../../../../data/public/test_utils'; -import { SavedObject } from '../../../../../../core/types'; +import { coreMock } from '../../../../../../../../core/public/mocks'; +import { IndexPatternAttributes } from '../../../../../../../data/common'; +import { getStubIndexPattern } from '../../../../../../../data/public/test_utils'; +import { SavedObject } from '../../../../../../../../core/types'; import { DiscoverSidebarResponsive, DiscoverSidebarResponsiveProps, } from './discover_sidebar_responsive'; -import { DiscoverServices } from '../../../build_services'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; -import { configMock } from '../../../__mocks__/config'; -import { indexPatternsMock } from '../../../__mocks__/index_patterns'; +import { DiscoverServices } from '../../../../../build_services'; +import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; import { DiscoverSidebar } from './discover_sidebar'; const mockServices = ({ @@ -52,7 +50,7 @@ const mockServices = ({ }, } as unknown) as DiscoverServices; -jest.mock('../../../kibana_services', () => ({ +jest.mock('../../../../../kibana_services', () => ({ getServices: () => mockServices, })); @@ -89,17 +87,15 @@ function getCompProps(): DiscoverSidebarResponsiveProps { } return { columns: ['extension'], - config: configMock, fieldCounts, hits, indexPatternList, - indexPatterns: indexPatternsMock, + onChangeIndexPattern: jest.fn(), onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, services: mockServices, - setAppState: jest.fn(), state: {}, trackUiMetric: jest.fn(), onEditRuntimeField: jest.fn(), diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx similarity index 91% rename from src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx index 6b8918e2d99656..cc33601f77728f 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx @@ -11,7 +11,6 @@ import { sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { UiCounterMetricType } from '@kbn/analytics'; -import { IUiSettingsClient } from 'kibana/public'; import { EuiTitle, EuiHideFor, @@ -28,14 +27,14 @@ import { EuiFlexItem, } from '@elastic/eui'; import { DiscoverIndexPattern } from './discover_index_pattern'; -import { IndexPatternAttributes, IndexPatternsContract } from '../../../../../data/common'; -import { SavedObject } from '../../../../../../core/types'; -import { IndexPatternField, IndexPattern } from '../../../../../data/public'; +import { IndexPatternAttributes } from '../../../../../../../data/common'; +import { SavedObject } from '../../../../../../../../core/types'; +import { IndexPatternField, IndexPattern } from '../../../../../../../data/public'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; -import { DiscoverServices } from '../../../build_services'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; -import { AppState } from '../../angular/discover_state'; +import { DiscoverServices } from '../../../../../build_services'; +import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; +import { AppState } from '../../services/discover_state'; import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; export interface DiscoverSidebarResponsiveProps { @@ -47,10 +46,6 @@ export interface DiscoverSidebarResponsiveProps { * the selected columns displayed in the doc table in discover */ columns: string[]; - /** - * Client of uiSettings - */ - config: IUiSettingsClient; /** * a statistics of the distribution of fields in the given hits */ @@ -63,10 +58,6 @@ export interface DiscoverSidebarResponsiveProps { * List of available index patterns */ indexPatternList: Array>; - /** - * Index patterns service - */ - indexPatterns: IndexPatternsContract; /** * Has been toggled closed */ @@ -79,6 +70,10 @@ export interface DiscoverSidebarResponsiveProps { * Callback function when adding a filter from sidebar */ onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + /** + * Callback function when changing an index pattern + */ + onChangeIndexPattern: (id: string) => void; /** * Callback function when removing a field * @param fieldName @@ -92,10 +87,6 @@ export interface DiscoverSidebarResponsiveProps { * Discover plugin services; */ services: DiscoverServices; - /** - * Function to set the current state - */ - setAppState: (state: Partial) => void; /** * Discover App state */ @@ -114,7 +105,6 @@ export interface DiscoverSidebarResponsiveProps { * Read from the Fields API */ useNewFieldsApi?: boolean; - /** * an object containing properties for proper handling of unmapped fields */ @@ -215,12 +205,9 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) o.attributes.title)} - indexPatterns={props.indexPatterns} - state={props.state} - setAppState={props.setAppState} /> diff --git a/src/plugins/discover/public/application/components/sidebar/index.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/index.ts similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/index.ts rename to src/plugins/discover/public/application/apps/main/components/sidebar/index.ts diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.js b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/lib/field_calculator.js rename to src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts similarity index 94% rename from src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts rename to src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts index 2cdd99774c2a8a..501f18116dc6f7 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts @@ -10,12 +10,12 @@ import _ from 'lodash'; // @ts-expect-error -import realHits from '../../../../__fixtures__/real_hits.js'; +import realHits from '../../../../../../__fixtures__/real_hits.js'; // @ts-expect-error -import stubbedLogstashFields from '../../../../__fixtures__/logstash_fields'; -import { coreMock } from '../../../../../../../core/public/mocks'; -import { IndexPattern } from '../../../../../../data/public'; -import { getStubIndexPattern } from '../../../../../../data/public/test_utils'; +import stubbedLogstashFields from '../../../../../../__fixtures__/logstash_fields'; +import { coreMock } from '../../../../../../../../../core/public/mocks'; +import { IndexPattern } from '../../../../../../../../data/public'; +import { getStubIndexPattern } from '../../../../../../../../data/public/test_utils'; // @ts-expect-error import { fieldCalculator } from './field_calculator'; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.test.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_filter.test.ts similarity index 97% rename from src/plugins/discover/public/application/components/sidebar/lib/field_filter.test.ts rename to src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_filter.test.ts index 04217c07f66381..8d7c543157ead7 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_filter.test.ts @@ -7,7 +7,7 @@ */ import { getDefaultFieldFilter, setFieldFilterProp, isFieldFiltered } from './field_filter'; -import { IndexPatternField } from '../../../../../../data/public'; +import { IndexPatternField } from '../../../../../../../../data/public'; describe('field_filter', function () { it('getDefaultFieldFilter should return default filter state', function () { diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_filter.ts similarity index 96% rename from src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts rename to src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_filter.ts index 8c7e48f173031d..25a8309d3d9634 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_filter.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPatternField } from '../../../../../../data/public'; +import { IndexPatternField } from '../../../../../../../../data/public'; export interface FieldFilterState { missing: boolean; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_details.ts similarity index 91% rename from src/plugins/discover/public/application/components/sidebar/lib/get_details.ts rename to src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_details.ts index 1e35717d249f8b..ab991f2cfb6563 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_details.ts @@ -8,8 +8,8 @@ // @ts-expect-error import { fieldCalculator } from './field_calculator'; -import { IndexPattern, IndexPatternField } from '../../../../../../data/public'; -import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; +import { IndexPattern, IndexPatternField } from '../../../../../../../../data/public'; +import { ElasticSearchHit } from '../../../../../doc_views/doc_views_types'; export function getDetails( field: IndexPatternField, diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_field_type_name.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_field_type_name.ts similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/lib/get_field_type_name.ts rename to src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_field_type_name.ts diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_index_pattern_field_list.ts similarity index 95% rename from src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts rename to src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_index_pattern_field_list.ts index 1586c2b26eefe0..8f2e18c64a5ebf 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_index_pattern_field_list.ts @@ -8,7 +8,7 @@ import { difference } from 'lodash'; import { IndexPattern, IndexPatternField } from 'src/plugins/data/public'; -import { isNestedFieldParent } from '../../../helpers/nested_fields'; +import { isNestedFieldParent } from '../../../utils/nested_fields'; export function getIndexPatternFieldList( indexPattern?: IndexPattern, diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_warnings.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_warnings.ts similarity index 92% rename from src/plugins/discover/public/application/components/sidebar/lib/get_warnings.ts rename to src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_warnings.ts index 05634973fae8c5..60ce5351e2cd3a 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_warnings.ts +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/get_warnings.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import { IndexPatternField } from '../../../../../../data/public'; +import { IndexPatternField } from '../../../../../../../../data/public'; export function getWarnings(field: IndexPatternField) { let warnings = []; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts similarity index 98% rename from src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts rename to src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts index e8eb07784cf9fc..58697206356214 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts @@ -8,7 +8,7 @@ import { groupFields } from './group_fields'; import { getDefaultFieldFilter } from './field_filter'; -import { IndexPatternField } from '../../../../../../data/common/index_patterns/fields'; +import { IndexPatternField } from '../../../../../../../../data/common/index_patterns/fields'; const fields = [ { diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx diff --git a/src/plugins/discover/public/application/components/sidebar/lib/visualize_trigger_utils.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.ts similarity index 95% rename from src/plugins/discover/public/application/components/sidebar/lib/visualize_trigger_utils.ts rename to src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.ts index 1a6cc38a4259bd..2fabaa0ddd1003 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/visualize_trigger_utils.ts +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/visualize_trigger_utils.ts @@ -11,9 +11,9 @@ import { VISUALIZE_GEO_FIELD_TRIGGER, visualizeFieldTrigger, visualizeGeoFieldTrigger, -} from '../../../../../../ui_actions/public'; -import { getUiActions } from '../../../../kibana_services'; -import { IndexPatternField, KBN_FIELD_TYPES } from '../../../../../../data/public'; +} from '../../../../../../../../ui_actions/public'; +import { getUiActions } from '../../../../../../kibana_services'; +import { IndexPatternField, KBN_FIELD_TYPES } from '../../../../../../../../data/public'; function getTriggerConstant(type: string) { return type === KBN_FIELD_TYPES.GEO_POINT || type === KBN_FIELD_TYPES.GEO_SHAPE diff --git a/src/plugins/discover/public/application/components/sidebar/string_progress_bar.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/string_progress_bar.tsx similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/string_progress_bar.tsx rename to src/plugins/discover/public/application/apps/main/components/sidebar/string_progress_bar.tsx diff --git a/src/plugins/discover/public/application/components/sidebar/types.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/types.ts similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/types.ts rename to src/plugins/discover/public/application/apps/main/components/sidebar/types.ts diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/index.ts b/src/plugins/discover/public/application/apps/main/components/skip_bottom_button/index.ts similarity index 100% rename from src/plugins/discover/public/application/components/skip_bottom_button/index.ts rename to src/plugins/discover/public/application/apps/main/components/skip_bottom_button/index.ts diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx b/src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.test.tsx similarity index 100% rename from src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx rename to src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.test.tsx diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx b/src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.tsx similarity index 100% rename from src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx rename to src/plugins/discover/public/application/apps/main/components/skip_bottom_button/skip_bottom_button.tsx diff --git a/src/plugins/discover/public/application/components/timechart_header/index.ts b/src/plugins/discover/public/application/apps/main/components/timechart_header/index.ts similarity index 100% rename from src/plugins/discover/public/application/components/timechart_header/index.ts rename to src/plugins/discover/public/application/apps/main/components/timechart_header/index.ts diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.scss b/src/plugins/discover/public/application/apps/main/components/timechart_header/timechart_header.scss similarity index 100% rename from src/plugins/discover/public/application/components/timechart_header/timechart_header.scss rename to src/plugins/discover/public/application/apps/main/components/timechart_header/timechart_header.scss diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx b/src/plugins/discover/public/application/apps/main/components/timechart_header/timechart_header.test.tsx similarity index 97% rename from src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx rename to src/plugins/discover/public/application/apps/main/components/timechart_header/timechart_header.test.tsx index 0da1fdd92d2cce..a68dc9a3b3756c 100644 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/timechart_header/timechart_header.test.tsx @@ -12,7 +12,7 @@ import { ReactWrapper } from 'enzyme'; import { TimechartHeader, TimechartHeaderProps } from './timechart_header'; import { EuiIconTip } from '@elastic/eui'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { DataPublicPluginStart } from '../../../../../data/public'; +import { DataPublicPluginStart } from '../../../../../../../data/public'; describe('timechart header', function () { let props: TimechartHeaderProps; diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx b/src/plugins/discover/public/application/apps/main/components/timechart_header/timechart_header.tsx similarity index 95% rename from src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx rename to src/plugins/discover/public/application/apps/main/components/timechart_header/timechart_header.tsx index a2fc17e05a203e..ec94679ece6759 100644 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx +++ b/src/plugins/discover/public/application/apps/main/components/timechart_header/timechart_header.tsx @@ -18,8 +18,14 @@ import { import moment from 'moment'; import { i18n } from '@kbn/i18n'; import dateMath from '@elastic/datemath'; -import { DataPublicPluginStart } from '../../../../../data/public'; import './timechart_header.scss'; +import { DataPublicPluginStart } from '../../../../../../../data/public'; + +export interface TimechartBucketInterval { + scaled?: boolean; + description?: string; + scale?: number; +} export interface TimechartHeaderProps { /** @@ -29,11 +35,7 @@ export interface TimechartHeaderProps { /** * Interval for the buckets of the recent request */ - bucketInterval?: { - scaled?: boolean; - description?: string; - scale?: number; - }; + bucketInterval?: TimechartBucketInterval; data: DataPublicPluginStart; /** * Interval Options diff --git a/src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap rename to src/plugins/discover/public/application/apps/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx new file mode 100644 index 00000000000000..687532cd94f083 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallowWithIntl } from '@kbn/test/jest'; +import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; +import { savedSearchMock } from '../../../../../__mocks__/saved_search'; +import { DiscoverTopNav, DiscoverTopNavProps } from './discover_topnav'; +import { TopNavMenuData } from '../../../../../../../navigation/public'; +import { ISearchSource, Query } from '../../../../../../../data/common'; +import { GetStateReturn } from '../../services/discover_state'; +import { setHeaderActionMenuMounter } from '../../../../../kibana_services'; +import { discoverServiceMock } from '../../../../../__mocks__/services'; + +setHeaderActionMenuMounter(jest.fn()); + +function getProps(savePermissions = true): DiscoverTopNavProps { + discoverServiceMock.capabilities.discover!.save = savePermissions; + + return { + stateContainer: {} as GetStateReturn, + indexPattern: indexPatternMock, + savedSearch: savedSearchMock, + navigateTo: jest.fn(), + services: discoverServiceMock, + query: {} as Query, + savedQuery: '', + updateQuery: jest.fn(), + onOpenInspector: jest.fn(), + searchSource: {} as ISearchSource, + }; +} + +describe('Discover topnav component', () => { + test('generated config of TopNavMenu config is correct when discover save permissions are assigned', () => { + const props = getProps(true); + const component = shallowWithIntl(); + const topMenuConfig = component.props().config.map((obj: TopNavMenuData) => obj.id); + expect(topMenuConfig).toEqual(['options', 'new', 'save', 'open', 'share', 'inspect']); + }); + + test('generated config of TopNavMenu config is correct when no discover save permissions are assigned', () => { + const props = getProps(false); + const component = shallowWithIntl(); + const topMenuConfig = component.props().config.map((obj: TopNavMenuData) => obj.id); + expect(topMenuConfig).toEqual(['options', 'new', 'open', 'share', 'inspect']); + }); +}); diff --git a/src/plugins/discover/public/application/components/discover_topnav.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.tsx similarity index 60% rename from src/plugins/discover/public/application/components/discover_topnav.tsx rename to src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.tsx index c5c0df6e6f74a1..9afda734010845 100644 --- a/src/plugins/discover/public/application/components/discover_topnav.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.tsx @@ -6,45 +6,53 @@ * Side Public License, v 1. */ import React, { useMemo } from 'react'; -import { DiscoverProps } from './types'; -import { getTopNavLinks } from './top_nav/get_top_nav_links'; -import { Query, TimeRange } from '../../../../data/common/query'; +import { DiscoverLayoutProps } from '../layout/types'; +import { getTopNavLinks } from './get_top_nav_links'; +import { Query, TimeRange } from '../../../../../../../data/common/query'; +import { getHeaderActionMenuMounter } from '../../../../../kibana_services'; +import { GetStateReturn } from '../../services/discover_state'; -export type DiscoverTopNavProps = Pick & { +export type DiscoverTopNavProps = Pick< + DiscoverLayoutProps, + 'indexPattern' | 'navigateTo' | 'savedSearch' | 'services' | 'searchSource' +> & { onOpenInspector: () => void; query?: Query; savedQuery?: string; updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; + stateContainer: GetStateReturn; }; export const DiscoverTopNav = ({ indexPattern, - opts, onOpenInspector, query, savedQuery, + stateContainer, updateQuery, searchSource, + navigateTo, + savedSearch, + services, }: DiscoverTopNavProps) => { const showDatePicker = useMemo(() => indexPattern.isTimeBased(), [indexPattern]); - const { TopNavMenu } = opts.services.navigation.ui; + const { TopNavMenu } = services.navigation.ui; const topNavMenu = useMemo( () => getTopNavLinks({ - getFieldCounts: opts.getFieldCounts, indexPattern, - navigateTo: opts.navigateTo, - savedSearch: opts.savedSearch, - services: opts.services, - state: opts.stateContainer, + navigateTo, + savedSearch, + services, + state: stateContainer, onOpenInspector, searchSource, }), - [indexPattern, opts, onOpenInspector, searchSource] + [indexPattern, navigateTo, onOpenInspector, searchSource, stateContainer, savedSearch, services] ); const updateSavedQueryId = (newSavedQueryId: string | undefined) => { - const { appStateContainer, setAppState } = opts.stateContainer; + const { appStateContainer, setAppState } = stateContainer; if (newSavedQueryId) { setAppState({ savedQuery: newSavedQueryId }); } else { @@ -56,6 +64,10 @@ export const DiscoverTopNav = ({ appStateContainer.set(newState); } }; + const setMenuMountPoint = useMemo(() => { + return getHeaderActionMenuMounter(); + }, []); + return ( diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts similarity index 87% rename from src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts rename to src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts index 2c50ce61c8afb7..6a6fb8a44a5cfe 100644 --- a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts @@ -8,10 +8,10 @@ import { ISearchSource } from 'src/plugins/data/public'; import { getTopNavLinks } from './get_top_nav_links'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; -import { savedSearchMock } from '../../../__mocks__/saved_search'; -import { DiscoverServices } from '../../../build_services'; -import { GetStateReturn } from '../../angular/discover_state'; +import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; +import { savedSearchMock } from '../../../../../__mocks__/saved_search'; +import { DiscoverServices } from '../../../../../build_services'; +import { GetStateReturn } from '../../services/discover_state'; const services = ({ capabilities: { @@ -28,7 +28,6 @@ const state = ({} as unknown) as GetStateReturn; test('getTopNavLinks result', () => { const topNavLinks = getTopNavLinks({ - getFieldCounts: jest.fn(), indexPattern: indexPatternMock, navigateTo: jest.fn(), onOpenInspector: jest.fn(), diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts similarity index 90% rename from src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts rename to src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts index 38bc831225c682..f19b30cda5f8a3 100644 --- a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts @@ -9,20 +9,19 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { showOpenSearchPanel } from './show_open_search_panel'; -import { getSharingData, showPublicUrlSwitch } from '../../helpers/get_sharing_data'; -import { unhashUrl } from '../../../../../kibana_utils/public'; -import { DiscoverServices } from '../../../build_services'; -import { SavedSearch } from '../../../saved_searches'; +import { getSharingData, showPublicUrlSwitch } from '../../utils/get_sharing_data'; +import { unhashUrl } from '../../../../../../../kibana_utils/public'; +import { DiscoverServices } from '../../../../../build_services'; +import { SavedSearch } from '../../../../../saved_searches'; import { onSaveSearch } from './on_save_search'; -import { GetStateReturn } from '../../angular/discover_state'; -import { IndexPattern, ISearchSource } from '../../../kibana_services'; +import { GetStateReturn } from '../../services/discover_state'; +import { IndexPattern, ISearchSource } from '../../../../../kibana_services'; import { openOptionsPopover } from './open_options_popover'; /** * Helper function to build the top nav links */ export const getTopNavLinks = ({ - getFieldCounts, indexPattern, navigateTo, savedSearch, @@ -31,7 +30,6 @@ export const getTopNavLinks = ({ onOpenInspector, searchSource, }: { - getFieldCounts: () => Promise>; indexPattern: IndexPattern; navigateTo: (url: string) => void; savedSearch: SavedSearch; diff --git a/src/plugins/discover/public/application/components/top_nav/on_save_search.test.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.test.tsx similarity index 63% rename from src/plugins/discover/public/application/components/top_nav/on_save_search.test.tsx rename to src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.test.tsx index ed79d16fd5ff1c..e5f44a0fbf2149 100644 --- a/src/plugins/discover/public/application/components/top_nav/on_save_search.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.test.tsx @@ -6,15 +6,15 @@ * Side Public License, v 1. */ -import { showSaveModal } from '../../../../../saved_objects/public'; -jest.mock('../../../../../saved_objects/public'); +import { showSaveModal } from '../../../../../../../saved_objects/public'; +jest.mock('../../../../../../../saved_objects/public'); import { onSaveSearch } from './on_save_search'; -import { indexPatternMock } from '../../../__mocks__/index_pattern'; -import { savedSearchMock } from '../../../__mocks__/saved_search'; -import { DiscoverServices } from '../../../build_services'; -import { GetStateReturn } from '../../angular/discover_state'; -import { i18nServiceMock } from '../../../../../../core/public/mocks'; +import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; +import { savedSearchMock } from '../../../../../__mocks__/saved_search'; +import { DiscoverServices } from '../../../../../build_services'; +import { GetStateReturn } from '../../services/discover_state'; +import { i18nServiceMock } from '../../../../../../../../core/public/mocks'; test('onSaveSearch', async () => { const serviceMock = ({ diff --git a/src/plugins/discover/public/application/components/top_nav/on_save_search.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx similarity index 89% rename from src/plugins/discover/public/application/components/top_nav/on_save_search.tsx rename to src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx index a0271acec22cfb..f4b969e977254b 100644 --- a/src/plugins/discover/public/application/components/top_nav/on_save_search.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { SavedObjectSaveModal, showSaveModal } from '../../../../../saved_objects/public'; -import { SavedSearch } from '../../../saved_searches'; -import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns'; -import { DiscoverServices } from '../../../build_services'; -import { GetStateReturn } from '../../angular/discover_state'; -import { setBreadcrumbsTitle } from '../../helpers/breadcrumbs'; -import { persistSavedSearch } from '../../helpers/persist_saved_search'; +import { SavedObjectSaveModal, showSaveModal } from '../../../../../../../saved_objects/public'; +import { SavedSearch } from '../../../../../saved_searches'; +import { IndexPattern } from '../../../../../../../data/common/index_patterns/index_patterns'; +import { DiscoverServices } from '../../../../../build_services'; +import { GetStateReturn } from '../../services/discover_state'; +import { setBreadcrumbsTitle } from '../../../../helpers/breadcrumbs'; +import { persistSavedSearch } from '../../utils/persist_saved_search'; async function saveDataSource({ indexPattern, diff --git a/src/plugins/discover/public/application/components/top_nav/open_options_popover.scss b/src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.scss similarity index 100% rename from src/plugins/discover/public/application/components/top_nav/open_options_popover.scss rename to src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.scss diff --git a/src/plugins/discover/public/application/components/top_nav/open_options_popover.test.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.test.tsx similarity index 93% rename from src/plugins/discover/public/application/components/top_nav/open_options_popover.test.tsx rename to src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.test.tsx index 5b3d1656d77e74..28e57aed7b6605 100644 --- a/src/plugins/discover/public/application/components/top_nav/open_options_popover.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.test.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { getServices } from '../../../kibana_services'; +import { getServices } from '../../../../../kibana_services'; -jest.mock('../../../kibana_services', () => { +jest.mock('../../../../../kibana_services', () => { const mockUiSettings = new Map(); return { getServices: () => ({ diff --git a/src/plugins/discover/public/application/components/top_nav/open_options_popover.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.tsx similarity index 97% rename from src/plugins/discover/public/application/components/top_nav/open_options_popover.tsx rename to src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.tsx index e32ffa4a05de38..6e90c702c2bfde 100644 --- a/src/plugins/discover/public/application/components/top_nav/open_options_popover.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/open_options_popover.tsx @@ -21,9 +21,9 @@ import { EuiButtonEmpty, EuiTextAlign, } from '@elastic/eui'; -import { getServices } from '../../../kibana_services'; import './open_options_popover.scss'; -import { DOC_TABLE_LEGACY } from '../../../../common'; +import { DOC_TABLE_LEGACY } from '../../../../../../common'; +import { getServices } from '../../../../../kibana_services'; const container = document.createElement('div'); let isOpen = false; diff --git a/src/plugins/discover/public/application/components/top_nav/open_search_panel.test.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.test.tsx similarity index 93% rename from src/plugins/discover/public/application/components/top_nav/open_search_panel.test.tsx rename to src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.test.tsx index 856d8c5f712362..b34abab728eb19 100644 --- a/src/plugins/discover/public/application/components/top_nav/open_search_panel.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -jest.mock('../../../kibana_services', () => { +jest.mock('../../../../../kibana_services', () => { return { getServices: () => ({ core: { uiSettings: {}, savedObjects: {} }, diff --git a/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx similarity index 95% rename from src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx rename to src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx index f6881d1856566a..e63f010a8dffc1 100644 --- a/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx @@ -20,8 +20,8 @@ import { EuiFlyoutBody, EuiTitle, } from '@elastic/eui'; -import { SavedObjectFinderUi } from '../../../../../saved_objects/public'; -import { getServices } from '../../../kibana_services'; +import { SavedObjectFinderUi } from '../../../../../../../saved_objects/public'; +import { getServices } from '../../../../../kibana_services'; const SEARCH_OBJECT_TYPE = 'search'; diff --git a/src/plugins/discover/public/application/components/top_nav/show_open_search_panel.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/show_open_search_panel.tsx similarity index 100% rename from src/plugins/discover/public/application/components/top_nav/show_open_search_panel.tsx rename to src/plugins/discover/public/application/apps/main/components/top_nav/show_open_search_panel.tsx diff --git a/src/plugins/discover/public/application/angular/directives/uninitialized.tsx b/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/directives/uninitialized.tsx rename to src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx diff --git a/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx b/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx new file mode 100644 index 00000000000000..0caa5f3f527c67 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { DiscoverMainApp } from './discover_main_app'; +import { discoverServiceMock } from '../../../__mocks__/services'; +import { savedSearchMock } from '../../../__mocks__/saved_search'; +import { createSearchSessionMock } from '../../../__mocks__/search_session'; +import { SavedObject } from '../../../../../../core/types'; +import { IndexPatternAttributes } from '../../../../../data/common'; +import { setHeaderActionMenuMounter } from '../../../kibana_services'; +import { findTestSubject } from '@elastic/eui/lib/test'; + +setHeaderActionMenuMounter(jest.fn()); + +describe('DiscoverMainApp', () => { + test('renders', () => { + const { history } = createSearchSessionMock(); + const indexPatternList = ([indexPatternMock].map((ip) => { + return { ...ip, ...{ attributes: { title: ip.title } } }; + }) as unknown) as Array>; + + const component = mountWithIntl( + + ); + + expect(findTestSubject(component, 'indexPattern-switch-link').text()).toBe( + indexPatternMock.title + ); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx new file mode 100644 index 00000000000000..5cc7147b49ff98 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx @@ -0,0 +1,145 @@ +/* + * 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, { useMemo, useCallback, useEffect } from 'react'; +import { History } from 'history'; +import { DiscoverLayout } from './components/layout'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; +import { useSavedSearch as useSavedSearchData } from './services/use_saved_search'; +import { setBreadcrumbsTitle } from '../../helpers/breadcrumbs'; +import { addHelpMenuToAppChrome } from '../../components/help_menu/help_menu_util'; +import { useDiscoverState } from './services/use_discover_state'; +import { useSearchSession } from './services/use_search_session'; +import { useUrl } from './services/use_url'; +import { IndexPattern, IndexPatternAttributes, SavedObject } from '../../../../../data/common'; +import { DiscoverServices } from '../../../build_services'; +import { SavedSearch } from '../../../saved_searches'; + +const DiscoverLayoutMemoized = React.memo(DiscoverLayout); + +export interface DiscoverMainProps { + /** + * Current IndexPattern + */ + indexPattern: IndexPattern; + + opts: { + /** + * Use angular router for navigation + */ + navigateTo: () => void; + /** + * Instance of browser history + */ + history: History; + /** + * List of available index patterns + */ + indexPatternList: Array>; + /** + * Kibana core services used by discover + */ + services: DiscoverServices; + /** + * Current instance of SavedSearch + */ + savedSearch: SavedSearch; + }; +} + +export function DiscoverMainApp(props: DiscoverMainProps) { + const { services, history, navigateTo, indexPatternList } = props.opts; + const { chrome, docLinks, uiSettings: config, data } = services; + + const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]); + + /** + * State related logic + */ + const { + stateContainer, + state, + indexPattern, + searchSource, + savedSearch, + resetSavedSearch, + } = useDiscoverState({ + services, + history, + initialIndexPattern: props.indexPattern, + initialSavedSearch: props.opts.savedSearch, + }); + + /** + * Url / Routing logic + */ + useUrl({ history, resetSavedSearch }); + + /** + * Search session logic + */ + const searchSessionManager = useSearchSession({ services, history, stateContainer, savedSearch }); + + /** + * Data fetching logic + */ + const { data$, refetch$ } = useSavedSearchData({ + indexPattern, + savedSearch, + searchSessionManager, + searchSource, + services, + state, + stateContainer, + useNewFieldsApi, + }); + + /** + * SavedSearch depended initializing + */ + useEffect(() => { + const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : ''; + chrome.docTitle.change(`Discover${pageTitleSuffix}`); + setBreadcrumbsTitle(savedSearch, chrome); + return () => { + data.search.session.clear(); + }; + }, [savedSearch, chrome, docLinks, refetch$, stateContainer, data, config]); + + /** + * Initializing syncing with state and help menu + */ + useEffect(() => { + addHelpMenuToAppChrome(chrome, docLinks); + stateContainer.replaceUrlAppState({}).then(() => { + stateContainer.startSync(); + }); + + return () => stateContainer.stopSync(); + }, [stateContainer, chrome, docLinks]); + + const resetQuery = useCallback(() => { + resetSavedSearch(savedSearch.id); + }, [resetSavedSearch, savedSearch]); + + return ( + + ); +} diff --git a/src/plugins/discover/public/application/components/constants.ts b/src/plugins/discover/public/application/apps/main/index.ts similarity index 72% rename from src/plugins/discover/public/application/components/constants.ts rename to src/plugins/discover/public/application/apps/main/index.ts index 42845e83b7435b..af30b0c9534346 100644 --- a/src/plugins/discover/public/application/components/constants.ts +++ b/src/plugins/discover/public/application/apps/main/index.ts @@ -6,9 +6,4 @@ * Side Public License, v 1. */ -export const fetchStatuses = { - UNINITIALIZED: 'uninitialized', - LOADING: 'loading', - COMPLETE: 'complete', - ERROR: 'error', -}; +export { DiscoverMainApp } from './discover_main_app'; diff --git a/src/plugins/discover/public/application/angular/discover_search_session.test.ts b/src/plugins/discover/public/application/apps/main/services/discover_search_session.test.ts similarity index 83% rename from src/plugins/discover/public/application/angular/discover_search_session.test.ts rename to src/plugins/discover/public/application/apps/main/services/discover_search_session.test.ts index 8e6fb11acb9633..97c6ada3636d5d 100644 --- a/src/plugins/discover/public/application/angular/discover_search_session.test.ts +++ b/src/plugins/discover/public/application/apps/main/services/discover_search_session.test.ts @@ -6,20 +6,10 @@ * Side Public License, v 1. */ -import { DiscoverSearchSessionManager } from './discover_search_session'; -import { createMemoryHistory } from 'history'; -import { dataPluginMock } from '../../../../data/public/mocks'; -import { DataPublicPluginStart } from '../../../../data/public'; +import { createSearchSessionMock } from '../../../../__mocks__/search_session'; describe('DiscoverSearchSessionManager', () => { - const history = createMemoryHistory(); - const session = dataPluginMock.createStartContract().search.session as jest.Mocked< - DataPublicPluginStart['search']['session'] - >; - const searchSessionManager = new DiscoverSearchSessionManager({ - history, - session, - }); + const { history, session, searchSessionManager } = createSearchSessionMock(); beforeEach(() => { history.push('/'); diff --git a/src/plugins/discover/public/application/angular/discover_search_session.ts b/src/plugins/discover/public/application/apps/main/services/discover_search_session.ts similarity index 93% rename from src/plugins/discover/public/application/angular/discover_search_session.ts rename to src/plugins/discover/public/application/apps/main/services/discover_search_session.ts index a3cdfa42c40dd2..81ab578229d198 100644 --- a/src/plugins/discover/public/application/angular/discover_search_session.ts +++ b/src/plugins/discover/public/application/apps/main/services/discover_search_session.ts @@ -8,13 +8,13 @@ import { History } from 'history'; import { filter } from 'rxjs/operators'; -import { DataPublicPluginStart } from '../../../../data/public'; +import { DataPublicPluginStart } from '../../../../../../data/public'; import { createQueryParamObservable, getQueryParams, removeQueryParam, -} from '../../../../kibana_utils/public'; -import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator'; +} from '../../../../../../kibana_utils/public'; +import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../../../url_generator'; export interface DiscoverSearchSessionManagerDeps { history: History; diff --git a/src/plugins/discover/public/application/angular/discover_state.test.ts b/src/plugins/discover/public/application/apps/main/services/discover_state.test.ts similarity index 97% rename from src/plugins/discover/public/application/angular/discover_state.test.ts rename to src/plugins/discover/public/application/apps/main/services/discover_state.test.ts index ddb4e874ccc64b..11f6954af97b0e 100644 --- a/src/plugins/discover/public/application/angular/discover_state.test.ts +++ b/src/plugins/discover/public/application/apps/main/services/discover_state.test.ts @@ -13,9 +13,9 @@ import { createSearchSessionRestorationDataProvider, } from './discover_state'; import { createBrowserHistory, History } from 'history'; -import { dataPluginMock } from '../../../../data/public/mocks'; -import { SavedSearch } from '../../saved_searches'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; +import { dataPluginMock } from '../../../../../../data/public/mocks'; +import { SavedSearch } from '../../../../saved_searches'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; let history: History; let state: GetStateReturn; diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/apps/main/services/discover_state.ts similarity index 95% rename from src/plugins/discover/public/application/angular/discover_state.ts rename to src/plugins/discover/public/application/apps/main/services/discover_state.ts index f71e3ac651f532..e90e62da16ba99 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/discover_state.ts @@ -18,19 +18,19 @@ import { StateContainer, syncState, withNotifyOnErrors, -} from '../../../../kibana_utils/public'; +} from '../../../../../../kibana_utils/public'; import { DataPublicPluginStart, esFilters, Filter, Query, SearchSessionInfoProvider, -} from '../../../../data/public'; -import { migrateLegacyQuery } from '../helpers/migrate_legacy_query'; -import { DiscoverGridSettings } from '../components/discover_grid/types'; -import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../url_generator'; -import { SavedSearch } from '../../saved_searches'; -import { handleSourceColumnState } from './helpers'; +} from '../../../../../../data/public'; +import { migrateLegacyQuery } from '../../../helpers/migrate_legacy_query'; +import { DiscoverGridSettings } from '../../../components/discover_grid/types'; +import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../../../url_generator'; +import { SavedSearch } from '../../../../saved_searches'; +import { handleSourceColumnState } from '../../../angular/helpers'; export interface AppState { /** diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts new file mode 100644 index 00000000000000..051a2d2dcd9cc7 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts @@ -0,0 +1,82 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { createSearchSessionMock } from '../../../../__mocks__/search_session'; +import { discoverServiceMock } from '../../../../__mocks__/services'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { useDiscoverState } from './use_discover_state'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; +import { SearchSource } from '../../../../../../data/common'; + +describe('test useDiscoverState', () => { + test('return is valid', async () => { + const { history } = createSearchSessionMock(); + + const { result } = renderHook(() => { + return useDiscoverState({ + services: discoverServiceMock, + history, + initialIndexPattern: indexPatternMock, + initialSavedSearch: savedSearchMock, + }); + }); + expect(result.current.state.index).toBe(indexPatternMock.id); + expect(result.current.stateContainer).toBeInstanceOf(Object); + expect(result.current.setState).toBeInstanceOf(Function); + expect(result.current.savedSearch.id).toBe(savedSearchMock.id); + expect(result.current.searchSource).toBeInstanceOf(SearchSource); + }); + + test('setState', async () => { + const { history } = createSearchSessionMock(); + + const { result } = renderHook(() => { + return useDiscoverState({ + services: discoverServiceMock, + history, + initialIndexPattern: indexPatternMock, + initialSavedSearch: savedSearchMock, + }); + }); + await act(async () => { + result.current.setState({ columns: ['123'] }); + }); + expect(result.current.state.columns).toEqual(['123']); + }); + + test('resetSavedSearch', async () => { + const { history } = createSearchSessionMock(); + + const { result, waitForValueToChange } = renderHook(() => { + return useDiscoverState({ + services: discoverServiceMock, + history, + initialIndexPattern: indexPatternMock, + initialSavedSearch: savedSearchMock, + }); + }); + + await act(async () => { + result.current.stateContainer.startSync(); + }); + + const initialColumns = result.current.state.columns; + await act(async () => { + result.current.setState({ columns: ['123'] }); + }); + expect(result.current.state.columns).toEqual(['123']); + + result.current.resetSavedSearch('the-saved-search-id'); + await waitForValueToChange(() => { + return result.current.state; + }); + + expect(result.current.state.columns).toEqual(initialColumns); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts new file mode 100644 index 00000000000000..a3546d54cd4932 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts @@ -0,0 +1,155 @@ +/* + * 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 { useMemo, useEffect, useState, useCallback } from 'react'; +import { cloneDeep } from 'lodash'; +import { History } from 'history'; +import { getState } from './discover_state'; +import { getStateDefaults } from '../utils/get_state_defaults'; +import { + esFilters, + connectToQueryState, + syncQueryStateWithUrl, + IndexPattern, +} from '../../../../../../data/public'; +import { DiscoverServices } from '../../../../build_services'; +import { SavedSearch } from '../../../../saved_searches'; +import { loadIndexPattern } from '../utils/resolve_index_pattern'; + +export function useDiscoverState({ + services, + history, + initialIndexPattern, + initialSavedSearch, +}: { + services: DiscoverServices; + initialSavedSearch: SavedSearch; + history: History; + initialIndexPattern: IndexPattern; +}) { + const { uiSettings: config, data, filterManager } = services; + const [indexPattern, setIndexPattern] = useState(initialIndexPattern); + const [savedSearch, setSavedSearch] = useState(initialSavedSearch); + + const searchSource = useMemo(() => { + savedSearch.searchSource.setField('index', indexPattern); + return savedSearch.searchSource.createChild(); + }, [savedSearch.searchSource, indexPattern]); + + const stateContainer = useMemo( + () => + getState({ + getStateDefaults: () => + getStateDefaults({ + config, + data, + savedSearch, + }), + storeInSessionStorage: config.get('state:storeInSessionStorage'), + history, + toasts: services.core.notifications.toasts, + uiSettings: config, + }), + [config, data, history, savedSearch, services.core.notifications.toasts] + ); + + const { appStateContainer, getPreviousAppState } = stateContainer; + + const [state, setState] = useState(appStateContainer.getState()); + + useEffect(() => { + if (stateContainer.appStateContainer.getState().index !== indexPattern.id) { + // used index pattern is different than the given by url/state which is invalid + stateContainer.setAppState({ index: indexPattern.id }); + } + // sync initial app filters from state to filterManager + const filters = appStateContainer.getState().filters; + if (filters) { + filterManager.setAppFilters(cloneDeep(filters)); + } + const query = appStateContainer.getState().query; + if (query) { + data.query.queryString.setQuery(query); + } + + const stopSyncingQueryAppStateWithStateContainer = connectToQueryState( + data.query, + appStateContainer, + { + filters: esFilters.FilterStateStore.APP_STATE, + query: true, + } + ); + + // syncs `_g` portion of url with query services + const { stop: stopSyncingGlobalStateWithUrl } = syncQueryStateWithUrl( + data.query, + stateContainer.kbnUrlStateStorage + ); + return () => { + stopSyncingQueryAppStateWithStateContainer(); + stopSyncingGlobalStateWithUrl(); + }; + }, [ + appStateContainer, + config, + data.query, + data.search.session, + getPreviousAppState, + indexPattern.id, + filterManager, + services.indexPatterns, + stateContainer, + ]); + + useEffect(() => { + const unsubscribe = stateContainer.appStateContainer.subscribe(async (nextState) => { + // NOTE: this is also called when navigating from discover app to context app + if (nextState.index && state.index !== nextState.index) { + const nextIndexPattern = await loadIndexPattern( + nextState.index, + services.indexPatterns, + config + ); + + if (nextIndexPattern) { + setIndexPattern(nextIndexPattern.loaded); + } + } + setState(nextState); + }); + return () => unsubscribe(); + }, [config, services.indexPatterns, state.index, stateContainer.appStateContainer, setState]); + + const resetSavedSearch = useCallback( + async (id?: string) => { + const newSavedSearch = await services.getSavedSearchById(id); + newSavedSearch.searchSource.setField('index', indexPattern); + const newAppState = getStateDefaults({ + config, + data, + savedSearch: newSavedSearch, + }); + await stateContainer.replaceUrlAppState(newAppState); + setState(newAppState); + if (savedSearch.id !== newSavedSearch.id) { + setSavedSearch(newSavedSearch); + } + }, + [services, indexPattern, config, data, stateContainer, savedSearch.id] + ); + + return { + state, + setState, + stateContainer, + indexPattern, + searchSource, + savedSearch, + resetSavedSearch, + }; +} diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts new file mode 100644 index 00000000000000..5976c8fea5ea4f --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { Subject } from 'rxjs'; +import { renderHook } from '@testing-library/react-hooks'; +import { createSearchSessionMock } from '../../../../__mocks__/search_session'; +import { discoverServiceMock } from '../../../../__mocks__/services'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; +import { useSavedSearch } from './use_saved_search'; +import { AppState, getState } from './discover_state'; +import { uiSettingsMock } from '../../../../__mocks__/ui_settings'; +import { useDiscoverState } from './use_discover_state'; + +describe('test useSavedSearch', () => { + test('useSavedSearch return is valid', async () => { + const { history, searchSessionManager } = createSearchSessionMock(); + const stateContainer = getState({ + getStateDefaults: () => ({ index: 'the-index-pattern-id' }), + history, + uiSettings: uiSettingsMock, + }); + + const { result } = renderHook(() => { + return useSavedSearch({ + indexPattern: indexPatternMock, + savedSearch: savedSearchMock, + searchSessionManager, + searchSource: savedSearchMock.searchSource.createCopy(), + services: discoverServiceMock, + state: {} as AppState, + stateContainer, + useNewFieldsApi: true, + }); + }); + + expect(result.current.refetch$).toBeInstanceOf(Subject); + expect(result.current.data$.value).toMatchInlineSnapshot(` + Object { + "state": "loading", + } + `); + }); + test('refetch$ triggers a search', async () => { + const { history, searchSessionManager } = createSearchSessionMock(); + const stateContainer = getState({ + getStateDefaults: () => ({ index: 'the-index-pattern-id' }), + history, + uiSettings: uiSettingsMock, + }); + + discoverServiceMock.data.query.timefilter.timefilter.getTime = jest.fn(() => { + return { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' }; + }); + + const { result: resultState } = renderHook(() => { + return useDiscoverState({ + services: discoverServiceMock, + history, + initialIndexPattern: indexPatternMock, + initialSavedSearch: savedSearchMock, + }); + }); + + const { result, waitForValueToChange } = renderHook(() => { + return useSavedSearch({ + indexPattern: indexPatternMock, + savedSearch: savedSearchMock, + searchSessionManager, + searchSource: resultState.current.searchSource, + services: discoverServiceMock, + state: {} as AppState, + stateContainer, + useNewFieldsApi: true, + }); + }); + + result.current.refetch$.next(); + + await waitForValueToChange(() => { + return result.current.data$.value.state === 'complete'; + }); + + expect(result.current.data$.value.hits).toBe(0); + expect(result.current.data$.value.rows).toEqual([]); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts new file mode 100644 index 00000000000000..2b0d9517248694 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts @@ -0,0 +1,337 @@ +/* + * 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 { useEffect, useRef, useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { merge, Subject, BehaviorSubject } from 'rxjs'; +import { debounceTime, tap, filter } from 'rxjs/operators'; +import { isEqual } from 'lodash'; +import { DiscoverServices } from '../../../../build_services'; +import { DiscoverSearchSessionManager } from './discover_search_session'; +import { + IndexPattern, + isCompleteResponse, + SearchSource, + tabifyAggResponse, +} from '../../../../../../data/common'; +import { SavedSearch } from '../../../../saved_searches'; +import { AppState, GetStateReturn } from './discover_state'; +import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; +import { RequestAdapter } from '../../../../../../inspector/public'; +import { AutoRefreshDoneFn, search } from '../../../../../../data/public'; +import { calcFieldCounts } from '../utils/calc_field_counts'; +import { SEARCH_ON_PAGE_LOAD_SETTING } from '../../../../../common'; +import { validateTimeRange } from '../utils/validate_time_range'; +import { updateSearchSource } from '../utils/update_search_source'; +import { SortOrder } from '../../../../saved_searches/types'; +import { getDimensions, getChartAggConfigs } from '../utils'; +import { buildPointSeriesData, Chart } from '../components/chart/point_series'; +import { TimechartBucketInterval } from '../components/timechart_header/timechart_header'; +import { useSingleton } from '../utils/use_singleton'; +import { FetchStatus } from '../../../types'; + +export type SavedSearchDataSubject = BehaviorSubject; +export type SavedSearchRefetchSubject = Subject; + +export interface UseSavedSearch { + refetch$: SavedSearchRefetchSubject; + data$: SavedSearchDataSubject; +} + +export type SavedSearchRefetchMsg = 'reset' | undefined; + +export interface SavedSearchDataMessage { + bucketInterval?: TimechartBucketInterval; + chartData?: Chart; + fetchCounter?: number; + fetchError?: Error; + fieldCounts?: Record; + hits?: number; + inspectorAdapters?: { requests: RequestAdapter }; + rows?: ElasticSearchHit[]; + state: FetchStatus; +} + +/** + * This hook return 2 observables, refetch$ allows to trigger data fetching, data$ to subscribe + * to the data fetching + * @param indexPattern + * @param savedSearch + * @param searchSessionManager + * @param searchSource + * @param services + * @param state + * @param stateContainer + * @param useNewFieldsApi + */ +export const useSavedSearch = ({ + indexPattern, + savedSearch, + searchSessionManager, + searchSource, + services, + state, + stateContainer, + useNewFieldsApi, +}: { + indexPattern: IndexPattern; + savedSearch: SavedSearch; + searchSessionManager: DiscoverSearchSessionManager; + searchSource: SearchSource; + services: DiscoverServices; + state: AppState; + stateContainer: GetStateReturn; + useNewFieldsApi: boolean; +}): UseSavedSearch => { + const { data, filterManager, uiSettings } = services; + const timefilter = data.query.timefilter.timefilter; + + const initFetchState: FetchStatus = useMemo(() => { + // A saved search is created on every page load, so we check the ID to see if we're loading a + // previously saved search or if it is just transient + const shouldSearchOnPageLoad = + uiSettings.get(SEARCH_ON_PAGE_LOAD_SETTING) || + savedSearch.id !== undefined || + timefilter.getRefreshInterval().pause === false || + searchSessionManager.hasSearchSessionIdInURL(); + return shouldSearchOnPageLoad ? FetchStatus.LOADING : FetchStatus.UNINITIALIZED; + }, [uiSettings, savedSearch.id, searchSessionManager, timefilter]); + + /** + * The observable the UI (aka React component) subscribes to get notified about + * the changes in the data fetching process (high level: fetching started, data was received) + */ + const data$: SavedSearchDataSubject = useSingleton( + () => + new BehaviorSubject({ + state: initFetchState, + }) + ); + /** + * The observable to trigger data fetching in UI + * By refetch$.next('reset') rows and fieldcounts are reset to allow e.g. editing of runtime fields + * to be processed correctly + */ + const refetch$ = useSingleton(() => new Subject()); + + /** + * Values that shouldn't trigger re-rendering when changed + */ + const refs = useRef<{ + abortController?: AbortController; + /** + * used to compare a new state against an old one, to evaluate if data needs to be fetched + */ + appState: AppState; + /** + * handler emitted by `timefilter.getAutoRefreshFetch$()` + * to notify when data completed loading and to start a new autorefresh loop + */ + autoRefreshDoneCb?: AutoRefreshDoneFn; + fetchCounter: number; + /** + * needed to right auto refresh behavior, a new auto refresh shouldnt be triggered when + * loading is still ongoing + */ + fetchStatus: FetchStatus; + /** + * needed for merging new with old field counts, high likely legacy, but kept this behavior + * because not 100% sure in this case + */ + fieldCounts: Record; + }>({ + appState: state, + fetchCounter: 0, + fieldCounts: {}, + fetchStatus: initFetchState, + }); + + const fetchAll = useCallback( + (reset = false) => { + if (!validateTimeRange(timefilter.getTime(), services.toastNotifications)) { + return Promise.reject(); + } + const inspectorAdapters = { requests: new RequestAdapter() }; + + if (refs.current.abortController) refs.current.abortController.abort(); + refs.current.abortController = new AbortController(); + const sessionId = searchSessionManager.getNextSearchSessionId(); + + // Let the UI know, data fetching started + const loadingMessage: SavedSearchDataMessage = { + state: FetchStatus.LOADING, + fetchCounter: ++refs.current.fetchCounter, + }; + + if (reset) { + // when runtime field was added, changed, deleted, index pattern was switched + loadingMessage.rows = []; + loadingMessage.fieldCounts = {}; + loadingMessage.chartData = undefined; + loadingMessage.bucketInterval = undefined; + } + data$.next(loadingMessage); + refs.current.fetchStatus = loadingMessage.state; + + const { sort } = stateContainer.appStateContainer.getState(); + updateSearchSource(searchSource, false, { + indexPattern, + services, + sort: sort as SortOrder[], + useNewFieldsApi, + }); + const chartAggConfigs = + indexPattern.timeFieldName && !state.hideChart && state.interval + ? getChartAggConfigs(searchSource, state.interval, data) + : undefined; + + if (!chartAggConfigs) { + searchSource.removeField('aggs'); + } else { + searchSource.setField('aggs', chartAggConfigs.toDsl()); + } + + const searchSourceFetch$ = searchSource.fetch$({ + abortSignal: refs.current.abortController.signal, + sessionId, + inspector: { + adapter: inspectorAdapters.requests, + title: i18n.translate('discover.inspectorRequestDataTitle', { + defaultMessage: 'data', + }), + description: i18n.translate('discover.inspectorRequestDescriptionDocument', { + defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.', + }), + }, + }); + + searchSourceFetch$.pipe(filter((res) => isCompleteResponse(res))).subscribe( + (res) => { + const documents = res.rawResponse.hits.hits; + + const message: SavedSearchDataMessage = { + state: FetchStatus.COMPLETE, + rows: documents, + inspectorAdapters, + fieldCounts: calcFieldCounts( + reset ? {} : refs.current.fieldCounts, + documents, + indexPattern + ), + hits: res.rawResponse.hits.total as number, + }; + + if (chartAggConfigs) { + const bucketAggConfig = chartAggConfigs!.aggs[1]; + const tabifiedData = tabifyAggResponse(chartAggConfigs, res.rawResponse); + const dimensions = getDimensions(chartAggConfigs, data); + if (dimensions) { + if (bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig)) { + message.bucketInterval = bucketAggConfig.buckets?.getInterval(); + } + message.chartData = buildPointSeriesData(tabifiedData, dimensions); + } + } + refs.current.fieldCounts = message.fieldCounts!; + refs.current.fetchStatus = message.state; + data$.next(message); + }, + (error) => { + if (error instanceof Error && error.name === 'AbortError') return; + data.search.showError(error); + refs.current.fetchStatus = FetchStatus.ERROR; + data$.next({ + state: FetchStatus.ERROR, + inspectorAdapters, + fetchError: error, + }); + }, + () => { + refs.current.autoRefreshDoneCb?.(); + refs.current.autoRefreshDoneCb = undefined; + } + ); + }, + [ + timefilter, + services, + stateContainer.appStateContainer, + searchSource, + indexPattern, + useNewFieldsApi, + state.hideChart, + state.interval, + data, + searchSessionManager, + data$, + ] + ); + + /** + * This part takes care of triggering the data fetching by creating and subscribing + * to an observable of various possible changes in state + */ + useEffect(() => { + const fetch$ = merge( + refetch$, + filterManager.getFetches$(), + timefilter.getFetch$(), + timefilter.getAutoRefreshFetch$().pipe( + tap((done) => { + refs.current.autoRefreshDoneCb = done; + }), + filter(() => refs.current.fetchStatus !== FetchStatus.LOADING) + ), + data.query.queryString.getUpdates$(), + searchSessionManager.newSearchSessionIdFromURL$.pipe(filter((sessionId) => !!sessionId)) + ).pipe(debounceTime(100)); + + const subscription = fetch$.subscribe((val) => { + fetchAll(val === 'reset'); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [ + data.query.queryString, + filterManager, + refetch$, + searchSessionManager.newSearchSessionIdFromURL$, + timefilter, + fetchAll, + ]); + + /** + * Track state changes that should trigger a fetch + */ + useEffect(() => { + const prevAppState = refs.current.appState; + + // chart was hidden, now it should be displayed, so data is needed + const chartDisplayChanged = state.hideChart !== prevAppState.hideChart && !state.hideChart; + const chartIntervalChanged = state.interval !== prevAppState.interval; + const docTableSortChanged = !isEqual(state.sort, prevAppState.sort); + const indexPatternChanged = !isEqual(state.index, prevAppState.index); + + refs.current.appState = state; + if (chartDisplayChanged || chartIntervalChanged || docTableSortChanged || indexPatternChanged) { + refetch$.next(indexPatternChanged ? 'reset' : undefined); + } + }, [refetch$, state.interval, state.sort, state]); + + useEffect(() => { + if (initFetchState === FetchStatus.LOADING) { + refetch$.next(); + } + }, [initFetchState, refetch$]); + + return { + refetch$, + data$, + }; +}; diff --git a/src/plugins/discover/public/application/apps/main/services/use_search_session.test.ts b/src/plugins/discover/public/application/apps/main/services/use_search_session.test.ts new file mode 100644 index 00000000000000..8aa6e0764d8ec7 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/services/use_search_session.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { useSearchSession } from './use_search_session'; +import { renderHook } from '@testing-library/react-hooks'; +import { createSearchSessionMock } from '../../../../__mocks__/search_session'; +import { discoverServiceMock } from '../../../../__mocks__/services'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { getState } from './discover_state'; +import { uiSettingsMock } from '../../../../__mocks__/ui_settings'; + +describe('test useSearchSession', () => { + test('getting the next session id', async () => { + const { history } = createSearchSessionMock(); + const stateContainer = getState({ + getStateDefaults: () => ({ index: 'test' }), + history, + uiSettings: uiSettingsMock, + }); + + const nextId = 'id'; + discoverServiceMock.data.search.session.start = jest.fn(() => nextId); + + const { result } = renderHook(() => { + return useSearchSession({ + services: discoverServiceMock, + history, + stateContainer, + savedSearch: savedSearchMock, + }); + }); + expect(result.current.getNextSearchSessionId()).toBe('id'); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/services/use_search_session.ts b/src/plugins/discover/public/application/apps/main/services/use_search_session.ts new file mode 100644 index 00000000000000..e37e8f6f338394 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/services/use_search_session.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { useMemo, useEffect } from 'react'; +import { History } from 'history'; +import { DiscoverSearchSessionManager } from './discover_search_session'; +import { createSearchSessionRestorationDataProvider, GetStateReturn } from './discover_state'; +import { noSearchSessionStorageCapabilityMessage } from '../../../../../../data/public'; +import { DiscoverServices } from '../../../../build_services'; +import { SavedSearch } from '../../../../saved_searches'; + +export function useSearchSession({ + services, + history, + stateContainer, + savedSearch, +}: { + services: DiscoverServices; + stateContainer: GetStateReturn; + history: History; + savedSearch: SavedSearch; +}) { + const { data, capabilities } = services; + /** + * Search session logic + */ + const searchSessionManager = useMemo( + () => + new DiscoverSearchSessionManager({ + history, + session: data.search.session, + }), + [data.search.session, history] + ); + + useEffect(() => { + data.search.session.enableStorage( + createSearchSessionRestorationDataProvider({ + appStateContainer: stateContainer.appStateContainer, + data, + getSavedSearch: () => savedSearch, + }), + { + isDisabled: () => + capabilities.discover.storeSearchSession + ? { disabled: false } + : { + disabled: true, + reasonText: noSearchSessionStorageCapabilityMessage, + }, + } + ); + }, [ + capabilities.discover.storeSearchSession, + data, + savedSearch, + stateContainer.appStateContainer, + ]); + + return searchSessionManager; +} diff --git a/src/plugins/discover/public/application/apps/main/services/use_url.test.ts b/src/plugins/discover/public/application/apps/main/services/use_url.test.ts new file mode 100644 index 00000000000000..740ca2bd140d78 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/services/use_url.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { renderHook } from '@testing-library/react-hooks'; +import { createSearchSessionMock } from '../../../../__mocks__/search_session'; +import { useUrl } from './use_url'; + +describe('test useUrl', () => { + test('resetSavedSearch is triggered once path it changed to /', () => { + const { history } = createSearchSessionMock(); + history.push('/view'); + const props = { + history, + resetSavedSearch: jest.fn(), + }; + renderHook(() => useUrl(props)); + history.push('/new'); + expect(props.resetSavedSearch).toHaveBeenCalledTimes(0); + + history.push('/'); + expect(props.resetSavedSearch).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/services/use_url.ts b/src/plugins/discover/public/application/apps/main/services/use_url.ts new file mode 100644 index 00000000000000..bc5554c35b36f7 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/services/use_url.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { useEffect } from 'react'; +import { History } from 'history'; +export function useUrl({ + history, + resetSavedSearch, +}: { + history: History; + resetSavedSearch: (val?: string) => void; +}) { + /** + * Url / Routing logic + */ + useEffect(() => { + // this listener is waiting for such a path http://localhost:5601/app/discover#/ + // which could be set through pressing "New" button in top nav or go to "Discover" plugin from the sidebar + // to reload the page in a right way + const unlistenHistoryBasePath = history.listen(({ pathname, search, hash }) => { + if (!search && !hash && pathname === '/') { + resetSavedSearch(); + } + }); + return () => unlistenHistoryBasePath(); + }, [history, resetSavedSearch]); +} diff --git a/src/plugins/discover/public/application/helpers/calc_field_counts.test.ts b/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.test.ts similarity index 83% rename from src/plugins/discover/public/application/helpers/calc_field_counts.test.ts rename to src/plugins/discover/public/application/apps/main/utils/calc_field_counts.test.ts index cf12b336c5b580..bdea6611d3b7eb 100644 --- a/src/plugins/discover/public/application/helpers/calc_field_counts.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.test.ts @@ -7,14 +7,15 @@ */ import { calcFieldCounts } from './calc_field_counts'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; +import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; describe('calcFieldCounts', () => { test('returns valid field count data', async () => { - const rows = [ + const rows = ([ { _id: 1, _source: { message: 'test1', bytes: 20 } }, { _id: 2, _source: { name: 'test2', extension: 'jpg' } }, - ]; + ] as unknown) as ElasticSearchHit[]; const result = calcFieldCounts({}, rows, indexPatternMock); expect(result).toMatchInlineSnapshot(` Object { @@ -28,10 +29,10 @@ describe('calcFieldCounts', () => { `); }); test('updates field count data', async () => { - const rows = [ + const rows = ([ { _id: 1, _source: { message: 'test1', bytes: 20 } }, { _id: 2, _source: { name: 'test2', extension: 'jpg' } }, - ]; + ] as unknown) as ElasticSearchHit[]; const result = calcFieldCounts({ message: 2 }, rows, indexPatternMock); expect(result).toMatchInlineSnapshot(` Object { diff --git a/src/plugins/discover/public/application/helpers/calc_field_counts.ts b/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts similarity index 84% rename from src/plugins/discover/public/application/helpers/calc_field_counts.ts rename to src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts index edeaa0b9bce52f..43c9a57a70f221 100644 --- a/src/plugins/discover/public/application/helpers/calc_field_counts.ts +++ b/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { IndexPattern } from '../../kibana_services'; +import { IndexPattern } from '../../../../kibana_services'; +import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; /** * This function is recording stats of the available fields, for usage in sidebar and sharing @@ -14,7 +15,7 @@ import { IndexPattern } from '../../kibana_services'; */ export function calcFieldCounts( counts = {} as Record, - rows: Array>, + rows: ElasticSearchHit[], indexPattern: IndexPattern ) { for (const hit of rows) { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_config.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_config.test.ts new file mode 100644 index 00000000000000..19820508746508 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_config.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { indexPatternWithTimefieldMock } from '../../../../__mocks__/index_pattern_with_timefield'; +import { SearchSource } from '../../../../../../data/public'; +import { dataPluginMock } from '../../../../../../data/public/mocks'; +import { getChartAggConfigs } from './get_chart_agg_configs'; + +describe('getChartAggConfigs', () => { + test('is working', () => { + const indexPattern = indexPatternWithTimefieldMock; + const setField = jest.fn(); + const searchSource = ({ + setField, + getField: (name: string) => { + if (name === 'index') { + return indexPattern; + } + }, + removeField: jest.fn(), + } as unknown) as SearchSource; + + const dataMock = dataPluginMock.createStartContract(); + + const aggsConfig = getChartAggConfigs(searchSource, 'auto', dataMock); + + expect(aggsConfig!.aggs).toMatchInlineSnapshot(` + Array [ + Object { + "enabled": true, + "id": "1", + "params": Object {}, + "schema": "metric", + "type": "count", + }, + Object { + "enabled": true, + "id": "2", + "params": Object { + "drop_partials": false, + "extended_bounds": Object {}, + "field": "timestamp", + "interval": "auto", + "min_doc_count": 1, + "scaleMetricValues": false, + "useNormalizedEsInterval": true, + "used_interval": "0ms", + }, + "schema": "segment", + "type": "date_histogram", + }, + ] + `); + }); +}); diff --git a/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.ts b/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_configs.ts similarity index 60% rename from src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.ts rename to src/plugins/discover/public/application/apps/main/utils/get_chart_agg_configs.ts index c5fb366f81c8cd..2665254027fd91 100644 --- a/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_configs.ts @@ -5,27 +5,19 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { IndexPattern, SearchSource } from '../../../../../data/common'; -import { DataPublicPluginStart } from '../../../../../data/public'; +import { SearchSource } from '../../../../../../data/common'; +import { DataPublicPluginStart } from '../../../../../../data/public'; /** * Helper function to apply or remove aggregations to a given search source used for gaining data * for Discover's histogram vis */ -export function applyAggsToSearchSource( - enabled: boolean, +export function getChartAggConfigs( searchSource: SearchSource, histogramInterval: string, - indexPattern: IndexPattern, data: DataPublicPluginStart ) { - if (!enabled) { - if (searchSource.getField('aggs')) { - // clean up fields in case it was set before - searchSource.removeField('aggs'); - } - return; - } + const indexPattern = searchSource.getField('index')!; const visStateAggs = [ { type: 'count', @@ -41,10 +33,5 @@ export function applyAggsToSearchSource( }, }, ]; - const chartAggConfigs = data.search.aggs.createAggConfigs(indexPattern, visStateAggs); - - searchSource.setField('aggs', function () { - return chartAggConfigs.toDsl(); - }); - return chartAggConfigs; + return data.search.aggs.createAggConfigs(indexPattern, visStateAggs); } diff --git a/src/plugins/discover/public/application/apps/main/utils/get_dimensions.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_dimensions.test.ts new file mode 100644 index 00000000000000..443fc634ba5430 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/utils/get_dimensions.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { dataPluginMock } from '../../../../../../data/public/mocks'; + +import { getDimensions } from './get_dimensions'; +import { indexPatternWithTimefieldMock } from '../../../../__mocks__/index_pattern_with_timefield'; +import { SearchSource, calculateBounds } from '../../../../../../data/common'; +import { getChartAggConfigs } from './get_chart_agg_configs'; + +test('getDimensions', () => { + const indexPattern = indexPatternWithTimefieldMock; + const setField = jest.fn(); + const searchSource = ({ + setField, + removeField: jest.fn(), + getField: (name: string) => { + if (name === 'index') { + return indexPattern; + } + }, + } as unknown) as SearchSource; + + const dataMock = dataPluginMock.createStartContract(); + dataMock.query.timefilter.timefilter.getTime = () => { + return { from: '1991-03-29T08:04:00.694Z', to: '2021-03-29T07:04:00.695Z' }; + }; + dataMock.query.timefilter.timefilter.calculateBounds = (timeRange) => { + return calculateBounds(timeRange); + }; + + const aggsConfig = getChartAggConfigs(searchSource, 'auto', dataMock); + const actual = getDimensions(aggsConfig!, dataMock); + expect(actual).toMatchInlineSnapshot(` + Object { + "x": Object { + "accessor": 0, + "format": Object { + "id": "date", + "params": Object { + "pattern": "HH:mm:ss.SSS", + }, + }, + "label": "timestamp per 0 milliseconds", + "params": Object { + "bounds": Object { + "max": "2021-03-29T07:04:00.695Z", + "min": "1991-03-29T08:04:00.694Z", + }, + "date": true, + "format": "HH:mm:ss.SSS", + "interval": "P0D", + "intervalESUnit": "ms", + "intervalESValue": 0, + }, + }, + "y": Object { + "accessor": 1, + "format": Object { + "id": "number", + }, + "label": "Count", + }, + } + `); +}); diff --git a/src/plugins/discover/public/application/components/histogram/get_dimensions.ts b/src/plugins/discover/public/application/apps/main/utils/get_dimensions.ts similarity index 72% rename from src/plugins/discover/public/application/components/histogram/get_dimensions.ts rename to src/plugins/discover/public/application/apps/main/utils/get_dimensions.ts index 6743c1c8431b9b..40b378f1c16983 100644 --- a/src/plugins/discover/public/application/components/histogram/get_dimensions.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_dimensions.ts @@ -7,10 +7,14 @@ */ import moment from 'moment'; import dateMath from '@elastic/datemath'; -import { IAggConfigs, TimeRangeBounds } from '../../../../../data/common'; -import { DataPublicPluginStart, search } from '../../../../../data/public'; +import { IAggConfigs } from '../../../../../../data/common'; +import { DataPublicPluginStart, search } from '../../../../../../data/public'; +import { Dimensions, HistogramParamsBounds } from '../components/chart/point_series'; -export function getDimensions(aggs: IAggConfigs, data: DataPublicPluginStart) { +export function getDimensions( + aggs: IAggConfigs, + data: DataPublicPluginStart +): Dimensions | undefined { const [metric, agg] = aggs.aggs; const { from, to } = data.query.timefilter.timefilter.getTime(); agg.params.timeRange = { @@ -18,16 +22,16 @@ export function getDimensions(aggs: IAggConfigs, data: DataPublicPluginStart) { to: dateMath.parse(to, { roundUp: true }), }; const bounds = agg.params.timeRange - ? data.query.timefilter.timefilter.calculateBounds(agg.params.timeRange) + ? (data.query.timefilter.timefilter.calculateBounds( + agg.params.timeRange + ) as HistogramParamsBounds) : null; const buckets = search.aggs.isDateHistogramBucketAggConfig(agg) ? agg.buckets : undefined; - if (!buckets) { + if (!buckets || !bounds) { return; } - buckets.setBounds(bounds as TimeRangeBounds); - const { esUnit, esValue } = buckets.getInterval(); return { x: { @@ -40,7 +44,7 @@ export function getDimensions(aggs: IAggConfigs, data: DataPublicPluginStart) { intervalESValue: esValue, intervalESUnit: esUnit, format: buckets.getScaledDateFormat(), - bounds: buckets.getBounds(), + bounds, }, }, y: { diff --git a/src/plugins/discover/public/application/helpers/get_result_state.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_result_state.test.ts similarity index 71% rename from src/plugins/discover/public/application/helpers/get_result_state.test.ts rename to src/plugins/discover/public/application/apps/main/utils/get_result_state.test.ts index 98e2b854ca5abe..512bc4240352c7 100644 --- a/src/plugins/discover/public/application/helpers/get_result_state.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_result_state.test.ts @@ -6,40 +6,40 @@ * Side Public License, v 1. */ import { getResultState, resultStatuses } from './get_result_state'; -import { fetchStatuses } from '../components/constants'; -import { ElasticSearchHit } from '../doc_views/doc_views_types'; +import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; +import { FetchStatus } from '../../../types'; describe('getResultState', () => { test('fetching uninitialized', () => { - const actual = getResultState(fetchStatuses.UNINITIALIZED, []); + const actual = getResultState(FetchStatus.UNINITIALIZED, []); expect(actual).toBe(resultStatuses.UNINITIALIZED); }); test('fetching complete with no records', () => { - const actual = getResultState(fetchStatuses.COMPLETE, []); + const actual = getResultState(FetchStatus.COMPLETE, []); expect(actual).toBe(resultStatuses.NO_RESULTS); }); test('fetching ongoing aka loading', () => { - const actual = getResultState(fetchStatuses.LOADING, []); + const actual = getResultState(FetchStatus.LOADING, []); expect(actual).toBe(resultStatuses.LOADING); }); test('fetching ready', () => { const record = ({ _id: 123 } as unknown) as ElasticSearchHit; - const actual = getResultState(fetchStatuses.COMPLETE, [record]); + const actual = getResultState(FetchStatus.COMPLETE, [record]); expect(actual).toBe(resultStatuses.READY); }); test('re-fetching after already data is available', () => { const record = ({ _id: 123 } as unknown) as ElasticSearchHit; - const actual = getResultState(fetchStatuses.LOADING, [record]); + const actual = getResultState(FetchStatus.LOADING, [record]); expect(actual).toBe(resultStatuses.READY); }); test('after a fetch error when data was successfully fetched before ', () => { const record = ({ _id: 123 } as unknown) as ElasticSearchHit; - const actual = getResultState(fetchStatuses.ERROR, [record]); + const actual = getResultState(FetchStatus.ERROR, [record]); expect(actual).toBe(resultStatuses.READY); }); }); diff --git a/src/plugins/discover/public/application/helpers/get_result_state.ts b/src/plugins/discover/public/application/apps/main/utils/get_result_state.ts similarity index 73% rename from src/plugins/discover/public/application/helpers/get_result_state.ts rename to src/plugins/discover/public/application/apps/main/utils/get_result_state.ts index 6f69832f369fd1..80d25566ea5784 100644 --- a/src/plugins/discover/public/application/helpers/get_result_state.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_result_state.ts @@ -5,8 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { ElasticSearchHit } from '../doc_views/doc_views_types'; -import { fetchStatuses } from '../components/constants'; +import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; +import { FetchStatus } from '../../../types'; export const resultStatuses = { UNINITIALIZED: 'uninitialized', @@ -19,13 +19,13 @@ export const resultStatuses = { * Returns the current state of the result, depends on fetchStatus and the given fetched rows * Determines what is displayed in Discover main view (loading view, data view, empty data view, ...) */ -export function getResultState(fetchStatus: string, rows: ElasticSearchHit[]) { - if (fetchStatus === fetchStatuses.UNINITIALIZED) { +export function getResultState(fetchStatus: FetchStatus, rows: ElasticSearchHit[]) { + if (fetchStatus === FetchStatus.UNINITIALIZED) { return resultStatuses.UNINITIALIZED; } const rowsEmpty = !Array.isArray(rows) || rows.length === 0; - if (rowsEmpty && fetchStatus === fetchStatuses.LOADING) return resultStatuses.LOADING; + if (rowsEmpty && fetchStatus === FetchStatus.LOADING) return resultStatuses.LOADING; else if (!rowsEmpty) return resultStatuses.READY; else return resultStatuses.NO_RESULTS; } diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts similarity index 96% rename from src/plugins/discover/public/application/helpers/get_sharing_data.test.ts rename to src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts index 6a51c085ebbc98..ffad3d955c1a3c 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts @@ -8,9 +8,9 @@ import { Capabilities, IUiSettingsClient } from 'kibana/public'; import { IndexPattern } from 'src/plugins/data/public'; -import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; -import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { createSearchSourceMock } from '../../../../../../data/common/search/search_source/mocks'; +import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; import { getSharingData, showPublicUrlSwitch } from './get_sharing_data'; describe('getSharingData', () => { diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.ts b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts similarity index 87% rename from src/plugins/discover/public/application/helpers/get_sharing_data.ts rename to src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts index 47be4b80371522..00473956c57e3a 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts @@ -7,11 +7,11 @@ */ import type { Capabilities, IUiSettingsClient } from 'kibana/public'; -import { ISearchSource } from '../../../../data/common'; -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'; +import { ISearchSource } from '../../../../../../data/common'; +import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; +import type { SavedSearch, SortOrder } from '../../../../saved_searches/types'; +import { AppState } from '../services/discover_state'; +import { getSortForSearchSource } from '../../../angular/doc_table'; /** * Preparing data to share the current state as link or CSV/Report diff --git a/src/plugins/discover/public/application/helpers/get_state_defaults.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts similarity index 74% rename from src/plugins/discover/public/application/helpers/get_state_defaults.test.ts rename to src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts index 7ce5b9286c775c..17f1802a47327a 100644 --- a/src/plugins/discover/public/application/helpers/get_state_defaults.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts @@ -7,20 +7,19 @@ */ import { getStateDefaults } from './get_state_defaults'; -import { createSearchSourceMock, dataPluginMock } from '../../../../data/public/mocks'; -import { uiSettingsMock } from '../../__mocks__/ui_settings'; -import { indexPatternWithTimefieldMock } from '../../__mocks__/index_pattern_with_timefield'; -import { savedSearchMock } from '../../__mocks__/saved_search'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { createSearchSourceMock, dataPluginMock } from '../../../../../../data/public/mocks'; +import { uiSettingsMock } from '../../../../__mocks__/ui_settings'; +import { indexPatternWithTimefieldMock } from '../../../../__mocks__/index_pattern_with_timefield'; +import { savedSearchMock } from '../../../../__mocks__/saved_search'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; describe('getStateDefaults', () => { test('index pattern with timefield', () => { + savedSearchMock.searchSource = createSearchSourceMock({ index: indexPatternWithTimefieldMock }); const actual = getStateDefaults({ config: uiSettingsMock, data: dataPluginMock.createStartContract(), - indexPattern: indexPatternWithTimefieldMock, savedSearch: savedSearchMock, - searchSource: createSearchSourceMock({ index: indexPatternWithTimefieldMock }), }); expect(actual).toMatchInlineSnapshot(` Object { @@ -42,12 +41,12 @@ describe('getStateDefaults', () => { }); test('index pattern without timefield', () => { + savedSearchMock.searchSource = createSearchSourceMock({ index: indexPatternMock }); + const actual = getStateDefaults({ config: uiSettingsMock, data: dataPluginMock.createStartContract(), - indexPattern: indexPatternMock, savedSearch: savedSearchMock, - searchSource: createSearchSourceMock({ index: indexPatternMock }), }); expect(actual).toMatchInlineSnapshot(` Object { diff --git a/src/plugins/discover/public/application/helpers/get_state_defaults.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts similarity index 71% rename from src/plugins/discover/public/application/helpers/get_state_defaults.ts rename to src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts index 3e012a1f85fd6b..426272fa8ce1cf 100644 --- a/src/plugins/discover/public/application/helpers/get_state_defaults.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts @@ -8,14 +8,13 @@ import { cloneDeep } from 'lodash'; import { IUiSettingsClient } from 'kibana/public'; -import { DEFAULT_COLUMNS_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; -import { getSortArray } from '../angular/doc_table'; -import { getDefaultSort } from '../angular/doc_table/lib/get_default_sort'; -import { SavedSearch } from '../../saved_searches'; -import { SearchSource } from '../../../../data/common/search/search_source'; -import { DataPublicPluginStart, IndexPattern } from '../../../../data/public'; +import { DEFAULT_COLUMNS_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; +import { getSortArray } from '../../../angular/doc_table'; +import { getDefaultSort } from '../../../angular/doc_table/lib/get_default_sort'; +import { SavedSearch } from '../../../../saved_searches'; +import { DataPublicPluginStart } from '../../../../../../data/public'; -import { AppState } from '../angular/discover_state'; +import { AppState } from '../services/discover_state'; function getDefaultColumns(savedSearch: SavedSearch, config: IUiSettingsClient) { if (savedSearch.columns && savedSearch.columns.length > 0) { @@ -27,18 +26,16 @@ function getDefaultColumns(savedSearch: SavedSearch, config: IUiSettingsClient) export function getStateDefaults({ config, data, - indexPattern, savedSearch, - searchSource, }: { config: IUiSettingsClient; data: DataPublicPluginStart; - indexPattern: IndexPattern; savedSearch: SavedSearch; - searchSource: SearchSource; }) { + const searchSource = savedSearch.searchSource; + const indexPattern = savedSearch.searchSource.getField('index'); const query = searchSource.getField('query') || data.query.queryString.getDefaultQuery(); - const sort = getSortArray(savedSearch.sort, indexPattern); + const sort = getSortArray(savedSearch.sort, indexPattern!); const columns = getDefaultColumns(savedSearch, config); const defaultState = { @@ -47,7 +44,7 @@ export function getStateDefaults({ ? getDefaultSort(indexPattern, config.get(SORT_DEFAULT_ORDER_SETTING, 'desc')) : sort, columns, - index: indexPattern.id, + index: indexPattern!.id, interval: 'auto', filters: cloneDeep(searchSource.getOwnField('filter')), } as AppState; diff --git a/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.test.ts similarity index 98% rename from src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.test.ts rename to src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.test.ts index 31addd807b78dc..9f9071ed403ced 100644 --- a/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.test.ts @@ -7,7 +7,7 @@ */ import { getSwitchIndexPatternAppState } from './get_switch_index_pattern_app_state'; -import { IndexPattern } from '../../../../data/common/index_patterns'; +import { IndexPattern } from '../../../../../../data/common/index_patterns'; /** * Helper function returning an index pattern diff --git a/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.ts b/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts similarity index 90% rename from src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.ts rename to src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts index 9756fc8fd448ba..f7154b26c7ed62 100644 --- a/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { getSortArray } from '../angular/doc_table'; -import { SortPairArr } from '../angular/doc_table/lib/get_sort'; -import { IndexPattern } from '../../kibana_services'; +import { getSortArray } from '../../../angular/doc_table'; +import { SortPairArr } from '../../../angular/doc_table/lib/get_sort'; +import { IndexPattern } from '../../../../kibana_services'; /** * Helper function to remove or adapt the currently selected columns/sort to be valid with the next diff --git a/src/plugins/discover/public/application/components/histogram/index.ts b/src/plugins/discover/public/application/apps/main/utils/index.ts similarity index 84% rename from src/plugins/discover/public/application/components/histogram/index.ts rename to src/plugins/discover/public/application/apps/main/utils/index.ts index 4af75de0a029d8..a79dea526ec557 100644 --- a/src/plugins/discover/public/application/components/histogram/index.ts +++ b/src/plugins/discover/public/application/apps/main/utils/index.ts @@ -6,5 +6,5 @@ * Side Public License, v 1. */ -export { applyAggsToSearchSource } from './apply_aggs_to_search_source'; +export { getChartAggConfigs } from './get_chart_agg_configs'; export { getDimensions } from './get_dimensions'; diff --git a/src/plugins/discover/public/application/helpers/nested_fields.ts b/src/plugins/discover/public/application/apps/main/utils/nested_fields.ts similarity index 100% rename from src/plugins/discover/public/application/helpers/nested_fields.ts rename to src/plugins/discover/public/application/apps/main/utils/nested_fields.ts diff --git a/src/plugins/discover/public/application/helpers/persist_saved_search.ts b/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts similarity index 75% rename from src/plugins/discover/public/application/helpers/persist_saved_search.ts rename to src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts index f44d4650da56a2..a5e1e2bb6c2ea6 100644 --- a/src/plugins/discover/public/application/helpers/persist_saved_search.ts +++ b/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts @@ -7,12 +7,12 @@ */ import { updateSearchSource } from './update_search_source'; -import { IndexPattern } from '../../../../data/public'; -import { SavedSearch } from '../../saved_searches'; -import { AppState } from '../angular/discover_state'; -import { SortOrder } from '../../saved_searches/types'; -import { SavedObjectSaveOpts } from '../../../../saved_objects/public'; -import { DiscoverServices } from '../../build_services'; +import { IndexPattern } from '../../../../../../data/public'; +import { SavedSearch } from '../../../../saved_searches'; +import { AppState } from '../services/discover_state'; +import { SortOrder } from '../../../../saved_searches/types'; +import { SavedObjectSaveOpts } from '../../../../../../saved_objects/public'; +import { DiscoverServices } from '../../../../build_services'; /** * Helper function to update and persist the given savedSearch @@ -35,12 +35,10 @@ export async function persistSavedSearch( state: AppState; } ) { - updateSearchSource({ - persistentSearchSource: savedSearch.searchSource, + updateSearchSource(savedSearch.searchSource, true, { indexPattern, services, sort: state.sort as SortOrder[], - columns: state.columns || [], useNewFieldsApi: false, }); diff --git a/src/plugins/discover/public/application/helpers/resolve_index_pattern.test.ts b/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.test.ts similarity index 89% rename from src/plugins/discover/public/application/helpers/resolve_index_pattern.test.ts rename to src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.test.ts index 50ff068086b0ac..9ebbeafd28e10e 100644 --- a/src/plugins/discover/public/application/helpers/resolve_index_pattern.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.test.ts @@ -11,9 +11,9 @@ import { getFallbackIndexPatternId, IndexPatternSavedObject, } from './resolve_index_pattern'; -import { indexPatternsMock } from '../../__mocks__/index_patterns'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; -import { configMock } from '../../__mocks__/config'; +import { indexPatternsMock } from '../../../../__mocks__/index_patterns'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; +import { configMock } from '../../../../__mocks__/config'; describe('Resolve index pattern tests', () => { test('returns valid data for an existing index pattern', async () => { diff --git a/src/plugins/discover/public/application/helpers/resolve_index_pattern.ts b/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.ts similarity index 95% rename from src/plugins/discover/public/application/helpers/resolve_index_pattern.ts rename to src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.ts index 3e22917f3f482f..a5149ea2b3dd7b 100644 --- a/src/plugins/discover/public/application/helpers/resolve_index_pattern.ts +++ b/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.ts @@ -8,8 +8,8 @@ import { i18n } from '@kbn/i18n'; import { IUiSettingsClient, SavedObject, ToastsStart } from 'kibana/public'; -import { IndexPattern } from '../../kibana_services'; -import { IndexPatternsService, SearchSource } from '../../../../data/common'; +import { IndexPattern } from '../../../../kibana_services'; +import { IndexPatternsContract, SearchSource } from '../../../../../../data/common'; export type IndexPatternSavedObject = SavedObject & { title: string }; @@ -77,7 +77,7 @@ export function getIndexPatternId( */ export async function loadIndexPattern( id: string, - indexPatterns: IndexPatternsService, + indexPatterns: IndexPatternsContract, config: IUiSettingsClient ): Promise { const indexPatternList = ((await indexPatterns.getCache()) as unknown) as IndexPatternSavedObject[]; diff --git a/src/plugins/discover/public/application/helpers/update_search_source.test.ts b/src/plugins/discover/public/application/apps/main/utils/update_search_source.test.ts similarity index 66% rename from src/plugins/discover/public/application/helpers/update_search_source.test.ts rename to src/plugins/discover/public/application/apps/main/utils/update_search_source.test.ts index d4e52c4e7d4fe8..9deabf96732b1e 100644 --- a/src/plugins/discover/public/application/helpers/update_search_source.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/update_search_source.test.ts @@ -7,25 +7,27 @@ */ import { updateSearchSource } from './update_search_source'; -import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { createSearchSourceMock } from '../../../../../../data/common/search/search_source/mocks'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; import { IUiSettingsClient } from 'kibana/public'; -import { DiscoverServices } from '../../build_services'; -import { dataPluginMock } from '../../../../data/public/mocks'; -import { SAMPLE_SIZE_SETTING } from '../../../common'; -import { SortOrder } from '../../saved_searches/types'; +import { DiscoverServices } from '../../../../build_services'; +import { dataPluginMock } from '../../../../../../data/public/mocks'; +import { SAMPLE_SIZE_SETTING } from '../../../../../common'; +import { SortOrder } from '../../../../saved_searches/types'; describe('updateSearchSource', () => { test('updates a given search source', async () => { const persistentSearchSourceMock = createSearchSourceMock({}); const volatileSearchSourceMock = createSearchSourceMock({}); + volatileSearchSourceMock.setParent(persistentSearchSourceMock); const sampleSize = 250; - updateSearchSource({ - persistentSearchSource: persistentSearchSourceMock, - volatileSearchSource: volatileSearchSourceMock, + updateSearchSource(volatileSearchSourceMock, false, { indexPattern: indexPatternMock, services: ({ data: dataPluginMock.createStartContract(), + timefilter: { + createFilter: jest.fn(), + }, uiSettings: ({ get: (key: string) => { if (key === SAMPLE_SIZE_SETTING) { @@ -36,7 +38,6 @@ describe('updateSearchSource', () => { } as unknown) as IUiSettingsClient, } as unknown) as DiscoverServices, sort: [] as SortOrder[], - columns: [], useNewFieldsApi: false, }); expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); @@ -47,13 +48,15 @@ describe('updateSearchSource', () => { test('updates a given search source with the usage of the new fields api', async () => { const persistentSearchSourceMock = createSearchSourceMock({}); const volatileSearchSourceMock = createSearchSourceMock({}); + volatileSearchSourceMock.setParent(persistentSearchSourceMock); const sampleSize = 250; - updateSearchSource({ - persistentSearchSource: persistentSearchSourceMock, - volatileSearchSource: volatileSearchSourceMock, + updateSearchSource(volatileSearchSourceMock, false, { indexPattern: indexPatternMock, services: ({ data: dataPluginMock.createStartContract(), + timefilter: { + createFilter: jest.fn(), + }, uiSettings: ({ get: (key: string) => { if (key === SAMPLE_SIZE_SETTING) { @@ -64,41 +67,10 @@ describe('updateSearchSource', () => { } as unknown) as IUiSettingsClient, } as unknown) as DiscoverServices, sort: [] as SortOrder[], - columns: [], useNewFieldsApi: true, }); expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize); - expect(volatileSearchSourceMock.getField('fields')).toEqual([{ field: '*' }]); - expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined); - }); - - test('requests unmapped fields when the flag is provided, using the new fields api', async () => { - const persistentSearchSourceMock = createSearchSourceMock({}); - const volatileSearchSourceMock = createSearchSourceMock({}); - const sampleSize = 250; - updateSearchSource({ - persistentSearchSource: persistentSearchSourceMock, - volatileSearchSource: volatileSearchSourceMock, - indexPattern: indexPatternMock, - services: ({ - data: dataPluginMock.createStartContract(), - uiSettings: ({ - get: (key: string) => { - if (key === SAMPLE_SIZE_SETTING) { - return sampleSize; - } - return false; - }, - } as unknown) as IUiSettingsClient, - } as unknown) as DiscoverServices, - sort: [] as SortOrder[], - columns: [], - useNewFieldsApi: true, - showUnmappedFields: true, - }); - expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); - expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize); expect(volatileSearchSourceMock.getField('fields')).toEqual([ { field: '*', include_unmapped: 'true' }, ]); @@ -108,13 +80,15 @@ describe('updateSearchSource', () => { test('updates a given search source when showUnmappedFields option is set to true', async () => { const persistentSearchSourceMock = createSearchSourceMock({}); const volatileSearchSourceMock = createSearchSourceMock({}); + volatileSearchSourceMock.setParent(persistentSearchSourceMock); const sampleSize = 250; - updateSearchSource({ - persistentSearchSource: persistentSearchSourceMock, - volatileSearchSource: volatileSearchSourceMock, + updateSearchSource(volatileSearchSourceMock, false, { indexPattern: indexPatternMock, services: ({ data: dataPluginMock.createStartContract(), + timefilter: { + createFilter: jest.fn(), + }, uiSettings: ({ get: (key: string) => { if (key === SAMPLE_SIZE_SETTING) { @@ -125,9 +99,7 @@ describe('updateSearchSource', () => { } as unknown) as IUiSettingsClient, } as unknown) as DiscoverServices, sort: [] as SortOrder[], - columns: [], useNewFieldsApi: true, - showUnmappedFields: true, }); expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize); @@ -140,13 +112,15 @@ describe('updateSearchSource', () => { test('does not explicitly request fieldsFromSource when not using fields API', async () => { const persistentSearchSourceMock = createSearchSourceMock({}); const volatileSearchSourceMock = createSearchSourceMock({}); + volatileSearchSourceMock.setParent(persistentSearchSourceMock); const sampleSize = 250; - updateSearchSource({ - persistentSearchSource: persistentSearchSourceMock, - volatileSearchSource: volatileSearchSourceMock, + updateSearchSource(volatileSearchSourceMock, false, { indexPattern: indexPatternMock, services: ({ data: dataPluginMock.createStartContract(), + timefilter: { + createFilter: jest.fn(), + }, uiSettings: ({ get: (key: string) => { if (key === SAMPLE_SIZE_SETTING) { @@ -157,9 +131,7 @@ describe('updateSearchSource', () => { } as unknown) as IUiSettingsClient, } as unknown) as DiscoverServices, sort: [] as SortOrder[], - columns: [], useNewFieldsApi: false, - showUnmappedFields: false, }); expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize); diff --git a/src/plugins/discover/public/application/helpers/update_search_source.ts b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts similarity index 50% rename from src/plugins/discover/public/application/helpers/update_search_source.ts rename to src/plugins/discover/public/application/apps/main/utils/update_search_source.ts index 07529ac8cb0d68..c72e207102e814 100644 --- a/src/plugins/discover/public/application/helpers/update_search_source.ts +++ b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts @@ -6,48 +6,47 @@ * Side Public License, v 1. */ -import { getSortForSearchSource } from '../angular/doc_table'; -import { SAMPLE_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; -import { IndexPattern, ISearchSource } from '../../../../data/common/'; -import { SortOrder } from '../../saved_searches/types'; -import { DiscoverServices } from '../../build_services'; +import { getSortForSearchSource } from '../../../angular/doc_table'; +import { SAMPLE_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; +import { IndexPattern, ISearchSource } from '../../../../../../data/common'; +import { SortOrder } from '../../../../saved_searches/types'; +import { DiscoverServices } from '../../../../build_services'; +import { indexPatterns as indexPatternsUtils } from '../../../../../../data/public'; /** * Helper function to update the given searchSource before fetching/sharing/persisting */ -export function updateSearchSource({ - indexPattern, - services, - sort, - columns, - useNewFieldsApi, - showUnmappedFields, - persistentSearchSource, - volatileSearchSource, -}: { - indexPattern: IndexPattern; - services: DiscoverServices; - sort: SortOrder[]; - columns: string[]; - useNewFieldsApi: boolean; - showUnmappedFields?: boolean; - persistentSearchSource: ISearchSource; - volatileSearchSource?: ISearchSource; -}) { +export function updateSearchSource( + searchSource: ISearchSource, + persist = true, + { + indexPattern, + services, + sort, + useNewFieldsApi, + }: { + indexPattern: IndexPattern; + services: DiscoverServices; + sort: SortOrder[]; + useNewFieldsApi: boolean; + } +) { const { uiSettings, data } = services; const usedSort = getSortForSearchSource( sort, indexPattern, uiSettings.get(SORT_DEFAULT_ORDER_SETTING) ); + const usedSearchSource = persist ? searchSource : searchSource.getParent()!; - persistentSearchSource + usedSearchSource .setField('index', indexPattern) .setField('query', data.query.queryString.getQuery() || null) .setField('filter', data.query.filterManager.getFilters()); - if (volatileSearchSource) { - volatileSearchSource + if (!persist) { + searchSource + .setField('trackTotalHits', true) .setField('size', uiSettings.get(SAMPLE_SIZE_SETTING)) .setField('sort', usedSort) .setField('highlightAll', true) @@ -56,15 +55,20 @@ export function updateSearchSource({ // document-like response. .setPreferredSearchStrategyId('default'); + // this is not the default index pattern, it determines that it's not of type rollup + if (indexPatternsUtils.isDefault(indexPattern)) { + searchSource.setField('filter', data.query.timefilter.timefilter.createFilter(indexPattern)); + } + if (useNewFieldsApi) { - volatileSearchSource.removeField('fieldsFromSource'); + searchSource.removeField('fieldsFromSource'); const fields: Record = { field: '*' }; - if (showUnmappedFields) { - fields.include_unmapped = 'true'; - } - volatileSearchSource.setField('fields', [fields]); + + fields.include_unmapped = 'true'; + + searchSource.setField('fields', [fields]); } else { - volatileSearchSource.removeField('fields'); + searchSource.removeField('fields'); } } } diff --git a/src/plugins/discover/public/application/apps/main/utils/use_singleton.ts b/src/plugins/discover/public/application/apps/main/utils/use_singleton.ts new file mode 100644 index 00000000000000..bac195896caa3a --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/utils/use_singleton.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { useRef } from 'react'; + +/** + * Allows lazy initialization of a singleton + * Context: https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily + * Why not using useMemo: We're using the useMemo here also kind of as a guarantee to + * only instantiate that subject once. Unfortunately useMemo explicitly does not give + * those guarantees: + * https://reactjs.org/docs/hooks-reference.html#usememo + */ +export function useSingleton(initialize: () => T): T { + const ref = useRef(null); + + if (ref.current === null) { + ref.current = initialize(); + } + + return ref.current; +} diff --git a/src/plugins/discover/public/application/helpers/validate_time_range.test.ts b/src/plugins/discover/public/application/apps/main/utils/validate_time_range.test.ts similarity index 94% rename from src/plugins/discover/public/application/helpers/validate_time_range.test.ts rename to src/plugins/discover/public/application/apps/main/utils/validate_time_range.test.ts index cd90be434aa5e0..8d9d9adc4e8dcb 100644 --- a/src/plugins/discover/public/application/helpers/validate_time_range.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/validate_time_range.test.ts @@ -7,7 +7,7 @@ */ import { validateTimeRange } from './validate_time_range'; -import { notificationServiceMock } from '../../../../../core/public/mocks'; +import { notificationServiceMock } from '../../../../../../../core/public/mocks'; describe('Discover validateTimeRange', () => { test('validates given time ranges correctly', async () => { diff --git a/src/plugins/discover/public/application/helpers/validate_time_range.ts b/src/plugins/discover/public/application/apps/main/utils/validate_time_range.ts similarity index 100% rename from src/plugins/discover/public/application/helpers/validate_time_range.ts rename to src/plugins/discover/public/application/apps/main/utils/validate_time_range.ts diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts deleted file mode 100644 index 049c9ac177eeab..00000000000000 --- a/src/plugins/discover/public/application/components/create_discover_directive.ts +++ /dev/null @@ -1,35 +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 { Discover } from './discover'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createDiscoverDirective(reactDirective: any) { - return reactDirective(Discover, [ - ['fetch', { watchDepth: 'reference' }], - ['fetchCounter', { watchDepth: 'reference' }], - ['fetchError', { watchDepth: 'reference' }], - ['fieldCounts', { watchDepth: 'reference' }], - ['histogramData', { watchDepth: 'reference' }], - ['hits', { watchDepth: 'reference' }], - ['indexPattern', { watchDepth: 'reference' }], - ['opts', { watchDepth: 'reference' }], - ['resetQuery', { watchDepth: 'reference' }], - ['resultState', { watchDepth: 'reference' }], - ['fetchStatus', { watchDepth: 'reference' }], - ['rows', { watchDepth: 'reference' }], - ['savedSearch', { watchDepth: 'reference' }], - ['searchSource', { watchDepth: 'reference' }], - ['showSaveQuery', { watchDepth: 'reference' }], - ['state', { watchDepth: 'reference' }], - ['topNavMenu', { watchDepth: 'reference' }], - ['updateQuery', { watchDepth: 'reference' }], - ['updateSavedQueryId', { watchDepth: 'reference' }], - ['unmappedFieldsConfig', { watchDepth: 'value' }], - ['refreshAppState', { watchDepth: 'reference' }], - ]); -} diff --git a/src/plugins/discover/public/application/components/discover.test.tsx b/src/plugins/discover/public/application/components/discover.test.tsx deleted file mode 100644 index d804d608704219..00000000000000 --- a/src/plugins/discover/public/application/components/discover.test.tsx +++ /dev/null @@ -1,106 +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 React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; -import { Discover } from './discover'; -import { esHits } from '../../__mocks__/es_hits'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; -import { DiscoverServices } from '../../build_services'; -import { GetStateReturn } from '../angular/discover_state'; -import { savedSearchMock } from '../../__mocks__/saved_search'; -import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; -import { dataPluginMock } from '../../../../data/public/mocks'; -import { createFilterManagerMock } from '../../../../data/public/query/filter_manager/filter_manager.mock'; -import { uiSettingsMock as mockUiSettings } from '../../__mocks__/ui_settings'; -import { IndexPattern, IndexPatternAttributes } from '../../../../data/common/index_patterns'; -import { SavedObject } from '../../../../../core/types'; -import { navigationPluginMock } from '../../../../navigation/public/mocks'; -import { indexPatternWithTimefieldMock } from '../../__mocks__/index_pattern_with_timefield'; -import { calcFieldCounts } from '../helpers/calc_field_counts'; -import { DiscoverProps } from './types'; -import { RequestAdapter } from '../../../../inspector/common'; -import { Subject } from 'rxjs'; -import { DiscoverSearchSessionManager } from '../angular/discover_search_session'; - -const mockNavigation = navigationPluginMock.createStartContract(); - -jest.mock('../../kibana_services', () => { - return { - getServices: () => ({ - metadata: { - branch: 'test', - }, - capabilities: { - discover: { - save: true, - }, - }, - navigation: mockNavigation, - uiSettings: mockUiSettings, - }), - }; -}); - -function getProps(indexPattern: IndexPattern): DiscoverProps { - const searchSourceMock = createSearchSourceMock({}); - const services = ({ - capabilities: { - discover: { - save: true, - }, - }, - uiSettings: mockUiSettings, - } as unknown) as DiscoverServices; - - return { - fetch: jest.fn(), - fetchCounter: 0, - fetchError: undefined, - fetchStatus: 'loading', - fieldCounts: calcFieldCounts({}, esHits, indexPattern), - hits: esHits.length, - indexPattern, - minimumVisibleRows: 10, - onSkipBottomButtonClick: jest.fn(), - opts: { - config: mockUiSettings, - data: dataPluginMock.createStartContract(), - filterManager: createFilterManagerMock(), - getFieldCounts: jest.fn(), - indexPatternList: (indexPattern as unknown) as Array>, - inspectorAdapters: { requests: {} as RequestAdapter }, - navigateTo: jest.fn(), - refetch$: {} as Subject, - sampleSize: 10, - savedSearch: savedSearchMock, - searchSessionManager: {} as DiscoverSearchSessionManager, - setHeaderActionMenu: jest.fn(), - timefield: indexPattern.timeFieldName || '', - setAppState: jest.fn(), - services, - stateContainer: {} as GetStateReturn, - }, - resetQuery: jest.fn(), - resultState: 'ready', - rows: esHits, - searchSource: searchSourceMock, - state: { columns: [] }, - }; -} - -describe('Discover component', () => { - test('selected index pattern without time field displays no chart toggle', () => { - const component = shallowWithIntl(); - expect(component.find('[data-test-subj="discoverChartToggle"]').length).toBe(0); - }); - test('selected index pattern with time field displays chart toggle', () => { - const component = shallowWithIntl(); - expect(component.find('[data-test-subj="discoverChartToggle"]').length).toBe(1); - }); -}); diff --git a/src/plugins/discover/public/application/components/discover_topnav.test.tsx b/src/plugins/discover/public/application/components/discover_topnav.test.tsx deleted file mode 100644 index d30e5bda1abe7e..00000000000000 --- a/src/plugins/discover/public/application/components/discover_topnav.test.tsx +++ /dev/null @@ -1,77 +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 React from 'react'; -import { mountWithIntl } from '@kbn/test/jest'; -import { indexPatternMock } from '../../__mocks__/index_pattern'; -import { DiscoverServices } from '../../build_services'; -import { GetStateReturn } from '../angular/discover_state'; -import { savedSearchMock } from '../../__mocks__/saved_search'; -import { dataPluginMock } from '../../../../data/public/mocks'; -import { createFilterManagerMock } from '../../../../data/public/query/filter_manager/filter_manager.mock'; -import { uiSettingsMock as mockUiSettings } from '../../__mocks__/ui_settings'; -import { IndexPatternAttributes } from '../../../../data/common/index_patterns'; -import { SavedObject } from '../../../../../core/types'; -import { DiscoverTopNav, DiscoverTopNavProps } from './discover_topnav'; -import { RequestAdapter } from '../../../../inspector/common/adapters/request'; -import { TopNavMenu } from '../../../../navigation/public'; -import { ISearchSource, Query } from '../../../../data/common'; -import { DiscoverSearchSessionManager } from '../angular/discover_search_session'; -import { Subject } from 'rxjs'; - -function getProps(): DiscoverTopNavProps { - const services = ({ - navigation: { - ui: { TopNavMenu }, - }, - capabilities: { - discover: { - save: true, - }, - advancedSettings: { - save: true, - }, - }, - uiSettings: mockUiSettings, - } as unknown) as DiscoverServices; - const indexPattern = indexPatternMock; - return { - indexPattern: indexPatternMock, - opts: { - config: mockUiSettings, - data: dataPluginMock.createStartContract(), - filterManager: createFilterManagerMock(), - getFieldCounts: jest.fn(), - indexPatternList: (indexPattern as unknown) as Array>, - inspectorAdapters: { requests: {} as RequestAdapter }, - navigateTo: jest.fn(), - refetch$: {} as Subject, - sampleSize: 10, - savedSearch: savedSearchMock, - searchSessionManager: {} as DiscoverSearchSessionManager, - services, - setAppState: jest.fn(), - setHeaderActionMenu: jest.fn(), - stateContainer: {} as GetStateReturn, - timefield: indexPattern.timeFieldName || '', - }, - query: {} as Query, - savedQuery: '', - updateQuery: jest.fn(), - onOpenInspector: jest.fn(), - searchSource: {} as ISearchSource, - }; -} - -describe('Discover topnav component', () => { - test('setHeaderActionMenu was called', () => { - const props = getProps(); - mountWithIntl(); - expect(props.opts.setHeaderActionMenu).toHaveBeenCalled(); - }); -}); diff --git a/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts b/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts deleted file mode 100644 index 5d2be533065e7a..00000000000000 --- a/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts +++ /dev/null @@ -1,89 +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 { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; -import { SearchSource } from '../../../../../data/public'; -import { dataPluginMock } from '../../../../../data/public/mocks'; -import { applyAggsToSearchSource } from './apply_aggs_to_search_source'; - -describe('applyAggsToSearchSource', () => { - test('enabled = true', () => { - const indexPattern = indexPatternWithTimefieldMock; - const setField = jest.fn(); - const searchSource = ({ - setField, - removeField: jest.fn(), - } as unknown) as SearchSource; - - const dataMock = dataPluginMock.createStartContract(); - - const aggsConfig = applyAggsToSearchSource(true, searchSource, 'auto', indexPattern, dataMock); - - expect(aggsConfig!.aggs).toMatchInlineSnapshot(` - Array [ - Object { - "enabled": true, - "id": "1", - "params": Object {}, - "schema": "metric", - "type": "count", - }, - Object { - "enabled": true, - "id": "2", - "params": Object { - "drop_partials": false, - "extended_bounds": Object {}, - "field": "timestamp", - "interval": "auto", - "min_doc_count": 1, - "scaleMetricValues": false, - "useNormalizedEsInterval": true, - "used_interval": "0ms", - }, - "schema": "segment", - "type": "date_histogram", - }, - ] - `); - - expect(setField).toHaveBeenCalledWith('aggs', expect.any(Function)); - const dslFn = setField.mock.calls[0][1]; - expect(dslFn()).toMatchInlineSnapshot(` - Object { - "2": Object { - "date_histogram": Object { - "field": "timestamp", - "min_doc_count": 1, - "time_zone": "America/New_York", - }, - }, - } - `); - }); - - test('enabled = false', () => { - const indexPattern = indexPatternWithTimefieldMock; - const setField = jest.fn(); - const getField = jest.fn(() => { - return true; - }); - const removeField = jest.fn(); - const searchSource = ({ - getField, - setField, - removeField, - } as unknown) as SearchSource; - - const dataMock = dataPluginMock.createStartContract(); - - const aggsConfig = applyAggsToSearchSource(false, searchSource, 'auto', indexPattern, dataMock); - expect(aggsConfig).toBeFalsy(); - expect(getField).toHaveBeenCalledWith('aggs'); - expect(removeField).toHaveBeenCalledWith('aggs'); - }); -}); diff --git a/src/plugins/discover/public/application/components/histogram/get_dimensions.test.ts b/src/plugins/discover/public/application/components/histogram/get_dimensions.test.ts deleted file mode 100644 index ad7031f3319924..00000000000000 --- a/src/plugins/discover/public/application/components/histogram/get_dimensions.test.ts +++ /dev/null @@ -1,63 +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 { dataPluginMock } from '../../../../../data/public/mocks'; - -import { getDimensions } from './get_dimensions'; -import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield'; -import { SearchSource } from '../../../../../data/common/search/search_source'; -import { applyAggsToSearchSource } from './apply_aggs_to_search_source'; -import { calculateBounds } from '../../../../../data/common/query/timefilter'; - -test('getDimensions', () => { - const indexPattern = indexPatternWithTimefieldMock; - const setField = jest.fn(); - const searchSource = ({ - setField, - removeField: jest.fn(), - } as unknown) as SearchSource; - - const dataMock = dataPluginMock.createStartContract(); - dataMock.query.timefilter.timefilter.getTime = () => { - return { from: 'now-30y', to: 'now' }; - }; - dataMock.query.timefilter.timefilter.calculateBounds = (timeRange) => { - return calculateBounds(timeRange); - }; - - const aggsConfig = applyAggsToSearchSource(true, searchSource, 'auto', indexPattern, dataMock); - const actual = getDimensions(aggsConfig!, dataMock); - expect(actual).toMatchInlineSnapshot(` - Object { - "x": Object { - "accessor": 0, - "format": Object { - "id": "date", - "params": Object { - "pattern": "HH:mm:ss.SSS", - }, - }, - "label": "timestamp per 0 milliseconds", - "params": Object { - "bounds": undefined, - "date": true, - "format": "HH:mm:ss.SSS", - "interval": "P365D", - "intervalESUnit": "d", - "intervalESValue": 365, - }, - }, - "y": Object { - "accessor": 1, - "format": Object { - "id": "number", - }, - "label": "Count", - }, - } - `); -}); diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/application/components/table/table.tsx index 093b445267241b..7a100d24c386e0 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/application/components/table/table.tsx @@ -10,7 +10,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { DocViewTableRow } from './table_row'; import { trimAngularSpan } from './table_helper'; -import { isNestedFieldParent } from '../../helpers/nested_fields'; +import { isNestedFieldParent } from '../../apps/main/utils/nested_fields'; import { DocViewRenderProps } from '../../doc_views/doc_views_types'; const COLLAPSE_LINE_LENGTH = 350; diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts deleted file mode 100644 index 93620bc1d6bca5..00000000000000 --- a/src/plugins/discover/public/application/components/types.ts +++ /dev/null @@ -1,172 +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 { IUiSettingsClient, MountPoint, SavedObject } from 'kibana/public'; -import { Subject } from 'rxjs'; -import { Chart } from '../angular/helpers/point_series'; -import { IndexPattern } from '../../../../data/common/index_patterns/index_patterns'; -import { ElasticSearchHit } from '../doc_views/doc_views_types'; -import { AggConfigs } from '../../../../data/common/search/aggs'; - -import { - DataPublicPluginStart, - FilterManager, - IndexPatternAttributes, - ISearchSource, -} from '../../../../data/public'; -import { SavedSearch } from '../../saved_searches'; -import { AppState, GetStateReturn } from '../angular/discover_state'; -import { RequestAdapter } from '../../../../inspector/common'; -import { DiscoverServices } from '../../build_services'; -import { DiscoverSearchSessionManager } from '../angular/discover_search_session'; - -export interface DiscoverProps { - /** - * Function to fetch documents from Elasticsearch - */ - fetch: () => void; - /** - * Counter how often data was fetched (used for testing) - */ - fetchCounter: number; - /** - * Error in case of a failing document fetch - */ - fetchError?: Error; - /** - * Statistics by fields calculated using the fetched documents - */ - fieldCounts: Record; - /** - * Current state of data fetching - */ - fetchStatus: string; - /** - * Histogram aggregation data - */ - histogramData?: Chart; - /** - * Number of documents found by recent fetch - */ - hits: number; - /** - * Current IndexPattern - */ - indexPattern: IndexPattern; - /** - * Value needed for legacy "infinite" loading functionality - * Determins how much records are rendered using the legacy table - * Increased when scrolling down - */ - minimumVisibleRows: number; - /** - * Function to scroll down the legacy table to the bottom - */ - onSkipBottomButtonClick: () => void; - opts: { - /** - * Date histogram aggregation config - */ - chartAggConfigs?: AggConfigs; - /** - * Client of uiSettings - */ - config: IUiSettingsClient; - /** - * returns field statistics based on the loaded data sample - */ - getFieldCounts: () => Promise>; - /** - * Use angular router for navigation - */ - navigateTo: () => void; - /** - * Inspect, for analyzing requests and responses - */ - inspectorAdapters: { requests: RequestAdapter }; - /** - * Data plugin - */ - data: DataPublicPluginStart; - /** - * Data plugin filter manager - */ - filterManager: FilterManager; - /** - * List of available index patterns - */ - indexPatternList: Array>; - /** - * Refetch observable - */ - refetch$: Subject; - /** - * Kibana core services used by discover - */ - services: DiscoverServices; - /** - * Helps with state management of search session - */ - searchSessionManager: DiscoverSearchSessionManager; - /** - * The number of documents that can be displayed in the table/grid - */ - sampleSize: number; - /** - * Current instance of SavedSearch - */ - savedSearch: SavedSearch; - /** - * Function to set the header menu - */ - setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; - /** - * Timefield of the currently used index pattern - */ - timefield: string; - /** - * Function to set the current state - */ - setAppState: (state: Partial) => void; - /** - * State container providing globalState, appState and functions - */ - stateContainer: GetStateReturn; - }; - /** - * Function to reset the current query - */ - resetQuery: () => void; - /** - * Current state of the actual query, one of 'uninitialized', 'loading' ,'ready', 'none' - */ - resultState: string; - /** - * Array of document of the recent successful search request - */ - rows: ElasticSearchHit[]; - /** - * Instance of SearchSource, the high level search API - */ - searchSource: ISearchSource; - /** - * Current app state of URL - */ - state: AppState; - /** - * An object containing properties for unmapped fields behavior - */ - unmappedFieldsConfig?: { - /** - * determines whether to display unmapped fields - */ - showUnmappedFields: boolean; - }; - - refreshAppState?: () => void; -} diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts index c913b9abd1b433..418cbf6eac9cdb 100644 --- a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts +++ b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts @@ -13,7 +13,7 @@ import { IndexPattern, IndexPatternsContract } from '../../kibana_services'; import { AppState as DiscoverState, GetStateReturn as DiscoverGetStateReturn, -} from '../angular/discover_state'; +} from '../apps/main/services/discover_state'; import { AppState as ContextState, GetStateReturn as ContextGetStateReturn, diff --git a/src/plugins/discover/public/application/types.ts b/src/plugins/discover/public/application/types.ts new file mode 100644 index 00000000000000..4d7f47182e98a3 --- /dev/null +++ b/src/plugins/discover/public/application/types.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 enum FetchStatus { + UNINITIALIZED = 'uninitialized', + LOADING = 'loading', + COMPLETE = 'complete', + ERROR = 'error', +} diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 19ee06aa517985..b42bf6a81742c6 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -56,7 +56,7 @@ export interface DiscoverServices { urlForwarding: UrlForwardingStart; timefilter: TimefilterContract; toastNotifications: ToastsStart; - getSavedSearchById: (id: string) => Promise; + getSavedSearchById: (id?: string) => Promise; getSavedSearchUrlById: (id: string) => Promise; getEmbeddableInjector: () => Promise; uiSettings: IUiSettingsClient; @@ -87,7 +87,7 @@ export async function buildServices( theme: plugins.charts.theme, filterManager: plugins.data.query.filterManager, getEmbeddableInjector, - getSavedSearchById: async (id: string) => savedObjectService.get(id), + getSavedSearchById: async (id?: string) => savedObjectService.get(id), getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id), history: getHistory, indexPatterns: plugins.data.indexPatterns, diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 3f55ab76372bc4..139b23d28a1d43 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -329,7 +329,7 @@ export class DiscoverPlugin return; } // this is used by application mount and tests - const { getInnerAngularModule } = await import('./get_inner_angular'); + const { getInnerAngularModule } = await import('./application/angular/get_inner_angular'); const module = getInnerAngularModule( innerAngularName, core, @@ -400,7 +400,9 @@ export class DiscoverPlugin } const { core, plugins } = await this.initializeServices(); getServices().kibanaLegacy.loadFontAwesome(); - const { getInnerAngularModuleEmbeddable } = await import('./get_inner_angular'); + const { getInnerAngularModuleEmbeddable } = await import( + './application/angular/get_inner_angular' + ); getInnerAngularModuleEmbeddable(embeddableAngularName, core, plugins); const mountpoint = document.createElement('div'); this.embeddableInjector = angular.bootstrap(mountpoint, [embeddableAngularName]); diff --git a/src/plugins/discover/public/shared/index.ts b/src/plugins/discover/public/shared/index.ts index b1e4d9d87000ec..c82ac074db1114 100644 --- a/src/plugins/discover/public/shared/index.ts +++ b/src/plugins/discover/public/shared/index.ts @@ -10,5 +10,5 @@ * Allows the getSharingData function to be lazy loadable */ export async function loadSharingDataHelpers() { - return await import('../application/helpers/get_sharing_data'); + return await import('../application/apps/main/utils/get_sharing_data'); } diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index 8ed54f88afea3a..b5279eaaa1f122 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -327,10 +327,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const intervalS = 5; await PageObjects.timePicker.startAutoRefresh(intervalS); - // check inspector panel request stats for timestamp - await inspector.open(); - const getRequestTimestamp = async () => { + // check inspector panel request stats for timestamp + await inspector.open(); const requestStats = await inspector.getTableData(); const requestStatsRow = requestStats.filter( (r) => r && r[0] && r[0].includes('Request timestamp') @@ -338,6 +337,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { if (!requestStatsRow || !requestStatsRow[0] || !requestStatsRow[0][1]) { return ''; } + await inspector.close(); return requestStatsRow[0][1]; }; diff --git a/test/functional/apps/discover/_indexpattern_without_timefield.ts b/test/functional/apps/discover/_indexpattern_without_timefield.ts index cef02a0f54f052..a1050222937ece 100644 --- a/test/functional/apps/discover/_indexpattern_without_timefield.ts +++ b/test/functional/apps/discover/_indexpattern_without_timefield.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -13,18 +13,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const security = getService('security'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); describe('indexpattern without timefield', () => { before(async () => { await security.testUser.setRoles(['kibana_admin', 'kibana_timefield']); await esArchiver.loadIfNeeded('index_pattern_without_timefield'); - await kibanaServer.uiSettings.replace({ defaultIndex: 'without-timefield' }); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'without-timefield', + 'timepicker:timeDefaults': '{ "from": "2019-01-18T19:37:13.000Z", "to": "now"}', + }); await PageObjects.common.navigateToApp('discover'); }); after(async () => { await security.testUser.restoreDefaults(); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults'); + await kibanaServer.uiSettings.unset('defaultIndex'); await esArchiver.unload('index_pattern_without_timefield'); }); @@ -34,6 +40,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } }); + it('should adapt sidebar fields when switching', async () => { + await PageObjects.discover.selectIndexPattern('with-timefield'); + const timefieldExistsWithTimefield = await testSubjects.exists('field-@timestamp'); + expect(timefieldExistsWithTimefield).to.be(true); + await PageObjects.discover.selectIndexPattern('without-timefield'); + await PageObjects.discover.waitForDocTableLoadingComplete(); + const timefieldExistsWithoutTimefield = await testSubjects.exists('field-@timestamp'); + expect(timefieldExistsWithoutTimefield).to.be(false); + }); + it('should display a timepicker after switching to an index pattern with timefield', async () => { await PageObjects.discover.selectIndexPattern('with-timefield'); if (!(await PageObjects.timePicker.timePickerExists())) { diff --git a/test/functional/apps/discover/_inspector.ts b/test/functional/apps/discover/_inspector.ts index ca8539df9a9260..85bd163bdc50ca 100644 --- a/test/functional/apps/discover/_inspector.ts +++ b/test/functional/apps/discover/_inspector.ts @@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const STATS_ROW_VALUE_INDEX = 1; function getHitCount(requestStats: string[][]): string | undefined { const hitsCountStatsRow = requestStats.find((statsRow) => { - return statsRow[STATS_ROW_NAME_INDEX] === 'Hits (total)'; + return statsRow[STATS_ROW_NAME_INDEX] === 'Hits'; }); if (!hitsCountStatsRow) { @@ -61,7 +61,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await inspector.open(); const requestStats = await inspector.getTableData(); - expect(getHitCount(requestStats)).to.be('14004'); + expect(getHitCount(requestStats)).to.be('500'); }); }); } diff --git a/test/functional/apps/discover/_runtime_fields_editor.ts b/test/functional/apps/discover/_runtime_fields_editor.ts index 62045e3c9a6b17..407b74c4fd0188 100644 --- a/test/functional/apps/discover/_runtime_fields_editor.ts +++ b/test/functional/apps/discover/_runtime_fields_editor.ts @@ -56,7 +56,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('allows creation of a new field', async function () { await createRuntimeField('runtimefield'); await PageObjects.header.waitUntilLoadingHasFinished(); - expect((await PageObjects.discover.getAllFieldNames()).includes('runtimefield')).to.be(true); + await retry.waitFor('fieldNames to include runtimefield', async () => { + const fieldNames = await PageObjects.discover.getAllFieldNames(); + return fieldNames.includes('runtimefield'); + }); }); it('allows editing of a newly created field', async function () { @@ -65,10 +68,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await fieldEditor.save(); await fieldEditor.confirmSave(); await PageObjects.header.waitUntilLoadingHasFinished(); - expect((await PageObjects.discover.getAllFieldNames()).includes('runtimefield')).to.be(false); - expect((await PageObjects.discover.getAllFieldNames()).includes('runtimefield edited')).to.be( - true - ); + + await retry.waitFor('fieldNames to include edits', async () => { + const fieldNames = await PageObjects.discover.getAllFieldNames(); + return fieldNames.includes('runtimefield edited'); + }); }); it('allows creation of a new field and use it in a saved search', async function () { @@ -94,7 +98,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.removeField('delete'); await fieldEditor.confirmDelete(); await PageObjects.header.waitUntilLoadingHasFinished(); - expect((await PageObjects.discover.getAllFieldNames()).includes('delete')).to.be(false); + await retry.waitFor('fieldNames to include edits', async () => { + const fieldNames = await PageObjects.discover.getAllFieldNames(); + return !fieldNames.includes('delete'); + }); }); it('doc view includes runtime fields', async function () { diff --git a/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json index 9493408a30040d..0888079ec7c52c 100644 --- a/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json +++ b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json @@ -5,7 +5,7 @@ "index": ".kibana", "source": { "index-pattern": { - "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "fields": "[]", "title": "without-timefield" }, "type": "index-pattern" @@ -20,7 +20,6 @@ "index": "without-timefield", "source": { "@message" : "5", - "@timestamp": "2019-09-22T23:50:13.253Z", "referer": "http://twitter.com/error/takuya-onishi", "request": "/uploads/dafydd-williams.jpg", "response": "200", @@ -37,7 +36,7 @@ "index": ".kibana", "source": { "index-pattern": { - "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "fields": "[]", "title": "with-timefield", "timeFieldName": "@timestamp" }, diff --git a/test/functional/fixtures/es_archiver/index_pattern_without_timefield/mappings.json b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/mappings.json index 00961119239511..dd41e01592a7b6 100644 --- a/test/functional/fixtures/es_archiver/index_pattern_without_timefield/mappings.json +++ b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/mappings.json @@ -2,13 +2,6 @@ "type": "index", "value": { "index": "without-timefield", - "mappings": { - "properties": { - "@timestamp": { - "type": "date" - } - } - }, "settings": { "index": { "number_of_replicas": "0", 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 7517396961c004..8b20ab1700120c 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 @@ -351,7 +351,7 @@ export class CsvGenerator { // 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); + const columns = this.getColumns(searchSource, table) || []; if (first) { first = false; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c198acaf45dfaa..e8d47d4dfc17ee 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1692,8 +1692,6 @@ "discover.howToChangeTheTimeTooltip": "時刻を変更するには、グローバル時刻フィルターを使用します。", "discover.howToSeeOtherMatchingDocumentsDescription": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。", "discover.howToSeeOtherMatchingDocumentsDescriptionGrid": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。", - "discover.inspectorRequestDataTitle": "データ", - "discover.inspectorRequestDescription": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。", "discover.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", "discover.json.copyToClipboardLabel": "クリップボードにコピー", "discover.localMenu.inspectTitle": "検査", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 916e8066018bf5..db211f5a0f7867 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1702,8 +1702,6 @@ "discover.howToChangeTheTimeTooltip": "要更改时间,请使用全局时间筛选。", "discover.howToSeeOtherMatchingDocumentsDescription": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。", "discover.howToSeeOtherMatchingDocumentsDescriptionGrid": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。", - "discover.inspectorRequestDataTitle": "数据", - "discover.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。", "discover.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", "discover.json.copyToClipboardLabel": "复制到剪贴板", "discover.localMenu.inspectTitle": "检查", diff --git a/x-pack/test/functional/services/search_sessions.ts b/x-pack/test/functional/services/search_sessions.ts index c1702742b28ac5..63dc880fda3804 100644 --- a/x-pack/test/functional/services/search_sessions.ts +++ b/x-pack/test/functional/services/search_sessions.ts @@ -55,6 +55,7 @@ export function SearchSessionsProvider({ getService }: FtrProviderContext) { const currentState = await ( await testSubjects.find(SEARCH_SESSION_INDICATOR_TEST_SUBJ) ).getAttribute('data-state'); + log.info(`searchSessions state current: ${currentState} expected: ${state}`); return currentState === state; }); } diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts index 0f73ce1a3bf58c..5d74338996e782 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts @@ -11,10 +11,18 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const queryBar = getService('queryBar'); + const log = getService('log'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); const inspector = getService('inspector'); - const PageObjects = getPageObjects(['discover', 'common', 'timePicker', 'header', 'context']); + const PageObjects = getPageObjects([ + 'discover', + 'common', + 'timePicker', + 'header', + 'context', + 'searchSessionsManagement', + ]); const searchSessions = getService('searchSessions'); const retry = getService('retry'); @@ -43,6 +51,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await searchSessions.expectState('restored'); await testSubjects.existOrFail('discoverNoResultsError'); // expect error because of fake searchSessionId + await PageObjects.common.clearAllToasts(); const searchSessionId1 = await getSearchSessionId(); expect(searchSessionId1).to.be(fakeSearchSessionId); await queryBar.clickQuerySubmitButton(); @@ -60,6 +69,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { url = await browser.getCurrentUrl(); expect(url).to.contain('searchSessionId'); await PageObjects.header.waitUntilLoadingHasFinished(); + // Note this currently fails, for some reason the fakeSearchSessionId is not restored await searchSessions.expectState('restored'); expect(await getSearchSessionId()).to.be(fakeSearchSessionId); }); @@ -82,6 +92,25 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.context.waitUntilContextLoadingHasFinished(); await searchSessions.missingOrFail(); }); + + it('relative timerange works', async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await searchSessions.save(); + await searchSessions.expectState('backgroundCompleted'); + const searchSessionId = await getSearchSessionId(); + expect(await PageObjects.discover.hasNoResults()).to.be(true); + log.info('searchSessionId', searchSessionId); + + // load URL to restore a saved session + await PageObjects.searchSessionsManagement.goTo(); + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + // navigate to Discover + await searchSessionList[0].view(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await searchSessions.expectState('restored'); + expect(await PageObjects.discover.hasNoResults()).to.be(true); + }); }); async function getSearchSessionId(): Promise { From 2d3340b5434c030c9019082c233057a0f1520244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Tue, 8 Jun 2021 11:38:12 +0200 Subject: [PATCH 03/10] Unskip the test (#101414) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../apis/management/index_lifecycle_management/policies.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js index a07b9666685452..756fda7566843b 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js @@ -32,8 +32,7 @@ export default function ({ getService }) { const { addPolicyToIndex } = registerIndexHelpers({ supertest }); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/101219 - describe.skip('policies', () => { + describe('policies', () => { after(() => Promise.all([cleanUpEsResources(), cleanUpPolicies()])); describe('list', () => { From 451f64bde0a5ceb80137f0e3fd2e73d9969928fc Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 8 Jun 2021 13:35:54 +0300 Subject: [PATCH 04/10] [Cases] Get connector's information from `actionsTypeRegistry` (#101279) Co-authored-by: Yuliia Naumenko --- .../common/lib/kibana/kibana_react.mock.ts | 8 +- .../components/case_view/index.test.tsx | 7 + .../configure_cases/connectors.test.tsx | 8 ++ .../connectors_dropdown.test.tsx | 54 +++---- .../configure_cases/connectors_dropdown.tsx | 8 +- .../configure_cases/field_mapping.test.tsx | 9 ++ .../configure_cases/field_mapping.tsx | 8 +- .../components/configure_cases/index.test.tsx | 135 ++++++++---------- .../configure_cases/mapping.test.tsx | 9 ++ .../components/configure_cases/mapping.tsx | 24 ++-- .../connector_selector/form.test.tsx | 8 +- .../public/components/connectors/card.tsx | 12 +- .../public/components/connectors/config.ts | 39 ----- .../public/components/connectors/index.ts | 1 - .../connectors/jira/case_fields.test.tsx | 7 + .../servicenow_itsm_case_fields.test.tsx | 7 + .../servicenow_sir_case_fields.test.tsx | 7 + .../public/components/connectors/types.ts | 2 +- .../components/create/connector.test.tsx | 8 ++ .../components/create/form_context.test.tsx | 8 ++ .../components/edit_connector/index.test.tsx | 10 +- .../components/edit_connector/index.tsx | 1 + .../components/recent_cases/index.test.tsx | 17 ++- .../public/components/recent_cases/index.tsx | 1 + .../cases/public/containers/configure/mock.ts | 8 ++ .../public/common/index.ts | 7 - 26 files changed, 232 insertions(+), 181 deletions(-) delete mode 100644 x-pack/plugins/cases/public/components/connectors/config.ts diff --git a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts index 326163f6cdc035..ff03782447846f 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts @@ -12,9 +12,15 @@ import { coreMock } from '../../../../../../../src/core/public/mocks'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { StartServices } from '../../../types'; import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; +import { securityMock } from '../../../../../security/public/mocks'; +import { triggersActionsUiMock } from '../../../../../triggers_actions_ui/public/mocks'; export const createStartServicesMock = (): StartServices => - (coreMock.createStart() as unknown) as StartServices; + (({ + ...coreMock.createStart(), + security: securityMock.createStart(), + triggersActionsUi: triggersActionsUiMock.createStart(), + } as unknown) as StartServices); export const createWithKibanaMock = () => { const services = createStartServicesMock(); diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx index d13e3978ce618c..f89218d5407eb2 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx @@ -28,6 +28,7 @@ import { useConnectors } from '../../containers/configure/use_connectors'; import { connectorsMock } from '../../containers/configure/mock'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { CaseType, ConnectorTypes } from '../../../common'; +import { useKibana } from '../../common/lib/kibana'; jest.mock('../../containers/use_update_case'); jest.mock('../../containers/use_get_case_user_actions'); @@ -35,11 +36,13 @@ jest.mock('../../containers/use_get_case'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/use_post_push_to_service'); jest.mock('../user_action_tree/user_action_timestamp'); +jest.mock('../../common/lib/kibana'); const useUpdateCaseMock = useUpdateCase as jest.Mock; const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock; const useConnectorsMock = useConnectors as jest.Mock; const usePostPushToServiceMock = usePostPushToService as jest.Mock; +const useKibanaMock = useKibana as jest.Mocked; const alertsHit = [ { @@ -160,6 +163,10 @@ describe('CaseView ', () => { pushCaseToExternalService, })); useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, loading: false })); + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ + actionTypeTitle: '.servicenow', + iconClass: 'logoSecurity', + }); }); it('should render CaseComponent', async () => { diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx index d5b9a885f2c6d9..e5b1c66d01e1a0 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx @@ -13,6 +13,10 @@ import { TestProviders } from '../../common/mock'; import { ConnectorsDropdown } from './connectors_dropdown'; import { connectors } from './__mock__'; import { ConnectorTypes } from '../../../common'; +import { useKibana } from '../../common/lib/kibana'; + +jest.mock('../../common/lib/kibana'); +const useKibanaMock = useKibana as jest.Mocked; describe('Connectors', () => { let wrapper: ReactWrapper; @@ -31,6 +35,10 @@ describe('Connectors', () => { }; beforeAll(() => { + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ + actionTypeTitle: 'test', + iconClass: 'logoSecurity', + }); wrapper = mount(, { wrappingComponent: TestProviders }); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx index 86f80f772944aa..141be31a093f1e 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx @@ -12,6 +12,10 @@ import { EuiSuperSelect } from '@elastic/eui'; import { ConnectorsDropdown, Props } from './connectors_dropdown'; import { TestProviders } from '../../common/mock'; import { connectors } from './__mock__'; +import { useKibana } from '../../common/lib/kibana'; + +jest.mock('../../common/lib/kibana'); +const useKibanaMock = useKibana as jest.Mocked; describe('ConnectorsDropdown', () => { let wrapper: ReactWrapper; @@ -24,6 +28,10 @@ describe('ConnectorsDropdown', () => { }; beforeAll(() => { + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ + actionTypeTitle: '.servicenow', + iconClass: 'logoSecurity', + }); wrapper = mount(, { wrappingComponent: TestProviders }); }); @@ -73,14 +81,7 @@ describe('ConnectorsDropdown', () => { > @@ -103,14 +104,7 @@ describe('ConnectorsDropdown', () => { > @@ -133,14 +127,7 @@ describe('ConnectorsDropdown', () => { > @@ -163,14 +150,7 @@ describe('ConnectorsDropdown', () => { > @@ -210,9 +190,13 @@ describe('ConnectorsDropdown', () => { wrappingComponent: TestProviders, }); - expect(newWrapper.find('button span:not([data-euiicon-type])').at(1).text()).toBe( - 'My Connector' - ); + expect( + newWrapper + .find('[data-test-subj="dropdown-connectors"]') + .first() + .text() + .includes('My Connector, is selected') + ).toBeTruthy(); }); test('if the props hideConnectorServiceNowSir is true, the connector should not be part of the list of options ', () => { diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx index 8c3a0f7ae19618..d26ec06d696fcf 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx @@ -11,8 +11,8 @@ import styled from 'styled-components'; import { ConnectorTypes } from '../../../common'; import { ActionConnector } from '../../containers/configure/types'; -import { connectorsConfiguration } from '../connectors'; import * as i18n from './translations'; +import { useKibana } from '../../common/lib/kibana'; export interface Props { connectors: ActionConnector[]; @@ -65,6 +65,7 @@ const ConnectorsDropdownComponent: React.FC = ({ appendAddConnectorButton = false, hideConnectorServiceNowSir = false, }) => { + const { triggersActionsUi } = useKibana().services; const connectorsAsOptions = useMemo(() => { const connectorsFormatted = connectors.reduce( (acc, connector) => { @@ -80,7 +81,10 @@ const ConnectorsDropdownComponent: React.FC = ({ diff --git a/x-pack/plugins/cases/public/components/configure_cases/field_mapping.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/field_mapping.test.tsx index 8c2a66ad7ee535..73f48654b22995 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/field_mapping.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/field_mapping.test.tsx @@ -12,6 +12,10 @@ import { FieldMapping, FieldMappingProps } from './field_mapping'; import { mappings } from './__mock__'; import { TestProviders } from '../../common/mock'; import { FieldMappingRowStatic } from './field_mapping_row_static'; +import { useKibana } from '../../common/lib/kibana'; + +jest.mock('../../common/lib/kibana'); +const useKibanaMock = useKibana as jest.Mocked; describe('FieldMappingRow', () => { let wrapper: ReactWrapper; @@ -22,8 +26,13 @@ describe('FieldMappingRow', () => { }; beforeAll(() => { + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ + actionTypeTitle: '.servicenow', + iconClass: 'logoSecurity', + }); wrapper = mount(, { wrappingComponent: TestProviders }); }); + test('it renders', () => { expect( wrapper.find('[data-test-subj="case-configure-field-mappings-row-wrapper"]').first().exists() diff --git a/x-pack/plugins/cases/public/components/configure_cases/field_mapping.tsx b/x-pack/plugins/cases/public/components/configure_cases/field_mapping.tsx index 7d5b72b583fae2..b5ce978a1b481a 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/field_mapping.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/field_mapping.tsx @@ -13,7 +13,7 @@ import { FieldMappingRowStatic } from './field_mapping_row_static'; import * as i18n from './translations'; import { CaseConnectorMapping } from '../../containers/configure/types'; -import { connectorsConfiguration } from '../connectors'; +import { useKibana } from '../../common/lib/kibana'; const FieldRowWrapper = styled.div` margin: 10px 0; @@ -31,8 +31,10 @@ const FieldMappingComponent: React.FC = ({ isLoading, mappings, }) => { + const { triggersActionsUi } = useKibana().services; const selectedConnector = useMemo( - () => connectorsConfiguration[connectorActionTypeId] ?? { fields: {} }, + () => triggersActionsUi.actionTypeRegistry.get(connectorActionTypeId) ?? { fields: {} }, + // eslint-disable-next-line react-hooks/exhaustive-deps [connectorActionTypeId] ); return mappings.length ? ( @@ -45,7 +47,7 @@ const FieldMappingComponent: React.FC = ({ - {i18n.FIELD_MAPPING_SECOND_COL(selectedConnector.name)} + {i18n.FIELD_MAPPING_SECOND_COL(selectedConnector.actionTypeTitle ?? '')} diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx index 0d9ede9bb7de89..7212a195f79119 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx @@ -7,18 +7,12 @@ import React from 'react'; import { ReactWrapper, mount } from 'enzyme'; +import { waitFor } from '@testing-library/react'; import { ConfigureCases } from '.'; import { TestProviders } from '../../common/mock'; import { Connectors } from './connectors'; import { ClosureOptions } from './closure_options'; -import { - ActionConnector, - ConnectorAddFlyout, - ConnectorEditFlyout, - TriggersAndActionsUIPublicPluginStart, -} from '../../../../triggers_actions_ui/public'; -import { actionTypeRegistryMock } from '../../../../triggers_actions_ui/public/application/action_type_registry.mock'; import { useKibana } from '../../common/lib/kibana'; import { useConnectors } from '../../containers/configure/use_connectors'; @@ -33,6 +27,7 @@ import { useActionTypesResponse, } from './__mock__'; import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common'; +import { actionTypeRegistryMock } from '../../../../triggers_actions_ui/public/application/action_type_registry.mock'; jest.mock('../../common/lib/kibana'); jest.mock('../../containers/configure/use_connectors'); @@ -46,52 +41,13 @@ const useGetUrlSearchMock = jest.fn(); const useActionTypesMock = useActionTypes as jest.Mock; describe('ConfigureCases', () => { + beforeAll(() => { + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ + actionTypeTitle: '.servicenow', + iconClass: 'logoSecurity', + }); + }); beforeEach(() => { - useKibanaMock().services.triggersActionsUi = ({ - actionTypeRegistry: actionTypeRegistryMock.create(), - getAddConnectorFlyout: jest.fn().mockImplementation(() => ( - {}} - actionTypeRegistry={actionTypeRegistryMock.create()} - actionTypes={[ - { - id: '.servicenow', - name: 'servicenow', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'gold', - }, - { - id: '.jira', - name: 'jira', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'gold', - }, - { - id: '.resilient', - name: 'resilient', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'gold', - }, - ]} - /> - )), - getEditConnectorFlyout: jest - .fn() - .mockImplementation(() => ( - {}} - actionTypeRegistry={actionTypeRegistryMock.create()} - initialConnector={connectors[1] as ActionConnector} - /> - )), - } as unknown) as TriggersAndActionsUIPublicPluginStart; - useActionTypesMock.mockImplementation(() => useActionTypesResponse); }); @@ -116,13 +72,11 @@ describe('ConfigureCases', () => { }); test('it does NOT render the ConnectorAddFlyout', () => { - // Components from triggersActionsUi do not have a data-test-subj - expect(wrapper.find(ConnectorAddFlyout).exists()).toBeFalsy(); + expect(wrapper.find('ConnectorAddFlyout').exists()).toBeFalsy(); }); test('it does NOT render the ConnectorEditFlyout', () => { - // Components from triggersActionsUi do not have a data-test-subj - expect(wrapper.find(ConnectorEditFlyout).exists()).toBeFalsy(); + expect(wrapper.find('ConnectorEditFlyout').exists()).toBeFalsy(); }); test('it does NOT render the EuiCallOut', () => { @@ -221,8 +175,8 @@ describe('ConfigureCases', () => { expect(wrapper.find(ClosureOptions).prop('closureTypeSelected')).toBe('close-by-user'); // Flyouts - expect(wrapper.find(ConnectorAddFlyout).exists()).toBe(false); - expect(wrapper.find(ConnectorEditFlyout).exists()).toBe(false); + expect(wrapper.find('ConnectorAddFlyout').exists()).toBe(false); + expect(wrapper.find('ConnectorEditFlyout').exists()).toBe(false); }); test('it disables correctly when the user cannot crud', () => { @@ -577,40 +531,67 @@ describe('user interactions', () => { useGetUrlSearchMock.mockImplementation(() => searchURL); }); - test('it show the add flyout when pressing the add connector button', () => { + test('it show the add flyout when pressing the add connector button', async () => { const wrapper = mount(, { wrappingComponent: TestProviders, }); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); wrapper.find('button[data-test-subj="dropdown-connector-add-connector"]').simulate('click'); - wrapper.update(); - expect(wrapper.find(ConnectorAddFlyout).exists()).toBe(true); - expect(wrapper.find(ConnectorAddFlyout).prop('actionTypes')).toEqual([ - expect.objectContaining({ - id: '.servicenow', - }), - expect.objectContaining({ - id: '.jira', - }), - expect.objectContaining({ - id: '.resilient', - }), - ]); + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('ConnectorAddFlyout').exists()).toBe(true); + expect(wrapper.find('ConnectorAddFlyout').prop('actionTypes')).toEqual([ + expect.objectContaining({ + id: '.servicenow', + }), + expect.objectContaining({ + id: '.jira', + }), + expect.objectContaining({ + id: '.resilient', + }), + expect.objectContaining({ + id: '.servicenow-sir', + }), + ]); + }); }); - test('it show the edit flyout when pressing the update connector button', () => { + test('it show the edit flyout when pressing the update connector button', async () => { + const actionType = actionTypeRegistryMock.createMockActionTypeModel({ + id: '.resilient', + validateConnector: () => { + return Promise.resolve({}); + }, + validateParams: () => { + const validationResult = { errors: {} }; + return Promise.resolve(validationResult); + }, + actionConnectorFields: null, + }); + + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest + .fn() + .mockReturnValue(actionType); + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.has = jest + .fn() + .mockReturnValue(true); + const wrapper = mount(, { wrappingComponent: TestProviders, }); wrapper .find('button[data-test-subj="case-configure-update-selected-connector-button"]') .simulate('click'); - wrapper.update(); - expect(wrapper.find(ConnectorEditFlyout).exists()).toBe(true); - expect(wrapper.find(ConnectorEditFlyout).prop('initialConnector')).toEqual(connectors[1]); + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('ConnectorEditFlyout').exists()).toBe(true); + expect(wrapper.find('ConnectorEditFlyout').prop('initialConnector')).toEqual(connectors[1]); + }); + expect( wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() ).toBeFalsy(); diff --git a/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx index 75b2410dde957f..0a1da1219342ed 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx @@ -11,6 +11,10 @@ import { mount } from 'enzyme'; import { TestProviders } from '../../common/mock'; import { Mapping, MappingProps } from './mapping'; import { mappings } from './__mock__'; +import { useKibana } from '../../common/lib/kibana'; + +jest.mock('../../common/lib/kibana'); +const useKibanaMock = useKibana as jest.Mocked; describe('Mapping', () => { const props: MappingProps = { @@ -21,7 +25,12 @@ describe('Mapping', () => { beforeEach(() => { jest.clearAllMocks(); + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ + actionTypeTitle: 'ServiceNow ITSM', + iconClass: 'logoSecurity', + }); }); + test('it shows mapping form group', () => { const wrapper = mount(, { wrappingComponent: TestProviders }); expect(wrapper.find('[data-test-subj="static-mappings"]').first().exists()).toBe(true); diff --git a/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx b/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx index 5ec6a33f48b6aa..16c02606ae90b8 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx @@ -14,7 +14,7 @@ import * as i18n from './translations'; import { FieldMapping } from './field_mapping'; import { CaseConnectorMapping } from '../../containers/configure/types'; -import { connectorsConfiguration } from '../connectors'; +import { useKibana } from '../../common/lib/kibana'; export interface MappingProps { connectorActionTypeId: string; @@ -27,21 +27,29 @@ const MappingComponent: React.FC = ({ isLoading, mappings, }) => { - const selectedConnector = useMemo(() => connectorsConfiguration[connectorActionTypeId], [ - connectorActionTypeId, - ]); + const { triggersActionsUi } = useKibana().services; + const selectedConnector = useMemo( + () => triggersActionsUi.actionTypeRegistry.get(connectorActionTypeId), + [connectorActionTypeId, triggersActionsUi] + ); const fieldMappingDesc: { desc: string; color: TextColor } = useMemo( () => mappings.length > 0 || isLoading - ? { desc: i18n.FIELD_MAPPING_DESC(selectedConnector.name), color: 'subdued' } - : { desc: i18n.FIELD_MAPPING_DESC_ERR(selectedConnector.name), color: 'danger' }, - [isLoading, mappings.length, selectedConnector.name] + ? { + desc: i18n.FIELD_MAPPING_DESC(selectedConnector.actionTypeTitle ?? ''), + color: 'subdued', + } + : { + desc: i18n.FIELD_MAPPING_DESC_ERR(selectedConnector.actionTypeTitle ?? ''), + color: 'danger', + }, + [isLoading, mappings.length, selectedConnector.actionTypeTitle] ); return ( -

{i18n.FIELD_MAPPING_TITLE(selectedConnector.name)}

+

{i18n.FIELD_MAPPING_TITLE(selectedConnector.actionTypeTitle ?? '')}

{fieldMappingDesc.desc} diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.test.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.test.tsx index ec136989dd9379..ea25fce2d4f7c4 100644 --- a/x-pack/plugins/cases/public/components/connector_selector/form.test.tsx +++ b/x-pack/plugins/cases/public/components/connector_selector/form.test.tsx @@ -11,17 +11,23 @@ import { UseField, Form, useForm, FormHook } from '../../common/shared_imports'; import { ConnectorSelector } from './form'; import { connectorsMock } from '../../containers/mock'; import { getFormMock } from '../__mock__/form'; +import { useKibana } from '../../common/lib/kibana'; jest.mock('../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'); +jest.mock('../../common/lib/kibana'); +const useKibanaMock = useKibana as jest.Mocked; const useFormMock = useForm as jest.Mock; describe('ConnectorSelector', () => { const formHookMock = getFormMock({ connectorId: connectorsMock[0].id }); beforeEach(() => { - jest.resetAllMocks(); useFormMock.mockImplementation(() => ({ form: formHookMock })); + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ + actionTypeTitle: 'test', + iconClass: 'logoSecurity', + }); }); it('it should render', async () => { diff --git a/x-pack/plugins/cases/public/components/connectors/card.tsx b/x-pack/plugins/cases/public/components/connectors/card.tsx index 82a508ccf34329..b27c7207e7b8a3 100644 --- a/x-pack/plugins/cases/public/components/connectors/card.tsx +++ b/x-pack/plugins/cases/public/components/connectors/card.tsx @@ -9,8 +9,8 @@ import React, { memo, useMemo } from 'react'; import { EuiCard, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; import styled from 'styled-components'; -import { connectorsConfiguration } from '.'; import { ConnectorTypes } from '../../../common'; +import { useKibana } from '../../common/lib/kibana'; interface ConnectorCardProps { connectorType: ConnectorTypes; @@ -31,6 +31,8 @@ const ConnectorCardDisplay: React.FC = ({ listItems, isLoading, }) => { + const { triggersActionsUi } = useKibana().services; + const description = useMemo( () => ( @@ -46,7 +48,13 @@ const ConnectorCardDisplay: React.FC = ({ [listItems] ); const icon = useMemo( - () => , + () => ( + + ), + // eslint-disable-next-line react-hooks/exhaustive-deps [connectorType] ); return ( diff --git a/x-pack/plugins/cases/public/components/connectors/config.ts b/x-pack/plugins/cases/public/components/connectors/config.ts deleted file mode 100644 index e8d87511c7e17d..00000000000000 --- a/x-pack/plugins/cases/public/components/connectors/config.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - getResilientActionType, - getServiceNowITSMActionType, - getServiceNowSIRActionType, - getJiraActionType, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../triggers_actions_ui/public/common'; -import { ConnectorConfiguration } from './types'; - -const resilient = getResilientActionType(); -const serviceNowITSM = getServiceNowITSMActionType(); -const serviceNowSIR = getServiceNowSIRActionType(); -const jira = getJiraActionType(); - -export const connectorsConfiguration: Record = { - '.servicenow': { - name: serviceNowITSM.actionTypeTitle ?? '', - logo: serviceNowITSM.iconClass, - }, - '.servicenow-sir': { - name: serviceNowSIR.actionTypeTitle ?? '', - logo: serviceNowSIR.iconClass, - }, - '.jira': { - name: jira.actionTypeTitle ?? '', - logo: jira.iconClass, - }, - '.resilient': { - name: resilient.actionTypeTitle ?? '', - logo: resilient.iconClass, - }, -}; diff --git a/x-pack/plugins/cases/public/components/connectors/index.ts b/x-pack/plugins/cases/public/components/connectors/index.ts index 71ba161eb63c9f..2a7d59ca7b0409 100644 --- a/x-pack/plugins/cases/public/components/connectors/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/index.ts @@ -19,7 +19,6 @@ import { export { getActionType as getCaseConnectorUi } from './case'; -export * from './config'; export * from './types'; interface GetCaseConnectorsReturn { diff --git a/x-pack/plugins/cases/public/components/connectors/jira/case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/jira/case_fields.test.tsx index 38a1e30616200c..ae9245534c78a1 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/case_fields.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { omit } from 'lodash/fp'; +import { useKibana } from '../../../common/lib/kibana'; import { connector, issues } from '../mock'; import { useGetIssueTypes } from './use_get_issue_types'; import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; @@ -27,6 +28,7 @@ const useGetIssueTypesMock = useGetIssueTypes as jest.Mock; const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock; const useGetSingleIssueMock = useGetSingleIssue as jest.Mock; const useGetIssuesMock = useGetIssues as jest.Mock; +const useKibanaMock = useKibana as jest.Mocked; describe('Jira Fields', () => { const useGetIssueTypesResponse = { @@ -87,6 +89,10 @@ describe('Jira Fields', () => { useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse); useGetSingleIssueMock.mockReturnValue(useGetSingleIssueResponse); + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ + actionTypeTitle: '.jira', + iconClass: 'logoSecurity', + }); jest.clearAllMocks(); }); @@ -144,6 +150,7 @@ describe('Jira Fields', () => { priority: 'High', }); }); + test('it searches parent correctly', async () => { useGetFieldsByIssueTypeMock.mockReturnValue({ ...useGetFieldsByIssueTypeResponse, diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx index 9688ca191d6727..b14842bbf1bbf4 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx @@ -10,6 +10,7 @@ import { waitFor, act } from '@testing-library/react'; import { EuiSelect } from '@elastic/eui'; import { mount } from 'enzyme'; +import { useKibana } from '../../../common/lib/kibana'; import { connector, choices as mockChoices } from '../mock'; import { Choice } from './types'; import Fields from './servicenow_itsm_case_fields'; @@ -24,6 +25,8 @@ jest.mock('./use_get_choices', () => ({ }, })); +const useKibanaMock = useKibana as jest.Mocked; + describe('ServiceNowITSM Fields', () => { const fields = { severity: '1', @@ -36,6 +39,10 @@ describe('ServiceNowITSM Fields', () => { beforeEach(() => { jest.clearAllMocks(); + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ + actionTypeTitle: '.servicenow', + iconClass: 'logoSecurity', + }); }); it('all params fields are rendered - isEdit: true', () => { diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx index 4a5b34cd3c3cb8..7d42c90a436f7c 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx @@ -10,6 +10,7 @@ import { mount } from 'enzyme'; import { waitFor, act } from '@testing-library/react'; import { EuiSelect } from '@elastic/eui'; +import { useKibana } from '../../../common/lib/kibana'; import { connector, choices as mockChoices } from '../mock'; import { Choice } from './types'; import Fields from './servicenow_sir_case_fields'; @@ -24,6 +25,8 @@ jest.mock('./use_get_choices', () => ({ }, })); +const useKibanaMock = useKibana as jest.Mocked; + describe('ServiceNowSIR Fields', () => { const fields = { destIp: true, @@ -38,6 +41,10 @@ describe('ServiceNowSIR Fields', () => { beforeEach(() => { jest.clearAllMocks(); + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ + actionTypeTitle: '.servicenow-sir', + iconClass: 'logoSecurity', + }); }); it('all params fields are rendered - isEdit: true', () => { diff --git a/x-pack/plugins/cases/public/components/connectors/types.ts b/x-pack/plugins/cases/public/components/connectors/types.ts index 1657153ab645bd..4eb97513b9f58e 100644 --- a/x-pack/plugins/cases/public/components/connectors/types.ts +++ b/x-pack/plugins/cases/public/components/connectors/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IconType } from '@elastic/eui/src/components/icon/icon'; +import { IconType } from '@elastic/eui'; import React from 'react'; import { diff --git a/x-pack/plugins/cases/public/components/create/connector.test.tsx b/x-pack/plugins/cases/public/components/create/connector.test.tsx index 9eb475f54221d1..b2a8838774b09c 100644 --- a/x-pack/plugins/cases/public/components/create/connector.test.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.test.tsx @@ -25,6 +25,14 @@ jest.mock('../../common/lib/kibana', () => { services: { notifications: {}, http: {}, + triggersActionsUi: { + actionTypeRegistry: { + get: jest.fn().mockReturnValue({ + actionTypeTitle: 'test', + iconClass: 'logoSecurity', + }), + }, + }, }, }), }; diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index cb053b2e784cda..e083f11ced7770 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -11,6 +11,7 @@ import { act, waitFor } from '@testing-library/react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common'; +import { useKibana } from '../../common/lib/kibana'; import { TestProviders } from '../../common/mock'; import { usePostCase } from '../../containers/use_post_case'; import { usePostComment } from '../../containers/use_post_comment'; @@ -54,6 +55,7 @@ jest.mock('../connectors/jira/use_get_fields_by_issue_type'); jest.mock('../connectors/jira/use_get_single_issue'); jest.mock('../connectors/jira/use_get_issues'); jest.mock('../connectors/servicenow/use_get_choices'); +jest.mock('../../common/lib/kibana'); const useConnectorsMock = useConnectors as jest.Mock; const useCaseConfigureMock = useCaseConfigure as jest.Mock; @@ -67,6 +69,7 @@ const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock; const useGetChoicesMock = useGetChoices as jest.Mock; const postCase = jest.fn(); const pushCaseToExternalService = jest.fn(); +const useKibanaMock = useKibana as jest.Mocked; const defaultPostCase = { isLoading: false, @@ -130,7 +133,12 @@ describe('Create case', () => { tags: sampleTags, fetchTags, })); + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ + actionTypeTitle: '.servicenow', + iconClass: 'logoSecurity', + }); }); + beforeEach(() => { jest.clearAllMocks(); }); diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx index 3b6d4bd3f33f2e..1385e8e8664c37 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx @@ -7,15 +7,18 @@ import React from 'react'; import { mount } from 'enzyme'; +import { waitFor } from '@testing-library/react'; import { EditConnector } from './index'; import { getFormMock, useFormMock } from '../__mock__/form'; import { TestProviders } from '../../common/mock'; import { connectorsMock } from '../../containers/configure/mock'; -import { waitFor } from '@testing-library/react'; import { caseUserActions } from '../../containers/mock'; +import { useKibana } from '../../common/lib/kibana'; jest.mock('../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'); +jest.mock('../../common/lib/kibana'); +const useKibanaMock = useKibana as jest.Mocked; const onSubmit = jest.fn(); const defaultProps = { @@ -33,8 +36,11 @@ describe('EditConnector ', () => { const formHookMock = getFormMock({ connectorId: sampleConnector }); beforeEach(() => { jest.clearAllMocks(); - jest.resetAllMocks(); useFormMock.mockImplementation(() => ({ form: formHookMock })); + useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ + actionTypeTitle: '.servicenow', + iconClass: 'logoSecurity', + }); }); it('Renders no connector, and then edit', async () => { diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index 56f1a77fc407ed..ad6b5a5e7cddf5 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -202,6 +202,7 @@ export const EditConnector = React.memo( payload: true, }); }, [dispatch]); + return ( diff --git a/x-pack/plugins/cases/public/components/recent_cases/index.test.tsx b/x-pack/plugins/cases/public/components/recent_cases/index.test.tsx index 5893d5f8c5af48..3a5cda489947a2 100644 --- a/x-pack/plugins/cases/public/components/recent_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/recent_cases/index.test.tsx @@ -13,8 +13,11 @@ import { TestProviders } from '../../common/mock'; import { useGetCases } from '../../containers/use_get_cases'; import { useGetCasesMockState } from '../../containers/mock'; import { SECURITY_SOLUTION_OWNER } from '../../../common'; +import { useCurrentUser } from '../../common/lib/kibana/hooks'; jest.mock('../../containers/use_get_cases'); +jest.mock('../../common/lib/kibana/hooks'); + configure({ testIdAttribute: 'data-test-subj' }); const defaultProps = { allCasesNavigation: { @@ -32,16 +35,25 @@ const defaultProps = { maxCasesToShow: 10, owner: [SECURITY_SOLUTION_OWNER], }; + const setFilters = jest.fn(); const mockData = { ...useGetCasesMockState, setFilters, }; + const useGetCasesMock = useGetCases as jest.Mock; +const useCurrentUserMock = useCurrentUser as jest.Mock; + describe('RecentCases', () => { beforeEach(() => { jest.clearAllMocks(); useGetCasesMock.mockImplementation(() => mockData); + useCurrentUserMock.mockResolvedValue({ + email: 'elastic@elastic.co', + fullName: 'Elastic', + username: 'elastic', + }); }); it('is good at loading', () => { @@ -83,8 +95,9 @@ describe('RecentCases', () => { ); - const yo = getByTestId('myRecentlyReported'); - userEvent.click(yo); + + const element = getByTestId('myRecentlyReported'); + userEvent.click(element); expect(setFilters).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/cases/public/components/recent_cases/index.tsx b/x-pack/plugins/cases/public/components/recent_cases/index.tsx index bb34f651d52dfd..8a08a46487c8b0 100644 --- a/x-pack/plugins/cases/public/components/recent_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/recent_cases/index.tsx @@ -50,6 +50,7 @@ const RecentCasesComponent = ({ : {}, [currentUser, recentCasesFilterBy] ); + return ( <> <> diff --git a/x-pack/plugins/cases/public/containers/configure/mock.ts b/x-pack/plugins/cases/public/containers/configure/mock.ts index ef287ea866dcbb..833c2cfb3aa7ca 100644 --- a/x-pack/plugins/cases/public/containers/configure/mock.ts +++ b/x-pack/plugins/cases/public/containers/configure/mock.ts @@ -114,6 +114,14 @@ export const actionTypesMock: ActionTypeConnector[] = [ enabledInConfig: true, enabledInLicense: true, }, + { + id: '.servicenow-sir', + name: 'ServiceNow SIR', + minimumLicenseRequired: 'platinum', + enabled: false, + enabledInConfig: true, + enabledInLicense: true, + }, ]; export const caseConfigurationResposeMock: CasesConfigureResponse = { diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index.ts index 01470bdddf4d73..67f4cc603ad926 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index.ts @@ -10,10 +10,3 @@ export * from './constants'; export * from './index_controls'; export * from './lib'; export * from './types'; - -export { - getServiceNowITSMActionType, - getServiceNowSIRActionType, -} from '../application/components/builtin_action_types/servicenow'; -export { getJiraActionType } from '../application/components/builtin_action_types/jira'; -export { getResilientActionType } from '../application/components/builtin_action_types/resilient'; From 15ab9d70fd17e5e21e22a71f17196ceb798696a2 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Tue, 8 Jun 2021 13:36:35 +0200 Subject: [PATCH 05/10] some fixes to expression tutorial (#101558) --- dev_docs/tutorials/expressions.mdx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/dev_docs/tutorials/expressions.mdx b/dev_docs/tutorials/expressions.mdx index 288fb9afdd7220..4688da98b7b019 100644 --- a/dev_docs/tutorials/expressions.mdx +++ b/dev_docs/tutorials/expressions.mdx @@ -10,10 +10,12 @@ tags: ['kibana', 'onboarding', 'dev', 'architecture'] ## Expressions service Expression service exposes a registry of reusable functions primary used for fetching and transposing data and a registry of renderer functions that can render data into a DOM element. -Adding functions is easy and so is reusing them. An expression is a chain of functions with provided arguments, which given a single input translates to a single output. +Adding functions is easy and so is reusing them. + +An expression is a chain of functions with provided arguments, which given a single input translates to a single output. Each expression is representable by a human friendly string which a user can type. -### creating expressions +### Creating expressions Here is a very simple expression string: @@ -23,7 +25,7 @@ essql 'select column1, column2 from myindex' | mapColumn name=column3 fn='{ colu It consists of 3 functions: - - essql which runs given sql query against elasticsearch and returns the results + - `essql` which runs given sql query against elasticsearch and returns the results - `mapColumn`, which computes a new column from existing ones; - `table`, which prepares the data for rendering in a tabular format. @@ -41,7 +43,7 @@ const expression = buildExpression([ Note: Consumers need to be aware which plugin registers specific functions with expressions function registry and import correct type definitions from there. - + The `expressions` service is available on both server and client, with similar APIs. @@ -54,7 +56,7 @@ const executionContract = expressions.execute(expression, input); const result = await executionContract.getData(); ``` - + Check the full spec of execute function [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md) @@ -68,7 +70,7 @@ This is the easiest way to get expressions rendered inside your application. ``` - + Check the full spec of ReactExpressionRenderer component props [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md) @@ -80,7 +82,7 @@ If you are not using React, you can use the loader expression service provides t const handler = loader(domElement, expression, params); ``` - + Check the full spec of expression loader params [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) @@ -103,7 +105,7 @@ const functionDefinition = { expressions.registerFunction(functionDefinition); ``` - + Check the full interface of ExpressionFuntionDefinition [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md) @@ -125,6 +127,6 @@ const rendererDefinition = { expressions.registerRenderer(rendererDefinition); ``` - + Check the full interface of ExpressionRendererDefinition [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderdefinition.md) From b740640a2ad5f69d732c177cea0fa96bc424c257 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 8 Jun 2021 14:20:39 +0200 Subject: [PATCH 06/10] [Core][Navigation] Chrome nav display application deepLinks (#100590) * chrome nav allows deepLinks * docs updated * use ChromeNavLink.url to call navigateToUrl * to_nav_link test cases added for deepLink parameter * snapshots updated * deep nav links functional test added * AppNavOptions type encapsulation * docs updated * docs for AppNavOptions * implement navigateToApp deepLinkId option * app searchable flag implementation * code cleaning and test case added * use explicit type Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/kibana-plugin-core-public.app.md | 7 +- ...ibana-plugin-core-public.app.searchable.md | 13 ++ .../kibana-plugin-core-public.appdeeplink.md | 3 +- ...-core-public.appnavoptions.euiicontype.md} | 4 +- ...-plugin-core-public.appnavoptions.icon.md} | 4 +- ...kibana-plugin-core-public.appnavoptions.md | 23 ++ ...plugin-core-public.appnavoptions.order.md} | 4 +- ...ugin-core-public.appnavoptions.tooltip.md} | 4 +- ...a-plugin-core-public.appupdatablefields.md | 2 +- ...kibana-plugin-core-public.chromenavlink.md | 2 +- ...na-plugin-core-public.chromenavlink.url.md | 4 +- .../core/public/kibana-plugin-core-public.md | 1 + ...-public.navigatetoappoptions.deeplinkid.md | 13 ++ ...plugin-core-public.navigatetoappoptions.md | 3 +- ...n-core-public.navigatetoappoptions.path.md | 2 +- ...lugin-core-public.publicappdeeplinkinfo.md | 3 +- ...kibana-plugin-core-public.publicappinfo.md | 3 +- .../application/application_service.test.ts | 126 ++++++++++ .../application/application_service.tsx | 41 +++- src/core/public/application/index.ts | 1 + .../application/integration_tests/utils.tsx | 3 + src/core/public/application/types.ts | 109 +++++---- .../application/ui/app_container.test.tsx | 2 + .../application/utils/get_app_info.test.ts | 215 +++++++++++++++++- .../public/application/utils/get_app_info.ts | 31 +-- src/core/public/chrome/nav_links/nav_link.ts | 5 +- .../nav_links/nav_links_service.test.ts | 44 +++- .../chrome/nav_links/nav_links_service.ts | 30 ++- .../chrome/nav_links/to_nav_link.test.ts | 111 ++++++++- .../public/chrome/nav_links/to_nav_link.ts | 41 +++- .../collapsible_nav.test.tsx.snap | 13 ++ .../header/__snapshots__/header.test.tsx.snap | 6 + .../chrome/ui/header/collapsible_nav.test.tsx | 2 + .../chrome/ui/header/collapsible_nav.tsx | 4 +- .../public/chrome/ui/header/header.test.tsx | 3 +- src/core/public/chrome/ui/header/nav_link.tsx | 8 +- src/core/public/index.ts | 1 + src/core/public/public.api.md | 32 +-- .../core_plugin_deep_links/kibana.json | 7 + .../core_plugin_deep_links/package.json | 14 ++ .../public/application.tsx | 158 +++++++++++++ .../core_plugin_deep_links/public/index.ts | 19 ++ .../core_plugin_deep_links/public/plugin.tsx | 73 ++++++ .../core_plugin_deep_links/tsconfig.json | 17 ++ .../core_plugins/application_deep_links.ts | 99 ++++++++ .../test_suites/core_plugins/index.ts | 1 + .../public/providers/application.test.ts | 9 +- .../public/providers/application.ts | 4 +- .../public/providers/get_app_results.test.ts | 17 +- .../public/providers/get_app_results.ts | 20 +- 50 files changed, 1216 insertions(+), 145 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.app.searchable.md rename docs/development/core/public/{kibana-plugin-core-public.app.euiicontype.md => kibana-plugin-core-public.appnavoptions.euiicontype.md} (60%) rename docs/development/core/public/{kibana-plugin-core-public.app.icon.md => kibana-plugin-core-public.appnavoptions.icon.md} (61%) create mode 100644 docs/development/core/public/kibana-plugin-core-public.appnavoptions.md rename docs/development/core/public/{kibana-plugin-core-public.app.order.md => kibana-plugin-core-public.appnavoptions.order.md} (59%) rename docs/development/core/public/{kibana-plugin-core-public.app.tooltip.md => kibana-plugin-core-public.appnavoptions.tooltip.md} (56%) create mode 100644 docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.deeplinkid.md create mode 100644 test/plugin_functional/plugins/core_plugin_deep_links/kibana.json create mode 100644 test/plugin_functional/plugins/core_plugin_deep_links/package.json create mode 100644 test/plugin_functional/plugins/core_plugin_deep_links/public/application.tsx create mode 100644 test/plugin_functional/plugins/core_plugin_deep_links/public/index.ts create mode 100644 test/plugin_functional/plugins/core_plugin_deep_links/public/plugin.tsx create mode 100644 test/plugin_functional/plugins/core_plugin_deep_links/tsconfig.json create mode 100644 test/plugin_functional/test_suites/core_plugins/application_deep_links.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.app.md b/docs/development/core/public/kibana-plugin-core-public.app.md index 721d9a2f121c73..d79a12a83367d0 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface App +export interface App extends AppNavOptions ``` ## Properties @@ -21,16 +21,13 @@ export interface App | [chromeless](./kibana-plugin-core-public.app.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | | [deepLinks](./kibana-plugin-core-public.app.deeplinks.md) | AppDeepLink[] | Input type for registering secondary in-app locations for an application.Deep links must include at least one of path or deepLinks. A deep link that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. | | [defaultPath](./kibana-plugin-core-public.app.defaultpath.md) | string | Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the path option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. | -| [euiIconType](./kibana-plugin-core-public.app.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | | [exactRoute](./kibana-plugin-core-public.app.exactroute.md) | boolean | If set to true, the application's route will only be checked against an exact match. Defaults to false. | -| [icon](./kibana-plugin-core-public.app.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | | [id](./kibana-plugin-core-public.app.id.md) | string | The unique identifier of the application | | [keywords](./kibana-plugin-core-public.app.keywords.md) | string[] | Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. | | [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | A mount function called when the user navigates to this app's route. | | [navLinkStatus](./kibana-plugin-core-public.app.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | -| [order](./kibana-plugin-core-public.app.order.md) | number | An ordinal used to sort nav links relative to one another for display. | +| [searchable](./kibana-plugin-core-public.app.searchable.md) | boolean | The initial flag to determine if the application is searchable in the global search. Defaulting to true if navLinkStatus is visible or omitted. | | [status](./kibana-plugin-core-public.app.status.md) | AppStatus | The initial status of the application. Defaulting to accessible | | [title](./kibana-plugin-core-public.app.title.md) | string | The title of the application. | -| [tooltip](./kibana-plugin-core-public.app.tooltip.md) | string | A tooltip shown when hovering over app link. | | [updater$](./kibana-plugin-core-public.app.updater_.md) | Observable<AppUpdater> | An [AppUpdater](./kibana-plugin-core-public.appupdater.md) observable that can be used to update the application [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) at runtime. | diff --git a/docs/development/core/public/kibana-plugin-core-public.app.searchable.md b/docs/development/core/public/kibana-plugin-core-public.app.searchable.md new file mode 100644 index 00000000000000..ab1b559a7f6a1d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.searchable.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [searchable](./kibana-plugin-core-public.app.searchable.md) + +## App.searchable property + +The initial flag to determine if the application is searchable in the global search. Defaulting to `true` if `navLinkStatus` is `visible` or omitted. + +Signature: + +```typescript +searchable?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appdeeplink.md b/docs/development/core/public/kibana-plugin-core-public.appdeeplink.md index 5aa951cffdcb54..30fd3085a4341a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appdeeplink.md +++ b/docs/development/core/public/kibana-plugin-core-public.appdeeplink.md @@ -16,7 +16,8 @@ export declare type AppDeepLink = { title: string; keywords?: string[]; navLinkStatus?: AppNavLinkStatus; -} & ({ + searchable?: boolean; +} & AppNavOptions & ({ path: string; deepLinks?: AppDeepLink[]; } | { diff --git a/docs/development/core/public/kibana-plugin-core-public.app.euiicontype.md b/docs/development/core/public/kibana-plugin-core-public.appnavoptions.euiicontype.md similarity index 60% rename from docs/development/core/public/kibana-plugin-core-public.app.euiicontype.md rename to docs/development/core/public/kibana-plugin-core-public.appnavoptions.euiicontype.md index ff79d832f92e2b..069eccf63a2351 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.euiicontype.md +++ b/docs/development/core/public/kibana-plugin-core-public.appnavoptions.euiicontype.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [euiIconType](./kibana-plugin-core-public.app.euiicontype.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppNavOptions](./kibana-plugin-core-public.appnavoptions.md) > [euiIconType](./kibana-plugin-core-public.appnavoptions.euiicontype.md) -## App.euiIconType property +## AppNavOptions.euiIconType property A EUI iconType that will be used for the app's icon. This icon takes precendence over the `icon` property. diff --git a/docs/development/core/public/kibana-plugin-core-public.app.icon.md b/docs/development/core/public/kibana-plugin-core-public.appnavoptions.icon.md similarity index 61% rename from docs/development/core/public/kibana-plugin-core-public.app.icon.md rename to docs/development/core/public/kibana-plugin-core-public.appnavoptions.icon.md index 98260da5d20219..3b809fc4aec396 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.icon.md +++ b/docs/development/core/public/kibana-plugin-core-public.appnavoptions.icon.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [icon](./kibana-plugin-core-public.app.icon.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppNavOptions](./kibana-plugin-core-public.appnavoptions.md) > [icon](./kibana-plugin-core-public.appnavoptions.icon.md) -## App.icon property +## AppNavOptions.icon property A URL to an image file used as an icon. Used as a fallback if `euiIconType` is not provided. diff --git a/docs/development/core/public/kibana-plugin-core-public.appnavoptions.md b/docs/development/core/public/kibana-plugin-core-public.appnavoptions.md new file mode 100644 index 00000000000000..52c28c861dc704 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appnavoptions.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppNavOptions](./kibana-plugin-core-public.appnavoptions.md) + +## AppNavOptions interface + +App navigation menu options + +Signature: + +```typescript +export interface AppNavOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [euiIconType](./kibana-plugin-core-public.appnavoptions.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | +| [icon](./kibana-plugin-core-public.appnavoptions.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | +| [order](./kibana-plugin-core-public.appnavoptions.order.md) | number | An ordinal used to sort nav links relative to one another for display. | +| [tooltip](./kibana-plugin-core-public.appnavoptions.tooltip.md) | string | A tooltip shown when hovering over app link. | + diff --git a/docs/development/core/public/kibana-plugin-core-public.app.order.md b/docs/development/core/public/kibana-plugin-core-public.appnavoptions.order.md similarity index 59% rename from docs/development/core/public/kibana-plugin-core-public.app.order.md rename to docs/development/core/public/kibana-plugin-core-public.appnavoptions.order.md index bb6be116b6b587..ca7ae482a04adb 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.order.md +++ b/docs/development/core/public/kibana-plugin-core-public.appnavoptions.order.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [order](./kibana-plugin-core-public.app.order.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppNavOptions](./kibana-plugin-core-public.appnavoptions.md) > [order](./kibana-plugin-core-public.appnavoptions.order.md) -## App.order property +## AppNavOptions.order property An ordinal used to sort nav links relative to one another for display. diff --git a/docs/development/core/public/kibana-plugin-core-public.app.tooltip.md b/docs/development/core/public/kibana-plugin-core-public.appnavoptions.tooltip.md similarity index 56% rename from docs/development/core/public/kibana-plugin-core-public.app.tooltip.md rename to docs/development/core/public/kibana-plugin-core-public.appnavoptions.tooltip.md index e901de0fdccc98..97c18c2e56a1ee 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.tooltip.md +++ b/docs/development/core/public/kibana-plugin-core-public.appnavoptions.tooltip.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [tooltip](./kibana-plugin-core-public.app.tooltip.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppNavOptions](./kibana-plugin-core-public.appnavoptions.md) > [tooltip](./kibana-plugin-core-public.appnavoptions.tooltip.md) -## App.tooltip property +## AppNavOptions.tooltip property A tooltip shown when hovering over app link. diff --git a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md index d7b12d4b707010..c24da05abe7ecd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md @@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug Signature: ```typescript -export declare type AppUpdatableFields = Pick; +export declare type AppUpdatableFields = Pick; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md index dfe8f119505aa8..c7dd461617e344 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md @@ -26,5 +26,5 @@ export interface ChromeNavLink | [order](./kibana-plugin-core-public.chromenavlink.order.md) | number | An ordinal used to sort nav links relative to one another for display. | | [title](./kibana-plugin-core-public.chromenavlink.title.md) | string | The title of the application. | | [tooltip](./kibana-plugin-core-public.chromenavlink.tooltip.md) | string | A tooltip shown when hovering over an app link. | -| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string | The route used to open the of an application. If unset, baseUrl will be used instead. | +| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string | The route used to open the default path and the deep links of an application. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md index 833930c4947862..b9d12e450df505 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md @@ -4,10 +4,10 @@ ## ChromeNavLink.url property -The route used to open the of an application. If unset, `baseUrl` will be used instead. +The route used to open the default path and the deep links of an application. Signature: ```typescript -readonly url?: string; +readonly url: string; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index 6239279f275d1c..f341a7cd9315f8 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -38,6 +38,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | | | [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) | | | [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) | | +| [AppNavOptions](./kibana-plugin-core-public.appnavoptions.md) | App navigation menu options | | [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Capabilities](./kibana-plugin-core-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [ChromeBadge](./kibana-plugin-core-public.chromebadge.md) | | diff --git a/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.deeplinkid.md b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.deeplinkid.md new file mode 100644 index 00000000000000..4039e1338fc1c0 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.deeplinkid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) > [deepLinkId](./kibana-plugin-core-public.navigatetoappoptions.deeplinkid.md) + +## NavigateToAppOptions.deepLinkId property + +optional [deep link](./kibana-plugin-core-public.app.deeplinks.md) id inside the application to navigate to. If an additional [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) is defined it will be appended to the deep link path. + +Signature: + +```typescript +deepLinkId?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.md b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.md index 79b59a19508e76..7b01bab056d843 100644 --- a/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.md @@ -16,8 +16,9 @@ export interface NavigateToAppOptions | Property | Type | Description | | --- | --- | --- | +| [deepLinkId](./kibana-plugin-core-public.navigatetoappoptions.deeplinkid.md) | string | optional [deep link](./kibana-plugin-core-public.app.deeplinks.md) id inside the application to navigate to. If an additional [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) is defined it will be appended to the deep link path. | | [openInNewTab](./kibana-plugin-core-public.navigatetoappoptions.openinnewtab.md) | boolean | if true, will open the app in new tab, will share session information via window.open if base | -| [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) | string | optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md)\` as default. | +| [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) | string | optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md) as default. | | [replace](./kibana-plugin-core-public.navigatetoappoptions.replace.md) | boolean | if true, will not create a new history entry when navigating (using replace instead of push) | | [state](./kibana-plugin-core-public.navigatetoappoptions.state.md) | unknown | optional state to forward to the application | diff --git a/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.path.md b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.path.md index 095553d05778c9..b39fc8c324ad9a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.path.md +++ b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.path.md @@ -4,7 +4,7 @@ ## NavigateToAppOptions.path property -optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md)\` as default. +optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md) as default. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappdeeplinkinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappdeeplinkinfo.md index d3a6a4de905fdf..40fd98687c6194 100644 --- a/docs/development/core/public/kibana-plugin-core-public.publicappdeeplinkinfo.md +++ b/docs/development/core/public/kibana-plugin-core-public.publicappdeeplinkinfo.md @@ -9,9 +9,10 @@ Public information about a registered app's [deepLinks](./kibana-plugin-core-pub Signature: ```typescript -export declare type PublicAppDeepLinkInfo = Omit & { +export declare type PublicAppDeepLinkInfo = Omit & { deepLinks: PublicAppDeepLinkInfo[]; keywords: string[]; navLinkStatus: AppNavLinkStatus; + searchable: boolean; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md index a5563eae83563f..01d23ae47a0d95 100644 --- a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md +++ b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md @@ -9,11 +9,12 @@ Public information about a registered [application](./kibana-plugin-core-public. Signature: ```typescript -export declare type PublicAppInfo = Omit & { +export declare type PublicAppInfo = Omit & { status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; keywords: string[]; deepLinks: PublicAppDeepLinkInfo[]; + searchable: boolean; }; ``` diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 2e2f1cad49f199..5658d3f6260772 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -107,6 +107,7 @@ describe('#setup()', () => { status: AppStatus.inaccessible, tooltip: 'App inaccessible due to reason', defaultPath: 'foo/bar', + deepLinks: [{ id: 'subapp2', title: 'Subapp 2', path: '/subapp2' }], })); applications = await applications$.pipe(take(1)).toPromise(); @@ -118,6 +119,9 @@ describe('#setup()', () => { status: AppStatus.inaccessible, defaultPath: 'foo/bar', tooltip: 'App inaccessible due to reason', + deepLinks: [ + expect.objectContaining({ id: 'subapp2', title: 'Subapp 2', path: '/subapp2' }), + ], }) ); expect(applications.get('app2')).toEqual( @@ -814,6 +818,128 @@ describe('#start()', () => { expect(MockHistory.replace).not.toHaveBeenCalled(); }); }); + + describe('deepLinkId option', () => { + beforeEach(() => { + MockHistory.push.mockClear(); + }); + + it('preserves trailing slash when path contains a hash', async () => { + const { register } = service.setup(setupDeps); + + register( + Symbol(), + createApp({ + id: 'app1', + appRoute: '/custom/app-path', + deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }], + }) + ); + + const { navigateToApp } = await service.start(startDeps); + await navigateToApp('app1', { deepLinkId: 'dl1', path: '#/' }); + expect(MockHistory.push).toHaveBeenLastCalledWith( + '/custom/app-path/deep-link#/', + undefined + ); + + await navigateToApp('app1', { deepLinkId: 'dl1', path: '#/foo/bar/' }); + expect(MockHistory.push).toHaveBeenLastCalledWith( + '/custom/app-path/deep-link#/foo/bar/', + undefined + ); + + await navigateToApp('app1', { deepLinkId: 'dl1', path: '/path#/' }); + expect(MockHistory.push).toHaveBeenLastCalledWith( + '/custom/app-path/deep-link/path#/', + undefined + ); + + await navigateToApp('app1', { deepLinkId: 'dl1', path: '/path#/hash/' }); + expect(MockHistory.push).toHaveBeenLastCalledWith( + '/custom/app-path/deep-link/path#/hash/', + undefined + ); + + await navigateToApp('app1', { deepLinkId: 'dl1', path: '/path/' }); + expect(MockHistory.push).toHaveBeenLastCalledWith( + '/custom/app-path/deep-link/path', + undefined + ); + }); + + it('omits the defaultPath when the deepLinkId parameter is specified', async () => { + const { register } = service.setup(setupDeps); + + register( + Symbol(), + createApp({ + id: 'app1', + defaultPath: 'default/path', + deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }], + }) + ); + register( + Symbol(), + createApp({ + id: 'app2', + appRoute: '/custom-app-path', + defaultPath: '/my-default', + deepLinks: [{ id: 'dl2', title: 'deep link 2', path: '/deep-link-2' }], + }) + ); + + const { navigateToApp } = await service.start(startDeps); + + await navigateToApp('app1', {}); + expect(MockHistory.push).toHaveBeenLastCalledWith('/app/app1/default/path', undefined); + + await navigateToApp('app1', { deepLinkId: 'dl1' }); + expect(MockHistory.push).toHaveBeenLastCalledWith('/app/app1/deep-link', undefined); + + await navigateToApp('app1', { deepLinkId: 'dl1', path: 'some-other-path' }); + expect(MockHistory.push).toHaveBeenLastCalledWith( + '/app/app1/deep-link/some-other-path', + undefined + ); + + await navigateToApp('app2', {}); + expect(MockHistory.push).toHaveBeenLastCalledWith('/custom-app-path/my-default', undefined); + + await navigateToApp('app2', { deepLinkId: 'dl2' }); + expect(MockHistory.push).toHaveBeenLastCalledWith( + '/custom-app-path/deep-link-2', + undefined + ); + + await navigateToApp('app2', { deepLinkId: 'dl2', path: 'some-other-path' }); + expect(MockHistory.push).toHaveBeenLastCalledWith( + '/custom-app-path/deep-link-2/some-other-path', + undefined + ); + }); + + it('ignores the deepLinkId parameter if it is unknown', async () => { + const { register } = service.setup(setupDeps); + + register( + Symbol(), + createApp({ + id: 'app1', + defaultPath: 'default/path', + deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }], + }) + ); + + const { navigateToApp } = await service.start(startDeps); + + await navigateToApp('app1', { deepLinkId: 'dl-unknown' }); + expect(MockHistory.push).toHaveBeenLastCalledWith('/app/app1/default/path', undefined); + + await navigateToApp('app1', { deepLinkId: 'dl-unknown', path: 'some-other-path' }); + expect(MockHistory.push).toHaveBeenLastCalledWith('/app/app1/some-other-path', undefined); + }); + }); }); describe('navigateToUrl', () => { diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index bbfea61220b513..32d45b32c32ffd 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -64,6 +64,10 @@ const getAppUrl = (mounters: Map, appId: string, path: string = return appendAppPath(appBasePath, path); }; +const getAppDeepLinkPath = (mounters: Map, appId: string, deepLinkId: string) => { + return mounters.get(appId)?.deepLinkPaths[deepLinkId]; +}; + const allApplicationsFilter = '__ALL__'; interface AppUpdaterWrapper { @@ -175,6 +179,7 @@ export class ApplicationService { this.mounters.set(app.id, { appRoute: app.appRoute!, appBasePath: basePath.prepend(app.appRoute!), + deepLinkPaths: toDeepLinkPaths(app.deepLinks), exactRoute: app.exactRoute ?? false, mount: wrapMount(plugin, app), unmountBeforeMounting: false, @@ -226,7 +231,7 @@ export class ApplicationService { const navigateToApp: InternalApplicationStart['navigateToApp'] = async ( appId, - { path, state, replace = false, openInNewTab = false }: NavigateToAppOptions = {} + { deepLinkId, path, state, replace = false, openInNewTab = false }: NavigateToAppOptions = {} ) => { const currentAppId = this.currentAppId$.value; const navigatingToSameApp = currentAppId === appId; @@ -235,6 +240,12 @@ export class ApplicationService { : await this.shouldNavigate(overlays, appId); if (shouldNavigate) { + if (deepLinkId) { + const deepLinkPath = getAppDeepLinkPath(availableMounters, appId, deepLinkId); + if (deepLinkPath) { + path = appendAppPath(deepLinkPath, path); + } + } if (path === undefined) { path = applications$.value.get(appId)?.defaultPath; } @@ -384,8 +395,18 @@ const updateStatus = (app: App, statusUpdaters: AppUpdaterWrapper[]): App => { ...fields, // status and navLinkStatus enums are ordered by reversed priority // if multiple updaters wants to change these fields, we will always follow the priority order. - status: Math.max(changes.status ?? 0, fields.status ?? 0), - navLinkStatus: Math.max(changes.navLinkStatus ?? 0, fields.navLinkStatus ?? 0), + status: Math.max( + changes.status ?? AppStatus.accessible, + fields.status ?? AppStatus.accessible + ), + navLinkStatus: Math.max( + changes.navLinkStatus ?? AppNavLinkStatus.default, + fields.navLinkStatus ?? AppNavLinkStatus.default + ), + // deepLinks take the last defined update + deepLinks: fields.deepLinks + ? populateDeepLinkDefaults(fields.deepLinks) + : changes.deepLinks, }; } }); @@ -396,10 +417,22 @@ const updateStatus = (app: App, statusUpdaters: AppUpdaterWrapper[]): App => { }; const populateDeepLinkDefaults = (deepLinks?: AppDeepLink[]): AppDeepLink[] => { - if (!deepLinks) return []; + if (!deepLinks) { + return []; + } return deepLinks.map((deepLink) => ({ ...deepLink, navLinkStatus: deepLink.navLinkStatus ?? AppNavLinkStatus.default, deepLinks: populateDeepLinkDefaults(deepLink.deepLinks), })); }; + +const toDeepLinkPaths = (deepLinks?: AppDeepLink[]): Mounter['deepLinkPaths'] => { + if (!deepLinks) { + return {}; + } + return deepLinks.reduce((deepLinkPaths: Mounter['deepLinkPaths'], deepLink) => { + if (deepLink.path) deepLinkPaths[deepLink.id] = deepLink.path; + return { ...deepLinkPaths, ...toDeepLinkPaths(deepLink.deepLinks) }; + }, {}); +}; diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index 68e1991646afbd..882555fcd60e09 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -17,6 +17,7 @@ export type { AppUnmount, AppMountParameters, AppUpdatableFields, + AppNavOptions, AppUpdater, AppDeepLink, ApplicationSetup, diff --git a/src/core/public/application/integration_tests/utils.tsx b/src/core/public/application/integration_tests/utils.tsx index 40f8f3b7957192..dcf071719c11ad 100644 --- a/src/core/public/application/integration_tests/utils.tsx +++ b/src/core/public/application/integration_tests/utils.tsx @@ -35,12 +35,14 @@ export const createAppMounter = ({ appId, html = `
App ${appId}
`, appRoute = `/app/${appId}`, + deepLinkPaths = {}, exactRoute = false, extraMountHook, }: { appId: string; html?: string; appRoute?: string; + deepLinkPaths?: Record; exactRoute?: boolean; extraMountHook?: (params: AppMountParameters) => void; }): MockedMounterTuple => { @@ -51,6 +53,7 @@ export const createAppMounter = ({ mounter: { appRoute, appBasePath: appRoute, + deepLinkPaths, exactRoute, mount: jest.fn(async (params: AppMountParameters) => { const { appBasePath: basename, element } = params; diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index ffc41955360bdf..60b0dbf158dd91 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -63,9 +63,37 @@ export enum AppNavLinkStatus { */ export type AppUpdatableFields = Pick< App, - 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'deepLinks' + 'status' | 'navLinkStatus' | 'searchable' | 'tooltip' | 'defaultPath' | 'deepLinks' >; +/** + * App navigation menu options + * @public + */ +export interface AppNavOptions { + /** + * An ordinal used to sort nav links relative to one another for display. + */ + order?: number; + + /** + * A tooltip shown when hovering over app link. + */ + tooltip?: string; + + /** + * A EUI iconType that will be used for the app's icon. This icon + * takes precendence over the `icon` property. + */ + euiIconType?: string; + + /** + * A URL to an image file used as an icon. Used as a fallback + * if `euiIconType` is not provided. + */ + icon?: string; +} + /** * Updater for applications. * see {@link ApplicationSetup} @@ -76,7 +104,7 @@ export type AppUpdater = (app: App) => Partial | undefined; /** * @public */ -export interface App { +export interface App extends AppNavOptions { /** * The unique identifier of the application */ @@ -107,6 +135,12 @@ export interface App { */ navLinkStatus?: AppNavLinkStatus; + /** + * The initial flag to determine if the application is searchable in the global search. + * Defaulting to `true` if `navLinkStatus` is `visible` or omitted. + */ + searchable?: boolean; + /** * Allow to define the default path a user should be directed to when navigating to the app. * When defined, this value will be used as a default for the `path` option when calling {@link ApplicationStart.navigateToApp | navigateToApp}`, @@ -148,28 +182,6 @@ export interface App { */ updater$?: Observable; - /** - * An ordinal used to sort nav links relative to one another for display. - */ - order?: number; - - /** - * A tooltip shown when hovering over app link. - */ - tooltip?: string; - - /** - * A EUI iconType that will be used for the app's icon. This icon - * takes precendence over the `icon` property. - */ - euiIconType?: string; - - /** - * A URL to an image file used as an icon. Used as a fallback - * if `euiIconType` is not provided. - */ - icon?: string; - /** * Custom capabilities defined by the app. */ @@ -261,11 +273,12 @@ export interface App { */ export type PublicAppDeepLinkInfo = Omit< AppDeepLink, - 'deepLinks' | 'keywords' | 'navLinkStatus' + 'deepLinks' | 'keywords' | 'navLinkStatus' | 'searchable' > & { deepLinks: PublicAppDeepLinkInfo[]; keywords: string[]; navLinkStatus: AppNavLinkStatus; + searchable: boolean; }; /** @@ -285,33 +298,40 @@ export type AppDeepLink = { keywords?: string[]; /** Optional status of the chrome navigation, defaults to `hidden` */ navLinkStatus?: AppNavLinkStatus; -} & ( - | { - /** URL path to access this link, relative to the application's appRoute. */ - path: string; - /** Optional array of links that are 'underneath' this section in the hierarchy */ - deepLinks?: AppDeepLink[]; - } - | { - /** Optional path to access this section. Omit if this part of the hierarchy does not have a page URL. */ - path?: string; - /** Array links that are 'underneath' this section in this hierarchy. */ - deepLinks: AppDeepLink[]; - } -); + /** Optional flag to determine if the link is searchable in the global search. Defaulting to `true` if `navLinkStatus` is `visible` or omitted */ + searchable?: boolean; +} & AppNavOptions & + ( + | { + /** URL path to access this link, relative to the application's appRoute. */ + path: string; + /** Optional array of links that are 'underneath' this section in the hierarchy */ + deepLinks?: AppDeepLink[]; + } + | { + /** Optional path to access this section. Omit if this part of the hierarchy does not have a page URL. */ + path?: string; + /** Array links that are 'underneath' this section in this hierarchy. */ + deepLinks: AppDeepLink[]; + } + ); /** * Public information about a registered {@link App | application} * * @public */ -export type PublicAppInfo = Omit & { +export type PublicAppInfo = Omit< + App, + 'mount' | 'updater$' | 'keywords' | 'deepLinks' | 'searchable' +> & { // remove optional on fields populated with default values status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; keywords: string[]; deepLinks: PublicAppDeepLinkInfo[]; + searchable: boolean; }; /** @@ -592,6 +612,7 @@ export interface AppLeaveActionFactory { export interface Mounter { appRoute: string; appBasePath: string; + deepLinkPaths: Record; mount: AppMount; exactRoute: boolean; unmountBeforeMounting?: boolean; @@ -657,11 +678,17 @@ export interface InternalApplicationSetup extends Pick { appRoute: '/some-route', unmountBeforeMounting: false, exactRoute: false, + deepLinkPaths: {}, mount: async ({ element }: AppMountParameters) => { await promise; const container = document.createElement('div'); @@ -133,6 +134,7 @@ describe('AppContainer', () => { const mounter = { appBasePath: '/base-path/some-route', appRoute: '/some-route', + deepLinkPaths: {}, unmountBeforeMounting: false, exactRoute: false, mount: async ({ element }: AppMountParameters) => { diff --git a/src/core/public/application/utils/get_app_info.test.ts b/src/core/public/application/utils/get_app_info.test.ts index ef4a06707d6664..fa1e2dd9a4537d 100644 --- a/src/core/public/application/utils/get_app_info.test.ts +++ b/src/core/public/application/utils/get_app_info.test.ts @@ -7,7 +7,7 @@ */ import { of } from 'rxjs'; -import { App, AppNavLinkStatus, AppStatus } from '../types'; +import { App, AppDeepLink, AppNavLinkStatus, AppStatus } from '../types'; import { getAppInfo } from './get_app_info'; describe('getAppInfo', () => { @@ -18,10 +18,22 @@ describe('getAppInfo', () => { title: 'some-title', status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.default, + searchable: true, appRoute: `/app/some-id`, ...props, }); + const createDeepLink = (props: Partial = {}): AppDeepLink => ({ + id: 'some-deep-link-id', + title: 'my deep link', + path: '/my-deep-link', + navLinkStatus: AppNavLinkStatus.default, + searchable: true, + deepLinks: [], + keywords: [], + ...props, + }); + it('converts an application and remove sensitive properties', () => { const app = createApp(); const info = getAppInfo(app); @@ -31,6 +43,7 @@ describe('getAppInfo', () => { title: 'some-title', status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, + searchable: true, appRoute: `/app/some-id`, keywords: [], deepLinks: [], @@ -54,12 +67,15 @@ describe('getAppInfo', () => { title: 'some-title', status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, + searchable: true, appRoute: `/app/some-id`, keywords: [], deepLinks: [ { id: 'sub-id', title: 'sub-title', + navLinkStatus: AppNavLinkStatus.hidden, + searchable: true, keywords: [], deepLinks: [ { @@ -67,6 +83,8 @@ describe('getAppInfo', () => { title: 'sub-sub-title', path: '/sub-sub', keywords: [], + navLinkStatus: AppNavLinkStatus.hidden, + searchable: true, deepLinks: [], // default empty array added }, ], @@ -102,7 +120,70 @@ describe('getAppInfo', () => { ); }); - it('adds default meta fields to sublinks when needed', () => { + it('computes the searchable flag depending on the navLinkStatus when needed', () => { + expect( + getAppInfo( + createApp({ + navLinkStatus: AppNavLinkStatus.default, + searchable: undefined, + }) + ) + ).toEqual( + expect.objectContaining({ + searchable: true, + }) + ); + expect( + getAppInfo( + createApp({ + navLinkStatus: AppNavLinkStatus.visible, + searchable: undefined, + }) + ) + ).toEqual( + expect.objectContaining({ + searchable: true, + }) + ); + expect( + getAppInfo( + createApp({ + navLinkStatus: AppNavLinkStatus.disabled, + searchable: undefined, + }) + ) + ).toEqual( + expect.objectContaining({ + searchable: false, + }) + ); + expect( + getAppInfo( + createApp({ + navLinkStatus: AppNavLinkStatus.hidden, + searchable: undefined, + }) + ) + ).toEqual( + expect.objectContaining({ + searchable: false, + }) + ); + expect( + getAppInfo( + createApp({ + navLinkStatus: AppNavLinkStatus.hidden, + searchable: true, + }) + ) + ).toEqual( + expect.objectContaining({ + searchable: true, + }) + ); + }); + + it('adds default deepLinks when needed', () => { const app = createApp({ deepLinks: [ { @@ -126,17 +207,22 @@ describe('getAppInfo', () => { title: 'some-title', status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, + searchable: true, appRoute: `/app/some-id`, keywords: [], deepLinks: [ { id: 'sub-id', title: 'sub-title', - keywords: [], // default empty array + navLinkStatus: AppNavLinkStatus.hidden, + searchable: true, + keywords: [], deepLinks: [ { id: 'sub-sub-id', title: 'sub-sub-title', + navLinkStatus: AppNavLinkStatus.hidden, + searchable: true, path: '/sub-sub', keywords: ['sub sub'], deepLinks: [], @@ -146,4 +232,127 @@ describe('getAppInfo', () => { ], }); }); + + it('computes the deepLinks navLinkStatus when needed', () => { + expect( + getAppInfo( + createApp({ + deepLinks: [ + createDeepLink({ + navLinkStatus: AppNavLinkStatus.visible, + }), + ], + }) + ) + ).toEqual( + expect.objectContaining({ + deepLinks: [ + expect.objectContaining({ + navLinkStatus: AppNavLinkStatus.visible, + }), + ], + }) + ); + expect( + getAppInfo( + createApp({ + deepLinks: [ + createDeepLink({ + navLinkStatus: AppNavLinkStatus.default, + }), + ], + }) + ) + ).toEqual( + expect.objectContaining({ + deepLinks: [ + expect.objectContaining({ + navLinkStatus: AppNavLinkStatus.hidden, + }), + ], + }) + ); + expect( + getAppInfo( + createApp({ + deepLinks: [ + createDeepLink({ + navLinkStatus: undefined, + }), + ], + }) + ) + ).toEqual( + expect.objectContaining({ + deepLinks: [ + expect.objectContaining({ + navLinkStatus: AppNavLinkStatus.hidden, + }), + ], + }) + ); + }); + + it('computes the deepLinks searchable depending on the navLinkStatus when needed', () => { + expect( + getAppInfo( + createApp({ + deepLinks: [ + createDeepLink({ + navLinkStatus: AppNavLinkStatus.default, + searchable: undefined, + }), + ], + }) + ) + ).toEqual( + expect.objectContaining({ + deepLinks: [ + expect.objectContaining({ + searchable: true, + }), + ], + }) + ); + expect( + getAppInfo( + createApp({ + deepLinks: [ + createDeepLink({ + navLinkStatus: AppNavLinkStatus.hidden, + searchable: undefined, + }), + ], + }) + ) + ).toEqual( + expect.objectContaining({ + deepLinks: [ + expect.objectContaining({ + searchable: false, + }), + ], + }) + ); + expect( + getAppInfo( + createApp({ + deepLinks: [ + createDeepLink({ + navLinkStatus: AppNavLinkStatus.hidden, + searchable: true, + }), + ], + }) + ) + ).toEqual( + expect.objectContaining({ + deepLinks: [ + expect.objectContaining({ + searchable: true, + }), + ], + }) + ); + }); }); diff --git a/src/core/public/application/utils/get_app_info.ts b/src/core/public/application/utils/get_app_info.ts index 4c94e24f501bc4..6c753b7a71a0f7 100644 --- a/src/core/public/application/utils/get_app_info.ts +++ b/src/core/public/application/utils/get_app_info.ts @@ -16,17 +16,19 @@ import { } from '../types'; export function getAppInfo(app: App): PublicAppInfo { - const navLinkStatus = - app.navLinkStatus === AppNavLinkStatus.default - ? app.status === AppStatus.inaccessible - ? AppNavLinkStatus.hidden - : AppNavLinkStatus.visible - : app.navLinkStatus!; - const { updater$, mount, ...infos } = app; + const { updater$, mount, navLinkStatus = AppNavLinkStatus.default, ...infos } = app; return { ...infos, status: app.status!, - navLinkStatus, + navLinkStatus: + navLinkStatus === AppNavLinkStatus.default + ? app.status === AppStatus.inaccessible + ? AppNavLinkStatus.hidden + : AppNavLinkStatus.visible + : navLinkStatus, + searchable: + app.searchable ?? + (navLinkStatus === AppNavLinkStatus.default || navLinkStatus === AppNavLinkStatus.visible), appRoute: app.appRoute!, keywords: app.keywords ?? [], deepLinks: getDeepLinkInfos(app.deepLinks), @@ -37,17 +39,18 @@ function getDeepLinkInfos(deepLinks?: AppDeepLink[]): PublicAppDeepLinkInfo[] { if (!deepLinks) return []; return deepLinks.map( - (rawDeepLink): PublicAppDeepLinkInfo => { - const navLinkStatus = - rawDeepLink.navLinkStatus === AppNavLinkStatus.default - ? AppNavLinkStatus.hidden - : rawDeepLink.navLinkStatus!; + ({ navLinkStatus = AppNavLinkStatus.default, ...rawDeepLink }): PublicAppDeepLinkInfo => { return { id: rawDeepLink.id, title: rawDeepLink.title, path: rawDeepLink.path, keywords: rawDeepLink.keywords ?? [], - navLinkStatus, + navLinkStatus: + navLinkStatus === AppNavLinkStatus.default ? AppNavLinkStatus.hidden : navLinkStatus, + searchable: + rawDeepLink.searchable ?? + (navLinkStatus === AppNavLinkStatus.default || + navLinkStatus === AppNavLinkStatus.visible), deepLinks: getDeepLinkInfos(rawDeepLink.deepLinks), }; } diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index 87175ea465b7f1..4e9158cac5cd47 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -33,10 +33,9 @@ export interface ChromeNavLink { readonly baseUrl: string; /** - * The route used to open the {@link AppBase.defaultPath | default path } of an application. - * If unset, `baseUrl` will be used instead. + * The route used to open the default path and the deep links of an application. */ - readonly url?: string; + readonly url: string; /** * An ordinal used to sort nav links relative to one another for display. diff --git a/src/core/public/chrome/nav_links/nav_links_service.test.ts b/src/core/public/chrome/nav_links/nav_links_service.test.ts index afb902fd6bd83d..e1d537da6959b7 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.test.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.test.ts @@ -20,6 +20,22 @@ const availableApps = new Map([ order: -10, title: 'App 2', euiIconType: 'canvasApp', + deepLinks: [ + { + id: 'deepApp1', + order: 50, + title: 'Deep App 1', + path: '/deepapp1', + deepLinks: [ + { + id: 'deepApp2', + order: 40, + title: 'Deep App 2', + path: '/deepapp2', + }, + ], + }, + ], }, ], ['chromelessApp', { id: 'chromelessApp', order: 20, title: 'Chromless App', chromeless: true }], @@ -66,7 +82,7 @@ describe('NavLinksService', () => { map((links) => links.map((l) => l.id)) ) .toPromise() - ).toEqual(['app2', 'app1']); + ).toEqual(['app2', 'app1', 'app2:deepApp2', 'app2:deepApp1']); }); it('emits multiple values', async () => { @@ -76,7 +92,7 @@ describe('NavLinksService', () => { start.showOnly('app1'); service.stop(); - expect(emittedLinks).toEqual([['app2', 'app1'], ['app1']]); + expect(emittedLinks).toEqual([['app2', 'app1', 'app2:deepApp2', 'app2:deepApp1'], ['app1']]); }); it('completes when service is stopped', async () => { @@ -98,7 +114,12 @@ describe('NavLinksService', () => { describe('#getAll()', () => { it('returns a sorted array of navlinks', () => { - expect(start.getAll().map((l) => l.id)).toEqual(['app2', 'app1']); + expect(start.getAll().map((l) => l.id)).toEqual([ + 'app2', + 'app1', + 'app2:deepApp2', + 'app2:deepApp1', + ]); }); }); @@ -123,7 +144,7 @@ describe('NavLinksService', () => { map((links) => links.map((l) => l.id)) ) .toPromise() - ).toEqual(['app2', 'app1']); + ).toEqual(['app2', 'app1', 'app2:deepApp2', 'app2:deepApp1']); }); it('does nothing on chromeless applications', async () => { @@ -136,7 +157,7 @@ describe('NavLinksService', () => { map((links) => links.map((l) => l.id)) ) .toPromise() - ).toEqual(['app2', 'app1']); + ).toEqual(['app2', 'app1', 'app2:deepApp2', 'app2:deepApp1']); }); it('removes all other links', async () => { @@ -152,6 +173,19 @@ describe('NavLinksService', () => { ).toEqual(['app2']); }); + it('show only deep link', async () => { + start.showOnly('app2:deepApp1'); + expect( + await start + .getNavLinks$() + .pipe( + take(1), + map((links) => links.map((l) => l.id)) + ) + .toPromise() + ).toEqual(['app2:deepApp1']); + }); + it('still removes all other links when availableApps are re-emitted', async () => { start.showOnly('app2'); mockAppService.applications$.next(mockAppService.applications$.value); diff --git a/src/core/public/chrome/nav_links/nav_links_service.ts b/src/core/public/chrome/nav_links/nav_links_service.ts index d41d8ae964d622..af961987a63095 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -10,8 +10,8 @@ import { sortBy } from 'lodash'; import { BehaviorSubject, combineLatest, Observable, ReplaySubject } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; -import { InternalApplicationStart } from '../../application'; -import { HttpStart } from '../../http'; +import { InternalApplicationStart, PublicAppDeepLinkInfo, PublicAppInfo } from '../../application'; +import { HttpStart, IBasePath } from '../../http'; import { ChromeNavLink, NavLinkWrapper } from './nav_link'; import { toNavLink } from './to_nav_link'; @@ -89,7 +89,13 @@ export class NavLinksService { return new Map( [...apps] .filter(([, app]) => !app.chromeless) - .map(([appId, app]) => [appId, toNavLink(app, http.basePath)]) + .reduce((navLinks: Array<[string, NavLinkWrapper]>, [appId, app]) => { + navLinks.push( + [appId, toNavLink(app, http.basePath)], + ...toNavDeepLinks(app, app.deepLinks, http.basePath) + ); + return navLinks; + }, []) ); }) ); @@ -163,3 +169,21 @@ function sortNavLinks(navLinks: ReadonlyMap) { 'order' ); } + +function toNavDeepLinks( + app: PublicAppInfo, + deepLinks: PublicAppDeepLinkInfo[], + basePath: IBasePath +): Array<[string, NavLinkWrapper]> { + if (!deepLinks) { + return []; + } + return deepLinks.reduce((navDeepLinks: Array<[string, NavLinkWrapper]>, deepLink) => { + const id = `${app.id}:${deepLink.id}`; + if (deepLink.path) { + navDeepLinks.push([id, toNavLink(app, basePath, { ...deepLink, id })]); + } + navDeepLinks.push(...toNavDeepLinks(app, deepLink.deepLinks, basePath)); + return navDeepLinks; + }, []); +} diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts index db783d0028f075..9791f1bd1354b6 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.test.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -6,7 +6,12 @@ * Side Public License, v 1. */ -import { PublicAppInfo, AppNavLinkStatus, AppStatus } from '../../application'; +import { + PublicAppInfo, + AppNavLinkStatus, + AppStatus, + PublicAppDeepLinkInfo, +} from '../../application'; import { toNavLink } from './to_nav_link'; import { httpServiceMock } from '../../mocks'; @@ -16,12 +21,24 @@ const app = (props: Partial = {}): PublicAppInfo => ({ title: 'some-title', status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.default, + searchable: true, appRoute: `/app/some-id`, keywords: [], deepLinks: [], ...props, }); +const deepLink = (props: Partial = {}): PublicAppDeepLinkInfo => ({ + id: 'some-deep-link-id', + title: 'my deep link', + path: '/my-deep-link', + navLinkStatus: AppNavLinkStatus.default, + searchable: true, + deepLinks: [], + keywords: [], + ...props, +}); + describe('toNavLink', () => { const basePath = httpServiceMock.createSetupContract({ basePath: '/base-path' }).basePath; @@ -64,7 +81,7 @@ describe('toNavLink', () => { }), basePath ); - expect(link.properties.url).toEqual('http://localhost/base-path/my-route/my-path'); + expect(link.properties.url).toEqual('/base-path/my-route/my-path'); link = toNavLink( app({ @@ -73,9 +90,7 @@ describe('toNavLink', () => { }), basePath ); - expect(link.properties.url).toEqual( - 'http://localhost/base-path/my-route/my-path/some/default/path' - ); + expect(link.properties.url).toEqual('/base-path/my-route/my-path/some/default/path'); }); it('uses the application status when the navLinkStatus is set to default', () => { @@ -153,4 +168,90 @@ describe('toNavLink', () => { }) ); }); + + describe('deepLink parameter', () => { + it('should be hidden and not disabled by default', () => { + expect(toNavLink(app(), basePath, deepLink()).properties).toEqual( + expect.objectContaining({ + disabled: false, + hidden: true, + }) + ); + }); + + it('should not be hidden when navLinkStatus is visible', () => { + expect( + toNavLink( + app(), + basePath, + deepLink({ + navLinkStatus: AppNavLinkStatus.visible, + }) + ).properties + ).toEqual( + expect.objectContaining({ + disabled: false, + hidden: false, + }) + ); + }); + + it('should be disabled when navLinkStatus is disabled', () => { + expect( + toNavLink( + app(), + basePath, + deepLink({ + navLinkStatus: AppNavLinkStatus.disabled, + }) + ).properties + ).toEqual( + expect.objectContaining({ + disabled: true, + hidden: false, + }) + ); + }); + + it('should have href, baseUrl and url containing the path', () => { + const testApp = app({ + appRoute: '/app/app-id', + defaultPath: '/default-path', + }); + + expect(toNavLink(testApp, basePath).properties).toEqual( + expect.objectContaining({ + baseUrl: 'http://localhost/base-path/app/app-id', + url: '/base-path/app/app-id/default-path', + href: 'http://localhost/base-path/app/app-id/default-path', + }) + ); + + expect( + toNavLink( + testApp, + basePath, + deepLink({ + id: 'deep-link-id', + path: '/my-deep-link', + }) + ).properties + ).toEqual( + expect.objectContaining({ + baseUrl: 'http://localhost/base-path/app/app-id', + url: '/base-path/app/app-id/my-deep-link', + href: 'http://localhost/base-path/app/app-id/my-deep-link', + }) + ); + }); + + it('should use the main app category', () => { + expect(toNavLink(app(), basePath, deepLink()).properties.category).toBeUndefined(); + + const category = { id: 'some-category', label: 'some category' }; + expect(toNavLink(app({ category }), basePath, deepLink()).properties.category).toEqual( + category + ); + }); + }); }); diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts index f56d496f42d8f6..6acf37d40f089b 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.ts @@ -6,29 +6,50 @@ * Side Public License, v 1. */ -import { PublicAppInfo, AppNavLinkStatus, AppStatus } from '../../application'; +import { + PublicAppInfo, + AppNavLinkStatus, + AppStatus, + PublicAppDeepLinkInfo, +} from '../../application'; import { IBasePath } from '../../http'; import { NavLinkWrapper } from './nav_link'; import { appendAppPath } from '../../application/utils'; -export function toNavLink(app: PublicAppInfo, basePath: IBasePath): NavLinkWrapper { - const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default; +export function toNavLink( + app: PublicAppInfo, + basePath: IBasePath, + deepLink?: PublicAppDeepLinkInfo +): NavLinkWrapper { const relativeBaseUrl = basePath.prepend(app.appRoute!); - const url = relativeToAbsolute(appendAppPath(relativeBaseUrl, app.defaultPath)); + const url = appendAppPath(relativeBaseUrl, deepLink?.path || app.defaultPath); + const href = relativeToAbsolute(url); const baseUrl = relativeToAbsolute(relativeBaseUrl); return new NavLinkWrapper({ - ...app, - hidden: useAppStatus - ? app.status === AppStatus.inaccessible - : app.navLinkStatus === AppNavLinkStatus.hidden, - disabled: useAppStatus ? false : app.navLinkStatus === AppNavLinkStatus.disabled, + ...(deepLink || app), + ...(app.category ? { category: app.category } : {}), // deepLinks use the main app category + hidden: deepLink ? isDeepNavLinkHidden(deepLink) : isAppNavLinkHidden(app), + disabled: (deepLink?.navLinkStatus ?? app.navLinkStatus) === AppNavLinkStatus.disabled, baseUrl, - href: url, + href, url, }); } +function isAppNavLinkHidden(app: PublicAppInfo) { + return app.navLinkStatus === AppNavLinkStatus.default + ? app.status === AppStatus.inaccessible + : app.navLinkStatus === AppNavLinkStatus.hidden; +} + +function isDeepNavLinkHidden(deepLink: PublicAppDeepLinkInfo) { + return ( + deepLink.navLinkStatus === AppNavLinkStatus.default || + deepLink.navLinkStatus === AppNavLinkStatus.hidden + ); +} + /** * @param {string} url - a relative or root relative url. If a relative path is given then the * absolute url returned will depend on the current page where this function is called from. For example diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 0f5efe667ec2fc..3668829a6888cd 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -73,6 +73,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "id": "Custom link", "isActive": true, "title": "Custom link", + "url": "/", }, "closed": false, "hasError": false, @@ -140,6 +141,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "id": "discover", "isActive": true, "title": "discover", + "url": "/", }, Object { "baseUrl": "/", @@ -154,6 +156,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "id": "siem", "isActive": true, "title": "siem", + "url": "/", }, Object { "baseUrl": "/", @@ -168,6 +171,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "id": "metrics", "isActive": true, "title": "metrics", + "url": "/", }, Object { "baseUrl": "/", @@ -182,6 +186,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "id": "monitoring", "isActive": true, "title": "monitoring", + "url": "/", }, Object { "baseUrl": "/", @@ -196,6 +201,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "id": "visualize", "isActive": true, "title": "visualize", + "url": "/", }, Object { "baseUrl": "/", @@ -210,6 +216,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "id": "dashboard", "isActive": true, "title": "dashboard", + "url": "/", }, Object { "baseUrl": "/", @@ -219,6 +226,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "id": "canvas", "isActive": true, "title": "canvas", + "url": "/", }, Object { "baseUrl": "/", @@ -233,6 +241,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "id": "logs", "isActive": true, "title": "logs", + "url": "/", }, ], "closed": false, @@ -351,6 +360,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "values": Array [], } } + url="/" > ) { id: title, href: title, baseUrl: '/', + url: '/', isActive: true, 'data-test-subj': title, }; @@ -50,6 +51,7 @@ function mockProps() { isLocked: false, isNavOpen: false, homeHref: '/', + url: '/', navLinks$: new BehaviorSubject([]), recentlyAccessed$: new BehaviorSubject([]), storage: new StubBrowserStorage(), diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index b5b2919b659bdf..0e85dbea237cb2 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -107,7 +107,7 @@ export function CollapsibleNav({ link, appId, dataTestSubj: 'collapsibleNavAppLink', - navigateToApp, + navigateToUrl, onClick: closeNav, ...(needsIcon && { basePath }), }); @@ -137,7 +137,7 @@ export function CollapsibleNav({ createEuiListItem({ link: customNavLink, basePath, - navigateToApp, + navigateToUrl, dataTestSubj: 'collapsibleNavCustomNavLink', onClick: closeNav, externalLink: true, diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index e0f10fe66c9f06..fdbdde8556eebf 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -61,13 +61,14 @@ describe('Header', () => { const breadcrumbs$ = new BehaviorSubject([{ text: 'test' }]); const isLocked$ = new BehaviorSubject(false); const navLinks$ = new BehaviorSubject([ - { id: 'kibana', title: 'kibana', baseUrl: '', href: '' }, + { id: 'kibana', title: 'kibana', baseUrl: '', href: '', url: '' }, ]); const headerBanner$ = new BehaviorSubject(undefined); const customNavLink$ = new BehaviorSubject({ id: 'cloud-deployment-link', title: 'Manage cloud deployment', baseUrl: '', + url: '', href: '', }); const recentlyAccessed$ = new BehaviorSubject([ diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index 84f758e2c737e5..b0ebf7cc5f8e5e 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -23,7 +23,7 @@ interface Props { basePath?: HttpStart['basePath']; dataTestSubj: string; onClick?: Function; - navigateToApp: CoreStart['application']['navigateToApp']; + navigateToUrl: CoreStart['application']['navigateToUrl']; externalLink?: boolean; } @@ -36,11 +36,11 @@ export function createEuiListItem({ appId, basePath, onClick = () => {}, - navigateToApp, + navigateToUrl, dataTestSubj, externalLink = false, }: Props) { - const { href, id, title, disabled, euiIconType, icon, tooltip } = link; + const { href, id, title, disabled, euiIconType, icon, tooltip, url } = link; return { label: tooltip ?? title, @@ -57,7 +57,7 @@ export function createEuiListItem({ !isModifiedOrPrevented(event) ) { event.preventDefault(); - navigateToApp(id); + navigateToUrl(url); } }, isActive: appId === id, diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 24b48683cdd937..32737ff427ef3e 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -91,6 +91,7 @@ export type { AppLeaveConfirmAction, AppUpdatableFields, AppUpdater, + AppNavOptions, AppDeepLink, PublicAppInfo, PublicAppDeepLinkInfo, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 667863d29623ed..608af520540406 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -53,24 +53,21 @@ import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/type export function __kbnBootstrap__(): Promise; // @public (undocumented) -export interface App { +export interface App extends AppNavOptions { appRoute?: string; capabilities?: Partial; category?: AppCategory; chromeless?: boolean; deepLinks?: AppDeepLink[]; defaultPath?: string; - euiIconType?: string; exactRoute?: boolean; - icon?: string; id: string; keywords?: string[]; mount: AppMount; navLinkStatus?: AppNavLinkStatus; - order?: number; + searchable?: boolean; status?: AppStatus; title: string; - tooltip?: string; updater$?: Observable; } @@ -92,7 +89,8 @@ export type AppDeepLink = { title: string; keywords?: string[]; navLinkStatus?: AppNavLinkStatus; -} & ({ + searchable?: boolean; +} & AppNavOptions & ({ path: string; deepLinks?: AppDeepLink[]; } | { @@ -179,6 +177,14 @@ export enum AppNavLinkStatus { visible = 1 } +// @public +export interface AppNavOptions { + euiIconType?: string; + icon?: string; + order?: number; + tooltip?: string; +} + // @public export enum AppStatus { accessible = 0, @@ -189,7 +195,7 @@ export enum AppStatus { export type AppUnmount = () => void; // @public -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick; // @public export type AppUpdater = (app: App) => Partial | undefined; @@ -314,8 +320,7 @@ export interface ChromeNavLink { readonly order?: number; readonly title: string; readonly tooltip?: string; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AppBase" - readonly url?: string; + readonly url: string; } // @public @@ -916,10 +921,9 @@ export interface IUiSettingsClient { // @public export type MountPoint = (element: T) => UnmountCallback; -// Warning: (ae-missing-release-tag) "NavigateToAppOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface NavigateToAppOptions { + deepLinkId?: string; openInNewTab?: boolean; path?: string; replace?: boolean; @@ -1067,19 +1071,21 @@ export interface PluginInitializerContext export type PluginOpaqueId = symbol; // @public -export type PublicAppDeepLinkInfo = Omit & { +export type PublicAppDeepLinkInfo = Omit & { deepLinks: PublicAppDeepLinkInfo[]; keywords: string[]; navLinkStatus: AppNavLinkStatus; + searchable: boolean; }; // @public -export type PublicAppInfo = Omit & { +export type PublicAppInfo = Omit & { status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; keywords: string[]; deepLinks: PublicAppDeepLinkInfo[]; + searchable: boolean; }; // @public diff --git a/test/plugin_functional/plugins/core_plugin_deep_links/kibana.json b/test/plugin_functional/plugins/core_plugin_deep_links/kibana.json new file mode 100644 index 00000000000000..539550974c563a --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deep_links/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "corePluginDeepLinks", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["core_plugin_deep_links"], + "ui": true +} diff --git a/test/plugin_functional/plugins/core_plugin_deep_links/package.json b/test/plugin_functional/plugins/core_plugin_deep_links/package.json new file mode 100644 index 00000000000000..3fe16ddee53214 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deep_links/package.json @@ -0,0 +1,14 @@ +{ + "name": "core_plugin_deep_links", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/core_plugin_deep_links", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "SSPL-1.0 OR Elastic License 2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../node_modules/.bin/tsc" + } +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/core_plugin_deep_links/public/application.tsx b/test/plugin_functional/plugins/core_plugin_deep_links/public/application.tsx new file mode 100644 index 00000000000000..90814bd552d37e --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deep_links/public/application.tsx @@ -0,0 +1,158 @@ +/* + * 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 { History } from 'history'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Router, Route, withRouter, RouteComponentProps, Redirect } from 'react-router-dom'; + +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiPageSideBar, + EuiTitle, + EuiSideNav, +} from '@elastic/eui'; + +import { CoreStart, AppMountParameters } from 'kibana/public'; + +const Home = () => ( + + + + +

Welcome to DL!

+
+
+
+ + + + +

DL home page section title

+
+
+
+ Wow this is the content! +
+
+); + +const PageA = () => ( + + + + +

DL Page A

+
+
+
+ + + + +

DL Page A section title

+
+
+
+ DL Page A's content goes here +
+
+); + +const PageB = () => ( + + + + +

DL Page B

+
+
+
+ + + + +

DL Page B section title

+
+
+
+ DL Page B's content goes here +
+
+); + +type NavProps = RouteComponentProps & { + navigateToApp: CoreStart['application']['navigateToApp']; +}; +const Nav = withRouter(({ history, navigateToApp }: NavProps) => ( + history.push('/home'), + 'data-test-subj': 'dlNavHome', + }, + { + id: 'page-a', + name: 'DL page A', + onClick: () => history.push('/page-a'), + 'data-test-subj': 'dlNavPageA', + }, + { + id: 'navigateDeepByPath', + name: 'DL section 1 page B', + onClick: () => { + navigateToApp('deeplinks', { path: '/page-b' }); + }, + 'data-test-subj': 'dlNavDeepPageB', + }, + { + id: 'navigateDeepById', + name: 'DL page A deep link', + onClick: () => { + navigateToApp('deeplinks', { deepLinkId: 'pageA' }); + }, + 'data-test-subj': 'dlNavDeepPageAById', + }, + ], + }, + ]} + /> +)); + +const DlApp = ({ history, coreStart }: { history: History; coreStart: CoreStart }) => ( + + + +

} - body={

{EMPTY_ROLE_MAPPINGS_BODY}

} - actions={addMappingButton} + const roleMappingsSection = ( + <> + initializeRoleMapping()} /> + - - ); - - const roleMappingsTable = ( - + ); return ( <> - - {roleMappingFlyoutOpen && } - 0}> - - - {roleMappings.length === 0 ? roleMappingEmptyState : roleMappingsTable} - - + + {roleMappingsSection} ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts index 47201988e8732e..be0c860627f799 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts @@ -7,6 +7,8 @@ import { i18n } from '@kbn/i18n'; +import { ProductName } from '../types'; + export const ANY_AUTH_PROVIDER = '*'; export const ANY_AUTH_PROVIDER_OPTION_LABEL = i18n.translate( @@ -104,7 +106,7 @@ export const DELETE_ROLE_MAPPING_BUTTON = i18n.translate( export const FILTER_ROLE_MAPPINGS_PLACEHOLDER = i18n.translate( 'xpack.enterpriseSearch.roleMapping.filterRoleMappingsPlaceholder', { - defaultMessage: 'Filter roles...', + defaultMessage: 'Filter role mappings', } ); @@ -125,21 +127,6 @@ export const MANAGE_ROLE_MAPPING_TITLE = i18n.translate( { defaultMessage: 'Manage role mapping' } ); -export const EMPTY_ROLE_MAPPINGS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.roleMapping.emptyRoleMappingsTitle', - { - defaultMessage: 'No role mappings yet', - } -); - -export const ROLE_MAPPINGS_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.roleMapping.roleMappingsDescription', - { - defaultMessage: - 'Define role mappings for elasticsearch-native and elasticsearch-saml authentication.', - } -); - export const ROLE_MAPPING_NOT_FOUND = i18n.translate( 'xpack.enterpriseSearch.roleMapping.notFoundMessage', { @@ -168,13 +155,6 @@ export const ROLE_MAPPING_FLYOUT_DESCRIPTION = i18n.translate( } ); -export const ROLE_MAPPING_ADD_BUTTON = i18n.translate( - 'xpack.enterpriseSearch.roleMapping.roleMappingAddButton', - { - defaultMessage: 'Add mapping', - } -); - export const ROLE_MAPPING_FLYOUT_CREATE_BUTTON = i18n.translate( 'xpack.enterpriseSearch.roleMapping.roleMappingFlyoutCreateButton', { @@ -198,3 +178,25 @@ export const UPDATE_ROLE_MAPPING = i18n.translate( 'xpack.enterpriseSearch.roleMapping.updateRoleMappingButtonLabel', { defaultMessage: 'Update role mapping' } ); + +export const ROLE_MAPPINGS_HEADING_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.roleMappingsHeadingTitle', + { defaultMessage: 'Role mappings' } +); + +export const ROLE_MAPPINGS_HEADING_DESCRIPTION = (productName: ProductName) => + i18n.translate('xpack.enterpriseSearch.roleMapping.roleMappingsHeadingDescription', { + defaultMessage: + 'Role mappings provide an interface to associate native or SAML-governed role attributes with {productName} permissions.', + values: { productName }, + }); + +export const ROLE_MAPPINGS_HEADING_DOCS_LINK = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.roleMappingsHeadingDocsLink', + { defaultMessage: 'Learn more about role mappings' } +); + +export const ROLE_MAPPINGS_HEADING_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.roleMappingsHeadingButton', + { defaultMessage: 'Create a new role mapping' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts index 6f67bc682f3336..0f9362157f50ad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts @@ -10,3 +10,4 @@ export { RoleMappingsTable } from './role_mappings_table'; export { RoleOptionLabel } from './role_option_label'; export { RoleSelector } from './role_selector'; export { RoleMappingFlyout } from './role_mapping_flyout'; +export { RoleMappingsHeading } from './role_mappings_heading'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx new file mode 100644 index 00000000000000..f0bf86fb306c65 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiTitle, EuiLink, EuiButton, EuiText } from '@elastic/eui'; + +import { RoleMappingsHeading } from './role_mappings_heading'; + +describe('RoleMappingsHeading', () => { + it('renders ', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTitle)).toHaveLength(1); + expect(wrapper.find(EuiText)).toHaveLength(1); + expect(wrapper.find(EuiLink)).toHaveLength(1); + expect(wrapper.find(EuiButton)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx new file mode 100644 index 00000000000000..b2143c6ff44028 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { ProductName } from '../types'; + +import { + ROLE_MAPPINGS_HEADING_TITLE, + ROLE_MAPPINGS_HEADING_DESCRIPTION, + ROLE_MAPPINGS_HEADING_DOCS_LINK, + ROLE_MAPPINGS_HEADING_BUTTON, +} from './constants'; + +interface Props { + productName: ProductName; + onClick(): void; +} + +// TODO: Replace EuiLink href with acutal docs link when available +const ROLE_MAPPINGS_DOCS_HREF = '#TODO'; + +export const RoleMappingsHeading: React.FC = ({ productName, onClick }) => ( + <> + + + +

{ROLE_MAPPINGS_HEADING_TITLE}

+
+ + +

+ {ROLE_MAPPINGS_HEADING_DESCRIPTION(productName)}{' '} + + {ROLE_MAPPINGS_HEADING_DOCS_LINK} + +

+
+
+ + + {ROLE_MAPPINGS_HEADING_BUTTON} + + +
+ + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx index d5d6d8b9cd227b..f185e9ffb418ee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx @@ -10,8 +10,6 @@ import React, { Fragment, useState } from 'react'; import { EuiButtonIcon, EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, EuiIconTip, EuiSpacer, EuiTable, @@ -54,7 +52,6 @@ interface Props { accessItemKey: 'groups' | 'engines'; accessHeader: string; roleMappings: Array; - addMappingButton: React.ReactNode; accessAllEngines?: boolean; shouldShowAuthProvider?: boolean; initializeRoleMapping(roleMappingId: string): void; @@ -72,7 +69,6 @@ export const RoleMappingsTable: React.FC = ({ accessItemKey, accessHeader, roleMappings, - addMappingButton, shouldShowAuthProvider, initializeRoleMapping, handleDeleteMapping, @@ -117,16 +113,11 @@ export const RoleMappingsTable: React.FC = ({ return ( <> - - - updateValue(e.target.value)} - /> - - {addMappingButton} - + updateValue(e.target.value)} + /> {filteredResults.length > 0 ? ( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 2cdadc1c6b0d30..f450ca556ebe25 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -35,3 +35,5 @@ export interface RoleMapping { content: string; }; } + +export type ProductName = 'App Search' | 'Workplace Search'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts index 62494b447efa0b..92c8b7827b9b67 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts @@ -73,14 +73,6 @@ export const GROUP_ASSIGNMENT_LABEL = i18n.translate( } ); -export const EMPTY_ROLE_MAPPINGS_BODY = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.roleMapping.emptyRoleMappingsBody', - { - defaultMessage: - 'New team members are assigned the admin role by default. An admin can access everything. Create a new role to override the default.', - } -); - export const ROLE_MAPPINGS_TABLE_HEADER = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsTableHeader', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx index d7be753eec173e..9559df2c1f9815 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx @@ -12,10 +12,8 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; - import { Loading } from '../../../shared/loading'; -import { RoleMappingsTable } from '../../../shared/role_mapping'; +import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping'; import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; import { RoleMapping } from './role_mapping'; @@ -53,13 +51,6 @@ describe('RoleMappings', () => { expect(wrapper.find(Loading)).toHaveLength(1); }); - it('renders empty state', () => { - setMockValues({ ...mockValues, roleMappings: [] }); - const wrapper = shallow(); - - expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); - }); - it('renders RoleMapping flyout', () => { setMockValues({ ...mockValues, roleMappingFlyoutOpen: true }); const wrapper = shallow(); @@ -67,10 +58,9 @@ describe('RoleMappings', () => { expect(wrapper.find(RoleMapping)).toHaveLength(1); }); - it('handles button click', () => { - setMockValues({ ...mockValues, roleMappings: [] }); + it('handles onClick', () => { const wrapper = shallow(); - wrapper.find(EuiEmptyPrompt).dive().find(EuiButton).simulate('click'); + wrapper.find(RoleMappingsHeading).prop('onClick')(); expect(initializeRoleMapping).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx index e5a9a31d463b89..46c426c3dad2a5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx @@ -9,21 +9,13 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; - import { FlashMessages } from '../../../shared/flash_messages'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { Loading } from '../../../shared/loading'; -import { RoleMappingsTable } from '../../../shared/role_mapping'; -import { - EMPTY_ROLE_MAPPINGS_TITLE, - ROLE_MAPPING_ADD_BUTTON, - ROLE_MAPPINGS_TITLE, - ROLE_MAPPINGS_DESCRIPTION, -} from '../../../shared/role_mapping/constants'; -import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping'; +import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; -import { EMPTY_ROLE_MAPPINGS_BODY, ROLE_MAPPINGS_TABLE_HEADER } from './constants'; +import { ROLE_MAPPINGS_TABLE_HEADER } from './constants'; import { RoleMapping } from './role_mapping'; import { RoleMappingsLogic } from './role_mappings_logic'; @@ -46,43 +38,26 @@ export const RoleMappings: React.FC = () => { if (dataLoading) return ; - const addMappingButton = ( - initializeRoleMapping()}> - {ROLE_MAPPING_ADD_BUTTON} - - ); - const emptyPrompt = ( - - {EMPTY_ROLE_MAPPINGS_TITLE}} - body={

{EMPTY_ROLE_MAPPINGS_BODY}

} - actions={addMappingButton} + const roleMappingsSection = ( + <> + initializeRoleMapping()} /> + -
- ); - const roleMappingsTable = ( - + ); return ( <> - - {roleMappingFlyoutOpen && } -
- - {roleMappings.length === 0 ? emptyPrompt : roleMappingsTable} -
+ + {roleMappingsSection} ); }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e8d47d4dfc17ee..c571e2da6f76f5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7902,7 +7902,6 @@ "xpack.enterpriseSearch.appSearch.result.documentDetailLink": "ドキュメントの詳細を表示", "xpack.enterpriseSearch.appSearch.result.hideAdditionalFields": "追加フィールドを非表示", "xpack.enterpriseSearch.appSearch.result.title": "ドキュメント{id}", - "xpack.enterpriseSearch.appSearch.roleMapping.emptyRoleMappingsBody": "認証が成功したすべてのユーザーには所有者ロールが割り当てられ、すべてのエンジンにアクセスできます。デフォルト設定を無効にするには、新しいロールを追加します。", "xpack.enterpriseSearch.appSearch.roleMappingCreatedMessage": "ロールマッピングが正常に作成されました。", "xpack.enterpriseSearch.appSearch.roleMappingDeletedMessage": "ロールマッピングが正常に削除されました", "xpack.enterpriseSearch.appSearch.roleMappingsEngineAccessHeading": "エンジンアクセス", @@ -7992,7 +7991,6 @@ "xpack.enterpriseSearch.roleMapping.deleteRoleMappingButton": "マッピングを削除", "xpack.enterpriseSearch.roleMapping.deleteRoleMappingDescription": "マッピングの削除は永久的であり、元に戻すことはできません", "xpack.enterpriseSearch.roleMapping.deleteRoleMappingTitle": "このロールマッピングを削除", - "xpack.enterpriseSearch.roleMapping.emptyRoleMappingsTitle": "ロールマッピングがありません", "xpack.enterpriseSearch.roleMapping.externalAttributeLabel": "外部属性", "xpack.enterpriseSearch.roleMapping.filterRoleMappingsPlaceholder": "ロールをフィルタリング...", "xpack.enterpriseSearch.roleMapping.individualAuthProviderLabel": "個別の認証プロバイダーを選択", @@ -8000,7 +7998,6 @@ "xpack.enterpriseSearch.roleMapping.moResults.message": "'{filterValue}'の結果が見つかりません。", "xpack.enterpriseSearch.roleMapping.newRoleMappingTitle": "ロールマッピングを追加", "xpack.enterpriseSearch.roleMapping.roleLabel": "ロール", - "xpack.enterpriseSearch.roleMapping.roleMappingsDescription": "elasticsearch-nativeおよびelasticsearch-saml認証のロールマッピングを定義します。", "xpack.enterpriseSearch.roleMapping.roleMappingsTitle": "ユーザーとロール", "xpack.enterpriseSearch.roleMapping.saveRoleMappingButtonLabel": "ロールマッピングの保存", "xpack.enterpriseSearch.roleMapping.updateRoleMappingButtonLabel": "ロールマッピングを更新", @@ -8308,7 +8305,6 @@ "xpack.enterpriseSearch.workplaceSearch.roleMapping.adminRoleTypeDescription": "管理者は、コンテンツソース、グループ、ユーザー管理機能など、すべての組織レベルの設定に無制限にアクセスできます。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.defaultGroupName": "デフォルト", "xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage": "このマッピングを完全に削除しますか?このアクションは元に戻せません。一部のユーザーがアクセスを失う可能性があります。", - "xpack.enterpriseSearch.workplaceSearch.roleMapping.emptyRoleMappingsBody": "新しいチームメンバーにはデフォルトで管理者ロールが割り当てられます。管理者はすべてにアクセスできます。デフォルト設定を無効にするには、新しいロールを作成します。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentInvalidError": "1つ以上の割り当てられたグループが必要です。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsTableHeader": "グループアクセス", "xpack.enterpriseSearch.workplaceSearch.roleMapping.userRoleTypeDescription": "ユーザーの機能アクセスは検索インターフェースと個人設定管理に制限されます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index db211f5a0f7867..bb0b740609594b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7970,7 +7970,6 @@ "xpack.enterpriseSearch.appSearch.result.hideAdditionalFields": "隐藏其他字段", "xpack.enterpriseSearch.appSearch.result.showAdditionalFields": "显示其他 {numberOfAdditionalFields, number} 个{numberOfAdditionalFields, plural, other {字段}}", "xpack.enterpriseSearch.appSearch.result.title": "文档 {id}", - "xpack.enterpriseSearch.appSearch.roleMapping.emptyRoleMappingsBody": "成功验证的所有用户将被分配所有者角色,可访问所有引擎。添加新角色以覆盖默认值。", "xpack.enterpriseSearch.appSearch.roleMappingCreatedMessage": "角色映射已成功创建。", "xpack.enterpriseSearch.appSearch.roleMappingDeletedMessage": "已成功删除角色映射", "xpack.enterpriseSearch.appSearch.roleMappingsEngineAccessHeading": "引擎访问", @@ -8060,7 +8059,6 @@ "xpack.enterpriseSearch.roleMapping.deleteRoleMappingButton": "删除映射", "xpack.enterpriseSearch.roleMapping.deleteRoleMappingDescription": "请注意,删除映射是永久性的,无法撤消", "xpack.enterpriseSearch.roleMapping.deleteRoleMappingTitle": "移除此角色映射", - "xpack.enterpriseSearch.roleMapping.emptyRoleMappingsTitle": "尚未有角色映射", "xpack.enterpriseSearch.roleMapping.externalAttributeLabel": "外部属性", "xpack.enterpriseSearch.roleMapping.filterRoleMappingsPlaceholder": "筛选角色......", "xpack.enterpriseSearch.roleMapping.individualAuthProviderLabel": "选择单个身份验证提供程序", @@ -8068,7 +8066,6 @@ "xpack.enterpriseSearch.roleMapping.moResults.message": "找不到“{filterValue}”的结果", "xpack.enterpriseSearch.roleMapping.newRoleMappingTitle": "添加角色映射", "xpack.enterpriseSearch.roleMapping.roleLabel": "角色", - "xpack.enterpriseSearch.roleMapping.roleMappingsDescription": "为 elasticsearch-native 和 elasticsearch-saml 身份验证定义角色映射。", "xpack.enterpriseSearch.roleMapping.roleMappingsTitle": "用户和角色", "xpack.enterpriseSearch.roleMapping.saveRoleMappingButtonLabel": "保存角色映射", "xpack.enterpriseSearch.roleMapping.updateRoleMappingButtonLabel": "更新角色映射", @@ -8376,7 +8373,6 @@ "xpack.enterpriseSearch.workplaceSearch.roleMapping.adminRoleTypeDescription": "管理员对所有组织范围设置 (包括内容源、组和用户管理功能) 具有完全权限。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.defaultGroupName": "默认", "xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage": "确定要永久删除此映射?此操作不可逆转,且某些用户可能会失去访问权限。", - "xpack.enterpriseSearch.workplaceSearch.roleMapping.emptyRoleMappingsBody": "默认情况下,会为新团队成员分配管理员角色。管理员可以访问任何内容。超级新角色以覆盖默认值。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentInvalidError": "至少需要一个分配的组。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsTableHeader": "组访问权限", "xpack.enterpriseSearch.workplaceSearch.roleMapping.userRoleTypeDescription": "用户的功能访问权限仅限于搜索界面和个人设置管理。", From f0e2a50da44c1b6741d3734ac81418257acad68a Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Tue, 8 Jun 2021 14:51:31 +0100 Subject: [PATCH 10/10] Collect additional fields for alert telemetry. (#101578) --- .../security_solution/server/lib/telemetry/sender.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index b47edbb21d178b..baf4fb2d2cfd0d 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -293,6 +293,9 @@ const allowlistProcessFields: AllowlistFields = { command_line: true, hash: true, pid: true, + pe: { + original_file_name: true, + }, uptime: true, Ext: { architecture: true, @@ -313,6 +316,9 @@ const allowlistBaseEventFields: AllowlistFields = { path: true, code_signature: true, malware_signature: true, + pe: { + original_file_name: true, + }, }, event: true, file: { @@ -326,6 +332,7 @@ const allowlistBaseEventFields: AllowlistFields = { hash: true, Ext: { code_signature: true, + header_data: true, malware_classification: true, malware_signature: true, quarantine_result: true, @@ -351,6 +358,9 @@ const allowlistBaseEventFields: AllowlistFields = { ...allowlistProcessFields, }, }, + user: { + id: true, + }, }; // Allow list for the data we include in the events. True means that it is deep-cloned