Skip to content

Commit

Permalink
Merge branch 'main' of github.com:elastic/kibana into assistant-kb-pa…
Browse files Browse the repository at this point in the history
…rt-deux
  • Loading branch information
spong committed Oct 3, 2023
2 parents 1768ce4 + 92f5a88 commit 9daaabc
Show file tree
Hide file tree
Showing 120 changed files with 4,741 additions and 2,740 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,23 @@
* Side Public License, v 1.
*/

import { SerializableRecord } from '@kbn/utility-types';
import deepEqual from 'fast-deep-equal';
import { SerializableRecord } from '@kbn/utility-types';

import { v4 } from 'uuid';
import { pick, omit, xor } from 'lodash';
import { ControlGroupInput } from '..';

import {
DEFAULT_CONTROL_GROW,
DEFAULT_CONTROL_STYLE,
DEFAULT_CONTROL_WIDTH,
} from './control_group_constants';
import { PersistableControlGroupInput, RawControlGroupAttributes } from './types';
import {
ControlPanelDiffSystems,
genericControlPanelDiffSystem,
} from './control_group_panel_diff_system';
import { ControlGroupInput } from '..';
import { ControlsPanels, PersistableControlGroupInput, RawControlGroupAttributes } from './types';

const safeJSONParse = <OutType>(jsonString?: string): OutType | undefined => {
if (!jsonString && typeof jsonString !== 'string') return;
Expand Down Expand Up @@ -103,6 +105,20 @@ export const controlGroupInputToRawControlGroupAttributes = (
};
};

export const generateNewControlIds = (controlGroupInput?: PersistableControlGroupInput) => {
if (!controlGroupInput?.panels) return;

const newPanelsMap: ControlsPanels = {};
for (const panel of Object.values(controlGroupInput.panels)) {
const newId = v4();
newPanelsMap[newId] = {
...panel,
explicitInput: { ...panel.explicitInput, id: newId },
};
}
return { ...controlGroupInput, panels: newPanelsMap };
};

export const rawControlGroupAttributesToControlGroupInput = (
rawControlGroupAttributes: RawControlGroupAttributes
): PersistableControlGroupInput | undefined => {
Expand Down
1 change: 1 addition & 0 deletions src/plugins/controls/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export {
serializableToRawControlGroupAttributes,
persistableControlGroupInputIsEqual,
getDefaultControlGroupInput,
generateNewControlIds,
} from './control_group/control_group_persistence';

export {
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/controls/common/options_list/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export interface OptionsListRequestBody {
allowExpensiveQueries: boolean;
sort?: OptionsListSortingType;
filters?: Array<{ bool: BoolQuery }>;
selectedOptions?: string[];
selectedOptions?: Array<string | number>;
runPastTimeout?: boolean;
searchString?: string;
fieldSpec?: FieldSpec;
Expand Down
41 changes: 41 additions & 0 deletions src/plugins/controls/public/hooks/use_field_formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 { FieldSpec } from '@kbn/data-views-plugin/common';
import { useEffect, useState } from 'react';
import { pluginServices } from '../services';

export const useFieldFormatter = ({
dataViewId,
fieldSpec,
}: {
dataViewId?: string;
fieldSpec?: FieldSpec;
}) => {
const {
dataViews: { get: getDataViewById },
} = pluginServices.getServices();
const [fieldFormatter, setFieldFormatter] = useState(() => (toFormat: string) => toFormat);

/**
* derive field formatter from fieldSpec and dataViewId
*/
useEffect(() => {
(async () => {
if (!dataViewId || !fieldSpec) return;
// dataViews are cached, and should always be available without having to hit ES.
const dataView = await getDataViewById(dataViewId);
setFieldFormatter(
() =>
dataView?.getFormatterForField(fieldSpec).getConverterFor('text') ??
((toFormat: string) => toFormat)
);
})();
}, [fieldSpec, dataViewId, getDataViewById]);

return fieldFormatter;
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@
font-weight: $euiFontWeightRegular;
}

.optionsList__filterValid {
font-weight: $euiFontWeightMedium;
}

.optionsList__filterInvalid {
color: $euiTextSubduedColor;
text-decoration: line-through;
margin-left: $euiSizeS;
font-weight: $euiFontWeightRegular;
}

.optionsList__negateLabel {
font-weight: bold;
font-weight: $euiFontWeightSemiBold;
font-size: $euiSizeM;
color: $euiColorDanger;
}
Expand All @@ -34,6 +37,7 @@
width: 0;
}
}

}

