diff --git a/packages/osd-stylelint-config/config/global_selectors.json b/packages/osd-stylelint-config/config/global_selectors.json index ef2ec5f9252b..285f11f40a2b 100644 --- a/packages/osd-stylelint-config/config/global_selectors.json +++ b/packages/osd-stylelint-config/config/global_selectors.json @@ -24,7 +24,8 @@ "src/plugins/vis_builder/public/application/components/side_nav.scss", "packages/osd-ui-framework/src/components/button/button_group/_button_group.scss", "src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_value.scss", - "src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss" + "src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss", + "src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss" ] } } \ No newline at end of file diff --git a/packages/osd-ui-shared-deps/package.json b/packages/osd-ui-shared-deps/package.json index 1c2b532a7227..c823feee03b2 100644 --- a/packages/osd-ui-shared-deps/package.json +++ b/packages/osd-ui-shared-deps/package.json @@ -52,4 +52,3 @@ "webpack": "npm:@amoo-miki/webpack@4.46.0-rc.2" } } - diff --git a/src/core/server/core_app/assets/legacy_dark_theme.css b/src/core/server/core_app/assets/legacy_dark_theme.css index 4ef4c726e414..b56eeed717a1 100644 --- a/src/core/server/core_app/assets/legacy_dark_theme.css +++ b/src/core/server/core_app/assets/legacy_dark_theme.css @@ -802,7 +802,7 @@ width: 100%; max-width: 100%; margin-bottom: 20px; - font-size: 14px; + font-size: 12px; } .table thead { font-size: 12px; diff --git a/src/core/server/core_app/assets/legacy_light_theme.css b/src/core/server/core_app/assets/legacy_light_theme.css index 9f9a0dc118d1..d4f6d10e7022 100644 --- a/src/core/server/core_app/assets/legacy_light_theme.css +++ b/src/core/server/core_app/assets/legacy_light_theme.css @@ -802,7 +802,7 @@ width: 100%; max-width: 100%; margin-bottom: 20px; - font-size: 14px; + font-size: 12px; } .table thead { font-size: 12px; diff --git a/src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx b/src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx index 2e6768197136..1c6876815a0e 100644 --- a/src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx +++ b/src/plugins/data/public/data_sources/datasource_selector/datasource_selectable.tsx @@ -103,6 +103,7 @@ export const DataSourceSelectable = ({ setDataSourceOptionList, onGetDataSetError, // onGetDataSetError, Callback for handling get data set errors. Ensure it's memoized. singleSelection = { asPlainText: true }, + ...comboBoxProps }: DataSourceSelectableProps) => { // This effect gets data sets and prepares the datasource list for UI rendering. useEffect(() => { @@ -131,6 +132,7 @@ export const DataSourceSelectable = ({ return ( , 'fullWidth'> { dataSources: GenericDataSource[]; onDataSourceSelect: (dataSourceOption: DataSourceOption[]) => void; singleSelection?: boolean | EuiComboBoxSingleSelectionShape; diff --git a/src/plugins/data/public/ui/query_string_input/_query_bar.scss b/src/plugins/data/public/ui/query_string_input/_query_bar.scss index 096c7c0ad8bc..c9eab29d9e2d 100644 --- a/src/plugins/data/public/ui/query_string_input/_query_bar.scss +++ b/src/plugins/data/public/ui/query_string_input/_query_bar.scss @@ -33,9 +33,8 @@ // Unlike most inputs within layout control groups, the text area still needs a border. // These adjusts help it sit above the control groups shadow to line up correctly. - padding: $euiSizeS; - padding-top: $euiSizeS + 3px; - transform: translateY(-1px) translateX(-1px); + padding: ($euiSizeS + 2px) $euiSizeS $euiSizeS; + transform: translateY(-2px) translateX(-1px); &:not(:focus):not(:invalid) { @include euiYScrollWithShadows; diff --git a/src/plugins/data_explorer/public/components/app_container.scss b/src/plugins/data_explorer/public/components/app_container.scss index d5b6df038208..7bd5ed6f69f6 100644 --- a/src/plugins/data_explorer/public/components/app_container.scss +++ b/src/plugins/data_explorer/public/components/app_container.scss @@ -1,8 +1,7 @@ $osdHeaderOffset: $euiHeaderHeightCompensation; .deSidebar { - max-width: 462px; - min-width: 400px; + height: 100%; @include ouiBreakpoint("xs", "s", "m") { max-width: initial; diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx index 8f37e9c1230f..529829140057 100644 --- a/src/plugins/data_explorer/public/components/app_container.tsx +++ b/src/plugins/data_explorer/public/components/app_container.tsx @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { EuiPage, EuiPageBody } from '@elastic/eui'; +import React, { memo } from 'react'; +import { EuiPage, EuiPageBody, EuiResizableContainer, useIsWithinBreakpoints } from '@elastic/eui'; import { Suspense } from 'react'; import { AppMountParameters } from '../../../../core/public'; import { Sidebar } from './sidebar'; @@ -13,6 +13,7 @@ import { View } from '../services/view_service/view'; import './app_container.scss'; export const AppContainer = ({ view, params }: { view?: View; params: AppMountParameters }) => { + const isMobile = useIsWithinBreakpoints(['xs', 's', 'm']); // TODO: Make this more robust. if (!view) { return ; @@ -20,18 +21,38 @@ export const AppContainer = ({ view, params }: { view?: View; params: AppMountPa const { Canvas, Panel, Context } = view; + const MemoizedPanel = memo(Panel); + const MemoizedCanvas = memo(Canvas); + // Render the application DOM. return ( {/* TODO: improve fallback state */} Loading...}> - - - - - - + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + + + + + + + + + )} + diff --git a/src/plugins/data_explorer/public/components/no_view.tsx b/src/plugins/data_explorer/public/components/no_view.tsx index a341e9d0564e..20bdf83f1de6 100644 --- a/src/plugins/data_explorer/public/components/no_view.tsx +++ b/src/plugins/data_explorer/public/components/no_view.tsx @@ -11,7 +11,7 @@ export const NoView = () => { return ( { const { indexPattern: indexPatternId } = useTypedSelector((state) => state.metadata); @@ -91,8 +92,18 @@ export const Sidebar: FC = ({ children }) => { return ( - - + + { onDataSourceSelect={handleSourceSelection} selectedSources={selectedSources} onGetDataSetError={handleGetDataSetError} + fullWidth /> - + {children} diff --git a/src/plugins/data_explorer/public/index.scss b/src/plugins/data_explorer/public/index.scss deleted file mode 100644 index 8389e31b426a..000000000000 --- a/src/plugins/data_explorer/public/index.scss +++ /dev/null @@ -1,9 +0,0 @@ -$osdHeaderOffset: $euiHeaderHeightCompensation; - -.dePageTemplate { - height: calc(100vh - #{$osdHeaderOffset}); -} - -.headerIsExpanded .dePageTemplate { - height: calc(100vh - #{$osdHeaderOffset * 2}); -} diff --git a/src/plugins/data_explorer/public/index.ts b/src/plugins/data_explorer/public/index.ts index cb33d2b7d90c..f8adda434ced 100644 --- a/src/plugins/data_explorer/public/index.ts +++ b/src/plugins/data_explorer/public/index.ts @@ -3,8 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.scss'; - import { DataExplorerPlugin } from './plugin'; // This exports static code and TypeScript types, diff --git a/src/plugins/discover/public/application/components/chart/histogram/histogram.tsx b/src/plugins/discover/public/application/components/chart/histogram/histogram.tsx index 51d3f1f1b706..87917c113c2f 100644 --- a/src/plugins/discover/public/application/components/chart/histogram/histogram.tsx +++ b/src/plugins/discover/public/application/components/chart/histogram/histogram.tsx @@ -306,6 +306,14 @@ export class DiscoverHistogram extends Component void; onFilter: DocViewFilterFn; + onMoveColumn: (colName: string, destination: number) => void; onRemoveColumn: (column: string) => void; - onSort: (sort: SortOrder[]) => void; + hits?: number; + onSort: (s: SortOrder[]) => void; rows: OpenSearchSearchHit[]; onSetColumns: (columns: string[]) => void; sort: SortOrder[]; @@ -36,6 +43,7 @@ export interface DataGridTableProps { isToolbarVisible?: boolean; isContextView?: boolean; isLoading?: boolean; + showPagination?: boolean; } export const DataGridTable = ({ @@ -43,10 +51,12 @@ export const DataGridTable = ({ indexPattern, onAddColumn, onFilter, + onMoveColumn, onRemoveColumn, onSetColumns, onSort, sort, + hits, rows, displayTimeColumn, title = '', @@ -54,12 +64,20 @@ export const DataGridTable = ({ isToolbarVisible = true, isContextView = false, isLoading = false, + showPagination, }: DataGridTableProps) => { - const { services } = useOpenSearchDashboards(); - + const services = getServices(); const [inspectedHit, setInspectedHit] = useState(); const rowCount = useMemo(() => (rows ? rows.length : 0), [rows]); - const pageSizeLimit = services.uiSettings?.get(SAMPLE_SIZE_SETTING); + const { toolbarOptions, lineCount } = useToolbarOptions(); + const [pageSizeLimit, isShortDots, hideTimeColumn, defaultSortOrder] = useMemo(() => { + return [ + services.uiSettings.get(SAMPLE_SIZE_SETTING), + services.uiSettings.get(UI_SETTINGS.SHORT_DOTS_ENABLE), + services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING), + services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc') as SortDirection, + ]; + }, [services.uiSettings]); const pagination = usePagination({ rowCount, pageSizeLimit }); let adjustedColumns = buildColumns(columns); @@ -77,10 +95,10 @@ export const DataGridTable = ({ const rowHeightsOptions = useMemo( () => ({ defaultHeight: { - lineCount: adjustedColumns.includes('_source') ? 3 : 1, + lineCount: lineCount || (includeSourceInColumns ? 3 : 1), }, }), - [adjustedColumns] + [includeSourceInColumns, lineCount] ); const onColumnSort = useCallback( @@ -90,12 +108,13 @@ export const DataGridTable = ({ [onSort] ); - const renderCellValue = useMemo(() => fetchTableDataCell(indexPattern, rows), [ + const renderCellValue = useMemo(() => fetchTableDataCell(indexPattern, rows, isShortDots), [ indexPattern, + isShortDots, rows, ]); - const dataGridTableColumns = useMemo( + const displayedTableColumns = useMemo( () => buildDataGridColumns( adjustedColumns, @@ -137,11 +156,53 @@ export const DataGridTable = ({ ]; }, []); - const table = useMemo( + const newDiscoverEnabled = getNewDiscoverSetting(services.storage); + + const legacyDiscoverTable = useMemo( + () => ( + setInspectedHit(undefined)} + sampleSize={pageSizeLimit} + showPagination={showPagination} + isShortDots={isShortDots} + hideTimeColumn={hideTimeColumn} + defaultSortOrder={defaultSortOrder} + /> + ), + [ + adjustedColumns, + hits, + rows, + indexPattern, + sort, + onSort, + onRemoveColumn, + onMoveColumn, + onAddColumn, + onFilter, + pageSizeLimit, + showPagination, + defaultSortOrder, + hideTimeColumn, + isShortDots, + ] + ); + + const dataGridTable = useMemo( () => ( ), [ - dataGridTableColumns, + displayedTableColumns, dataGridTableColumnsVisibility, leadingControlColumns, pagination, @@ -162,10 +223,27 @@ export const DataGridTable = ({ rowCount, sorting, isToolbarVisible, + toolbarOptions, rowHeightsOptions, ] ); + const tablePanelProps = newDiscoverEnabled + ? { + paddingSize: 'none' as const, + style: { + margin: '8px', + }, + color: 'transparent' as const, + } + : { + paddingSize: 'none' as const, + style: { + margin: '0px', + }, + color: 'transparent' as const, + }; + return ( - - - {table} - + + {newDiscoverEnabled ? dataGridTable : legacyDiscoverTable} - {inspectedHit && ( + {newDiscoverEnabled && inspectedHit && ( { it('should display empty span if no data', () => { - const DataGridTableCellValue = fetchTableDataCell(indexPatternMock, dataRowsMock); + const DataGridTableCellValue = fetchTableDataCell(indexPatternMock, dataRowsMock, false); const comp = shallow( { }); it('should display empty span if field is not defined in index pattern', () => { - const DataGridTableCellValue = fetchTableDataCell(indexPatternMock, dataRowsMock); + const DataGridTableCellValue = fetchTableDataCell(indexPatternMock, dataRowsMock, false); const comp = shallow( { }); it('should display JSON string representation of the data if columnId is _source and isDetails is false', () => { - const DataGridTableCellValue = fetchTableDataCell(customizedIndexPatternMock, dataRowsMock); + const DataGridTableCellValue = fetchTableDataCell( + customizedIndexPatternMock, + dataRowsMock, + false + ); const comp = shallow( { }); it('should display EuiDescriptionList if columnId is _source and isDetails is false', () => { - const DataGridTableCellValue = fetchTableDataCell(customizedIndexPatternMock, dataRowsMock); + const DataGridTableCellValue = fetchTableDataCell( + customizedIndexPatternMock, + dataRowsMock, + false + ); const comp = shallow( { expect(comp).toMatchInlineSnapshot(` - order_date + order_date: { }); it('should correctly display data if columnId is in index pattern and is not _source', () => { - const DataGridTableCellValue = fetchTableDataCell(customizedIndexPatternMock, dataRowsMock); + const DataGridTableCellValue = fetchTableDataCell( + customizedIndexPatternMock, + dataRowsMock, + false + ); const comp = shallow( , columnId: string, - isDetails: boolean + isDetails: boolean, + isShortDots: boolean ) { if (isDetails) { return {stringify(row[columnId], null, 2)}; } const formattedRow = idxPattern.formatHit(row); + const rawKeys = Object.keys(formattedRow); + const keys = isShortDots ? rawKeys.map((k) => shortenDottedString(k)) : rawKeys; return ( - - {Object.keys(formattedRow).map((key) => ( + + {keys.map((key, index) => ( - {key} + {key + ':'} + {index !== keys.length - 1 && ' '} ))} @@ -46,7 +51,8 @@ function fetchSourceTypeDataCell( export const fetchTableDataCell = ( idxPattern: IndexPattern, - dataRows: OpenSearchSearchHit[] | undefined + dataRows: OpenSearchSearchHit[] | undefined, + isShortDots: boolean ) => ({ rowIndex, columnId, isDetails }: EuiDataGridCellValueElementProps) => { const singleRow = dataRows ? (dataRows[rowIndex] as Record) : undefined; const flattenedRows = dataRows ? dataRows.map((hit) => idxPattern.flattenHit(hit)) : []; @@ -68,7 +74,7 @@ export const fetchTableDataCell = ( } if (fieldInfo?.type === '_source') { - return fetchSourceTypeDataCell(idxPattern, singleRow, columnId, isDetails); + return fetchSourceTypeDataCell(idxPattern, singleRow, columnId, isDetails, isShortDots); } const formattedValue = idxPattern.formatField(singleRow, columnId); diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_toolbar.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_toolbar.tsx new file mode 100644 index 000000000000..07204560040e --- /dev/null +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_toolbar.tsx @@ -0,0 +1,81 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButtonEmpty, + EuiDataGridToolBarVisibilityOptions, + EuiFormRow, + EuiPopover, + EuiRange, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { useLocalStorage } from 'react-use'; + +const AddtitionalControls = ({ + setLineCount, + lineCount, +}: { + setLineCount: (lineCount: number) => void; + lineCount: number; +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const handleRangeChange = (e: any) => { + setLineCount(Number(e.target.value)); + }; + + const onButtonClick = () => setIsPopoverOpen((open) => !open); + const closePopover = () => {}; + + const button = ( + + Display + + ); + + return ( + + + + + + ); +}; + +export const useToolbarOptions = (): { + toolbarOptions: EuiDataGridToolBarVisibilityOptions | boolean; + lineCount: number; +} => { + const [lineCount, setLineCount] = useLocalStorage('discover:lineCount', 1); + + const toolbarOptions = { + showColumnSelector: { + allowHide: false, + allowReorder: true, + }, + showStyleSelector: false, + showFullScreenSelector: false, + additionalControls: , + }; + + return { + toolbarOptions, + lineCount, + }; +}; diff --git a/src/plugins/discover/public/application/components/default_discover_table/_doc_table.scss b/src/plugins/discover/public/application/components/default_discover_table/_doc_table.scss new file mode 100644 index 000000000000..1e780a7e4d8a --- /dev/null +++ b/src/plugins/discover/public/application/components/default_discover_table/_doc_table.scss @@ -0,0 +1,165 @@ +/** + * 1. Stack content vertically so the table can scroll when its constrained by a fixed container height. + */ +doc-table { + @include euiScrollBar; + + overflow: auto; + flex: 1 1 100%; + flex-direction: column; /* 1 */ + + th { + text-align: left; + font-weight: bold; + } + + .spinner { + position: absolute; + top: 40%; + left: 0; + right: 0; + z-index: $euiZLevel1; + opacity: 0.5; + } +} + +.osdDocTable__container.loading { + opacity: 0.5; +} + +.osdDocTable { + font-size: $euiFontSizeXS; + + th { + white-space: nowrap; + padding-right: $euiSizeS; + + .fa { + font-size: 1.1em; + } + } +} + +.osd-table, +.osdDocTable { + @include ouiCodeFont; + + // To fight intruding styles that conflict with OUI's + & > tbody > tr > td { + line-height: inherit; + } + + /** + * Style OpenSearch document _source in table view
key:
value
+ * Use alpha so this will stand out against non-white backgrounds, e.g. the highlighted + * row in the Context Log. + */ + + dl.source { + margin-bottom: 0; + line-height: 2em; + word-break: break-word; + + dt, + dd { + display: inline; + } + + dt { + background-color: transparentize(shade($euiColorPrimary, 20%), 0.9); + color: $euiTextColor; + padding: calc($euiSizeXS / 2) $euiSizeXS; + margin-right: $euiSizeXS; + word-break: normal; + border-radius: $euiBorderRadius; + } + } +} + +.osdDocTable__row { + td { + position: relative; + + &:hover { + .osdDocTableRowFilterButton { + opacity: 1; + } + } + } +} + +.osdDocTable__row--highlight { + td, + .osdDocTableRowFilterButton { + background-color: tintOrShade($euiColorPrimary, 90%, 70%); + } +} + +.osdDocTable__bar { + margin: $euiSizeXS $euiSizeXS 0; +} + +.osdDocTable__bar--footer { + position: relative; + margin: -($euiSize * 3) $euiSizeXS 0; +} + +.osdDocTable__padBottom { + padding-bottom: $euiSizeXL; +} + +.osdDocTable__error { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1 0 100%; + text-align: center; +} + +.truncate-by-height { + overflow: hidden; +} + +.table { + // Nesting + .table { + background-color: $euiColorEmptyShade; + } +} + +.osd-table { + // sub tables should not have a leading border + .table .table { + margin-bottom: 0; + + tr:first-child > td { + border-top: none; + } + + td.field-name { + font-weight: $euiFontWeightBold; + } + } +} + +table { + th { + i.fa-sort { + color: $euiColorLightShade; + } + + button.fa-sort-asc, + button.fa-sort-down, + i.fa-sort-asc, + i.fa-sort-down { + color: $euiColorPrimary; + } + + button.fa-sort-desc, + button.fa-sort-up, + i.fa-sort-desc, + i.fa-sort-up { + color: $euiColorPrimary; + } + } +} diff --git a/src/plugins/discover/public/application/components/default_discover_table/_pagination.scss b/src/plugins/discover/public/application/components/default_discover_table/_pagination.scss new file mode 100644 index 000000000000..793e707e2085 --- /dev/null +++ b/src/plugins/discover/public/application/components/default_discover_table/_pagination.scss @@ -0,0 +1,7 @@ +.osdDocTable_pagination { + width: 100%; + + & ~ table { + margin-bottom: 0; + } +} diff --git a/src/plugins/discover/public/application/components/default_discover_table/_table_cell.scss b/src/plugins/discover/public/application/components/default_discover_table/_table_cell.scss new file mode 100644 index 000000000000..c960e87a9477 --- /dev/null +++ b/src/plugins/discover/public/application/components/default_discover_table/_table_cell.scss @@ -0,0 +1,97 @@ +.osdDocTable__detailsParent { + border-top: none !important; +} + +// stylelint-disable-next-line @osd/stylelint/no_modifying_global_selectors +.euiFlexItem.osdDocTable__detailsIconContainer { + margin-right: 0; +} + +.osd-table td.osdDocTableCell__toggleDetails { + padding: 5px 0 0 4px; +} + +/** + * 1. Align icon with text in cell. + * 2. Use opacity to make this element accessible to screen readers and keyboard. + * 3. Show on focus to enable keyboard accessibility. + */ + +.osdDocTableCell { + position: relative; + + &__filter { + position: absolute; + display: flex; + flex-grow: 0; + + // Vertically align the button group with the first line of text + // 8px is set by .table and 2em is the line-height + top: calc(2em / 2 + 8px); + transform: translateY(-50%); + + // Stick it to the right but use the padding of the container to distance it from the edge (below) + right: 0; + + // Just to have some distance from the content behind it; larger for left so we can show a gradiant + // 8px is set by .table + padding: 8px 8px 8px 16px; + + &::before { + content: ""; + position: absolute; + display: block; + right: 0; + top: 0; + height: 100%; + width: 100%; + background-image: linear-gradient(to right, transparent 0, $ouiColorEmptyShade 16px); + z-index: 1; + } + + & > * { + // So they will appear over the background in ::before + z-index: 2; + } + } + + &__filterButton, + &__filter { + opacity: 0; + transition: opacity $euiAnimSpeedFast; + + @include ouiBreakpoint("xs", "s", "m") { + opacity: 1; + } + } + + &:hover &__filterButton, + &:focus &__filterButton, + &:hover &__filter, + &:focus &__filter { + opacity: 1; + } + + .osdDescriptionListFieldTitle { + margin: 0 4px 0 0 !important; + } + + // stylelint-disable-next-line @osd/stylelint/no_modifying_global_selectors + &.eui-textNoWrap { + // To make sure the time-series column never stretches + width: 1%; + } +} + +.osdDocTableCell__source { + .truncate-by-height { + transform: translateY(-1.5px); + margin-bottom: -1.5px; + } + + dd, + dl, + dt { + font-size: inherit !important; + } +} diff --git a/src/plugins/discover/public/application/components/default_discover_table/_table_header.scss b/src/plugins/discover/public/application/components/default_discover_table/_table_header.scss new file mode 100644 index 000000000000..cd29c1e54ad0 --- /dev/null +++ b/src/plugins/discover/public/application/components/default_discover_table/_table_header.scss @@ -0,0 +1,36 @@ +.osdDocTableHeader { + white-space: nowrap; + text-align: left; +} + +.osdDocTableHeader button { + margin-left: $euiSizeXS; +} + +.osdDocTableHeader__move, +.osdDocTableHeader__sortChange { + opacity: 0; + + &:focus, + th:hover & { + opacity: 1; + } +} + +.docTableHeaderField { + &__actionButton { + opacity: 0; + height: 10px; + width: 10px; + transition: opacity $euiAnimSpeedFast; + + @include ouiBreakpoint("xs", "s", "m") { + opacity: 1; + } + } + + &:hover &__actionButton, + &:focus &__actionButton { + opacity: 1; + } +} diff --git a/src/plugins/discover/public/application/components/default_discover_table/default_discover_table.tsx b/src/plugins/discover/public/application/components/default_discover_table/default_discover_table.tsx new file mode 100644 index 000000000000..fe8092ed8c9c --- /dev/null +++ b/src/plugins/discover/public/application/components/default_discover_table/default_discover_table.tsx @@ -0,0 +1,195 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import './_doc_table.scss'; + +import React, { useEffect, useRef, useState } from 'react'; +import { EuiButtonEmpty, EuiCallOut, EuiProgress } from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import { TableHeader } from './table_header'; +import { DocViewFilterFn, OpenSearchSearchHit } from '../../doc_views/doc_views_types'; +import { TableRow } from './table_rows'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { Pagination } from './pagination'; +import { getLegacyDisplayedColumns } from './helper'; +import { SortDirection, SortOrder } from '../../../saved_searches/types'; + +export interface DefaultDiscoverTableProps { + columns: string[]; + hits?: number; + rows: OpenSearchSearchHit[]; + indexPattern: IndexPattern; + sort: SortOrder[]; + onSort: (s: SortOrder[]) => void; + onRemoveColumn: (column: string) => void; + onMoveColumn: (colName: string, destination: number) => void; + onAddColumn: (column: string) => void; + onFilter: DocViewFilterFn; + onClose: () => void; + sampleSize: number; + isShortDots: boolean; + hideTimeColumn: boolean; + defaultSortOrder: SortDirection; + showPagination?: boolean; +} + +export const LegacyDiscoverTable = ({ + columns, + hits, + rows, + indexPattern, + sort, + onSort, + onRemoveColumn, + onMoveColumn, + onAddColumn, + onFilter, + onClose, + sampleSize, + isShortDots, + hideTimeColumn, + defaultSortOrder, + showPagination, +}: DefaultDiscoverTableProps) => { + const displayedColumns = getLegacyDisplayedColumns( + columns, + indexPattern, + hideTimeColumn, + isShortDots + ); + const displayedColumnNames = displayedColumns.map((column) => column.name); + const pageSize = 50; + const [renderedRowCount, setRenderedRowCount] = useState(50); // Start with 50 rows + const [displayedRows, setDisplayedRows] = useState(rows.slice(0, pageSize)); + const [currentRowCounts, setCurrentRowCounts] = useState({ + startRow: 0, + endRow: rows.length < pageSize ? rows.length : pageSize, + }); + const observerRef = useRef(null); + const sentinelRef = useRef(null); + + const loadMoreRows = () => { + setRenderedRowCount((prevRowCount) => prevRowCount + 50); // Load 50 more rows + }; + + useEffect(() => { + const sentinel = sentinelRef.current; + observerRef.current = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + loadMoreRows(); + } + }, + { threshold: 1.0 } + ); + + if (sentinelRef.current) { + observerRef.current.observe(sentinelRef.current); + } + + return () => { + if (observerRef.current && sentinel) { + observerRef.current.unobserve(sentinel); + } + }; + }, []); + + const [activePage, setActivePage] = useState(0); + const pageCount = Math.ceil(rows.length / pageSize); + + const goToPage = (pageNumber: number) => { + const startRow = pageNumber * pageSize; + const endRow = + rows.length < pageNumber * pageSize + pageSize + ? rows.length + : pageNumber * pageSize + pageSize; + setCurrentRowCounts({ + startRow, + endRow, + }); + setDisplayedRows(rows.slice(startRow, endRow)); + setActivePage(pageNumber); + }; + + return ( + indexPattern && ( + <> + {showPagination ? ( + + ) : null} + + + + + + {(showPagination ? displayedRows : rows.slice(0, renderedRowCount)).map( + (row: OpenSearchSearchHit, index: number) => { + return ( + + ); + } + )} + +
+ {!showPagination && renderedRowCount < rows.length && ( +
+ +
+ )} + {!showPagination && rows.length === sampleSize && ( + + + + window.scrollTo(0, 0)}> + + + + )} + {showPagination ? ( + + ) : null} + + ) + ); +}; diff --git a/src/plugins/discover/public/application/components/default_discover_table/helper.tsx b/src/plugins/discover/public/application/components/default_discover_table/helper.tsx new file mode 100644 index 000000000000..82ac73acd784 --- /dev/null +++ b/src/plugins/discover/public/application/components/default_discover_table/helper.tsx @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { shortenDottedString } from '../../helpers'; + +export interface LegacyDisplayedColumn { + name: string; + displayName: string; + isSortable: boolean; + isRemoveable: boolean; + colLeftIdx: number; + colRightIdx: number; +} + +export interface ColumnProps { + name: string; + displayName: string; + isSortable: boolean; + isRemoveable: boolean; + colLeftIdx: number; + colRightIdx: number; +} + +/** + * Returns properties necessary to display the time column + * If it's an IndexPattern with timefield, the time column is + * prepended, not moveable and removeable + * @param timeFieldName + */ +export function getTimeColumn(timeFieldName: string): ColumnProps { + return { + name: timeFieldName, + displayName: 'Time', + isSortable: true, + isRemoveable: false, + colLeftIdx: -1, + colRightIdx: -1, + }; +} +/** + * A given array of column names returns an array of properties + * necessary to display the columns. If the given indexPattern + * has a timefield, a time column is prepended + * @param columns + * @param indexPattern + * @param hideTimeField + * @param isShortDots + */ +export function getLegacyDisplayedColumns( + columns: string[], + indexPattern: IndexPattern, + hideTimeField: boolean, + isShortDots: boolean +): LegacyDisplayedColumn[] { + if (!Array.isArray(columns) || typeof indexPattern !== 'object' || !indexPattern.getFieldByName) { + return []; + } + const columnProps = columns.map((column, idx) => { + const field = indexPattern.getFieldByName(column); + return { + name: column, + displayName: isShortDots ? shortenDottedString(column) : column, + isSortable: field && field.sortable ? true : false, + isRemoveable: column !== '_source' || columns.length > 1, + colLeftIdx: idx - 1 < 0 ? -1 : idx - 1, + colRightIdx: idx + 1 >= columns.length ? -1 : idx + 1, + }; + }); + return !hideTimeField && indexPattern.timeFieldName + ? [getTimeColumn(indexPattern.timeFieldName), ...columnProps] + : columnProps; +} diff --git a/src/plugins/discover/public/application/components/default_discover_table/pagination.tsx b/src/plugins/discover/public/application/components/default_discover_table/pagination.tsx new file mode 100644 index 000000000000..feec7631f735 --- /dev/null +++ b/src/plugins/discover/public/application/components/default_discover_table/pagination.tsx @@ -0,0 +1,63 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiFlexGroup, EuiFlexItem, EuiPagination, EuiTextColor } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import './_pagination.scss'; + +interface Props { + pageCount: number; + activePage: number; + goToPage: (page: number) => void; + startItem: number; + endItem: number; + totalItems?: number; + sampleSize: number; +} + +export const Pagination = ({ + pageCount, + activePage, + goToPage, + startItem, + endItem, + totalItems = 0, + sampleSize, +}: Props) => { + return ( + + {endItem >= sampleSize && ( + + + + + + )} + + + + + goToPage(currentPage)} + /> + + + ); +}; diff --git a/src/plugins/discover/public/application/components/default_discover_table/table_cell.tsx b/src/plugins/discover/public/application/components/default_discover_table/table_cell.tsx new file mode 100644 index 000000000000..a542e70ff646 --- /dev/null +++ b/src/plugins/discover/public/application/components/default_discover_table/table_cell.tsx @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import './_table_cell.scss'; + +import React from 'react'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { DocViewFilterFn } from '../../doc_views/doc_views_types'; + +export interface TableCellProps { + columnId: string; + isTimeField?: boolean; + onFilter: DocViewFilterFn; + filterable?: boolean; + fieldMapping?: any; + sanitizedCellValue: string; +} + +export const TableCell = ({ + columnId, + isTimeField, + onFilter, + fieldMapping, + sanitizedCellValue, +}: TableCellProps) => { + const content = ( + <> + {/* eslint-disable-next-line react/no-danger */} + + + + onFilter(columnId, fieldMapping, '+')} + iconType="plusInCircle" + aria-label={i18n.translate('discover.filterForValueLabel', { + defaultMessage: 'Filter for value', + })} + data-test-subj="filterForValue" + className="osdDocTableCell__filterButton" + /> + + + onFilter(columnId, fieldMapping, '-')} + iconType="minusInCircle" + aria-label={i18n.translate('discover.filterOutValueLabel', { + defaultMessage: 'Filter out value', + })} + data-test-subj="filterOutValue" + className="osdDocTableCell__filterButton" + /> + + + + ); + + return isTimeField ? ( + + {content} + + ) : ( + +
{content}
+ + ); +}; diff --git a/src/plugins/discover/public/application/components/default_discover_table/table_header.tsx b/src/plugins/discover/public/application/components/default_discover_table/table_header.tsx new file mode 100644 index 000000000000..52ef078387c8 --- /dev/null +++ b/src/plugins/discover/public/application/components/default_discover_table/table_header.tsx @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import './_table_header.scss'; + +import React from 'react'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { TableHeaderColumn } from './table_header_column'; +import { LegacyDisplayedColumn } from './helper'; +import { getDefaultSort } from '../../view_components/utils/get_default_sort'; +import { SortDirection, SortOrder } from '../../../saved_searches/types'; + +interface Props { + displayedColumns: LegacyDisplayedColumn[]; + defaultSortOrder: SortDirection; + indexPattern: IndexPattern; + onChangeSortOrder?: (sortOrder: SortOrder[]) => void; + onRemoveColumn?: (name: string) => void; + onMoveColumn?: (colName: string, destination: number) => void; + sortOrder: SortOrder[]; +} + +export function TableHeader({ + displayedColumns, + defaultSortOrder, + indexPattern, + onChangeSortOrder, + onMoveColumn, + onRemoveColumn, + sortOrder, +}: Props) { + return ( + + + {displayedColumns.map((col) => { + return ( + + ); + })} + + ); +} diff --git a/src/plugins/discover/public/application/components/default_discover_table/table_header_column.tsx b/src/plugins/discover/public/application/components/default_discover_table/table_header_column.tsx new file mode 100644 index 000000000000..9316eb81817e --- /dev/null +++ b/src/plugins/discover/public/application/components/default_discover_table/table_header_column.tsx @@ -0,0 +1,195 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import './_table_header.scss'; + +import React, { ReactNode } from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { SortOrder } from '../../../saved_searches/types'; + +interface Props { + colLeftIdx: number; // idx of the column to the left, -1 if moving is not possible + colRightIdx: number; // idx of the column to the right, -1 if moving is not possible + displayName: ReactNode; + isRemoveable: boolean; + isSortable?: boolean; + name: string; + onChangeSortOrder?: (sortOrder: SortOrder[]) => void; + onMoveColumn?: (colName: string, destination: number) => void; + onRemoveColumn?: (name: string) => void; + sortOrder: SortOrder[]; +} + +const sortDirectionToIcon: Record = { + desc: 'sortDown', + asc: 'sortUp', + '': 'sortable', +}; + +export function TableHeaderColumn({ + colLeftIdx, + colRightIdx, + displayName, + isRemoveable, + isSortable, + name, + onChangeSortOrder, + onMoveColumn, + onRemoveColumn, + sortOrder, +}: Props) { + const currentSortWithoutColumn = sortOrder.filter((pair) => pair[0] !== name); + const currentColumnSort = sortOrder.find((pair) => pair[0] === name); + const currentColumnSortDirection = (currentColumnSort && currentColumnSort[1]) || ''; + + const btnSortIcon = sortDirectionToIcon[currentColumnSortDirection]; + const btnSortClassName = + currentColumnSortDirection !== '' + ? btnSortIcon + : `osdDocTableHeader__sortChange ${btnSortIcon}`; + + const handleChangeSortOrder = () => { + if (!onChangeSortOrder) return; + + // Cycle goes Unsorted -> Asc -> Desc -> Unsorted + if (currentColumnSort === undefined) { + onChangeSortOrder([...currentSortWithoutColumn, [name, 'asc']]); + } else if (currentColumnSortDirection === 'asc') { + onChangeSortOrder([...currentSortWithoutColumn, [name, 'desc']]); + } else if (currentColumnSortDirection === 'desc' && currentSortWithoutColumn.length === 0) { + // If we're at the end of the cycle and this is the only existing sort, we switch + // back to ascending sort instead of removing it. + onChangeSortOrder([...currentSortWithoutColumn, [name, 'asc']]); + } else { + onChangeSortOrder(currentSortWithoutColumn); + } + }; + + const getSortButtonAriaLabel = () => { + const sortAscendingMessage = i18n.translate( + 'discover.docTable.tableHeader.sortByColumnAscendingAriaLabel', + { + defaultMessage: 'Sort {columnName} ascending', + values: { columnName: name }, + } + ); + const sortDescendingMessage = i18n.translate( + 'discover.docTable.tableHeader.sortByColumnDescendingAriaLabel', + { + defaultMessage: 'Sort {columnName} descending', + values: { columnName: name }, + } + ); + const stopSortingMessage = i18n.translate( + 'discover.docTable.tableHeader.sortByColumnUnsortedAriaLabel', + { + defaultMessage: 'Stop sorting on {columnName}', + values: { columnName: name }, + } + ); + + if (currentColumnSort === undefined) { + return sortAscendingMessage; + } else if (currentColumnSortDirection === 'asc') { + return sortDescendingMessage; + } else if (currentColumnSortDirection === 'desc' && currentSortWithoutColumn.length === 0) { + return sortAscendingMessage; + } else { + return stopSortingMessage; + } + }; + + // action buttons displayed on the right side of the column name + const buttons = [ + // Sort Button + { + active: isSortable && typeof onChangeSortOrder === 'function', + ariaLabel: getSortButtonAriaLabel(), + className: btnSortClassName, + onClick: handleChangeSortOrder, + testSubject: `docTableHeaderFieldSort_${name}`, + tooltip: getSortButtonAriaLabel(), + iconType: btnSortIcon, + }, + // Remove Button + { + active: isRemoveable && typeof onRemoveColumn === 'function', + ariaLabel: i18n.translate('discover.docTable.tableHeader.removeColumnButtonAriaLabel', { + defaultMessage: 'Remove {columnName} column', + values: { columnName: name }, + }), + className: 'fa fa-remove osdDocTableHeader__move', + onClick: () => onRemoveColumn && onRemoveColumn(name), + testSubject: `docTableRemoveHeader-${name}`, + tooltip: i18n.translate('discover.docTable.tableHeader.removeColumnButtonTooltip', { + defaultMessage: 'Remove Column', + }), + iconType: 'cross', + }, + // Move Left Button + { + active: colLeftIdx >= 0 && typeof onMoveColumn === 'function', + ariaLabel: i18n.translate('discover.docTable.tableHeader.moveColumnLeftButtonAriaLabel', { + defaultMessage: 'Move {columnName} column to the left', + values: { columnName: name }, + }), + className: 'fa fa-angle-double-left osdDocTableHeader__move', + onClick: () => onMoveColumn && onMoveColumn(name, colLeftIdx), + testSubject: `docTableMoveLeftHeader-${name}`, + tooltip: i18n.translate('discover.docTable.tableHeader.moveColumnLeftButtonTooltip', { + defaultMessage: 'Move column to the left', + }), + iconType: 'sortLeft', + }, + // Move Right Button + { + active: colRightIdx >= 0 && typeof onMoveColumn === 'function', + ariaLabel: i18n.translate('discover.docTable.tableHeader.moveColumnRightButtonAriaLabel', { + defaultMessage: 'Move {columnName} column to the right', + values: { columnName: name }, + }), + className: 'fa fa-angle-double-right osdDocTableHeader__move', + onClick: () => onMoveColumn && onMoveColumn(name, colRightIdx), + testSubject: `docTableMoveRightHeader-${name}`, + tooltip: i18n.translate('discover.docTable.tableHeader.moveColumnRightButtonTooltip', { + defaultMessage: 'Move column to the right', + }), + iconType: 'sortRight', + }, + ]; + + return ( + + + {displayName} + {buttons + .filter((button) => button.active) + .map((button, idx) => ( + + + + ))} + + + ); +} diff --git a/src/plugins/discover/public/application/components/default_discover_table/table_rows.tsx b/src/plugins/discover/public/application/components/default_discover_table/table_rows.tsx new file mode 100644 index 000000000000..cb2767afd801 --- /dev/null +++ b/src/plugins/discover/public/application/components/default_discover_table/table_rows.tsx @@ -0,0 +1,183 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React, { useState } from 'react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import dompurify from 'dompurify'; +import { TableCell } from './table_cell'; +import { DocViewerLinks } from '../doc_viewer_links/doc_viewer_links'; +import { DocViewer } from '../doc_viewer/doc_viewer'; +import { DocViewFilterFn, OpenSearchSearchHit } from '../../doc_views/doc_views_types'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { fetchSourceTypeDataCell } from '../data_grid/data_grid_table_cell_value'; + +export interface TableRowProps { + row: OpenSearchSearchHit; + columns: string[]; + indexPattern: IndexPattern; + onRemoveColumn: (column: string) => void; + onAddColumn: (column: string) => void; + onFilter: DocViewFilterFn; + onClose: () => void; + isShortDots: boolean; +} + +export const TableRow = ({ + row, + columns, + indexPattern, + onRemoveColumn, + onAddColumn, + onFilter, + onClose, + isShortDots, +}: TableRowProps) => { + const flattened = indexPattern.flattenHit(row); + const [isExpanded, setIsExpanded] = useState(false); + const tableRow = ( + + + setIsExpanded(!isExpanded)} + iconType={isExpanded ? 'arrowDown' : 'arrowRight'} + aria-label="Next" + data-test-subj="docTableExpandToggleColumn" + /> + + {columns.map((colName) => { + const fieldInfo = indexPattern.fields.getByName(colName); + const fieldMapping = flattened[colName]; + + if (typeof row === 'undefined') { + return ( + + - + + ); + } + + if (fieldInfo?.type === '_source') { + return ( + +
+ {fetchSourceTypeDataCell(indexPattern, row, colName, false, isShortDots)} +
+ + ); + } + + const formattedValue = indexPattern.formatField(row, colName); + + if (typeof formattedValue === 'undefined') { + return ( + + - + + ); + } + + const sanitizedCellValue = dompurify.sanitize(formattedValue); + + if (!fieldInfo?.filterable) { + return ( + +
+ {/* eslint-disable-next-line react/no-danger */} + +
+ + ); + } + + return ( + + ); + })} + + ); + + const expandedTableRow = ( + + + + + + + +

+ Expanded document +

+
+ + + +
+ + + { + onRemoveColumn(columnName); + onClose(); + }} + onAddColumn={(columnName: string) => { + onAddColumn(columnName); + onClose(); + }} + filter={(mapping, value, mode) => { + onFilter(mapping, value, mode); + onClose(); + }} + /> + + + + + ); + + return ( + <> + {tableRow} + {isExpanded && expandedTableRow} + + ); +}; diff --git a/src/plugins/discover/public/application/components/doc_views/context_app.tsx b/src/plugins/discover/public/application/components/doc_views/context_app.tsx index b6fe75473b58..c3a6da2d8cef 100644 --- a/src/plugins/discover/public/application/components/doc_views/context_app.tsx +++ b/src/plugins/discover/public/application/components/doc_views/context_app.tsx @@ -3,8 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo, Fragment } from 'react'; -import { useCallback } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { SurrDocType } from './context/api/context'; import { ActionBar } from './context/components/action_bar/action_bar'; import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; @@ -16,6 +15,7 @@ import { DataGridTable } from '../data_grid/data_grid_table'; import { DocViewFilterFn } from '../../doc_views/doc_views_types'; import { IndexPattern } from '../../../opensearch_dashboards_services'; import { AppState } from './context/utils/context_state'; +import { SortOrder } from '../../../saved_searches/types'; export interface Props { onAddFilter: DocViewFilterFn; @@ -73,7 +73,7 @@ export function ContextApp({ [setAppState] ); - const sort = useMemo(() => { + const sort: SortOrder[] = useMemo(() => { return [[indexPattern.timeFieldName!, SortDirection.desc]]; }, [indexPattern]); @@ -83,7 +83,7 @@ export function ContextApp({ ); return ( - + <> {}} onFilter={onAddFilter} + onMoveColumn={() => {}} onRemoveColumn={() => {}} onSetColumns={() => {}} onSort={() => {}} sort={sort} rows={rows} displayTimeColumn={displayTimeColumn} - services={services} isToolbarVisible={false} isContextView={true} /> @@ -120,6 +120,6 @@ export function ContextApp({ onChangeCount={onChangeCount} type={SurrDocType.SUCCESSORS} /> - + ); } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.scss b/src/plugins/discover/public/application/components/sidebar/discover_field.scss index 39cacdcd0c97..e869707ebcd3 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.scss +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.scss @@ -14,7 +14,8 @@ } &:hover &__actionButton, - &:focus &__actionButton { + &:focus &__actionButton, + .dscSidebarField__actionButton:focus { opacity: 1; } } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss index 7b1d1e2a82e7..b0bffeb2b216 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss @@ -1,8 +1,14 @@ -.dscSideBarFieldListHeader { - padding-left: $euiSizeS; +.dscSideBar_fieldGroup { + width: 100%; + + .euiButtonEmpty__content { + justify-content: flex-end; + } } .dscSidebar__item:hover, .dscSidebar__item:focus { - background-color: tintOrShade($euiColorPrimary, 90%, 90%); + background-color: $euiColorLightestShade; + padding-left: $euiSizeXS; + margin-left: -$euiSizeXS; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index f5e091e6ab82..9dcb5bf337cb 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -32,15 +32,15 @@ import './discover_sidebar.scss'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { i18n } from '@osd/i18n'; import { - EuiTitle, EuiDragDropContext, DropResult, EuiDroppable, EuiDraggable, EuiPanel, EuiSplitPanel, + EuiButtonEmpty, } from '@elastic/eui'; -import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; +import { I18nProvider } from '@osd/i18n/react'; import { DiscoverField } from './discover_field'; import { DiscoverFieldSearch } from './discover_field_search'; import { FIELDS_LIMIT_SETTING } from '../../../../common'; @@ -50,6 +50,7 @@ import { getDetails } from './lib/get_details'; import { getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; import { getServices } from '../../../opensearch_dashboards_services'; +import { FieldDetails } from './types'; export interface DiscoverSidebarProps { /** @@ -87,16 +88,8 @@ export interface DiscoverSidebarProps { selectedIndexPattern?: IndexPattern; } -export function DiscoverSidebar({ - columns, - fieldCounts, - hits, - onAddField, - onAddFilter, - onRemoveField, - onReorderFields, - selectedIndexPattern, -}: DiscoverSidebarProps) { +export function DiscoverSidebar(props: DiscoverSidebarProps) { + const { columns, fieldCounts, hits, onAddField, onReorderFields, selectedIndexPattern } = props; const [fields, setFields] = useState(null); const [fieldFilterState, setFieldFilterState] = useState(getDefaultFieldFilter()); const services = useMemo(() => getServices(), []); @@ -120,7 +113,7 @@ export function DiscoverSidebar({ ); const popularLimit = services.uiSettings.get(FIELDS_LIMIT_SETTING); - const useShortDots = services.uiSettings.get(UI_SETTINGS.SHORT_DOTS_ENABLE); + const shortDotsEnabled = services.uiSettings.get(UI_SETTINGS.SHORT_DOTS_ENABLE); const { selected: selectedFields, @@ -193,6 +186,7 @@ export function DiscoverSidebar({ )} borderRadius="none" color="transparent" + hasBorder={false} > {fields.length > 0 && ( <> - -

- -

-
- - {selectedFields.map((field: IndexPatternField, index) => { - return ( - - - {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} - - - - ); + - -

- -

-
- + {...props} + /> {popularFields.length > 0 && ( - - - - - - {popularFields.map((field: IndexPatternField, index) => { - return ( - - - {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} - - - - ); - })} - - + )} - - {unpopularFields.map((field: IndexPatternField, index) => { - return ( - - - {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} - - - - ); + + {...props} + /> )}
@@ -354,3 +238,85 @@ export function DiscoverSidebar({ ); } + +interface FieldGroupProps extends DiscoverSidebarProps { + category: 'selected' | 'popular' | 'unpopular'; + title: string; + fields: IndexPatternField[]; + getDetailsByField: (field: IndexPatternField) => FieldDetails; + shortDotsEnabled: boolean; +} + +const FieldList = ({ + category, + title, + fields, + columns, + selectedIndexPattern, + onAddField, + onRemoveField, + onAddFilter, + getDetailsByField, + shortDotsEnabled, +}: FieldGroupProps) => { + const [expanded, setExpanded] = useState(true); + + if (!selectedIndexPattern) return null; + + return ( + <> + setExpanded(!expanded)} + size="xs" + className="dscSideBar_fieldGroup" + > + {title} + + {expanded && ( + + {fields.map((field: IndexPatternField, index) => { + return ( + + + {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} + + + + ); + })} + + )} + + ); +}; diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx index 29c19887412c..1b20c444e860 100644 --- a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx @@ -5,8 +5,8 @@ import { i18n } from '@osd/i18n'; import React from 'react'; +import { EuiText } from '@elastic/eui'; import { DiscoverViewServices } from '../../../build_services'; -import { showOpenSearchPanel } from './show_open_search_panel'; import { SavedSearch } from '../../../saved_searches'; import { Adapters } from '../../../../../inspector/public'; import { TopNavMenuData } from '../../../../../navigation/public'; @@ -16,11 +16,17 @@ import { SavedObjectSaveModal, showSaveModal, } from '../../../../../saved_objects/public'; +import { + OpenSearchDashboardsContextProvider, + toMountPoint, +} from '../../../../../opensearch_dashboards_react/public'; import { DiscoverState, setSavedSearchId } from '../../utils/state_management'; import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../common'; import { getSortForSearchSource } from '../../view_components/utils/get_sort_for_search_source'; import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; import { syncQueryStateWithUrl } from '../../../../../data/public'; +import { getNewDiscoverSetting, setNewDiscoverSetting } from '../utils/local_storage'; +import { OpenSearchPanel } from './open_search_panel'; export const getTopNavLinks = ( services: DiscoverViewServices, @@ -38,6 +44,7 @@ export const getTopNavLinks = ( store, data: { query }, osdUrlStateStorage, + storage, } = services; const newSearch = { @@ -162,11 +169,20 @@ export const getTopNavLinks = ( }), testId: 'discoverOpenButton', run: () => { - showOpenSearchPanel({ - makeUrl: (searchId) => `#/view/${encodeURIComponent(searchId)}`, - I18nContext: core.i18n.Context, - services, - }); + const flyoutSession = services.overlays.openFlyout( + toMountPoint( + + { + if (flyoutSession) { + flyoutSession.close(); + } + }} + makeUrl={(searchId) => `#/view/${encodeURIComponent(searchId)}`} + /> + + ) + ); }, }; @@ -197,7 +213,7 @@ export const getTopNavLinks = ( ...sharingData, title: savedSearch.title, }, - isDirty: !savedSearch.id || state.isDirty, + isDirty: !savedSearch.id || state.isDirty || false, }); }, }; @@ -218,7 +234,61 @@ export const getTopNavLinks = ( }, }; + const newDiscoverButtonLabel = i18n.translate('discover.localMenu.discoverButton.label.new', { + defaultMessage: 'Try new Discover', + }); + const oldDiscoverButtonLabel = i18n.translate('discover.localMenu.discoverButton.label.old', { + defaultMessage: 'Use legacy Discover', + }); + const isNewDiscover = getNewDiscoverSetting(storage); + const newTable: TopNavMenuData = { + id: 'table-datagrid', + label: isNewDiscover ? oldDiscoverButtonLabel : newDiscoverButtonLabel, + description: i18n.translate('discover.localMenu.newTableDescription', { + defaultMessage: 'New Discover toggle Experience', + }), + testId: 'datagridTableButton', + run: async () => { + // Read the current state from localStorage + const newDiscoverEnabled = getNewDiscoverSetting(storage); + if (newDiscoverEnabled) { + const confirmed = await services.overlays.openConfirm( + toMountPoint( + +

+ Help drive future improvements by{' '} + + providing feedback + {' '} + about your experience. +

+
+ ), + { + title: i18n.translate('discover.localMenu.newTableConfirmModalTitle', { + defaultMessage: 'Share your thoughts on the latest Discover features', + }), + cancelButtonText: 'Cancel', + confirmButtonText: 'Turn off new features', + defaultFocusedButton: 'confirm', + } + ); + + if (confirmed) { + setNewDiscoverSetting(false, storage); + window.location.reload(); + } + } else { + // Save the new setting to localStorage + setNewDiscoverSetting(true, storage); + window.location.reload(); + } + }, + iconType: isNewDiscover ? 'editorUndo' : 'cheer', + }; + return [ + newTable, newSearch, ...(capabilities.discover?.save ? [saveSearch] : []), openSearch, @@ -278,7 +348,7 @@ const getSharingData = async ({ const searchSourceInstance = searchSource.createCopy(); const indexPattern = await searchSourceInstance.getField('index'); - const { searchFields, selectFields } = await getSharingDataFields( + const { searchFields } = await getSharingDataFields( state.columns, services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING), indexPattern?.timeFieldName diff --git a/src/plugins/discover/public/application/components/top_nav/show_open_search_panel.tsx b/src/plugins/discover/public/application/components/top_nav/show_open_search_panel.tsx deleted file mode 100644 index 5bc95b1b9cf2..000000000000 --- a/src/plugins/discover/public/application/components/top_nav/show_open_search_panel.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import { OpenSearchPanel } from './open_search_panel'; -import { I18nStart } from '../../../../../../core/public'; -import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; -import { DiscoverViewServices } from '../../../build_services'; - -let isOpen = false; - -export function showOpenSearchPanel({ - makeUrl, - I18nContext, - services, -}: { - makeUrl: (id: string) => string; - I18nContext: I18nStart['Context']; - services: DiscoverViewServices; -}) { - if (isOpen) { - return; - } - - isOpen = true; - const container = document.createElement('div'); - const onClose = () => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - isOpen = false; - }; - - document.body.appendChild(container); - const element = ( - - - - - - ); - ReactDOM.render(element, container); -} diff --git a/src/plugins/discover/public/application/components/utils/local_storage.ts b/src/plugins/discover/public/application/components/utils/local_storage.ts new file mode 100644 index 000000000000..5e812de8e97d --- /dev/null +++ b/src/plugins/discover/public/application/components/utils/local_storage.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Storage } from '../../../../../opensearch_dashboards_utils/public'; + +export const NEW_DISCOVER_KEY = 'discover:newExpereince'; + +export const getNewDiscoverSetting = (storage: Storage): boolean => { + const storedValue = storage.get(NEW_DISCOVER_KEY); + return storedValue !== null ? JSON.parse(storedValue) : false; +}; + +export const setNewDiscoverSetting = (value: boolean, storage: Storage) => { + storage.set(NEW_DISCOVER_KEY, JSON.stringify(value)); +}; diff --git a/src/plugins/discover/public/application/helpers/breadcrumbs.ts b/src/plugins/discover/public/application/helpers/breadcrumbs.ts index 382deec77eee..429c4db5e617 100644 --- a/src/plugins/discover/public/application/helpers/breadcrumbs.ts +++ b/src/plugins/discover/public/application/helpers/breadcrumbs.ts @@ -39,7 +39,7 @@ export function getRootBreadcrumbs(): EuiBreadcrumb[] { text: i18n.translate('discover.rootBreadcrumb', { defaultMessage: 'Discover', }), - onClick: () => core.application.navigateToApp('discover'), + onClick: () => core.application.navigateToApp('data-explorer', { path: 'discover' }), }, ]; } diff --git a/src/plugins/discover/public/application/utils/state_management/common.test.ts b/src/plugins/discover/public/application/utils/state_management/common.test.ts index 64a2dba99dd4..c9c41a914c74 100644 --- a/src/plugins/discover/public/application/utils/state_management/common.test.ts +++ b/src/plugins/discover/public/application/utils/state_management/common.test.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { addColumn, removeColumn, reorderColumn } from './common'; +import { addColumn, removeColumn, reorderColumn, moveColumn } from './common'; describe('commonUtils', () => { it('should handle addColumn', () => { @@ -22,4 +22,41 @@ describe('commonUtils', () => { 'column1', ]); }); + + it('should handle moveColumn', () => { + // test moving a column within the array + expect(moveColumn(['column1', 'column2', 'column3'], 'column2', 0)).toEqual([ + 'column2', + 'column1', + 'column3', + ]); + + // test moving a column to the same index (should result in no change) + expect(moveColumn(['column1', 'column2', 'column3'], 'column2', 1)).toEqual([ + 'column1', + 'column2', + 'column3', + ]); + + // test moving a column to the end + expect(moveColumn(['column1', 'column2', 'column3'], 'column1', 2)).toEqual([ + 'column2', + 'column3', + 'column1', + ]); + + // test trying to move a column to an index out of bounds (should return original array) + expect(moveColumn(['column1', 'column2', 'column3'], 'column1', 3)).toEqual([ + 'column1', + 'column2', + 'column3', + ]); + + // test trying to move a column that doesn't exist (should return original array) + expect(moveColumn(['column1', 'column2', 'column3'], 'column4', 1)).toEqual([ + 'column1', + 'column2', + 'column3', + ]); + }); }); diff --git a/src/plugins/discover/public/application/utils/state_management/common.ts b/src/plugins/discover/public/application/utils/state_management/common.ts index 800753fb4a36..1acba05ecb75 100644 --- a/src/plugins/discover/public/application/utils/state_management/common.ts +++ b/src/plugins/discover/public/application/utils/state_management/common.ts @@ -21,3 +21,13 @@ export const reorderColumn = (columns: string[], source: number, destination: nu newColumns.splice(destination, 0, removed); return newColumns; }; + +export function moveColumn(columns: string[], columnName: string, newIndex: number) { + if (newIndex < 0 || newIndex >= columns.length || !columns.includes(columnName)) { + return columns; + } + const modifiedColumns = [...columns]; + modifiedColumns.splice(modifiedColumns.indexOf(columnName), 1); // remove at old index + modifiedColumns.splice(newIndex, 0, columnName); // insert before new index + return modifiedColumns; +} diff --git a/src/plugins/discover/public/application/utils/state_management/discover_slice.test.tsx b/src/plugins/discover/public/application/utils/state_management/discover_slice.test.tsx index cbfc9c3769b0..ba94236e1492 100644 --- a/src/plugins/discover/public/application/utils/state_management/discover_slice.test.tsx +++ b/src/plugins/discover/public/application/utils/state_management/discover_slice.test.tsx @@ -82,4 +82,56 @@ describe('discoverSlice', () => { const result = discoverSlice.reducer(initialState, action); expect(result.sort).toEqual([['field2', 'desc']]); }); + + it('should handle moveColumn', () => { + initialState = { + columns: ['column1', 'column2', 'column3'], + sort: [], + }; + const action = { + type: 'discover/moveColumn', + payload: { columnName: 'column2', destination: 0 }, + }; + const result = discoverSlice.reducer(initialState, action); + expect(result.columns).toEqual(['column2', 'column1', 'column3']); + }); + + it('should maintain columns order when moving a column to its current position', () => { + initialState = { + columns: ['column1', 'column2', 'column3'], + sort: [], + }; + const action = { + type: 'discover/moveColumn', + payload: { columnName: 'column2', destination: 1 }, + }; + const result = discoverSlice.reducer(initialState, action); + expect(result.columns).toEqual(['column1', 'column2', 'column3']); + }); + + it('should handle moveColumn when destination is out of range', () => { + initialState = { + columns: ['column1', 'column2', 'column3'], + sort: [], + }; + const action = { + type: 'discover/moveColumn', + payload: { columnName: 'column1', destination: 5 }, + }; + const result = discoverSlice.reducer(initialState, action); + expect(result.columns).toEqual(['column1', 'column2', 'column3']); + }); + + it('should not change columns if column to move does not exist', () => { + initialState = { + columns: ['column1', 'column2', 'column3'], + sort: [], + }; + const action = { + type: 'discover/moveColumn', + payload: { columnName: 'nonExistingColumn', destination: 0 }, + }; + const result = discoverSlice.reducer(initialState, action); + expect(result.columns).toEqual(['column1', 'column2', 'column3']); + }); }); diff --git a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx index f90d400ff3d5..90fb417c2b0e 100644 --- a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx +++ b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx @@ -42,7 +42,16 @@ export interface DiscoverState { * dirty flag to indicate if the saved search has been modified * since the last save */ - isDirty: boolean; + isDirty?: boolean; + /** + * Metadata for the view + */ + metadata?: { + /** + * Number of lines to display per row + */ + lineCount?: number; + }; } export interface DiscoverRootState extends RootState { @@ -128,6 +137,17 @@ export const discoverSlice = createSlice({ isDirty: true, }; }, + moveColumn(state, action: PayloadAction<{ columnName: string; destination: number }>) { + const columns = utils.moveColumn( + state.columns, + action.payload.columnName, + action.payload.destination + ); + return { + ...state, + columns, + }; + }, setColumns(state, action: PayloadAction<{ columns: string[] }>) { return { ...state, @@ -159,6 +179,15 @@ export const discoverSlice = createSlice({ isDirty: false, }; }, + setMetadata(state, action: PayloadAction>) { + return { + ...state, + metadata: { + ...state.metadata, + ...action.payload, + }, + }; + }, }, }); @@ -167,11 +196,13 @@ export const { addColumn, removeColumn, reorderColumn, + moveColumn, setColumns, setSort, setInterval, setState, updateState, setSavedSearchId, + setMetadata, } = discoverSlice.actions; export const { reducer } = discoverSlice; diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss b/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss index 92e1131a05e3..36408bd88366 100644 --- a/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss +++ b/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss @@ -6,6 +6,10 @@ /* stylelint-disable-next-line */ container-name: canvas; height: 100%; + + &_results { + margin-left: $euiSizeM; + } } // TopNav styles for the Discover diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx index 3f7bbf4ec7b8..17f9f26e8b54 100644 --- a/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx @@ -10,7 +10,9 @@ import { DataGridTable } from '../../components/data_grid/data_grid_table'; import { useDiscoverContext } from '../context'; import { addColumn, + moveColumn, removeColumn, + reorderColumn, setColumns, setSort, useDispatch, @@ -55,6 +57,14 @@ export const DiscoverTable = ({ rows }: Props) => { dispatch(removeColumn(col)); }; + + const onMoveColumn = (col: string, destination: number) => { + if (indexPattern && capabilities.discover?.save) { + popularizeField(indexPattern, col, indexPatterns); + } + dispatch(moveColumn({ columnName: col, destination })); + }; + const onSetColumns = (cols: string[]) => dispatch(setColumns({ columns: cols })); const onSetSort = (s: SortOrder[]) => { dispatch(setSort(s)); @@ -96,6 +106,7 @@ export const DiscoverTable = ({ rows }: Props) => { indexPattern={indexPattern} onAddColumn={onAddColumn} onFilter={onAddFilter as DocViewFilterFn} + onMoveColumn={onMoveColumn} onRemoveColumn={onRemoveColumn} onSetColumns={onSetColumns} onSort={onSetSort} diff --git a/src/plugins/discover/public/application/view_components/canvas/index.tsx b/src/plugins/discover/public/application/view_components/canvas/index.tsx index d5c54158e997..e3efe878aa83 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -112,14 +112,10 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history }: ViewPro )} {fetchState.status === ResultStatus.LOADING && } {fetchState.status === ResultStatus.READY && ( - <> - - - - - + + - + )}
); diff --git a/src/plugins/discover/public/application/view_components/utils/get_default_sort.ts b/src/plugins/discover/public/application/view_components/utils/get_default_sort.ts index 584e47047c57..2a958ec00fcb 100644 --- a/src/plugins/discover/public/application/view_components/utils/get_default_sort.ts +++ b/src/plugins/discover/public/application/view_components/utils/get_default_sort.ts @@ -29,18 +29,17 @@ */ import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { SortOrder } from '../../../saved_searches/types'; // @ts-ignore import { isSortable } from './get_sort'; -export type SortOrder = [string, string]; - /** * use in case the user didn't manually sort. * the default sort is returned depending of the index pattern */ export function getDefaultSort( indexPattern: IndexPattern, - defaultSortOrder: string = 'desc' + defaultSortOrder: 'asc' | 'desc' = 'desc' ): SortOrder[] { if (indexPattern.timeFieldName && isSortable(indexPattern.timeFieldName, indexPattern)) { return [[indexPattern.timeFieldName, defaultSortOrder]]; diff --git a/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts b/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts index b19128a432e0..4eba4d833944 100644 --- a/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts +++ b/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts @@ -31,8 +31,7 @@ import { OpenSearchQuerySortValue, IndexPattern } from '../../../opensearch_dashboards_services'; import { getSort } from './get_sort'; import { getDefaultSort } from './get_default_sort'; - -export type SortOrder = [string, string]; +import { SortDirection, SortOrder } from '../../../saved_searches/types'; /** * Prepares sort for search source, that's sending the request to OpenSearch @@ -44,7 +43,7 @@ export type SortOrder = [string, string]; export function getSortForSearchSource( sort?: SortOrder[], indexPattern?: IndexPattern, - defaultDirection: string = 'desc' + defaultDirection: SortDirection = 'desc' ): OpenSearchQuerySortValue[] { if (!sort || !indexPattern) { return []; diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 785e72536417..36a8908594f0 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -58,6 +58,7 @@ import { OpenSearchDashboardsLegacyStart } from '../../opensearch_dashboards_leg import { UrlForwardingStart } from '../../url_forwarding/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; import { DataExplorerServices } from '../../data_explorer/public'; +import { Storage } from '../../opensearch_dashboards_utils/public'; export interface DiscoverServices { addBasePath: (path: string) => string; @@ -82,6 +83,7 @@ export interface DiscoverServices { getSavedSearchUrlById: (id: string) => Promise; uiSettings: IUiSettingsClient; visualizations: VisualizationsStart; + storage: Storage; } export function buildServices( @@ -97,6 +99,7 @@ export function buildServices( overlays: core.overlays, }; const savedObjectService = createSavedSearchesLoader(services); + const storage = new Storage(localStorage); return { addBasePath: core.http.basePath.prepend, @@ -123,6 +126,7 @@ export function buildServices( toastNotifications: core.notifications.toasts, uiSettings: core.uiSettings, visualizations: plugins.visualizations, + storage, }; } diff --git a/src/plugins/discover/public/embeddable/search_embeddable.tsx b/src/plugins/discover/public/embeddable/search_embeddable.tsx index a37a001ad798..79080cf8657f 100644 --- a/src/plugins/discover/public/embeddable/search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/search_embeddable.tsx @@ -80,10 +80,11 @@ export interface SearchProps { onRemoveColumn?: (column: string) => void; onAddColumn?: (column: string) => void; onMoveColumn?: (column: string, index: number) => void; + onReorderColumn?: (col: string, source: number, destination: number) => void; onFilter?: (field: IFieldType, value: string[], operator: string) => void; rows?: any[]; indexPattern?: IndexPattern; - totalHitCount?: number; + hits?: number; isLoading?: boolean; displayTimeColumn?: boolean; services: DiscoverServices; @@ -359,7 +360,7 @@ export class SearchEmbeddable inspectorRequest.stats(getResponseInspectorStats(resp, searchSource)).ok({ json: resp }); this.searchProps!.rows = resp.hits.hits; - this.searchProps!.totalHitCount = resp.hits.total; + this.searchProps!.hits = resp.hits.total; this.searchProps!.isLoading = false; } catch (error) { this.updateOutput({ loading: false, error }); diff --git a/src/plugins/discover/public/embeddable/search_embeddable_component.tsx b/src/plugins/discover/public/embeddable/search_embeddable_component.tsx index 97df2e5c45b7..ecf0f1bc2b30 100644 --- a/src/plugins/discover/public/embeddable/search_embeddable_component.tsx +++ b/src/plugins/discover/public/embeddable/search_embeddable_component.tsx @@ -12,55 +12,62 @@ import { DataGridTableProps, } from '../application/components/data_grid/data_grid_table'; import { VisualizationNoResults } from '../../../visualizations/public'; +import { getServices } from '../opensearch_dashboards_services'; import './search_embeddable.scss'; +import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; interface SearchEmbeddableProps { searchProps: SearchProps; } -export interface DiscoverEmbeddableProps extends DataGridTableProps { - totalHitCount: number; -} +export type DiscoverEmbeddableProps = DataGridTableProps; export const DataGridTableMemoized = React.memo((props: DataGridTableProps) => ( )); export function SearchEmbeddableComponent({ searchProps }: SearchEmbeddableProps) { + const services = getServices(); const discoverEmbeddableProps = { columns: searchProps.columns, indexPattern: searchProps.indexPattern, onAddColumn: searchProps.onAddColumn, onFilter: searchProps.onFilter, + onMoveColumn: searchProps.onMoveColumn, onRemoveColumn: searchProps.onRemoveColumn, + onReorderColumn: searchProps.onReorderColumn, onSort: searchProps.onSort, rows: searchProps.rows, onSetColumns: searchProps.onSetColumns, sort: searchProps.sort, displayTimeColumn: searchProps.displayTimeColumn, services: searchProps.services, - totalHitCount: searchProps.totalHitCount, + hits: searchProps.hits, title: searchProps.title, description: searchProps.description, + showPagination: true, } as DiscoverEmbeddableProps; return ( - - {discoverEmbeddableProps.totalHitCount !== 0 ? ( - - - - ) : ( - - - - )} - + + + {discoverEmbeddableProps.hits !== 0 ? ( + + + + ) : ( + + + + )} + + ); } diff --git a/src/plugins/discover/public/embeddable/types.ts b/src/plugins/discover/public/embeddable/types.ts index 24a1aac92b49..855f2d96e47b 100644 --- a/src/plugins/discover/public/embeddable/types.ts +++ b/src/plugins/discover/public/embeddable/types.ts @@ -34,7 +34,7 @@ import { EmbeddableOutput, IEmbeddable, } from 'src/plugins/embeddable/public'; -import { Filter, IIndexPattern, TimeRange, Query } from '../../../../data/public'; +import { Filter, IIndexPattern, TimeRange, Query } from 'src/plugins/data/public'; import { SortOrder } from '../saved_searches/types'; import { SavedSearch } from '../saved_searches'; diff --git a/src/plugins/discover/public/migrate_state.ts b/src/plugins/discover/public/migrate_state.ts index 2a3cd77d26bc..b0ec5af810aa 100644 --- a/src/plugins/discover/public/migrate_state.ts +++ b/src/plugins/discover/public/migrate_state.ts @@ -66,12 +66,12 @@ export function migrateUrlState(oldPath: string, newPath = '/'): string { let path = newPath; const pathPatterns = [ { - pattern: '#/context/:indexPattern/:id\\?:appState?', + pattern: '#/context/:indexPattern/:id', extraState: { docView: 'context' }, path: `context`, }, { - pattern: '#/doc/:indexPattern/:index\\?:appState?', + pattern: '#/doc/:indexPattern/:id', extraState: { docView: 'doc' }, path: `doc`, }, @@ -99,6 +99,7 @@ export function migrateUrlState(oldPath: string, newPath = '/'): string { case `doc`: case `context`: path = oldPath; + break; case `discover`: case `savedSearch`: const params = matchPath(oldPath, { diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts index f67df00a900c..73cb25774c4a 100644 --- a/src/plugins/discover/public/saved_searches/types.ts +++ b/src/plugins/discover/public/saved_searches/types.ts @@ -31,7 +31,8 @@ import { SavedObject } from '../../../saved_objects/public'; import { ISearchSource } from '../../../data/public'; -export type SortOrder = [string, 'asc' | 'desc']; +export type SortDirection = 'asc' | 'desc'; +export type SortOrder = [string, SortDirection]; export interface SavedSearch extends Pick< SavedObject, diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/components/styles.scss b/src/plugins/vis_augmenter/public/view_events_flyout/components/styles.scss index 75cb154d1957..1fb5c007fadc 100644 --- a/src/plugins/vis_augmenter/public/view_events_flyout/components/styles.scss +++ b/src/plugins/vis_augmenter/public/view_events_flyout/components/styles.scss @@ -1,7 +1,7 @@ $vis-description-width: 200px; $event-vis-height: 55px; $timeline-panel-height: 90px; -$content-padding-top: 110px; // Padding needed within view events flyout content to sit comfortably below flyout header +$content-padding-top: 110px; // Padding required for view events flyout content to align below flyout header. $date-range-height: 45px; // Static height we want for the date range picker component $error-icon-padding-right: -8px; // This is so the error icon is aligned consistent with the event count icons $base-vis-min-height: 25vh; // Visualizations require the container to have a valid width and height to render diff --git a/test/functional/apps/context/_context_navigation.js b/test/functional/apps/context/_context_navigation.js index 5e722a85d099..4e78d3a9c14b 100644 --- a/test/functional/apps/context/_context_navigation.js +++ b/test/functional/apps/context/_context_navigation.js @@ -46,6 +46,7 @@ export default function ({ getService, getPageObjects }) { before(async function () { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.switchDiscoverTable('new'); for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { await PageObjects.discover.clickFieldListItemDetails(columnName); await PageObjects.discover.clickFieldListPlusFilter(columnName, value); diff --git a/test/functional/apps/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js index ac45d86555d7..8fe1d2034c88 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.js @@ -43,6 +43,8 @@ export default function ({ getService, getPageObjects }) { describe('context view for date_nanos', () => { before(async function () { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.switchDiscoverTable('new'); await security.testUser.setRoles([ 'opensearch_dashboards_admin', 'opensearch_dashboards_date_nanos', diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index 5680b28921a7..afbc9390a3c5 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -47,6 +47,7 @@ export default function ({ getService, getPageObjects }) { before(async () => { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.switchDiscoverTable('new'); for (const columnName of TEST_COLUMN_NAMES) { await PageObjects.discover.clickFieldListItemAdd(columnName); diff --git a/test/functional/apps/context/_filters.js b/test/functional/apps/context/_filters.js index 077a8376aa7b..17bbe5bcbe08 100644 --- a/test/functional/apps/context/_filters.js +++ b/test/functional/apps/context/_filters.js @@ -41,11 +41,13 @@ export default function ({ getService, getPageObjects }) { const browser = getService('browser'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'context']); + const PageObjects = getPageObjects(['common', 'context', 'discover']); describe('context filters', function contextSize() { beforeEach(async function () { await browser.refresh(); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.switchDiscoverTable('new'); await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID, { columns: TEST_COLUMN_NAMES, }); diff --git a/test/functional/apps/context/_size.js b/test/functional/apps/context/_size.js index bd44b159bca4..70323b01460a 100644 --- a/test/functional/apps/context/_size.js +++ b/test/functional/apps/context/_size.js @@ -37,11 +37,13 @@ export default function ({ getService, getPageObjects }) { const opensearchDashboardsServer = getService('opensearchDashboardsServer'); const retry = getService('retry'); const dataGrid = getService('dataGrid'); - const PageObjects = getPageObjects(['context']); + const PageObjects = getPageObjects(['common', 'context', 'discover']); let expectedRowLength = 2 * TEST_DEFAULT_CONTEXT_SIZE + 1; describe('context size', function contextSize() { before(async function () { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.switchDiscoverTable('new'); await opensearchDashboardsServer.uiSettings.update({ 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`, 'context:step': `${TEST_STEP_SIZE}`, diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.js b/test/functional/apps/dashboard/dashboard_filter_bar.js index 9a0ce6a9042a..dde86c697e3c 100644 --- a/test/functional/apps/dashboard/dashboard_filter_bar.js +++ b/test/functional/apps/dashboard/dashboard_filter_bar.js @@ -39,7 +39,14 @@ export default function ({ getService, getPageObjects }) { const opensearchArchiver = getService('opensearchArchiver'); const opensearchDashboardsServer = getService('opensearchDashboardsServer'); const browser = getService('browser'); - const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); + const PageObjects = getPageObjects([ + 'common', + 'dashboard', + 'header', + 'visualize', + 'timePicker', + 'discover', + ]); describe('dashboard filter bar', () => { before(async () => { @@ -185,6 +192,9 @@ export default function ({ getService, getPageObjects }) { describe('saved search filtering', function () { before(async () => { await filterBar.ensureFieldEditorModalIsClosed(); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.switchDiscoverTable('new'); + await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.gotoDashboardLandingPage(); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.timePicker.setDefaultDataRange(); diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index e934169513f6..1040b87f6168 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -46,7 +46,14 @@ export default function ({ getService, getPageObjects }) { const opensearchDashboardsServer = getService('opensearchDashboardsServer'); const security = getService('security'); const dashboardPanelActions = getService('dashboardPanelActions'); - const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); + const PageObjects = getPageObjects([ + 'common', + 'dashboard', + 'header', + 'visualize', + 'timePicker', + 'discover', + ]); describe('dashboard filtering', function () { this.tags('includeFirefox'); @@ -72,6 +79,10 @@ export default function ({ getService, getPageObjects }) { describe('adding a filter that excludes all data', () => { before(async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.switchDiscoverTable('new'); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.timePicker.setDefaultDataRange(); await dashboardAddPanel.addEveryVisualization('"Filter Bytes Test"'); @@ -219,7 +230,7 @@ export default function ({ getService, getPageObjects }) { }); it('saved searches', async () => { - await dashboardExpect.savedSearchRowCount(1); + await testSubjects.existOrFail('docTableExpandToggleColumn'); }); it('vega', async () => { diff --git a/test/functional/apps/dashboard/dashboard_state.js b/test/functional/apps/dashboard/dashboard_state.js index edb2002624f5..11196a1b69b9 100644 --- a/test/functional/apps/dashboard/dashboard_state.js +++ b/test/functional/apps/dashboard/dashboard_state.js @@ -56,6 +56,9 @@ export default function ({ getService, getPageObjects }) { describe('dashboard state', function describeIndexTests() { before(async function () { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.switchDiscoverTable('new'); + await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.initTests(); await PageObjects.dashboard.preserveCrossAppState(); await browser.refresh(); diff --git a/test/functional/apps/dashboard/dashboard_time_picker.js b/test/functional/apps/dashboard/dashboard_time_picker.js index b1e57fbe8e5e..e5da381ec06e 100644 --- a/test/functional/apps/dashboard/dashboard_time_picker.js +++ b/test/functional/apps/dashboard/dashboard_time_picker.js @@ -68,14 +68,14 @@ export default function ({ getService, getPageObjects }) { fields: ['bytes', 'agent'], }); // Current data grid loads 100 rows per page by default with inspect button and time range - await dashboardExpect.dataGridTableCellCount(400); + await dashboardExpect.savedSearchRowCountFromLegacyTable(100); // Set to time range with no data await PageObjects.timePicker.setAbsoluteRange( 'Jan 1, 2000 @ 00:00:00.000', 'Jan 1, 2000 @ 01:00:00.000' ); - await dashboardExpect.dataGridTableCellCount(0); + await dashboardExpect.savedSearchRowCountFromLegacyTable(0); }); it('Timepicker start, end, interval values are set by url', async () => { diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index 4c1cf4f69ce1..a790a884ccbe 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -125,9 +125,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await pieChart.expectPieSliceCount(4); log.debug('Checking area, bar and heatmap charts rendered'); await dashboardExpect.seriesElementCount(15); - // The saved search of data explorer now renders 100 lines max log.debug('Checking saved searches rendered'); - await dashboardExpect.savedSearchRowCount(100); + await dashboardExpect.savedSearchRowCountFromLegacyTable(100); log.debug('Checking input controls rendered'); await dashboardExpect.inputControlItemCount(3); log.debug('Checking tag cloud rendered'); diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js index 87c31a26f8c3..f0712d82a176 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -161,6 +161,7 @@ export default function ({ getService, getPageObjects }) { const fromTime = 'Sep 17, 2015 @ 06:31:44.000'; const toTime = 'Sep 18, 2015 @ 18:31:44.000'; await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.switchDiscoverTable('new'); await PageObjects.discover.selectIndexPattern('logstash-*'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); @@ -279,6 +280,7 @@ export default function ({ getService, getPageObjects }) { const fromTime = 'Sep 17, 2015 @ 06:31:44.000'; const toTime = 'Sep 18, 2015 @ 18:31:44.000'; await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.switchDiscoverTable('new'); await PageObjects.discover.selectIndexPattern('logstash-*'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); @@ -375,6 +377,7 @@ export default function ({ getService, getPageObjects }) { const fromTime = 'Sep 17, 2015 @ 06:31:44.000'; const toTime = 'Sep 18, 2015 @ 18:31:44.000'; await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.switchDiscoverTable('new'); await PageObjects.discover.selectIndexPattern('logstash-*'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); @@ -414,6 +417,7 @@ export default function ({ getService, getPageObjects }) { }); it('should filter by scripted field value in Discover', async function () { + await testSubjects.moveMouseTo(`field-${scriptedPainlessFieldName2}`); await PageObjects.discover.clickFieldListItemDetails(scriptedPainlessFieldName2); await log.debug('filter by "true" in the expanded scripted field list'); await PageObjects.discover.clickFieldListPlusFilter(scriptedPainlessFieldName2, 'true'); @@ -473,6 +477,7 @@ export default function ({ getService, getPageObjects }) { const fromTime = 'Sep 17, 2015 @ 19:22:00.000'; const toTime = 'Sep 18, 2015 @ 07:00:00.000'; await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.switchDiscoverTable('new'); await PageObjects.discover.selectIndexPattern('logstash-*'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index f81c287a478f..cf0b03022ab9 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -516,6 +516,32 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider `Could not find a clickable list item for column "${columnName}" with list item "${title}".` ); } + + public async switchDiscoverTable(tableType: string) { + await retry.try(async () => { + const switchButton = await testSubjects.find('datagridTableButton'); + const buttonText = await switchButton.getVisibleText(); + + if (tableType === 'new' && buttonText.includes('Try new Discover')) { + await switchButton.click(); + } else if (tableType === 'legacy' && buttonText.includes('Use legacy Discover')) { + await switchButton.click(); + } + }); + + // Wait for the query input to be visible + await this.waitForQueryInput(); + } + + async waitForQueryInput() { + // Wait for the query input to be visible + await retry.try(async () => { + const queryInputVisible = await testSubjects.exists('queryInput'); + if (!queryInputVisible) { + throw new Error('Query input not yet visible'); + } + }); + } } return new DiscoverPage(); diff --git a/test/functional/services/dashboard/expectations.ts b/test/functional/services/dashboard/expectations.ts index 641c56b586fd..266517045747 100644 --- a/test/functional/services/dashboard/expectations.ts +++ b/test/functional/services/dashboard/expectations.ts @@ -240,6 +240,17 @@ export function DashboardExpectProvider({ getService, getPageObjects }: FtrProvi }); } + async savedSearchRowCountFromLegacyTable(expectedCount: number) { + log.debug(`DashboardExpect.savedSearchRowCount(${expectedCount})`); + await retry.try(async () => { + const savedSearchRows = await testSubjects.findAll( + 'docTableExpandToggleColumn', + findTimeout + ); + expect(savedSearchRows.length).to.be(expectedCount); + }); + } + async seriesElementCount(expectedCount: number) { log.debug(`DashboardExpect.seriesElementCount(${expectedCount})`); await retry.try(async () => { diff --git a/test/plugin_functional/test_suites/doc_views/doc_views.ts b/test/plugin_functional/test_suites/doc_views/doc_views.ts index 0944c8feb77a..b745d6e8a417 100644 --- a/test/plugin_functional/test_suites/doc_views/doc_views.ts +++ b/test/plugin_functional/test_suites/doc_views/doc_views.ts @@ -36,9 +36,10 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const find = getService('find'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); - describe('custom doc views', function () { + describe('custom doc views with datagrid table', function () { before(async () => { await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.switchDiscoverTable('new'); // TODO: change back to setDefaultRange() once we resolve // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5241 await PageObjects.timePicker.setDefaultRangeForDiscover();