mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
82d32a757f
commit
80f3c191ce
18 changed files with 269 additions and 246 deletions
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -230,7 +230,7 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
|
||||
return {
|
||||
api,
|
||||
Component: (props, test) => {
|
||||
Component: () => {
|
||||
const controlsInOrder = useStateFromPublishingSubject(controlOrder);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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} />;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 ?? ''}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue