[Lens/SCSS] Replace scss to css-in-js for Lens codebase (#209768)

Replace SCSS in css-in-js for Lens codebase
This commit is contained in:
Marta Bondyra 2025-03-19 18:33:23 +01:00 committed by GitHub
parent 231507bf28
commit de52f41a5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
127 changed files with 1672 additions and 1979 deletions

File diff suppressed because one or more lines are too long

View file

@ -2,16 +2,11 @@
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react",
"@emotion/css/types"
],
},
"include": [
"**/*.ts",
"**/*.tsx",
"../../../../../typings/**/*"
],
"kbn_references": [
"@kbn/ui-theme"

View file

@ -75,9 +75,12 @@ function DimensionButtonImpl({
<EuiFlexItem>
<EuiToolTip content={message?.content} position="left">
<EuiLink
className="lnsLayerPanel__dimensionLink"
css={css`
width: 100%;
&:focus {
background-color: transparent;
text-decoration-thickness: ${euiTheme.border.thin} !important;
}
&:hover {
text-decoration: none;
}

View file

@ -59,6 +59,9 @@ export const DimensionTrigger = ({
<span
className="dimensionTrigger__textLabel"
css={css`
.domDroppable--replacing & {
text-decoration: line-through;
}
transition: background-color ${euiThemeVars.euiAnimSpeedFast} ease-in-out;
&:hover {

View file

@ -256,7 +256,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'10'
);
await testSubjects.click('unifiedHistogramEditFlyoutVisualization');
expect(await getCurrentVisTitle()).to.be('Line');
expect(await discover.getVisContextSuggestionType()).to.be('histogramForESQL');
});

View file

@ -1,69 +0,0 @@
// sass-lint:disable-block indentation, no-color-keywords
// SASSTODO: Create this in EUI
@mixin lnsOverflowShadowHorizontal {
$hideHeight: $euiScrollBarCorner * 1.25;
mask-image: linear-gradient(
to right,
transparentize($euiColorDanger, .9) 0%,
transparentize($euiColorDanger, 0) $hideHeight,
transparentize($euiColorDanger, 0) calc(100% - #{$hideHeight}),
transparentize($euiColorDanger, .9) 100%
);
}
// Removes EUI focus ring
@mixin removeEuiFocusRing {
outline: none;
&:focus-visible {
outline-style: none;
}
}
// Passes focus ring styles down to a child of a focused element
@mixin passDownFocusRing($target) {
@include removeEuiFocusRing;
#{$target} {
@include euiFocusBackground;
outline: $euiFocusRingSize solid currentColor; // Safari & Firefox
}
&:focus-visible #{$target} {
outline-style: auto; // Chrome
}
&:not(:focus-visible) #{$target} {
outline: none;
}
}
@mixin euiFlyout {
border-left: $euiBorderThin;
// The mixin augments the above
// sass-lint:disable mixins-before-declarations
@include euiBottomShadowLarge;
position: fixed;
top: 0;
bottom: 0;
right: 0;
height: 100%;
z-index: $euiZFlyout;
background: $euiColorEmptyShade;
display: flex;
flex-direction: column;
align-items: stretch;
}
@keyframes euiFlyoutAnimation {
0% {
opacity: 0;
transform: translateX(100%);
}
75% {
opacity: 1;
transform: translateX(0%);
}
}

View file

@ -1,10 +0,0 @@
$lnsPanelMinWidth: $euiSize * 18;
// These sizes also match canvas' page thumbnails for consistency
$lnsSuggestionHeight: 100px;
$lnsSuggestionWidth: 150px;
$lnsZLevel0: 0;
$lnsZLevel1: 1;
$lnsZLevel2: 2;
$lnsZLevel3: 3;

View file

