mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Discover] Update layout for unified histogram (#139446)
* [Discover] Replace view mode toggle group with tabs * [Discover] Clean up new Discover tabs * [Discover] Refactor layout to have resizable sections * [Discover] Getting histogram resizing to work * [Discover] Set panel sizes on load * [Discover] Create discover_main_content component * [Discover] Improve layout resizing so chart stays fixed when window is resized * [Discover] Clean up Discover layout resize code, and implement auto resizing functionality to handle window resizing edge cases * [Discover] Improving mobile support * [Discover] Simplify histogram layout * [Discover] Fix field stats layout * [Discover] Comment flexbox CSS fix * [Discover] Refactor discover_main_content to include a fixed panels layout and a resizable panels layout, and switch to fixed panels when in mobile * [Discover] Fix Discover layout performance issues when resizing to and from mobile * [Discover] Refactor reverse portals usage to clean things up * [Discover] Rename Discover panel tsx files * [Discover] Rollback unnecessary css change * [Discover] Fix component names for Discover layout * [Discover] Fix broken discover_layout Jest test * [Discover] Decoupled discover_panels from discover_main_content to improve testability and reusability * [Discover] Clean up discover panels for testing * [Discover] Add Discover panels Jest tests * [Discover] Clean up Jest tests * [Discover] Add functional test for resizable layout panels * [Discover] Fix broken discover_layout Jest tests * [Discover] Removing unnecessary CSS in discover_panels_fixed.tsx * [Discover] Fix issue where resizable panels with extra whitespace are shown when data view is not time based, and fix a flexbox issue with fixed panels that caused content to overflow the container * [Discover] Change Discover view mode tabs to use smaller font, and force blur the Discover layout resize button after a resize * [Discover] Fix data-test-subj casing for resizable layout work * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ad0a7ac18d
commit
56680ab18c
21 changed files with 1271 additions and 168 deletions
|
@ -5,7 +5,7 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { memo, ReactElement, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
|
@ -14,7 +14,6 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiToolTip,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
|
@ -24,8 +23,6 @@ import { GetStateReturn } from '../../services/discover_state';
|
|||
import { DiscoverHistogram } from './histogram';
|
||||
import { DataCharts$, DataTotalHits$ } from '../../hooks/use_saved_search';
|
||||
import { useChartPanels } from './use_chart_panels';
|
||||
import { VIEW_MODE, DocumentViewModeToggle } from '../../../../components/view_mode_toggle';
|
||||
import { SHOW_FIELD_STATISTICS } from '../../../../../common';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import {
|
||||
getVisualizeInformation,
|
||||
|
@ -36,33 +33,32 @@ const DiscoverHistogramMemoized = memo(DiscoverHistogram);
|
|||
export const CHART_HIDDEN_KEY = 'discover:chartHidden';
|
||||
|
||||
export function DiscoverChart({
|
||||
className,
|
||||
resetSavedSearch,
|
||||
savedSearch,
|
||||
savedSearchDataChart$,
|
||||
savedSearchDataTotalHits$,
|
||||
stateContainer,
|
||||
dataView,
|
||||
viewMode,
|
||||
setDiscoverViewMode,
|
||||
hideChart,
|
||||
interval,
|
||||
isTimeBased,
|
||||
appendHistogram,
|
||||
}: {
|
||||
className?: string;
|
||||
resetSavedSearch: () => void;
|
||||
savedSearch: SavedSearch;
|
||||
savedSearchDataChart$: DataCharts$;
|
||||
savedSearchDataTotalHits$: DataTotalHits$;
|
||||
stateContainer: GetStateReturn;
|
||||
dataView: DataView;
|
||||
viewMode: VIEW_MODE;
|
||||
setDiscoverViewMode: (viewMode: VIEW_MODE) => void;
|
||||
isTimeBased: boolean;
|
||||
hideChart?: boolean;
|
||||
interval?: string;
|
||||
appendHistogram?: ReactElement;
|
||||
}) {
|
||||
const { uiSettings, data, storage } = useDiscoverServices();
|
||||
const { data, storage } = useDiscoverServices();
|
||||
const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false);
|
||||
const showViewModeToggle = uiSettings.get(SHOW_FIELD_STATISTICS) ?? false;
|
||||
|
||||
const chartRef = useRef<{ element: HTMLElement | null; moveFocus: boolean }>({
|
||||
element: null,
|
||||
|
@ -126,9 +122,15 @@ export function DiscoverChart({
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" alignItems="stretch" gutterSize="none" responsive={false}>
|
||||
<EuiFlexGroup
|
||||
className={className}
|
||||
direction="column"
|
||||
alignItems="stretch"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false} className="dscResultCount">
|
||||
<EuiFlexGroup justifyContent="spaceBetween" responsive={false}>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="none" responsive={false}>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
className="dscResultCount__title eui-textTruncate eui-textNoWrap"
|
||||
|
@ -139,14 +141,6 @@ export function DiscoverChart({
|
|||
onResetQuery={resetSavedSearch}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{showViewModeToggle && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<DocumentViewModeToggle
|
||||
viewMode={viewMode}
|
||||
setDiscoverViewMode={setDiscoverViewMode}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{isTimeBased && (
|
||||
<EuiFlexItem className="dscResultCount__toggle" grow={false}>
|
||||
<EuiFlexGroup direction="row" gutterSize="s" responsive={false}>
|
||||
|
@ -203,7 +197,7 @@ export function DiscoverChart({
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{isTimeBased && !hideChart && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexItem>
|
||||
<section
|
||||
ref={(element) => (chartRef.current.element = element)}
|
||||
tabIndex={-1}
|
||||
|
@ -218,7 +212,7 @@ export function DiscoverChart({
|
|||
stateContainer={stateContainer}
|
||||
/>
|
||||
</section>
|
||||
<EuiSpacer size="s" />
|
||||
{appendHistogram}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -18,6 +18,8 @@ import {
|
|||
isErrorEmbeddable,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import { FIELD_STATISTICS_LOADED } from './constants';
|
||||
import type { GetStateReturn } from '../../services/discover_state';
|
||||
|
@ -226,13 +228,22 @@ export const FieldStatisticsTable = (props: FieldStatisticsTableProps) => {
|
|||
};
|
||||
}, [embeddable, embeddableRoot, trackUiMetric]);
|
||||
|
||||
const statsTableCss = css`
|
||||
overflow-y: auto;
|
||||
|
||||
.kbnDocTableWrapper {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test-subj="dscFieldStatsEmbeddedContent"
|
||||
ref={embeddableRoot}
|
||||
style={{ height: '100%', overflowY: 'auto', overflowX: 'hidden' }}
|
||||
// Match the scroll bar of the Discover doc table
|
||||
className="kbnDocTableWrapper"
|
||||
/>
|
||||
<EuiFlexItem css={statsTableCss}>
|
||||
<div
|
||||
data-test-subj="dscFieldStatsEmbeddedContent"
|
||||
ref={embeddableRoot}
|
||||
// Match the scroll bar of the Discover doc table
|
||||
className="kbnDocTableWrapper"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -70,7 +70,9 @@ discover-app {
|
|||
}
|
||||
|
||||
.dscTimechart {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
// SASSTODO: the visualizing component should have an option or a modifier
|
||||
|
@ -81,13 +83,12 @@ discover-app {
|
|||
}
|
||||
|
||||
.dscHistogram {
|
||||
height: $euiSize * 7;
|
||||
padding: 0 $euiSizeS $euiSizeS * 2 $euiSizeS;
|
||||
flex-grow: 1;
|
||||
padding: 0 $euiSizeS $euiSizeS $euiSizeS;
|
||||
}
|
||||
|
||||
.dscHistogramTimeRange {
|
||||
padding: 0 $euiSizeS 0 $euiSizeS;
|
||||
margin-top: - $euiSizeS;
|
||||
}
|
||||
|
||||
.dscTable {
|
||||
|
|
|
@ -175,24 +175,27 @@ function mountComponent(
|
|||
|
||||
describe('Discover component', () => {
|
||||
test('selected data view without time field displays no chart toggle', () => {
|
||||
const component = mountComponent(dataViewMock);
|
||||
expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeFalsy();
|
||||
const container = document.createElement('div');
|
||||
mountComponent(dataViewMock, undefined, { attachTo: container });
|
||||
expect(container.querySelector('[data-test-subj="discoverChartOptionsToggle"]')).toBeNull();
|
||||
});
|
||||
|
||||
test('selected data view with time field displays chart toggle', () => {
|
||||
const component = mountComponent(dataViewWithTimefieldMock);
|
||||
expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeTruthy();
|
||||
const container = document.createElement('div');
|
||||
mountComponent(dataViewWithTimefieldMock, undefined, { attachTo: container });
|
||||
expect(container.querySelector('[data-test-subj="discoverChartOptionsToggle"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('sql query displays no chart toggle', () => {
|
||||
const component = mountComponent(
|
||||
const container = document.createElement('div');
|
||||
mountComponent(
|
||||
dataViewWithTimefieldMock,
|
||||
false,
|
||||
{},
|
||||
{ attachTo: container },
|
||||
{ sql: 'SELECT * FROM test' },
|
||||
true
|
||||
);
|
||||
expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeFalsy();
|
||||
expect(container.querySelector('[data-test-subj="discoverChartOptionsToggle"]')).toBeNull();
|
||||
});
|
||||
|
||||
test('the saved search title h1 gains focus on navigate', () => {
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHideFor,
|
||||
EuiHorizontalRule,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent_Deprecated as EuiPageContent,
|
||||
|
@ -34,20 +33,17 @@ import { SEARCH_FIELDS_FROM_SOURCE, SHOW_FIELD_STATISTICS } from '../../../../..
|
|||
import { popularizeField } from '../../../../utils/popularize_field';
|
||||
import { DiscoverTopNav } from '../top_nav/discover_topnav';
|
||||
import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types';
|
||||
import { DiscoverChart } from '../chart';
|
||||
import { getResultState } from '../../utils/get_result_state';
|
||||
import { DiscoverUninitialized } from '../uninitialized/uninitialized';
|
||||
import { DataMainMsg, RecordRawType } from '../../hooks/use_saved_search';
|
||||
import { useColumns } from '../../../../hooks/use_data_grid_columns';
|
||||
import { DiscoverDocuments } from './discover_documents';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import { useDataState } from '../../hooks/use_data_state';
|
||||
import { FieldStatisticsTable } from '../field_stats_table';
|
||||
import { VIEW_MODE } from '../../../../components/view_mode_toggle';
|
||||
import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants';
|
||||
import { hasActiveFilter } from './utils';
|
||||
import { getRawRecordType } from '../../utils/get_raw_record_type';
|
||||
import { SavedSearchURLConflictCallout } from '../../../../components/saved_search_url_conflict_callout/saved_search_url_conflict_callout';
|
||||
import { DiscoverMainContent } from './discover_main_content';
|
||||
|
||||
/**
|
||||
* Local storage key for sidebar persistence state
|
||||
|
@ -56,8 +52,6 @@ export const SIDEBAR_CLOSED_KEY = 'discover:sidebarClosed';
|
|||
|
||||
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
|
||||
const TopNavMemoized = React.memo(DiscoverTopNav);
|
||||
const DiscoverChartMemoized = React.memo(DiscoverChart);
|
||||
const FieldStatisticsTableMemoized = React.memo(FieldStatisticsTable);
|
||||
|
||||
export function DiscoverLayout({
|
||||
dataView,
|
||||
|
@ -91,7 +85,7 @@ export function DiscoverLayout({
|
|||
spaces,
|
||||
inspector,
|
||||
} = useDiscoverServices();
|
||||
const { main$, charts$, totalHits$ } = savedSearchData$;
|
||||
const { main$ } = savedSearchData$;
|
||||
const dataState: DataMainMsg = useDataState(main$);
|
||||
|
||||
const viewMode = useMemo(() => {
|
||||
|
@ -99,21 +93,6 @@ export function DiscoverLayout({
|
|||
return state.viewMode ?? VIEW_MODE.DOCUMENT_LEVEL;
|
||||
}, [uiSettings, state.viewMode]);
|
||||
|
||||
const setDiscoverViewMode = useCallback(
|
||||
(mode: VIEW_MODE) => {
|
||||
stateContainer.setAppState({ viewMode: mode });
|
||||
|
||||
if (trackUiMetric) {
|
||||
if (mode === VIEW_MODE.AGGREGATED_LEVEL) {
|
||||
trackUiMetric(METRIC_TYPE.CLICK, FIELD_STATISTICS_VIEW_CLICK);
|
||||
} else {
|
||||
trackUiMetric(METRIC_TYPE.CLICK, DOCUMENTS_VIEW_CLICK);
|
||||
}
|
||||
}
|
||||
},
|
||||
[trackUiMetric, stateContainer]
|
||||
);
|
||||
|
||||
const fetchCounter = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -210,6 +189,8 @@ export function DiscoverLayout({
|
|||
}
|
||||
}, [dataState.error, isPlainRecord]);
|
||||
|
||||
const resizeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<EuiPage className="dscPage" data-fetch-counter={fetchCounter.current}>
|
||||
<h1
|
||||
|
@ -298,6 +279,7 @@ export function DiscoverLayout({
|
|||
</EuiHideFor>
|
||||
<EuiFlexItem className="dscPageContent__wrapper">
|
||||
<EuiPageContent
|
||||
panelRef={resizeRef}
|
||||
verticalPosition={contentCentered ? 'center' : undefined}
|
||||
horizontalPosition={contentCentered ? 'center' : undefined}
|
||||
paddingSize="none"
|
||||
|
@ -322,61 +304,25 @@ export function DiscoverLayout({
|
|||
)}
|
||||
{resultState === 'loading' && <LoadingSpinner />}
|
||||
{resultState === 'ready' && (
|
||||
<EuiFlexGroup
|
||||
className="dscPageContent__inner"
|
||||
direction="column"
|
||||
alignItems="stretch"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
{!isPlainRecord && (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<DiscoverChartMemoized
|
||||
resetSavedSearch={resetSavedSearch}
|
||||
savedSearch={savedSearch}
|
||||
savedSearchDataChart$={charts$}
|
||||
savedSearchDataTotalHits$={totalHits$}
|
||||
stateContainer={stateContainer}
|
||||
dataView={dataView}
|
||||
viewMode={viewMode}
|
||||
setDiscoverViewMode={setDiscoverViewMode}
|
||||
hideChart={state.hideChart}
|
||||
interval={state.interval}
|
||||
isTimeBased={isTimeBased}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</>
|
||||
)}
|
||||
{viewMode === VIEW_MODE.DOCUMENT_LEVEL ? (
|
||||
<DiscoverDocuments
|
||||
documents$={savedSearchData$.documents$}
|
||||
expandedDoc={expandedDoc}
|
||||
dataView={dataView}
|
||||
navigateTo={navigateTo}
|
||||
onAddFilter={!isPlainRecord ? (onAddFilter as DocViewFilterFn) : undefined}
|
||||
savedSearch={savedSearch}
|
||||
setExpandedDoc={setExpandedDoc}
|
||||
state={state}
|
||||
stateContainer={stateContainer}
|
||||
onFieldEdited={!isPlainRecord ? onFieldEdited : undefined}
|
||||
/>
|
||||
) : (
|
||||
<FieldStatisticsTableMemoized
|
||||
availableFields$={savedSearchData$.availableFields$}
|
||||
savedSearch={savedSearch}
|
||||
dataView={dataView}
|
||||
query={state.query}
|
||||
filters={state.filters}
|
||||
columns={columns}
|
||||
stateContainer={stateContainer}
|
||||
onAddFilter={!isPlainRecord ? (onAddFilter as DocViewFilterFn) : undefined}
|
||||
trackUiMetric={trackUiMetric}
|
||||
savedSearchRefetch$={savedSearchRefetch$}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<DiscoverMainContent
|
||||
isPlainRecord={isPlainRecord}
|
||||
dataView={dataView}
|
||||
navigateTo={navigateTo}
|
||||
resetSavedSearch={resetSavedSearch}
|
||||
expandedDoc={expandedDoc}
|
||||
setExpandedDoc={setExpandedDoc}
|
||||
savedSearch={savedSearch}
|
||||
savedSearchData$={savedSearchData$}
|
||||
savedSearchRefetch$={savedSearchRefetch$}
|
||||
state={state}
|
||||
stateContainer={stateContainer}
|
||||
isTimeBased={isTimeBased}
|
||||
viewMode={viewMode}
|
||||
onAddFilter={onAddFilter as DocViewFilterFn}
|
||||
onFieldEdited={onFieldEdited}
|
||||
columns={columns}
|
||||
resizeRef={resizeRef}
|
||||
/>
|
||||
)}
|
||||
</EuiPageContent>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* 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 { Subject, BehaviorSubject } from 'rxjs';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { esHits } from '../../../../__mocks__/es_hits';
|
||||
import { dataViewMock } from '../../../../__mocks__/data_view';
|
||||
import { savedSearchMock } from '../../../../__mocks__/saved_search';
|
||||
import { GetStateReturn } from '../../services/discover_state';
|
||||
import {
|
||||
AvailableFields$,
|
||||
DataCharts$,
|
||||
DataDocuments$,
|
||||
DataMain$,
|
||||
DataTotalHits$,
|
||||
RecordRawType,
|
||||
} from '../../hooks/use_saved_search';
|
||||
import { discoverServiceMock } from '../../../../__mocks__/services';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import { Chart } from '../chart/point_series';
|
||||
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { buildDataTableRecord } from '../../../../utils/build_data_record';
|
||||
import { DiscoverMainContent, DiscoverMainContentProps } from './discover_main_content';
|
||||
import { VIEW_MODE } from '@kbn/saved-search-plugin/public';
|
||||
import { DiscoverPanels, DISCOVER_PANELS_MODE } from './discover_panels';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { CoreTheme } from '@kbn/core/public';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { setTimeout } from 'timers/promises';
|
||||
import { DiscoverChart } from '../chart';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { DocumentViewModeToggle } from '../../../../components/view_mode_toggle';
|
||||
|
||||
const mountComponent = async ({
|
||||
isPlainRecord = false,
|
||||
hideChart = false,
|
||||
isTimeBased = true,
|
||||
}: {
|
||||
isPlainRecord?: boolean;
|
||||
hideChart?: boolean;
|
||||
isTimeBased?: boolean;
|
||||
} = {}) => {
|
||||
const services = discoverServiceMock;
|
||||
services.data.query.timefilter.timefilter.getAbsoluteTime = () => {
|
||||
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
|
||||
};
|
||||
|
||||
const main$ = new BehaviorSubject({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
recordRawType: isPlainRecord ? RecordRawType.PLAIN : RecordRawType.DOCUMENT,
|
||||
foundDocuments: true,
|
||||
}) as DataMain$;
|
||||
|
||||
const documents$ = new BehaviorSubject({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
result: esHits.map((esHit) => buildDataTableRecord(esHit, dataViewMock)),
|
||||
}) as DataDocuments$;
|
||||
|
||||
const availableFields$ = new BehaviorSubject({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
fields: [] as string[],
|
||||
}) as AvailableFields$;
|
||||
|
||||
const totalHits$ = new BehaviorSubject({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
result: Number(esHits.length),
|
||||
}) as DataTotalHits$;
|
||||
|
||||
const chartData = {
|
||||
xAxisOrderedValues: [
|
||||
1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000,
|
||||
1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000,
|
||||
1624917600000, 1625004000000, 1625090400000,
|
||||
],
|
||||
xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } },
|
||||
xAxisLabel: 'order_date per day',
|
||||
yAxisFormat: { id: 'number' },
|
||||
ordered: {
|
||||
date: true,
|
||||
interval: {
|
||||
asMilliseconds: jest.fn(),
|
||||
},
|
||||
intervalESUnit: 'd',
|
||||
intervalESValue: 1,
|
||||
min: '2021-03-18T08:28:56.411Z',
|
||||
max: '2021-07-01T07:28:56.411Z',
|
||||
},
|
||||
yAxisLabel: 'Count',
|
||||
values: [
|
||||
{ x: 1623880800000, y: 134 },
|
||||
{ x: 1623967200000, y: 152 },
|
||||
{ x: 1624053600000, y: 141 },
|
||||
{ x: 1624140000000, y: 138 },
|
||||
{ x: 1624226400000, y: 142 },
|
||||
{ x: 1624312800000, y: 157 },
|
||||
{ x: 1624399200000, y: 149 },
|
||||
{ x: 1624485600000, y: 146 },
|
||||
{ x: 1624572000000, y: 170 },
|
||||
{ x: 1624658400000, y: 137 },
|
||||
{ x: 1624744800000, y: 150 },
|
||||
{ x: 1624831200000, y: 144 },
|
||||
{ x: 1624917600000, y: 147 },
|
||||
{ x: 1625004000000, y: 137 },
|
||||
{ x: 1625090400000, y: 66 },
|
||||
],
|
||||
} as unknown as Chart;
|
||||
|
||||
const charts$ = new BehaviorSubject({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
chartData,
|
||||
bucketInterval: {
|
||||
scaled: true,
|
||||
description: 'test',
|
||||
scale: 2,
|
||||
},
|
||||
}) as DataCharts$;
|
||||
|
||||
const savedSearchData$ = {
|
||||
main$,
|
||||
documents$,
|
||||
totalHits$,
|
||||
charts$,
|
||||
availableFields$,
|
||||
};
|
||||
|
||||
const props: DiscoverMainContentProps = {
|
||||
isPlainRecord,
|
||||
dataView: dataViewMock,
|
||||
navigateTo: jest.fn(),
|
||||
resetSavedSearch: jest.fn(),
|
||||
setExpandedDoc: jest.fn(),
|
||||
savedSearch: savedSearchMock,
|
||||
savedSearchData$,
|
||||
savedSearchRefetch$: new Subject(),
|
||||
state: { columns: [], hideChart },
|
||||
stateContainer: {
|
||||
setAppState: () => {},
|
||||
appStateContainer: {
|
||||
getState: () => ({
|
||||
interval: 'auto',
|
||||
}),
|
||||
},
|
||||
} as unknown as GetStateReturn,
|
||||
isTimeBased,
|
||||
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
|
||||
onAddFilter: jest.fn(),
|
||||
onFieldEdited: jest.fn(),
|
||||
columns: [],
|
||||
resizeRef: { current: null },
|
||||
};
|
||||
|
||||
const coreTheme$ = new BehaviorSubject<CoreTheme>({ darkMode: false });
|
||||
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={services}>
|
||||
<KibanaThemeProvider theme$={coreTheme$}>
|
||||
<DiscoverMainContent {...props} />
|
||||
</KibanaThemeProvider>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
// useIsWithinBreakpoints triggers state updates which cause act
|
||||
// issues and prevent our resize events from being fired correctly
|
||||
// https://github.com/enzymejs/enzyme/issues/2073
|
||||
await act(() => setTimeout(0));
|
||||
|
||||
return component;
|
||||
};
|
||||
|
||||
const setWindowWidth = (component: ReactWrapper, width: string) => {
|
||||
window.innerWidth = parseInt(width, 10);
|
||||
act(() => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
component.update();
|
||||
};
|
||||
|
||||
describe('Discover main content component', () => {
|
||||
const windowWidth = window.innerWidth;
|
||||
|
||||
beforeEach(() => {
|
||||
window.innerWidth = windowWidth;
|
||||
});
|
||||
|
||||
it('should set the panels mode to DISCOVER_PANELS_MODE.RESIZABLE when viewing on medium screens and above', async () => {
|
||||
const component = await mountComponent();
|
||||
setWindowWidth(component, euiThemeVars.euiBreakpoints.m);
|
||||
expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.RESIZABLE);
|
||||
});
|
||||
|
||||
it('should set the panels mode to DISCOVER_PANELS_MODE.FIXED when viewing on small screens and below', async () => {
|
||||
const component = await mountComponent();
|
||||
setWindowWidth(component, euiThemeVars.euiBreakpoints.s);
|
||||
expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.FIXED);
|
||||
});
|
||||
|
||||
it('should set the panels mode to DISCOVER_PANELS_MODE.FIXED if hideChart is true', async () => {
|
||||
const component = await mountComponent({ hideChart: true });
|
||||
expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.FIXED);
|
||||
});
|
||||
|
||||
it('should set the panels mode to DISCOVER_PANELS_MODE.FIXED if isTimeBased is false', async () => {
|
||||
const component = await mountComponent({ isTimeBased: false });
|
||||
expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.FIXED);
|
||||
});
|
||||
|
||||
it('should set the panels mode to DISCOVER_PANELS_MODE.SINGLE if isPlainRecord is true', async () => {
|
||||
const component = await mountComponent({ isPlainRecord: true });
|
||||
expect(component.find(DiscoverPanels).prop('mode')).toBe(DISCOVER_PANELS_MODE.SINGLE);
|
||||
});
|
||||
|
||||
it('should set a fixed height for DiscoverChart when panels mode is DISCOVER_PANELS_MODE.FIXED and hideChart is false', async () => {
|
||||
const component = await mountComponent();
|
||||
setWindowWidth(component, euiThemeVars.euiBreakpoints.s);
|
||||
const expectedHeight = component.find(DiscoverPanels).prop('initialTopPanelHeight');
|
||||
expect(component.find(DiscoverChart).childAt(0).getDOMNode()).toHaveStyle({
|
||||
height: `${expectedHeight}px`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not set a fixed height for DiscoverChart when panels mode is DISCOVER_PANELS_MODE.FIXED and hideChart is true', async () => {
|
||||
const component = await mountComponent({ hideChart: true });
|
||||
setWindowWidth(component, euiThemeVars.euiBreakpoints.s);
|
||||
const expectedHeight = component.find(DiscoverPanels).prop('initialTopPanelHeight');
|
||||
expect(component.find(DiscoverChart).childAt(0).getDOMNode()).not.toHaveStyle({
|
||||
height: `${expectedHeight}px`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not set a fixed height for DiscoverChart when panels mode is DISCOVER_PANELS_MODE.FIXED and isTimeBased is false', async () => {
|
||||
const component = await mountComponent({ isTimeBased: false });
|
||||
setWindowWidth(component, euiThemeVars.euiBreakpoints.s);
|
||||
const expectedHeight = component.find(DiscoverPanels).prop('initialTopPanelHeight');
|
||||
expect(component.find(DiscoverChart).childAt(0).getDOMNode()).not.toHaveStyle({
|
||||
height: `${expectedHeight}px`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show DocumentViewModeToggle when isPlainRecord is false', async () => {
|
||||
const component = await mountComponent();
|
||||
expect(component.find(DocumentViewModeToggle).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not show DocumentViewModeToggle when isPlainRecord is true', async () => {
|
||||
const component = await mountComponent({ isPlainRecord: true });
|
||||
expect(component.find(DocumentViewModeToggle).exists()).toBe(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
useEuiTheme,
|
||||
useIsWithinBreakpoints,
|
||||
} from '@elastic/eui';
|
||||
import { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import React, { RefObject, useCallback, useMemo } from 'react';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
|
||||
import { css } from '@emotion/css';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import { DataTableRecord } from '../../../../types';
|
||||
import { DocumentViewModeToggle, VIEW_MODE } from '../../../../components/view_mode_toggle';
|
||||
import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types';
|
||||
import { DataRefetch$, SavedSearchData } from '../../hooks/use_saved_search';
|
||||
import { AppState, GetStateReturn } from '../../services/discover_state';
|
||||
import { DiscoverChart } from '../chart';
|
||||
import { FieldStatisticsTable } from '../field_stats_table';
|
||||
import { DiscoverDocuments } from './discover_documents';
|
||||
import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants';
|
||||
import { DiscoverPanels, DISCOVER_PANELS_MODE } from './discover_panels';
|
||||
|
||||
const DiscoverChartMemoized = React.memo(DiscoverChart);
|
||||
const FieldStatisticsTableMemoized = React.memo(FieldStatisticsTable);
|
||||
|
||||
export interface DiscoverMainContentProps {
|
||||
isPlainRecord: boolean;
|
||||
dataView: DataView;
|
||||
navigateTo: (url: string) => void;
|
||||
resetSavedSearch: () => void;
|
||||
expandedDoc?: DataTableRecord;
|
||||
setExpandedDoc: (doc?: DataTableRecord) => void;
|
||||
savedSearch: SavedSearch;
|
||||
savedSearchData$: SavedSearchData;
|
||||
savedSearchRefetch$: DataRefetch$;
|
||||
state: AppState;
|
||||
stateContainer: GetStateReturn;
|
||||
isTimeBased: boolean;
|
||||
viewMode: VIEW_MODE;
|
||||
onAddFilter: DocViewFilterFn | undefined;
|
||||
onFieldEdited: () => void;
|
||||
columns: string[];
|
||||
resizeRef: RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const DiscoverMainContent = ({
|
||||
isPlainRecord,
|
||||
dataView,
|
||||
navigateTo,
|
||||
resetSavedSearch,
|
||||
expandedDoc,
|
||||
setExpandedDoc,
|
||||
savedSearch,
|
||||
savedSearchData$,
|
||||
savedSearchRefetch$,
|
||||
state,
|
||||
stateContainer,
|
||||
isTimeBased,
|
||||
viewMode,
|
||||
onAddFilter,
|
||||
onFieldEdited,
|
||||
columns,
|
||||
resizeRef,
|
||||
}: DiscoverMainContentProps) => {
|
||||
const { trackUiMetric } = useDiscoverServices();
|
||||
|
||||
const setDiscoverViewMode = useCallback(
|
||||
(mode: VIEW_MODE) => {
|
||||
stateContainer.setAppState({ viewMode: mode });
|
||||
|
||||
if (trackUiMetric) {
|
||||
if (mode === VIEW_MODE.AGGREGATED_LEVEL) {
|
||||
trackUiMetric(METRIC_TYPE.CLICK, FIELD_STATISTICS_VIEW_CLICK);
|
||||
} else {
|
||||
trackUiMetric(METRIC_TYPE.CLICK, DOCUMENTS_VIEW_CLICK);
|
||||
}
|
||||
}
|
||||
},
|
||||
[trackUiMetric, stateContainer]
|
||||
);
|
||||
|
||||
const topPanelNode = useMemo(
|
||||
() => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }),
|
||||
[]
|
||||
);
|
||||
|
||||
const mainPanelNode = useMemo(
|
||||
() => createHtmlPortalNode({ attributes: { class: 'eui-fullHeight' } }),
|
||||
[]
|
||||
);
|
||||
|
||||
const hideChart = state.hideChart || !isTimeBased;
|
||||
const showFixedPanels = useIsWithinBreakpoints(['xs', 's']) || isPlainRecord || hideChart;
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const topPanelHeight = euiTheme.base * 12;
|
||||
const minTopPanelHeight = euiTheme.base * 8;
|
||||
const minMainPanelHeight = euiTheme.base * 10;
|
||||
|
||||
const chartClassName =
|
||||
showFixedPanels && !hideChart
|
||||
? css`
|
||||
height: ${topPanelHeight}px;
|
||||
`
|
||||
: 'eui-fullHeight';
|
||||
|
||||
const panelsMode = isPlainRecord
|
||||
? DISCOVER_PANELS_MODE.SINGLE
|
||||
: showFixedPanels
|
||||
? DISCOVER_PANELS_MODE.FIXED
|
||||
: DISCOVER_PANELS_MODE.RESIZABLE;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InPortal node={topPanelNode}>
|
||||
<DiscoverChartMemoized
|
||||
className={chartClassName}
|
||||
resetSavedSearch={resetSavedSearch}
|
||||
savedSearch={savedSearch}
|
||||
savedSearchDataChart$={savedSearchData$.charts$}
|
||||
savedSearchDataTotalHits$={savedSearchData$.totalHits$}
|
||||
stateContainer={stateContainer}
|
||||
dataView={dataView}
|
||||
hideChart={state.hideChart}
|
||||
interval={state.interval}
|
||||
isTimeBased={isTimeBased}
|
||||
appendHistogram={showFixedPanels ? <EuiSpacer size="s" /> : <EuiSpacer size="m" />}
|
||||
/>
|
||||
</InPortal>
|
||||
<InPortal node={mainPanelNode}>
|
||||
<EuiFlexGroup
|
||||
className="eui-fullHeight"
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
{!isPlainRecord && (
|
||||
<EuiFlexItem grow={false}>
|
||||
{!showFixedPanels && <EuiSpacer size="s" />}
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<DocumentViewModeToggle
|
||||
viewMode={viewMode}
|
||||
setDiscoverViewMode={setDiscoverViewMode}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{viewMode === VIEW_MODE.DOCUMENT_LEVEL ? (
|
||||
<DiscoverDocuments
|
||||
documents$={savedSearchData$.documents$}
|
||||
expandedDoc={expandedDoc}
|
||||
dataView={dataView}
|
||||
navigateTo={navigateTo}
|
||||
onAddFilter={!isPlainRecord ? onAddFilter : undefined}
|
||||
savedSearch={savedSearch}
|
||||
setExpandedDoc={setExpandedDoc}
|
||||
state={state}
|
||||
stateContainer={stateContainer}
|
||||
onFieldEdited={!isPlainRecord ? onFieldEdited : undefined}
|
||||
/>
|
||||
) : (
|
||||
<FieldStatisticsTableMemoized
|
||||
availableFields$={savedSearchData$.availableFields$}
|
||||
savedSearch={savedSearch}
|
||||
dataView={dataView}
|
||||
query={state.query}
|
||||
filters={state.filters}
|
||||
columns={columns}
|
||||
stateContainer={stateContainer}
|
||||
onAddFilter={!isPlainRecord ? onAddFilter : undefined}
|
||||
trackUiMetric={trackUiMetric}
|
||||
savedSearchRefetch$={savedSearchRefetch$}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</InPortal>
|
||||
<DiscoverPanels
|
||||
className="dscPageContent__inner"
|
||||
mode={panelsMode}
|
||||
resizeRef={resizeRef}
|
||||
initialTopPanelHeight={topPanelHeight}
|
||||
minTopPanelHeight={minTopPanelHeight}
|
||||
minMainPanelHeight={minMainPanelHeight}
|
||||
topPanel={<OutPortal node={topPanelNode} />}
|
||||
mainPanel={<OutPortal node={mainPanelNode} />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
import React, { ReactElement, RefObject } from 'react';
|
||||
import { DiscoverPanels, DISCOVER_PANELS_MODE } from './discover_panels';
|
||||
import { DiscoverPanelsResizable } from './discover_panels_resizable';
|
||||
import { DiscoverPanelsFixed } from './discover_panels_fixed';
|
||||
|
||||
describe('Discover panels component', () => {
|
||||
const mountComponent = ({
|
||||
mode = DISCOVER_PANELS_MODE.RESIZABLE,
|
||||
resizeRef = { current: null },
|
||||
initialTopPanelHeight = 200,
|
||||
minTopPanelHeight = 100,
|
||||
minMainPanelHeight = 100,
|
||||
topPanel = <></>,
|
||||
mainPanel = <></>,
|
||||
}: {
|
||||
mode?: DISCOVER_PANELS_MODE;
|
||||
resizeRef?: RefObject<HTMLDivElement>;
|
||||
initialTopPanelHeight?: number;
|
||||
minTopPanelHeight?: number;
|
||||
minMainPanelHeight?: number;
|
||||
mainPanel?: ReactElement;
|
||||
topPanel?: ReactElement;
|
||||
}) => {
|
||||
return mount(
|
||||
<DiscoverPanels
|
||||
mode={mode}
|
||||
resizeRef={resizeRef}
|
||||
initialTopPanelHeight={initialTopPanelHeight}
|
||||
minTopPanelHeight={minTopPanelHeight}
|
||||
minMainPanelHeight={minMainPanelHeight}
|
||||
topPanel={topPanel}
|
||||
mainPanel={mainPanel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
it('should show DiscoverPanelsFixed when mode is DISCOVER_PANELS_MODE.SINGLE', () => {
|
||||
const topPanel = <div data-test-subj="topPanel" />;
|
||||
const mainPanel = <div data-test-subj="mainPanel" />;
|
||||
const component = mountComponent({ mode: DISCOVER_PANELS_MODE.SINGLE, topPanel, mainPanel });
|
||||
expect(component.find(DiscoverPanelsFixed).exists()).toBe(true);
|
||||
expect(component.find(DiscoverPanelsResizable).exists()).toBe(false);
|
||||
expect(component.contains(topPanel)).toBe(false);
|
||||
expect(component.contains(mainPanel)).toBe(true);
|
||||
});
|
||||
|
||||
it('should show DiscoverPanelsFixed when mode is DISCOVER_PANELS_MODE.FIXED', () => {
|
||||
const topPanel = <div data-test-subj="topPanel" />;
|
||||
const mainPanel = <div data-test-subj="mainPanel" />;
|
||||
const component = mountComponent({ mode: DISCOVER_PANELS_MODE.FIXED, topPanel, mainPanel });
|
||||
expect(component.find(DiscoverPanelsFixed).exists()).toBe(true);
|
||||
expect(component.find(DiscoverPanelsResizable).exists()).toBe(false);
|
||||
expect(component.contains(topPanel)).toBe(true);
|
||||
expect(component.contains(mainPanel)).toBe(true);
|
||||
});
|
||||
|
||||
it('should show DiscoverPanelsResizable when mode is DISCOVER_PANELS_MODE.RESIZABLE', () => {
|
||||
const topPanel = <div data-test-subj="topPanel" />;
|
||||
const mainPanel = <div data-test-subj="mainPanel" />;
|
||||
const component = mountComponent({ mode: DISCOVER_PANELS_MODE.RESIZABLE, topPanel, mainPanel });
|
||||
expect(component.find(DiscoverPanelsFixed).exists()).toBe(false);
|
||||
expect(component.find(DiscoverPanelsResizable).exists()).toBe(true);
|
||||
expect(component.contains(topPanel)).toBe(true);
|
||||
expect(component.contains(mainPanel)).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass true for hideTopPanel when mode is DISCOVER_PANELS_MODE.SINGLE', () => {
|
||||
const topPanel = <div data-test-subj="topPanel" />;
|
||||
const mainPanel = <div data-test-subj="mainPanel" />;
|
||||
const component = mountComponent({ mode: DISCOVER_PANELS_MODE.SINGLE, topPanel, mainPanel });
|
||||
expect(component.find(DiscoverPanelsFixed).prop('hideTopPanel')).toBe(true);
|
||||
expect(component.contains(topPanel)).toBe(false);
|
||||
expect(component.contains(mainPanel)).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass false for hideTopPanel when mode is DISCOVER_PANELS_MODE.FIXED', () => {
|
||||
const topPanel = <div data-test-subj="topPanel" />;
|
||||
const mainPanel = <div data-test-subj="mainPanel" />;
|
||||
const component = mountComponent({ mode: DISCOVER_PANELS_MODE.FIXED, topPanel, mainPanel });
|
||||
expect(component.find(DiscoverPanelsFixed).prop('hideTopPanel')).toBe(false);
|
||||
expect(component.contains(topPanel)).toBe(true);
|
||||
expect(component.contains(mainPanel)).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { ReactElement, RefObject } from 'react';
|
||||
import { DiscoverPanelsResizable } from './discover_panels_resizable';
|
||||
import { DiscoverPanelsFixed } from './discover_panels_fixed';
|
||||
|
||||
export enum DISCOVER_PANELS_MODE {
|
||||
SINGLE = 'single',
|
||||
FIXED = 'fixed',
|
||||
RESIZABLE = 'resizable',
|
||||
}
|
||||
|
||||
export interface DiscoverPanelsProps {
|
||||
className?: string;
|
||||
mode: DISCOVER_PANELS_MODE;
|
||||
resizeRef: RefObject<HTMLDivElement>;
|
||||
initialTopPanelHeight: number;
|
||||
minTopPanelHeight: number;
|
||||
minMainPanelHeight: number;
|
||||
topPanel: ReactElement;
|
||||
mainPanel: ReactElement;
|
||||
}
|
||||
|
||||
const fixedModes = [DISCOVER_PANELS_MODE.SINGLE, DISCOVER_PANELS_MODE.FIXED];
|
||||
|
||||
export const DiscoverPanels = ({
|
||||
className,
|
||||
mode,
|
||||
resizeRef,
|
||||
initialTopPanelHeight,
|
||||
minTopPanelHeight,
|
||||
minMainPanelHeight,
|
||||
topPanel,
|
||||
mainPanel,
|
||||
}: DiscoverPanelsProps) => {
|
||||
const panelsProps = { className, topPanel, mainPanel };
|
||||
|
||||
return fixedModes.includes(mode) ? (
|
||||
<DiscoverPanelsFixed hideTopPanel={mode === DISCOVER_PANELS_MODE.SINGLE} {...panelsProps} />
|
||||
) : (
|
||||
<DiscoverPanelsResizable
|
||||
resizeRef={resizeRef}
|
||||
initialTopPanelHeight={initialTopPanelHeight}
|
||||
minTopPanelHeight={minTopPanelHeight}
|
||||
minMainPanelHeight={minMainPanelHeight}
|
||||
{...panelsProps}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 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 { mount } from 'enzyme';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { DiscoverPanelsFixed } from './discover_panels_fixed';
|
||||
|
||||
describe('Discover panels fixed', () => {
|
||||
const mountComponent = ({
|
||||
hideTopPanel = false,
|
||||
topPanel = <></>,
|
||||
mainPanel = <></>,
|
||||
}: {
|
||||
hideTopPanel?: boolean;
|
||||
topPanel: ReactElement;
|
||||
mainPanel: ReactElement;
|
||||
}) => {
|
||||
return mount(
|
||||
<DiscoverPanelsFixed hideTopPanel={hideTopPanel} topPanel={topPanel} mainPanel={mainPanel} />
|
||||
);
|
||||
};
|
||||
|
||||
it('should render both panels when hideTopPanel is false', () => {
|
||||
const topPanel = <div data-test-subj="topPanel" />;
|
||||
const mainPanel = <div data-test-subj="mainPanel" />;
|
||||
const component = mountComponent({ topPanel, mainPanel });
|
||||
expect(component.contains(topPanel)).toBe(true);
|
||||
expect(component.contains(mainPanel)).toBe(true);
|
||||
});
|
||||
|
||||
it('should render only main panel when hideTopPanel is true', () => {
|
||||
const topPanel = <div data-test-subj="topPanel" />;
|
||||
const mainPanel = <div data-test-subj="mainPanel" />;
|
||||
const component = mountComponent({ hideTopPanel: true, topPanel, mainPanel });
|
||||
expect(component.contains(topPanel)).toBe(false);
|
||||
expect(component.contains(mainPanel)).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
export const DiscoverPanelsFixed = ({
|
||||
className,
|
||||
hideTopPanel,
|
||||
topPanel,
|
||||
mainPanel,
|
||||
}: {
|
||||
className?: string;
|
||||
hideTopPanel?: boolean;
|
||||
topPanel: ReactElement;
|
||||
mainPanel: ReactElement;
|
||||
}) => {
|
||||
// By default a flex item has overflow: visible, min-height: auto, and min-width: auto.
|
||||
// This can cause the item to overflow the flexbox parent when its content is too large.
|
||||
// Setting the overflow to something other than visible (e.g. auto) resets the min-height
|
||||
// and min-width to 0 and makes the item respect the flexbox parent's size.
|
||||
// https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size
|
||||
const mainPanelCss = css`
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
className={className}
|
||||
direction="column"
|
||||
alignItems="stretch"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
{!hideTopPanel && <EuiFlexItem grow={false}>{topPanel}</EuiFlexItem>}
|
||||
<EuiFlexItem css={mainPanelCss}>{mainPanel}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* 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 { mount, ReactWrapper } from 'enzyme';
|
||||
import React, { ReactElement, RefObject } from 'react';
|
||||
import { DiscoverPanelsResizable } from './discover_panels_resizable';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
const containerHeight = 1000;
|
||||
const topPanelId = 'topPanel';
|
||||
|
||||
jest.mock('@elastic/eui', () => ({
|
||||
...jest.requireActual('@elastic/eui'),
|
||||
useResizeObserver: jest.fn(),
|
||||
useGeneratedHtmlId: jest.fn(() => topPanelId),
|
||||
}));
|
||||
|
||||
import * as eui from '@elastic/eui';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
|
||||
describe('Discover panels resizable', () => {
|
||||
const mountComponent = ({
|
||||
className = '',
|
||||
resizeRef = { current: null },
|
||||
initialTopPanelHeight = 0,
|
||||
minTopPanelHeight = 0,
|
||||
minMainPanelHeight = 0,
|
||||
topPanel = <></>,
|
||||
mainPanel = <></>,
|
||||
attachTo,
|
||||
}: {
|
||||
className?: string;
|
||||
resizeRef?: RefObject<HTMLDivElement>;
|
||||
initialTopPanelHeight?: number;
|
||||
minTopPanelHeight?: number;
|
||||
minMainPanelHeight?: number;
|
||||
topPanel?: ReactElement;
|
||||
mainPanel?: ReactElement;
|
||||
attachTo?: HTMLElement;
|
||||
}) => {
|
||||
return mount(
|
||||
<DiscoverPanelsResizable
|
||||
className={className}
|
||||
resizeRef={resizeRef}
|
||||
initialTopPanelHeight={initialTopPanelHeight}
|
||||
minTopPanelHeight={minTopPanelHeight}
|
||||
minMainPanelHeight={minMainPanelHeight}
|
||||
topPanel={topPanel}
|
||||
mainPanel={mainPanel}
|
||||
/>,
|
||||
attachTo ? { attachTo } : undefined
|
||||
);
|
||||
};
|
||||
|
||||
const expectCorrectPanelSizes = (
|
||||
component: ReactWrapper,
|
||||
currentContainerHeight: number,
|
||||
topPanelHeight: number
|
||||
) => {
|
||||
const topPanelSize = (topPanelHeight / currentContainerHeight) * 100;
|
||||
expect(component.find('[data-test-subj="dscResizablePanelTop"]').at(0).prop('size')).toBe(
|
||||
topPanelSize
|
||||
);
|
||||
expect(component.find('[data-test-subj="dscResizablePanelMain"]').at(0).prop('size')).toBe(
|
||||
100 - topPanelSize
|
||||
);
|
||||
};
|
||||
|
||||
const forceRender = (component: ReactWrapper) => {
|
||||
component.setProps({}).update();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: containerHeight, width: 0 });
|
||||
});
|
||||
|
||||
it('should render both panels', () => {
|
||||
const topPanel = <div data-test-subj="topPanel" />;
|
||||
const mainPanel = <div data-test-subj="mainPanel" />;
|
||||
const component = mountComponent({ topPanel, mainPanel });
|
||||
expect(component.contains(topPanel)).toBe(true);
|
||||
expect(component.contains(mainPanel)).toBe(true);
|
||||
});
|
||||
|
||||
it('should set the initial heights of both panels', () => {
|
||||
const initialTopPanelHeight = 200;
|
||||
const component = mountComponent({ initialTopPanelHeight });
|
||||
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
|
||||
});
|
||||
|
||||
it('should set the correct heights of both panels when the panels are resized', () => {
|
||||
const initialTopPanelHeight = 200;
|
||||
const component = mountComponent({ initialTopPanelHeight });
|
||||
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
|
||||
const newTopPanelSize = 30;
|
||||
const onPanelSizeChange = component
|
||||
.find('[data-test-subj="dscResizableContainer"]')
|
||||
.at(0)
|
||||
.prop('onPanelWidthChange') as Function;
|
||||
act(() => {
|
||||
onPanelSizeChange({ [topPanelId]: newTopPanelSize });
|
||||
});
|
||||
forceRender(component);
|
||||
expectCorrectPanelSizes(component, containerHeight, containerHeight * (newTopPanelSize / 100));
|
||||
});
|
||||
|
||||
it('should maintain the height of the top panel and resize the main panel when the container height changes', () => {
|
||||
const initialTopPanelHeight = 200;
|
||||
const component = mountComponent({ initialTopPanelHeight });
|
||||
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
|
||||
const newContainerHeight = 2000;
|
||||
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: newContainerHeight, width: 0 });
|
||||
forceRender(component);
|
||||
expectCorrectPanelSizes(component, newContainerHeight, initialTopPanelHeight);
|
||||
});
|
||||
|
||||
it('should resize the top panel once the main panel is at its minimum height', () => {
|
||||
const initialTopPanelHeight = 500;
|
||||
const minTopPanelHeight = 100;
|
||||
const minMainPanelHeight = 100;
|
||||
const component = mountComponent({
|
||||
initialTopPanelHeight,
|
||||
minTopPanelHeight,
|
||||
minMainPanelHeight,
|
||||
});
|
||||
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
|
||||
const newContainerHeight = 400;
|
||||
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: newContainerHeight, width: 0 });
|
||||
forceRender(component);
|
||||
expectCorrectPanelSizes(component, newContainerHeight, newContainerHeight - minMainPanelHeight);
|
||||
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: containerHeight, width: 0 });
|
||||
forceRender(component);
|
||||
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
|
||||
});
|
||||
|
||||
it('should maintain the minimum heights of both panels when the container is too small to fit them', () => {
|
||||
const initialTopPanelHeight = 500;
|
||||
const minTopPanelHeight = 100;
|
||||
const minMainPanelHeight = 150;
|
||||
const component = mountComponent({
|
||||
initialTopPanelHeight,
|
||||
minTopPanelHeight,
|
||||
minMainPanelHeight,
|
||||
});
|
||||
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
|
||||
const newContainerHeight = 200;
|
||||
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: newContainerHeight, width: 0 });
|
||||
forceRender(component);
|
||||
expect(component.find('[data-test-subj="dscResizablePanelTop"]').at(0).prop('size')).toBe(
|
||||
(minTopPanelHeight / newContainerHeight) * 100
|
||||
);
|
||||
expect(component.find('[data-test-subj="dscResizablePanelMain"]').at(0).prop('size')).toBe(
|
||||
(minMainPanelHeight / newContainerHeight) * 100
|
||||
);
|
||||
jest.spyOn(eui, 'useResizeObserver').mockReturnValue({ height: containerHeight, width: 0 });
|
||||
forceRender(component);
|
||||
expectCorrectPanelSizes(component, containerHeight, initialTopPanelHeight);
|
||||
});
|
||||
|
||||
it('should blur the resize button after a resize', async () => {
|
||||
const attachTo = document.createElement('div');
|
||||
document.body.appendChild(attachTo);
|
||||
const component = mountComponent({ attachTo });
|
||||
const wrapper = component.find('[data-test-subj="dscResizableContainerWrapper"]');
|
||||
const resizeButton = component.find('button[data-test-subj="dsc-resizable-button"]');
|
||||
const resizeButtonInner = component.find('[data-test-subj="dscResizableButtonInner"]');
|
||||
const mouseEvent = {
|
||||
pageX: 0,
|
||||
pageY: 0,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
};
|
||||
resizeButtonInner.simulate('mousedown', mouseEvent);
|
||||
resizeButton.simulate('mousedown', mouseEvent);
|
||||
(resizeButton.getDOMNode() as HTMLElement).focus();
|
||||
wrapper.simulate('mouseup', mouseEvent);
|
||||
resizeButton.simulate('click', mouseEvent);
|
||||
expect(resizeButton.getDOMNode()).toHaveFocus();
|
||||
await waitFor(() => {
|
||||
expect(resizeButton.getDOMNode()).not.toHaveFocus();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* 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 { EuiResizableContainer, useGeneratedHtmlId, useResizeObserver } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { ReactElement, RefObject, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
const percentToPixels = (containerHeight: number, percentage: number) =>
|
||||
Math.round(containerHeight * (percentage / 100));
|
||||
|
||||
const pixelsToPercent = (containerHeight: number, pixels: number) =>
|
||||
+((pixels / containerHeight) * 100).toFixed(4);
|
||||
|
||||
export const DiscoverPanelsResizable = ({
|
||||
className,
|
||||
resizeRef,
|
||||
initialTopPanelHeight,
|
||||
minTopPanelHeight,
|
||||
minMainPanelHeight,
|
||||
topPanel,
|
||||
mainPanel,
|
||||
}: {
|
||||
className?: string;
|
||||
resizeRef: RefObject<HTMLDivElement>;
|
||||
initialTopPanelHeight: number;
|
||||
minTopPanelHeight: number;
|
||||
minMainPanelHeight: number;
|
||||
topPanel: ReactElement;
|
||||
mainPanel: ReactElement;
|
||||
}) => {
|
||||
const topPanelId = useGeneratedHtmlId({ prefix: 'topPanel' });
|
||||
const { height: containerHeight } = useResizeObserver(resizeRef.current);
|
||||
const [topPanelHeight, setTopPanelHeight] = useState(initialTopPanelHeight);
|
||||
const [panelSizes, setPanelSizes] = useState({ topPanelSize: 0, mainPanelSize: 0 });
|
||||
|
||||
// EuiResizableContainer doesn't work properly when used with react-reverse-portal and
|
||||
// will cancel the resize. To work around this we keep track of when resizes start and
|
||||
// end to toggle the rendering of a transparent overlay which prevents the cancellation.
|
||||
// EUI issue: https://github.com/elastic/eui/issues/6199
|
||||
const [resizeWithPortalsHackIsResizing, setResizeWithPortalsHackIsResizing] = useState(false);
|
||||
const enableResizeWithPortalsHack = () => setResizeWithPortalsHackIsResizing(true);
|
||||
const disableResizeWithPortalsHack = () => setResizeWithPortalsHackIsResizing(false);
|
||||
const resizeWithPortalsHackFillCss = css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
const resizeWithPortalsHackButtonCss = css`
|
||||
z-index: 3;
|
||||
`;
|
||||
const resizeWithPortalsHackButtonInnerCss = css`
|
||||
${resizeWithPortalsHackFillCss}
|
||||
z-index: 1;
|
||||
`;
|
||||
const resizeWithPortalsHackOverlayCss = css`
|
||||
${resizeWithPortalsHackFillCss}
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
// Instead of setting the panel sizes directly, we convert the top panel height
|
||||
// from a percentage of the container height to a pixel value. This will trigger
|
||||
// the effect below to update the panel sizes.
|
||||
const onPanelSizeChange = useCallback(
|
||||
({ [topPanelId]: topPanelSize }: { [key: string]: number }) => {
|
||||
setTopPanelHeight(percentToPixels(containerHeight, topPanelSize));
|
||||
},
|
||||
[containerHeight, topPanelId]
|
||||
);
|
||||
|
||||
// This effect will update the panel sizes based on the top panel height whenever
|
||||
// it or the container height changes. This allows us to keep the height of the
|
||||
// top panel panel fixed when the window is resized.
|
||||
useEffect(() => {
|
||||
if (!containerHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
let topPanelSize: number;
|
||||
let mainPanelSize: number;
|
||||
|
||||
// If the container height is less than the minimum main content height
|
||||
// plus the current top panel height, then we need to make some adjustments.
|
||||
if (containerHeight < minMainPanelHeight + topPanelHeight) {
|
||||
const newTopPanelHeight = containerHeight - minMainPanelHeight;
|
||||
|
||||
// Try to make the top panel height fit within the container, but if it
|
||||
// doesn't then just use the minimum heights.
|
||||
if (newTopPanelHeight < minTopPanelHeight) {
|
||||
topPanelSize = pixelsToPercent(containerHeight, minTopPanelHeight);
|
||||
mainPanelSize = pixelsToPercent(containerHeight, minMainPanelHeight);
|
||||
} else {
|
||||
topPanelSize = pixelsToPercent(containerHeight, newTopPanelHeight);
|
||||
mainPanelSize = 100 - topPanelSize;
|
||||
}
|
||||
} else {
|
||||
topPanelSize = pixelsToPercent(containerHeight, topPanelHeight);
|
||||
mainPanelSize = 100 - topPanelSize;
|
||||
}
|
||||
|
||||
setPanelSizes({ topPanelSize, mainPanelSize });
|
||||
}, [containerHeight, topPanelHeight, minTopPanelHeight, minMainPanelHeight]);
|
||||
|
||||
const onResizeEnd = () => {
|
||||
// We don't want the resize button to retain focus after the resize is complete,
|
||||
// but EuiResizableContainer will force focus it onClick. To work around this we
|
||||
// use setTimeout to wait until after onClick has been called before blurring.
|
||||
if (resizeWithPortalsHackIsResizing && document.activeElement instanceof HTMLElement) {
|
||||
const button = document.activeElement;
|
||||
setTimeout(() => {
|
||||
button.blur();
|
||||
});
|
||||
}
|
||||
|
||||
disableResizeWithPortalsHack();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="eui-fullHeight"
|
||||
onMouseUp={onResizeEnd}
|
||||
onMouseLeave={onResizeEnd}
|
||||
onTouchEnd={onResizeEnd}
|
||||
data-test-subj="dscResizableContainerWrapper"
|
||||
>
|
||||
<EuiResizableContainer
|
||||
className={className}
|
||||
direction="vertical"
|
||||
onPanelWidthChange={onPanelSizeChange}
|
||||
data-test-subj="dscResizableContainer"
|
||||
>
|
||||
{(EuiResizablePanel, EuiResizableButton) => (
|
||||
<>
|
||||
<EuiResizablePanel
|
||||
id={topPanelId}
|
||||
minSize={`${minTopPanelHeight}px`}
|
||||
size={panelSizes.topPanelSize}
|
||||
paddingSize="none"
|
||||
data-test-subj="dscResizablePanelTop"
|
||||
>
|
||||
{topPanel}
|
||||
</EuiResizablePanel>
|
||||
<EuiResizableButton
|
||||
css={resizeWithPortalsHackButtonCss}
|
||||
data-test-subj="dsc-resizable-button"
|
||||
>
|
||||
<span
|
||||
onMouseDown={enableResizeWithPortalsHack}
|
||||
onTouchStart={enableResizeWithPortalsHack}
|
||||
css={resizeWithPortalsHackButtonInnerCss}
|
||||
data-test-subj="dscResizableButtonInner"
|
||||
/>
|
||||
</EuiResizableButton>
|
||||
<EuiResizablePanel
|
||||
minSize={`${minMainPanelHeight}px`}
|
||||
size={panelSizes.mainPanelSize}
|
||||
paddingSize="none"
|
||||
data-test-subj="dscResizablePanelMain"
|
||||
>
|
||||
{mainPanel}
|
||||
</EuiResizablePanel>
|
||||
{resizeWithPortalsHackIsResizing ? (
|
||||
<div css={resizeWithPortalsHackOverlayCss} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</EuiResizableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
@import 'view_mode_toggle';
|
|
@ -1,12 +0,0 @@
|
|||
.dscViewModeToggle {
|
||||
padding-right: $euiSize;
|
||||
}
|
||||
|
||||
.fieldStatsButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fieldStatsBetaBadge {
|
||||
margin-left: $euiSizeXS;
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { EuiTab } from '@elastic/eui';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import React from 'react';
|
||||
import { VIEW_MODE } from './constants';
|
||||
import { DocumentViewModeToggle } from './view_mode_toggle';
|
||||
|
||||
describe('Document view mode toggle component', () => {
|
||||
const mountComponent = ({
|
||||
showFieldStatistics = true,
|
||||
viewMode = VIEW_MODE.DOCUMENT_LEVEL,
|
||||
setDiscoverViewMode = jest.fn(),
|
||||
} = {}) => {
|
||||
const serivces = {
|
||||
uiSettings: {
|
||||
get: () => showFieldStatistics,
|
||||
},
|
||||
};
|
||||
|
||||
return mountWithIntl(
|
||||
<KibanaContextProvider services={serivces}>
|
||||
<DocumentViewModeToggle viewMode={viewMode} setDiscoverViewMode={setDiscoverViewMode} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
it('should render if SHOW_FIELD_STATISTICS is true', () => {
|
||||
const component = mountComponent();
|
||||
expect(component.isEmptyRender()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not render if SHOW_FIELD_STATISTICS is false', () => {
|
||||
const component = mountComponent({ showFieldStatistics: false });
|
||||
expect(component.isEmptyRender()).toBe(true);
|
||||
});
|
||||
|
||||
it('should set the view mode to VIEW_MODE.DOCUMENT_LEVEL when dscViewModeDocumentButton is clicked', () => {
|
||||
const setDiscoverViewMode = jest.fn();
|
||||
const component = mountComponent({ setDiscoverViewMode });
|
||||
component.find('[data-test-subj="dscViewModeDocumentButton"]').at(0).simulate('click');
|
||||
expect(setDiscoverViewMode).toHaveBeenCalledWith(VIEW_MODE.DOCUMENT_LEVEL);
|
||||
});
|
||||
|
||||
it('should set the view mode to VIEW_MODE.AGGREGATED_LEVEL when dscViewModeFieldStatsButton is clicked', () => {
|
||||
const setDiscoverViewMode = jest.fn();
|
||||
const component = mountComponent({ setDiscoverViewMode });
|
||||
component.find('[data-test-subj="dscViewModeFieldStatsButton"]').at(0).simulate('click');
|
||||
expect(setDiscoverViewMode).toHaveBeenCalledWith(VIEW_MODE.AGGREGATED_LEVEL);
|
||||
});
|
||||
|
||||
it('should select the Documents tab if viewMode is VIEW_MODE.DOCUMENT_LEVEL', () => {
|
||||
const component = mountComponent();
|
||||
expect(component.find(EuiTab).at(0).prop('isSelected')).toBe(true);
|
||||
});
|
||||
|
||||
it('should select the Field statistics tab if viewMode is VIEW_MODE.AGGREGATED_LEVEL', () => {
|
||||
const component = mountComponent({ viewMode: VIEW_MODE.AGGREGATED_LEVEL });
|
||||
expect(component.find(EuiTab).at(1).prop('isSelected')).toBe(true);
|
||||
});
|
||||
});
|
|
@ -6,12 +6,22 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiButtonGroup, EuiBetaBadge } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiTabs,
|
||||
EuiTab,
|
||||
useEuiPaddingSize,
|
||||
EuiBetaBadge,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { VIEW_MODE } from './constants';
|
||||
import './_index.scss';
|
||||
import { SHOW_FIELD_STATISTICS } from '../../../common';
|
||||
import { useDiscoverServices } from '../../hooks/use_discover_services';
|
||||
|
||||
export const DocumentViewModeToggle = ({
|
||||
viewMode,
|
||||
|
@ -20,23 +30,47 @@ export const DocumentViewModeToggle = ({
|
|||
viewMode: VIEW_MODE;
|
||||
setDiscoverViewMode: (viewMode: VIEW_MODE) => void;
|
||||
}) => {
|
||||
const toggleButtons = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: VIEW_MODE.DOCUMENT_LEVEL,
|
||||
label: i18n.translate('discover.viewModes.document.label', {
|
||||
defaultMessage: 'Documents',
|
||||
}),
|
||||
'data-test-subj': 'dscViewModeDocumentButton',
|
||||
},
|
||||
{
|
||||
id: VIEW_MODE.AGGREGATED_LEVEL,
|
||||
label: (
|
||||
<div className="fieldStatsButton" data-test-subj="dscViewModeFieldStatsButton">
|
||||
const { uiSettings } = useDiscoverServices();
|
||||
|
||||
const tabsCss = css`
|
||||
padding: 0 ${useEuiPaddingSize('s')};
|
||||
background-color: ${euiThemeVars.euiPageBackgroundColor};
|
||||
`;
|
||||
|
||||
const badgeCellCss = css`
|
||||
margin-left: ${useEuiPaddingSize('s')};
|
||||
`;
|
||||
|
||||
const showViewModeToggle = uiSettings.get(SHOW_FIELD_STATISTICS) ?? false;
|
||||
|
||||
if (!showViewModeToggle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiTabs size="s" css={tabsCss} data-test-subj="dscViewModeToggle">
|
||||
<EuiTab
|
||||
isSelected={viewMode === VIEW_MODE.DOCUMENT_LEVEL}
|
||||
onClick={() => setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL)}
|
||||
className="dscViewModeToggle__tab"
|
||||
data-test-subj="dscViewModeDocumentButton"
|
||||
>
|
||||
<FormattedMessage id="discover.viewModes.document.label" defaultMessage="Documents" />
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
isSelected={viewMode === VIEW_MODE.AGGREGATED_LEVEL}
|
||||
onClick={() => setDiscoverViewMode(VIEW_MODE.AGGREGATED_LEVEL)}
|
||||
className="dscViewModeToggle__tab"
|
||||
data-test-subj="dscViewModeFieldStatsButton"
|
||||
>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<FormattedMessage
|
||||
id="discover.viewModes.fieldStatistics.label"
|
||||
defaultMessage="Field statistics"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem css={badgeCellCss}>
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate('discover.viewModes.fieldStatistics.betaTitle', {
|
||||
defaultMessage: 'Beta',
|
||||
|
@ -44,22 +78,9 @@ export const DocumentViewModeToggle = ({
|
|||
size="s"
|
||||
className="fieldStatsBetaBadge"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiButtonGroup
|
||||
className={'dscViewModeToggle'}
|
||||
legend={i18n.translate('discover.viewModes.legend', { defaultMessage: 'View modes' })}
|
||||
buttonSize={'compressed'}
|
||||
options={toggleButtons}
|
||||
idSelected={viewMode}
|
||||
onChange={(id: string) => setDiscoverViewMode(id as VIEW_MODE)}
|
||||
data-test-subj={'dscViewModeToggle'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const queryBar = getService('queryBar');
|
||||
const inspector = getService('inspector');
|
||||
const elasticChart = getService('elasticChart');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
|
||||
|
||||
const defaultSettings = {
|
||||
|
@ -342,5 +343,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.timePicker.pauseAutoRefresh();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resizable layout panels', () => {
|
||||
it('should allow resizing the layout panels', async () => {
|
||||
const resizeDistance = 100;
|
||||
const topPanel = await testSubjects.find('dscResizablePanelTop');
|
||||
const mainPanel = await testSubjects.find('dscResizablePanelMain');
|
||||
const resizeButton = await testSubjects.find('dsc-resizable-button');
|
||||
const topPanelSize = (await topPanel.getPosition()).height;
|
||||
const mainPanelSize = (await mainPanel.getPosition()).height;
|
||||
await browser.dragAndDrop(
|
||||
{ location: resizeButton },
|
||||
{ location: { x: 0, y: resizeDistance } }
|
||||
);
|
||||
const newTopPanelSize = (await topPanel.getPosition()).height;
|
||||
const newMainPanelSize = (await mainPanel.getPosition()).height;
|
||||
expect(newTopPanelSize).to.be(topPanelSize + resizeDistance);
|
||||
expect(newMainPanelSize).to.be(mainPanelSize - resizeDistance);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2327,7 +2327,6 @@
|
|||
"discover.viewModes.document.label": "Documents",
|
||||
"discover.viewModes.fieldStatistics.betaTitle": "Bêta",
|
||||
"discover.viewModes.fieldStatistics.label": "Statistiques de champ",
|
||||
"discover.viewModes.legend": "Modes d'affichage",
|
||||
"embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} a été ajouté.",
|
||||
"embeddableApi.attributeService.saveToLibraryError": "Une erreur s'est produite lors de l'enregistrement. Erreur : {errorMessage}.",
|
||||
"embeddableApi.errors.embeddableFactoryNotFound": "Impossible de charger {type}. Veuillez effectuer une mise à niveau vers la distribution par défaut d'Elasticsearch et de Kibana avec la licence appropriée.",
|
||||
|
|
|
@ -2323,7 +2323,6 @@
|
|||
"discover.viewModes.document.label": "ドキュメント",
|
||||
"discover.viewModes.fieldStatistics.betaTitle": "ベータ",
|
||||
"discover.viewModes.fieldStatistics.label": "フィールド統計情報",
|
||||
"discover.viewModes.legend": "表示モード",
|
||||
"embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} が追加されました",
|
||||
"embeddableApi.attributeService.saveToLibraryError": "保存中にエラーが発生しました。エラー:{errorMessage}",
|
||||
"embeddableApi.errors.embeddableFactoryNotFound": "{type} を読み込めません。Elasticsearch と Kibana のデフォルトのディストリビューションを適切なライセンスでアップグレードしてください。",
|
||||
|
|
|
@ -2327,7 +2327,6 @@
|
|||
"discover.viewModes.document.label": "文档",
|
||||
"discover.viewModes.fieldStatistics.betaTitle": "公测版",
|
||||
"discover.viewModes.fieldStatistics.label": "字段统计信息",
|
||||
"discover.viewModes.legend": "视图模式",
|
||||
"embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} 已添加",
|
||||
"embeddableApi.attributeService.saveToLibraryError": "保存时出错。错误:{errorMessage}",
|
||||
"embeddableApi.errors.embeddableFactoryNotFound": "{type} 无法加载。请升级到具有适当许可的默认 Elasticsearch 和 Kibana 分发。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue