[Discover] Redesign for the grid, panels and sidebar v1 (#165866)

## Summary

### Part 1

- Resolves https://github.com/elastic/kibana/issues/164287
- Closes https://github.com/elastic/kibana/issues/146339
- Previously separate PR https://github.com/elastic/kibana/pull/164187

Changes:
- ~~swaps checkbox and row selection~~
- removes vertical borders
- adds rows highlight
- increases cell padding
- adds row stripes
- updates header background
- removes grey background from field name and makes it bolder (part of
https://github.com/elastic/kibana/issues/164634)
- updates Surrounding Documents side paddings

### Part 2

- Resolves https://github.com/elastic/kibana/issues/164661
- Previously separate PR https://github.com/elastic/kibana/pull/165687

Changes:
- removes background from panels, tabs and sidebar
- updates "Add a field" button style
- removes shadow from field list items
- makes field search compact

### Part 3

- Resolves https://github.com/elastic/kibana/issues/164662

Changes:
- wraps "Add a field" button in its own container with a top border
- ~~adds a drag handle to sidebar items~~
- ~~adds new Show/Hide buttons to toggle sidebar~~ moves sidebar toggle
button from discover plugin to unified field list
- reduces spaces between sidebar items from 4px to 2px
- reduces padding on Single Document page
- removes border above grid tabs

<img width="600" alt="Screenshot 2023-09-07 at 14 39 48"
src="976db247-fd70-4c9b-8634-552ece45b522">


Please note that "auto" row height is in a separate PR which is also
ready for review https://github.com/elastic/kibana/pull/164218

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Davis McPhee <davismcphee@hotmail.com>
Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
This commit is contained in:
Julia Rechkunova 2023-09-12 08:51:34 +02:00 committed by GitHub
parent 6eea595153
commit 6cb937a37a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 807 additions and 280 deletions

View file

@ -33,6 +33,8 @@ const getCreationOptions: UnifiedFieldListSidebarContainerProps['getCreationOpti
originatingApp: PLUGIN_ID,
localStorageKeyPrefix: 'examples',
timeRangeUpdatesType: 'timefilter',
compressed: true,
showSidebarToggleButton: true,
disablePopularFields: true,
};
};

View file

@ -119,6 +119,28 @@ describe('DragDrop', () => {
});
});
test('dragstart sets dragClassName as expected', async () => {
const dndDispatch = jest.fn();
const component = mount(
<ChildDragDropProvider value={[{ ...defaultContextState, dragging: undefined }, dndDispatch]}>
<DragDrop value={value} draggable={true} order={[2, 0, 1, 0]} dragClassName="dragTest">
<button>Hi!</button>
</DragDrop>
</ChildDragDropProvider>
);
const dragDrop = component.find('[data-test-subj="testDragDrop"]').at(0);
expect(dragDrop.getDOMNode().querySelector('.dragTest')).toBeNull();
dragDrop.simulate('dragstart', { dataTransfer });
expect(dragDrop.getDOMNode().querySelector('.dragTest')).toBeDefined();
act(() => {
jest.runAllTimers();
});
expect(dragDrop.getDOMNode().querySelector('.dragTest')).toBeNull();
});
test('drop resets all the things', async () => {
const preventDefault = jest.fn();
const stopPropagation = jest.fn();

View file

@ -42,6 +42,10 @@ interface BaseProps {
* The CSS class(es) for the root element.
*/
className?: string;
/**
* CSS class to apply when the item is being dragged
*/
dragClassName?: string;
/**
* The event handler that fires when an item
@ -212,6 +216,7 @@ const removeSelection = () => {
const DragInner = memo(function DragInner({
dataTestSubj,
className,
dragClassName,
value,
children,
dndDispatch,
@ -305,6 +310,18 @@ const DragInner = memo(function DragInner({
// so we know we have DraggableProps if we reach this code.
if (e && 'dataTransfer' in e) {
e.dataTransfer.setData('text', value.humanData.label);
// Apply an optional class to the element being dragged so the ghost
// can be styled. We must add it to the actual element for a single
// frame before removing it so the ghost picks up the styling.
const current = e.currentTarget;
if (dragClassName && !current.classList.contains(dragClassName)) {
current.classList.add(dragClassName);
requestAnimationFrame(() => {
current.classList.remove(dragClassName);
});
}
}
// Chrome causes issues if you try to render from within a

View file

@ -3,32 +3,6 @@
max-width: 100%;
height: 100%;
overflow: hidden;
border-radius: $euiBorderRadius;
.euiDataGrid__controls {
border: none;
border-bottom: $euiBorderThin;
}
.euiDataGridRowCell.euiDataGridRowCell--firstColumn {
border-left: none;
padding: 0;
}
.euiDataGridRowCell.euiDataGridRowCell--lastColumn {
border-right: none;
}
.unifiedDataTable__table .euiDataGridRowCell:first-of-type,
.unifiedDataTable__table .euiDataGrid--headerShade.euiDataGrid--bordersAll .euiDataGridHeaderCell:first-of-type {
border-left: none;
border-right: none;
}
.euiDataGridRowCell:last-of-type,
.euiDataGridHeaderCell:last-of-type {
border-right: none;
}
}
.unifiedDataTable__cellValue {
@ -57,6 +31,29 @@
flex-direction: column;
flex-wrap: nowrap;
height: 100%;
.euiDataGrid__content {
background: transparent;
}
.euiDataGrid__controls {
border-top: $euiBorderThin;
}
.euiDataGrid--headerUnderline .euiDataGridHeaderCell {
border-bottom: $euiBorderThin;
}
.euiDataGridRowCell.euiDataGridRowCell--controlColumn[data-gridcell-column-id='openDetails'],
.euiDataGridRowCell.euiDataGridRowCell--controlColumn[data-gridcell-column-id='select'] {
padding-left: 0;
padding-right: 0;
}
.euiDataGrid--rowHoverHighlight .euiDataGridRow:hover,
.euiDataGrid--rowHoverHighlight .euiDataGridRow:hover .euiDataGridRowCell__contentByHeight + .euiDataGridRowCell__expandActions {
background-color: tintOrShade($euiColorLightShade, 50%, 0);
}
}
.unifiedDataTable__table {
@ -65,14 +62,6 @@
min-height: 0;
}
.unifiedDataTable__footer {
flex-shrink: 0;
background-color: $euiColorLightShade;
padding: $euiSize / 2 $euiSize;
margin-top: $euiSize / 4;
text-align: center;
}
.unifiedDataTable__flyoutHeader {
white-space: nowrap;
}
@ -118,7 +107,35 @@
@include euiTextTruncate;
}
.unifiedDataTable__rowControl {
// fine-tuning the vertical alignment with the text for any row height setting
margin-top: -3px;
.euiDataGridRowCell__truncate & { // "Single line" row height setting
margin-top: 0;
}
}
.unifiedDataTable__descriptionList {
// force the content truncation when "Single line" row height setting is active
.euiDataGridRowCell__truncate & {
-webkit-line-clamp: 1;
display: -webkit-box;
-webkit-box-orient: vertical;
height: 100%;
overflow: hidden;
}
}
.unifiedDataTable__descriptionListTitle {
margin-inline: 0 0;
padding-inline: 0;
background: transparent;
font-weight: $euiFontWeightBold;
}
.unifiedDataTable__descriptionListDescription {
margin-inline: $euiSizeS $euiSizeS;
padding-inline: 0;
word-break: break-all;
white-space: normal;

View file

@ -218,7 +218,7 @@ describe('Data table columns', function () {
</div>,
"displayAsText": "timestamp",
"id": "timestamp",
"initialWidth": 210,
"initialWidth": 212,
"isSortable": true,
"schema": "datetime",
"visibleCellActions": undefined,
@ -406,7 +406,7 @@ describe('Data table columns', function () {
</div>,
"displayAsText": "timestamp",
"id": "timestamp",
"initialWidth": 210,
"initialWidth": 212,
"isSortable": true,
"schema": "datetime",
"visibleCellActions": undefined,

View file

@ -30,7 +30,7 @@ import { buildEditFieldButton } from './build_edit_field_button';
const openDetails = {
id: 'openDetails',
width: 24,
width: 26,
headerCellRender: () => (
<EuiScreenReaderOnly>
<span>

View file

@ -15,14 +15,19 @@ import {
EuiCopy,
EuiDataGridCellValueElementProps,
EuiPopover,
EuiFlexGroup,
EuiFlexItem,
useEuiTheme,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { euiDarkVars as themeDark, euiLightVars as themeLight } from '@kbn/ui-theme';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import { UnifiedDataTableContext } from '../table_context';
export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => {
const { euiTheme } = useEuiTheme();
const { selectedDocs, expanded, rows, isDarkMode, setSelectedDocs } =
useContext(UnifiedDataTableContext);
const doc = useMemo(() => rows[rowIndex], [rows, rowIndex]);
@ -46,20 +51,33 @@ export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
}, [expanded, doc, setCellProps, isDarkMode]);
return (
<EuiCheckbox
id={doc.id}
aria-label={toggleDocumentSelectionLabel}
checked={checked}
data-test-subj={`dscGridSelectDoc-${doc.id}`}
onChange={() => {
if (checked) {
const newSelection = selectedDocs.filter((docId) => docId !== doc.id);
setSelectedDocs(newSelection);
} else {
setSelectedDocs([...selectedDocs, doc.id]);
}
}}
/>
<EuiFlexGroup
responsive={false}
direction="column"
justifyContent="center"
className="unifiedDataTable__rowControl"
css={css`
padding-block: ${euiTheme.size.xs}; // to have the same height as "openDetails" control
padding-left: ${euiTheme.size.xs}; // space between controls
`}
>
<EuiFlexItem grow={false}>
<EuiCheckbox
id={doc.id}
aria-label={toggleDocumentSelectionLabel}
checked={checked}
data-test-subj={`dscGridSelectDoc-${doc.id}`}
onChange={() => {
if (checked) {
const newSelection = selectedDocs.filter((docId) => docId !== doc.id);
setSelectedDocs(newSelection);
} else {
setSelectedDocs([...selectedDocs, doc.id]);
}
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -62,23 +62,25 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
}
return (
<EuiToolTip content={buttonLabel} delay="long" ref={toolTipRef}>
<EuiButtonIcon
id={rowIndex === 0 ? tourStep : undefined}
size="xs"
iconSize="s"
aria-label={buttonLabel}
data-test-subj={testSubj}
onClick={() => {
const nextHit = isCurrentRowExpanded ? undefined : current;
toolTipRef.current?.hideToolTip();
setPressed(Boolean(nextHit));
setExpanded?.(nextHit);
}}
color={isCurrentRowExpanded ? 'primary' : 'text'}
iconType={isCurrentRowExpanded ? 'minimize' : 'expand'}
isSelected={isCurrentRowExpanded}
/>
</EuiToolTip>
<div className="unifiedDataTable__rowControl">
<EuiToolTip content={buttonLabel} delay="long" ref={toolTipRef}>
<EuiButtonIcon
id={rowIndex === 0 ? tourStep : undefined}
size="xs"
iconSize="s"
aria-label={buttonLabel}
data-test-subj={testSubj}
onClick={() => {
const nextHit = isCurrentRowExpanded ? undefined : current;
toolTipRef.current?.hideToolTip();
setPressed(Boolean(nextHit));
setExpanded?.(nextHit);
}}
color={isCurrentRowExpanded ? 'primary' : 'text'}
iconType={isCurrentRowExpanded ? 'minimize' : 'expand'}
isSelected={isCurrentRowExpanded}
/>
</EuiToolTip>
</div>
);
};

View file

@ -13,15 +13,17 @@ export const MAX_LOADED_GRID_ROWS = 10000;
export const ROWS_PER_PAGE_OPTIONS = [10, 25, 50, DEFAULT_ROWS_PER_PAGE, 250, 500];
export const defaultMonacoEditorWidth = 370;
export const defaultTimeColumnWidth = 210;
export const defaultTimeColumnWidth = 212;
export const kibanaJSON = 'kibana-json';
export const GRID_STYLE = {
border: 'all',
export const GRID_STYLE: EuiDataGridStyle = {
border: 'horizontal',
fontSize: 's',
cellPadding: 's',
rowHover: 'none',
} as EuiDataGridStyle;
cellPadding: 'l',
rowHover: 'highlight',
header: 'underline',
stripes: true,
};
export const toolbarVisibility = {
showColumnSelector: {

View file

@ -212,7 +212,9 @@ describe('Unified data table cell rendering', function () {
compressed={true}
type="inline"
>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
extension
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
@ -223,7 +225,9 @@ describe('Unified data table cell rendering', function () {
}
}
/>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
bytesDisplayName
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
@ -234,7 +238,9 @@ describe('Unified data table cell rendering', function () {
}
}
/>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
_index
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
@ -245,7 +251,9 @@ describe('Unified data table cell rendering', function () {
}
}
/>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
_score
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
@ -363,7 +371,9 @@ describe('Unified data table cell rendering', function () {
compressed={true}
type="inline"
>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
extension
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
@ -376,7 +386,9 @@ describe('Unified data table cell rendering', function () {
}
}
/>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
bytesDisplayName
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
@ -389,7 +401,9 @@ describe('Unified data table cell rendering', function () {
}
}
/>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
_index
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
@ -400,7 +414,9 @@ describe('Unified data table cell rendering', function () {
}
}
/>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
_score
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
@ -443,7 +459,9 @@ describe('Unified data table cell rendering', function () {
compressed={true}
type="inline"
>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
extension
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
@ -456,7 +474,9 @@ describe('Unified data table cell rendering', function () {
}
}
/>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
bytesDisplayName
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
@ -469,7 +489,9 @@ describe('Unified data table cell rendering', function () {
}
}
/>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
_index
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
@ -480,7 +502,9 @@ describe('Unified data table cell rendering', function () {
}
}
/>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
_score
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
@ -603,7 +627,9 @@ describe('Unified data table cell rendering', function () {
compressed={true}
type="inline"
>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
object.value
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
@ -646,7 +672,9 @@ describe('Unified data table cell rendering', function () {
compressed={true}
type="inline"
>
<EuiDescriptionListTitle>
<EuiDescriptionListTitle
className="unifiedDataTable__descriptionListTitle"
>
object.value
</EuiDescriptionListTitle>
<EuiDescriptionListDescription

View file

@ -137,7 +137,9 @@ export const getRenderCellValueFn =
>
{pairs.map(([key, value]) => (
<Fragment key={key}>
<EuiDescriptionListTitle>{key}</EuiDescriptionListTitle>
<EuiDescriptionListTitle className="unifiedDataTable__descriptionListTitle">
{key}
</EuiDescriptionListTitle>
<EuiDescriptionListDescription
className="unifiedDataTable__descriptionListDescription"
dangerouslySetInnerHTML={{ __html: value }}

View file

@ -191,7 +191,7 @@ exports[`UnifiedFieldList <FieldItemButton /> renders properly with a drag handl
"aria-label": "Preview bytes: number",
}
}
className="unifiedFieldListItemButton unifiedFieldListItemButton--number unifiedFieldListItemButton--exists custom"
className="unifiedFieldListItemButton unifiedFieldListItemButton--number unifiedFieldListItemButton--exists unifiedFieldListItemButton--withDragHandle custom"
dataTestSubj="test-subj"
dragHandle={
<span>

View file

@ -105,6 +105,7 @@ export function FieldItemButton<T extends FieldListItem = DataViewField>({
[`unifiedFieldListItemButton--${type}`]: type,
[`unifiedFieldListItemButton--exists`]: !isEmpty,
[`unifiedFieldListItemButton--missing`]: isEmpty,
[`unifiedFieldListItemButton--withDragHandle`]: Boolean(otherProps.dragHandle),
},
className
);

View file

@ -23,6 +23,7 @@ export interface FieldListFiltersProps<T extends FieldListItem> {
getCustomFieldType?: FieldTypeFilterProps<T>['getCustomFieldType'];
onSupportedFieldFilter?: FieldTypeFilterProps<T>['onSupportedFieldFilter'];
onChangeFieldTypes: FieldTypeFilterProps<T>['onChange'];
compressed?: FieldNameSearchProps['compressed'];
nameFilter: FieldNameSearchProps['nameFilter'];
screenReaderDescriptionId?: FieldNameSearchProps['screenReaderDescriptionId'];
onChangeNameFilter: FieldNameSearchProps['onChange'];
@ -38,6 +39,7 @@ export interface FieldListFiltersProps<T extends FieldListItem> {
* @param getCustomFieldType
* @param onSupportedFieldFilter
* @param onChangeFieldTypes
* @param compressed
* @param nameFilter
* @param screenReaderDescriptionId
* @param onChangeNameFilter
@ -52,6 +54,7 @@ function InnerFieldListFilters<T extends FieldListItem = DataViewField>({
getCustomFieldType,
onSupportedFieldFilter,
onChangeFieldTypes,
compressed,
nameFilter,
screenReaderDescriptionId,
onChangeNameFilter,
@ -72,6 +75,7 @@ function InnerFieldListFilters<T extends FieldListItem = DataViewField>({
/>
) : undefined
}
compressed={compressed}
nameFilter={nameFilter}
screenReaderDescriptionId={screenReaderDescriptionId}
onChange={onChangeNameFilter}

View file

@ -16,6 +16,7 @@ import { EuiFieldSearch, type EuiFieldSearchProps } from '@elastic/eui';
export interface FieldNameSearchProps {
'data-test-subj': string;
append?: EuiFieldSearchProps['append'];
compressed?: EuiFieldSearchProps['compressed'];
nameFilter: string;
screenReaderDescriptionId?: string;
onChange: (nameFilter: string) => unknown;
@ -25,6 +26,7 @@ export interface FieldNameSearchProps {
* Search input for fields list
* @param dataTestSubject
* @param append
* @param compressed
* @param nameFilter
* @param screenReaderDescriptionId
* @param onChange
@ -33,6 +35,7 @@ export interface FieldNameSearchProps {
export const FieldNameSearch: React.FC<FieldNameSearchProps> = ({
'data-test-subj': dataTestSubject,
append,
compressed,
nameFilter,
screenReaderDescriptionId,
onChange,
@ -52,6 +55,7 @@ export const FieldNameSearch: React.FC<FieldNameSearchProps> = ({
placeholder={searchPlaceholder}
value={nameFilter}
append={append}
compressed={compressed}
/>
);
};

View file

@ -82,6 +82,7 @@ async function getComponent({
isEmpty: false,
groupIndex: 1,
itemIndex: 0,
size: 'xs',
workspaceSelectedFieldNames: [],
};
const comp = await mountWithIntl(<UnifiedFieldListItem {...props} />);

View file

@ -33,6 +33,7 @@ import type {
interface GetCommonFieldItemButtonPropsParams {
stateService: UnifiedFieldListSidebarContainerStateService;
field: DataViewField;
size: FieldItemButtonProps<DataViewField>['size'];
isSelected: boolean;
toggleDisplay: (field: DataViewField, isSelected?: boolean) => void;
}
@ -40,10 +41,12 @@ interface GetCommonFieldItemButtonPropsParams {
function getCommonFieldItemButtonProps({
stateService,
field,
size,
isSelected,
toggleDisplay,
}: GetCommonFieldItemButtonPropsParams): {
field: FieldItemButtonProps<DataViewField>['field'];
size: FieldItemButtonProps<DataViewField>['size'];
isSelected: FieldItemButtonProps<DataViewField>['isSelected'];
buttonAddFieldToWorkspaceProps?: FieldItemButtonProps<DataViewField>['buttonAddFieldToWorkspaceProps'];
buttonRemoveFieldFromWorkspaceProps?: FieldItemButtonProps<DataViewField>['buttonRemoveFieldFromWorkspaceProps'];
@ -54,6 +57,7 @@ function getCommonFieldItemButtonProps({
field.name === '_source' ? undefined : (f: DataViewField) => toggleDisplay(f, isSelected);
return {
field,
size,
isSelected,
buttonAddFieldToWorkspaceProps: stateService.creationOptions.buttonAddFieldToWorkspaceProps,
buttonRemoveFieldFromWorkspaceProps:
@ -68,10 +72,11 @@ interface MultiFieldsProps {
multiFields: NonNullable<UnifiedFieldListItemProps['multiFields']>;
toggleDisplay: (field: DataViewField) => void;
alwaysShowActionButton: boolean;
size: FieldItemButtonProps<DataViewField>['size'];
}
const MultiFields: React.FC<MultiFieldsProps> = memo(
({ stateService, multiFields, toggleDisplay, alwaysShowActionButton }) => (
({ stateService, multiFields, toggleDisplay, alwaysShowActionButton, size }) => (
<React.Fragment>
<EuiTitle size="xxxs">
<h5>
@ -84,7 +89,6 @@ const MultiFields: React.FC<MultiFieldsProps> = memo(
{multiFields.map((entry) => (
<FieldItemButton
key={entry.field.name}
size="xs"
flush="both"
isEmpty={false}
isActive={false}
@ -95,6 +99,7 @@ const MultiFields: React.FC<MultiFieldsProps> = memo(
field: entry.field,
isSelected: entry.isSelected,
toggleDisplay,
size,
})}
/>
))}
@ -187,6 +192,10 @@ export interface UnifiedFieldListItemProps {
* Item index in the field list
*/
itemIndex: number;
/**
* Item size
*/
size: FieldItemButtonProps<DataViewField>['size'];
}
function UnifiedFieldListItemComponent({
@ -209,6 +218,7 @@ function UnifiedFieldListItemComponent({
workspaceSelectedFieldNames,
groupIndex,
itemIndex,
size,
}: UnifiedFieldListItemProps) {
const [infoIsOpen, setOpen] = useState(false);
@ -284,6 +294,7 @@ function UnifiedFieldListItemComponent({
multiFields={multiFields}
alwaysShowActionButton={alwaysShowActionButton}
toggleDisplay={toggleDisplay}
size={size}
/>
</>
)}
@ -315,6 +326,8 @@ function UnifiedFieldListItemComponent({
[field, itemIndex]
);
const order = useMemo(() => [0, groupIndex, itemIndex], [groupIndex, itemIndex]);
const isDragDisabled =
alwaysShowActionButton || stateService.creationOptions.disableFieldListItemDragAndDrop;
return (
<FieldPopover
@ -322,26 +335,30 @@ function UnifiedFieldListItemComponent({
button={
<DragDrop
draggable
dragClassName="unifiedFieldListItemButton__dragging"
order={order}
value={value}
onDragStart={closePopover}
isDisabled={
alwaysShowActionButton || stateService.creationOptions.disableFieldListItemDragAndDrop
}
isDisabled={isDragDisabled}
dataTestSubj={`${
stateService.creationOptions.dataTestSubj?.fieldListItemDndDataTestSubjPrefix ??
'unifiedFieldListItemDnD'
}-${field.name}`}
>
<FieldItemButton
size="xs"
fieldSearchHighlight={highlight}
isEmpty={isEmpty}
isActive={infoIsOpen}
flush={alwaysShowActionButton ? 'both' : undefined}
shouldAlwaysShowAction={alwaysShowActionButton}
onClick={field.type !== '_source' ? togglePopover : undefined}
{...getCommonFieldItemButtonProps({ stateService, field, isSelected, toggleDisplay })}
{...getCommonFieldItemButtonProps({
stateService,
field,
isSelected,
toggleDisplay,
size,
})}
/>
</DragDrop>
}

View file

@ -6,6 +6,11 @@
width: $euiSize * 19;
height: 100%;
&--collapsed {
width: auto;
padding: $euiSizeS $euiSizeS 0;
}
@include euiBreakpoint('xs', 's') {
width: 100%;
padding: $euiSize;
@ -14,7 +19,7 @@
}
.unifiedFieldListSidebar__list {
padding: $euiSizeS 0 $euiSizeS $euiSizeS;
padding: $euiSizeS $euiSizeS 0;
@include euiBreakpoint('xs', 's') {
padding: $euiSizeS 0 0 0;
@ -38,3 +43,18 @@
.unifiedFieldListSidebar__flyoutHeader {
align-items: center;
}
.unifiedFieldListSidebar .unifiedFieldListItemButton {
&.kbnFieldButton {
margin-bottom: $euiSizeXS / 2;
}
&.domDragDrop-isDraggable {
box-shadow: none;
}
&:not(.unifiedFieldListItemButton__dragging) {
padding: 0;
background: none;
}
}

View file

@ -9,16 +9,29 @@
import './field_list_sidebar.scss';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPageSidebar } from '@elastic/eui';
import { css } from '@emotion/react';
import classnames from 'classnames';
import {
EuiButton,
EuiButtonProps,
EuiFlexGroup,
EuiFlexItem,
EuiHideFor,
EuiPageSidebar,
EuiPageSidebarProps,
useEuiTheme,
} from '@elastic/eui';
import { ToolbarButton } from '@kbn/shared-ux-button-toolbar';
import { type DataViewField } from '@kbn/data-views-plugin/public';
import { getDataViewFieldSubtypeMulti } from '@kbn/es-query/src/utils';
import { FIELDS_LIMIT_SETTING, SEARCH_FIELDS_FROM_SOURCE } from '@kbn/discover-utils';
import { FieldList } from '../../components/field_list';
import { FieldListFilters } from '../../components/field_list_filters';
import { FieldListGrouped, type FieldListGroupedProps } from '../../components/field_list_grouped';
import { FieldsGroupNames } from '../../types';
import { FieldsGroupNames, type ButtonAddFieldVariant } from '../../types';
import { GroupedFieldsParams, useGroupedFields } from '../../hooks/use_grouped_fields';
import { UnifiedFieldListItem, type UnifiedFieldListItemProps } from '../unified_field_list_item';
import { SidebarToggleButton, type SidebarToggleButtonProps } from './sidebar_toggle_button';
import {
getSelectedFields,
shouldShowField,
@ -46,6 +59,11 @@ export type UnifiedFieldListSidebarCustomizableProps = Pick<
*/
showFieldList?: boolean;
/**
* Compressed view
*/
compressed?: boolean;
/**
* Custom logic for determining which field is selected
*/
@ -83,6 +101,22 @@ interface UnifiedFieldListSidebarInternalProps {
*/
alwaysShowActionButton?: UnifiedFieldListItemProps['alwaysShowActionButton'];
/**
* What button style type to use
*/
buttonAddFieldVariant: ButtonAddFieldVariant;
/**
* In case if sidebar is collapsible by default
* Pass `undefined` to hide the collapse/expand buttons from the sidebar
*/
isSidebarCollapsed?: boolean;
/**
* A handler to toggle the sidebar
*/
onToggleSidebar?: SidebarToggleButtonProps['onChange'];
/**
* Trigger a field editing
*/
@ -104,10 +138,13 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
workspaceSelectedFieldNames,
isProcessing,
alwaysShowActionButton,
buttonAddFieldVariant,
isSidebarCollapsed,
allFields,
dataView,
trackUiMetric,
showFieldList = true,
compressed = true,
isAffectedByGlobalFilter,
prepend,
onAddFieldToWorkspace,
@ -116,6 +153,7 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
onSelectedFieldFilter,
onEditField,
onDeleteField,
onToggleSidebar,
}) => {
const { dataViews, core } = services;
const useNewFieldsApi = useMemo(
@ -210,6 +248,7 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
services={services}
alwaysShowActionButton={alwaysShowActionButton}
field={field}
size={compressed ? 'xs' : 's'}
highlight={fieldSearchHighlight}
dataView={dataView!}
onAddFieldToWorkspace={onAddFieldToWorkspace}
@ -235,6 +274,7 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
searchMode,
services,
alwaysShowActionButton,
compressed,
dataView,
onAddFieldToWorkspace,
onRemoveFieldFromWorkspace,
@ -248,40 +288,94 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
]
);
const { euiTheme } = useEuiTheme();
if (!dataView) {
return null;
}
const sidebarToggleButton =
typeof isSidebarCollapsed === 'boolean' && onToggleSidebar ? (
<SidebarToggleButton
buttonSize={compressed ? 's' : 'm'}
isSidebarCollapsed={isSidebarCollapsed}
onChange={onToggleSidebar}
/>
) : null;
const pageSidebarProps: Partial<EuiPageSidebarProps> = {
className: classnames('unifiedFieldListSidebar', {
'unifiedFieldListSidebar--collapsed': isSidebarCollapsed,
}),
'aria-label': i18n.translate(
'unifiedFieldList.fieldListSidebar.indexAndFieldsSectionAriaLabel',
{
defaultMessage: 'Index and fields',
}
),
id:
stateService.creationOptions.dataTestSubj?.fieldListSidebarDataTestSubj ??
'unifiedFieldListSidebarId',
'data-test-subj':
stateService.creationOptions.dataTestSubj?.fieldListSidebarDataTestSubj ??
'unifiedFieldListSidebarId',
};
if (isSidebarCollapsed && sidebarToggleButton) {
return (
<EuiHideFor sizes={['xs', 's']}>
<div {...pageSidebarProps}>{sidebarToggleButton}</div>
</EuiHideFor>
);
}
const hasButtonAddFieldToolbarStyle = buttonAddFieldVariant === 'toolbar';
const buttonAddFieldCommonProps: Partial<EuiButtonProps> = {
size: 's',
iconType: 'indexOpen',
'data-test-subj':
stateService.creationOptions.dataTestSubj?.fieldListAddFieldButtonTestSubj ??
'unifiedFieldListAddField',
};
const buttonAddFieldLabel = i18n.translate(
'unifiedFieldList.fieldListSidebar.addFieldButtonLabel',
{
defaultMessage: 'Add a field',
}
);
return (
<EuiPageSidebar
className="unifiedFieldListSidebar"
aria-label={i18n.translate(
'unifiedFieldList.fieldListSidebar.indexAndFieldsSectionAriaLabel',
{
defaultMessage: 'Index and fields',
}
)}
id={
stateService.creationOptions.dataTestSubj?.fieldListSidebarDataTestSubj ??
'unifiedFieldListSidebarId'
}
data-test-subj={
stateService.creationOptions.dataTestSubj?.fieldListSidebarDataTestSubj ??
'unifiedFieldListSidebarId'
}
>
<EuiPageSidebar {...pageSidebarProps}>
<EuiFlexGroup
className="unifiedFieldListSidebar__group"
direction="column"
alignItems="stretch"
gutterSize="s"
gutterSize="none"
responsive={false}
>
{Boolean(prepend) && <EuiFlexItem grow={false}>{prepend}</EuiFlexItem>}
{Boolean(prepend) && (
<EuiFlexItem
grow={false}
css={css`
margin-bottom: ${euiTheme.size.s};
`}
>
{prepend}
</EuiFlexItem>
)}
<EuiFlexItem>
<FieldList
isProcessing={isProcessing}
prepend={<FieldListFilters {...fieldListFiltersProps} />}
prepend={
<EuiFlexGroup direction="row" gutterSize="s" responsive={false}>
{sidebarToggleButton && (
<EuiFlexItem grow={false}>{sidebarToggleButton}</EuiFlexItem>
)}
<EuiFlexItem>
<FieldListFilters {...fieldListFiltersProps} compressed={compressed} />
</EuiFlexItem>
</EuiFlexGroup>
}
className="unifiedFieldListSidebar__list"
>
{showFieldList ? (
@ -293,25 +387,33 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
) : (
<EuiFlexItem grow />
)}
{!!onEditField && (
<EuiFlexItem grow={false}>
<EuiButton
iconType="indexOpen"
data-test-subj={
stateService.creationOptions.dataTestSubj?.fieldListAddFieldButtonTestSubj ??
'unifiedFieldListAddField'
}
onClick={() => onEditField()}
size="s"
>
{i18n.translate('unifiedFieldList.fieldListSidebar.addFieldButtonLabel', {
defaultMessage: 'Add a field',
})}
</EuiButton>
</EuiFlexItem>
)}
</FieldList>
</EuiFlexItem>
{!!onEditField && (
<EuiFlexItem
grow={false}
css={
hasButtonAddFieldToolbarStyle
? css`
padding: ${euiTheme.size.s};
border-top: ${euiTheme.border.thin};
`
: undefined
}
>
{hasButtonAddFieldToolbarStyle ? (
<ToolbarButton
{...buttonAddFieldCommonProps}
label={buttonAddFieldLabel}
onClick={() => onEditField()}
/>
) : (
<EuiButton {...buttonAddFieldCommonProps} onClick={() => onEditField()}>
{buttonAddFieldLabel}
</EuiButton>
)}
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiPageSidebar>
);

View file

@ -35,6 +35,7 @@ import {
type ExistingFieldsFetcher,
} from '../../hooks/use_existing_fields';
import { useQuerySubscriber } from '../../hooks/use_query_subscriber';
import { useSidebarToggle } from '../../hooks/use_sidebar_toggle';
import {
UnifiedFieldListSidebar,
type UnifiedFieldListSidebarCustomizableProps,
@ -72,11 +73,6 @@ export type UnifiedFieldListSidebarContainerProps = Omit<
*/
getCreationOptions: () => UnifiedFieldListSidebarContainerCreationOptions;
/**
* In case if you have a sidebar toggle button
*/
isSidebarCollapsed?: boolean;
/**
* Custom content to render at the top of field list in the flyout (for example a data view picker)
*/
@ -115,7 +111,6 @@ const UnifiedFieldListSidebarContainer = forwardRef<
services,
dataView,
workspaceSelectedFieldNames,
isSidebarCollapsed, // TODO later: pull the logic of collapsing the sidebar to this component
prependInFlyout,
variant = 'responsive',
onFieldEdited,
@ -125,6 +120,7 @@ const UnifiedFieldListSidebarContainer = forwardRef<
);
const { data, dataViewFieldEditor } = services;
const [isFieldListFlyoutVisible, setIsFieldListFlyoutVisible] = useState<boolean>(false);
const { isSidebarCollapsed, onToggleSidebar } = useSidebarToggle({ stateService });
const canEditDataView =
Boolean(dataViewFieldEditor?.userPermissions.editIndexPattern()) ||
@ -250,8 +246,15 @@ const UnifiedFieldListSidebarContainer = forwardRef<
isAffectedByGlobalFilter,
onEditField: editField,
onDeleteField: deleteField,
compressed: stateService.creationOptions.compressed ?? false,
buttonAddFieldVariant: stateService.creationOptions.buttonAddFieldVariant ?? 'primary',
};
if (stateService.creationOptions.showSidebarToggleButton) {
commonSidebarProps.isSidebarCollapsed = isSidebarCollapsed;
commonSidebarProps.onToggleSidebar = onToggleSidebar;
}
const buttonPropsToTriggerFlyout = stateService.creationOptions.buttonPropsToTriggerFlyout;
const renderListVariant = () => {
@ -319,6 +322,8 @@ const UnifiedFieldListSidebarContainer = forwardRef<
<UnifiedFieldListSidebar
{...commonSidebarProps}
alwaysShowActionButton={true}
buttonAddFieldVariant="primary" // always for the flyout
isSidebarCollapsed={undefined}
prepend={prependInFlyout?.()}
/>
</EuiFlyout>
@ -333,12 +338,12 @@ const UnifiedFieldListSidebarContainer = forwardRef<
}
if (variant === 'list-always') {
return (!isSidebarCollapsed && renderListVariant()) || null;
return renderListVariant();
}
return (
<>
{!isSidebarCollapsed && <EuiHideFor sizes={['xs', 's']}>{renderListVariant()}</EuiHideFor>}
<EuiHideFor sizes={['xs', 's']}>{renderListVariant()}</EuiHideFor>
<EuiShowFor sizes={['xs', 's']}>{renderButtonVariant()}</EuiShowFor>
</>
);

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { SidebarToggleButton, type SidebarToggleButtonProps } from './sidebar_toggle_button';

View file

@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { IconButtonGroup, type IconButtonGroupProps } from '@kbn/shared-ux-button-toolbar';
/**
* Toggle button props
*/
export interface SidebarToggleButtonProps {
'data-test-subj'?: string;
isSidebarCollapsed: boolean;
buttonSize: IconButtonGroupProps['buttonSize'];
onChange: (isSidebarCollapsed: boolean) => void;
}
/**
* A toggle button for the fields sidebar
* @param data-test-subj
* @param isSidebarCollapsed
* @param onChange
* @constructor
*/
export const SidebarToggleButton: React.FC<SidebarToggleButtonProps> = ({
'data-test-subj': dataTestSubj = 'unifiedFieldListSidebar__toggle',
isSidebarCollapsed,
buttonSize,
onChange,
}) => {
// TODO: replace with new Eui icons once available
return (
<div data-test-subj={dataTestSubj}>
<IconButtonGroup
legend={i18n.translate('unifiedFieldList.fieldListSidebar.toggleSidebarLegend', {
defaultMessage: 'Toggle sidebar',
})}
buttonSize={buttonSize}
buttons={[
...(isSidebarCollapsed
? [
{
label: i18n.translate('unifiedFieldList.fieldListSidebar.expandSidebarButton', {
defaultMessage: 'Show sidebar',
}),
iconType: 'menuRight',
'data-test-subj': `${dataTestSubj}-expand`,
onClick: () => onChange(false),
},
]
: [
{
label: i18n.translate('unifiedFieldList.fieldListSidebar.collapseSidebarButton', {
defaultMessage: 'Hide sidebar',
}),
iconType: 'menuLeft',
'data-test-subj': `${dataTestSubj}-collapse`,
onClick: () => onChange(true),
},
]),
]}
/>
</div>
);
};

View file

@ -0,0 +1,104 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useSidebarToggle } from './use_sidebar_toggle';
import * as localStorageModule from 'react-use/lib/useLocalStorage';
jest.spyOn(localStorageModule, 'default');
describe('UnifiedFieldList useSidebarToggle', () => {
const stateService = {
creationOptions: {
originatingApp: 'test',
localStorageKeyPrefix: 'this',
},
};
beforeEach(() => {
(localStorageModule.default as jest.Mock).mockClear();
});
it('should toggle correctly', async () => {
const storeMock = jest.fn();
(localStorageModule.default as jest.Mock).mockImplementation(() => {
return [false, storeMock];
});
const { result } = renderHook(useSidebarToggle, {
initialProps: {
stateService,
},
});
expect(result.current.isSidebarCollapsed).toBe(false);
act(() => {
result.current.onToggleSidebar(true);
});
expect(result.current.isSidebarCollapsed).toBe(true);
expect(storeMock).toHaveBeenCalledWith(true);
act(() => {
result.current.onToggleSidebar(false);
});
expect(result.current.isSidebarCollapsed).toBe(false);
expect(storeMock).toHaveBeenLastCalledWith(false);
});
it('should restore collapsed state and expand from it', async () => {
const storeMock = jest.fn();
(localStorageModule.default as jest.Mock).mockImplementation(() => {
return [true, storeMock];
});
const { result } = renderHook(useSidebarToggle, {
initialProps: {
stateService,
},
});
expect(result.current.isSidebarCollapsed).toBe(true);
act(() => {
result.current.onToggleSidebar(false);
});
expect(result.current.isSidebarCollapsed).toBe(false);
expect(storeMock).toHaveBeenCalledWith(false);
});
it('should not persist if local storage key is not defined', async () => {
const storeMock = jest.fn();
(localStorageModule.default as jest.Mock).mockImplementation(() => {
return [false, storeMock];
});
const { result } = renderHook(useSidebarToggle, {
initialProps: {
stateService: {
creationOptions: {
originatingApp: 'test',
localStorageKeyPrefix: undefined,
},
},
},
});
expect(result.current.isSidebarCollapsed).toBe(false);
act(() => {
result.current.onToggleSidebar(true);
});
expect(result.current.isSidebarCollapsed).toBe(true);
expect(storeMock).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { useCallback, useState, useMemo } from 'react';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import type { UnifiedFieldListSidebarContainerStateService } from '../types';
/**
* Hook params
*/
export interface UseSidebarToggleParams {
/**
* Service for managing the state
*/
stateService: UnifiedFieldListSidebarContainerStateService;
}
/**
* Hook result type
*/
export interface UseSidebarToggleResult {
isSidebarCollapsed: boolean;
onToggleSidebar: (isSidebarCollapsed: boolean) => void;
}
/**
* Hook for managing sidebar toggle state
* @param stateService
*/
export const useSidebarToggle = ({
stateService,
}: UseSidebarToggleParams): UseSidebarToggleResult => {
const [initialIsSidebarCollapsed, storeIsSidebarCollapsed] = useLocalStorage<boolean>(
`${stateService.creationOptions.localStorageKeyPrefix ?? 'unifiedFieldList'}:sidebarClosed`, // as legacy `discover:sidebarClosed` key
false
);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState<boolean>(
initialIsSidebarCollapsed ?? false
);
const onToggleSidebar = useCallback(
(isCollapsed) => {
setIsSidebarCollapsed(isCollapsed);
if (stateService.creationOptions.localStorageKeyPrefix) {
storeIsSidebarCollapsed(isCollapsed);
}
},
[
storeIsSidebarCollapsed,
setIsSidebarCollapsed,
stateService.creationOptions.localStorageKeyPrefix,
]
);
return useMemo(
() => ({ isSidebarCollapsed, onToggleSidebar }),
[isSidebarCollapsed, onToggleSidebar]
);
};

View file

@ -107,6 +107,8 @@ export type OverrideFieldGroupDetails = (
export type TimeRangeUpdatesType = 'search-session' | 'timefilter';
export type ButtonAddFieldVariant = 'primary' | 'toolbar';
export type SearchMode = 'documents' | 'text-based';
export interface UnifiedFieldListSidebarContainerCreationOptions {
@ -116,7 +118,12 @@ export interface UnifiedFieldListSidebarContainerCreationOptions {
originatingApp: string;
/**
* Your app name: "discover", "lens", etc. If not provided, sections state would not be persisted.
* Pass `true` to enable the compressed view
*/
compressed?: boolean;
/**
* Your app name: "discover", "lens", etc. If not provided, sections and sidebar toggle states would not be persisted.
*/
localStorageKeyPrefix?: string;
@ -125,6 +132,16 @@ export interface UnifiedFieldListSidebarContainerCreationOptions {
*/
timeRangeUpdatesType?: TimeRangeUpdatesType;
/**
* Choose how the bottom "Add a field" button should look like. Default `primary`.
*/
buttonAddFieldVariant?: ButtonAddFieldVariant;
/**
* Pass `true` to make the sidebar collapsible. Additionally, define `localStorageKeyPrefix` to persist toggle state.
*/
showSidebarToggleButton?: boolean;
/**
* Pass `true` to skip auto fetching of fields existence info
*/

View file

@ -29,6 +29,7 @@
"@kbn/shared-ux-utility",
"@kbn/discover-utils",
"@kbn/ebt-tools",
"@kbn/shared-ux-button-toolbar",
],
"exclude": ["target/**/*"]
}

View file

@ -21,7 +21,6 @@ import {
SEARCH_FIELDS_FROM_SOURCE,
SHOW_MULTIFIELDS,
} from '@kbn/discover-utils';
import { SIDEBAR_CLOSED_KEY } from '../../application/main/components/layout/discover_layout';
import { LocalStorageMock } from '../local_storage_mock';
import { DiscoverServices } from '../../build_services';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
@ -73,9 +72,7 @@ export const services = {
docLinks: { links: { discover: {} } },
theme,
},
storage: new LocalStorageMock({
[SIDEBAR_CLOSED_KEY]: false,
}) as unknown as Storage,
storage: new LocalStorageMock({}) as unknown as Storage,
data: {
query: {
timefilter: {

View file

@ -156,7 +156,7 @@ export function ActionBar({
</EuiFlexGroup>
{!isSuccessor && showWarning && <EuiSpacer size="s" />}
{!isSuccessor && showWarning && <ActionBarWarning docCount={docCountAvailable} type={type} />}
{!isSuccessor && <EuiSpacer size="s" />}
<EuiSpacer size="s" />
</form>
);
}

View file

@ -17,8 +17,4 @@
&__cell--highlight {
background-color: tintOrShade($euiColorPrimary, 90%, 70%);
}
.euiDataGridRowCell.euiDataGridRowCell--firstColumn {
padding: 0;
}
}

View file

@ -10,7 +10,8 @@ import React, { Fragment, memo, useEffect, useRef, useMemo, useCallback } from '
import './context_app.scss';
import classNames from 'classnames';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiText, EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui';
import { EuiText, EuiPage, EuiPageBody, EuiSpacer, useEuiPaddingSize } from '@elastic/eui';
import { css } from '@emotion/react';
import { cloneDeep } from 'lodash';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
@ -215,6 +216,8 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
};
};
const titlePadding = useEuiPaddingSize('m');
return (
<Fragment>
{fetchedState.anchorStatus.value === LoadingStatus.FAILED ? (
@ -235,12 +238,16 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
<EuiPage className={classNames({ dscDocsPage: !isLegacy })}>
<EuiPageBody
panelled
paddingSize="s"
paddingSize="none"
className="dscDocsContent"
panelProps={{ role: 'main' }}
>
<EuiSpacer size="s" />
<EuiText data-test-subj="contextDocumentSurroundingHeader">
<EuiText
data-test-subj="contextDocumentSurroundingHeader"
css={css`
padding: ${titlePadding} ${titlePadding} 0;
`}
>
<strong>
<FormattedMessage
id="discover.context.contextOfTitle"

View file

@ -8,7 +8,8 @@
import React, { Fragment, useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiHorizontalRule, EuiSpacer, EuiText } from '@elastic/eui';
import { EuiSpacer, EuiText, useEuiPaddingSize } from '@elastic/eui';
import { css } from '@emotion/react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { SortDirection } from '@kbn/data-plugin/public';
import type { SortOrder } from '@kbn/saved-search-plugin/public';
@ -149,27 +150,28 @@ export function ContextAppContent({
return (
<Fragment>
{!!interceptedWarnings?.length && (
<>
<SearchResponseWarnings
variant="callout"
interceptedWarnings={interceptedWarnings}
data-test-subj="dscContextInterceptedWarnings"
/>
<EuiSpacer size="s" />
</>
)}
<ActionBarMemoized
type={SurrDocType.PREDECESSORS}
defaultStepSize={defaultStepSize}
docCount={predecessorCount}
docCountAvailable={predecessors.length}
onChangeCount={onChangeCount}
isLoading={arePredecessorsLoading}
isDisabled={isAnchorLoading}
/>
{loadingFeedback()}
<EuiHorizontalRule margin="xs" />
<WrapperWithPadding>
{!!interceptedWarnings?.length && (
<>
<SearchResponseWarnings
variant="callout"
interceptedWarnings={interceptedWarnings}
data-test-subj="dscContextInterceptedWarnings"
/>
<EuiSpacer size="s" />
</>
)}
<ActionBarMemoized
type={SurrDocType.PREDECESSORS}
defaultStepSize={defaultStepSize}
docCount={predecessorCount}
docCountAvailable={predecessors.length}
onChangeCount={onChangeCount}
isLoading={arePredecessorsLoading}
isDisabled={isAnchorLoading}
/>
{loadingFeedback()}
</WrapperWithPadding>
{isLegacy && rows && rows.length !== 0 && (
<DocTableContextMemoized
columns={columns}
@ -215,16 +217,31 @@ export function ContextAppContent({
</CellActionsProvider>
</div>
)}
<EuiHorizontalRule margin="xs" />
<ActionBarMemoized
type={SurrDocType.SUCCESSORS}
defaultStepSize={defaultStepSize}
docCount={successorCount}
docCountAvailable={successors.length}
onChangeCount={onChangeCount}
isLoading={areSuccessorsLoading}
isDisabled={isAnchorLoading}
/>
<WrapperWithPadding>
<ActionBarMemoized
type={SurrDocType.SUCCESSORS}
defaultStepSize={defaultStepSize}
docCount={successorCount}
docCountAvailable={successors.length}
onChangeCount={onChangeCount}
isLoading={areSuccessorsLoading}
isDisabled={isAnchorLoading}
/>
</WrapperWithPadding>
</Fragment>
);
}
const WrapperWithPadding: React.FC = ({ children }) => {
const padding = useEuiPaddingSize('s');
return (
<div
css={css`
padding: 0 ${padding};
`}
>
{children}
</div>
);
};

View file

@ -50,7 +50,7 @@ export function Doc(props: DocProps) {
values: { id: props.id },
})}
</h1>
<EuiPageBody panelled paddingSize="l" panelProps={{ role: 'main' }}>
<EuiPageBody panelled paddingSize="m" panelProps={{ role: 'main' }}>
{reqState === ElasticRequestState.NotFoundDataView && (
<EuiCallOut
color="danger"

View file

@ -32,18 +32,12 @@ discover-app {
}
.dscPageContent__wrapper {
padding: $euiSizeS $euiSizeS $euiSizeS 0;
overflow: hidden; // Ensures horizontal scroll of table
@include euiBreakpoint('xs', 's') {
padding: 0 $euiSizeS $euiSizeS;
}
}
.dscPageContent {
position: relative;
overflow: hidden;
border: $euiBorderThin;
height: 100%;
}

View file

@ -12,7 +12,7 @@ import { EuiPageSidebar } from '@elastic/eui';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import type { Query, AggregateQuery } from '@kbn/es-query';
import { setHeaderActionMenuMounter } from '../../../../kibana_services';
import { DiscoverLayout, SIDEBAR_CLOSED_KEY } from './discover_layout';
import { DiscoverLayout } from './discover_layout';
import { dataViewMock, esHitsMock } from '@kbn/discover-utils/src/__mocks__';
import { savedSearchMock } from '../../../../__mocks__/saved_search';
import {
@ -31,9 +31,7 @@ import {
import { createDiscoverServicesMock } from '../../../../__mocks__/services';
import { FetchStatus } from '../../../types';
import { RequestAdapter } from '@kbn/inspector-plugin/common';
import { LocalStorageMock } from '../../../../__mocks__/local_storage_mock';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { DiscoverServices } from '../../../../build_services';
import { buildDataTableRecord } from '@kbn/discover-utils';
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
import { createSearchSessionMock } from '../../../../__mocks__/search_session';
@ -41,6 +39,9 @@ import { getSessionServiceMock } from '@kbn/data-plugin/public/search/session/mo
import { DiscoverMainProvider } from '../../services/discover_state_provider';
import { act } from 'react-dom/test-utils';
import { ErrorCallout } from '../../../../components/common/error_callout';
import * as localStorageModule from 'react-use/lib/useLocalStorage';
jest.spyOn(localStorageModule, 'default');
setHeaderActionMenuMounter(jest.fn());
@ -57,12 +58,7 @@ async function mountComponent(
}) as DataMain$
) {
const searchSourceMock = createSearchSourceMock({});
const services = {
...createDiscoverServicesMock(),
storage: new LocalStorageMock({
[SIDEBAR_CLOSED_KEY]: prevSidebarClosed,
}) as unknown as Storage,
} as unknown as DiscoverServices;
const services = createDiscoverServicesMock();
const time = { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
services.data.query.timefilter.timefilter.getTime = () => time;
(services.data.query.queryString.getDefaultQuery as jest.Mock).mockReturnValue({
@ -77,6 +73,9 @@ async function mountComponent(
(searchSourceInstanceMock.fetch$ as jest.Mock).mockImplementation(
jest.fn().mockReturnValue(of({ rawResponse: { hits: { total: 2 } } }))
);
(localStorageModule.default as jest.Mock).mockImplementation(
jest.fn(() => [prevSidebarClosed, jest.fn()])
);
const stateContainer = getDiscoverStateMock({ isTimeBased: true });

View file

@ -6,17 +6,18 @@
* Side Public License, v 1.
*/
import './discover_layout.scss';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiHideFor,
EuiPage,
EuiPageBody,
EuiPanel,
EuiSpacer,
useEuiBackgroundColor,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { METRIC_TYPE } from '@kbn/analytics';
import classNames from 'classnames';
@ -52,11 +53,6 @@ import { DiscoverHistogramLayout } from './discover_histogram_layout';
import { ErrorCallout } from '../../../../components/common/error_callout';
import { addLog } from '../../../../utils/add_log';
/**
* Local storage key for sidebar persistence state
*/
export const SIDEBAR_CLOSED_KEY = 'discover:sidebarClosed';
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
const TopNavMemoized = React.memo(DiscoverTopNav);
@ -72,11 +68,12 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
data,
uiSettings,
filterManager,
storage,
history,
spaces,
inspector,
} = useDiscoverServices();
const { euiTheme } = useEuiTheme();
const pageBackgroundColor = useEuiBackgroundColor('plain');
const globalQueryState = data.query.getState();
const { main$ } = stateContainer.dataState.data$;
const [query, savedQuery, columns, sort] = useAppStateSelector((state) => [
@ -109,8 +106,6 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
return dataView.type !== DataViewType.ROLLUP && dataView.isTimeBased();
}, [dataView]);
const initialSidebarClosed = Boolean(storage.get(SIDEBAR_CLOSED_KEY));
const [isSidebarClosed, setIsSidebarClosed] = useState(initialSidebarClosed);
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
const isPlainRecord = useMemo(() => getRawRecordType(query) === RecordRawType.PLAIN, [query]);
@ -172,11 +167,6 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
filterManager.setFilters(disabledFilters);
}, [filterManager]);
const toggleSidebarCollapse = useCallback(() => {
storage.set(SIDEBAR_CLOSED_KEY, !isSidebarClosed);
setIsSidebarClosed(!isSidebarClosed);
}, [isSidebarClosed, storage]);
const contentCentered = resultState === 'uninitialized' || resultState === 'none';
const documentState = useDataState(stateContainer.dataState.data$.documents$);
@ -240,7 +230,13 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
]);
return (
<EuiPage className="dscPage" data-fetch-counter={fetchCounter.current}>
<EuiPage
className="dscPage"
data-fetch-counter={fetchCounter.current}
css={css`
background-color: ${pageBackgroundColor};
`}
>
<h1
id="savedSearchTitle"
className="euiScreenReaderOnly"
@ -274,7 +270,7 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
spaces={spaces}
history={history}
/>
<EuiFlexGroup className="dscPageBody__contents" gutterSize="s">
<EuiFlexGroup className="dscPageBody__contents" gutterSize="none">
<EuiFlexItem grow={false}>
<SidebarMemoized
documents$={stateContainer.dataState.data$.documents$}
@ -284,7 +280,6 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
onRemoveField={onRemoveColumn}
onChangeDataView={stateContainer.actions.onChangeDataView}
selectedDataView={dataView}
isClosed={isSidebarClosed}
trackUiMetric={trackUiMetric}
onFieldEdited={onFieldEdited}
onDataViewCreated={stateContainer.actions.onDataViewCreated}
@ -292,23 +287,12 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
/>
</EuiFlexItem>
<EuiHideFor sizes={['xs', 's']}>
<EuiFlexItem grow={false}>
<div>
<EuiSpacer size="s" />
<EuiButtonIcon
iconType={isSidebarClosed ? 'menuRight' : 'menuLeft'}
iconSize="m"
size="xs"
onClick={toggleSidebarCollapse}
data-test-subj="collapseSideBarButton"
aria-controls="discover-sidebar"
aria-expanded={isSidebarClosed ? 'false' : 'true'}
aria-label={i18n.translate('discover.toggleSidebarAriaLabel', {
defaultMessage: 'Toggle sidebar',
})}
/>
</div>
</EuiFlexItem>
<EuiFlexItem
grow={false}
css={css`
border-right: ${euiTheme.border.thin};
`}
/>
</EuiHideFor>
<EuiFlexItem className="dscPageContent__wrapper">
{resultState === 'none' ? (
@ -335,7 +319,10 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
role="main"
panelRef={resizeRef}
paddingSize="none"
borderRadius="none"
hasShadow={false}
hasBorder={false}
color="transparent"
className={classNames('dscPageContent', {
'dscPageContent--centered': contentCentered,
})}

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { DragDrop, type DropType, DropOverlayWrapper } from '@kbn/dom-drag-drop';
import useObservable from 'react-use/lib/useObservable';
import React, { useCallback } from 'react';
@ -97,7 +97,6 @@ export const DiscoverMainContent = ({
data-test-subj="dscMainContent"
>
<EuiFlexItem grow={false}>
<EuiHorizontalRule margin="none" />
{!isPlainRecord && (
<DocumentViewModeToggle
viewMode={viewMode}

View file

@ -119,9 +119,9 @@ export const NoResultsSuggestions: React.FC<NoResultsSuggestionProps> = ({
return (
<EuiEmptyPrompt
layout="horizontal"
color="plain"
color="transparent"
icon={<NoResultsIllustration />}
hasBorder
hasBorder={false}
title={
<h2 data-test-subj="discoverNoResults">
<FormattedMessage

View file

@ -748,5 +748,14 @@ describe('discover responsive sidebar', function () {
expect(comp.find('[data-test-subj="custom-data-view-picker"]').exists()).toBe(true);
});
it('should allow to toggle sidebar', async function () {
const comp = await mountComponent(props);
expect(findTestSubject(comp, 'fieldList').exists()).toBe(true);
findTestSubject(comp, 'unifiedFieldListSidebar__toggle-collapse').simulate('click');
expect(findTestSubject(comp, 'fieldList').exists()).toBe(false);
findTestSubject(comp, 'unifiedFieldListSidebar__toggle-expand').simulate('click');
expect(findTestSubject(comp, 'fieldList').exists()).toBe(true);
});
});
});

View file

@ -42,7 +42,10 @@ const getCreationOptions: UnifiedFieldListSidebarContainerProps['getCreationOpti
return {
originatingApp: PLUGIN_ID,
localStorageKeyPrefix: 'discover',
compressed: true,
showSidebarToggleButton: true,
disableFieldsExistenceAutoFetching: true,
buttonAddFieldVariant: 'toolbar',
buttonPropsToTriggerFlyout: {
contentProps: {
id: DISCOVER_TOUR_STEP_ANCHOR_IDS.addFields,
@ -87,10 +90,6 @@ export interface DiscoverSidebarResponsiveProps {
* hits fetched from ES, displayed in the doc table
*/
documents$: DataDocuments$;
/**
* Has been toggled closed
*/
isClosed?: boolean;
/**
* Callback function when selecting a field
*/
@ -380,7 +379,6 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
ref={initializeUnifiedFieldListSidebarContainerApi}
variant={fieldListVariant}
getCreationOptions={getCreationOptions}
isSidebarCollapsed={props.isClosed}
services={fieldListSidebarServices}
dataView={selectedDataView}
trackUiMetric={trackUiMetric}

View file

@ -6,11 +6,10 @@
* Side Public License, v 1.
*/
import { EuiTabs, EuiTab, useEuiPaddingSize } from '@elastic/eui';
import React from 'react';
import { EuiTab, EuiTabs, useEuiPaddingSize, useEuiTheme } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { SHOW_FIELD_STATISTICS } from '@kbn/discover-utils';
import { VIEW_MODE } from '../../../common/constants';
import { useDiscoverServices } from '../../hooks/use_discover_services';
@ -22,11 +21,12 @@ export const DocumentViewModeToggle = ({
viewMode: VIEW_MODE;
setDiscoverViewMode: (viewMode: VIEW_MODE) => void;
}) => {
const { euiTheme } = useEuiTheme();
const { uiSettings } = useDiscoverServices();
const tabsCss = css`
padding: 0 ${useEuiPaddingSize('s')};
background-color: ${euiThemeVars.euiPageBackgroundColor};
border-bottom: ${viewMode === VIEW_MODE.AGGREGATED_LEVEL ? euiTheme.border.thin : 'none'};
`;
const showViewModeToggle = uiSettings.get(SHOW_FIELD_STATISTICS) ?? false;
@ -36,7 +36,7 @@ export const DocumentViewModeToggle = ({
}
return (
<EuiTabs size="s" css={tabsCss} data-test-subj="dscViewModeToggle">
<EuiTabs size="s" css={tabsCss} data-test-subj="dscViewModeToggle" bottomBorder={false}>
<EuiTab
isSelected={viewMode === VIEW_MODE.DOCUMENT_LEVEL}
onClick={() => setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL)}

View file

@ -275,7 +275,7 @@ export const UnifiedHistogramLayout = ({
chart={chart}
breakdown={breakdown}
appendHitsCounter={appendHitsCounter}
appendHistogram={showFixedPanels ? <EuiSpacer size="s" /> : <EuiSpacer size="l" />}
appendHistogram={<EuiSpacer size="s" />}
disableAutoFetching={disableAutoFetching}
disableTriggers={disableTriggers}
disabledActions={disabledActions}

View file

@ -6,12 +6,7 @@
* Side Public License, v 1.
*/
import {
EuiResizableContainer,
useEuiTheme,
useGeneratedHtmlId,
useResizeObserver,
} from '@elastic/eui';
import { EuiResizableContainer, useGeneratedHtmlId, useResizeObserver } from '@elastic/eui';
import type { ResizeTrigger } from '@elastic/eui/src/components/resizable_container/types';
import { css } from '@emotion/react';
import { isEqual, round } from 'lodash';
@ -162,12 +157,6 @@ export const PanelsResizable = ({
disableResizeWithPortalsHack();
}, [disableResizeWithPortalsHack, resizeWithPortalsHackIsResizing]);
const { euiTheme } = useEuiTheme();
const buttonCss = css`
margin-top: -${euiTheme.size.base};
margin-bottom: 0;
`;
return (
<EuiResizableContainer
className={className}
@ -189,7 +178,7 @@ export const PanelsResizable = ({
{topPanel}
</EuiResizablePanel>
<EuiResizableButton
css={[resizeWithPortalsHackButtonCss, buttonCss]}
css={resizeWithPortalsHackButtonCss}
data-test-subj="unifiedHistogramResizableButton"
/>
<EuiResizablePanel

View file

@ -15,6 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'timePicker', 'discover']);
const kibanaServer = getService('kibanaServer');
const security = getService('security');
const browser = getService('browser');
const from = 'Jan 1, 2019 @ 00:00:00.000';
const to = 'Jan 1, 2019 @ 23:59:59.999';
@ -25,7 +26,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await kibanaServer.importExport.load(
'test/functional/fixtures/kbn_archiver/date_nanos_mixed'
);
await kibanaServer.uiSettings.replace({ defaultIndex: 'timestamp-*' });
await kibanaServer.uiSettings.replace({
defaultIndex: 'timestamp-*',
hideAnnouncements: true, // should be enough vertical space to render rows
});
await browser.setWindowSize(1200, 900);
await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos_mixed']);
await PageObjects.common.setTime({ from, to });
await PageObjects.common.navigateToApp('discover');

View file

@ -88,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
return actualCount <= expectedCount;
});
const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
expect(Math.round(newDurationHours)).to.be(26);
expect(Math.round(newDurationHours)).to.be(24); // might fail if histogram's width changes
await retry.waitFor('doc table containing the documents of the brushed range', async () => {
const rowData = await PageObjects.discover.getDocTableField(1);

View file

@ -214,16 +214,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('collapse expand', function () {
it('should initially be expanded', async function () {
await testSubjects.existOrFail('discover-sidebar');
await testSubjects.existOrFail('fieldList');
});
it('should collapse when clicked', async function () {
await PageObjects.discover.toggleSidebarCollapse();
await testSubjects.missingOrFail('discover-sidebar');
await testSubjects.existOrFail('discover-sidebar');
await testSubjects.missingOrFail('fieldList');
});
it('should expand when clicked', async function () {
await PageObjects.discover.toggleSidebarCollapse();
await testSubjects.existOrFail('discover-sidebar');
await testSubjects.existOrFail('fieldList');
});
});

View file

@ -370,13 +370,14 @@ export class DiscoverPageObject extends FtrService {
}
public async toggleSidebarCollapse() {
return await this.testSubjects.click('collapseSideBarButton');
return await this.testSubjects.click('unifiedFieldListSidebar__toggle');
}
public async closeSidebar() {
await this.retry.tryForTime(2 * 1000, async () => {
await this.toggleSidebarCollapse();
await this.testSubjects.missingOrFail('discover-sidebar');
await this.testSubjects.click('unifiedFieldListSidebar__toggle-collapse');
await this.testSubjects.missingOrFail('unifiedFieldListSidebar__toggle-collapse');
await this.testSubjects.missingOrFail('fieldList');
});
}

View file

@ -2395,7 +2395,6 @@
"discover.serverLocatorExtension.titleFromLocatorUnknown": "Recherche inconnue",
"discover.singleDocRoute.errorTitle": "Une erreur s'est produite",
"discover.skipToBottomButtonLabel": "Atteindre la fin du tableau",
"discover.toggleSidebarAriaLabel": "Activer/Désactiver la barre latérale",
"discover.topNav.openSearchPanel.manageSearchesButtonLabel": "Gérer les recherches",
"discover.topNav.openSearchPanel.noSearchesFoundDescription": "Aucune recherche correspondante trouvée.",
"discover.topNav.openSearchPanel.openSearchTitle": "Ouvrir une recherche",

View file

@ -2410,7 +2410,6 @@
"discover.serverLocatorExtension.titleFromLocatorUnknown": "不明な検索",
"discover.singleDocRoute.errorTitle": "エラーが発生しました",
"discover.skipToBottomButtonLabel": "テーブルの最後に移動",
"discover.toggleSidebarAriaLabel": "サイドバーを切り替える",
"discover.topNav.openSearchPanel.manageSearchesButtonLabel": "検索の管理",
"discover.topNav.openSearchPanel.noSearchesFoundDescription": "一致する検索が見つかりませんでした。",
"discover.topNav.openSearchPanel.openSearchTitle": "検索を開く",

View file

@ -2410,7 +2410,6 @@
"discover.serverLocatorExtension.titleFromLocatorUnknown": "未知搜索",
"discover.singleDocRoute.errorTitle": "发生错误",
"discover.skipToBottomButtonLabel": "转到表尾",
"discover.toggleSidebarAriaLabel": "切换侧边栏",
"discover.topNav.openSearchPanel.manageSearchesButtonLabel": "管理搜索",
"discover.topNav.openSearchPanel.noSearchesFoundDescription": "未找到匹配的搜索。",
"discover.topNav.openSearchPanel.openSearchTitle": "打开搜索",