@ -1,40 +0,0 @@
.lnsAppWrapper {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.lnsApp {
flex: 1 1 auto;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.lnsApp__frame {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
}
// Less-than-ideal styles to add a vertical divider after this button. Consider restructuring markup for better semantics and styling options in the future.
.lnsNavItem__withDivider {
@include euiBreakpoint('m', 'l', 'xl') {
margin-right: $euiSizeM;
position: relative;
}
&::after {
@include euiBreakpoint('m', 'l', 'xl') {
border-right: $euiBorderThin;
bottom: 0;
content: '';
display: block;
pointer-events: none;
position: absolute;
right: -$euiSizeS;
top: 0;
}
}
}

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import './app.scss';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import type { TimeRange } from '@kbn/es-query';
@ -13,6 +12,7 @@ import { EuiConfirmModal } from '@elastic/eui';
import { useExecutionContext, useKibana } from '@kbn/kibana-react-plugin/public';
import { OnSaveProps } from '@kbn/saved-objects-plugin/public';
import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
import { css } from '@emotion/react';
import { LensAppProps, LensAppServices } from './types';
import { LensTopNavMenu } from './lens_top_nav';
import { AddUserMessages, EditorFrameInstance, Simplify, UserMessagesGetter } from '../types';
@ -437,7 +437,18 @@ export function App({
return (
<>
<div className="lnsApp" data-test-subj="lnsApp" role="main">
<div
data-test-subj="lnsApp"
className="lnsApp"
role="main"
css={css`
flex: 1 1 auto;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
`}
>
<LensTopNavMenu
initialInput={initialInput}
redirectToOrigin={redirectToOrigin}

View file

@ -17,8 +17,9 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
import { DataViewPickerProps } from '@kbn/unified-search-plugin/public';
import { getManagedContentBadge } from '@kbn/managed-content-badge';
import moment from 'moment';
import { EuiCallOut } from '@elastic/eui';
import { EuiCallOut, UseEuiTheme, euiBreakpoint } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { SerializedStyles, css } from '@emotion/react';
import { LENS_APP_LOCATOR } from '../../common/locator/locator';
import { LENS_APP_NAME } from '../../common/constants';
import { LensAppServices, LensTopNavActions, LensTopNavMenuProps } from './types';
@ -102,6 +103,23 @@ function getSaveButtonMeta({
}
}
const navItemWithDividerStyles = (euiThemeContext: UseEuiTheme) => css`
${euiBreakpoint(euiThemeContext, ['m', 'l', 'xl'])} {
margin-right: ${euiThemeContext.euiTheme.size.m};
position: relative;
&:after {
border-right: ${euiThemeContext.euiTheme.border.thin};
bottom: 0;
content: '';
display: block;
pointer-events: none;
position: absolute;
right: -${euiThemeContext.euiTheme.size.s};
top: 0;
}
}
`;
function getLensTopNavConfig(options: {
isByValueMode: boolean;
actions: LensTopNavActions;
@ -123,7 +141,10 @@ function getLensTopNavConfig(options: {
contextFromEmbeddable,
isByValueMode,
} = options;
const topNavMenu: TopNavMenuData[] = [];
const topNavMenu: Array<
TopNavMenuData | ({ css: ({ euiTheme }: UseEuiTheme) => SerializedStyles } & TopNavMenuData)
> = [];
const showSaveAndReturn = actions.saveAndReturn.visible;
@ -150,13 +171,13 @@ function getLensTopNavConfig(options: {
values: { contextOriginatingApp },
}),
run: actions.goBack.execute,
className: 'lnsNavItem__withDivider',
testId: 'lnsApp_goBackToAppButton',
description: i18n.translate('xpack.lens.app.goBackLabel', {
defaultMessage: `Go back to {contextOriginatingApp}`,
values: { contextOriginatingApp },
}),
disableButton: !actions.goBack.enabled,
css: navItemWithDividerStyles,
});
}
@ -169,12 +190,12 @@ function getLensTopNavConfig(options: {
label: exploreDataInDiscoverLabel,
run: actions.getUnderlyingDataUrl.execute,
testId: 'lnsApp_openInDiscover',
className: 'lnsNavItem__withDivider',
description: exploreDataInDiscoverLabel,
disableButton: !actions.getUnderlyingDataUrl.enabled,
tooltip: actions.getUnderlyingDataUrl.tooltip,
target: '_blank',
href: actions.getUnderlyingDataUrl.getLink?.(),
css: navItemWithDividerStyles,
});
}
@ -210,11 +231,11 @@ function getLensTopNavConfig(options: {
defaultMessage: 'Settings',
}),
run: actions.openSettings.execute,
className: 'lnsNavItem__withDivider',
testId: 'lnsApp_settingsButton',
description: i18n.translate('xpack.lens.app.settingsAriaLabel', {
defaultMessage: 'Open the Lens settings menu',
}),
css: navItemWithDividerStyles,
});
if (actions.cancel.visible) {

View file

@ -398,8 +398,6 @@ export async function mountApp(
window.dispatchEvent(new HashChangeEvent('hashchange'));
});
params.element.classList.add('lnsAppWrapper');
render(
<KibanaRenderContextProvider {...coreStart}>
<KibanaContextProvider services={lensServices}>

View file

@ -122,6 +122,7 @@ export const FlyoutWrapper = ({
margin-left: -${euiThemeVars.euiFormMaxWidth};
pointer-events: none;
.euiFlyoutBody__overflow {
transform: initial;
-webkit-mask-image: none;
padding-left: inherit;
margin-left: inherit;

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { UseEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
export const dataPanelStyles = ({ euiTheme }: UseEuiTheme) => {
return css`
padding: ${euiTheme.size.base} ${euiTheme.size.base} 0;
.unifiedFieldListItemButton.kbnFieldButton {
background: none;
box-shadow: none;
margin-bottom: calc(${euiTheme.size.xs} / 2);
}
.unifiedFieldListItemButton__dragging {
background: ${euiTheme.colors.backgroundBasePlain};
}
`;
};

View file

@ -1,4 +0,0 @@
.lnsFieldItem__fieldPanel {
min-width: 260px;
max-width: 300px;
}

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import './field_item.scss';
import React, { useCallback, useState, useMemo } from 'react';
import { EuiText, EuiButton, EuiPopoverFooter } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -251,6 +249,10 @@ export function InnerFieldItem(props: FieldItemProps) {
isOpen={infoIsOpen}
closePopover={closePopover}
panelClassName="lnsFieldItem__fieldPanel"
panelStyle={{
minWidth: '260px',
maxWidth: '300px',
}}
initialFocus=".lnsFieldItem__fieldPanel"
data-test-subj="lnsFieldListPanelField"
panelProps={{

View file

@ -1,27 +0,0 @@
.lnsInnerIndexPatternDataPanel {
width: 100%;
height: 100%;
padding: $euiSize $euiSize 0;
.unifiedFieldListItemButton.kbnFieldButton {
background: none;
box-shadow: none;
margin-bottom: calc($euiSizeXS / 2);
}
.unifiedFieldListItemButton__dragging {
background: $euiColorBackgroundBasePlain;
}
}
.lnsInnerIndexPatternDataPanel__switcher {
min-width: 0;
}
.lnsInnerIndexPatternDataPanel__header {
display: flex;
align-items: center;
margin-bottom: $euiSizeS;
}
.lnsChangeIndexPatternPopover__trigger {
padding: 0 $euiSize;
}

View file

@ -10,8 +10,7 @@ import { DataView } from '@kbn/data-views-plugin/public';
import { UI_SETTINGS } from '@kbn/data-plugin/public';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { render, screen, within } from '@testing-library/react';
import { I18nProvider } from '@kbn/i18n-react';
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FormBasedDataPanel, FormBasedDataPanelProps } from './datapanel';
import * as UseExistingFieldsApi from '@kbn/unified-field-list/src/hooks/use_existing_fields';
@ -28,6 +27,7 @@ import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
import { createIndexPatternServiceMock } from '../../mocks/data_views_service_mock';
import { createMockFramePublicAPI } from '../../mocks';
import { DataViewsState } from '../../state_management';
import { renderWithProviders } from '../../test_utils/test_utils';
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
@ -241,11 +241,8 @@ const waitToLoad = async () =>
await act(async () => new Promise((resolve) => setTimeout(resolve, 0)));
const renderFormBasedDataPanel = async (propsOverrides?: Partial<FormBasedDataPanelProps>) => {
const { rerender, ...rest } = render(
<FormBasedDataPanel {...defaultProps} {...propsOverrides} />,
{
wrapper: ({ children }) => <I18nProvider>{children}</I18nProvider>,
}
const { rerender, ...rest } = renderWithProviders(
<FormBasedDataPanel {...defaultProps} {...propsOverrides} />
);
await waitToLoad();
return {

View file

@ -5,10 +5,9 @@
* 2.0.
*/
import './datapanel.scss';
import { uniq } from 'lodash';
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiCallOut, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { CoreStart } from '@kbn/core/public';
@ -41,6 +40,7 @@ import type {
import type { FormBasedPrivateState } from './types';
import { IndexPatternServiceAPI } from '../../data_views_service/service';
import { FieldItem } from '../common/field_item';
import { dataPanelStyles } from '../common/datapanel.styles';
export type FormBasedDataPanelProps = Omit<
DatasourceDataPanelProps<FormBasedPrivateState, Query>,
@ -102,6 +102,7 @@ export function FormBasedDataPanel({
const { indexPatterns, indexPatternRefs } = frame.dataViews;
const { currentIndexPatternId } = state;
const euiThemeContext = useEuiTheme();
const activeIndexPatterns = useMemo(() => {
return uniq(
(
@ -118,7 +119,7 @@ export function FormBasedDataPanel({
{Object.keys(indexPatterns).length === 0 && indexPatternRefs.length === 0 ? (
<EuiFlexGroup
gutterSize="m"
className="lnsInnerIndexPatternDataPanel"
css={dataPanelStyles(euiThemeContext)}
direction="column"
responsive={false}
>
@ -201,6 +202,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
layerFields?: string[];
activeIndexPatterns: IndexPattern[];
}) {
const euiThemeContext = useEuiTheme();
const { indexPatterns } = frame.dataViews;
const currentIndexPattern = indexPatterns[currentIndexPatternId];
@ -400,7 +402,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
return (
<FieldList
className="lnsInnerIndexPatternDataPanel"
css={dataPanelStyles(euiThemeContext)}
isProcessing={isProcessing}
prepend={<FieldListFilters {...fieldListFiltersProps} data-test-subj="lnsIndexPattern" />}
>

View file

@ -32,6 +32,7 @@ export function AdvancedOptions(props: { options: AdvancedOption[] }) {
}
css={css`
padding: 0 ${euiTheme.size.base} ${euiTheme.size.base};
color: ${euiTheme.colors.primary};
`}
>
{props.options.map(({ dataTestSubj, inlineElement }) => (

View file

@ -1,65 +0,0 @@
.lnsIndexPatternDimensionEditor {
height: 100%;
}
.lnsIndexPatternDimensionEditor__header {
position: sticky;
top: 0;
background: $euiColorEmptyShade;
z-index: $euiZLevel1; // Raise it above the elements that are after it in DOM order
padding: 0 $euiSize;
}
.lnsIndexPatternDimensionEditor-isFullscreen {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.lnsIndexPatternDimensionEditor--padded {
padding: $euiSize;
}
.lnsIndexPatternDimensionEditor--collapseNext {
margin-bottom: -$euiSizeL;
border-top: $euiBorderThin;
margin-top: 0 !important;
}
.lnsIndexPatternDimensionEditor__columns {
display: block;
column-count: 2;
column-gap: $euiSizeM;
}
.lnsIndexPatternDimensionEditor__operation .euiListGroupItem__label {
width: 100%;
}
.lnsIndexPatternDimensionEditor__operation > button {
padding-top: 0;
padding-bottom: 0;
min-block-size: $euiSizeL;
}
.lnsIndexPatternDimensionEditor__warning {
margin-bottom: $euiSize;
margin-top: $euiSizeS;
}
.lnsIndexPatternDimensionEditor__droppable {
padding: $euiSizeXS;
border-radius: $euiBorderRadius;
}
.lnsIndexPatternDimensionEditor__droppableItem {
margin-right: $euiSizeS;
}
.lnsIndexPatternDimensionEditor-advancedOptions button {
&:hover, &:focus {
text-decoration-color: $euiColorPrimary;
}
}

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import './dimension_editor.scss';
import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
@ -25,6 +24,7 @@ import {
EuiPanel,
EuiBasicTable,
EuiButtonIcon,
type UseEuiTheme,
} from '@elastic/eui';
import ReactDOM from 'react-dom';
import { NameInput } from '@kbn/visualization-ui-components';
@ -75,6 +75,7 @@ import { ParamEditorProps } from '../operations/definitions';
import { WrappingHelpPopover } from '../help_popover';
import { isColumn } from '../operations/definitions/helpers';
import type { FieldChoiceWithOperationType } from './field_select';
import { operationsButtonStyles } from './shared_styles';
import type { IndexPattern, IndexPatternField } from '../../../types';
import { documentField } from '../document_field';
@ -141,7 +142,9 @@ export function DimensionEditor(props: DimensionEditorProps) {
const temporaryQuickFunction = Boolean(temporaryState === quickFunctionsName);
const temporaryStaticValue = Boolean(temporaryState === staticValueOperationName);
const { euiTheme } = useEuiTheme();
const euiThemeContext = useEuiTheme();
const { euiTheme } = euiThemeContext;
const updateLayer = useCallback(
(newLayer: Partial<FormBasedLayer>) =>
@ -534,7 +537,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
isActive,
size: 's',
isDisabled: !!disabledStatus,
className: 'lnsIndexPatternDimensionEditor__operation',
css: operationsButtonStyles(euiThemeContext),
'data-test-subj': `lns-indexPatternDimension-${operationType}${
compatibleWithCurrentField ? '' : ' incompatible'
}`,
@ -811,7 +814,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
fullWidth
>
<EuiListGroup
className={sideNavItems.length > 3 ? 'lnsIndexPatternDimensionEditor__columns' : ''}
css={sideNavItems.length > 3 ? operationsTwoColumnsStyles(euiThemeContext) : undefined}
gutterSize="none"
color="primary"
listItems={
@ -1290,3 +1293,11 @@ export function DimensionEditor(props: DimensionEditorProps) {
</div>
);
}
const operationsTwoColumnsStyles = ({ euiTheme }: UseEuiTheme) => {
return css`
display: block;
column-count: 2;
column-gap: ${euiTheme.size.m};
`;
};

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { ReactWrapper, ShallowWrapper, ComponentType, mount } from 'enzyme';
import { ReactWrapper, ShallowWrapper } from 'enzyme';
import React, { ChangeEvent } from 'react';
import { screen, act, render, within } from '@testing-library/react';
import { screen, act, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { findTestSubject } from '@elastic/eui/lib/test';
import {
@ -17,7 +17,6 @@ import {
EuiRange,
EuiSelect,
EuiComboBoxProps,
EuiThemeProvider,
} from '@elastic/eui';
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
@ -48,9 +47,7 @@ import { TimeShift } from './time_shift';
import { ReducedTimeRange } from './reduced_time_range';
import { DimensionEditor } from './dimension_editor';
import { AdvancedOptions } from './advanced_options';
import { coreMock } from '@kbn/core/public/mocks';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { LensAppServices } from '../../../app_plugin/types';
import { mountWithProviders, renderWithProviders } from '../../../test_utils/test_utils';
jest.mock('./reference_editor', () => ({
ReferenceEditor: () => null,
@ -167,25 +164,6 @@ const bytesColumn: GenericIndexPatternColumn = {
params: { format: { id: 'bytes' } },
};
const wrappingComponent: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
return (
<KibanaContextProvider services={coreMock.createStart() as unknown as LensAppServices}>
<EuiThemeProvider>{children}</EuiThemeProvider>
</KibanaContextProvider>
);
};
function mountWithServices(component: React.ReactElement): ReactWrapper {
return mount(component, {
// This is an elegant way to wrap a component in Enzyme
// preserving the root at the component level rather than
// at the wrapper one
wrappingComponent: wrappingComponent as ComponentType<{}>,
});
}
/**
* The datasource exposes four main pieces of code which are tested at
* an integration test level. The main reason for this fairly high level
@ -292,21 +270,8 @@ describe('FormBasedDimensionEditor', () => {
});
const renderDimensionPanel = (propsOverrides = {}) => {
const Wrapper: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
return (
<KibanaContextProvider services={coreMock.createStart() as unknown as LensAppServices}>
{children}
</KibanaContextProvider>
);
};
const rtlRender = render(
<FormBasedDimensionEditorComponent {...defaultProps} {...propsOverrides} />,
{
wrapper: Wrapper,
}
const rtlRender = renderWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} {...propsOverrides} />
);
const getVisibleFieldSelectOptions = () => {
@ -396,14 +361,14 @@ describe('FormBasedDimensionEditor', () => {
};
});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
const options = getFieldSelectComboBox(wrapper).prop('options');
expect(options![1].options!.map(({ label }) => label)).toEqual(['timestampLabel', 'source']);
});
it('should indicate fields which are incompatible for the operation of the current column', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={getStateWithColumns({ col1: bytesColumn })}
@ -423,7 +388,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should indicate operations which are incompatible for the field of the current column', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={getStateWithColumns({ col1: bytesColumn })}
@ -447,7 +412,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should indicate when a transition is invalid due to filterOperations', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={getStateWithColumns({
@ -472,7 +437,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should not display hidden operation types', () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || [];
@ -482,7 +447,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should indicate that reference-based operations are not compatible when they are incomplete', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={getStateWithColumns({
@ -519,7 +484,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should indicate that reference-based operations are compatible sometimes', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={getStateWithColumns({
@ -566,7 +531,7 @@ describe('FormBasedDimensionEditor', () => {
it('should keep the operation when switching to another field compatible with this operation', async () => {
const initialState: FormBasedPrivateState = getStateWithColumns({ col1: bytesColumn });
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} state={initialState} />
);
@ -601,7 +566,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should switch operations when selecting a field that requires another operation', async () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
const comboBox = getFieldSelectComboBox(wrapper);
const option = comboBox.prop('options')![1].options!.find(({ label }) => label === 'source')!;
@ -633,7 +598,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should keep the field when switching to another operation compatible for this field', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={getStateWithColumns({ col1: bytesColumn })}
@ -665,7 +630,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should not set the state if selecting the currently active operation', () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
act(() => {
wrapper
@ -677,7 +642,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should update label and custom label flag on label input changes', () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
act(() => {
wrapper
@ -705,7 +670,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should not keep the label as long as it is the default label', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={getStateWithColumns({ col1: bytesColumn })}
@ -734,7 +699,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should keep the label on operation change if it is custom', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={getStateWithColumns({
@ -770,7 +735,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should remove customLabel flag if label is set to default', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={getStateWithColumns({
@ -810,7 +775,7 @@ describe('FormBasedDimensionEditor', () => {
describe('transient invalid state', () => {
it('should set the state if selecting an operation incompatible with the current field', async () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
await act(async () => {
await wrapper
@ -836,7 +801,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should show error message in invalid state', () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
act(() => {
wrapper
@ -850,7 +815,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should leave error state if a compatible operation is selected', () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
act(() => {
wrapper
@ -868,7 +833,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should leave error state if the original operation is re-selected', () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
act(() => {
wrapper
@ -886,7 +851,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should leave error state when switching from incomplete state to fieldless operation', async () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
await act(async () => {
await wrapper
@ -901,7 +866,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should leave error state when re-selecting the original fieldless function', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={getStateWithColumns({
@ -933,7 +898,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should indicate fields compatible with selected operation', async () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
await act(async () => {
await wrapper
@ -954,7 +919,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should select compatible operation if field not compatible with selected operation', async () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} columnId={'col2'} />
);
@ -1022,7 +987,7 @@ describe('FormBasedDimensionEditor', () => {
references: ['ref'],
},
});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} state={baseState} columnId={'col2'} />
);
@ -1049,7 +1014,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should select the Records field when count is selected on non-existing column', async () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={getStateWithColumns({})}
@ -1069,7 +1034,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should indicate document and field compatibility with selected document operation', async () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={getStateWithColumns({
@ -1101,7 +1066,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should set datasource state if compatible field is selected for operation', async () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
await act(async () => {
await wrapper
.find('button[data-test-subj="lns-indexPatternDimension-terms incompatible"]')
@ -1165,7 +1130,7 @@ describe('FormBasedDimensionEditor', () => {
}
it('should default to None if time scaling is not set', () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...getProps({})} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...getProps({})} />);
act(() => {
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
});
@ -1179,7 +1144,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should show current time scaling if set', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...getProps({ timeScale: 'd' })} />
);
act(() => {
@ -1195,7 +1160,7 @@ describe('FormBasedDimensionEditor', () => {
it('should allow to set time scaling initially', () => {
const props = getProps({});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
});
@ -1232,7 +1197,7 @@ describe('FormBasedDimensionEditor', () => {
operationType: 'sum',
label: 'Sum of bytes per hour',
});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click');
});
@ -1261,7 +1226,7 @@ describe('FormBasedDimensionEditor', () => {
operationType: 'sum',
label: 'Sum of bytes per hour',
});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
wrapper
.find('button[data-test-subj="lns-indexPatternDimension-average"]')
@ -1287,7 +1252,7 @@ describe('FormBasedDimensionEditor', () => {
it('should allow to change time scaling', () => {
const props = getProps({ timeScale: 's', label: 'Count of records per second' });
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
});
@ -1320,7 +1285,7 @@ describe('FormBasedDimensionEditor', () => {
it('should not adjust label if it is custom', () => {
const props = getProps({ timeScale: 's', customLabel: true, label: 'My label' });
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
wrapper
.find('[data-test-subj="indexPattern-time-scaling-unit"] select')
@ -1390,7 +1355,7 @@ describe('FormBasedDimensionEditor', () => {
}),
columnId: 'col2',
};
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
});
@ -1400,7 +1365,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should show current reduced time range if set', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...getProps({ reducedTimeRange: '5m' })} />
);
expect(
@ -1410,7 +1375,7 @@ describe('FormBasedDimensionEditor', () => {
it('should allow to set reduced time range initially', () => {
const props = getProps({});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
});
@ -1442,7 +1407,7 @@ describe('FormBasedDimensionEditor', () => {
operationType: 'sum',
label: 'Sum of bytes per hour',
});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click');
});
@ -1466,7 +1431,7 @@ describe('FormBasedDimensionEditor', () => {
const props = getProps({
timeShift: '1d',
});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
wrapper.find(ReducedTimeRange).find(EuiComboBox).prop('onCreateOption')!('7m', []);
});
@ -1490,7 +1455,7 @@ describe('FormBasedDimensionEditor', () => {
const props = getProps({
reducedTimeRange: '5 months',
});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
expect(wrapper.find(ReducedTimeRange).find(EuiComboBox).prop('isInvalid')).toBeTruthy();
@ -1547,7 +1512,7 @@ describe('FormBasedDimensionEditor', () => {
}),
columnId: 'col2',
};
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...props}
indexPatterns={{
@ -1566,7 +1531,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should show custom options if time shift is available', () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...getProps({})} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...getProps({})} />);
expect(
wrapper
.find(DimensionEditor)
@ -1576,7 +1541,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should show current time shift if set', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...getProps({ timeShift: '1d' })} />
);
expect(wrapper.find(TimeShift).find(EuiComboBox).prop('selectedOptions')[0].value).toEqual(
@ -1586,7 +1551,7 @@ describe('FormBasedDimensionEditor', () => {
it('should allow to set time shift initially', () => {
const props = getProps({});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
});
@ -1616,7 +1581,7 @@ describe('FormBasedDimensionEditor', () => {
operationType: 'sum',
label: 'Sum of bytes per hour',
});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click');
});
@ -1640,7 +1605,7 @@ describe('FormBasedDimensionEditor', () => {
const props = getProps({
timeShift: '1d',
});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
wrapper.find(TimeShift).find(EuiComboBox).prop('onCreateOption')!('1h', []);
});
@ -1664,7 +1629,7 @@ describe('FormBasedDimensionEditor', () => {
const props = getProps({
timeShift: '5 months',
});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
expect(wrapper.find(TimeShift).find(EuiComboBox).prop('isInvalid')).toBeTruthy();
@ -1681,7 +1646,7 @@ describe('FormBasedDimensionEditor', () => {
const props = getProps({
timeShift: 'startAt(2022-11-02T00:00:00.000Z)',
});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
expect(wrapper.find(TimeShift).find(EuiComboBox).prop('isInvalid')).toBeTruthy();
@ -1725,7 +1690,7 @@ describe('FormBasedDimensionEditor', () => {
}
it('should not show custom options if time scaling is not available', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...getProps({
operationType: 'terms',
@ -1744,7 +1709,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should show custom options if filtering is available', () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...getProps({})} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...getProps({})} />);
act(() => {
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
});
@ -1754,7 +1719,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should show current filter if set', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...getProps({ filter: { language: 'kuery', query: 'a: b' } })}
/>
@ -1775,7 +1740,7 @@ describe('FormBasedDimensionEditor', () => {
operationType: 'sum',
label: 'Sum of bytes per hour',
});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
wrapper.find('button[data-test-subj="lns-indexPatternDimension-count"]').simulate('click');
});
@ -1801,7 +1766,7 @@ describe('FormBasedDimensionEditor', () => {
filter: { language: 'kuery', query: 'a: b' },
});
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...props} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...props} />);
act(() => {
const { updateLayer, columnId, layer } = wrapper.find(Filtering).props();
@ -1832,7 +1797,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should render invalid field if field reference is broken', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={{
@ -1861,7 +1826,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should support selecting the operation before the field', async () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} columnId={'col2'} />
);
await act(async () => {
@ -1915,7 +1880,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should select operation directly if only one field is possible', async () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
columnId={'col2'}
@ -1959,7 +1924,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should select operation directly if only document is possible', async () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} columnId={'col2'} />
);
await act(async () => {
@ -1991,7 +1956,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should indicate compatible fields when selecting the operation first', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} columnId={'col2'} />
);
@ -2015,7 +1980,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should indicate document compatibility when document operation is selected', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={getStateWithColumns({
@ -2037,7 +2002,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should not update when selecting the current field again', async () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
const comboBox = getFieldSelectComboBox(wrapper);
@ -2053,7 +2018,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should show all operations that are not filtered out', () => {
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
filterOperations={(op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'}
@ -2086,7 +2051,7 @@ describe('FormBasedDimensionEditor', () => {
// Prevents field format from being loaded
setState.mockImplementation(() => {});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} columnId={'col2'} />
);
@ -2129,7 +2094,7 @@ describe('FormBasedDimensionEditor', () => {
const initialState: FormBasedPrivateState = getStateWithColumns({
col1: bytesColumn,
});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} state={initialState} />
);
act(() => {
@ -2149,7 +2114,7 @@ describe('FormBasedDimensionEditor', () => {
});
it('should keep the latest valid dimension when removing the selection in field combobox', () => {
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
act(() => {
getFieldSelectComboBox(wrapper as ReactWrapper).prop('onChange')!([]);
});
@ -2169,7 +2134,7 @@ describe('FormBasedDimensionEditor', () => {
},
});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} state={stateWithNumberCol} />
);
@ -2213,7 +2178,7 @@ describe('FormBasedDimensionEditor', () => {
},
},
});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} state={stateWithNumberCol} />
);
@ -2254,7 +2219,7 @@ describe('FormBasedDimensionEditor', () => {
},
});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} state={stateWithNumberCol} />
);
@ -2286,7 +2251,7 @@ describe('FormBasedDimensionEditor', () => {
it('should hide the top level field selector when switching from non-reference to reference', async () => {
(generateId as jest.Mock).mockReturnValue(`second`);
wrapper = mountWithServices(<FormBasedDimensionEditorComponent {...defaultProps} />);
wrapper = mountWithProviders(<FormBasedDimensionEditorComponent {...defaultProps} />);
expect(wrapper.find('ReferenceEditor')).toHaveLength(0);
@ -2311,7 +2276,7 @@ describe('FormBasedDimensionEditor', () => {
},
});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} state={stateWithReferences} />
);
@ -2337,7 +2302,7 @@ describe('FormBasedDimensionEditor', () => {
},
});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} state={stateWithInvalidCol} />
);
@ -2362,7 +2327,7 @@ describe('FormBasedDimensionEditor', () => {
}),
};
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={stateWithoutTime}
@ -2425,7 +2390,7 @@ describe('FormBasedDimensionEditor', () => {
}),
};
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...props} state={stateWithInvalidCol} />
);
@ -2444,7 +2409,7 @@ describe('FormBasedDimensionEditor', () => {
},
});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} state={stateWithFormulaColumn} />
);
@ -2465,7 +2430,7 @@ describe('FormBasedDimensionEditor', () => {
},
});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} state={stateWithFormulaColumn} />
);
@ -2484,7 +2449,7 @@ describe('FormBasedDimensionEditor', () => {
},
});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
supportStaticValue
@ -2500,7 +2465,7 @@ describe('FormBasedDimensionEditor', () => {
it('should select the quick function tab by default', () => {
const stateWithNoColumn: FormBasedPrivateState = getStateWithColumns({});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent {...defaultProps} state={stateWithNoColumn} />
);
@ -2515,7 +2480,7 @@ describe('FormBasedDimensionEditor', () => {
it('should select the static value tab when supported by default', () => {
const stateWithNoColumn: FormBasedPrivateState = getStateWithColumns({});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
supportStaticValue
@ -2540,7 +2505,7 @@ describe('FormBasedDimensionEditor', () => {
},
});
wrapper = mountWithServices(
wrapper = mountWithProviders(
<FormBasedDimensionEditorComponent
{...defaultProps}
state={stateWithFormulaColumn}

View file

@ -12,10 +12,16 @@
* 2.0.
*/
import './dimension_editor.scss';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiCallOut, EuiButtonGroup, EuiFormRow } from '@elastic/eui';
import {
EuiCallOut,
EuiButtonGroup,
EuiFormRow,
type UseEuiTheme,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { nonNullable } from '../../../utils';
import {
operationDefinitionMap,
@ -141,6 +147,7 @@ export const CalloutWarning = ({
currentOperationType: keyof typeof operationDefinitionMap | undefined;
temporaryStateType: TemporaryState;
}) => {
const euiThemeContext = useEuiTheme();
if (
temporaryStateType === 'none' ||
(currentOperationType != null && isQuickFunction(currentOperationType))
@ -154,7 +161,7 @@ export const CalloutWarning = ({
return (
<>
<EuiCallOut
className="lnsIndexPatternDimensionEditor__warning"
css={dimensionEditorWarningStyles(euiThemeContext)}
size="s"
title={i18n.translate('xpack.lens.indexPattern.staticValueWarning', {
defaultMessage: 'Static value currently applied',
@ -174,7 +181,7 @@ export const CalloutWarning = ({
return (
<>
<EuiCallOut
className="lnsIndexPatternDimensionEditor__warning"
css={dimensionEditorWarningStyles(euiThemeContext)}
size="s"
title={i18n.translate('xpack.lens.indexPattern.formulaWarning', {
defaultMessage: 'Formula currently applied',
@ -252,3 +259,10 @@ export const DimensionEditorButtonGroups = ({
</EuiFormRow>
);
};
const dimensionEditorWarningStyles = ({ euiTheme }: UseEuiTheme) => {
return css`
margin-bottom: ${euiTheme.size.base};
margin-top: ${euiTheme.size.s};
`;
};

View file

@ -1,7 +0,0 @@
.lnFieldSelect__option--incompatible {
color: $euiColorLightShade;
}
.lnFieldSelect__option--nonExistant {
background-color: $euiColorLightestShade;
}

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import './field_select.scss';
import { partition } from 'lodash';
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';

View file

@ -8,11 +8,9 @@
import React from 'react';
import { FormatSelector, FormatSelectorProps } from './format_selector';
import { GenericIndexPatternColumn } from '../../..';
import { LensAppServices } from '../../../app_plugin/types';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { I18nProvider } from '@kbn/i18n-react';
import { coreMock, docLinksServiceMock } from '@kbn/core/public/mocks';
import { fireEvent, render, screen, within } from '@testing-library/react';
import { renderWithProviders } from '../../../test_utils/test_utils';
import { docLinksServiceMock } from '@kbn/core/public/mocks';
import { fireEvent, screen, within } from '@testing-library/react';
import userEvent, { type UserEvent } from '@testing-library/user-event';
const props = {
@ -30,39 +28,10 @@ const props = {
docLinks: docLinksServiceMock.createStartContract(),
};
function createMockServices(): LensAppServices {
const services = coreMock.createStart();
services.uiSettings.get.mockImplementation(() => '0.0');
return {
...services,
docLinks: {
links: {
indexPatterns: { fieldFormattersNumber: '' },
},
},
} as unknown as LensAppServices;
}
const renderFormatSelector = (propsOverrides?: Partial<FormatSelectorProps>) => {
const WrappingComponent: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
return (
<I18nProvider>
<KibanaContextProvider services={createMockServices()}>{children}</KibanaContextProvider>
</I18nProvider>
);
};
return render(<FormatSelector {...props} {...propsOverrides} />, {
wrapper: WrappingComponent,
});
return renderWithProviders(<FormatSelector {...props} {...propsOverrides} />);
};
// Skipped for update of userEvent v14: https://github.com/elastic/kibana/pull/189949
// It looks like the individual tests within each it block are not really pure,
// see for example the first two tests, they run the same code but expect
// different results. With the updated userEvent code the tests no longer work
// with this setup and should be refactored.
describe('FormatSelector', () => {
let user: UserEvent;

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import './dimension_editor.scss';
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRowProps, EuiSpacer, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
@ -32,6 +31,7 @@ import type { FormBasedLayer } from '../types';
import type { IndexPattern, IndexPatternField, ParamEditorCustomProps } from '../../../types';
import type { FormBasedDimensionEditorProps } from './dimension_panel';
import { FormRow } from '../operations/definitions/shared_components';
import { operationsButtonStyles } from './shared_styles';
const operationDisplay = getOperationDisplay();
@ -56,7 +56,7 @@ const getFunctionOptions = (
return {
label,
value: operationType,
className: 'lnsIndexPatternDimensionEditor__operation',
css: operationsButtonStyles,
'data-test-subj': `lns-indexPatternDimension-${operationType}${
isCompatible ? '' : ' incompatible'
}`,
@ -204,7 +204,7 @@ export const ReferenceEditor = (props: ReferenceEditorProps) => {
const brokenFunctionOption = {
label: selectedOperationType && operationDisplay[selectedOperationType].displayName,
value: selectedOperationType,
className: 'lnsIndexPatternDimensionEditor__operation',
css: operationsButtonStyles,
'data-test-subj': `lns-indexPatternDimension-${selectedOperationType} incompatible`,
} as EuiComboBoxOptionOption<string>;
functionOptions?.push(brokenFunctionOption);

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { UseEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
export const operationsButtonStyles = ({ euiTheme }: UseEuiTheme) => {
return css`
> button {
padding-top: 0;
padding-bottom: 0;
min-block-size: ${euiTheme.size.l};
}
`;
};

View file

@ -1,13 +0,0 @@
.lnsHelpPopover__panel {
max-inline-size: $euiSize * 30 !important;
}
.lnsHelpPopover__content {
max-height: 40vh;
padding: $euiSizeM;
@include euiYScrollWithShadows;
}
.lnsHelpPopover__buttonIcon {
margin-right: $euiSizeXS;
}

View file

@ -16,10 +16,12 @@ import {
EuiWrappingPopoverProps,
EuiPopoverTitle,
EuiText,
type UseEuiTheme,
useEuiTheme,
} from '@elastic/eui';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { css } from '@emotion/react';
import { StartServices } from '../../types';
import './help_popover.scss';
export const HelpPopoverButton = ({
children,
@ -28,17 +30,39 @@ export const HelpPopoverButton = ({
children: string;
onClick: EuiLinkButtonProps['onClick'];
}) => {
const euiThemeContext = useEuiTheme();
return (
<EuiText size="xs">
<EuiLink onClick={onClick}>
<EuiIcon className="lnsHelpPopover__buttonIcon" size="s" type="help" />
<EuiIcon size="s" type="help" css={helpPopoverStyles.button(euiThemeContext)} />
{children}
</EuiLink>
</EuiText>
);
};
const HelpPopoverContent = ({ title, children }: { title?: string; children: ReactNode }) => {
const euiThemeContext = useEuiTheme();
return (
<>
{title && <EuiPopoverTitle paddingSize="m">{title}</EuiPopoverTitle>}
<EuiText className="eui-yScroll" size="s" css={helpPopoverStyles.content(euiThemeContext)}>
{children}
</EuiText>
</>
);
};
const helpPopoverStyles = {
button: ({ euiTheme }: UseEuiTheme) => css`
margin-right: ${euiTheme.size.xs};
`,
content: ({ euiTheme }: UseEuiTheme) => css`
max-height: 40vh;
padding: ${euiTheme.size.m};
`,
};
export const HelpPopover = ({
anchorPosition,
button,
@ -58,18 +82,13 @@ export const HelpPopover = ({
<EuiPopover
anchorPosition={anchorPosition}
button={button}
className="lnsHelpPopover"
closePopover={closePopover}
isOpen={isOpen}
ownFocus
panelClassName="lnsHelpPopover__panel"
panelStyle={{ maxInlineSize: '480px' }}
panelPaddingSize="none"
>
{title && <EuiPopoverTitle paddingSize="m">{title}</EuiPopoverTitle>}
<EuiText className="lnsHelpPopover__content" size="s">
{children}
</EuiText>
<HelpPopoverContent title={title}>{children}</HelpPopoverContent>
</EuiPopover>
);
};
@ -96,18 +115,13 @@ export const WrappingHelpPopover = ({
<EuiWrappingPopover
anchorPosition={anchorPosition}
button={button}
className="lnsHelpPopover"
closePopover={closePopover}
isOpen={isOpen}
ownFocus
panelClassName="lnsHelpPopover__panel"
panelStyle={{ maxInlineSize: '480px' }}
panelPaddingSize="none"
>
{title && <EuiPopoverTitle paddingSize="m">{title}</EuiPopoverTitle>}
<EuiText className="lnsHelpPopover__content" size="s">
{children}
</EuiText>
<HelpPopoverContent title={title}>{children}</HelpPopoverContent>
</EuiWrappingPopover>
</KibanaRenderContextProvider>
);

View file

@ -8,10 +8,11 @@
import React from 'react';
import { FormBasedPrivateState } from './types';
import { FormBasedLayerPanelProps, LayerPanel } from './layerpanel';
import { fireEvent, render, screen, within } from '@testing-library/react';
import { fireEvent, screen, within } from '@testing-library/react';
import { getFieldByNameFactory } from './pure_helpers';
import { TermsIndexPatternColumn } from './operations';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../../test_utils/test_utils';
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { value: 400 });
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { value: 200 });
@ -225,7 +226,7 @@ describe('Layer Data Panel', () => {
};
});
const renderLayerPanel = () => render(<LayerPanel {...defaultProps} />);
const renderLayerPanel = () => renderWithProviders(<LayerPanel {...defaultProps} />);
it('should list all index patterns', async () => {
renderLayerPanel();

View file

@ -1,3 +0,0 @@
.lnsIndexPatternDimensionEditor__filtersEditor {
width: $euiSize * 60;
}

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import './filter_popover.scss';
import React, { useState } from 'react';
import { EuiPopover, EuiSpacer } from '@elastic/eui';
import type { Query } from '@kbn/es-query';
@ -66,7 +64,9 @@ export const FilterPopover = ({
return (
<EuiPopover
data-test-subj="indexPattern-filters-existingFilterContainer"
panelClassName="lnsIndexPatternDimensionEditor__filtersEditor"
panelStyle={{
width: '960px',
}}
isOpen={isOpen}
ownFocus
closePopover={closePopover}

View file

@ -1,6 +0,0 @@
.lnsFiltersOperation__popoverButton {
@include euiTextBreakWord;
@include euiFontSizeS;
min-height: $euiSizeXL;
width: 100%;
}

View file

@ -13,12 +13,13 @@ import type { IUiSettingsClient, HttpSetup } from '@kbn/core/public';
import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { fireEvent, render, screen } from '@testing-library/react';
import { fireEvent, screen } from '@testing-library/react';
import type { FiltersIndexPatternColumn } from '.';
import { filtersOperation } from '..';
import type { FormBasedLayer } from '../../../types';
import { createMockedIndexPattern } from '../../../mocks';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../../../../../test_utils/test_utils';
const uiSettingsMock = {} as IUiSettingsClient;
@ -304,7 +305,7 @@ describe('filters', () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const updateLayerSpy = jest.fn();
render(
renderWithProviders(
<InlineOptions
{...defaultProps}
layer={layer}
@ -346,7 +347,7 @@ describe('filters', () => {
describe('Modify filters', () => {
it('should correctly show existing filters ', () => {
const updateLayerSpy = jest.fn();
render(
renderWithProviders(
<InlineOptions
{...defaultProps}
layer={layer}
@ -366,7 +367,7 @@ describe('filters', () => {
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
jest.useFakeTimers();
const updateLayerSpy = jest.fn();
render(
renderWithProviders(
<InlineOptions
{...defaultProps}
layer={layer}

View file

@ -5,11 +5,10 @@
* 2.0.
*/
import './filters.scss';
import React, { useState } from 'react';
import { omit } from 'lodash';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiLink, htmlIdGenerator } from '@elastic/eui';
import { EuiFormRow, EuiLink, htmlIdGenerator, useEuiTheme } from '@elastic/eui';
import type { Query } from '@kbn/es-query';
import type { AggFunctionsMapping } from '@kbn/data-plugin/public';
import { queryFilterToAst } from '@kbn/data-plugin/common';
@ -27,6 +26,7 @@ import type { BaseIndexPatternColumn } from '../column_types';
import { FilterPopover } from './filter_popover';
import { TermsIndexPatternColumn } from '../terms';
import { isColumnOfType } from '../helpers';
import { draggablePopoverButtonStyles } from '../styles';
const generateId = htmlIdGenerator();
const OPERATION_NAME = 'filters';
@ -181,6 +181,7 @@ export const FilterList = ({
indexPattern: IndexPattern;
defaultQuery: Filter;
}) => {
const euiThemeContext = useEuiTheme();
const [activeFilterId, setActiveFilterId] = useState('');
const [localFilters, setLocalFilters] = useState(() =>
filters.map((filter) => ({ ...filter, id: generateId() }))
@ -275,6 +276,7 @@ export const FilterList = ({
title={i18n.translate('xpack.lens.indexPattern.filters.clickToEdit', {
defaultMessage: 'Click to edit',
})}
css={draggablePopoverButtonStyles(euiThemeContext)}
>
{filter.label || (filter.input.query as string) || defaultLabel}
</EuiLink>
@ -285,9 +287,7 @@ export const FilterList = ({
})}
</DragDropBuckets>
<NewBucketButton
onClick={() => {
onAddFilter();
}}
onClick={onAddFilter}
label={i18n.translate('xpack.lens.indexPattern.filters.addaFilter', {
defaultMessage: 'Add a filter',
})}

View file

@ -1,100 +0,0 @@
.lnsFormula {
display: flex;
flex-direction: column;
.lnsIndexPatternDimensionEditor-isFullscreen & {
height: 100%;
}
& > * {
flex: 1;
min-height: 0;
}
& > * + * {
border-top: $euiBorderThin;
}
}
.lnsFormula__editor {
.lnsIndexPatternDimensionEditor-isFullscreen & {
border-bottom: none;
display: flex;
flex-direction: column;
}
& > * + * {
border-top: $euiBorderThin;
}
}
.lnsFormula__editorHeader,
.lnsFormula__editorFooter {
padding: $euiSizeS;
}
.lnsFormula__editorFooter {
// make sure docs are rendered in front of monaco
z-index: 1;
border-bottom-right-radius: $euiBorderRadius;
border-bottom-left-radius: $euiBorderRadius;
}
.lnsFormula__editorHeaderGroup,
.lnsFormula__editorFooterGroup {
display: block; // Overrides EUI's styling of `display: flex` on `EuiFlexItem` components
}
.lnsFormula__editorContent {
background-color: $euiColorBackgroundBasePlain;
min-height: 0;
position: relative;
.lnsIndexPatternDimensionEditor:not(.lnsIndexPatternDimensionEditor-isFullscreen) & {
height: 200px;
}
.lnsIndexPatternDimensionEditor-isFullscreen & {
flex: 1;
}
}
.lnsFormula__editorPlaceholder {
position: absolute;
top: 0;
left: $euiSize;
right: 0;
color: $euiTextSubduedColor;
// Matches monaco editor
font-family: Menlo, Monaco, 'Courier New', monospace;
pointer-events: none;
}
.lnsFormula__warningText + .lnsFormula__warningText {
margin-top: $euiSizeS;
border-top: $euiBorderThin;
padding-top: $euiSizeS;
}
.lnsFormula__editorHelp--inline {
align-items: center;
display: flex;
padding: $euiSizeXS;
& > * + * {
margin-left: $euiSizeXS;
}
}
.lnsFormula__editorError {
white-space: nowrap;
}
.lnsFormula__docs {
background: $euiColorEmptyShade;
}
.lnsFormulaOverflow {
// Needs to be higher than the modal and all flyouts
z-index: $euiZLevel9 + 1;
}

View file

@ -25,10 +25,10 @@ import {
EuiToolTip,
EuiSpacer,
useEuiTheme,
type UseEuiTheme,
} from '@elastic/eui';
import useUnmount from 'react-use/lib/useUnmount';
import { monaco } from '@kbn/monaco';
import classNames from 'classnames';
import { CodeEditor, CodeEditorProps } from '@kbn/code-editor';
import { UI_SETTINGS } from '@kbn/data-plugin/public';
import { useDebounceWithOptions } from '../../../../../../shared_components';
@ -50,7 +50,6 @@ import {
} from './math_completion';
import { LANGUAGE_ID } from './math_tokenization';
import './formula.scss';
import { FormulaIndexPatternColumn } from '../formula';
import { insertOrReplaceFormulaColumn } from '../parse';
import { filterByVisibleOperation } from '../util';
@ -126,7 +125,8 @@ export function FormulaEditor({
const disposables = React.useRef<monaco.IDisposable[]>([]);
const editor1 = React.useRef<monaco.editor.IStandaloneCodeEditor>();
const { euiTheme } = useEuiTheme();
const euiThemeContext = useEuiTheme();
const { euiTheme } = euiThemeContext;
const visibleOperationsMap = useMemo(
() => filterByVisibleOperation(operationDefinitionMap),
@ -685,10 +685,12 @@ export function FormulaEditor({
// in the behavior of Monaco when it's first loaded and then reloaded.
return (
<div
className={classNames({
lnsIndexPatternDimensionEditor: true,
'lnsIndexPatternDimensionEditor-isFullscreen': isFullscreen,
})}
css={[
sharedEditorStyles.self(euiThemeContext),
isFullscreen
? fullscreenEditorStyles(euiThemeContext)
: defaultEditorStyles(euiThemeContext),
]}
>
{!isFullscreen && (
<EuiFormLabel
@ -705,17 +707,21 @@ export function FormulaEditor({
<div
className="lnsFormula"
css={{
css={css({
backgroundColor: euiTheme.colors.backgroundBaseSubdued,
border: isFullscreen ? 'none' : euiTheme.border.thin,
borderRadius: isFullscreen ? 0 : euiTheme.border.radius.medium,
height: isFullscreen ? '100%' : 'auto',
}}
})}
>
<div className="lnsFormula__editor">
<div className="lnsFormula__editorHeader">
<div css={sharedEditorStyles.editorHeader(euiThemeContext)}>
<EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}>
<EuiFlexItem className="lnsFormula__editorHeaderGroup">
<EuiFlexItem
css={css`
display: block;
`}
>
<EuiToolTip
content={
isWordWrapped
@ -752,7 +758,12 @@ export function FormulaEditor({
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem className="lnsFormula__editorHeaderGroup" grow={false}>
<EuiFlexItem
css={css`
display: block;
`}
grow={false}
>
<EuiButtonEmpty
onClick={() => {
toggleFullscreen();
@ -803,7 +814,7 @@ export function FormulaEditor({
/>
{!text ? (
<div className="lnsFormula__editorPlaceholder">
<div css={sharedEditorStyles.editorPlaceholder(euiThemeContext)}>
<EuiText color="subdued" size="s">
{i18n.translate('xpack.lens.formulaPlaceholderText', {
defaultMessage: 'Type a formula by combining functions with math, like:',
@ -815,9 +826,9 @@ export function FormulaEditor({
) : null}
</div>
<div className="lnsFormula__editorFooter">
<div css={sharedEditorStyles.editorFooter(euiThemeContext)}>
<EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}>
<EuiFlexItem className="lnsFormula__editorFooterGroup">
<EuiFlexItem grow={false}>
{isFullscreen ? (
<EuiToolTip
content={
@ -837,6 +848,7 @@ export function FormulaEditor({
defaultMessage: 'Hide function reference',
})}
className="lnsFormula__editorHelp lnsFormula__editorHelp--inline"
css={sharedEditorStyles.editorHelpLink(euiThemeContext)}
color="text"
onClick={() => setIsHelpOpen(!isHelpOpen)}
>
@ -866,7 +878,7 @@ export function FormulaEditor({
</EuiFlexItem>
{errorCount || warningCount ? (
<EuiFlexItem className="lnsFormula__editorFooterGroup" grow={false}>
<EuiFlexItem grow={false}>
<EuiPopover
ownFocus={false}
isOpen={isWarningOpen}
@ -874,6 +886,9 @@ export function FormulaEditor({
button={
<EuiButtonEmpty
color={errorCount ? 'danger' : 'warning'}
css={css`
white-space: nowrap;
`}
className="lnsFormula__editorError"
iconType="warning"
size="xs"
@ -904,7 +919,10 @@ export function FormulaEditor({
`}
>
{warnings.map(({ message, severity }, index) => (
<div key={index} className="lnsFormula__warningText">
<div
key={index}
css={index !== 0 && sharedEditorStyles.warningText(euiThemeContext)}
>
<EuiText
size="s"
color={
@ -926,13 +944,8 @@ export function FormulaEditor({
{/* fix the css here */}
{isFullscreen && isHelpOpen ? (
<div
className="lnsFormula__docs documentation__docs--inline"
css={css`
display: flex;
flex-direction: column;
// make sure docs are rendered in front of monaco
z-index: 1;
`}
className="documentation__docs--inline"
css={sharedEditorStyles.formulaDocs(euiThemeContext)}
>
<LanguageDocumentationPopoverContent
language="Formula"
@ -944,3 +957,101 @@ export function FormulaEditor({
</div>
);
}
const sharedEditorStyles = {
self: ({ euiTheme }: UseEuiTheme) => {
return css`
.lnsFormula {
display: flex;
flex-direction: column;
& > * {
flex: 1;
min-height: 0;
}
& > * + * {
border-top: ${euiTheme.border.thin};
}
}
.lnsFormulaOverflow {
// Needs to be higher than the modal and all flyouts
z-index: ${euiTheme.levels.toast} + 1;
}
.lnsFormula__editorContent {
background-color: ${euiTheme.colors.backgroundBasePlain};
min-height: 0;
position: relative;
}
`;
},
formulaDocs: ({ euiTheme }: UseEuiTheme) => css`
display: flex;
flex-direction: column;
// make sure docs are rendered in front of monaco
z-index: 1;
background: ${euiTheme.colors.backgroundBasePlain};
`,
editorHeader: ({ euiTheme }: UseEuiTheme) => css`
padding: ${euiTheme.size.s};
`,
editorFooter: ({ euiTheme }: UseEuiTheme) => css`
padding: ${euiTheme.size.s};
// make sure docs are rendered in front of monaco
z-index: 1;
border-bottom-right-radius: ${euiTheme.border.radius.medium};
border-bottom-left-radius: ${euiTheme.border.radius.medium};
`,
editorPlaceholder: ({ euiTheme }: UseEuiTheme) => css`
position: absolute;
top: 0;
left: ${euiTheme.size.base};
right: 0;
color: ${euiTheme.colors.textSubdued}
// Matches monaco editor
font-family: Menlo, Monaco, 'Courier New', monospace;
pointer-events: none;
`,
warningText: ({ euiTheme }: UseEuiTheme) => css`
margin-top: ${euiTheme.size.s};
border-top: ${euiTheme.border.thin};
padding-top: ${euiTheme.size.s};
`,
editorHelpLink: ({ euiTheme }: UseEuiTheme) => css`
align-items: center;
display: flex;
padding: ${euiTheme.size.xs};
& > * + * {
margin-left: ${euiTheme.size.xs};
}
`,
};
const defaultEditorStyles = ({ euiTheme }: UseEuiTheme) => {
return css`
.lnsFormula__editorContent {
height: 200px;
}
`;
};
const fullscreenEditorStyles = ({ euiTheme }: UseEuiTheme) => {
return css`
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
.lnsFormula__editor {
border-bottom: none;
display: flex;
flex-direction: column;
& > * + * {
border-top: ${euiTheme.border.thin};
}
}
.lnsFormula__editorContent {
flex: 1;
}
`;
};

View file

@ -1,10 +0,0 @@
.lnsRangesOperation__popoverButton {
@include euiTextBreakWord;
@include euiFontSizeS;
min-height: $euiSizeXL;
width: 100%;
}
.lnsRangesOperation__popoverNumberField {
width: 14ch; // Roughly 10 characters plus extra for the padding
}

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import './advanced_editor.scss';
import React, { useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import {
@ -21,6 +19,7 @@ import {
EuiToolTip,
htmlIdGenerator,
keys,
useEuiTheme,
} from '@elastic/eui';
import { IFieldFormat } from '@kbn/field-formats-plugin/common';
import {
@ -28,11 +27,13 @@ import {
DraggableBucketContainer,
NewBucketButton,
} from '@kbn/visualization-ui-components';
import { css } from '@emotion/react';
import { useDebounceWithOptions } from '../../../../../shared_components';
import { RangeTypeLens, isValidRange } from './ranges';
import { FROM_PLACEHOLDER, TO_PLACEHOLDER, TYPING_DEBOUNCE_TIME } from './constants';
import { LabelInput } from '../shared_components';
import { isValidNumber } from '../helpers';
import { draggablePopoverButtonStyles } from '../styles';
const generateId = htmlIdGenerator();
@ -101,7 +102,9 @@ export const RangePopover = ({
<EuiFlexGroup gutterSize="s" responsive={false} alignItems="center">
<EuiFlexItem>
<EuiFieldNumber
className="lnsRangesOperation__popoverNumberField"
css={css`
width: 14ch; // Roughly 10 characters plus extra for the padding
`}
value={isValidNumber(from) ? Number(from) : ''}
onChange={({ target }) => {
const newRange = {
@ -132,7 +135,9 @@ export const RangePopover = ({
</EuiFlexItem>
<EuiFlexItem>
<EuiFieldNumber
className="lnsRangesOperation__popoverNumberField"
css={css`
width: 14ch; // Roughly 10 characters plus extra for the padding
`}
value={isValidNumber(to) ? Number(to) : ''}
inputRef={(node) => {
if (toRef && node) {
@ -203,6 +208,7 @@ export const AdvancedRangeEditor = ({
onToggleEditor: () => void;
formatter: IFieldFormat;
}) => {
const euiThemeContext = useEuiTheme();
const [activeRangeId, setActiveRangeId] = useState('');
// use a local state to store ids with range objects
const [localRanges, setLocalRanges] = useState<LocalRangeType[]>(() =>
@ -303,8 +309,8 @@ export const AdvancedRangeEditor = ({
<EuiLink
color="text"
onClick={() => changeActiveRange(range.id)}
className="lnsRangesOperation__popoverButton"
data-test-subj="indexPattern-ranges-popover-trigger"
data-test-subj="dataView-ranges-popover-trigger"
css={draggablePopoverButtonStyles(euiThemeContext)}
>
<EuiText
size="s"

View file

@ -6,7 +6,6 @@
*/
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { EuiFieldNumber, EuiRange, EuiButtonEmpty, EuiLink, EuiText } from '@elastic/eui';
import { IUiSettingsClient, HttpSetup } from '@kbn/core/public';
@ -15,6 +14,7 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks';
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
import { mountWithProviders } from '../../../../../test_utils/test_utils';
import type { FormBasedLayer } from '../../../types';
import { rangeOperation } from '..';
import { RangeIndexPatternColumn } from './ranges';
@ -376,7 +376,7 @@ describe('ranges', () => {
it('should start update the state with the default maxBars value', () => {
const updateLayerSpy = jest.fn();
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -392,7 +392,7 @@ describe('ranges', () => {
it('should update state when changing Max bars number', () => {
const updateLayerSpy = jest.fn();
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -435,7 +435,7 @@ describe('ranges', () => {
it('should update the state using the plus or minus buttons by the step amount', () => {
const updateLayerSpy = jest.fn();
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -505,7 +505,7 @@ describe('ranges', () => {
it('should show one range interval to start with', () => {
const updateLayerSpy = jest.fn();
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -521,7 +521,7 @@ describe('ranges', () => {
it('should use the parentFormat to create the trigger label', () => {
const updateLayerSpy = jest.fn();
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -532,7 +532,7 @@ describe('ranges', () => {
);
expect(
instance.find('[data-test-subj="indexPattern-ranges-popover-trigger"]').first().text()
instance.find('[data-test-subj="dataView-ranges-popover-trigger"]').first().text()
).toBe('0 - 1000');
});
@ -541,7 +541,7 @@ describe('ranges', () => {
// we intercept the formatter without an id assigned an print "Error"
const updateLayerSpy = jest.fn();
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -560,14 +560,14 @@ describe('ranges', () => {
);
expect(
instance.find('[data-test-subj="indexPattern-ranges-popover-trigger"]').first().text()
instance.find('[data-test-subj="dataView-ranges-popover-trigger"]').first().text()
).not.toBe('Error');
});
it('should add a new range', () => {
const updateLayerSpy = jest.fn();
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -621,7 +621,7 @@ describe('ranges', () => {
it('should add a new range with custom label', () => {
const updateLayerSpy = jest.fn();
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -674,7 +674,7 @@ describe('ranges', () => {
it('should open a popover to edit an existing range', () => {
const updateLayerSpy = jest.fn();
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -721,7 +721,7 @@ describe('ranges', () => {
it('should not accept invalid ranges', () => {
const updateLayerSpy = jest.fn();
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -771,7 +771,7 @@ describe('ranges', () => {
label: '',
});
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -809,7 +809,7 @@ describe('ranges', () => {
label: '',
});
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -841,7 +841,7 @@ describe('ranges', () => {
it('should correctly handle the default formatter for the field', () => {
const updateLayerSpy = jest.fn();
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -871,7 +871,7 @@ describe('ranges', () => {
params: { decimals: 0 },
};
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -895,7 +895,7 @@ describe('ranges', () => {
it('should not update the state on mount', () => {
const updateLayerSpy = jest.fn();
mount(
mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}
@ -915,7 +915,7 @@ describe('ranges', () => {
params: { decimals: 3 },
};
const instance = mount(
const instance = mountWithProviders(
<InlineOptions
{...defaultOptions}
layer={layer}

View file

@ -1,3 +0,0 @@
.lnsIndexPatternDimensionEditor__labelCustomRank {
min-width: 96px;
}

View file

@ -7,7 +7,7 @@
import React from 'react';
import { EuiFormLabel, EuiFormRow, EuiFormRowProps } from '@elastic/eui';
import './form_row.scss';
import { css } from '@emotion/react';
type FormRowProps = EuiFormRowProps & { isInline?: boolean };
@ -20,7 +20,11 @@ export const FormRow = ({ children, label, isInline, ...props }: FormRowProps) =
<div data-test-subj={props['data-test-subj']}>
{React.cloneElement(children, {
prepend: (
<EuiFormLabel className="lnsIndexPatternDimensionEditor__labelCustomRank">
<EuiFormLabel
css={css`
min-width: 96px;
`}
>
{label}
</EuiFormLabel>
),

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { euiFontSize, euiTextBreakWord, UseEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
export const draggablePopoverButtonStyles = (euiThemeContext: UseEuiTheme) => {
return css`
${euiTextBreakWord()};
${euiFontSize(euiThemeContext, 's')};
min-height: ${euiThemeContext.euiTheme.size.xl};
width: 100%;
`;
};

View file

@ -25,6 +25,7 @@ import {
import { uniq } from 'lodash';
import { AggFunctionsMapping } from '@kbn/data-plugin/public';
import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
import { css } from '@emotion/react';
import { DOCUMENT_FIELD_NAME } from '../../../../../../common/constants';
import { insertOrReplaceColumn, updateColumnParam, updateDefaultLabels } from '../../layer_helpers';
import type { DataType, OperationMetadata } from '../../../../../types';
@ -1008,6 +1009,9 @@ The top values of a specified field ranked by the chosen metric.
<>
<EuiSpacer size="m" />
<EuiAccordion
css={css`
color: ${euiTheme.colors.primary};
`}
id="lnsTermsAdvanced"
arrowProps={{ color: 'primary' }}
buttonContent={

View file

@ -25,11 +25,13 @@ import {
useGroupedFields,
} from '@kbn/unified-field-list';
import { OverrideFieldGroupDetails } from '@kbn/unified-field-list/src/types';
import { useEuiTheme } from '@elastic/eui';
import type { DatasourceDataPanelProps } from '../../../types';
import type { TextBasedPrivateState } from '../types';
import { getStateFromAggregateQuery } from '../utils';
import { FieldItem } from '../../common/field_item';
import { getColumnsFromCache } from '../fieldlist_cache';
import { dataPanelStyles } from '../../common/datapanel.styles';
const getCustomFieldType: GetCustomFieldType<DatatableColumn> = (field) => field?.meta.type;
@ -129,7 +131,7 @@ export function TextBasedDataPanel({
},
[hasSuggestionForField, dropOntoWorkspace]
);
const euiThemeContext = useEuiTheme();
return (
<KibanaContextProvider
services={{
@ -137,7 +139,7 @@ export function TextBasedDataPanel({
}}
>
<FieldList
className="lnsInnerIndexPatternDataPanel"
css={dataPanelStyles(euiThemeContext)}
isProcessing={!dataHasLoaded}
prepend={
<FieldListFilters {...fieldListFiltersProps} data-test-subj="lnsTextBasedLanguages" />

View file

@ -15,6 +15,8 @@ import {
Droppable,
DroppableProps,
} from '@kbn/dom-drag-drop';
import { css } from '@emotion/react';
import { useEuiTheme } from '@elastic/eui';
import { isDraggedField } from '../../../../utils';
import {
Datasource,
@ -64,6 +66,7 @@ export function DraggableDimensionButton({
indexPatterns: IndexPatternMap;
}) {
const [{ dragging }] = useDragDropContext();
const { euiTheme } = useEuiTheme();
let getDropProps;
@ -139,6 +142,12 @@ export function DraggableDimensionButton({
ref={registerNewButtonRefMemoized}
className="lnsLayerPanel__dimensionContainer"
data-test-subj={group.dataTestSubj}
css={css`
position: relative;
& + & {
margin-top: ${euiTheme.size.s};
}
`}
>
<Draggable
dragType={isOperation(dragging) ? 'move' : 'copy'}

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import './chart_switch.scss';
import React from 'react';
import {
EuiFlexItem,
@ -36,7 +35,7 @@ export const ChartOption = ({
`}
>
<EuiFlexItem grow={false}>
<EuiIcon className="lnsChartSwitch__chartIcon" type={option.icon || 'empty'} />
<EuiIcon type={option.icon || 'empty'} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" data-test-subj="lnsChartSwitch-option-label">
@ -88,7 +87,6 @@ const DataLossWarning = ({ content, id }: { content?: string; id: string }) => {
color={euiTheme.colors.warning}
content={content}
iconProps={{
className: 'lnsChartSwitch__chartIcon',
'data-test-subj': `lnsChartSwitchPopoverAlert_${id}`,
}}
/>

View file

@ -1,39 +0,0 @@
.lnsChartSwitch__header {
> * {
display: flex;
align-items: center;
}
}
.lnsChartSwitch__options {
width: 384px;
}
.lnsChartSwitch__summaryIcon {
transform: translateY(-1px);
color: $euiTextSubduedColor;
@include euiBreakpoint('xl') {
margin-right: $euiSizeS;
}
}
.lnsChartSwitch__summaryText {
@include euiBreakpoint('xs', 's', 'm', 'l') {
@include euiScreenReaderOnly;
}
}
.lnsChartSwitch__append {
display: inline-flex;
}
// Targeting img as this won't target normal EuiIcon's only the custom svgs's
img.lnsChartSwitch__chartIcon { // stylelint-disable-line selector-no-qualifying-type
// The large icons aren't square so max out the width to fill the height
width: 100%;
}
.lnsChartSwitch__search {
width: 10 * $euiSizeXXL;
}

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import './chart_switch.scss';
import React, { useState, useMemo, memo } from 'react';
import { i18n } from '@kbn/i18n';
import { ExperimentalBadge } from '../../../../shared_components';

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import './chart_switch.scss';
import React, { useState, memo } from 'react';
import { EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ChartSwitchTrigger } from '@kbn/visualization-ui-components';
import { css } from '@emotion/react';
import { useLensSelector, selectVisualization } from '../../../../state_management';
import { ChartSwitch, ChartSwitchProps } from './chart_switch';
@ -31,28 +31,29 @@ export const ChartSwitchPopover = memo(function ChartSwitchPopover(
};
return (
<div className="lnsChartSwitch__header">
<EuiPopover
id="lnsChartSwitchPopover"
ownFocus
initialFocus=".lnsChartSwitch__popoverPanel"
panelClassName="lnsChartSwitch__popoverPanel"
panelPaddingSize="none"
repositionOnScroll
button={
<ChartSwitchTrigger
icon={icon}
label={label}
dataTestSubj="lnsChartSwitchPopover"
onClick={() => setFlyoutOpen(!flyoutOpen)}
/>
}
isOpen={flyoutOpen}
closePopover={() => setFlyoutOpen(false)}
anchorPosition="downLeft"
>
{flyoutOpen ? <ChartSwitch {...props} onChartSelect={() => setFlyoutOpen(false)} /> : null}
</EuiPopover>
</div>
<EuiPopover
css={css`
display: flex;
`}
id="lnsChartSwitchPopover"
ownFocus
initialFocus=".lnsChartSwitch__popoverPanel"
panelClassName="lnsChartSwitch__popoverPanel"
panelPaddingSize="none"
repositionOnScroll
button={
<ChartSwitchTrigger
icon={icon}
label={label}
dataTestSubj="lnsChartSwitchPopover"
onClick={() => setFlyoutOpen(!flyoutOpen)}
/>
}
isOpen={flyoutOpen}
closePopover={() => setFlyoutOpen(false)}
anchorPosition="downLeft"
>
{flyoutOpen ? <ChartSwitch {...props} onChartSelect={() => setFlyoutOpen(false)} /> : null}
</EuiPopover>
);
});

View file

@ -44,6 +44,9 @@ export const ChartSwitchSelectable = ({
isPreFiltered
data-test-subj="lnsChartSwitchList"
className="lnsChartSwitch__options"
css={css`
width: 384px;
`}
height={computeListHeight(props.options as SelectableEntry[])}
searchProps={{
compressed: true,
@ -51,6 +54,9 @@ export const ChartSwitchSelectable = ({
inputRef: (ref) => {
ref?.focus({ preventScroll: true });
},
css: css`
width: 400px;
`,
className: 'lnsChartSwitch__search',
'data-test-subj': 'lnsChartSwitchSearch',
onChange: setSearchTerm,

View file

@ -24,7 +24,7 @@ import { coreMock } from '@kbn/core/public/mocks';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
import { generateId } from '../../../id_generator';
import { mountWithProvider } from '../../../mocks';
import { mountWithReduxStore } from '../../../mocks';
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
import { ReactWrapper } from 'enzyme';
import { createIndexPatternServiceMock } from '../../../mocks/data_views_service_mock';
@ -82,7 +82,7 @@ describe('ConfigPanel', () => {
query?: Query | AggregateQuery
) {
(generateId as jest.Mock).mockReturnValue(`newId`);
return mountWithProvider(
return mountWithReduxStore(
<LayerPanels {...props} />,
{
preloadedState: {

View file

@ -6,7 +6,7 @@
*/
import React, { useMemo, memo, useCallback } from 'react';
import { EuiForm } from '@elastic/eui';
import { EuiForm, euiBreakpoint, useEuiTheme } from '@elastic/eui';
import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
import { isOfAggregateQueryType } from '@kbn/es-query';
import {
@ -15,6 +15,7 @@ import {
} from '@kbn/unified-search-plugin/public';
import { DragDropIdentifier, DropType } from '@kbn/dom-drag-drop';
import { css } from '@emotion/react';
import {
changeIndexPattern,
onDropToDimension,
@ -62,6 +63,9 @@ export function LayerPanels(
(state) => state.lens
);
const euiThemeContext = useEuiTheme();
const { euiTheme } = euiThemeContext;
const dispatchLens = useLensDispatch();
const layerIds = activeVisualization.getLayerIds(visualization.state);
@ -252,7 +256,20 @@ export function LayerPanels(
const hideAddLayerButton = query && isOfAggregateQueryType(query);
return (
<EuiForm className="lnsConfigPanel">
<EuiForm
className="eui-yScroll"
css={css`
.lnsApp & {
padding: ${euiTheme.size.base} ${euiTheme.size.base} ${euiTheme.size.xl}
calc(400px + ${euiTheme.size.base});
margin-left: -400px;
${euiBreakpoint(euiThemeContext, ['xs', 's', 'm'])} {
padding-left: ${euiTheme.size.base};
margin-left: 0;
}
}
`}
>
{layerIds.map((layerId, layerIndex) => {
const { hidden, groups } = activeVisualization.getConfiguration({
layerId,

View file

@ -1,17 +0,0 @@
.lnsLayerAddButton {
&:last-child {
align-self: unset;
}
&:hover {
text-decoration: none;
.lnsLayerAddButton__label {
text-decoration: underline;
}
.lnsLayerAddButton__techBadge,
.lnsLayerAddButton__techBadge * {
cursor: pointer;
}}
}

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import './dimension_container.scss';
import React from 'react';
import { FlyoutContainer } from '../../../shared_components/flyout_container';

View file

@ -1,92 +0,0 @@
@import '../../../mixins';
.lnsLayerPanel {
margin-bottom: $euiSize;
// disable focus ring for mouse clicks, leave it for keyboard users
&:focus:not(:focus-visible) {
animation: none !important; // sass-lint:disable-line no-important
}
}
.lnsLayerPanel__layerHeader {
padding: $euiSize;
border-bottom: $euiBorderThin;
}
// fixes truncation for too long chart switcher labels
.lnsLayerPanel__layerSettingsWrapper {
min-width: 0;
}
.lnsLayerPanel__settingsStaticHeader {
padding-left: $euiSizeXS;
}
.lnsLayerPanel__settingsStaticHeaderIcon {
margin-right: $euiSizeS;
vertical-align: inherit;
}
.lnsLayerPanel__settingsStaticHeaderTitle {
display: inline;
}
.lnsLayerPanel__row {
padding: $euiSize;
&:last-child {
border-radius: 0 0 $euiBorderRadius $euiBorderRadius;
}
// Add border to the top of the next same panel
&+& {
border-top: $euiBorderThin;
margin-top: 0;
}
&>* {
margin-bottom: 0;
}
// Targeting EUI class as we are unable to apply a class to this element in component
&,
.euiFormRow__fieldWrapper {
&>*+* {
margin-top: $euiSizeS;
}
}
}
.lnsLayerPanel__group {
margin: (-$euiSizeXS) (-$euiSize);
padding: $euiSizeXS $euiSize;
}
.lnsLayerPanel__styleEditor {
padding: $euiSize;
}
// Start dimension style overrides
.lnsLayerPanel__dimensionContainer {
position: relative;
&+& {
margin-top: $euiSizeS;
}
}
.domDroppable--replacing {
.dimensionTrigger__textLabel {
text-decoration: line-through;
}
}
// Added .lnsLayerPanel__dimension specificity required for animation style override
.lnsLayerPanel__dimension .lnsLayerPanel__dimensionLink {
&:focus {
background-color: transparent;
text-decoration-thickness: $euiBorderWidthThin !important;
@include passDownFocusRing('.dimensionTrigger__textLabel');
}
}

View file

@ -18,7 +18,7 @@ import {
createMockVisualization,
createMockFramePublicAPI,
createMockDatasource,
mountWithProvider,
mountWithReduxStore,
createMockedDragDropContext,
renderWithReduxStore,
} from '../../../mocks';
@ -703,7 +703,7 @@ describe('LayerPanel', () => {
target.columnId !== 'a' ? { dropTypes: ['field_replace'], nextLabel: '' } : undefined
);
const { instance } = await mountWithProvider(
const { instance } = mountWithReduxStore(
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingField })}>
<LayerPanel {...getDefaultProps()} />
</ChildDragDropProvider>
@ -762,7 +762,7 @@ describe('LayerPanel', () => {
nextLabel: '',
});
const { instance } = await mountWithProvider(
const { instance } = mountWithReduxStore(
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel {...getDefaultProps()} />
</ChildDragDropProvider>
@ -822,7 +822,7 @@ describe('LayerPanel', () => {
const holder = document.createElement('div');
document.body.appendChild(holder);
const { instance } = await mountWithProvider(
const { instance } = mountWithReduxStore(
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel {...getDefaultProps()} />
</ChildDragDropProvider>,
@ -860,7 +860,7 @@ describe('LayerPanel', () => {
],
});
const { instance } = await mountWithProvider(
const { instance } = mountWithReduxStore(
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel {...getDefaultProps()} />
</ChildDragDropProvider>
@ -897,7 +897,7 @@ describe('LayerPanel', () => {
],
});
const { instance } = await mountWithProvider(
const { instance } = mountWithReduxStore(
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel {...getDefaultProps()} activeVisualization={mockVis} />
</ChildDragDropProvider>
@ -938,7 +938,7 @@ describe('LayerPanel', () => {
mockDatasource.onDrop.mockReturnValue(true);
const updateVisualization = jest.fn();
const { instance } = await mountWithProvider(
const { instance } = mountWithReduxStore(
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel
{...getDefaultProps()}
@ -989,7 +989,7 @@ describe('LayerPanel', () => {
mockDatasource.onDrop.mockReturnValue(false);
const updateVisualization = jest.fn();
const { instance } = await mountWithProvider(
const { instance } = mountWithReduxStore(
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel
{...getDefaultProps()}
@ -1029,7 +1029,7 @@ describe('LayerPanel', () => {
mockDatasource.onDrop.mockReturnValue(true);
const { instance } = await mountWithProvider(
const { instance } = mountWithReduxStore(
<ChildDragDropProvider value={createMockedDragDropContext({ dragging: draggingOperation })}>
<LayerPanel {...getDefaultProps()} />
</ChildDragDropProvider>

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import './layer_panel.scss';
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import {
EuiPanel,
@ -16,6 +14,7 @@ import {
EuiFormRow,
EuiText,
EuiIconTip,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
@ -51,6 +50,7 @@ export function LayerPanel(props: LayerPanelProps) {
}>({});
const [isPanelSettingsOpen, setPanelSettingsOpen] = useState(false);
const { euiTheme } = useEuiTheme();
const {
framePublicAPI,
@ -366,13 +366,30 @@ export function LayerPanel(props: LayerPanelProps) {
<section
tabIndex={-1}
ref={registerLayerRef}
className="lnsLayerPanel"
css={css`
margin-bottom: ${euiTheme.size.base};
// disable focus ring for mouse clicks, leave it for keyboard users
&:focus:not(:focus-visible) {
animation: none !important; // sass-lint:disable-line no-important
}
`}
data-test-subj={`lns-layerPanel-${layerIndex}`}
>
<EuiPanel paddingSize="none" hasShadow={false} hasBorder>
<header className="lnsLayerPanel__layerHeader">
<header
className="lnsLayerPanel__layerHeader"
css={css`
padding: ${euiTheme.size.base};
border-bottom: ${euiTheme.border.thin};
`}
>
<EuiFlexGroup gutterSize="s" responsive={false} alignItems="center">
<EuiFlexItem grow className="lnsLayerPanel__layerSettingsWrapper">
<EuiFlexItem
grow
css={css`
min-width: 0; // fixes truncation for too long chart switcher labels
`}
>
<LayerHeader
layerConfigProps={{
...layerVisualizationConfigProps,
@ -485,6 +502,31 @@ export function LayerPanel(props: LayerPanelProps) {
const isOptional = !group.requiredMinDimensionCount && !group.suggestedValue;
return (
<EuiFormRow
css={css`
padding: ${euiTheme.size.base};
&:last-child {
border-radius: 0 0 ${euiTheme.border.radius.medium}
${euiTheme.border.radius.medium};
}
// Add border to the top of the next same panel
& + & {
border-top: ${euiTheme.border.thin};
margin-top: 0;
}
& > * {
margin-bottom: 0;
}
// Targeting EUI class as we are unable to apply a class to this element in component
&,
.euiFormRow__fieldWrapper {
& > * + * {
margin-top: ${euiTheme.size.s};
}
}
`}
className="lnsLayerPanel__row"
fullWidth
label={
@ -523,8 +565,11 @@ export function LayerPanel(props: LayerPanelProps) {
<>
{group.accessors.length ? (
<ReorderProvider
className={'lnsLayerPanel__group'}
dataTestSubj="lnsDragDrop"
css={css`
margin: -${euiTheme.size.xs} -${euiTheme.size.base};
padding: ${euiTheme.size.xs} ${euiTheme.size.base};
`}
>
{group.accessors.map((accessorConfig, accessorIndex) => {
const { columnId } = accessorConfig;
@ -744,7 +789,7 @@ export function LayerPanel(props: LayerPanelProps) {
isInlineEditing={isInlineEditing}
handleClose={closeDimensionEditor}
panel={
<>
<div>
{openColumnGroup &&
openColumnId &&
layerDatasource &&
@ -790,7 +835,11 @@ export function LayerPanel(props: LayerPanelProps) {
activeVisualization.DimensionEditorComponent &&
openColumnGroup?.enableDimensionEditor && (
<>
<div className="lnsLayerPanel__styleEditor">
<div
css={css`
padding: ${euiTheme.size.base};
`}
>
<activeVisualization.DimensionEditorComponent
{...{
...layerVisualizationConfigProps,
@ -821,7 +870,7 @@ export function LayerPanel(props: LayerPanelProps) {
)}
</>
)}
</>
</div>
}
/>
</>

View file

@ -1,10 +0,0 @@
.lnsDataPanelWrapper {
flex: 1 0 100%;
overflow: hidden;
}
.lnsDataPanelWrapper__switchSource {
position: absolute;
right: $euiSize + $euiSizeXS;
top: $euiSize + $euiSizeXS;
}

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import './data_panel_wrapper.scss';
import React, { useMemo, memo, useEffect, useCallback } from 'react';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
@ -15,6 +13,7 @@ import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'
import { DragDropIdentifier } from '@kbn/dom-drag-drop';
import memoizeOne from 'memoize-one';
import { isEqual } from 'lodash';
import { css } from '@emotion/react';
import { Easteregg } from './easteregg';
import {
StateSetter,
@ -194,7 +193,14 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
<>
<Easteregg query={externalContext?.query} />
{DataPanelComponent && (
<div className="lnsDataPanelWrapper" data-test-subj="lnsDataPanelWrapper">
<div
className="lnsDataPanelWrapper"
data-test-subj="lnsDataPanelWrapper"
css={css`
flex: 1 0 100%;
overflow: hidden;
`}
>
{DataPanelComponent(datasourceProps)}
</div>
)}

View file

@ -1,123 +0,0 @@
@import '../../variables';
.lnsFrameLayout {
padding: 0;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
flex-direction: column;
@include euiBreakpoint('xs', 's', 'm') {
position: static;
}
}
.lnsFrameLayout__wrapper {
position: relative;
}
.lnsFrameLayout__pageContent {
overflow: hidden;
flex-grow: 1;
flex-direction: row;
@include euiBreakpoint('xs', 's', 'm') {
flex-wrap: wrap;
overflow: auto;
> * {
flex-basis: 100%;
}
> .lnsFrameLayout__sidebar {
min-height: $euiSizeL * 15;
}
}
}
.visEditor {
height: 100%;
@include flexParent();
@include euiBreakpoint('xs', 's', 'm') {
.visualization {
// While we are on a small screen the visualization is below the
// editor. In this cases it needs a minimum height, since it would otherwise
// maybe end up with 0 height since it just gets the flexbox rest of the screen.
min-height: $euiSizeL * 15;
}
}
/* 1. Without setting this to 0 you will run into a bug where the filter bar modal is hidden under
a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */
> .visualize {
height: 100%;
flex: 1 1 auto;
display: flex;
z-index: 0; /* 1 */
}
}
.lnsFrameLayout__pageBody {
min-width: $lnsPanelMinWidth + $euiSizeXL;
overflow: hidden auto;
display: flex;
flex-direction: column;
flex: 1 1 100%;
// Leave out bottom padding so the suggestions scrollbar stays flush to window edge
// Leave out left padding so the left sidebar's focus states are visible outside of content bounds
// This also means needing to add same amount of margin to page content and suggestion items
padding: $euiSize $euiSize 0;
position: relative;
z-index: $lnsZLevel1;
border-left: $euiBorderThin;
border-right: $euiBorderThin;
@include euiScrollBar;
&:first-child {
padding-left: $euiSize;
}
&.lnsFrameLayout__pageBody-isFullscreen {
flex: 1;
padding: 0;
}
}
.lnsFrameLayout__sidebar {
margin: 0;
flex: 1 0 18%;
min-width: $lnsPanelMinWidth + $euiSize;
display: flex;
flex-direction: column;
position: relative;
}
.lnsFrameLayout-isFullscreen .lnsFrameLayout__sidebar--left {
// Hide the datapanel in fullscreen mode. Using display: none does trigger
// a rerender when the container becomes visible again, maybe pushing offscreen is better
display: none;
}
.lnsFrameLayout__sidebar--right {
flex-basis: 25%;
min-width: $lnsPanelMinWidth + 70;
max-width: $euiFormMaxWidth + $euiSizeXXL;
max-height: 100%;
@include euiBreakpoint('xs', 's', 'm') {
max-width: 100%;
}
.lnsConfigPanel {
padding: $euiSize $euiSize $euiSizeXL ($euiFormMaxWidth + $euiSize);
margin-left: -$euiFormMaxWidth;
@include euiYScroll;
@include euiBreakpoint('xs', 's', 'm') {
padding-left: $euiSize;
margin-left: 0;
}
}
}
.lnsFrameLayout__sidebar-isFullscreen {
flex: 1;
max-width: none;
}

View file

@ -5,12 +5,19 @@
* 2.0.
*/
import './frame_layout.scss';
import React from 'react';
import { EuiScreenReaderOnly, EuiFlexGroup, EuiFlexItem, EuiPage, EuiPageBody } from '@elastic/eui';
import {
EuiScreenReaderOnly,
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPageBody,
useEuiTheme,
euiBreakpoint,
type UseEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import classNames from 'classnames';
import { css } from '@emotion/react';
import { useLensSelector, selectIsFullscreenDatasource } from '../../state_management';
export interface FrameLayoutProps {
@ -23,6 +30,8 @@ export interface FrameLayoutProps {
export function FrameLayout(props: FrameLayoutProps) {
const isFullscreen = useLensSelector(selectIsFullscreenDatasource);
const euiThemeContext = useEuiTheme();
const { euiTheme } = euiThemeContext;
return (
<EuiFlexGroup direction="column" responsive={false} gutterSize="none" alignItems="stretch">
@ -40,21 +49,57 @@ export function FrameLayout(props: FrameLayoutProps) {
</aside>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={true} className="lnsFrameLayout__wrapper">
<EuiFlexItem
grow={true}
css={css`
position: relative;
`}
>
<EuiPage
paddingSize="none"
className={classNames('lnsFrameLayout', {
'lnsFrameLayout-isFullscreen': isFullscreen,
})}
css={css`
padding: 0;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
flex-direction: column;
${euiBreakpoint(euiThemeContext, ['xs', 's', 'm'])} {
position: static;
}
`}
>
<EuiPageBody
restrictWidth={false}
className="lnsFrameLayout__pageContent"
aria-labelledby="lns_ChartTitle"
css={css`
overflow: hidden;
flex-grow: 1;
flex-direction: row;
${euiBreakpoint(euiThemeContext, ['xs', 's', 'm'])} {
flex-wrap: wrap;
overflow: auto;
> * {
flex-basis: 100%;
}
}
`}
>
<section
className={'lnsFrameLayout__sidebar lnsFrameLayout__sidebar--left hide-for-sharing'}
className="hide-for-sharing"
aria-labelledby="dataPanelId"
css={[
sidebarStyles(euiThemeContext),
isFullscreen &&
css`
// Hide the datapanel in fullscreen mode. Using display: none does trigger
// a rerender when the container becomes visible again, maybe pushing offscreen is better
display: none;
`,
]}
>
<EuiScreenReaderOnly>
<h2 id="dataPanelId">
@ -66,9 +111,29 @@ export function FrameLayout(props: FrameLayoutProps) {
{props.dataPanel}
</section>
<section
className={classNames('lnsFrameLayout__pageBody', {
'lnsFrameLayout__pageBody-isFullscreen': isFullscreen,
})}
className="eui-scrollBar"
css={css`
min-width: 432px;
overflow: hidden auto;
display: flex;
flex-direction: column;
flex: 1 1 100%;
// Leave out bottom padding so the suggestions scrollbar stays flush to window edge
// Leave out left padding so the left sidebar's focus states are visible outside of content bounds
// This also means needing to add same amount of margin to page content and suggestion items
padding: ${euiTheme.size.base} ${euiTheme.size.base} 0;
position: relative;
z-index: 1;
border-left: ${euiTheme.border.thin};
border-right: ${euiTheme.border.thin};
&:first-child {
padding-left: ${euiTheme.size.base};
}
${isFullscreen &&
`
flex: 1;
padding: 0;`}
`}
aria-labelledby="workspaceId"
>
<EuiScreenReaderOnly>
@ -79,18 +144,23 @@ export function FrameLayout(props: FrameLayoutProps) {
</h2>
</EuiScreenReaderOnly>
{props.workspacePanel}
<div className="lnsFrameLayout__suggestionPanel hide-for-sharing">
{props.suggestionsPanel}
</div>
<div className="hide-for-sharing">{props.suggestionsPanel}</div>
</section>
<section
className={classNames(
'lnsFrameLayout__sidebar lnsFrameLayout__sidebar--right',
'hide-for-sharing',
{
'lnsFrameLayout__sidebar-isFullscreen': isFullscreen,
}
)}
css={[
sidebarStyles(euiThemeContext),
css`
flex-basis: 25%;
min-width: 358px;
max-width: 440px;
max-height: 100%;
${euiBreakpoint(euiThemeContext, ['xs', 's', 'm'])} {
max-width: 100%;
}
${isFullscreen && `flex: 1; max-width: none;`}
`,
]}
className="hide-for-sharing"
aria-labelledby="configPanel"
>
<EuiScreenReaderOnly>
@ -108,3 +178,15 @@ export function FrameLayout(props: FrameLayoutProps) {
</EuiFlexGroup>
);
}
const sidebarStyles = (euiThemeContext: UseEuiTheme) => css`
margin: 0;
flex: 1 0 18%;
min-width: 304px;
display: flex;
flex-direction: column;
position: relative;
${euiBreakpoint(euiThemeContext, ['xs', 's', 'm'])} {
min-height: 360px;
}
`;

View file

@ -1,106 +0,0 @@
@import '../../mixins';
@import '../../variables';
.lnsSuggestionPanel .euiAccordion__buttonContent {
width: 100%;
}
.lnsSuggestionPanel__suggestions {
@include lnsOverflowShadowHorizontal;
padding-top: $euiSizeXS;
overflow-x: scroll;
overflow-y: hidden;
display: flex;
// Padding / negative margins to make room for overflow shadow
padding-left: $euiSizeXS;
margin-left: -$euiSizeXS;
padding-right: $euiSizeXS;
margin-right: -$euiSizeXS;
@include euiScrollBar;
}
.lnsSuggestionPanel__button {
position: relative; // Let the expression progress indicator position itself against the button
flex: 0 0 auto;
height: $lnsSuggestionHeight;
margin-right: $euiSizeS;
margin-left: calc($euiSizeXS / 2);
margin-bottom: calc($euiSizeXS / 2);
padding: 0 $euiSizeS;
box-shadow: none !important; // sass-lint:disable-line no-important
&:focus {
transform: none !important; // sass-lint:disable-line no-important
@include euiFocusRing;
}
.lnsSuggestionPanel__expressionRenderer {
position: static; // Let the progress indicator position itself against the button
}
}
.lnsSuggestionPanel__button-isSelected {
background-color: $euiColorLightestShade !important; // sass-lint:disable-line no-important
border-color: $euiColorMediumShade !important; // sass-lint:disable-line no-important
&:not(:focus) {
box-shadow: none !important; // sass-lint:disable-line no-important
}
&:focus {
@include euiFocusRing;
}
&:hover {
transform: none !important; // sass-lint:disable-line no-important
}
}
.lnsSuggestionPanel__button-fixedWidth {
width: $lnsSuggestionWidth !important; // sass-lint:disable-line no-important
}
.lnsSuggestionPanel__suggestionIcon {
color: $euiColorDarkShade;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: $euiSizeS;
&:not(:only-child) {
height: calc(100% - #{$euiSizeL});
}
}
.lnsSuggestionPanel__chartWrapper {
display: flex;
height: 100%;
width: 100%;
pointer-events: none;
}
.lnsSuggestionPanel__chartWrapper--withLabel {
height: calc(100% - #{$euiSizeL});
}
.lnsSuggestionPanel__buttonLabel {
@include euiTextTruncate;
@include euiFontSizeXS;
display: block;
font-weight: $euiFontWeightBold;
text-align: center;
flex-grow: 0;
}
.lnsSuggestionPanel__applyChangesPrompt {
height: $lnsSuggestionHeight;
background-color: $euiColorLightestShade !important;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

View file

@ -13,14 +13,16 @@ import {
createExpressionRendererMock,
DatasourceMock,
createMockFramePublicAPI,
renderWithReduxStore,
} from '../../mocks';
import { screen } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { ReactExpressionRendererType } from '@kbn/expressions-plugin/public';
import { SuggestionPanel, SuggestionPanelProps, SuggestionPanelWrapper } from './suggestion_panel';
import { getSuggestions } from './suggestion_helpers';
import { EuiIcon, EuiPanel, EuiToolTip, EuiAccordion } from '@elastic/eui';
import { IconChartDatatable } from '@kbn/chart-icons';
import { mountWithProvider } from '../../mocks';
import { mountWithReduxStore } from '../../mocks';
import { coreMock } from '@kbn/core/public/mocks';
import {
@ -32,6 +34,7 @@ import {
VisualizationState,
} from '../../state_management';
import { setChangesApplied } from '../../state_management/lens_slice';
import { userEvent } from '@testing-library/user-event';
const SELECTORS = {
APPLY_CHANGES_BUTTON: 'button[data-test-subj="lnsApplyChanges__suggestions"]',
@ -113,7 +116,7 @@ describe('suggestion_panel', () => {
});
it('should avoid completely to render SuggestionPanel when in fullscreen mode', async () => {
const { instance, lensStore } = await mountWithProvider(
const { instance, lensStore } = mountWithReduxStore(
<SuggestionPanelWrapper {...defaultProps} />
);
expect(instance.find(SuggestionPanel).exists()).toBe(true);
@ -128,7 +131,7 @@ describe('suggestion_panel', () => {
});
it('should display apply-changes prompt when changes not applied', async () => {
const { instance, lensStore } = await mountWithProvider(<SuggestionPanel {...defaultProps} />, {
const { instance, lensStore } = mountWithReduxStore(<SuggestionPanel {...defaultProps} />, {
preloadedState: {
...preloadedState,
visualization: {
@ -160,7 +163,7 @@ describe('suggestion_panel', () => {
});
it('should list passed in suggestions', async () => {
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />, {
const { instance } = mountWithReduxStore(<SuggestionPanel {...defaultProps} />, {
preloadedState,
});
@ -196,10 +199,9 @@ describe('suggestion_panel', () => {
});
it('should not update suggestions if current state is moved to staged preview', async () => {
const { instance, lensStore } = await mountWithProvider(
<SuggestionPanel {...defaultProps} />,
{ preloadedState }
);
const { instance, lensStore } = mountWithReduxStore(<SuggestionPanel {...defaultProps} />, {
preloadedState,
});
getSuggestionsMock.mockClear();
lensStore.dispatch(setState({ stagedPreview }));
instance.update();
@ -207,10 +209,9 @@ describe('suggestion_panel', () => {
});
it('should update suggestions if staged preview is removed', async () => {
const { instance, lensStore } = await mountWithProvider(
<SuggestionPanel {...defaultProps} />,
{ preloadedState }
);
const { instance, lensStore } = mountWithReduxStore(<SuggestionPanel {...defaultProps} />, {
preloadedState,
});
getSuggestionsMock.mockClear();
lensStore.dispatch(setState({ stagedPreview, ...suggestionState }));
instance.update();
@ -219,25 +220,19 @@ describe('suggestion_panel', () => {
expect(getSuggestionsMock).toHaveBeenCalledTimes(1);
});
it('should highlight currently active suggestion', async () => {
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />, {
it('should select currently active suggestion', async () => {
const getSuggestionByName = (name: string) => screen.getByRole('listitem', { name });
renderWithReduxStore(<SuggestionPanel {...defaultProps} />, undefined, {
preloadedState,
});
act(() => {
instance.find(SELECTORS.SUGGESTION_TILE_BUTTON).at(2).simulate('click');
});
instance.update();
expect(instance.find(SELECTORS.SUGGESTION_TILE_BUTTON).at(2).prop('className')).toContain(
'lnsSuggestionPanel__button-isSelected'
);
expect(getSuggestionByName('Current visualization')).toHaveAttribute('aria-current', 'true');
await userEvent.click(getSuggestionByName('Suggestion1'));
expect(getSuggestionByName('Suggestion1')).toHaveAttribute('aria-current', 'true');
});
it('should rollback suggestion if current panel is clicked', async () => {
const { instance, lensStore } = await mountWithProvider(
<SuggestionPanel {...defaultProps} />
);
const { instance, lensStore } = mountWithReduxStore(<SuggestionPanel {...defaultProps} />);
act(() => {
instance.find(SELECTORS.SUGGESTION_TILE_BUTTON).at(2).simulate('click');
@ -262,7 +257,7 @@ describe('suggestion_panel', () => {
});
it('should dispatch visualization switch action if suggestion is clicked', async () => {
const { instance, lensStore } = await mountWithProvider(<SuggestionPanel {...defaultProps} />, {
const { instance, lensStore } = mountWithReduxStore(<SuggestionPanel {...defaultProps} />, {
preloadedState,
});
@ -316,7 +311,7 @@ describe('suggestion_panel', () => {
mockDatasource.toExpression.mockReturnValue('datasource_expression');
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />, {
const { instance } = mountWithReduxStore(<SuggestionPanel {...defaultProps} />, {
preloadedState,
});
@ -344,14 +339,14 @@ describe('suggestion_panel', () => {
},
};
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />, {
const { instance } = mountWithReduxStore(<SuggestionPanel {...defaultProps} />, {
preloadedState: newPreloadedState,
});
expect(instance.html()).toEqual(null);
});
it('should hide the selections when the accordion is hidden', async () => {
const { instance } = await mountWithProvider(<SuggestionPanel {...defaultProps} />);
const { instance } = mountWithReduxStore(<SuggestionPanel {...defaultProps} />);
expect(instance.find(EuiAccordion)).toHaveLength(1);
act(() => {
instance.find(EuiAccordion).at(0).simulate('change');
@ -386,7 +381,7 @@ describe('suggestion_panel', () => {
.mockReturnValueOnce('test | expression');
mockDatasource.toExpression.mockReturnValue('datasource_expression');
mountWithProvider(<SuggestionPanel {...defaultProps} frame={createMockFramePublicAPI()} />);
mountWithReduxStore(<SuggestionPanel {...defaultProps} frame={createMockFramePublicAPI()} />);
expect(expressionRendererMock).toHaveBeenCalledTimes(1);
const passedExpression = (expressionRendererMock as jest.Mock).mock.calls[0][0].expression;

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import './suggestion_panel.scss';
import { camelCase, pick } from 'lodash';
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
@ -22,12 +20,17 @@ import {
EuiAccordion,
EuiText,
EuiNotificationBadge,
type UseEuiTheme,
useEuiTheme,
euiFocusRing,
useEuiFontSize,
euiTextTruncate,
transparentize,
} from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
import { IconType } from '@elastic/eui/src/components/icon/icon';
import { Ast, fromExpression, toExpression } from '@kbn/interpreter';
import { i18n } from '@kbn/i18n';
import classNames from 'classnames';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { ExecutionContextSearch } from '@kbn/es-query';
import {
@ -126,8 +129,10 @@ const PreviewRenderer = ({
hasError: boolean;
onRender: () => void;
}) => {
const euiThemeContext = useEuiTheme();
const { euiTheme } = euiThemeContext;
const onErrorMessage = (
<div className="lnsSuggestionPanel__suggestionIcon">
<div css={suggestionStyles.icon(euiThemeContext)}>
<EuiIconTip
size="xl"
color="danger"
@ -143,9 +148,13 @@ const PreviewRenderer = ({
);
return (
<div
className={classNames('lnsSuggestionPanel__chartWrapper', {
'lnsSuggestionPanel__chartWrapper--withLabel': withLabel,
})}
css={css`
display: flex;
height: 100%;
width: 100%;
pointer-events: none;
${withLabel ? `height: calc(100% - ${euiTheme.size.l});` : ''}
`}
>
{!expression || hasError ? (
onErrorMessage
@ -188,6 +197,9 @@ const SuggestionPreview = ({
onRender: () => void;
wrapSuggestions?: boolean;
}) => {
const euiThemeContext = useEuiTheme();
const { euiTheme } = euiThemeContext;
const xsFontSize = useEuiFontSize('xs');
return (
<EuiToolTip
content={preview.title}
@ -207,10 +219,48 @@ const SuggestionPreview = ({
<EuiPanel
hasBorder={true}
hasShadow={false}
className={classNames('lnsSuggestionPanel__button', {
'lnsSuggestionPanel__button-isSelected': selected,
'lnsSuggestionPanel__button-fixedWidth': !wrapSuggestions,
})}
css={css`
position: relative; // Let the expression progress indicator position itself against the button
flex: 0 0 auto;
height: 100px;
margin-right: ${euiTheme.size.s};
margin-left: ${euiTheme.size.xxs};
margin-bottom: ${euiTheme.size.xxs};
padding: 0 ${euiTheme.size.s};
box-shadow: none !important; // sass-lint:disable-line no-important
&:focus {
transform: none !important; // sass-lint:disable-line no-important
${euiFocusRing(euiThemeContext)};
}
${selected
? `
background-color: ${
euiTheme.colors.lightestShade
} !important; // sass-lint:disable-line no-important
border-color: ${
euiTheme.colors.mediumShade
} !important; // sass-lint:disable-line no-important
&:not(:focus) {
box-shadow: none !important; // sass-lint:disable-line no-important
}
&:focus {
${euiFocusRing(euiThemeContext)};
}
&:hover {
transform: none !important; // sass-lint:disable-line no-important
}
`
: ''}
${!wrapSuggestions
? `
width: 150px !important; // sass-lint:disable-line no-important
`
: ''}
`}
paddingSize="none"
data-test-subj="lnsSuggestion"
onClick={onSelect}
@ -228,12 +278,23 @@ const SuggestionPreview = ({
onRender={onRender}
/>
) : (
<span className="lnsSuggestionPanel__suggestionIcon">
<span css={suggestionStyles.icon(euiThemeContext)}>
<EuiIcon size="xxl" type={preview.icon} />
</span>
)}
{showTitleAsLabel && (
<span className="lnsSuggestionPanel__buttonLabel">{preview.title}</span>
<span
css={css`
${euiTextTruncate()}
${xsFontSize};
font-weight: ${euiTheme.font.weight.bold};
display: block;
text-align: center;
flex-grow: 0;
`}
>
{preview.title}
</span>
)}
</EuiPanel>
</div>
@ -266,6 +327,8 @@ export function SuggestionPanel({
const existsStagedPreview = useLensSelector((state) => Boolean(state.lens.stagedPreview));
const currentVisualization = useLensSelector(selectCurrentVisualization);
const currentDatasourceStates = useLensSelector(selectCurrentDatasourceStates);
const euiThemeContext = useEuiTheme();
const { euiTheme } = euiThemeContext;
const framePublicAPI = useLensSelector((state) => selectFramePublicAPI(state, datasourceMap));
const changesApplied = useLensSelector(selectChangesApplied);
@ -438,7 +501,19 @@ export function SuggestionPanel({
}
const renderApplyChangesPrompt = () => (
<EuiPanel hasShadow={false} className="lnsSuggestionPanel__applyChangesPrompt" paddingSize="m">
<EuiPanel
hasShadow={false}
className="lnsSuggestionPanel__applyChangesPrompt"
paddingSize="m"
css={css`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100px;
background-color: ${euiTheme.colors.lightestShade} !important;
`}
>
<EuiText size="s" color="subdued" className="lnsSuggestionPanel__applyChangesMessage">
<p>
<FormattedMessage
@ -540,9 +615,11 @@ export function SuggestionPanel({
'data-test-subj': 'lensSuggestionsPanelToggleButton',
paddingSize: wrapSuggestions ? 'm' : 's',
}}
className="lnsSuggestionPanel"
css={css`
padding-bottom: ${wrapSuggestions ? 0 : euiThemeVars.euiSizeS};
.euiAccordion__buttonContent {
width: 100%;
}
`}
buttonContent={title}
forceState={hideSuggestions ? 'closed' : 'open'}
@ -582,13 +659,24 @@ export function SuggestionPanel({
}
>
<div
className="lnsSuggestionPanel__suggestions"
className="eui-scrollBar"
data-test-subj="lnsSuggestionsPanel"
role="list"
tabIndex={0}
css={css`
flex-wrap: ${wrapSuggestions ? 'wrap' : 'nowrap'};
gap: ${wrapSuggestions ? euiThemeVars.euiSize : 0};
gap: ${wrapSuggestions ? euiTheme.size.base : 0};
overflow-x: scroll;
overflow-y: hidden;
display: flex;
padding-top: ${euiTheme.size.xs};
mask-image: linear-gradient(
to right,
${transparentize(euiTheme.colors.danger, 0.1)} 0%,
${euiTheme.colors.danger} 5px,
${euiTheme.colors.danger} calc(100% - 5px),
${transparentize(euiTheme.colors.danger, 0.1)} 100%
);
`}
>
{changesApplied ? renderSuggestionsUI() : renderApplyChangesPrompt()}
@ -699,3 +787,18 @@ function preparePreviewExpression(
return typeof expression === 'string' ? fromExpression(expression) : expression;
}
const suggestionStyles = {
icon: ({ euiTheme }: UseEuiTheme) => css`
color: ${euiTheme.colors.darkShade};
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: ${euiTheme.size.s};
&:not(:only-child) {
height: calc(100% - ${euiTheme.size.l});
}
`,
};

View file

@ -1,5 +0,0 @@
.lnsVisualizeGeoFieldWorkspacePanel__dragDrop {
padding: $euiSizeXXL ($euiSizeXL * 2);
border: $euiBorderThin;
border-radius: $euiBorderRadius;
}

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { EuiText } from '@elastic/eui';
import { EuiText, UseEuiTheme } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { UiActionsStart, VISUALIZE_GEO_FIELD_TRIGGER } from '@kbn/ui-actions-plugin/public';
@ -15,7 +15,7 @@ import { Droppable } from '@kbn/dom-drag-drop';
import { IndexPattern } from '../../../types';
import { getVisualizeGeoFieldMessage } from '../../../utils';
import { APP_ID } from '../../../../common/constants';
import './geo_field_workspace_panel.scss';
import { pageContentBodyStyles, promptIllustrationStyle } from './workspace_panel';
interface Props {
fieldType: string;
@ -45,15 +45,15 @@ export function GeoFieldWorkspacePanel(props: Props) {
}
return (
<div className="lnsWorkspacePanelWrapper__pageContentBody">
<EuiText className="lnsWorkspacePanel__emptyContent" textAlign="center" size="s">
<div className="eui-scrollBar" css={pageContentBodyStyles}>
<EuiText textAlign="center" size="s">
<div>
<h2>
<strong>{getVisualizeGeoFieldMessage(props.fieldType)}</strong>
</h2>
<GlobeIllustration aria-hidden={true} className="lnsWorkspacePanel__promptIllustration" />
<GlobeIllustration aria-hidden={true} css={promptIllustrationStyle} />
<Droppable
className="lnsVisualizeGeoFieldWorkspacePanel__dragDrop"
css={droppableStyles}
dataTestSubj="lnsGeoFieldWorkspace"
dropTypes={['field_add']}
order={dragDropOrder}
@ -74,3 +74,11 @@ export function GeoFieldWorkspacePanel(props: Props) {
</div>
);
}
const droppableStyles = ({ euiTheme }: UseEuiTheme) => {
return `
padding: ${euiTheme.size.xxl} ${euiTheme.size.xxxl};
border: ${euiTheme.border.thin};
border-radius: ${euiTheme.border.radius};
`;
};

View file

@ -1,26 +0,0 @@
.lnsWorkspaceWarning__buttonText {
@include euiBreakpoint('xs', 's', 'm', 'l') {
@include euiScreenReaderOnly;
}
}
.lnsWorkspaceWarningList {
max-height: $euiSize * 20;
width: $euiSize * 16;
@include euiYScroll;
}
.lnsWorkspaceWarningList__item {
& + & {
border-top: $euiBorderThin;
}
}
.lnsWorkspaceWarningList__textItem {
padding: $euiSize;
}
.lnsWorkspaceWarningList__description {
overflow-wrap: break-word;
min-width: 0;
}

View file

@ -5,9 +5,6 @@
* 2.0.
*/
import './workspace_panel_wrapper.scss';
import './message_list.scss';
import React, { useState } from 'react';
import {
EuiPopover,
@ -17,6 +14,7 @@ import {
EuiToolTip,
EuiFlexGroup,
EuiFlexItem,
type UseEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { css, SerializedStyles } from '@emotion/react';
@ -78,7 +76,6 @@ export const MessageList = ({
minWidth={0}
color={errorCount ? 'danger' : 'warning'}
onClick={onButtonClick}
className="lnsWorkspaceWarning__button"
data-test-subj="lens-message-list-trigger"
title={buttonLabel}
css={customButtonStyles}
@ -106,11 +103,11 @@ export const MessageList = ({
isOpen={isPopoverOpen}
closePopover={closePopover}
>
<ul className="lnsWorkspaceWarningList">
<ul css={workspaceWarningListStyles.self} className="eui-yScroll">
{messages.map(({ hidePopoverIcon = false, ...message }, index) => (
<li
key={index}
className="lnsWorkspaceWarningList__item"
css={workspaceWarningListStyles.item}
data-test-subj={`lens-message-list-${message.severity}`}
>
{typeof message.longMessage === 'function' ? (
@ -119,7 +116,7 @@ export const MessageList = ({
<EuiFlexGroup
gutterSize="s"
responsive={false}
className="lnsWorkspaceWarningList__textItem"
css={workspaceWarningListStyles.textItem}
>
{!hidePopoverIcon && (
<EuiFlexItem grow={false}>
@ -130,7 +127,7 @@ export const MessageList = ({
)}
</EuiFlexItem>
)}
<EuiFlexItem grow={1} className="lnsWorkspaceWarningList__description">
<EuiFlexItem grow={1} css={workspaceWarningListStyles.description}>
<EuiText size="s">{message.longMessage}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
@ -141,3 +138,22 @@ export const MessageList = ({
</EuiPopover>
);
};
const workspaceWarningListStyles = {
self: css`
max-height: 320px;
width: 256px;
`,
item: ({ euiTheme }: UseEuiTheme) => `
& + & {
border-top: 1px solid ${euiTheme.colors.lightShade};
}
`,
textItem: ({ euiTheme }: UseEuiTheme) => `
padding: ${euiTheme.size.base}
`,
description: css`
overflow-wrap: break-word;
min-width: 0;
`,
};

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import './workspace_panel_wrapper.scss';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiScreenReaderOnly } from '@elastic/eui';

View file

@ -17,7 +17,7 @@ import {
renderWithReduxStore,
} from '../../../mocks';
import { mockDataPlugin, mountWithProvider } from '../../../mocks';
import { mockDataPlugin, mountWithReduxStore } from '../../../mocks';
import { WorkspacePanel } from './workspace_panel';
import { ReactWrapper } from 'enzyme';
@ -367,7 +367,7 @@ describe('workspace_panel', () => {
const visualizationShowing = () => instance.exists(expressionRendererMock);
const mounted = await mountWithProvider(
const mounted = mountWithReduxStore(
<WorkspacePanel
{...defaultProps}
datasourceMap={{
@ -431,7 +431,7 @@ describe('workspace_panel', () => {
mockDatasource.getLayers.mockReturnValue(['first']);
const props = defaultProps;
const mounted = await mountWithProvider(
const mounted = mountWithReduxStore(
<WorkspacePanel
{...props}
datasourceMap={{
@ -467,7 +467,7 @@ describe('workspace_panel', () => {
mockDatasource.getLayers.mockReturnValue(['first']);
const props = defaultProps;
const mounted = await mountWithProvider(
const mounted = mountWithReduxStore(
<WorkspacePanel
{...props}
datasourceMap={{
@ -505,7 +505,7 @@ describe('workspace_panel', () => {
mockDatasource.getLayers.mockReturnValue(['first']);
const props = defaultProps;
const mounted = await mountWithProvider(
const mounted = mountWithReduxStore(
<WorkspacePanel
{...props}
datasourceMap={{
@ -540,7 +540,7 @@ describe('workspace_panel', () => {
mockDatasource.toExpression.mockReturnValue('datasource');
mockDatasource.getLayers.mockReturnValue(['table1']);
const mounted = await mountWithProvider(
const mounted = mountWithReduxStore(
<WorkspacePanel
{...defaultProps}
datasourceMap={{
@ -585,7 +585,7 @@ describe('workspace_panel', () => {
expressionRendererMock = jest.fn((_arg) => <span />);
const mounted = await mountWithProvider(
const mounted = mountWithReduxStore(
<WorkspacePanel
{...defaultProps}
datasourceMap={{
@ -629,7 +629,7 @@ describe('workspace_panel', () => {
.mockReturnValueOnce('datasource second');
expressionRendererMock = jest.fn((_arg) => <span />);
const mounted = await mountWithProvider(
const mounted = mountWithReduxStore(
<WorkspacePanel
{...defaultProps}
datasourceMap={{
@ -687,7 +687,7 @@ describe('workspace_panel', () => {
const getUserMessages = jest.fn(() => messages);
const mounted = await mountWithProvider(
const mounted = mountWithReduxStore(
<WorkspacePanel
{...defaultProps}
getUserMessages={getUserMessages}
@ -717,7 +717,7 @@ describe('workspace_panel', () => {
let userMessages = [] as UserMessage[];
const getUserMessageFn = jest.fn(() => userMessages);
const mounted = await mountWithProvider(
const mounted = mountWithReduxStore(
<WorkspacePanel
{...defaultProps}
getUserMessages={getUserMessageFn}
@ -786,7 +786,7 @@ describe('workspace_panel', () => {
const mockAddUserMessages = jest.fn(() => mockRemoveUserMessages);
const mockGetUserMessages = jest.fn<UserMessage[], unknown[]>(() => []);
const mounted = await mountWithProvider(
const mounted = mountWithReduxStore(
<WorkspacePanel
{...defaultProps}
datasourceMap={{
@ -814,7 +814,7 @@ describe('workspace_panel', () => {
first: mockDatasource.publicAPIMock,
};
const mounted = await mountWithProvider(
const mounted = mountWithReduxStore(
<WorkspacePanel
{...defaultProps}
datasourceMap={{
@ -844,7 +844,7 @@ describe('workspace_panel', () => {
framePublicAPI.datasourceLayers = {
first: mockDatasource.publicAPIMock,
};
const mounted = await mountWithProvider(
const mounted = mountWithReduxStore(
<WorkspacePanel
{...defaultProps}
datasourceMap={{

View file

@ -12,7 +12,16 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { toExpression } from '@kbn/interpreter';
import type { KibanaExecutionContext } from '@kbn/core-execution-context-common';
import { i18n } from '@kbn/i18n';
import { EuiText, EuiButtonEmpty, EuiLink, EuiTextColor } from '@elastic/eui';
import {
EuiText,
EuiButtonEmpty,
EuiLink,
EuiTextColor,
transparentize,
useEuiTheme,
EuiSpacer,
type UseEuiTheme,
} from '@elastic/eui';
import type { CoreStart } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type {
@ -27,6 +36,7 @@ import { DropIllustration } from '@kbn/chart-icons';
import { useDragDropContext, DragDropIdentifier, Droppable } from '@kbn/dom-drag-drop';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { ChartSizeSpec, isChartSizeEvent } from '@kbn/chart-expressions-common';
import { css } from '@emotion/react';
import { getSuccessfulRequestTimings } from '../../../report_performance_metric_util';
import { trackUiCounterEvents } from '../../../lens_ui_telemetry';
import { getSearchWarningMessages } from '../../../utils';
@ -51,6 +61,7 @@ import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
import applyChangesIllustrationDark from '../../../assets/render_dark@2x.png';
import applyChangesIllustrationLight from '../../../assets/render_light@2x.png';
import { getOriginalRequestErrorMessages } from '../../error_helper';
import { lnsExpressionRendererStyle } from '../../../expression_renderer_styles';
import {
onActiveDataChange,
useLensDispatch,
@ -195,6 +206,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
const dataReceivedTime = useRef<number>(NaN);
const esTookTime = useRef<number>(0);
const { euiTheme } = useEuiTheme();
const onRender$ = useCallback(() => {
if (renderDeps.current) {
if (!initialVisualizationRenderComplete.current) {
@ -495,19 +508,18 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
}
return (
<EuiText
className={classNames('lnsWorkspacePanel__emptyContent')}
textAlign="center"
data-test-subj="workspace-drag-drop-prompt"
size="s"
>
<EuiText textAlign="center" data-test-subj="workspace-drag-drop-prompt" size="s">
<div>
<DropIllustration
aria-hidden={true}
className={classNames(
'lnsWorkspacePanel__promptIllustration',
'lnsWorkspacePanel__dropIllustration'
)}
css={[
css`
filter: drop-shadow(0 6px 12px ${transparentize(euiTheme.colors.shadow, 0.2)})
drop-shadow(0 4px 4px ${transparentize(euiTheme.colors.shadow, 0.2)})
drop-shadow(0 2px 2px ${transparentize(euiTheme.colors.shadow, 0.2)});
`,
promptIllustrationStyle,
]}
/>
<h2>
<strong>
@ -521,15 +533,21 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
</strong>
</h2>
{!expressionExists && (
<>
<EuiTextColor color="subdued" component="div">
<p>
{i18n.translate('xpack.lens.editorFrame.emptyWorkspaceHeading', {
defaultMessage: 'Lens is the recommended editor for creating visualizations',
})}
</p>
<div
css={css`
.domDroppable--active & {
filter: blur(5px);
transition: filter ${euiTheme.animation.fast} ease-in-out;
}
`}
>
<EuiTextColor color="subdued" component="p">
{i18n.translate('xpack.lens.editorFrame.emptyWorkspaceHeading', {
defaultMessage: 'Lens is the recommended editor for creating visualizations',
})}
</EuiTextColor>
<p className="lnsWorkspacePanel__actions">
<EuiSpacer size="s" />
<p>
<EuiLink
href="https://www.elastic.co/products/kibana/feedback"
target="_blank"
@ -540,7 +558,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
})}
</EuiLink>
</p>
</>
</div>
)}
</div>
</EuiText>
@ -557,18 +575,13 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
});
return (
<EuiText
className={classNames('lnsWorkspacePanel__emptyContent')}
textAlign="center"
data-test-subj="workspace-apply-changes-prompt"
size="s"
>
<EuiText textAlign="center" data-test-subj="workspace-apply-changes-prompt" size="s">
<div>
<img
aria-hidden={true}
css={promptIllustrationStyle}
src={IS_DARK_THEME ? applyChangesIllustrationDark : applyChangesIllustrationLight}
alt={applyChangesString}
className="lnsWorkspacePanel__promptIllustration"
/>
<h2>
<strong>
@ -577,7 +590,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
})}
</strong>
</h2>
<p className="lnsWorkspacePanel__actions">
<EuiSpacer size="s" />
<p>
<EuiButtonEmpty
size="s"
className={DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS}
@ -645,13 +659,28 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
className={classNames('lnsWorkspacePanel__dragDrop', {
'lnsWorkspacePanel__dragDrop--fullscreen': isFullscreen,
})}
css={css`
${isFullscreen && `border: none !important;`}
`}
dataTestSubj="lnsWorkspace"
dropTypes={suggestionForDraggedField ? ['field_add'] : undefined}
onDrop={onDrop}
value={dropProps.value}
order={dropProps.order}
>
<div className="lnsWorkspacePanelWrapper__pageContentBody">{renderWorkspaceContents()}</div>
<div
className="eui-scrollBar"
css={[
pageContentBodyStyles,
isFullscreen &&
`
box-shadow: none;
border-radius: 0;
`,
]}
>
{renderWorkspaceContents()}
</div>
</Droppable>
);
};
@ -730,6 +759,7 @@ export const VisualizationWrapper = ({
onComponentRendered();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { euiTheme } = useEuiTheme();
const searchContext = useLensSelector(selectExecutionContextSearch);
// Used for reporting
@ -765,7 +795,16 @@ export const VisualizationWrapper = ({
return (
<div
className="lnsExpressionRenderer"
className="lnsExpressionRenderer eui-scrollBar"
css={[
lnsExpressionRendererStyle,
`
.domDroppable--active & {
filter: blur(${euiTheme.size.xs}) !important;
opacity: .25 !important;
transition: filter ${euiTheme.animation.normal} ease-in-out, opacity ${euiTheme.animation.normal} ease-in-out;
}`,
]}
data-shared-items-container
data-render-complete={isRenderComplete}
data-shared-item=""
@ -773,7 +812,6 @@ export const VisualizationWrapper = ({
ref={nodeRef}
>
<ExpressionRendererComponent
className="lnsExpressionRenderer__component"
padding={displayOptions?.noPadding ? undefined : 'm'}
expression={expression!}
allowCache={true}
@ -807,3 +845,36 @@ export const VisualizationWrapper = ({
</div>
);
};
export const promptIllustrationStyle = ({ euiTheme }: UseEuiTheme) => {
return css`
overflow: visible; // Shows arrow animation when it gets out of bounds
margin-top: 0;
margin-bottom: -${euiTheme.size.base};
margin-right: auto;
margin-left: auto;
max-width: 176px;
max-height: 176px;
`;
};
export const pageContentBodyStyles = ({ euiTheme }: UseEuiTheme) => {
return css`
flex-grow: 1;
display: flex;
align-items: stretch;
justify-content: stretch;
border: ${euiTheme.border.thin};
border-radius: ${euiTheme.border.radius.medium};
background: ${euiTheme.colors.emptyShade};
height: 100%;
overflow: hidden;
& > * {
flex: 1 1 100%;
display: flex;
align-items: center;
justify-content: center;
}
`;
};

View file

@ -1,186 +0,0 @@
@import '../../../mixins';
.lnsWorkspacePanelWrapper {
margin-bottom: $euiSize;
display: flex;
flex-direction: column;
position: relative; // For positioning the dnd overlay
min-height: $euiSizeXXL * 10;
overflow: visible;
height: 100%;
.lnsWorkspacePanelWrapper__content {
width: 100%;
height: 100%;
position: absolute;
}
.lnsWorkspacePanelWrapper__pageContentBody {
flex-grow: 1;
display: flex;
align-items: stretch;
justify-content: stretch;
border: $euiBorderThin;
border-radius: $euiBorderRadius;
background: $euiColorEmptyShade;
height: 100%;
overflow: hidden;
@include euiScrollBar;
&>* {
flex: 1 1 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
}
&.lnsWorkspacePanelWrapper--fullscreen {
margin-bottom: 0;
.lnsWorkspacePanelWrapper__pageContentBody {
box-shadow: none;
}
}
}
.lnsWorkspacePanel__dragDrop {
&.domDroppable--active {
p {
transition: filter $euiAnimSpeedFast ease-in-out;
filter: blur(5px);
}
.lnsExpressionRenderer {
transition: filter $euiAnimSpeedNormal ease-in-out, opacity $euiAnimSpeedNormal ease-in-out;
filter: blur($euiSizeXS);
opacity: .25;
}
}
&.domDroppable--hover {
.lnsDropIllustration__hand {
animation: lnsWorkspacePanel__illustrationPulseContinuous 1.5s ease-in-out 0s infinite normal forwards;
}
}
&.lnsWorkspacePanel__dragDrop--fullscreen {
border: none;
}
}
.lnsWorkspacePanel__emptyContent {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
display: flex;
justify-content: center;
align-items: center;
transition: background-color $euiAnimSpeedFast ease-in-out;
.lnsWorkspacePanel__actions {
margin-top: $euiSizeL;
}
}
.lnsWorkspacePanelWrapper__toolbar {
margin-bottom: $euiSizeXS;
}
.lnsWorkspacePanelWrapper__toolbar--fullscreen {
background-color: $euiColorEmptyShade;
justify-content: flex-end;
margin-bottom: 0;
padding: $euiSizeS $euiSizeS 0;
}
.lnsWorkspacePanelWrapper__applyButton .euiButton__text {
@include euiBreakpoint('xs', 's', 'm', 'l') {
@include euiScreenReaderOnly;
}
}
.lnsWorkspacePanel__promptIllustration {
overflow: visible; // Shows arrow animation when it gets out of bounds
margin-top: 0;
margin-bottom: -$euiSize;
margin-right: auto;
margin-left: auto;
max-width: 176px;
max-height: 176px;
}
.lnsWorkspacePanel__dropIllustration {
// Drop shadow values is a dupe of @euiBottomShadowMedium but used as a filter
// Hard-coded px values OK (@cchaos)
// sass-lint:disable-block indentation
filter:
drop-shadow(0 6px 12px transparentize($euiShadowColor, .8)) drop-shadow(0 4px 4px transparentize($euiShadowColor, .8)) drop-shadow(0 2px 2px transparentize($euiShadowColor, .8));
}
.lnsDropIllustration__adjustFill {
fill: $euiColorFullShade;
}
.lnsDropIllustration__hand {
animation: lnsWorkspacePanel__illustrationPulseArrow 5s ease-in-out 0s infinite normal forwards;
}
@keyframes lnsWorkspacePanel__illustrationPulseArrow {
0% {
transform: translateY(0%);
}
65% {
transform: translateY(0%);
}
72% {
transform: translateY(10%);
}
79% {
transform: translateY(7%);
}
86% {
transform: translateY(10%);
}
95% {
transform: translateY(0);
}
}
@keyframes lnsWorkspacePanel__illustrationPulseContinuous {
0% {
transform: translateY(10%);
}
25% {
transform: translateY(15%);
}
50% {
transform: translateY(10%);
}
75% {
transform: translateY(15%);
}
100% {
transform: translateY(10%);
}
}
.lnsVisualizationToolbar--fixed {
position: fixed;
width: 100%;
z-index: 1;
background-color: $euiColorLightestShade;
}

View file

@ -5,10 +5,8 @@
* 2.0.
*/
import './workspace_panel_wrapper.scss';
import React, { useCallback } from 'react';
import { EuiPageTemplate, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { EuiPageTemplate, EuiFlexGroup, EuiFlexItem, EuiButton, useEuiTheme } from '@elastic/eui';
import classNames from 'classnames';
import { FormattedMessage } from '@kbn/i18n-react';
import { ChartSizeSpec } from '@kbn/chart-expressions-common';
@ -74,11 +72,10 @@ const getAspectRatioStyles = ({ x, y }: { x: number; y: number }) => {
export function VisualizationToolbar(props: {
activeVisualization: Visualization | null;
framePublicAPI: FramePublicAPI;
isFixedPosition?: boolean;
}) {
const dispatchLens = useLensDispatch();
const visualization = useLensSelector(selectVisualizationState);
const { activeVisualization, isFixedPosition } = props;
const { activeVisualization } = props;
const setVisualizationState = useCallback(
(newState: unknown) => {
if (!activeVisualization) {
@ -99,12 +96,7 @@ export function VisualizationToolbar(props: {
return (
<>
{ToolbarComponent && (
<EuiFlexItem
grow={false}
className={classNames({
'lnsVisualizationToolbar--fixed': isFixedPosition,
})}
>
<EuiFlexItem grow={false}>
{ToolbarComponent({
frame: props.framePublicAPI,
state: visualization.state,
@ -128,6 +120,9 @@ export function WorkspacePanelWrapper({
}: WorkspacePanelWrapperProps) {
const dispatchLens = useLensDispatch();
const euiThemeContext = useEuiTheme();
const { euiTheme } = euiThemeContext;
const changesApplied = useLensSelector(selectChangesApplied);
const autoApplyEnabled = useLensSelector(selectAutoApplyEnabled);
@ -180,9 +175,16 @@ export function WorkspacePanelWrapper({
alignItems="flexEnd"
gutterSize="s"
direction="row"
className={classNames('lnsWorkspacePanelWrapper__toolbar', {
'lnsWorkspacePanelWrapper__toolbar--fullscreen': isFullscreen,
})}
css={css`
margin-bottom: ${euiTheme.size.xs};
${isFullscreen &&
`
background-color: ${euiTheme.colors.emptyShade};
justify-content: flex-end;
margin-bottom: 0;
padding: ${euiTheme.size.s} ${euiTheme.size.s} 0;
`}
`}
responsive={false}
>
{!isFullscreen && (
@ -238,10 +240,30 @@ export function WorkspacePanelWrapper({
contentProps={{
className: 'lnsWorkspacePanelWrapper__content',
}}
className={classNames('lnsWorkspacePanelWrapper stretch-for-sharing', {
'lnsWorkspacePanelWrapper--fullscreen': isFullscreen,
})}
css={{ height: '100%' }}
className={classNames('lnsWorkspacePanelWrapper stretch-for-sharing')}
css={css`
height: 100%;
margin-bottom: ${euiTheme.size.base};
display: flex;
flex-direction: column;
position: relative; // For positioning the dnd overlay
min-height: 400px;
overflow: visible;
height: 100%;
.lnsWorkspacePanelWrapper__content {
width: 100%;
height: 100%;
position: absolute;
}
${isFullscreen &&
`
margin-bottom: 0;
.lnsWorkspacePanelWrapper__content {
padding: ${euiTheme.size.s}
}
`}
`}
color="transparent"
>
<EuiFlexGroup

View file

@ -24,6 +24,7 @@ import {
DataViewsPublicPluginStart,
} from '@kbn/data-views-plugin/public';
import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
import { css } from '@emotion/react';
import { LensDocument } from '../persistence/saved_object_store';
import {
Datasource,
@ -136,7 +137,14 @@ export class EditorFrameService {
addUserMessages,
}) => {
return (
<div className="lnsApp__frame">
<div
css={css`
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
`}
>
<EditorFrame
data-test-subj="lnsEditorFrame"
core={core}

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { UseEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
export const lnsExpressionRendererStyle = (euiThemeContext: UseEuiTheme) => {
return css`
position: relative;
width: 100%;
height: 100%;
display: flex;
overflow: auto;
`;
};

View file

@ -25,7 +25,7 @@ export {
mockDatasourceStates,
defaultState,
makeLensStore,
mountWithProvider,
mountWithReduxStore,
renderWithReduxStore,
} from './store_mocks';
export { lensPluginMock } from './lens_plugin_mock';

View file

@ -6,12 +6,12 @@
*/
import React, { PropsWithChildren, ReactElement } from 'react';
import { ReactWrapper, mount } from 'enzyme';
import { ReactWrapper } from 'enzyme';
import { Provider } from 'react-redux';
import { PreloadedState } from '@reduxjs/toolkit';
import { RenderOptions, render } from '@testing-library/react';
import { I18nProvider } from '@kbn/i18n-react';
import { RenderOptions } from '@testing-library/react';
import { LensAppServices } from '../app_plugin/types';
import { mountWithProviders, renderWithProviders } from '../test_utils/test_utils';
import { makeConfigureStore, LensAppState, LensState, LensStoreDeps } from '../state_management';
import { getResolvedDateRange } from '../utils';
import { DatasourceMap, VisualizationMap } from '../types';
@ -19,21 +19,15 @@ import { mockVisualizationMap } from './visualization_mock';
import { mockDatasourceMap } from './datasource_mock';
import { makeDefaultServices } from './services_mock';
export const mockStoreDeps = (
{
lensServices = makeDefaultServices(),
datasourceMap = mockDatasourceMap(),
visualizationMap = mockVisualizationMap(),
}: {
lensServices?: LensAppServices;
datasourceMap?: DatasourceMap;
visualizationMap?: VisualizationMap;
} = {
lensServices: makeDefaultServices(),
datasourceMap: mockDatasourceMap(),
visualizationMap: mockVisualizationMap(),
}
) => ({
export const mockStoreDeps = ({
lensServices = makeDefaultServices(),
datasourceMap = mockDatasourceMap(),
visualizationMap = mockVisualizationMap(),
}: {
lensServices?: LensAppServices;
datasourceMap?: DatasourceMap;
visualizationMap?: VisualizationMap;
} = {}) => ({
datasourceMap,
visualizationMap,
lensServices,
@ -86,17 +80,13 @@ export const renderWithReduxStore = (
const CustomWrapper = wrapper as React.ComponentType<React.PropsWithChildren<{}>>;
const Wrapper: React.FC<PropsWithChildren<{}>> = ({ children }) => {
return (
<Provider store={store}>
<I18nProvider>
{wrapper ? <CustomWrapper>{children}</CustomWrapper> : children}
</I18nProvider>
</Provider>
);
};
const Wrapper: React.FC<PropsWithChildren<{}>> = ({ children }) => (
<Provider store={store}>
{wrapper ? <CustomWrapper>{children}</CustomWrapper> : children}
</Provider>
);
const rtlRender = render(ui, { wrapper: Wrapper, ...options });
const rtlRender = renderWithProviders(ui, { wrapper: Wrapper, ...options });
return {
store,
@ -106,13 +96,11 @@ export const renderWithReduxStore = (
export function makeLensStore({
preloadedState,
dispatch,
storeDeps = mockStoreDeps(),
}: {
storeDeps?: LensStoreDeps;
preloadedState?: Partial<LensAppState>;
dispatch?: jest.Mock;
}) {
} = {}) {
const data = storeDeps.lensServices.data;
const store = makeConfigureStore(storeDeps, {
lens: {
@ -124,18 +112,17 @@ export function makeLensStore({
},
} as unknown as PreloadedState<LensState>);
const origDispatch = store.dispatch;
store.dispatch = jest.fn(dispatch || origDispatch);
store.dispatch = jest.spyOn(store, 'dispatch') as jest.Mock;
return { store, deps: storeDeps };
}
export interface MountStoreProps {
storeDeps?: LensStoreDeps;
preloadedState?: Partial<LensAppState>;
dispatch?: jest.Mock;
}
export const mountWithProvider = async (
// legacy enzyme usage: remove when all tests are migrated to @testing-library/react
export const mountWithReduxStore = (
component: React.ReactElement,
store?: MountStoreProps,
options?: {
@ -144,52 +131,24 @@ export const mountWithProvider = async (
attachTo?: HTMLElement;
}
) => {
const { mountArgs, lensStore, deps } = getMountWithProviderParams(component, store, options);
const instance = mount(mountArgs.component, mountArgs.options);
return { instance, lensStore, deps };
};
const getMountWithProviderParams = (
component: React.ReactElement,
store?: MountStoreProps,
options?: {
wrappingComponent?: React.FC<PropsWithChildren<{}>>;
wrappingComponentProps?: Record<string, unknown>;
attachTo?: HTMLElement;
}
) => {
const { store: lensStore, deps } = makeLensStore(store || {});
const { store: lensStore, deps } = makeLensStore(store);
let wrappingComponent: React.FC<PropsWithChildren<{}>> = ({ children }) => (
<I18nProvider>
<Provider store={lensStore}>{children}</Provider>
</I18nProvider>
<Provider store={lensStore}>{children}</Provider>
);
let restOptions: {
attachTo?: HTMLElement | undefined;
} = {};
if (options) {
const { wrappingComponent: _wrappingComponent, wrappingComponentProps, ...rest } = options;
restOptions = rest;
if (_wrappingComponent) {
wrappingComponent = ({ children }) => {
return _wrappingComponent({
...wrappingComponentProps,
children: <Provider store={lensStore}>{children}</Provider>,
});
};
}
if (options?.wrappingComponent) {
wrappingComponent = ({ children }) => {
return options?.wrappingComponent?.({
...options?.wrappingComponentProps,
children: wrappingComponent({ children }),
});
};
}
const mountArgs = {
component,
options: {
wrappingComponent,
...restOptions,
} as unknown as ReactWrapper,
};
const instance = mountWithProviders(component, {
...options,
wrappingComponent,
} as unknown as ReactWrapper);
return { mountArgs, lensStore, deps };
return { instance, lensStore, deps };
};

View file

@ -18,6 +18,7 @@ import classNames from 'classnames';
import { getOriginalRequestErrorMessages } from '../editor_frame_service/error_helper';
import { LensInspector } from '../lens_inspector_service';
import { UserMessage } from '../types';
import { lnsExpressionRendererStyle } from '../expression_renderer_styles';
export interface ExpressionWrapperProps {
ExpressionRenderer: ReactExpressionRendererType;
@ -76,12 +77,12 @@ export function ExpressionWrapper({
if (!expression) return null;
return (
<div
className={classNames('lnsExpressionRenderer', className)}
className={classNames('lnsExpressionRenderer', 'eui-scrollBar', className)}
css={lnsExpressionRendererStyle}
style={style}
data-test-subj="lens-embeddable"
>
<ExpressionRendererComponent
className="lnsExpressionRenderer__component"
padding={noPadding ? undefined : 's'}
variables={variables}
allowCache={true}

View file

@ -10,6 +10,7 @@ import { TracksOverlays } from '@kbn/presentation-containers';
import { toMountPoint } from '@kbn/react-kibana-mount';
import React from 'react';
import ReactDOM from 'react-dom';
import { type UseEuiTheme } from '@elastic/eui';
/**
* Shared logic to mount the inline config panel
@ -41,6 +42,7 @@ export function mountInlineEditPanel(
),
{
className: 'lnsConfigPanel__overlay',
css: inlineFlyoutStyles,
size: 's',
'data-test-subj': 'customizeLens',
type: 'push',
@ -60,3 +62,22 @@ export function mountInlineEditPanel(
}
}
}
// styles needed to display extra drop targets that are outside of the config panel main area while also allowing to scroll vertically
const inlineFlyoutStyles = ({ euiTheme }: UseEuiTheme) => `
clip-path: polygon(-100% 0, 100% 0, 100% 100%, -100% 100%);
max-inline-size: 640px;
min-inline-size: 256px;
background:${euiTheme.colors.backgroundBaseSubdued};
@include euiBreakpoint('xs', 's', 'm') {
clip-path: none;
}
.kbnOverlayMountWrapper {
padding-left: 400px;
margin-left: -400px;
pointer-events: none;
.euiFlyoutFooter {
pointer-events: auto;
}
}
`;

View file

@ -8,6 +8,7 @@
import { EuiEmptyPrompt } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { UserMessage } from '../../types';
import { getLongMessage } from '../../user_messages_utils';
@ -24,7 +25,16 @@ export function VisualizationErrorPanel({
const showMore = errors.length > 1;
const canFixInLens = canEdit && errors.some(({ fixableInEditor }) => fixableInEditor);
return (
<div className="lnsEmbeddedError">
<div
className="lnsEmbeddedError"
css={css`
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
`}
>
<EuiEmptyPrompt
iconType="warning"
iconColor="danger"

View file

@ -1,4 +0,0 @@
.lnsPanelFeatureList {
max-height: $euiSize * 20;
@include euiYScroll;
}

View file

@ -19,7 +19,6 @@ import { i18n } from '@kbn/i18n';
import React, { Fragment } from 'react';
import { useState } from 'react';
import type { UserMessage } from '../../types';
import './info_badges.scss';
import { getLongMessage } from '../../user_messages_utils';
export const EmbeddableFeatureBadge = ({ messages }: { messages: UserMessage[] }) => {
@ -65,7 +64,13 @@ export const EmbeddableFeatureBadge = ({ messages }: { messages: UserMessage[] }
gap: ${euiTheme.size.xs};
}
&:hover {
color: ${euiTheme.colors.text};
color: ${euiTheme.colors.textParagraph};
}
// Make the visualization modifiers icon appear only on panel hover
.embPanel__content:hover & {
background: ${euiTheme.colors.backgroundBasePlain};
transition: color ${euiTheme.animation.slow}, background ${euiTheme.animation.slow};
color: ${euiTheme.colors.textParagraph};
}
`}
iconType="wrench"
@ -101,7 +106,12 @@ export const EmbeddableFeatureBadge = ({ messages }: { messages: UserMessage[] }
<EuiTitle size="xxs" css={css`color=${euiTheme.colors.title}`}>
<h3>{shortMessage}</h3>
</EuiTitle>
<ul className="lnsPanelFeatureList">
<ul
className="eui-yScroll"
css={css`
max-height: 320px;
`}
>
{messageGroup.map((message, i) => (
<Fragment key={`${uniqueId}-${i}`}>{getLongMessage(message)}</Fragment>
))}

View file

@ -1,61 +0,0 @@
.kbnToolbarButton {
line-height: $euiButtonHeight; // Keeps alignment of text and chart icon
// todo: once issue https://github.com/elastic/eui/issues/4730 is merged, this code might be safe to remove
// Some toolbar buttons are just icons, but EuiButton comes with margin and min-width that need to be removed
min-width: 0;
border-width: $euiBorderWidthThin;
border-style: solid;
border-color: $euiBorderColor; // Lighten the border color for all states
// Override background color for non-disabled buttons
&:not(:disabled) {
background-color: $euiColorEmptyShade;
}
.kbnToolbarButton__text > svg {
margin-top: -1px; // Just some weird alignment issue when icon is the child not the `iconType`
}
.kbnToolbarButton__text:empty {
margin: 0;
}
// Toolbar buttons don't look good with centered text when fullWidth
&[class*='fullWidth'] {
text-align: left;
.kbnToolbarButton__content {
justify-content: space-between;
}
}
}
.kbnToolbarButton--groupLeft {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.kbnToolbarButton--groupCenter {
border-radius: 0;
border-left: none;
}
.kbnToolbarButton--groupRight {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: none;
}
.kbnToolbarButton--bold {
font-weight: $euiFontWeightBold;
}
.kbnToolbarButton--normal {
font-weight: $euiFontWeightRegular;
}
.kbnToolbarButton--s {
box-shadow: none !important; // sass-lint:disable-line no-important
font-size: $euiFontSizeS;
}

View file

@ -5,10 +5,17 @@
* 2.0.
*/
import './toolbar_button.scss';
import React from 'react';
import classNames from 'classnames';
import { EuiButton, PropsOf, EuiButtonProps } from '@elastic/eui';
import {
EuiButton,
PropsOf,
EuiButtonProps,
type UseEuiTheme,
euiFontSize,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
const groupPositionToClassMap = {
none: null,
@ -57,6 +64,7 @@ export const ToolbarButton: React.FunctionComponent<ToolbarButtonProps> = ({
textProps,
...rest
}) => {
const euiThemeContext = useEuiTheme();
const classes = classNames(
'kbnToolbarButton',
groupPositionToClassMap[groupPosition],
@ -69,6 +77,7 @@ export const ToolbarButton: React.FunctionComponent<ToolbarButtonProps> = ({
data-test-subj={dataTestSubj}
className={classes}
iconSide="right"
css={toolbarButtonStyles(euiThemeContext)}
iconType={hasArrow ? 'arrowDown' : ''}
color="text"
contentProps={{
@ -85,3 +94,70 @@ export const ToolbarButton: React.FunctionComponent<ToolbarButtonProps> = ({
</EuiButton>
);
};
const toolbarButtonStyles = (euiThemeContext: UseEuiTheme) => {
const { euiTheme } = euiThemeContext;
return css`
&.kbnToolbarButton {
line-height: ${euiTheme.size.xxl}; // Keeps alignment of text and chart icon
// todo: once issue https://github.com/elastic/eui/issues/4730 is merged, this code might be safe to remove
// Some toolbar buttons are just icons, but EuiButton comes with margin and min-width that need to be removed
min-width: 0;
border-width: ${euiTheme.border.width.thin};
border-style: solid;
border-color: ${euiTheme.border.color}; // Lighten the border color for all states
// Override background color for non-disabled buttons
&:not(:disabled) {
background-color: ${euiTheme.colors.backgroundBasePlain};
}
&.kbnToolbarButton__text > svg {
margin-top: -1px; // Just some weird alignment issue when icon is the child not the iconType
}
&.kbnToolbarButton__text:empty {
margin: 0;
}
// Toolbar buttons don't look good with centered text when fullWidth
&[class*='fullWidth'] {
text-align: left;
.kbnToolbarButton__content {
justify-content: space-between;
}
}
}
&.kbnToolbarButton--groupLeft {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&.kbnToolbarButton--groupCenter {
border-radius: 0;
border-left: none;
}
&.kbnToolbarButton--groupRight {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: none;
}
&.kbnToolbarButton--bold {
font-weight: ${euiTheme.font.weight.bold};
}
&.kbnToolbarButton--normal {
font-weight: ${euiTheme.font.weight.regular};
}
&.kbnToolbarButton--s {
box-shadow: none !important; // sass-lint:disable-line no-important
font-size: ${euiFontSize(euiThemeContext, 's').fontSize};
}
`;
};

View file

@ -6,21 +6,23 @@
*/
import React from 'react';
import { EuiIcon } from '@elastic/eui';
import { render, screen } from '@testing-library/react';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TriggerButton } from './trigger';
import { renderWithProviders } from '../../test_utils/test_utils';
import * as ToolbarButtonFile from './toolbar_button';
describe('TriggerButton', () => {
describe('base version (no icons)', () => {
it('should render the basic button', () => {
render(
renderWithProviders(
<TriggerButton togglePopover={jest.fn()} label={'Trigger label'} dataTestSubj="test-id" />
);
expect(screen.getByText('Trigger label')).toBeInTheDocument();
});
it('should render the title if provided', () => {
render(
renderWithProviders(
<TriggerButton
togglePopover={jest.fn()}
label={'Trigger'}
@ -33,7 +35,7 @@ describe('TriggerButton', () => {
it('should call the toggle callback on click', async () => {
const toggleFn = jest.fn();
render(
renderWithProviders(
<TriggerButton
togglePopover={toggleFn}
label={'Trigger'}
@ -47,7 +49,8 @@ describe('TriggerButton', () => {
});
it('should render the main label as red if missing', () => {
render(
const ToolbarButtonSpy = jest.spyOn(ToolbarButtonFile, 'ToolbarButton');
renderWithProviders(
<TriggerButton
togglePopover={jest.fn()}
label={'Trigger'}
@ -56,14 +59,16 @@ describe('TriggerButton', () => {
isMissingCurrent
/>
);
// EUI danger red: rgb(167, 22, 39)
expect(screen.getByTestId('test-id')).toHaveStyle({ color: 'rgb(167, 22, 39)' });
expect(ToolbarButtonSpy).toHaveBeenCalledWith(
expect.objectContaining({ color: 'danger' }),
{}
);
});
});
describe('with icons', () => {
it('should render one icon', () => {
render(
renderWithProviders(
<TriggerButton
togglePopover={jest.fn()}
label={'Trigger label'}
@ -83,7 +88,7 @@ describe('TriggerButton', () => {
it('should render multiple icons', () => {
const indexes = [1, 2, 3];
render(
renderWithProviders(
<TriggerButton
togglePopover={jest.fn()}
label={'Trigger label'}
@ -102,7 +107,7 @@ describe('TriggerButton', () => {
});
it('should render the value together with the provided component', () => {
render(
renderWithProviders(
<TriggerButton
togglePopover={jest.fn()}
label={'Trigger label'}

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { UseEuiTheme, euiShadow } from '@elastic/eui';
import { css, keyframes } from '@emotion/react';
const flyoutOpenCloseAnimation = keyframes`
0% {
opacity: 0;
transform: translateX(100%);
}
75% {
opacity: 1;
transform: translateX(0%);
}
`;
export const flyoutContainerStyles = (euiThemeContext: UseEuiTheme) => css`
border-left: ${euiThemeContext.euiTheme.border.thin};
${euiShadow(euiThemeContext, 'xl')};
position: fixed;
top: 0;
bottom: 0;
right: 0;
height: 100%;
z-index: ${euiThemeContext.euiTheme.levels.flyout};
background: ${euiThemeContext.euiTheme.colors.backgroundBasePlain};
display: flex;
flex-direction: column;
align-items: stretch;
animation: ${flyoutOpenCloseAnimation} ${euiThemeContext.euiTheme.animation.normal}
${euiThemeContext.euiTheme.animation.resistance};
.lnsIndexPatternDimensionEditor--padded {
padding: ${euiThemeContext.euiTheme.size.base};
}
.lnsIndexPatternDimensionEditor--collapseNext {
margin-bottom: -${euiThemeContext.euiTheme.size.l};
border-top: ${euiThemeContext.euiTheme.border.thin};
margin-top: 0 !important;
}
`;

View file

@ -1,47 +0,0 @@
@import '../mixins';
.lnsDimensionContainer {
// Use the EuiFlyout style
@include euiFlyout;
// But with custom positioning to keep it within the sidebar contents
animation: euiFlyoutAnimation $euiAnimSpeedNormal $euiAnimSlightResistance;
max-width: none !important;
left: 0;
z-index: $euiZContentMenu;
@include euiBreakpoint('m', 'l', 'xl') {
height: 100% !important;
position: absolute;
top: 0 !important;
}
.lnsFrameLayout__sidebar-isFullscreen & {
border-left: $euiBorderThin; // Force border regardless of theme in fullscreen
box-shadow: none;
}
}
.lnsDimensionContainer__header {
padding: $euiSize;
.lnsFrameLayout__sidebar-isFullscreen & {
display: none;
}
}
.lnsDimensionContainer__content {
flex: 1;
@include euiYScroll;
}
.lnsDimensionContainer__footer {
padding: $euiSize;
.lnsFrameLayout__sidebar-isFullscreen & {
display: none;
}
}
.lnsBody--overflowHidden {
overflow: hidden;
}

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import './flyout_container.scss';
import React, { useState, useEffect, useCallback } from 'react';
import { css } from '@emotion/react';
import {
@ -18,9 +16,13 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiFocusTrap,
type UseEuiTheme,
euiBreakpoint,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS } from '../utils';
import { flyoutContainerStyles } from './flyout.styles';
function fromExcludedClickTarget(event: Event) {
for (
@ -61,6 +63,7 @@ export function FlyoutContainer({
isInlineEditing?: boolean;
}) {
const [focusTrapIsEnabled, setFocusTrapIsEnabled] = useState(false);
const euiThemeContext = useEuiTheme();
const closeFlyout = useCallback(() => {
setFocusTrapIsEnabled(false);
@ -69,12 +72,14 @@ export function FlyoutContainer({
useEffect(() => {
if (!isInlineEditing) {
document.body.classList.toggle('lnsBody--overflowHidden', isOpen);
if (isOpen) {
document.body.style.overflow = isOpen ? 'hidden' : '';
}
return () => {
if (isOpen) {
setFocusTrapIsEnabled(false);
}
document.body.classList.remove('lnsBody--overflowHidden');
document.body.style.overflow = '';
};
}
}, [isInlineEditing, isOpen]);
@ -100,10 +105,13 @@ export function FlyoutContainer({
ref={panelContainerRef}
role="dialog"
aria-labelledby="lnsDimensionContainerTitle"
className="lnsDimensionContainer"
css={css`
box-shadow: ${isInlineEditing ? 'none !important' : 'inherit'};
`}
css={[
css`
box-shadow: ${isInlineEditing || isFullscreen ? 'none !important' : 'inherit'};
`,
flyoutContainerStyles(euiThemeContext),
dimensionContainerStyles.self(euiThemeContext),
]}
onAnimationEnd={() => {
if (isOpen) {
// EuiFocusTrap interferes with animating elements with absolute position:
@ -113,7 +121,7 @@ export function FlyoutContainer({
}
}}
>
<EuiFlyoutHeader hasBorder className="lnsDimensionContainer__header">
<EuiFlyoutHeader hasBorder css={dimensionContainerStyles.header(euiThemeContext)}>
<EuiFlexGroup gutterSize="m" alignItems="center" responsive={false}>
{isInlineEditing && (
<EuiFlexItem grow={false}>
@ -131,12 +139,7 @@ export function FlyoutContainer({
)}
<EuiFlexItem grow={true}>
<EuiTitle size="xs">
<h2
id="lnsDimensionContainerTitle"
className="lnsDimensionContainer__headerTitle"
>
{label}
</h2>
<h2 id="lnsDimensionContainerTitle">{label}</h2>
</EuiTitle>
</EuiFlexItem>
@ -157,10 +160,18 @@ export function FlyoutContainer({
</EuiFlexGroup>
</EuiFlyoutHeader>
<div className="lnsDimensionContainer__content">{children}</div>
<div
className="eui-yScroll"
css={css`
flex: 1;
z-index: 1;
`}
>
{children}
</div>
{customFooter || (
<EuiFlyoutFooter className="lnsDimensionContainer__footer">
<EuiFlyoutFooter css={dimensionContainerStyles.footer(euiThemeContext)}>
<EuiButtonEmpty
flush="left"
size="s"
@ -183,3 +194,24 @@ export function FlyoutContainer({
</div>
);
}
const dimensionContainerStyles = {
self: (euiThemeContext: UseEuiTheme) => {
return css`
// But with custom positioning to keep it within the sidebar contents
max-width: none !important;
left: 0;
${euiBreakpoint(euiThemeContext, ['m', 'l', 'xl'])} {
height: 100% !important;
position: absolute;
top: 0 !important;
}
`;
},
header: ({ euiTheme }: UseEuiTheme) => css`
padding: ${euiTheme.size.base};
`,
footer: ({ euiTheme }: UseEuiTheme) => css`
padding: ${euiTheme.size.base};
`,
};

View file

@ -1,28 +0,0 @@
@import '../mixins';
.lnsSettingWithSiblingFlyout {
// Use the EuiFlyout style
@include euiFlyout;
// But with custom positioning to keep it within the sidebar contents
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
animation: euiFlyoutAnimation $euiAnimSpeedNormal $euiAnimSlightResistance;
// making just a bit higher than the dimension flyout to stack on top of it
z-index: $euiZLevel3 + 1
}
.lnsSettingWithSiblingFlyout__header {
padding: $euiSize;
}
.lnsSettingWithSiblingFlyout__content {
flex: 1;
@include euiYScroll;
}
.lnsSettingWithSiblingFlyout__footer {
padding: $euiSize;
}

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import './setting_with_sibling_flyout.scss';
import { i18n } from '@kbn/i18n';
import React, { useState, useEffect, MutableRefObject } from 'react';
import {
@ -20,7 +18,11 @@ import {
EuiFocusTrap,
EuiOutsideClickDetector,
EuiPortal,
type UseEuiTheme,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { flyoutContainerStyles } from './flyout.styles';
const DEFAULT_TITLE = i18n.translate('xpack.lens.colorSiblingFlyoutTitle', {
defaultMessage: 'Color',
@ -43,6 +45,7 @@ export function SettingWithSiblingFlyout({
}) {
const [focusTrapIsEnabled, setFocusTrapIsEnabled] = useState(false);
const [isFlyoutOpen, setIsFlyoutOpen] = useState(false);
const euiThemeContext = useEuiTheme();
const toggleFlyout = () => {
setIsFlyoutOpen(!isFlyoutOpen);
@ -74,9 +77,15 @@ export function SettingWithSiblingFlyout({
role="dialog"
aria-labelledby="lnsSettingWithSiblingFlyoutTitle"
data-test-subj={dataTestSubj}
className="lnsSettingWithSiblingFlyout"
css={[
flyoutContainerStyles(euiThemeContext),
siblingflyoutContainerStyles.self(euiThemeContext),
]}
>
<EuiFlyoutHeader hasBorder className="lnsSettingWithSiblingFlyout__header">
<EuiFlyoutHeader
hasBorder
css={siblingflyoutContainerStyles.header(euiThemeContext)}
>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButtonIcon
@ -92,20 +101,24 @@ export function SettingWithSiblingFlyout({
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xs">
<h3
id="lnsSettingWithSiblingFlyoutTitle"
className="lnsSettingWithSiblingFlyout__headerTitle"
>
{title}
</h3>
<h3 id="lnsSettingWithSiblingFlyoutTitle">{title}</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
{children && <div className="lnsSettingWithSiblingFlyout__content">{children}</div>}
{children && (
<div
className="eui-yScroll"
css={css`
flex: 1;
`}
>
{children}
</div>
)}
<EuiFlyoutFooter className="lnsSettingWithSiblingFlyout__footer">
<EuiFlyoutFooter css={siblingflyoutContainerStyles.footer(euiThemeContext)}>
<EuiButtonEmpty flush="left" size="s" iconType="sortLeft" onClick={closeFlyout}>
{i18n.translate('xpack.lens.settingWithSiblingFlyout.back', {
defaultMessage: 'Back',
@ -120,3 +133,21 @@ export function SettingWithSiblingFlyout({
</EuiFlexGroup>
);
}
const siblingflyoutContainerStyles = {
self: ({ euiTheme }: UseEuiTheme) => css`
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
// making just a bit higher than the dimension flyout to stack on top of it
z-index: ${euiTheme.levels.menu};
`,
header: ({ euiTheme }: UseEuiTheme) => css`
padding: ${euiTheme.size.base};
`,
footer: ({ euiTheme }: UseEuiTheme) => css`
padding: ${euiTheme.size.base};
`,
};

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTitle, IconType } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTitle, IconType, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
export const StaticHeader = ({
@ -18,12 +18,15 @@ export const StaticHeader = ({
icon?: IconType;
indicator?: React.ReactNode;
}) => {
const { euiTheme } = useEuiTheme();
return (
<EuiFlexGroup
gutterSize="s"
alignItems="center"
responsive={false}
className={'lnsLayerPanel__settingsStaticHeader'}
css={css`
padding-left: ${euiTheme.size.xs};
`}
>
{icon && (
<EuiFlexItem grow={false}>

View file

@ -1,3 +0,0 @@
.lnsVisToolbar__popover {
width: 410px;
}

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import './toolbar_popover.scss';
import React, { PropsWithChildren, useState } from 'react';
import { EuiFlexItem, EuiPopover, EuiPopoverProps, EuiPopoverTitle, IconType } from '@elastic/eui';
import { ToolbarButton, ToolbarButtonProps } from '@kbn/shared-ux-button-toolbar';
@ -41,6 +40,8 @@ export type ToolbarPopoverProps = Partial<EuiPopoverProps> & {
handleClose?: () => void;
};
const defaultPanelStyles = { width: '410px' };
export const ToolbarPopover: React.FC<PropsWithChildren<ToolbarPopoverProps>> = ({
children,
title,
@ -49,7 +50,7 @@ export const ToolbarPopover: React.FC<PropsWithChildren<ToolbarPopoverProps>> =
groupPosition,
buttonDataTestSubj,
handleClose,
panelClassName = 'lnsVisToolbar__popover',
panelStyle = defaultPanelStyles,
...euiPopoverProps
}) => {
const [isOpen, setIsOpen] = useState(false);
@ -59,7 +60,7 @@ export const ToolbarPopover: React.FC<PropsWithChildren<ToolbarPopoverProps>> =
return (
<EuiFlexItem grow={false}>
<EuiPopover
panelClassName={panelClassName}
panelStyle={panelStyle}
ownFocus
aria-label={title}
button={

View file

@ -37,7 +37,7 @@ import { layerTypes } from '../../common/layer_types';
describe('lensSlice', () => {
let store: EnhancedStore<{ lens: LensAppState }>;
beforeEach(() => {
store = makeLensStore({}).store;
store = makeLensStore().store;
jest.clearAllMocks();
});
const customQuery = { query: 'custom' } as Query;

View file

@ -118,13 +118,13 @@ describe('Initializing the store', () => {
},
});
const { store, deps } = makeLensStore({
const { store } = makeLensStore({
storeDeps,
preloadedState,
});
await loadInitialAppState(store, defaultProps);
const { datasourceMap } = deps;
const { datasourceMap } = storeDeps;
expect(datasourceMap.testDatasource.initialize).toHaveBeenCalledWith(
datasource1State,

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiThemeProvider } from '@elastic/eui';
import { coreMock } from '@kbn/core/public/mocks';
import { I18nProvider } from '@kbn/i18n-react';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { RenderOptions, render } from '@testing-library/react';
import { ComponentType, MountRendererProps, mount } from 'enzyme';
import React from 'react';
import { PropsWithChildren, ReactElement } from 'react';
import { LensAppServices } from '../app_plugin/types';
export const renderWithProviders = (
ui: ReactElement,
renderOptions?: RenderOptions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): any => {
const { wrapper, ...options } = renderOptions || {};
const CustomWrapper = wrapper as React.ComponentType<React.PropsWithChildren<{}>>;
const Wrapper: React.FC<PropsWithChildren<{}>> = ({ children }) => {
return (
<KibanaContextProvider services={coreMock.createStart() as unknown as LensAppServices}>
<I18nProvider>
<EuiThemeProvider>
{wrapper ? <CustomWrapper>{children}</CustomWrapper> : children}
</EuiThemeProvider>
</I18nProvider>
</KibanaContextProvider>
);
};
const rtlRender = render(ui, { wrapper: Wrapper, ...options });
return rtlRender;
};
// legacy enzyme usage: remove when all tests are migrated to @testing-library/react
export const mountWithProviders = (component: React.ReactElement, options?: MountRendererProps) => {
const { wrappingComponent, wrappingComponentProps } = options || {};
const WrappingComponent = wrappingComponent as React.ComponentType<React.PropsWithChildren<{}>>;
const wrapper: React.FC<PropsWithChildren<{}>> = ({ children }) => (
<KibanaContextProvider services={coreMock.createStart() as unknown as LensAppServices}>
<I18nProvider>
<EuiThemeProvider>
{WrappingComponent ? (
<WrappingComponent {...wrappingComponentProps}>{children}</WrappingComponent>
) : (
children
)}
</EuiThemeProvider>
</I18nProvider>
</KibanaContextProvider>
);
const instance = mount(component, {
...options,
wrappingComponent: wrapper as ComponentType<PropsWithChildren<{}>>,
});
return instance;
};

View file

@ -1,24 +0,0 @@
// styles needed to display extra drop targets that are outside of the config panel main area while also allowing to scroll vertically
.lnsConfigPanel__overlay {
clip-path: polygon(-100% 0, 100% 0, 100% 100%, -100% 100%);
max-inline-size: $euiSizeXXL * 20;
min-inline-size: $euiSizeXXL * 8;
background: $euiColorBackgroundBaseSubdued;
@include euiBreakpoint('xs', 's', 'm') {
clip-path: none;
}
.kbnOverlayMountWrapper {
padding-left: $euiFormMaxWidth;
margin-left: -$euiFormMaxWidth;
pointer-events: none;
.euiFlyoutFooter {
pointer-events: auto;
}
}
}
.lnsEditFlyoutBody {
.euiFlyoutBody__overflow {
transform: initial;
}
}

View file

@ -9,7 +9,6 @@ import { isOfAggregateQueryType } from '@kbn/es-query';
import { ENABLE_ESQL } from '@kbn/esql-utils';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { BehaviorSubject } from 'rxjs';
import '../helpers.scss';
import { PublishingSubject } from '@kbn/presentation-publishing';
import { LensPluginStartDependencies } from '../../../plugin';
import { DatasourceMap, VisualizationMap } from '../../../types';

Some files were not shown because too many files have changed in this diff Show more