Skip to content
Open
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 @@ -57,6 +57,7 @@ export const ControlPanel = ({
const initialState = useMemo(() => {
return parentApi.layout$.getValue().controls[uuid];
}, [parentApi, uuid]);
console.log('INITIAL STATE', initialState);

const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: uuid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
export const ACTION_CLEAR_CONTROL = 'clearControl';
export const ACTION_CREATE_CONTROL = 'createControl';
export const ACTION_CREATE_ESQL_CONTROL = 'createESQLControl';
export const ACTION_CREATE_TIME_SLIDER = 'createTimeSlider';
export const ACTION_PIN_CONTROL = 'pinControl';

export const OPTIONS_LIST_ACTION = 'addOptionsList';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type { CreateControlTypeContext } from './control_panel_actions';

export const createControlAction = (): ActionDefinition<EmbeddableApiContext> => ({
id: ACTION_CREATE_CONTROL,
order: 0,
order: 1,
getIconType: () => 'controlsHorizontal',
isCompatible: async ({ embeddable }) => apiCanAddNewPanel(embeddable),
execute: async ({ embeddable }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { uiActionsService } from '../services/kibana_services';

export const createESQLControlAction = (): ActionDefinition<EmbeddableApiContext> => ({
id: ACTION_CREATE_ESQL_CONTROL,
order: 0,
order: 1,
getIconType: () => 'controlsHorizontal',
isCompatible: async ({ embeddable }) => apiCanAddNewPanel(embeddable),
execute: async ({ embeddable }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { apiCanAddNewPanel, apiCanPinPanel } from '@kbn/presentation-containers';
import { type EmbeddableApiContext } from '@kbn/presentation-publishing';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import type { ActionDefinition } from '@kbn/ui-actions-plugin/public/actions';
import { TIME_SLIDER_CONTROL } from '@kbn/controls-constants';
import { apiPublishesLayout } from '@kbn/dashboard-plugin/public';
import { map } from 'rxjs';
import { ACTION_CREATE_TIME_SLIDER } from './constants';

const compatibilityCheck = (api: unknown | null) =>
apiCanAddNewPanel(api) &&
apiCanPinPanel(api) &&
apiPublishesLayout(api) &&
!Object.values(api.layout$.getValue().controls).find(
(control) => control.type === TIME_SLIDER_CONTROL
);

export const createTimeSliderAction = (): ActionDefinition<EmbeddableApiContext> => ({
id: ACTION_CREATE_TIME_SLIDER,
order: 0,
getIconType: () => 'controlsHorizontal',
couldBecomeCompatible: ({ embeddable }) =>
apiCanAddNewPanel(embeddable) && apiCanPinPanel(embeddable),
getCompatibilityChangesSubject: ({ embeddable }) =>
apiPublishesLayout(embeddable) ? embeddable.layout$.pipe(map(() => undefined)) : undefined,
isCompatible: async ({ embeddable }) => compatibilityCheck(embeddable),
execute: async ({ embeddable }) => {
if (!apiCanAddNewPanel(embeddable) || !apiCanPinPanel(embeddable))
throw new IncompatibleActionError();
const newPanel = await embeddable.addNewPanel<{}, { uuid: string }>({
panelType: TIME_SLIDER_CONTROL,
serializedState: {
rawState: {},
},
});
if (!newPanel) throw new Error('Failed tp create time slider panel');
embeddable.pinPanel(newPanel.uuid);
Comment on lines +38 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should combine this into a single action - i.e. don't create then pin, just add a new function addPinnedPanel to the Dashboard API. What do you think @Zacqary?

},
getDisplayName: () =>
i18n.translate('controls.timeSlider.displayNameAriaLabel', {
defaultMessage: 'Time slider',
}),

getDisplayNameTooltip: ({ embeddable }) =>
compatibilityCheck(embeddable)
? i18n.translate('controls.timeSlider.tooltip', {
defaultMessage: 'Add a time slider control to your dashboard.',
})
: i18n.translate('controls.timeSlider.disabledTooltip', {
defaultMessage: 'Only one time slider control can be added per dashboard.',
}),
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ACTION_CLEAR_CONTROL,
ACTION_CREATE_CONTROL,
ACTION_CREATE_ESQL_CONTROL,
ACTION_CREATE_TIME_SLIDER,
ACTION_PIN_CONTROL,
OPTIONS_LIST_ACTION,
RANGE_SLIDER_ACTION,
Expand Down Expand Up @@ -49,6 +50,12 @@ export function registerActions(uiActions: UiActionsStart) {
const { createESQLControlAction } = await import('../controls_module');
return createESQLControlAction();
});

uiActions.addTriggerActionAsync(ADD_PANEL_TRIGGER, ACTION_CREATE_TIME_SLIDER, async () => {
const { createTimeSliderAction } = await import('../controls_module');
return createTimeSliderAction();
});

uiActions.addTriggerActionAsync(CONTROL_MENU_TRIGGER, OPTIONS_LIST_ACTION, async () => {
const { createOptionsListControlAction } = await import('./create_options_list_action');
return createOptionsListControlAction();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,15 @@ import { EuiInputPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { PublishingSubject, ViewMode } from '@kbn/presentation-publishing';
import {
apiHasParentApi,
apiPublishesDataLoading,
getViewModeSubject,
useBatchedPublishingSubjects,
} from '@kbn/presentation-publishing';

import { initializeUnsavedChanges } from '@kbn/presentation-containers';
import { TIME_SLIDER_CONTROL } from '@kbn/controls-constants';
import {
defaultControlComparators,
initializeDefaultControlManager,
} from '../default_control_manager';
import type { ControlFactory } from '../types';
import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { css } from '@emotion/react';
import { TimeSliderPopoverButton } from './components/time_slider_popover_button';
import { TimeSliderPopoverContent } from './components/time_slider_popover_content';
import { TimeSliderPrepend } from './components/time_slider_prepend';
Expand All @@ -48,24 +44,23 @@ const displayName = i18n.translate('controls.timesliderControl.displayName', {
defaultMessage: 'Time slider',
});

export const getTimesliderControlFactory = (): ControlFactory<
export const getTimesliderControlFactory = (): EmbeddableFactory<
TimesliderControlState,
TimesliderControlApi
> => {
return {
type: TIME_SLIDER_CONTROL,
getIconType: () => 'search',
getDisplayName: () => displayName,
buildControl: async ({ initialState, finalizeApi, uuid, controlGroupApi }) => {
buildEmbeddable: async ({ initialState, finalizeApi, uuid, parentApi }) => {
const state = initialState.rawState;
const { timeRangeMeta$, formatDate, cleanupTimeRangeSubscription } =
initTimeRangeSubscription(controlGroupApi);
initTimeRangeSubscription(parentApi);
const timeslice$ = new BehaviorSubject<[number, number] | undefined>(undefined);
const isAnchored$ = new BehaviorSubject<boolean | undefined>(initialState.isAnchored);
const isAnchored$ = new BehaviorSubject<boolean | undefined>(state.isAnchored);
const isPopoverOpen$ = new BehaviorSubject(false);
const hasTimeSliceSelection$ = new BehaviorSubject<boolean>(Boolean(timeslice$));

const timeRangePercentage = initTimeRangePercentage(
initialState,
state,
syncTimesliceWithTimeRangePercentage
);

Expand Down Expand Up @@ -192,17 +187,11 @@ export const getTimesliderControlFactory = (): ControlFactory<
}

const viewModeSubject =
getViewModeSubject(controlGroupApi) ?? new BehaviorSubject('view' as ViewMode);

const defaultControlManager = initializeDefaultControlManager({
...initialState,
width: 'large',
});
getViewModeSubject(parentApi) ?? new BehaviorSubject('view' as ViewMode);

const dashboardDataLoading$ =
apiHasParentApi(controlGroupApi) && apiPublishesDataLoading(controlGroupApi.parentApi)
? controlGroupApi.parentApi.dataLoading$
: new BehaviorSubject<boolean | undefined>(false);
const dashboardDataLoading$ = apiPublishesDataLoading(parentApi)
? parentApi.dataLoading$
: new BehaviorSubject<boolean | undefined>(false);
const waitForDashboardPanelsToLoad$ = dashboardDataLoading$.pipe(
// debounce to give time for panels to start loading if they are going to load from time changes
debounceTime(300),
Expand All @@ -219,41 +208,39 @@ export const getTimesliderControlFactory = (): ControlFactory<
function serializeState() {
return {
rawState: {
...defaultControlManager.getLatestState(),
...timeRangePercentage.getLatestState(),
isAnchored: isAnchored$.value,
width: 'large',
grow: true,
},
references: [],
};
}

const unsavedChangesApi = initializeUnsavedChanges<TimesliderControlState>({
uuid,
parentApi: controlGroupApi,
parentApi,
serializeState,
anyStateChange$: merge(
defaultControlManager.anyStateChange$,
timeRangePercentage.anyStateChange$,
isAnchored$.pipe(map(() => undefined))
),
getComparators: () => {
return {
...defaultControlComparators,
...timeRangePercentageComparators,
width: 'skip',
isAnchored: 'skip',
};
},
onReset: (lastSaved) => {
defaultControlManager.reinitializeState(lastSaved?.rawState);
timeRangePercentage.reinitializeState(lastSaved?.rawState);
setIsAnchored(lastSaved?.rawState?.isAnchored);
},
});

const api = finalizeApi({
...unsavedChangesApi,
...defaultControlManager.api,
isPinnable: false, // Disable the user-facing unpin action; panel can still be pinned programatically when it's created
defaultTitle$: new BehaviorSubject<string | undefined>(displayName),
timeslice$,
serializeState,
Expand All @@ -263,10 +250,8 @@ export const getTimesliderControlFactory = (): ControlFactory<
},
hasSelections$: hasTimeSliceSelection$ as PublishingSubject<boolean | undefined>,
CustomPrependComponent: () => {
const [autoApplySelections, viewMode] = useBatchedPublishingSubjects(
controlGroupApi.autoApplySelections$,
viewModeSubject
);
const autoApplySelections = true; // TODO Reimplement or remove
const [viewMode] = useBatchedPublishingSubjects(viewModeSubject);

return (
<TimeSliderPrepend
Expand Down Expand Up @@ -315,7 +300,11 @@ export const getTimesliderControlFactory = (): ControlFactory<
return (
<EuiInputPopover
{...controlPanelClassNames}
panelClassName="timeSlider__panelOverride"
css={css`
width: 100%;
height: 100%;
max-inline-size: 100%;
`}
input={
<TimeSliderPopoverButton
onClick={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export function registerTimeSliderControl() {
// registerControlPanelType(TIME_SLIDER_CONTROL, async () => {
// const [{ getTimesliderControlFactory }] = await Promise.all([
// import('../../controls_module'),
// untilPluginStartServicesReady(),
// ]);
// return getTimesliderControlFactory();
// });
import { TIME_SLIDER_CONTROL } from '@kbn/controls-constants';
import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
import { untilPluginStartServicesReady } from '../../services/kibana_services';

export function registerTimeSliderControl(embeddable: EmbeddableSetup) {
embeddable.registerReactEmbeddableFactory(TIME_SLIDER_CONTROL, async () => {
const [{ getTimesliderControlFactory }] = await Promise.all([
import('../../controls_module'),
untilPluginStartServicesReady(),
]);
return getTimesliderControlFactory();
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export { getESQLControlFactory } from './controls/esql_control/get_esql_control_

export { createControlAction } from './actions/create_control_action';
export { createESQLControlAction } from './actions/create_esql_control_action';
export { createTimeSliderAction } from './actions/create_time_slider_action';
12 changes: 10 additions & 2 deletions src/platform/plugins/shared/controls/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { ESQL_CONTROL, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '@kbn/controls-constants';
import {
ESQL_CONTROL,
OPTIONS_LIST_CONTROL,
RANGE_SLIDER_CONTROL,
TIME_SLIDER_CONTROL,
} from '@kbn/controls-constants';
import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { PanelPlacementStrategy } from '@kbn/dashboard-plugin/public';

Expand Down Expand Up @@ -38,7 +43,7 @@ export class ControlsPlugin

registerOptionsListControl(embeddable);
registerRangeSliderControl(embeddable);
registerTimeSliderControl();
registerTimeSliderControl(embeddable);
registerESQLControl(embeddable);
}

Expand All @@ -55,6 +60,9 @@ export class ControlsPlugin
startPlugins.dashboard.registerDashboardPanelSettings(ESQL_CONTROL, () => {
return CONTROL_PANEL_PLACEMENT;
});
startPlugins.dashboard.registerDashboardPanelSettings(TIME_SLIDER_CONTROL, () => {
return CONTROL_PANEL_PLACEMENT;
});
}

public stop() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { initializeUnifiedSearchManager } from './unified_search_manager';
import { initializeUnsavedChangesManager } from './unsaved_changes_manager';
import { initializeViewModeManager } from './view_mode_manager';
import { initializeESQLVariablesManager } from './esql_variables_manager';
import { initializeTimesliceManager } from './timeslice_manager';

export function getDashboardApi({
creationOptions,
Expand Down Expand Up @@ -75,6 +76,7 @@ export function getDashboardApi({
const settingsManager = initializeSettingsManager(initialState);

const esqlVariablesManager = initializeESQLVariablesManager(layoutManager.api.children$);
const timesliceManager = initializeTimesliceManager(layoutManager.api.children$);

const unifiedSearchManager = initializeUnifiedSearchManager(
initialState,
Expand Down Expand Up @@ -134,6 +136,7 @@ export function getDashboardApi({
...unsavedChangesManager.api,
...trackOverlayApi,
...esqlVariablesManager.api,
...timesliceManager.api,
...initializeTrackContentfulRender(),
executionContext: {
type: 'dashboard',
Expand Down Expand Up @@ -236,6 +239,8 @@ export function getDashboardApi({
unifiedSearchManager.cleanup();
unsavedChangesManager.cleanup();
layoutManager.cleanup();
esqlVariablesManager.cleanup();
timesliceManager.cleanup();
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export type { DashboardLayout, DashboardLayoutPanel } from './types';
export type { DashboardLayout, DashboardLayoutPanel, PublishesLayout } from './types';
export { apiPublishesLayout } from './types';
export { areLayoutsEqual } from './are_layouts_equal';
export { initializeLayoutManager } from './layout_manager';
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
import { asyncForEach } from '@kbn/std';
import type { StickyControlLayoutState } from '@kbn/controls-schemas/src/types';

import { TIME_SLIDER_CONTROL } from '@kbn/controls-constants';
import type { DashboardState } from '../../../common';
import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../../common/content_management';
import type { DashboardPanel } from '../../../server';
Expand Down Expand Up @@ -495,6 +496,9 @@ export function initializeLayoutManager(
newControls[uuid] = {
type: controlToPin.type as StickyControlLayoutState['type'],
order: Object.keys(newControls).length,
// TODO Remove this when grow and width settings for pinned controls are implemented
// https://github.com/elastic/kibana/issues/234681
...(controlToPin.type === TIME_SLIDER_CONTROL ? { width: 'large', grow: true } : {}),
Comment on lines +499 to +501
Copy link
Contributor

@Heenawter Heenawter Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to hard code the values in the layout manager for now, added a TODO comment so we know to remove when we implement real grow/width settings.

This is not a TODO. We definitely shouldn't hardcode the TIME_SLIDER_CONTROL type check like this (which could be avoided if we go the route of adding a new addPinnedPanel function instead) but the time slider control should not support editable width and grow state, as I mentioned here. So #234681 is irrelevant here.

};
const newPanels = { ...layout$.getValue().panels };
delete newPanels[uuid];
Expand Down
Loading