[Embeddable Rebuild] [Controls] Clean up styling + add clear selections to timeslider (#186656)

## Summary

The primary goal of this PR is to clean up the styling of the
`ControlPanel` component for the new React control renderer.
Specifically, this fixes the following:
- I switched the inline Emotion styling to CSS classes instead
- I made it so that the timeslider control renders the drag handler in
edit mode and **doesn't** render the empty icon for the drag handler in
view mode

<p align="center"><img width="600px"
src="d5bf169b-2106-4f88-9698-f00162809d0a"/><p>

- I fixed the timeslider prepend so that it no longer wraps

<p align="center"><img width="500px"
src="7859d67b-1454-45b5-b7d8-7000086641a7"/><p>

- I moved the error component into the `EuiFormControlLayout` component,
which ensures that the drag handler is rendered for when a control has a
blocking error. I also fixed the styling for the error component:

<p align="center"><img width="600px"
src="13e0f041-8c51-494c-9079-323ed518c87b"/><p>

When I was working on these style changes, I noticed that the timeslider
control wasn't implementing `CanClearSelections` which meant that it no
longer had the clear selections action. This made me realize that this
interface should probably be part of the `DefaultControlApi` rather than
`DefaultDataControlApi` so, I moved it and added `clearSelections` to
the timeslider API.

<p align="center"><img width="600px"
src="47f7b648-bb2d-4158-b058-456bfdf5cdb5"/><p>



### Checklist

- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Hannah Mudge 2024-07-03 08:07:21 -06:00 committed by GitHub
parent 82d32a757f
commit 80f3c191ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 269 additions and 246 deletions

View file

@ -16,6 +16,7 @@ import {
EuiTab,
EuiTabs,
} from '@elastic/eui';
import { I18nProvider } from '@kbn/i18n-react';
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
@ -47,35 +48,37 @@ const App = ({
}
return (
<EuiPage>
<EuiPageBody>
<EuiPageSection>
<EuiPageHeader pageTitle="Controls" />
</EuiPageSection>
<EuiPageTemplate.Section>
<I18nProvider>
<EuiPage>
<EuiPageBody>
<EuiPageSection>
<EuiTabs>
<EuiTab
onClick={() => onSelectedTabChanged(CONTROLS_REFACTOR_TEST)}
isSelected={CONTROLS_REFACTOR_TEST === selectedTabId}
>
Register a new React control
</EuiTab>
<EuiTab
onClick={() => onSelectedTabChanged(CONTROLS_AS_A_BUILDING_BLOCK)}
isSelected={CONTROLS_AS_A_BUILDING_BLOCK === selectedTabId}
>
Controls as a building block
</EuiTab>
</EuiTabs>
<EuiSpacer />
{renderTabContent()}
<EuiPageHeader pageTitle="Controls" />
</EuiPageSection>
</EuiPageTemplate.Section>
</EuiPageBody>
</EuiPage>
<EuiPageTemplate.Section>
<EuiPageSection>
<EuiTabs>
<EuiTab
onClick={() => onSelectedTabChanged(CONTROLS_REFACTOR_TEST)}
isSelected={CONTROLS_REFACTOR_TEST === selectedTabId}
>
Register a new React control
</EuiTab>
<EuiTab
onClick={() => onSelectedTabChanged(CONTROLS_AS_A_BUILDING_BLOCK)}
isSelected={CONTROLS_AS_A_BUILDING_BLOCK === selectedTabId}
>
Controls as a building block
</EuiTab>
</EuiTabs>
<EuiSpacer />
{renderTabContent()}
</EuiPageSection>
</EuiPageTemplate.Section>
</EuiPageBody>
</EuiPage>
</I18nProvider>
);
};

View file

@ -6,14 +6,16 @@
* Side Public License, v 1.
*/
import React, { useEffect, useMemo, useState } from 'react';
import { BehaviorSubject, combineLatest } from 'rxjs';
import {
EuiButton,
EuiButtonGroup,
EuiCallOut,
EuiCodeBlock,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
EuiSpacer,
EuiSuperDatePicker,
OnTimeChangeProps,
@ -23,27 +25,20 @@ import { CoreStart } from '@kbn/core/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { ReactEmbeddableRenderer, ViewMode } from '@kbn/embeddable-plugin/public';
import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import { combineCompatibleChildrenApis, PresentationContainer } from '@kbn/presentation-containers';
import { combineCompatibleChildrenApis } from '@kbn/presentation-containers';
import {
apiPublishesDataLoading,
HasUniqueId,
PublishesDataLoading,
PublishesUnifiedSearch,
PublishesViewMode,
PublishingSubject,
useBatchedPublishingSubjects,
useStateFromPublishingSubject,
ViewMode as ViewModeType,
} from '@kbn/presentation-publishing';
import { toMountPoint } from '@kbn/react-kibana-mount';
import React, { useEffect, useMemo, useState } from 'react';
import useAsync from 'react-use/lib/useAsync';
import useMount from 'react-use/lib/useMount';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { ControlGroupApi } from '../react_controls/control_group/types';
import { RANGE_SLIDER_CONTROL_TYPE } from '../react_controls/data_controls/range_slider/types';
import { SEARCH_CONTROL_TYPE } from '../react_controls/data_controls/search_control/types';
import { TIMESLIDER_CONTROL_TYPE } from '../react_controls/timeslider_control/types';
import { RANGE_SLIDER_CONTROL_TYPE } from '../react_controls/data_controls/range_slider/types';
const toggleViewButtons = [
{
@ -104,18 +99,7 @@ const controlGroupPanels = {
},
};
/**
* I am mocking the dashboard API so that the data table embeddble responds to changes to the
* data view publishing subject from the control group
*/
type MockedDashboardApi = PresentationContainer &
PublishesDataLoading &
PublishesViewMode &
PublishesUnifiedSearch & {
setViewMode: (newViewMode: ViewMode) => void;
setChild: (child: HasUniqueId) => void;
unifiedSearchFilters$: PublishingSubject<Filter[] | undefined>;
};
const WEB_LOGS_DATA_VIEW_ID = '90943e30-9a47-11e8-b64d-95841ca0b247';
export const ReactControlExample = ({
core,
@ -145,28 +129,34 @@ export const ReactControlExample = ({
const timeslice$ = useMemo(() => {
return new BehaviorSubject<[number, number] | undefined>(undefined);
}, []);
const [dataLoading, timeRange] = useBatchedPublishingSubjects(dataLoading$, timeRange$);
const viewMode$ = useMemo(() => {
return new BehaviorSubject<ViewModeType>(ViewMode.EDIT as ViewModeType);
}, []);
const [dataLoading, timeRange, viewMode] = useBatchedPublishingSubjects(
dataLoading$,
timeRange$,
viewMode$
);
const [dashboardApi, setDashboardApi] = useState<MockedDashboardApi | undefined>(undefined);
const [controlGroupApi, setControlGroupApi] = useState<ControlGroupApi | undefined>(undefined);
const viewModeSelected = useStateFromPublishingSubject(dashboardApi?.viewMode);
const [dataViewNotFound, setDataViewNotFound] = useState(false);
useMount(() => {
const viewMode = new BehaviorSubject<ViewModeType>(ViewMode.EDIT as ViewModeType);
const dashboardApi = useMemo(() => {
const query$ = new BehaviorSubject<Query | AggregateQuery | undefined>(undefined);
const children$ = new BehaviorSubject<{ [key: string]: unknown }>({});
setDashboardApi({
return {
dataLoading: dataLoading$,
viewMode,
unifiedSearchFilters$,
viewMode: viewMode$,
filters$,
query$,
timeRange$,
timeslice$,
children$,
setViewMode: (newViewMode) => viewMode.next(newViewMode),
setChild: (child) => children$.next({ ...children$.getValue(), [child.uuid]: child }),
publishFilters: (newFilters: Filter[] | undefined) => filters$.next(newFilters),
setChild: (child: HasUniqueId) =>
children$.next({ ...children$.getValue(), [child.uuid]: child }),
removePanel: () => {},
replacePanel: () => {
return Promise.resolve('');
@ -177,8 +167,9 @@ export const ReactControlExample = ({
addNewPanel: () => {
return Promise.resolve(undefined);
},
});
});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const subscription = combineCompatibleChildrenApis<PublishesDataLoading, boolean | undefined>(
@ -199,13 +190,18 @@ export const ReactControlExample = ({
};
}, [dashboardApi, dataLoading$]);
// TODO: Maybe remove `useAsync` - see https://github.com/elastic/kibana/pull/182842#discussion_r1624909709
const {
loading,
value: dataViews,
error,
} = useAsync(async () => {
return await dataViewsService.find('kibana_sample_data_logs');
useEffect(() => {
let ignore = false;
dataViewsService.get(WEB_LOGS_DATA_VIEW_ID).catch(() => {
if (!ignore) {
setDataViewNotFound(true);
}
});
return () => {
ignore = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
@ -244,20 +240,16 @@ export const ReactControlExample = ({
};
}, [controlGroupFilters$, filters$, unifiedSearchFilters$]);
if (error || (!dataViews?.[0]?.id && !loading))
return (
<EuiEmptyPrompt
iconType="error"
color="danger"
title={<h2>There was an error!</h2>}
body={<p>{error ? error.message : 'Please add at least one data view.'}</p>}
/>
);
return loading ? (
<EuiLoadingSpinner />
) : (
return (
<>
{dataViewNotFound && (
<>
<EuiCallOut color="warning" iconType="warning">
<p>{`Install "Sample web logs" to run example`}</p>
</EuiCallOut>
<EuiSpacer size="m" />
</>
)}
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
@ -294,9 +286,9 @@ export const ReactControlExample = ({
<EuiButtonGroup
legend="Change the view mode"
options={toggleViewButtons}
idSelected={`viewModeToggle_${viewModeSelected}`}
idSelected={`viewModeToggle_${viewMode}`}
onChange={(_, value) => {
dashboardApi?.setViewMode(value);
viewMode$.next(value);
}}
/>
</EuiFlexItem>
@ -336,12 +328,12 @@ export const ReactControlExample = ({
{
name: `controlGroup_${searchControlId}:${SEARCH_CONTROL_TYPE}DataView`,
type: 'index-pattern',
id: dataViews?.[0].id!,
id: WEB_LOGS_DATA_VIEW_ID,
},
{
name: `controlGroup_${rangeSliderControlId}:${RANGE_SLIDER_CONTROL_TYPE}DataView`,
type: 'index-pattern',
id: dataViews?.[0].id!,
id: WEB_LOGS_DATA_VIEW_ID,
},
],
}),

View file

@ -9,7 +9,7 @@
import React, { useState } from 'react';
import { EuiButtonEmpty, EuiPopover } from '@elastic/eui';
import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
import { FormattedMessage } from '@kbn/i18n-react';
import { Markdown } from '@kbn/shared-ux-markdown';
/** TODO: This file is duplicated from the controls plugin to avoid exporting it */
@ -24,13 +24,15 @@ export const ControlError = ({ error }: ControlErrorProps) => {
const popoverButton = (
<EuiButtonEmpty
flush="left"
color="danger"
iconSize="m"
iconType="error"
data-test-subj="control-frame-error"
onClick={() => setPopoverOpen((open) => !open)}
className={'errorEmbeddableCompact__button'}
className="errorEmbeddableCompact__button controlErrorButton"
textProps={{ className: 'errorEmbeddableCompact__text' }}
contentProps={{ className: 'controlErrorButton--content' }}
>
<FormattedMessage
id="controls.frame.error.message"
@ -40,17 +42,15 @@ export const ControlError = ({ error }: ControlErrorProps) => {
);
return (
<I18nProvider>
<EuiPopover
button={popoverButton}
isOpen={isPopoverOpen}
className="errorEmbeddableCompact__popover"
closePopover={() => setPopoverOpen(false)}
>
<Markdown data-test-subj="errorMessageMarkdown" readOnly>
{errorMessage}
</Markdown>
</EuiPopover>
</I18nProvider>
<EuiPopover
button={popoverButton}
isOpen={isPopoverOpen}
className="controlPanel errorEmbeddableCompact__popover"
closePopover={() => setPopoverOpen(false)}
>
<Markdown data-test-subj="errorMessageMarkdown" readOnly>
{errorMessage}
</Markdown>
</EuiPopover>
);
};

View file

@ -230,7 +230,7 @@ export const getControlGroupEmbeddableFactory = (services: {
return {
api,
Component: (props, test) => {
Component: () => {
const controlsInOrder = useStateFromPublishingSubject(controlOrder);
useEffect(() => {

View file

@ -0,0 +1,36 @@
.controlPanel {
width: 100%;
max-inline-size: 100% !important;
height: calc($euiButtonHeight - 2px);
box-shadow: none !important;
background-color: $euiFormBackgroundColor !important;
border-radius: 0 $euiBorderRadius $euiBorderRadius 0 !important;
&--roundedBorders {
border-radius: $euiBorderRadius !important;
}
&--label {
@include euiTextTruncate;
max-width: 40%;
background-color: transparent;
border-radius: $euiBorderRadius;
margin-left: 0 !important;
padding-left: 0 !important;
}
&--hideComponent {
display: none;
}
.controlErrorButton {
width: 100%;
border-radius: 0 $euiBorderRadius $euiBorderRadius 0 !important;
&--content {
justify-content: left;
padding-left: $euiSizeM;
}
}
}

View file

@ -10,7 +10,6 @@ import classNames from 'classnames';
import React, { useState } from 'react';
import { EuiFlexItem, EuiFormControlLayout, EuiFormLabel, EuiFormRow, EuiIcon } from '@elastic/eui';
import { css } from '@emotion/react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import {
@ -19,15 +18,24 @@ import {
useBatchedOptionalPublishingSubjects,
} from '@kbn/presentation-publishing';
import { FloatingActions } from '@kbn/presentation-util-plugin/public';
import { euiThemeVars } from '@kbn/ui-theme';
import { ControlError } from './control_error_component';
import { ControlPanelProps, DefaultControlApi } from './types';
import './control_panel.scss';
/**
* TODO: Handle dragging
*/
const DragHandle = ({ isEditable, controlTitle }: { isEditable: boolean; controlTitle?: string }) =>
const DragHandle = ({
isEditable,
controlTitle,
hideEmptyDragHandle,
}: {
isEditable: boolean;
controlTitle?: string;
hideEmptyDragHandle: boolean;
}) =>
isEditable ? (
<button
aria-label={i18n.translate('controls.controlGroup.ariaActions.moveControlButtonAction', {
@ -38,7 +46,9 @@ const DragHandle = ({ isEditable, controlTitle }: { isEditable: boolean; control
>
<EuiIcon type="grabHorizontal" />
</button>
) : null;
) : hideEmptyDragHandle ? null : (
<EuiIcon size="s" type="empty" />
);
export const ControlPanel = <ApiType extends DefaultControlApi = DefaultControlApi>({
Component,
@ -115,63 +125,49 @@ export const ControlPanel = <ApiType extends DefaultControlApi = DefaultControlA
fullWidth
label={usingTwoLineLayout ? panelTitle || defaultPanelTitle || '...' : undefined}
>
{blockingError ? (
<EuiFormControlLayout>
<ControlError
error={
blockingError ??
i18n.translate('controls.blockingError', {
defaultMessage: 'There was an error loading this control.',
})
}
/>
</EuiFormControlLayout>
) : (
<EuiFormControlLayout
fullWidth
isLoading={Boolean(dataLoading)}
prepend={
api?.CustomPrependComponent ? (
<EuiFormControlLayout
fullWidth
isLoading={Boolean(dataLoading)}
prepend={
<>
<DragHandle
isEditable={isEditable}
controlTitle={panelTitle || defaultPanelTitle}
hideEmptyDragHandle={usingTwoLineLayout || Boolean(api?.CustomPrependComponent)}
/>
{api?.CustomPrependComponent ? (
<api.CustomPrependComponent />
) : usingTwoLineLayout ? (
<DragHandle
isEditable={isEditable}
controlTitle={panelTitle || defaultPanelTitle}
/>
) : (
<>
<DragHandle
isEditable={isEditable}
controlTitle={panelTitle || defaultPanelTitle}
/>{' '}
<EuiFormLabel
className="eui-textTruncate"
// TODO: Convert this to a class when replacing the legacy control group
css={css`
background-color: transparent !important;
`}
>
{panelTitle || defaultPanelTitle}
</EuiFormLabel>
</>
)
}
>
) : usingTwoLineLayout ? null : (
<EuiFormLabel className="controlPanel--label">
{panelTitle || defaultPanelTitle}
</EuiFormLabel>
)}
</>
}
>
<>
{blockingError && (
<ControlError
error={
blockingError ??
i18n.translate('controls.blockingError', {
defaultMessage: 'There was an error loading this control.',
})
}
/>
)}
<Component
// TODO: Convert this to a class when replacing the legacy control group
css={css`
height: calc(${euiThemeVars.euiButtonHeight} - 2px);
box-shadow: none !important;
${!isEditable && usingTwoLineLayout
? `border-radius: ${euiThemeVars.euiBorderRadius} !important`
: ''};
`}
className={classNames('controlPanel', {
'controlPanel--roundedBorders':
!api?.CustomPrependComponent && !isEditable && usingTwoLineLayout,
'controlPanel--hideComponent': Boolean(blockingError), // don't want to unmount component on error; just hide it
})}
ref={(newApi) => {
if (newApi && !api) setApi(newApi);
}}
/>
</EuiFormControlLayout>
)}
</>
</EuiFormControlLayout>
</EuiFormRow>
</FloatingActions>
</EuiFlexItem>

View file

@ -10,7 +10,6 @@ import React, { useImperativeHandle, useMemo } from 'react';
import { BehaviorSubject } from 'rxjs';
import { v4 as generateId } from 'uuid';
import { SerializedStyles } from '@emotion/react';
import { StateComparators } from '@kbn/presentation-publishing';
import { getControlFactory } from './control_factory_registry';
@ -68,7 +67,7 @@ export const ControlRenderer = <
parentApi
);
return React.forwardRef<typeof api, { css: SerializedStyles }>((props, ref) => {
return React.forwardRef<typeof api, { className: string }>((props, ref) => {
// expose the api into the imperative handle
useImperativeHandle(ref, () => api, []);
return <Component {...props} />;

View file

@ -1,25 +1,16 @@
.rangeSliderAnchor__button {
.euiFormControlLayout {
align-items: center;
box-shadow: none;
background-color: transparent;
.euiFormControlLayout__childrenWrapper {
background-color: transparent;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: $euiBorderRadius;
border-bottom-right-radius: $euiBorderRadius;
.euiFormControlLayoutDelimited__delimiter, .euiFormControlLayoutIcons--static {
height: auto !important;
}
}
background-color: transparent !important;
}
.rangeSlider__invalidToken {
height: $euiSizeS * 2;
padding: 0 $euiSizeS;
display: flex;
.euiToolTipAnchor {
align-self: center;
}
.euiIcon {
background-color: transparent;
@ -32,9 +23,7 @@
.rangeSliderAnchor__fieldNumber {
font-weight: $euiFontWeightMedium;
box-shadow: none;
text-align: center;
background-color: transparent;
height: calc($euiButtonHeight - 3px) !important;
&.rangeSliderAnchor__fieldNumber--valid:invalid:not(:focus) {
background-image: none; // override the red underline for values between steps

View file

@ -36,6 +36,7 @@ export const RangeSliderControl: FC<Props> = ({
step,
value,
uuid,
...rest
}: Props) => {
const rangeSliderRef = useRef<EuiDualRangeProps | null>(null);
@ -178,6 +179,7 @@ export const RangeSliderControl: FC<Props> = ({
max={displayedMax}
isLoading={isLoading}
inputPopoverProps={{
...rest,
panelMinWidth: MIN_POPOVER_WIDTH,
}}
append={

View file

@ -209,7 +209,7 @@ export const getRangesliderControlFactory = (
return {
api,
Component: () => {
Component: (controlPanelClassNames) => {
const [dataLoading, dataViews, fieldName, max, min, selectionHasNotResults, step, value] =
useBatchedPublishingSubjects(
dataLoading$,
@ -245,6 +245,7 @@ export const getRangesliderControlFactory = (
return (
<RangeSliderControl
{...controlPanelClassNames}
fieldFormatter={fieldFormatter}
isInvalid={selectionHasNotResults}
isLoading={typeof dataLoading === 'boolean' ? dataLoading : false}

View file

@ -185,10 +185,10 @@ export const getSearchControlFactory = ({
return {
api,
/**
* The `conrolStyleProps` prop is necessary because it contains the props from the generic
* The `controlPanelClassNamess` prop is necessary because it contains the class names from the generic
* ControlPanel that are necessary for styling
*/
Component: (conrolStyleProps) => {
Component: (controlPanelClassNames) => {
const currentSearch = useStateFromPublishingSubject(searchString);
useEffect(() => {
@ -202,7 +202,7 @@ export const getSearchControlFactory = ({
return (
<EuiFieldSearch
{...conrolStyleProps}
{...controlPanelClassNames}
incremental={true}
isClearable={false} // this will be handled by the clear floating action instead
value={currentSearch ?? ''}

View file

@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
import { CanClearSelections } from '@kbn/controls-plugin/public';
import { DataViewField } from '@kbn/data-views-plugin/common';
import { Filter } from '@kbn/es-query';
import {
@ -25,7 +24,6 @@ import {
export type DataControlApi = DefaultControlApi &
Omit<PublishesPanelTitle, 'hidePanelTitle'> & // control titles cannot be hidden
HasEditCapabilities &
CanClearSelections &
PublishesDataViews &
PublishesFilters & {
setOutputFilter: (filter: Filter | undefined) => void; // a control should only ever output a **single** filter

View file

@ -1,23 +1,19 @@
.timeSlider__popoverOverride {
width: 100%;
max-inline-size: 100% !important;
}
.timeSlider-playToggle:enabled {
background-color: $euiColorPrimary !important;
}
.timeSlider-prependButton {
background-color: transparent !important;
}
.timeSlider__anchor {
width: 100%;
height: 100%;
box-shadow: none;
overflow: hidden;
@include euiFormControlSideBorderRadius($euiFormControlBorderRadius, $side: 'right', $internal: true);
.euiText {
background-color: $euiFormBackgroundColor !important;
// background-color: transparent !important; TODO revert to this rule once control group provides background color
background-color: transparent !important;
&:hover {
text-decoration: underline;

View file

@ -6,13 +6,12 @@
* Side Public License, v 1.
*/
import { EuiButtonIcon } from '@elastic/eui';
import React, { FC, useCallback, useState } from 'react';
import { Observable, Subscription } from 'rxjs';
import { first } from 'rxjs';
import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui';
import { ViewMode } from '@kbn/presentation-publishing';
import { TimeSliderStrings } from './time_slider_strings';
import React, { FC, useCallback, useState } from 'react';
import { first, Observable, Subscription } from 'rxjs';
import { PlayButton } from './play_button';
import { TimeSliderStrings } from './time_slider_strings';
interface Props {
onNext: () => void;
@ -66,35 +65,43 @@ export const TimeSliderPrepend: FC<Props> = (props: Props) => {
}, [props, subscription, timeoutId]);
return (
<div>
<EuiButtonIcon
onClick={() => {
onPause();
props.onPrevious();
}}
iconType="framePrevious"
color="text"
aria-label={TimeSliderStrings.control.getPreviousButtonAriaLabel()}
data-test-subj="timeSlider-previousTimeWindow"
/>
<PlayButton
onPlay={onPlay}
onPause={onPause}
waitForControlOutputConsumersToLoad$={props.waitForControlOutputConsumersToLoad$}
viewMode={props.viewMode}
disablePlayButton={props.disablePlayButton}
isPaused={isPaused}
/>
<EuiButtonIcon
onClick={() => {
onPause();
props.onNext();
}}
iconType="frameNext"
color="text"
aria-label={TimeSliderStrings.control.getNextButtonAriaLabel()}
data-test-subj="timeSlider-nextTimeWindow"
/>
</div>
<>
<EuiFlexItem grow={false}>
<EuiButtonIcon
onClick={() => {
onPause();
props.onPrevious();
}}
iconType="framePrevious"
color="text"
className={'timeSlider-prependButton'}
aria-label={TimeSliderStrings.control.getPreviousButtonAriaLabel()}
data-test-subj="timeSlider-previousTimeWindow"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<PlayButton
onPlay={onPlay}
onPause={onPause}
waitForControlOutputConsumersToLoad$={props.waitForControlOutputConsumersToLoad$}
viewMode={props.viewMode}
disablePlayButton={props.disablePlayButton}
isPaused={isPaused}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
onClick={() => {
onPause();
props.onNext();
}}
iconType="frameNext"
color="text"
className={'timeSlider-prependButton'}
aria-label={TimeSliderStrings.control.getNextButtonAriaLabel()}
data-test-subj="timeSlider-nextTimeWindow"
/>
</EuiFlexItem>
</>
);
};

View file

@ -53,6 +53,14 @@ export const getTimesliderControlFactory = (
const { timeRangeMeta$, formatDate, cleanupTimeRangeSubscription } =
initTimeRangeSubscription(controlGroupApi, services);
const timeslice$ = new BehaviorSubject<[number, number] | undefined>(undefined);
const isAnchored$ = new BehaviorSubject<boolean | undefined>(initialState.isAnchored);
const isPopoverOpen$ = new BehaviorSubject(false);
const timeRangePercentage = initTimeRangePercentage(
initialState,
syncTimesliceWithTimeRangePercentage
);
function syncTimesliceWithTimeRangePercentage(
startPercentage: number | undefined,
endPercentage: number | undefined
@ -73,18 +81,16 @@ export const getTimesliderControlFactory = (
]);
setSelectedRange(to - from);
}
const timeRangePercentage = initTimeRangePercentage(
initialState,
syncTimesliceWithTimeRangePercentage
);
function setTimeslice(timeslice?: Timeslice) {
timeRangePercentage.setTimeRangePercentage(timeslice, timeRangeMeta$.value);
timeslice$.next(timeslice);
}
const isAnchored$ = new BehaviorSubject<boolean | undefined>(initialState.isAnchored);
function setIsAnchored(isAnchored: boolean | undefined) {
isAnchored$.next(isAnchored);
}
let selectedRange: number | undefined;
function setSelectedRange(nextSelectedRange?: number) {
selectedRange =
@ -176,10 +182,6 @@ export const getTimesliderControlFactory = (
setTimeslice([from, Math.min(to, timeRangeMax)]);
}
const isPopoverOpen$ = new BehaviorSubject(false);
function setIsPopoverOpen(value: boolean) {
isPopoverOpen$.next(value);
}
const viewModeSubject =
getViewModeSubject(controlGroupApi) ?? new BehaviorSubject('view' as ViewMode);
@ -217,6 +219,9 @@ export const getTimesliderControlFactory = (
references: [],
};
},
clearSelections: () => {
setTimeslice(undefined);
},
CustomPrependComponent: () => {
const [autoApplySelections, viewMode] = useBatchedPublishingSubjects(
controlGroupApi.autoApplySelections$,
@ -229,7 +234,7 @@ export const getTimesliderControlFactory = (
onPrevious={onPrevious}
viewMode={viewMode}
disablePlayButton={!autoApplySelections}
setIsPopoverOpen={setIsPopoverOpen}
setIsPopoverOpen={(value) => isPopoverOpen$.next(value)}
waitForControlOutputConsumersToLoad$={waitForDashboardPanelsToLoad$}
/>
);
@ -253,7 +258,7 @@ export const getTimesliderControlFactory = (
return {
api,
Component: (controlStyleProps) => {
Component: (controlPanelClassNames) => {
const [isAnchored, isPopoverOpen, timeRangeMeta, timeslice] =
useBatchedPublishingSubjects(isAnchored$, isPopoverOpen$, timeRangeMeta$, timeslice$);
@ -273,13 +278,12 @@ export const getTimesliderControlFactory = (
return (
<EuiInputPopover
{...controlStyleProps}
className="timeSlider__popoverOverride"
{...controlPanelClassNames}
panelClassName="timeSlider__panelOverride"
input={
<TimeSliderPopoverButton
onClick={() => {
setIsPopoverOpen(!isPopoverOpen);
isPopoverOpen$.next(!isPopoverOpen);
}}
formatDate={formatDate}
from={from}
@ -287,7 +291,7 @@ export const getTimesliderControlFactory = (
/>
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
closePopover={() => isPopoverOpen$.next(false)}
panelPaddingSize="s"
>
<TimeSliderPopoverContent

View file

@ -8,8 +8,7 @@
import { BehaviorSubject } from 'rxjs';
import { SerializedStyles } from '@emotion/react';
import { ControlWidth } from '@kbn/controls-plugin/public/types';
import { CanClearSelections, ControlWidth } from '@kbn/controls-plugin/public/types';
import { HasSerializableState } from '@kbn/presentation-containers';
import { PanelCompatibleComponent } from '@kbn/presentation-panel-plugin/public/panel_component/types';
import {
@ -41,10 +40,12 @@ export type DefaultControlApi = PublishesDataLoading &
PublishesUnsavedChanges &
PublishesControlDisplaySettings &
Partial<PublishesPanelTitle & PublishesDisabledActionIds & HasCustomPrepend> &
CanClearSelections &
HasType &
HasUniqueId &
HasSerializableState &
HasParentApi<ControlGroupApi> & {
/** TODO: Make these non-public as part of https://github.com/elastic/kibana/issues/174961 */
setDataLoading: (loading: boolean) => void;
setBlockingError: (error: Error | undefined) => void;
};
@ -90,7 +91,7 @@ export type ControlStateManager<State extends object = object> = {
export interface ControlPanelProps<
ApiType extends DefaultControlApi = DefaultControlApi,
PropsType extends {} = { css: SerializedStyles }
PropsType extends {} = { className: string }
> {
Component: PanelCompatibleComponent<ApiType, PropsType>;
}

View file

@ -31,7 +31,6 @@
"@kbn/react-kibana-mount",
"@kbn/content-management-utils",
"@kbn/presentation-util-plugin",
"@kbn/ui-theme",
"@kbn/core-lifecycle-browser",
"@kbn/presentation-panel-plugin",
"@kbn/datemath",

View file

@ -13,7 +13,7 @@ import type { DataControlInput } from '../types';
import { OptionsListSearchTechnique } from './suggestions_searching';
import type { OptionsListSortingType } from './suggestions_sorting';
export const OPTIONS_LIST_CONTROL = 'optionsListControl';
export const OPTIONS_LIST_CONTROL = 'optionsListControl'; // TODO: Replace with OPTIONS_LIST_CONTROL_TYPE
export interface OptionsListEmbeddableInput extends DataControlInput {
searchTechnique?: OptionsListSearchTechnique;