.optionsList--sortPopover {
Expand All @@ -42,24 +46,22 @@

.optionsList__existsFilter {
font-style: italic;
font-weight: $euiFontWeightMedium;
}

.optionsList__popoverOverride {
@include euiBottomShadowMedium;
filter: none; // overwrite the default popover shadow
transform: translateY(-$euiSizeS) translateX(0); // prevent "slide in" animation on open/close
}

.optionsList__popover {
.optionsList__actions {
padding: $euiSizeS;
padding-bottom: 0;
padding: 0 $euiSizeS;
border-bottom: $euiBorderThin;
border-color: darken($euiColorLightestShade, 2%);

.optionsList__sortButton {
box-shadow: inset 0 0 0 $euiBorderWidthThin $euiFormBorderColor;
background-color: $euiFormBackgroundColor;
.optionsList__searchRow {
padding-top: $euiSizeS
}

.optionsList__actionsRow {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useOptionsList } from '../embeddable/options_list_embeddable';
import './options_list.scss';
import { ControlError } from '../../control_group/component/control_error_component';
import { MIN_POPOVER_WIDTH } from '../../constants';
import { useFieldFormatter } from '../../hooks/use_field_formatter';

export const OptionsListControl = ({
typeaheadSubject,
Expand All @@ -35,6 +36,7 @@ export const OptionsListControl = ({
const isPopoverOpen = optionsList.select((state) => state.componentState.popoverOpen);
const validSelections = optionsList.select((state) => state.componentState.validSelections);
const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
const fieldSpec = optionsList.select((state) => state.componentState.field);

const id = optionsList.select((state) => state.explicitInput.id);
const exclude = optionsList.select((state) => state.explicitInput.exclude);
Expand All @@ -46,6 +48,8 @@ export const OptionsListControl = ({
const selectedOptions = optionsList.select((state) => state.explicitInput.selectedOptions);

const loading = optionsList.select((state) => state.output.loading);
const dataViewId = optionsList.select((state) => state.output.dataViewId);
const fieldFormatter = useFieldFormatter({ dataViewId, fieldSpec });

useEffect(() => {
return () => {
Expand Down Expand Up @@ -87,6 +91,8 @@ export const OptionsListControl = ({
);

const { hasSelections, selectionDisplayNode, validSelectionsCount } = useMemo(() => {
const delimiter = OptionsListStrings.control.getSeparator(fieldSpec?.type);

return {
hasSelections: !isEmpty(validSelections) || !isEmpty(invalidSelections),
validSelectionsCount: validSelections?.length,
Expand All @@ -107,20 +113,30 @@ export const OptionsListControl = ({
</span>
) : (
<>
{validSelections && (
<span>{validSelections.join(OptionsListStrings.control.getSeparator())}</span>
)}
{invalidSelections && (
{validSelections?.length ? (
<span className="optionsList__filterValid">
{validSelections.map((value) => fieldFormatter(value)).join(delimiter)}
</span>
) : null}
{validSelections?.length && invalidSelections?.length ? delimiter : null}
{invalidSelections?.length ? (
<span className="optionsList__filterInvalid">
{invalidSelections.join(OptionsListStrings.control.getSeparator())}
{invalidSelections.map((value) => fieldFormatter(value)).join(delimiter)}
</span>
)}
) : null}
</>
)}
</>
),
};
}, [exclude, existsSelected, validSelections, invalidSelections]);
}, [
exclude,
existsSelected,
validSelections,
invalidSelections,
fieldFormatter,
fieldSpec?.type,
]);

const button = (
<EuiFilterButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export const OptionsListEditorOptions = ({
</EuiFormRow>
) : (
allowExpensiveQueries &&
fieldType !== 'ip' && (
!['ip', 'date'].includes(fieldType) && (
<EuiFormRow
label={OptionsListStrings.editor.getSearchOptionsTitle()}
data-test-subj="optionsListControl__searchOptionsRadioGroup"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,18 @@ describe('Options list popover', () => {
expect(optionsText).toEqual(['By document count. Checked option.']);
});

test('when sorting suggestions, show "By date" sorting option for date fields', async () => {
const popover = await mountComponent({
componentState: { field: { name: 'Test date field', type: 'date' } as FieldSpec },
});
const sortButton = findTestSubject(popover, 'optionsListControl__sortingOptionsButton');
sortButton.simulate('click');

const sortingOptionsDiv = findTestSubject(popover, 'optionsListControl__sortingOptions');
const optionsText = sortingOptionsDiv.find('ul li').map((element) => element.text().trim());
expect(optionsText).toEqual(['By document count. Checked option.', 'By date']);
});

test('ensure warning icon does not show up when testAllowExpensiveQueries = true/undefined', async () => {
const popover = await mountComponent({
componentState: { field: { name: 'Test keyword field', type: 'keyword' } as FieldSpec },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const OptionsListPopoverActionBar = ({
const totalCardinality =
optionsList.select((state) => state.componentState.totalCardinality) ?? 0;
const searchString = optionsList.select((state) => state.componentState.searchString);
const fieldSpec = optionsList.select((state) => state.componentState.field);
const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);

const hideSort = optionsList.select((state) => state.explicitInput.hideSort);
Expand All @@ -50,29 +51,22 @@ export const OptionsListPopoverActionBar = ({

return (
<div className="optionsList__actions">
<EuiFormRow fullWidth>
<EuiFlexGroup className="optionsList__searchSortRow" gutterSize="xs" responsive={false}>
<EuiFlexItem grow={true}>
<EuiFieldSearch
isInvalid={!searchString.valid}
compressed
disabled={showOnlySelected}
fullWidth
onChange={(event) => updateSearchString(event.target.value)}
value={searchString.value}
data-test-subj="optionsList-control-search-input"
placeholder={OptionsListStrings.popover.searchPlaceholder[
searchTechnique ?? OPTIONS_LIST_DEFAULT_SEARCH_TECHNIQUE
].getPlaceholderText()}
/>
</EuiFlexItem>
{!hideSort && (
<EuiFlexItem className="optionsList__sortButtonWrapper" grow={false}>
<OptionsListPopoverSortingButton showOnlySelected={showOnlySelected} />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFormRow>
{fieldSpec?.type !== 'date' && (
<EuiFormRow className="optionsList__searchRow" fullWidth>
<EuiFieldSearch
isInvalid={!searchString.valid}
compressed
disabled={showOnlySelected}
fullWidth
onChange={(event) => updateSearchString(event.target.value)}
value={searchString.value}
data-test-subj="optionsList-control-search-input"
placeholder={OptionsListStrings.popover.searchPlaceholder[
searchTechnique ?? OPTIONS_LIST_DEFAULT_SEARCH_TECHNIQUE
].getPlaceholderText()}
/>
</EuiFormRow>
)}
<EuiFormRow className="optionsList__actionsRow" fullWidth>
<EuiFlexGroup
justifyContent="spaceBetween"
Expand Down Expand Up @@ -133,6 +127,11 @@ export const OptionsListPopoverActionBar = ({
/>
</EuiToolTip>
</EuiFlexItem>
{!hideSort && (
<EuiFlexItem grow={false}>
<OptionsListPopoverSortingButton showOnlySelected={showOnlySelected} />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,26 @@ import {

import { OptionsListStrings } from './options_list_strings';
import { useOptionsList } from '../embeddable/options_list_embeddable';
import { useFieldFormatter } from '../../hooks/use_field_formatter';

export const OptionsListPopoverInvalidSelections = () => {
const optionsList = useOptionsList();

const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
const fieldName = optionsList.select((state) => state.explicitInput.fieldName);

const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
const fieldSpec = optionsList.select((state) => state.componentState.field);

const dataViewId = optionsList.select((state) => state.output.dataViewId);
const fieldFormatter = useFieldFormatter({ dataViewId, fieldSpec });

const [selectableOptions, setSelectableOptions] = useState<EuiSelectableOption[]>([]); // will be set in following useEffect
useEffect(() => {
/* This useEffect makes selectableOptions responsive to unchecking options */
const options: EuiSelectableOption[] = (invalidSelections ?? []).map((key) => {
return {
key,
label: key,
label: fieldFormatter(key),
checked: 'on',
className: 'optionsList__selectionInvalid',
'data-test-subj': `optionsList-control-ignored-selection-${key}`,
Expand All @@ -46,7 +52,7 @@ export const OptionsListPopoverInvalidSelections = () => {
};
});
setSelectableOptions(options);
}, [invalidSelections]);
}, [fieldFormatter, invalidSelections]);

return (
<>
Expand Down
Loading

0 comments on commit 9daaabc

Please sign in to comment.