Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
042d15a
Do not ignore invalid selections
nickpeihl Jan 3, 2024
3c14316
All selections use same font weight
nickpeihl Jan 3, 2024
2f6df87
Do not ignore invalid values in range slider
nickpeihl Jan 5, 2024
29599ff
Run validations after control has fully loaded
nickpeihl Jan 30, 2024
017e728
Update tests
nickpeihl Jan 30, 2024
f6c3661
Merge branch 'main' of https://github.com/elastic/kibana into control…
nickpeihl Jan 30, 2024
9146dc2
Remove commented code
nickpeihl Jan 30, 2024
f778f55
Warn user about invalid selections no longer ignored
nickpeihl Jan 31, 2024
1f180b2
Fix range slider subscriptions
nickpeihl Jan 31, 2024
eb18331
Merge remote-tracking branch 'upstream/main' into controls-highlight-…
nickpeihl Jan 31, 2024
56f5d26
Add stub for storage service
nickpeihl Jan 31, 2024
bbca6c8
Suggestion
Heenawter Jan 31, 2024
ec93377
Slightly cleaner implementation
Heenawter Feb 1, 2024
9d6790b
Buggy version
Heenawter Feb 1, 2024
03fa8d3
Better version
Heenawter Feb 2, 2024
b8b9a37
Clean up + add to range slider
Heenawter Feb 2, 2024
d19a28b
Give example of custom text + clean up
Heenawter Feb 2, 2024
8173e16
Use toast instead of tour
nickpeihl Feb 9, 2024
779acf5
Merge pull request #3 from Heenawter/try-to-clean-up-tour-stuff
nickpeihl Feb 12, 2024
811fd64
Merge remote-tracking branch 'upstream/main' into controls-highlight-…
nickpeihl Feb 12, 2024
db0cc43
Match range slider and options list font weights
nickpeihl Feb 12, 2024
213b161
Fix story service
nickpeihl Feb 12, 2024
016ccdd
First attempt to add eui token
nickpeihl Feb 12, 2024
ae917a3
Fix positioning of invalid token
Heenawter Feb 12, 2024
886d5b3
Fix truncation
Heenawter Feb 12, 2024
430d5c7
Add invalid token to range slider
Heenawter Feb 13, 2024
72543a3
Wrap tokens in tooltip
nickpeihl Feb 13, 2024
3b83205
lint
nickpeihl Feb 13, 2024
373479d
Don't show icons for invalid selections in popover menu
nickpeihl Feb 13, 2024
5628911
Fix test with uncleared toast
nickpeihl Feb 13, 2024
134bdaf
Fix toast collision in functional test
nickpeihl Feb 13, 2024
fb2ab34
Address review feedback
nickpeihl Feb 13, 2024
b329226
Simplify invalid selections logic
nickpeihl Feb 13, 2024
be3d10f
Move setting loading state outside of runRangeSliderQuery method
nickpeihl Feb 14, 2024
fe3478f
Merge branch 'main' into controls-highlight-invalid
nickpeihl Feb 14, 2024
c98f322
Remove invalid function
nickpeihl Feb 14, 2024
23fea13
Merge remote-tracking branch 'refs/remotes/origin/controls-highlight-…
nickpeihl Feb 14, 2024
951d463
Add warning icon to invalid selections list group label
nickpeihl Feb 15, 2024
243ddd9
Merge branch 'main' into controls-highlight-invalid
Heenawter Feb 20, 2024
0458fe7
Fix failing test
Heenawter Feb 20, 2024
5a56610
Switch to warning toast + fix text
Heenawter Feb 20, 2024
f1ad19c
Switch back to EuiTour
Heenawter Feb 20, 2024
3237fe2
Add custom range slider text
Heenawter Feb 20, 2024
b5b8c29
Switch invalid selections popover title design
Heenawter Feb 20, 2024
7b9aea6
Fix failing test
Heenawter Feb 21, 2024
b5bf9da
Change body of tour step
Heenawter Feb 21, 2024
15cbff4
Fix copy
Heenawter Feb 21, 2024
6fd9848
Another copy change
Heenawter Feb 22, 2024
d8530a0
Merge branch 'main' into controls-highlight-invalid
Heenawter Feb 22, 2024
3484856
Make options list text match range slider
Heenawter Feb 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/plugins/controls/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"unifiedSearch",
"uiActions"
],
"extraPublicDirs": ["common"]
"extraPublicDirs": ["common"],
"requiredBundles": ["kibanaUtils"]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,44 @@

