Skip to content

Commit

Permalink
[8.x] feat: [Dashboards] Hide clear control button in floating action…
Browse files Browse the repository at this point in the history
…s when no filters are selected (#200177) (#201927)

# Backport

This will backport the following commits from `main` to `8.x`:
- [feat: [Dashboards] Hide clear control button in floating actions when
no filters are selected
(#200177)](#200177)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Paulina
Shakirova","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-11-26T19:36:12Z","message":"feat:
[Dashboards] Hide clear control button in floating actions when no
filters are selected (#200177)\n\n## Summary\nThis PR resolves the issue
[[Controls] Show clear option only when\nvalues have
been\nselected](https://github.com/elastic/kibana/issues/192619).\n\nPreviously,
the clear button would be visible at all times, regardless\nof whether
any filters were applied.\n\n![Screenshot 2024-11-19 at 09
33\n21](https://github.com/user-attachments/assets/6122225b-8497-4bef-953e-efbfc70b4276)\n\nWith
this PR, the clear control button will only be visible if there are\nany
filters applied and will dynamically disappear when the user
clears\ntheir filters.\n\n![Screenshot 2024-11-19 at 09
37\n35](https://github.com/user-attachments/assets/2f4c8d0e-85f6-47d9-affb-590a5812f16f)\n\n---------\n\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"95ef95c9f23f8d0d4ed8db3f3b91786622ac2935","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Presentation","v9.0.0","backport:prev-minor","papercut","v8.17.0"],"number":200177,"url":"https://github.com/elastic/kibana/pull/200177","mergeCommit":{"message":"feat:
[Dashboards] Hide clear control button in floating actions when no
filters are selected (#200177)\n\n## Summary\nThis PR resolves the issue
[[Controls] Show clear option only when\nvalues have
been\nselected](https://github.com/elastic/kibana/issues/192619).\n\nPreviously,
the clear button would be visible at all times, regardless\nof whether
any filters were applied.\n\n![Screenshot 2024-11-19 at 09
33\n21](https://github.com/user-attachments/assets/6122225b-8497-4bef-953e-efbfc70b4276)\n\nWith
this PR, the clear control button will only be visible if there are\nany
filters applied and will dynamically disappear when the user
clears\ntheir filters.\n\n![Screenshot 2024-11-19 at 09
37\n35](https://github.com/user-attachments/assets/2f4c8d0e-85f6-47d9-affb-590a5812f16f)\n\n---------\n\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"95ef95c9f23f8d0d4ed8db3f3b91786622ac2935"}},"sourceBranch":"main","suggestedTargetBranches":["8.17"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/200177","number":200177,"mergeCommit":{"message":"feat:
[Dashboards] Hide clear control button in floating actions when no
filters are selected (#200177)\n\n## Summary\nThis PR resolves the issue
[[Controls] Show clear option only when\nvalues have
been\nselected](https://github.com/elastic/kibana/issues/192619).\n\nPreviously,
the clear button would be visible at all times, regardless\nof whether
any filters were applied.\n\n![Screenshot 2024-11-19 at 09
33\n21](https://github.com/user-attachments/assets/6122225b-8497-4bef-953e-efbfc70b4276)\n\nWith
this PR, the clear control button will only be visible if there are\nany
filters applied and will dynamically disappear when the user
clears\ntheir filters.\n\n![Screenshot 2024-11-19 at 09
37\n35](https://github.com/user-attachments/assets/2f4c8d0e-85f6-47d9-affb-590a5812f16f)\n\n---------\n\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"95ef95c9f23f8d0d4ed8db3f3b91786622ac2935"}},{"branch":"8.17","label":"v8.17.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
  • Loading branch information
paulinashakirova authored Nov 27, 2024
1 parent f7a222f commit 242981d
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 37 deletions.
71 changes: 71 additions & 0 deletions src/plugins/controls/public/actions/clear_control_action.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { BehaviorSubject } from 'rxjs';
import { getMockedControlGroupApi } from '../controls/mocks/control_mocks';
import { ClearControlAction } from './clear_control_action';

import type { ViewMode } from '@kbn/presentation-publishing';

const dashboardApi = {
viewMode: new BehaviorSubject<ViewMode>('view'),
};
const controlGroupApi = getMockedControlGroupApi(dashboardApi, {
removePanel: jest.fn(),
replacePanel: jest.fn(),
addNewPanel: jest.fn(),
children$: new BehaviorSubject({}),
});

const clearControlAction = new ClearControlAction();
const hasSelections$ = new BehaviorSubject<boolean | undefined>(undefined);

const controlApi = {
type: 'test',
uuid: '1',
parentApi: controlGroupApi,
hasSelections$,
clearSelections: jest.fn(),
};
beforeEach(() => {
hasSelections$.next(false);
});

describe('ClearControlAction', () => {
test('should throw an error when called with an embeddable not in a parent', async () => {
const noParentApi = { ...controlApi, parentApi: undefined };

await expect(async () => {
await clearControlAction.execute({ embeddable: noParentApi });
}).rejects.toThrow(Error);
});

test('should call onChange when isCompatible changes', () => {
const onChange = jest.fn();

hasSelections$.next(true);
clearControlAction.subscribeToCompatibilityChanges({ embeddable: controlApi }, onChange);

expect(onChange).toHaveBeenCalledWith(true, clearControlAction);
});

describe('Clear control button compatibility', () => {
test('should be incompatible if there is no selection', async () => {
const nothingIsSelected = { ...controlApi, hasSelections$: false };

expect(await clearControlAction.isCompatible({ embeddable: nothingIsSelected })).toBe(false);
});

test('should be compatible if there is a selection', async () => {
const hasSelections = { ...controlApi, hasSelections$: true };

expect(await clearControlAction.isCompatible({ embeddable: hasSelections })).toBe(true);
});
});
});
26 changes: 24 additions & 2 deletions src/plugins/controls/public/actions/clear_control_action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ import React, { SyntheticEvent } from 'react';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { EmbeddableApiContext, HasUniqueId } from '@kbn/presentation-publishing';
import { IncompatibleActionError, type Action } from '@kbn/ui-actions-plugin/public';
import {
IncompatibleActionError,
FrequentCompatibilityChangeAction,
type Action,
} from '@kbn/ui-actions-plugin/public';
import { isClearableControl } from '../types';

import { ACTION_CLEAR_CONTROL } from '.';

export class ClearControlAction implements Action<EmbeddableApiContext> {
export class ClearControlAction
implements Action<EmbeddableApiContext>, FrequentCompatibilityChangeAction<EmbeddableApiContext>
{
public readonly type = ACTION_CLEAR_CONTROL;
public readonly id = ACTION_CLEAR_CONTROL;
public order = 1;
Expand Down Expand Up @@ -50,6 +57,21 @@ export class ClearControlAction implements Action<EmbeddableApiContext> {
return 'eraser';
}

public couldBecomeCompatible({ embeddable }: EmbeddableApiContext) {
return isClearableControl(embeddable);
}

public subscribeToCompatibilityChanges(
{ embeddable }: EmbeddableApiContext,
onChange: (isCompatible: boolean, action: ClearControlAction) => void
) {
if (!isClearableControl(embeddable)) return;

return embeddable.hasSelections$.subscribe((selection) => {
onChange(Boolean(selection), this);
});
}

public async isCompatible({ embeddable }: EmbeddableApiContext) {
const { isCompatible } = await import('./clear_control_action_compatibility_check');
return isCompatible(embeddable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@

import fastIsEqual from 'fast-deep-equal';
import React, { useEffect } from 'react';
import { BehaviorSubject, combineLatest, debounceTime, filter, map, skip } from 'rxjs';
import {
BehaviorSubject,
combineLatest,
debounceTime,
distinctUntilChanged,
filter,
map,
skip,
} from 'rxjs';

import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter, Filter } from '@kbn/es-query';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { PublishingSubject, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';

import { OPTIONS_LIST_CONTROL } from '../../../../common';
import type {
Expand Down Expand Up @@ -205,6 +213,22 @@ export const getOptionsListControlFactory = (): DataControlFactory<
if (currentSelections.length > 1) selections.setSelectedOptions([currentSelections[0]]);
});

const hasSelections$ = new BehaviorSubject<boolean>(
Boolean(initialState.selectedOptions?.length || initialState.existsSelected)
);
const hasSelectionsSubscription = combineLatest([
selections.selectedOptions$,
selections.existsSelected$,
])
.pipe(
map(([selectedOptions, existsSelected]) => {
return Boolean(selectedOptions?.length || existsSelected);
}),
distinctUntilChanged()
)
.subscribe((hasSelections) => {
hasSelections$.next(hasSelections);
});
/** Output filters when selections change */
const outputFilterSubscription = combineLatest([
dataControl.api.dataViews,
Expand Down Expand Up @@ -269,6 +293,7 @@ export const getOptionsListControlFactory = (): DataControlFactory<
if (selections.existsSelected$.getValue()) selections.setExistsSelected(false);
if (invalidSelections$.getValue().size) invalidSelections$.next(new Set([]));
},
hasSelections$: hasSelections$ as PublishingSubject<boolean | undefined>,
},
{
...dataControl.comparators,
Expand Down Expand Up @@ -382,6 +407,7 @@ export const getOptionsListControlFactory = (): DataControlFactory<
outputFilterSubscription.unsubscribe();
singleSelectSubscription.unsubscribe();
validSearchStringSubscription.unsubscribe();
hasSelectionsSubscription.unsubscribe();
};
}, []);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const getRangesliderControlFactory = (): DataControlFactory<
clearSelections: () => {
selections.setValue(undefined);
},
hasSelections$: selections.hasRangeSelection$,
},
{
...dataControl.comparators,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ export function initializeRangeControlSelections(
onSelectionChange: () => void
) {
const value$ = new BehaviorSubject<RangeValue | undefined>(initialState.value);
const hasRangeSelection$ = new BehaviorSubject<boolean>(Boolean(value$.getValue()));

function setValue(next: RangeValue | undefined) {
if (value$.value !== next) {
value$.next(next);
hasRangeSelection$.next(Boolean(next));
onSelectionChange();
}
}
Expand All @@ -29,6 +32,7 @@ export function initializeRangeControlSelections(
} as StateComparators<Pick<RangesliderControlState, 'value'>>,
hasInitialSelections: initialState.value !== undefined,
value$: value$ as PublishingSubject<RangeValue | undefined>,
hasRangeSelection$: hasRangeSelection$ as PublishingSubject<boolean | undefined>,
setValue,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { BehaviorSubject, debounceTime, first, map } from 'rxjs';
import { EuiInputPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
PublishingSubject,
ViewMode,
apiHasParentApi,
apiPublishesDataLoading,
Expand Down Expand Up @@ -57,6 +58,7 @@ export const getTimesliderControlFactory = (): ControlFactory<
const timeslice$ = new BehaviorSubject<[number, number] | undefined>(undefined);
const isAnchored$ = new BehaviorSubject<boolean | undefined>(initialState.isAnchored);
const isPopoverOpen$ = new BehaviorSubject(false);
const hasTimeSliceSelection$ = new BehaviorSubject<boolean>(Boolean(timeslice$));

const timeRangePercentage = initTimeRangePercentage(
initialState,
Expand Down Expand Up @@ -102,6 +104,7 @@ export const getTimesliderControlFactory = (): ControlFactory<
}

function onChange(timeslice?: Timeslice) {
hasTimeSliceSelection$.next(Boolean(timeslice));
setTimeslice(timeslice);
const nextSelectedRange = timeslice
? timeslice[TO_INDEX] - timeslice[FROM_INDEX]
Expand Down Expand Up @@ -224,7 +227,9 @@ export const getTimesliderControlFactory = (): ControlFactory<
},
clearSelections: () => {
setTimeslice(undefined);
hasTimeSliceSelection$.next(false);
},
hasSelections$: hasTimeSliceSelection$ as PublishingSubject<boolean | undefined>,
CustomPrependComponent: () => {
const [autoApplySelections, viewMode] = useBatchedPublishingSubjects(
controlGroupApi.autoApplySelections$,
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/controls/public/controls/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export type ControlApiRegistration<ControlApi extends DefaultControlApi = Defaul
export type ControlApiInitialization<ControlApi extends DefaultControlApi = DefaultControlApi> =
Omit<
ControlApiRegistration<ControlApi>,
'serializeState' | 'getTypeDisplayName' | 'clearSelections'
'serializeState' | 'getTypeDisplayName' | keyof CanClearSelections
>;

export interface ControlFactory<
Expand Down
7 changes: 6 additions & 1 deletion src/plugins/controls/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { PublishingSubject } from '@kbn/presentation-publishing';

export interface CanClearSelections {
clearSelections: () => void;
hasSelections$: PublishingSubject<boolean | undefined>;
}

export const isClearableControl = (control: unknown): control is CanClearSelections => {
return typeof (control as CanClearSelections).clearSelections === 'function';
return (
typeof (control as CanClearSelections).clearSelections === 'function' &&
Boolean((control as CanClearSelections).hasSelections$)
);
};

/**
Expand Down
Loading

0 comments on commit 242981d

Please sign in to comment.