Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,34 @@

import '../control_group.scss';

import {
arrayMove,
SortableContext,
rectSortingStrategy,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
KeyboardSensor,
LayoutMeasuringStrategy,
PointerSensor,
useSensor,
useSensors,
LayoutMeasuringStrategy,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import classNames from 'classnames';
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { TypedUseSelectorHook, useSelector } from 'react-redux';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel } 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 +47,15 @@ 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 controlsHaveInvalidSelections = contextSelect(
(state) => state.componentState.controlsHaveInvalidSelections
);

useEffect(() => {
if (controlsHaveInvalidSelections) {
controlGroup.showInvalidSelectionsToast();
}
}, [controlGroup, controlsHaveInvalidSelections]);

const isEditable = viewMode === ViewMode.EDIT;

Expand Down
15 changes: 15 additions & 0 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,21 @@ import { i18n } from '@kbn/i18n';
import { RANGE_SLIDER_CONTROL } from '../range_slider';

export const ControlGroupStrings = {
invalidControlWarning: {
title: i18n.translate('controls.controlGroup.invalidControlWarning.title', {
defaultMessage: 'Selections are returning no results',
}),
text: i18n.translate('controls.controlGroup.invalidControlWarning.text', {
defaultMessage:
'Some control selections are returning no results. Remove them for complete results.',
}),
dismissButtonLabel: i18n.translate(
'controls.controlGroup.invalidControlWarning.dismissButtonLabel',
{
defaultMessage: 'Do not show again',
}
),
},
manageControl: {
getFlyoutCreateTitle: () =>
i18n.translate('controls.controlGroup.manageControl.createFlyoutTitle', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { compareFilters, COMPARE_ALL_OPTIONS, Filter, uniqFilters } from '@kbn/es-query';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { isEqual, pick } from 'lodash';
import React, { createContext, useContext } from 'react';
import ReactDOM from 'react-dom';
import { Provider, TypedUseSelectorHook, useSelector } from 'react-redux';
import { BehaviorSubject, merge, Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, skip } from 'rxjs/operators';

import { OverlayRef } from '@kbn/core/public';
import { OverlayRef, Toast } from '@kbn/core/public';
import { Container, EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
Expand Down Expand Up @@ -55,6 +58,8 @@ import {
controlOrdersAreEqual,
} from './control_group_chaining_system';
import { getNextPanelOrder } from './control_group_helpers';
import { ControlGroupStrings } from '../control_group_strings';
import { ControlsStorageService } from '../../services/storage/types';

let flyoutRef: OverlayRef | undefined;
export const setFlyoutRef = (newRef: OverlayRef | undefined) => {
Expand Down Expand Up @@ -86,11 +91,16 @@ 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 };
private invalidSelectionsToast?: Toast;

public diffingSubscription: Subscription = new Subscription();

// state management
Expand Down Expand Up @@ -126,6 +136,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 +165,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 +180,69 @@ export class ControlGroupContainer extends Container<
this.fieldFilterPredicate = fieldFilterPredicate;
}

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

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

public showInvalidSelectionsToast = () => {
if (!this.canShowInvalidSelectionsWarning()) return;
const {
core: { notifications, theme, i18n },
} = pluginServices.getServices();

// remove any existing toasts to avoid a toast storm
if (this.invalidSelectionsToast) {
notifications.toasts.remove(this.invalidSelectionsToast);
}

this.invalidSelectionsToast = notifications.toasts.add({
title: ControlGroupStrings.invalidControlWarning.title,
text: toMountPoint(
<>
<p>{ControlGroupStrings.invalidControlWarning.text}</p>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
color="text"
onClick={() => {
this.supressInvalidSelectionsWarning();
if (this.invalidSelectionsToast) {
notifications.toasts.remove(this.invalidSelectionsToast);
}
}}
>
{ControlGroupStrings.invalidControlWarning.dismissButtonLabel}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</>,
{ theme, i18n }
),
});
};

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.setControlsHaveInvalidSelections(childrenWithInvalidSelections.length > 0);
};

private setupSubscriptions = () => {
/**
* refresh control order cache and make all panels refreshInputFromParent whenever panel orders change
Expand Down Expand Up @@ -201,7 +280,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 +292,16 @@ 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() {
// reset invalid selections state on reload
this.dispatch.setControlsHaveInvalidSelections(undefined);
super.reload();
}

public getPersistableInput: () => PersistableControlGroupInput & { id: string } = () => {
const input = this.getInput();
return pick(input, [...persistableControlGroupInputKeys, 'id']);
Expand Down Expand Up @@ -284,13 +372,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;
},
setControlsHaveInvalidSelections: (
state: WritableDraft<ControlGroupReduxState>,
action: PayloadAction<ControlGroupComponentState['controlsHaveInvalidSelections']>
) => {
state.componentState.controlsHaveInvalidSelections = action.payload;
},
setControlStyle: (
state: WritableDraft<ControlGroupReduxState>,
action: PayloadAction<ControlGroupInput['controlStyle']>
Expand Down
2 changes: 2 additions & 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,8 @@ export interface ControlGroupSettings {

export type ControlGroupComponentState = ControlGroupSettings & {
lastSavedInput: PersistableControlGroupInput;
invalidSelectionsControlId?: string;
controlsHaveInvalidSelections?: boolean;
};

export {
Expand Down
Loading