import '../control_group.scss';

import {
arrayMove,
SortableContext,
rectSortingStrategy,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import classNames from 'classnames';
import React, { useEffect, useMemo, useState } from 'react';
import { TypedUseSelectorHook, useSelector } from 'react-redux';

import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
KeyboardSensor,
LayoutMeasuringStrategy,
PointerSensor,
useSensor,
useSensors,
LayoutMeasuringStrategy,
} from '@dnd-kit/core';
import classNames from 'classnames';
import React, { useMemo, useState } from 'react';
import { TypedUseSelectorHook, useSelector } from 'react-redux';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';

import {
arrayMove,
rectSortingStrategy,
SortableContext,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPanel,
EuiText,
EuiTourStep,
} from '@elastic/eui';
import { ViewMode } from '@kbn/embeddable-plugin/public';

import { ControlGroupReduxState } from '../types';
import { ControlGroupStrings } from '../control_group_strings';
import { ControlClone, SortableControl } from './control_group_sortable_item';
import { useControlGroupContainer } from '../embeddable/control_group_container';
import { ControlGroupReduxState } from '../types';
import { ControlClone, SortableControl } from './control_group_sortable_item';

const contextSelect = useSelector as TypedUseSelectorHook<ControlGroupReduxState>;

Expand All @@ -47,6 +57,12 @@ export const ControlGroup = () => {
const viewMode = contextSelect((state) => state.explicitInput.viewMode);
const controlStyle = contextSelect((state) => state.explicitInput.controlStyle);
const showAddButton = contextSelect((state) => state.componentState.showAddButton);
const controlWithInvalidSelectionsId = contextSelect(
(state) => state.componentState.controlWithInvalidSelectionsId
);
const [tourStepOpen, setTourStepOpen] = useState<boolean>(true);
const [suppressTourChecked, setSuppressTourChecked] = useState<boolean>(false);
const [renderTourStep, setRenderTourStep] = useState(false);

const isEditable = viewMode === ViewMode.EDIT;

Expand All @@ -61,6 +77,87 @@ export const ControlGroup = () => {
[panels]
);

useEffect(() => {
/**
* This forces the tour step to get unmounted so that it can attach to the new invalid
* control - otherwise, the anchor will remain attached to the old invalid control
*/
setRenderTourStep(false);
setTimeout(() => setRenderTourStep(true), 100);
}, [controlWithInvalidSelectionsId]);

const tourStep = useMemo(() => {
if (
!renderTourStep ||
!controlGroup.canShowInvalidSelectionsWarning() ||
!tourStepOpen ||
!controlWithInvalidSelectionsId
) {
return null;
}
const invalidControlType = panels[controlWithInvalidSelectionsId].type;

return (
<EuiTourStep
step={1}
stepsTotal={1}
minWidth={300}
maxWidth={300}
display="block"
isStepOpen={true}
repositionOnScroll
onFinish={() => {}}
panelPaddingSize="m"
anchorPosition="downCenter"
panelClassName="controlGroup--invalidSelectionsTour"
anchor={`#controlFrame--${controlWithInvalidSelectionsId}`}
title={
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="warning" color="warning" />
</EuiFlexItem>
<EuiFlexItem>{ControlGroupStrings.invalidControlWarning.getTourTitle()}</EuiFlexItem>
</EuiFlexGroup>
}
content={ControlGroupStrings.invalidControlWarning.getTourContent(invalidControlType)}
footerAction={[
<EuiCheckbox
compressed
checked={suppressTourChecked}
id={'controlGroup--suppressTourCheckbox'}
className="controlGroup--suppressTourCheckbox"
onChange={(e) => setSuppressTourChecked(e.target.checked)}
label={
<EuiText size="xs" className="controlGroup--suppressTourCheckboxLabel">
{ControlGroupStrings.invalidControlWarning.getSuppressTourLabel()}
</EuiText>
}
/>,
<EuiButtonEmpty
size="xs"
flush="right"
color="text"
onClick={() => {
setTourStepOpen(false);
if (suppressTourChecked) {
controlGroup.suppressInvalidSelectionsWarning();
}
}}
>
{ControlGroupStrings.invalidControlWarning.getDismissButton()}
</EuiButtonEmpty>,
]}
/>
);
}, [
panels,
controlGroup,
tourStepOpen,
renderTourStep,
suppressTourChecked,
controlWithInvalidSelectionsId,
]);

const [draggingId, setDraggingId] = useState<string | null>(null);
const draggingIndex = useMemo(
() => (draggingId ? idsInOrder.indexOf(draggingId) : -1),
Expand Down Expand Up @@ -117,6 +214,7 @@ export const ControlGroup = () => {
alignItems="center"
data-test-subj="controls-group"
>
{tourStep}
<EuiFlexItem>
<DndContext
onDragStart={({ active }) => setDraggingId(active.id)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,12 @@ $controlMinWidth: $euiSize * 14;
top: (-$euiSizeXS) !important;
}
}

.controlGroup--invalidSelectionsTour {
.controlGroup--suppressTourCheckbox {
height: 22px;
&Label {
font-weight: $euiFontWeightMedium;
}
}
}
39 changes: 37 additions & 2 deletions src/plugins/controls/public/control_group/control_group_strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,42 @@ import { i18n } from '@kbn/i18n';
import { RANGE_SLIDER_CONTROL } from '../range_slider';

export const ControlGroupStrings = {
invalidControlWarning: {
getTourTitle: () =>
i18n.translate('controls.controlGroup.invalidControlWarning.tourStepTitle.default', {
defaultMessage: 'Invalid selections are no longer ignored',
}),
getTourContent: (controlType: string) => {
switch (controlType) {
case RANGE_SLIDER_CONTROL: {
return i18n.translate(
'controls.controlGroup.invalidControlWarning.tourStepContent.rangeSlider',
{
defaultMessage: 'The selected range is returning no results. Try changing the range.',
}
);
}
default: {
return i18n.translate(
'controls.controlGroup.invalidControlWarning.tourStepContent.default',
{
defaultMessage:
'Some selections are returning no results. Try changing the selections.',
}
);
}
}
},

getDismissButton: () =>
i18n.translate('controls.controlGroup.invalidControlWarning.dismissButtonLabel', {
defaultMessage: 'Dismiss',
}),
getSuppressTourLabel: () =>
i18n.translate('controls.controlGroup.invalidControlWarning.suppressTourLabel', {
defaultMessage: "Don't show again",
}),
},
manageControl: {
getFlyoutCreateTitle: () =>
i18n.translate('controls.controlGroup.manageControl.createFlyoutTitle', {
Expand Down Expand Up @@ -258,8 +294,7 @@ export const ControlGroupStrings = {
}),
getValidateSelectionsSubTitle: () =>
i18n.translate('controls.controlGroup.management.validate.subtitle', {
defaultMessage:
'Automatically ignore any control selection that would result in no data.',
defaultMessage: 'Highlight control selections that result in no data.',
}),
},
controlChaining: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { compareFilters, COMPARE_ALL_OPTIONS, Filter, uniqFilters } from '@kbn/es-query';
import { isEqual, pick } from 'lodash';
import React, { createContext, useContext } from 'react';
Expand All @@ -24,6 +25,7 @@ import {
persistableControlGroupInputKeys,
} from '../../../common';
import { pluginServices } from '../../services';
import { ControlsStorageService } from '../../services/storage/types';
import { ControlEmbeddable, ControlInput, ControlOutput } from '../../types';
import { ControlGroup } from '../component/control_group_component';
import { openAddDataControlFlyout } from '../editor/open_add_data_control_flyout';
Expand Down Expand Up @@ -86,11 +88,15 @@ export class ControlGroupContainer extends Container<

private initialized$ = new BehaviorSubject(false);

private storageService: ControlsStorageService;

private subscriptions: Subscription = new Subscription();
private domNode?: HTMLElement;
private recalculateFilters$: Subject<null>;
private relevantDataViewId?: string;
private lastUsedDataViewId?: string;
private invalidSelectionsState: { [childId: string]: boolean };

public diffingSubscription: Subscription = new Subscription();

// state management
Expand Down Expand Up @@ -126,6 +132,8 @@ export class ControlGroupContainer extends Container<
ControlGroupChainingSystems[initialInput.chainingSystem]?.getContainerSettings(initialInput)
);

({ storage: this.storageService } = pluginServices.getServices());

this.recalculateFilters$ = new Subject();
this.onFiltersPublished$ = new Subject<Filter[]>();
this.onControlRemoved$ = new Subject<string>();
Expand Down Expand Up @@ -153,6 +161,10 @@ export class ControlGroupContainer extends Container<

this.store = reduxEmbeddableTools.store;

this.invalidSelectionsState = this.getChildIds().reduce((prev, id) => {
return { ...prev, [id]: false };
}, {});

// when all children are ready setup subscriptions
this.untilAllChildrenReady().then(() => {
this.recalculateDataViews();
Expand All @@ -164,6 +176,32 @@ export class ControlGroupContainer extends Container<
this.fieldFilterPredicate = fieldFilterPredicate;
}

public canShowInvalidSelectionsWarning = () =>
this.storageService.getShowInvalidSelectionWarning() ?? true;

public suppressInvalidSelectionsWarning = () => {
this.storageService.setShowInvalidSelectionWarning(false);
};

public reportInvalidSelections = ({
id,
hasInvalidSelections,
}: {
id: string;
hasInvalidSelections: boolean;
}) => {
this.invalidSelectionsState = { ...this.invalidSelectionsState, [id]: hasInvalidSelections };

const childrenWithInvalidSelections = cachedChildEmbeddableOrder(
this.getInput().panels
).idsInOrder.filter((childId) => {
return this.invalidSelectionsState[childId];
});
this.dispatch.setControlWithInvalidSelectionsId(
childrenWithInvalidSelections.length > 0 ? childrenWithInvalidSelections[0] : undefined
);
};

private setupSubscriptions = () => {
/**
* refresh control order cache and make all panels refreshInputFromParent whenever panel orders change
Expand Down Expand Up @@ -201,7 +239,9 @@ export class ControlGroupContainer extends Container<
* debounce output recalculation
*/
this.subscriptions.add(
this.recalculateFilters$.pipe(debounceTime(10)).subscribe(() => this.recalculateFilters())
this.recalculateFilters$.pipe(debounceTime(10)).subscribe(() => {
this.recalculateFilters();
})
);
};

Expand All @@ -211,9 +251,14 @@ export class ControlGroupContainer extends Container<
} = this.getState();
if (!persistableControlGroupInputIsEqual(this.getPersistableInput(), lastSavedInput)) {
this.updateInput(lastSavedInput);
this.reload(); // this forces the children to update their inputs + perform validation as necessary
}
}

public reload() {
super.reload();
}

public getPersistableInput: () => PersistableControlGroupInput & { id: string } = () => {
const input = this.getInput();
return pick(input, [...persistableControlGroupInputKeys, 'id']);
Expand Down Expand Up @@ -284,13 +329,14 @@ export class ControlGroupContainer extends Container<
private recalculateFilters = () => {
const allFilters: Filter[] = [];
let timeslice;
Object.values(this.children).map((child) => {
Object.values(this.children).map((child: ControlEmbeddable) => {
const childOutput = child.getOutput() as ControlOutput;
allFilters.push(...(childOutput?.filters ?? []));
if (childOutput.timeslice) {
timeslice = childOutput.timeslice;
}
});

// if filters are different, publish them
if (
!compareFilters(this.output.filters ?? [], allFilters ?? [], COMPARE_ALL_OPTIONS) ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export const controlGroupReducers = {
) => {
state.componentState.lastSavedInput = action.payload;
},
setControlWithInvalidSelectionsId: (
state: WritableDraft<ControlGroupReduxState>,
action: PayloadAction<ControlGroupComponentState['controlWithInvalidSelectionsId']>
) => {
state.componentState.controlWithInvalidSelectionsId = action.payload;
},
setControlStyle: (
state: WritableDraft<ControlGroupReduxState>,
action: PayloadAction<ControlGroupInput['controlStyle']>
Expand Down
1 change: 1 addition & 0 deletions src/plugins/controls/public/control_group/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface ControlGroupSettings {

export type ControlGroupComponentState = ControlGroupSettings & {
lastSavedInput: PersistableControlGroupInput;
controlWithInvalidSelectionsId?: string;
};

export {
Expand Down
Loading