[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, EuiTab,
EuiTabs, EuiTabs,
} from '@elastic/eui'; } from '@elastic/eui';
import { I18nProvider } from '@kbn/i18n-react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
@ -47,35 +48,37 @@ const App = ({
} }
return ( return (
<EuiPage> <I18nProvider>
<EuiPageBody> <EuiPage>
<EuiPageSection> <EuiPageBody>
<EuiPageHeader pageTitle="Controls" />
</EuiPageSection>
<EuiPageTemplate.Section>
<EuiPageSection> <EuiPageSection>
<EuiTabs> <EuiPageHeader pageTitle="Controls" />
<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> </EuiPageSection>
</EuiPageTemplate.Section> <EuiPageTemplate.Section>
</EuiPageBody> <EuiPageSection>
</EuiPage> <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. * Side Public License, v 1.
*/ */
import React, { useEffect, useMemo, useState } from 'react';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { import {
EuiButton, EuiButton,
EuiButtonGroup, EuiButtonGroup,
EuiCallOut,
EuiCodeBlock, EuiCodeBlock,
EuiEmptyPrompt,
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
EuiLoadingSpinner,
EuiSpacer, EuiSpacer,
EuiSuperDatePicker, EuiSuperDatePicker,
OnTimeChangeProps, OnTimeChangeProps,
@ -23,27 +25,20 @@ import { CoreStart } from '@kbn/core/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { ReactEmbeddableRenderer, ViewMode } from '@kbn/embeddable-plugin/public'; import { ReactEmbeddableRenderer, ViewMode } from '@kbn/embeddable-plugin/public';
import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import { combineCompatibleChildrenApis, PresentationContainer } from '@kbn/presentation-containers'; import { combineCompatibleChildrenApis } from '@kbn/presentation-containers';
import { import {
apiPublishesDataLoading, apiPublishesDataLoading,
HasUniqueId, HasUniqueId,
PublishesDataLoading, PublishesDataLoading,
PublishesUnifiedSearch,
PublishesViewMode,
PublishingSubject,
useBatchedPublishingSubjects, useBatchedPublishingSubjects,
useStateFromPublishingSubject,
ViewMode as ViewModeType, ViewMode as ViewModeType,
} from '@kbn/presentation-publishing'; } from '@kbn/presentation-publishing';
import { toMountPoint } from '@kbn/react-kibana-mount'; 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 { 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 { SEARCH_CONTROL_TYPE } from '../react_controls/data_controls/search_control/types';
import { TIMESLIDER_CONTROL_TYPE } from '../react_controls/timeslider_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 = [ const toggleViewButtons = [
{ {
@ -104,18 +99,7 @@ const controlGroupPanels = {
}, },
}; };
/** const WEB_LOGS_DATA_VIEW_ID = '90943e30-9a47-11e8-b64d-95841ca0b247';
* 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>;
};
export const ReactControlExample = ({ export const ReactControlExample = ({
core, core,
@ -145,28 +129,34 @@ export const ReactControlExample = ({
const timeslice$ = useMemo(() => { const timeslice$ = useMemo(() => {
return new BehaviorSubject<[number, number] | undefined>(undefined); 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 [controlGroupApi, setControlGroupApi] = useState<ControlGroupApi | undefined>(undefined);
const viewModeSelected = useStateFromPublishingSubject(dashboardApi?.viewMode); const [dataViewNotFound, setDataViewNotFound] = useState(false);
useMount(() => { const dashboardApi = useMemo(() => {
const viewMode = new BehaviorSubject<ViewModeType>(ViewMode.EDIT as ViewModeType);
const query$ = new BehaviorSubject<Query | AggregateQuery | undefined>(undefined); const query$ = new BehaviorSubject<Query | AggregateQuery | undefined>(undefined);
const children$ = new BehaviorSubject<{ [key: string]: unknown }>({}); const children$ = new BehaviorSubject<{ [key: string]: unknown }>({});
setDashboardApi({ return {
dataLoading: dataLoading$, dataLoading: dataLoading$,
viewMode,
unifiedSearchFilters$, unifiedSearchFilters$,
viewMode: viewMode$,
filters$, filters$,
query$, query$,
timeRange$, timeRange$,
timeslice$, timeslice$,
children$, children$,
setViewMode: (newViewMode) => viewMode.next(newViewMode), publishFilters: (newFilters: Filter[] | undefined) => filters$.next(newFilters),
setChild: (child) => children$.next({ ...children$.getValue(), [child.uuid]: child }), setChild: (child: HasUniqueId) =>
children$.next({ ...children$.getValue(), [child.uuid]: child }),
removePanel: () => {}, removePanel: () => {},
replacePanel: () => { replacePanel: () => {
return Promise.resolve(''); return Promise.resolve('');
@ -177,8 +167,9 @@ export const ReactControlExample = ({
addNewPanel: () => { addNewPanel: () => {
return Promise.resolve(undefined); return Promise.resolve(undefined);
}, },
}); };
}); // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => { useEffect(() => {
const subscription = combineCompatibleChildrenApis<PublishesDataLoading, boolean | undefined>( const subscription = combineCompatibleChildrenApis<PublishesDataLoading, boolean | undefined>(
@ -199,13 +190,18 @@ export const ReactControlExample = ({
}; };
}, [dashboardApi, dataLoading$]); }, [dashboardApi, dataLoading$]);
// TODO: Maybe remove `useAsync` - see https://github.com/elastic/kibana/pull/182842#discussion_r1624909709 useEffect(() => {
const { let ignore = false;
loading, dataViewsService.get(WEB_LOGS_DATA_VIEW_ID).catch(() => {
value: dataViews, if (!ignore) {
error, setDataViewNotFound(true);
} = useAsync(async () => { }
return await dataViewsService.find('kibana_sample_data_logs'); });
return () => {
ignore = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -244,20 +240,16 @@ export const ReactControlExample = ({
}; };
}, [controlGroupFilters$, filters$, unifiedSearchFilters$]); }, [controlGroupFilters$, filters$, unifiedSearchFilters$]);
if (error || (!dataViews?.[0]?.id && !loading)) return (
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 />
) : (
<> <>
{dataViewNotFound && (
<>
<EuiCallOut color="warning" iconType="warning">
<p>{`Install "Sample web logs" to run example`}</p>
</EuiCallOut>
<EuiSpacer size="m" />
</>
)}
<EuiFlexGroup> <EuiFlexGroup>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiButton <EuiButton
@ -294,9 +286,9 @@ export const ReactControlExample = ({
<EuiButtonGroup <EuiButtonGroup
legend="Change the view mode" legend="Change the view mode"
options={toggleViewButtons} options={toggleViewButtons}
idSelected={`viewModeToggle_${viewModeSelected}`} idSelected={`viewModeToggle_${viewMode}`}
onChange={(_, value) => { onChange={(_, value) => {
dashboardApi?.setViewMode(value); viewMode$.next(value);
}} }}
/> />
</EuiFlexItem> </EuiFlexItem>
@ -336,12 +328,12 @@ export const ReactControlExample = ({
{ {
name: `controlGroup_${searchControlId}:${SEARCH_CONTROL_TYPE}DataView`, name: `controlGroup_${searchControlId}:${SEARCH_CONTROL_TYPE}DataView`,
type: 'index-pattern', type: 'index-pattern',
id: dataViews?.[0].id!, id: WEB_LOGS_DATA_VIEW_ID,
}, },
{ {
name: `controlGroup_${rangeSliderControlId}:${RANGE_SLIDER_CONTROL_TYPE}DataView`, name: `controlGroup_${rangeSliderControlId}:${RANGE_SLIDER_CONTROL_TYPE}DataView`,
type: 'index-pattern', 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 React, { useState } from 'react';
import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; 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'; import { Markdown } from '@kbn/shared-ux-markdown';
/** TODO: This file is duplicated from the controls plugin to avoid exporting it */ /** TODO: This file is duplicated from the controls plugin to avoid exporting it */
@ -24,13 +24,15 @@ export const ControlError = ({ error }: ControlErrorProps) => {
const popoverButton = ( const popoverButton = (
<EuiButtonEmpty <EuiButtonEmpty
flush="left"
color="danger" color="danger"
iconSize="m" iconSize="m"
iconType="error" iconType="error"
data-test-subj="control-frame-error" data-test-subj="control-frame-error"
onClick={() => setPopoverOpen((open) => !open)} onClick={() => setPopoverOpen((open) => !open)}
className={'errorEmbeddableCompact__button'} className="errorEmbeddableCompact__button controlErrorButton"
textProps={{ className: 'errorEmbeddableCompact__text' }} textProps={{ className: 'errorEmbeddableCompact__text' }}
contentProps={{ className: 'controlErrorButton--content' }}
> >
<FormattedMessage <FormattedMessage
id="controls.frame.error.message" id="controls.frame.error.message"
@ -40,17 +42,15 @@ export const ControlError = ({ error }: ControlErrorProps) => {
); );
return ( return (
<I18nProvider> <EuiPopover
<EuiPopover button={popoverButton}
button={popoverButton} isOpen={isPopoverOpen}
isOpen={isPopoverOpen} className="controlPanel errorEmbeddableCompact__popover"
className="errorEmbeddableCompact__popover" closePopover={() => setPopoverOpen(false)}
closePopover={() => setPopoverOpen(false)} >
> <Markdown data-test-subj="errorMessageMarkdown" readOnly>
<Markdown data-test-subj="errorMessageMarkdown" readOnly> {errorMessage}
{errorMessage} </Markdown>
</Markdown> </EuiPopover>
</EuiPopover>
</I18nProvider>
); );
}; };

View file

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

View file

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

View file

@ -1,25 +1,16 @@
.rangeSliderAnchor__button { .rangeSliderAnchor__button {
.euiFormControlLayout { .euiFormControlLayout {
align-items: center;
box-shadow: none; box-shadow: none;
background-color: transparent; background-color: transparent !important;
.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;
}
}
} }
.rangeSlider__invalidToken { .rangeSlider__invalidToken {
height: $euiSizeS * 2;
padding: 0 $euiSizeS; padding: 0 $euiSizeS;
display: flex;
.euiToolTipAnchor {
align-self: center;
}
.euiIcon { .euiIcon {
background-color: transparent; background-color: transparent;
@ -32,9 +23,7 @@
.rangeSliderAnchor__fieldNumber { .rangeSliderAnchor__fieldNumber {
font-weight: $euiFontWeightMedium; font-weight: $euiFontWeightMedium;
box-shadow: none; height: calc($euiButtonHeight - 3px) !important;
text-align: center;
background-color: transparent;
&.rangeSliderAnchor__fieldNumber--valid:invalid:not(:focus) { &.rangeSliderAnchor__fieldNumber--valid:invalid:not(:focus) {
background-image: none; // override the red underline for values between steps background-image: none; // override the red underline for values between steps

View file

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

View file

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

View file

@ -185,10 +185,10 @@ export const getSearchControlFactory = ({
return { return {
api, 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 * ControlPanel that are necessary for styling
*/ */
Component: (conrolStyleProps) => { Component: (controlPanelClassNames) => {
const currentSearch = useStateFromPublishingSubject(searchString); const currentSearch = useStateFromPublishingSubject(searchString);
useEffect(() => { useEffect(() => {
@ -202,7 +202,7 @@ export const getSearchControlFactory = ({
return ( return (
<EuiFieldSearch <EuiFieldSearch
{...conrolStyleProps} {...controlPanelClassNames}
incremental={true} incremental={true}
isClearable={false} // this will be handled by the clear floating action instead isClearable={false} // this will be handled by the clear floating action instead
value={currentSearch ?? ''} value={currentSearch ?? ''}

View file

@ -6,7 +6,6 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { CanClearSelections } from '@kbn/controls-plugin/public';
import { DataViewField } from '@kbn/data-views-plugin/common'; import { DataViewField } from '@kbn/data-views-plugin/common';
import { Filter } from '@kbn/es-query'; import { Filter } from '@kbn/es-query';
import { import {
@ -25,7 +24,6 @@ import {
export type DataControlApi = DefaultControlApi & export type DataControlApi = DefaultControlApi &
Omit<PublishesPanelTitle, 'hidePanelTitle'> & // control titles cannot be hidden Omit<PublishesPanelTitle, 'hidePanelTitle'> & // control titles cannot be hidden
HasEditCapabilities & HasEditCapabilities &
CanClearSelections &
PublishesDataViews & PublishesDataViews &
PublishesFilters & { PublishesFilters & {
setOutputFilter: (filter: Filter | undefined) => void; // a control should only ever output a **single** filter 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 { .timeSlider-playToggle:enabled {
background-color: $euiColorPrimary !important; background-color: $euiColorPrimary !important;
} }
.timeSlider-prependButton {
background-color: transparent !important;
}
.timeSlider__anchor { .timeSlider__anchor {
width: 100%; width: 100%;
height: 100%; height: 100%;
box-shadow: none; box-shadow: none;
overflow: hidden; overflow: hidden;
@include euiFormControlSideBorderRadius($euiFormControlBorderRadius, $side: 'right', $internal: true);
.euiText { .euiText {
background-color: $euiFormBackgroundColor !important; background-color: transparent !important;
// background-color: transparent !important; TODO revert to this rule once control group provides background color
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;

View file

@ -6,13 +6,12 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { EuiButtonIcon } from '@elastic/eui'; import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui';
import React, { FC, useCallback, useState } from 'react';
import { Observable, Subscription } from 'rxjs';
import { first } from 'rxjs';
import { ViewMode } from '@kbn/presentation-publishing'; 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 { PlayButton } from './play_button';
import { TimeSliderStrings } from './time_slider_strings';
interface Props { interface Props {
onNext: () => void; onNext: () => void;
@ -66,35 +65,43 @@ export const TimeSliderPrepend: FC<Props> = (props: Props) => {
}, [props, subscription, timeoutId]); }, [props, subscription, timeoutId]);
return ( return (
<div> <>
<EuiButtonIcon <EuiFlexItem grow={false}>
onClick={() => { <EuiButtonIcon
onPause(); onClick={() => {
props.onPrevious(); onPause();
}} props.onPrevious();
iconType="framePrevious" }}
color="text" iconType="framePrevious"
aria-label={TimeSliderStrings.control.getPreviousButtonAriaLabel()} color="text"
data-test-subj="timeSlider-previousTimeWindow" className={'timeSlider-prependButton'}
/> aria-label={TimeSliderStrings.control.getPreviousButtonAriaLabel()}
<PlayButton data-test-subj="timeSlider-previousTimeWindow"
onPlay={onPlay} />
onPause={onPause} </EuiFlexItem>
waitForControlOutputConsumersToLoad$={props.waitForControlOutputConsumersToLoad$} <EuiFlexItem grow={false}>
viewMode={props.viewMode} <PlayButton
disablePlayButton={props.disablePlayButton} onPlay={onPlay}
isPaused={isPaused} onPause={onPause}
/> waitForControlOutputConsumersToLoad$={props.waitForControlOutputConsumersToLoad$}
<EuiButtonIcon viewMode={props.viewMode}
onClick={() => { disablePlayButton={props.disablePlayButton}
onPause(); isPaused={isPaused}
props.onNext(); />
}} </EuiFlexItem>
iconType="frameNext" <EuiFlexItem grow={false}>
color="text" <EuiButtonIcon
aria-label={TimeSliderStrings.control.getNextButtonAriaLabel()} onClick={() => {
data-test-subj="timeSlider-nextTimeWindow" onPause();
/> props.onNext();
</div> }}
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 } = const { timeRangeMeta$, formatDate, cleanupTimeRangeSubscription } =
initTimeRangeSubscription(controlGroupApi, services); initTimeRangeSubscription(controlGroupApi, services);
const timeslice$ = new BehaviorSubject<[number, number] | undefined>(undefined); 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( function syncTimesliceWithTimeRangePercentage(
startPercentage: number | undefined, startPercentage: number | undefined,
endPercentage: number | undefined endPercentage: number | undefined
@ -73,18 +81,16 @@ export const getTimesliderControlFactory = (
]); ]);
setSelectedRange(to - from); setSelectedRange(to - from);
} }
const timeRangePercentage = initTimeRangePercentage(
initialState,
syncTimesliceWithTimeRangePercentage
);
function setTimeslice(timeslice?: Timeslice) { function setTimeslice(timeslice?: Timeslice) {
timeRangePercentage.setTimeRangePercentage(timeslice, timeRangeMeta$.value); timeRangePercentage.setTimeRangePercentage(timeslice, timeRangeMeta$.value);
timeslice$.next(timeslice); timeslice$.next(timeslice);
} }
const isAnchored$ = new BehaviorSubject<boolean | undefined>(initialState.isAnchored);
function setIsAnchored(isAnchored: boolean | undefined) { function setIsAnchored(isAnchored: boolean | undefined) {
isAnchored$.next(isAnchored); isAnchored$.next(isAnchored);
} }
let selectedRange: number | undefined; let selectedRange: number | undefined;
function setSelectedRange(nextSelectedRange?: number) { function setSelectedRange(nextSelectedRange?: number) {
selectedRange = selectedRange =
@ -176,10 +182,6 @@ export const getTimesliderControlFactory = (
setTimeslice([from, Math.min(to, timeRangeMax)]); setTimeslice([from, Math.min(to, timeRangeMax)]);
} }
const isPopoverOpen$ = new BehaviorSubject(false);
function setIsPopoverOpen(value: boolean) {
isPopoverOpen$.next(value);
}
const viewModeSubject = const viewModeSubject =
getViewModeSubject(controlGroupApi) ?? new BehaviorSubject('view' as ViewMode); getViewModeSubject(controlGroupApi) ?? new BehaviorSubject('view' as ViewMode);
@ -217,6 +219,9 @@ export const getTimesliderControlFactory = (
references: [], references: [],
}; };
}, },
clearSelections: () => {
setTimeslice(undefined);
},
CustomPrependComponent: () => { CustomPrependComponent: () => {
const [autoApplySelections, viewMode] = useBatchedPublishingSubjects( const [autoApplySelections, viewMode] = useBatchedPublishingSubjects(
controlGroupApi.autoApplySelections$, controlGroupApi.autoApplySelections$,
@ -229,7 +234,7 @@ export const getTimesliderControlFactory = (
onPrevious={onPrevious} onPrevious={onPrevious}
viewMode={viewMode} viewMode={viewMode}
disablePlayButton={!autoApplySelections} disablePlayButton={!autoApplySelections}
setIsPopoverOpen={setIsPopoverOpen} setIsPopoverOpen={(value) => isPopoverOpen$.next(value)}
waitForControlOutputConsumersToLoad$={waitForDashboardPanelsToLoad$} waitForControlOutputConsumersToLoad$={waitForDashboardPanelsToLoad$}
/> />
); );
@ -253,7 +258,7 @@ export const getTimesliderControlFactory = (
return { return {
api, api,
Component: (controlStyleProps) => { Component: (controlPanelClassNames) => {
const [isAnchored, isPopoverOpen, timeRangeMeta, timeslice] = const [isAnchored, isPopoverOpen, timeRangeMeta, timeslice] =
useBatchedPublishingSubjects(isAnchored$, isPopoverOpen$, timeRangeMeta$, timeslice$); useBatchedPublishingSubjects(isAnchored$, isPopoverOpen$, timeRangeMeta$, timeslice$);
@ -273,13 +278,12 @@ export const getTimesliderControlFactory = (
return ( return (
<EuiInputPopover <EuiInputPopover
{...controlStyleProps} {...controlPanelClassNames}
className="timeSlider__popoverOverride"
panelClassName="timeSlider__panelOverride" panelClassName="timeSlider__panelOverride"
input={ input={
<TimeSliderPopoverButton <TimeSliderPopoverButton
onClick={() => { onClick={() => {
setIsPopoverOpen(!isPopoverOpen); isPopoverOpen$.next(!isPopoverOpen);
}} }}
formatDate={formatDate} formatDate={formatDate}
from={from} from={from}
@ -287,7 +291,7 @@ export const getTimesliderControlFactory = (
/> />
} }
isOpen={isPopoverOpen} isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)} closePopover={() => isPopoverOpen$.next(false)}
panelPaddingSize="s" panelPaddingSize="s"
> >
<TimeSliderPopoverContent <TimeSliderPopoverContent

View file

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

View file

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

View file

@ -13,7 +13,7 @@ import type { DataControlInput } from '../types';
import { OptionsListSearchTechnique } from './suggestions_searching'; import { OptionsListSearchTechnique } from './suggestions_searching';
import type { OptionsListSortingType } from './suggestions_sorting'; 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 { export interface OptionsListEmbeddableInput extends DataControlInput {
searchTechnique?: OptionsListSearchTechnique; searchTechnique?: OptionsListSearchTechnique;