mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 19:13:14 -04:00
PR cleans up presentation interface names for consistentency * adds `$` suffix to all observables. For example, `dataLoading` => `dataLoading$` * removes `Panel` naming convention from interface names since an api may not be a panel, an api may be a dashboard. For example, `PublisesPanelTitle` => `PublishesTitle` #### Note to Reviewers Pay special attention to any place where your application creates an untyped API. In the example below, there is no typescript violation when the parent returns `dataLoading` instead of `dataLoading$` since the parent is not typed as `PublishesDataLoading`. Please check for instances like these. ``` <ReactEmbeddableRenderer getParentApi={() => { dataLoading: new BehaviorSubject() }} /> ``` --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
364 lines
14 KiB
TypeScript
364 lines
14 KiB
TypeScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the "Elastic License
|
|
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
|
* Public License v 1"; you may not use this file except in compliance with, at
|
|
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
|
*/
|
|
|
|
import deepEqual from 'fast-deep-equal';
|
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import ReactDOM from 'react-dom';
|
|
import { combineLatest, debounceTime } from 'rxjs';
|
|
|
|
import {
|
|
EuiBadge,
|
|
EuiButton,
|
|
EuiButtonEmpty,
|
|
EuiButtonGroup,
|
|
EuiCallOut,
|
|
EuiFlexGroup,
|
|
EuiFlexItem,
|
|
EuiFormRow,
|
|
EuiPageTemplate,
|
|
EuiPopover,
|
|
EuiRange,
|
|
EuiSpacer,
|
|
transparentize,
|
|
useEuiTheme,
|
|
} from '@elastic/eui';
|
|
import { css } from '@emotion/react';
|
|
import { AppMountParameters } from '@kbn/core-application-browser';
|
|
import { CoreStart } from '@kbn/core-lifecycle-browser';
|
|
import { AddEmbeddableButton } from '@kbn/embeddable-examples-plugin/public';
|
|
import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public';
|
|
import { GridLayout, GridLayoutData } from '@kbn/grid-layout';
|
|
import { i18n } from '@kbn/i18n';
|
|
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
|
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
|
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
|
|
|
import {
|
|
clearSerializedDashboardState,
|
|
getSerializedDashboardState,
|
|
setSerializedGridLayout,
|
|
} from './serialized_grid_layout';
|
|
import { MockSerializedDashboardState } from './types';
|
|
import { useMockDashboardApi } from './use_mock_dashboard_api';
|
|
import { dashboardInputToGridLayout, gridLayoutToDashboardPanelMap } from './utils';
|
|
|
|
const DASHBOARD_MARGIN_SIZE = 8;
|
|
const DASHBOARD_GRID_HEIGHT = 20;
|
|
const DASHBOARD_GRID_COLUMN_COUNT = 48;
|
|
|
|
export const GridExample = ({
|
|
coreStart,
|
|
uiActions,
|
|
}: {
|
|
coreStart: CoreStart;
|
|
uiActions: UiActionsStart;
|
|
}) => {
|
|
const { euiTheme } = useEuiTheme();
|
|
|
|
const savedState = useRef<MockSerializedDashboardState>(getSerializedDashboardState());
|
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(false);
|
|
const [currentLayout, setCurrentLayout] = useState<GridLayoutData>(
|
|
dashboardInputToGridLayout(savedState.current)
|
|
);
|
|
const [isSettingsPopoverOpen, setIsSettingsPopoverOpen] = useState(false);
|
|
const [gutterSize, setGutterSize] = useState<number>(DASHBOARD_MARGIN_SIZE);
|
|
const [rowHeight, setRowHeight] = useState<number>(DASHBOARD_GRID_HEIGHT);
|
|
|
|
const mockDashboardApi = useMockDashboardApi({ savedState: savedState.current });
|
|
const [viewMode, expandedPanelId] = useBatchedPublishingSubjects(
|
|
mockDashboardApi.viewMode$,
|
|
mockDashboardApi.expandedPanelId$
|
|
);
|
|
|
|
useEffect(() => {
|
|
combineLatest([mockDashboardApi.panels$, mockDashboardApi.rows$])
|
|
.pipe(debounceTime(0)) // debounce to avoid subscribe being called twice when both panels$ and rows$ publish
|
|
.subscribe(([panels, rows]) => {
|
|
const panelIds = Object.keys(panels);
|
|
let panelsAreEqual = true;
|
|
for (const panelId of panelIds) {
|
|
if (!panelsAreEqual) break;
|
|
const currentPanel = panels[panelId];
|
|
const savedPanel = savedState.current.panels[panelId];
|
|
panelsAreEqual = deepEqual(
|
|
{ row: 0, ...currentPanel.gridData },
|
|
{ row: 0, ...savedPanel.gridData }
|
|
);
|
|
}
|
|
|
|
const hasChanges = !(panelsAreEqual && deepEqual(rows, savedState.current.rows));
|
|
setHasUnsavedChanges(hasChanges);
|
|
setCurrentLayout(dashboardInputToGridLayout({ panels, rows }));
|
|
});
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
const renderPanelContents = useCallback(
|
|
(id: string, setDragHandles?: (refs: Array<HTMLElement | null>) => void) => {
|
|
const currentPanels = mockDashboardApi.panels$.getValue();
|
|
|
|
return (
|
|
<ReactEmbeddableRenderer
|
|
key={id}
|
|
maybeId={id}
|
|
type={currentPanels[id].type}
|
|
getParentApi={() => mockDashboardApi}
|
|
panelProps={{
|
|
showBadges: true,
|
|
showBorder: true,
|
|
showNotifications: true,
|
|
showShadow: false,
|
|
setDragHandles,
|
|
}}
|
|
/>
|
|
);
|
|
},
|
|
[mockDashboardApi]
|
|
);
|
|
|
|
const customLayoutCss = useMemo(() => {
|
|
const gridColor = transparentize(euiTheme.colors.backgroundFilledAccentSecondary, 0.2);
|
|
return css`
|
|
.kbnGridRow--targeted {
|
|
background-position: top calc((var(--kbnGridGutterSize) / 2) * -1px) left
|
|
calc((var(--kbnGridGutterSize) / 2) * -1px);
|
|
background-size: calc((var(--kbnGridColumnWidth) + var(--kbnGridGutterSize)) * 1px)
|
|
calc((var(--kbnGridRowHeight) + var(--kbnGridGutterSize)) * 1px);
|
|
background-image: linear-gradient(to right, ${gridColor} 1px, transparent 1px),
|
|
linear-gradient(to bottom, ${gridColor} 1px, transparent 1px);
|
|
background-color: ${transparentize(euiTheme.colors.backgroundFilledAccentSecondary, 0.1)};
|
|
}
|
|
|
|
.kbnGridPanel--dragPreview {
|
|
border-radius: ${euiTheme.border.radius};
|
|
background-color: ${transparentize(euiTheme.colors.backgroundFilledAccentSecondary, 0.2)};
|
|
transition: opacity 100ms linear;
|
|
}
|
|
|
|
.kbnGridPanel--resizeHandle {
|
|
opacity: 0;
|
|
transition: opacity 0.2s, border 0.2s;
|
|
border-radius: 7px 0 7px 0;
|
|
border-bottom: 2px solid ${euiTheme.colors.accentSecondary};
|
|
border-right: 2px solid ${euiTheme.colors.accentSecondary};
|
|
&:hover,
|
|
&:focus {
|
|
outline-style: none !important;
|
|
opacity: 1;
|
|
background-color: ${transparentize(euiTheme.colors.accentSecondary, 0.05)};
|
|
}
|
|
}
|
|
`;
|
|
}, [euiTheme]);
|
|
|
|
return (
|
|
<KibanaRenderContextProvider {...coreStart}>
|
|
<EuiPageTemplate grow={false} offset={0} restrictWidth={false}>
|
|
<EuiPageTemplate.Header
|
|
iconType={'dashboardApp'}
|
|
pageTitle={i18n.translate('examples.gridExample.pageTitle', {
|
|
defaultMessage: 'Grid Layout Example',
|
|
})}
|
|
/>
|
|
<EuiPageTemplate.Section
|
|
color="subdued"
|
|
contentProps={{
|
|
css: { flexGrow: 1, display: 'flex', flexDirection: 'column' },
|
|
}}
|
|
>
|
|
<EuiCallOut
|
|
title={i18n.translate('examples.gridExample.sessionStorageCallout', {
|
|
defaultMessage:
|
|
'This example uses session storage to persist saved state and unsaved changes',
|
|
})}
|
|
>
|
|
<EuiButton
|
|
color="accent"
|
|
size="s"
|
|
onClick={() => {
|
|
clearSerializedDashboardState();
|
|
window.location.reload();
|
|
}}
|
|
>
|
|
{i18n.translate('examples.gridExample.resetExampleButton', {
|
|
defaultMessage: 'Reset example',
|
|
})}
|
|
</EuiButton>
|
|
</EuiCallOut>
|
|
<EuiSpacer size="m" />
|
|
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
|
<EuiFlexItem grow={false}>
|
|
<EuiFlexGroup gutterSize="s" alignItems="center">
|
|
<EuiFlexItem grow={false}>
|
|
<AddEmbeddableButton pageApi={mockDashboardApi} uiActions={uiActions} />{' '}
|
|
</EuiFlexItem>
|
|
<EuiFlexItem grow={false}>
|
|
<EuiPopover
|
|
button={
|
|
<EuiButton
|
|
iconType="arrowDown"
|
|
iconSide="right"
|
|
onClick={() => setIsSettingsPopoverOpen(!isSettingsPopoverOpen)}
|
|
>
|
|
{i18n.translate('examples.gridExample.settingsPopover.title', {
|
|
defaultMessage: 'Layout settings',
|
|
})}
|
|
</EuiButton>
|
|
}
|
|
isOpen={isSettingsPopoverOpen}
|
|
closePopover={() => setIsSettingsPopoverOpen(false)}
|
|
>
|
|
<>
|
|
<EuiFormRow
|
|
label={i18n.translate('examples.gridExample.settingsPopover.viewMode', {
|
|
defaultMessage: 'View mode',
|
|
})}
|
|
>
|
|
<EuiButtonGroup
|
|
legend={i18n.translate('examples.gridExample.layoutOptionsLegend', {
|
|
defaultMessage: 'Layout options',
|
|
})}
|
|
options={[
|
|
{
|
|
id: 'view',
|
|
label: i18n.translate('examples.gridExample.viewOption', {
|
|
defaultMessage: 'View',
|
|
}),
|
|
toolTipContent:
|
|
'The layout adjusts when the window is resized. Panel interactivity, such as moving and resizing within the grid, is disabled.',
|
|
},
|
|
{
|
|
id: 'edit',
|
|
label: i18n.translate('examples.gridExample.editOption', {
|
|
defaultMessage: 'Edit',
|
|
}),
|
|
toolTipContent:
|
|
'The layout does not adjust when the window is resized.',
|
|
},
|
|
]}
|
|
idSelected={viewMode}
|
|
onChange={(id) => {
|
|
mockDashboardApi.viewMode$.next(id);
|
|
}}
|
|
/>
|
|
</EuiFormRow>
|
|
<EuiFormRow
|
|
label={i18n.translate('examples.gridExample.settingsPopover.gutterSize', {
|
|
defaultMessage: 'Gutter size',
|
|
})}
|
|
>
|
|
<EuiRange
|
|
min={1}
|
|
max={30}
|
|
value={gutterSize}
|
|
onChange={(e) => setGutterSize(parseInt(e.currentTarget.value, 10))}
|
|
showLabels
|
|
showValue
|
|
/>
|
|
</EuiFormRow>
|
|
<EuiFormRow
|
|
label={i18n.translate('examples.gridExample.settingsPopover.rowHeight', {
|
|
defaultMessage: 'Row height',
|
|
})}
|
|
>
|
|
<EuiRange
|
|
min={5}
|
|
max={30}
|
|
step={5}
|
|
value={rowHeight}
|
|
onChange={(e) => setRowHeight(parseInt(e.currentTarget.value, 10))}
|
|
showLabels
|
|
showValue
|
|
/>
|
|
</EuiFormRow>
|
|
</>
|
|
</EuiPopover>
|
|
</EuiFlexItem>
|
|
</EuiFlexGroup>
|
|
</EuiFlexItem>
|
|
<EuiFlexItem grow={false}>
|
|
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
|
{hasUnsavedChanges && (
|
|
<EuiFlexItem grow={false}>
|
|
<EuiBadge color="warning">
|
|
{i18n.translate('examples.gridExample.unsavedChangesBadge', {
|
|
defaultMessage: 'Unsaved changes',
|
|
})}
|
|
</EuiBadge>
|
|
</EuiFlexItem>
|
|
)}
|
|
<EuiFlexItem grow={false}>
|
|
<EuiButtonEmpty
|
|
onClick={() => {
|
|
const { panels, rows } = savedState.current;
|
|
mockDashboardApi.panels$.next(panels);
|
|
mockDashboardApi.rows$.next(rows);
|
|
}}
|
|
>
|
|
{i18n.translate('examples.gridExample.resetLayoutButton', {
|
|
defaultMessage: 'Reset',
|
|
})}
|
|
</EuiButtonEmpty>
|
|
</EuiFlexItem>
|
|
<EuiFlexItem grow={false}>
|
|
<EuiButton
|
|
onClick={() => {
|
|
const newSavedState = {
|
|
panels: mockDashboardApi.panels$.getValue(),
|
|
rows: mockDashboardApi.rows$.getValue(),
|
|
};
|
|
savedState.current = newSavedState;
|
|
setHasUnsavedChanges(false);
|
|
setSerializedGridLayout(newSavedState);
|
|
}}
|
|
>
|
|
{i18n.translate('examples.gridExample.saveLayoutButton', {
|
|
defaultMessage: 'Save',
|
|
})}
|
|
</EuiButton>
|
|
</EuiFlexItem>
|
|
</EuiFlexGroup>
|
|
</EuiFlexItem>
|
|
</EuiFlexGroup>
|
|
<EuiSpacer size="m" />
|
|
|
|
<GridLayout
|
|
accessMode={viewMode === 'view' ? 'VIEW' : 'EDIT'}
|
|
expandedPanelId={expandedPanelId}
|
|
layout={currentLayout}
|
|
gridSettings={{
|
|
gutterSize,
|
|
rowHeight,
|
|
columnCount: DASHBOARD_GRID_COLUMN_COUNT,
|
|
}}
|
|
renderPanelContents={renderPanelContents}
|
|
onLayoutChange={(newLayout) => {
|
|
const { panels, rows } = gridLayoutToDashboardPanelMap(
|
|
mockDashboardApi.panels$.getValue(),
|
|
newLayout
|
|
);
|
|
mockDashboardApi.panels$.next(panels);
|
|
mockDashboardApi.rows$.next(rows);
|
|
}}
|
|
css={customLayoutCss}
|
|
/>
|
|
</EuiPageTemplate.Section>
|
|
</EuiPageTemplate>
|
|
</KibanaRenderContextProvider>
|
|
);
|
|
};
|
|
|
|
export const renderGridExampleApp = (
|
|
element: AppMountParameters['element'],
|
|
deps: { uiActions: UiActionsStart; coreStart: CoreStart }
|
|
) => {
|
|
ReactDOM.render(<GridExample {...deps} />, element);
|
|
|
|
return () => ReactDOM.unmountComponentAtNode(element);
|
|
};
|