Skip to content

Commit 5cad708

Browse files
authored
Explore: Enable resize of split pane (grafana#58683)
* Move layout to paneleditor, make SplitPaneWrapper more generic * Read/write the size ratio in local storage * Add min height to enable scrollbar * Enable show/hide panel options * Add new component to explore * Add styles * Bring in code from other branch * Fix update size function, add min size to explore container * Add window size, save width as a ratio * Fix tests * Allow for one child * Remove children type definition * Use library methods for min/max size instead of hooks
1 parent 0c4aa6d commit 5cad708

File tree

9 files changed

+231
-44
lines changed

9 files changed

+231
-44
lines changed

packages/grafana-ui/src/components/PageLayout/PageToolbar.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface Props {
2424
className?: string;
2525
isFullscreen?: boolean;
2626
'aria-label'?: string;
27+
buttonOverflowAlignment?: 'left' | 'right';
2728
}
2829

2930
/** @alpha */
@@ -42,6 +43,7 @@ export const PageToolbar: FC<Props> = React.memo(
4243
className,
4344
/** main nav-container aria-label **/
4445
'aria-label': ariaLabel,
46+
buttonOverflowAlignment = 'right',
4547
}) => {
4648
const styles = useStyles2(getStyles);
4749

@@ -132,7 +134,9 @@ export const PageToolbar: FC<Props> = React.memo(
132134
)}
133135
</nav>
134136
</div>
135-
<ToolbarButtonRow alignment="right">{React.Children.toArray(children).filter(Boolean)}</ToolbarButtonRow>
137+
<ToolbarButtonRow alignment={buttonOverflowAlignment}>
138+
{React.Children.toArray(children).filter(Boolean)}
139+
</ToolbarButtonRow>
136140
</nav>
137141
);
138142
}

public/app/core/components/SplitPaneWrapper/SplitPaneWrapper.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ interface Props {
99
splitOrientation?: Split;
1010
paneSize: number;
1111
splitVisible?: boolean;
12+
minSize?: number;
1213
maxSize?: number;
1314
primary?: 'first' | 'second';
1415
onDragFinished?: (size?: number) => void;
16+
paneStyle?: React.CSSProperties;
1517
secondaryPaneStyle?: React.CSSProperties;
1618
}
1719

@@ -49,39 +51,66 @@ export class SplitPaneWrapper extends PureComponent<Props> {
4951
};
5052

5153
render() {
52-
const { paneSize, splitOrientation, maxSize, primary, secondaryPaneStyle } = this.props;
54+
const {
55+
children,
56+
paneSize,
57+
splitOrientation,
58+
maxSize,
59+
minSize,
60+
primary,
61+
paneStyle,
62+
secondaryPaneStyle,
63+
splitVisible = true,
64+
} = this.props;
65+
66+
let childrenArr = [];
67+
if (Array.isArray(children)) {
68+
childrenArr = children;
69+
} else {
70+
childrenArr.push(children);
71+
}
72+
5373
// Limit options pane width to 90% of screen.
54-
const styles = getStyles(config.theme2);
74+
const styles = getStyles(config.theme2, splitVisible);
5575

5676
// Need to handle when width is relative. ie a percentage of the viewport
5777
const paneSizePx =
5878
paneSize <= 1
5979
? paneSize * (splitOrientation === 'horizontal' ? window.innerHeight : window.innerWidth)
6080
: paneSize;
6181

82+
// the react split pane library always wants 2 children. This logic ensures that happens, even if one child is passed in
83+
const childrenFragments = [
84+
<React.Fragment key="leftPane">{childrenArr[0]}</React.Fragment>,
85+
<React.Fragment key="rightPane">{childrenArr[1] || undefined}</React.Fragment>,
86+
];
87+
6288
return (
6389
<SplitPane
6490
split={splitOrientation}
91+
minSize={minSize}
6592
maxSize={maxSize}
66-
size={paneSizePx}
67-
primary={primary}
93+
size={splitVisible ? paneSizePx : 0}
94+
primary={splitVisible ? primary : 'second'}
6895
resizerClassName={splitOrientation === 'horizontal' ? styles.resizerH : styles.resizerV}
6996
onDragStarted={() => this.onDragStarted()}
7097
onDragFinished={(size) => this.onDragFinished(size)}
98+
paneStyle={paneStyle}
7199
pane2Style={secondaryPaneStyle}
72100
>
73-
{this.props.children}
101+
{childrenFragments}
74102
</SplitPane>
75103
);
76104
}
77105
}
78106

79-
const getStyles = (theme: GrafanaTheme2) => {
107+
const getStyles = (theme: GrafanaTheme2, hasSplit: boolean) => {
80108
const handleColor = theme.v1.palette.blue95;
81109
const paneSpacing = theme.spacing(2);
82110

83111
const resizer = css`
84112
position: relative;
113+
display: ${hasSplit ? 'block' : 'none'};
85114
86115
&::before {
87116
content: '';

public/app/features/explore/ExplorePaneContainer.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { css, cx } from '@emotion/css';
1+
import { css } from '@emotion/css';
22
import memoizeOne from 'memoize-one';
33
import React from 'react';
44
import { connect, ConnectedProps } from 'react-redux';
@@ -36,20 +36,18 @@ const getStyles = (theme: GrafanaTheme2) => {
3636
display: flex;
3737
flex: 1 1 auto;
3838
flex-direction: column;
39+
overflow: scroll;
40+
min-width: 600px;
3941
& + & {
4042
border-left: 1px dotted ${theme.colors.border.medium};
4143
}
4244
`,
43-
exploreSplit: css`
44-
width: 50%;
45-
`,
4645
};
4746
};
4847

4948
interface OwnProps extends Themeable2 {
5049
exploreId: ExploreId;
5150
urlQuery: string;
52-
split: boolean;
5351
eventBus: EventBus;
5452
}
5553

@@ -144,11 +142,10 @@ class ExplorePaneContainerUnconnected extends React.PureComponent<Props> {
144142
};
145143

146144
render() {
147-
const { theme, split, exploreId, initialized, eventBus } = this.props;
145+
const { theme, exploreId, initialized, eventBus } = this.props;
148146
const styles = getStyles(theme);
149-
const exploreClass = cx(styles.explore, split && styles.exploreSplit);
150147
return (
151-
<div className={exploreClass} ref={this.getRef} data-testid={selectors.pages.Explore.General.container}>
148+
<div className={styles.explore} ref={this.getRef} data-testid={selectors.pages.Explore.General.container}>
152149
{initialized && <Explore exploreId={exploreId} eventBus={eventBus} />}
153150
</div>
154151
);

public/app/features/explore/ExploreToolbar.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { getFiscalYearStartMonth, getTimeZone } from '../profile/state/selectors
2626
import { ExploreTimeControls } from './ExploreTimeControls';
2727
import { LiveTailButton } from './LiveTailButton';
2828
import { changeDatasource } from './state/datasource';
29-
import { splitClose, splitOpen } from './state/main';
29+
import { splitClose, splitOpen, maximizePaneAction, evenPaneResizeAction } from './state/main';
3030
import { cancelQueries, runQueries } from './state/query';
3131
import { isSplit } from './state/selectors';
3232
import { syncTimes, changeRefreshInterval } from './state/time';
@@ -133,13 +133,24 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
133133
isPaused,
134134
hasLiveOption,
135135
containerWidth,
136+
largerExploreId,
136137
} = this.props;
137138
const showSmallTimePicker = splitted || containerWidth < 1210;
138139

140+
const isLargerExploreId = largerExploreId === exploreId;
141+
139142
const showExploreToDashboard =
140143
contextSrv.hasAccess(AccessControlAction.DashboardsCreate, contextSrv.isEditor) ||
141144
contextSrv.hasAccess(AccessControlAction.DashboardsWrite, contextSrv.isEditor);
142145

146+
const onClickResize = () => {
147+
if (isLargerExploreId) {
148+
this.props.evenPaneResizeAction();
149+
} else {
150+
this.props.maximizePaneAction({ exploreId: exploreId });
151+
}
152+
};
153+
143154
return [
144155
!splitted ? (
145156
<ToolbarButton
@@ -152,9 +163,21 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
152163
Split
153164
</ToolbarButton>
154165
) : (
155-
<ToolbarButton key="split" tooltip="Close split pane" onClick={this.onCloseSplitView} icon="times">
156-
Close
157-
</ToolbarButton>
166+
<React.Fragment key="splitActions">
167+
<ToolbarButton
168+
tooltip={`${isLargerExploreId ? 'Narrow' : 'Widen'} pane`}
169+
disabled={isLive}
170+
onClick={onClickResize}
171+
icon={
172+
(exploreId === 'left' && isLargerExploreId) || (exploreId === 'right' && !isLargerExploreId)
173+
? 'angle-left'
174+
: 'angle-right'
175+
}
176+
/>
177+
<ToolbarButton tooltip="Close split pane" onClick={this.onCloseSplitView} icon="times">
178+
Close
179+
</ToolbarButton>
180+
</React.Fragment>
158181
),
159182

160183
showExploreToDashboard && (
@@ -285,7 +308,7 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
285308
}
286309

287310
const mapStateToProps = (state: StoreState, { exploreId }: OwnProps) => {
288-
const { syncedTimes } = state.explore;
311+
const { syncedTimes, largerExploreId } = state.explore;
289312
const exploreItem = state.explore[exploreId]!;
290313
const { datasourceInstance, datasourceMissing, range, refreshInterval, loading, isLive, isPaused, containerWidth } =
291314
exploreItem;
@@ -307,6 +330,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps) => {
307330
isPaused,
308331
syncedTimes,
309332
containerWidth,
333+
largerExploreId,
310334
};
311335
};
312336

@@ -320,6 +344,8 @@ const mapDispatchToProps = {
320344
syncTimes,
321345
onChangeTimeZone: updateTimeZoneForSession,
322346
onChangeFiscalYearStartMonth: updateFiscalYearStartMonthForSession,
347+
maximizePaneAction,
348+
evenPaneResizeAction,
323349
};
324350

325351
const connector = connect(mapStateToProps, mapDispatchToProps);

public/app/features/explore/Wrapper.test.tsx

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { locationService, config } from '@grafana/runtime';
88
import { changeDatasource } from './spec/helper/interactions';
99
import { makeLogsQueryResponse, makeMetricsQueryResponse } from './spec/helper/query';
1010
import { setupExplore, tearDown, waitForExplore } from './spec/helper/setup';
11-
import { splitOpen } from './state/main';
11+
import * as mainState from './state/main';
1212
import * as queryState from './state/query';
1313

1414
jest.mock('app/core/core', () => {
@@ -154,7 +154,7 @@ describe('Wrapper', () => {
154154
});
155155
});
156156

157-
describe('Handles open/close splits in UI and URL', () => {
157+
describe('Handles open/close splits and related events in UI and URL', () => {
158158
it('opens the split pane when split button is clicked', async () => {
159159
setupExplore();
160160
// Wait for rendering the editor
@@ -226,8 +226,8 @@ describe('Wrapper', () => {
226226
await userEvent.click(closeButtons[1]);
227227

228228
await waitFor(() => {
229-
const logsPanels = screen.queryAllByLabelText(/Close split pane/i);
230-
expect(logsPanels.length).toBe(0);
229+
const postCloseButtons = screen.queryAllByLabelText(/Close split pane/i);
230+
expect(postCloseButtons.length).toBe(0);
231231
});
232232
});
233233

@@ -261,12 +261,35 @@ describe('Wrapper', () => {
261261
// to work
262262
await screen.findByText(`loki Editor input: { label="value"}`);
263263

264-
store.dispatch(splitOpen<any>({ datasourceUid: 'elastic', query: { expr: 'error' } }) as any);
264+
store.dispatch(mainState.splitOpen<any>({ datasourceUid: 'elastic', query: { expr: 'error' } }) as any);
265265

266266
// Editor renders the new query
267267
await screen.findByText(`elastic Editor input: error`);
268268
await screen.findByText(`loki Editor input: { label="value"}`);
269269
});
270+
271+
it('handles split size events and sets relevant variables', async () => {
272+
setupExplore();
273+
const splitButton = await screen.findByText(/split/i);
274+
fireEvent.click(splitButton);
275+
await waitForExplore(undefined, true);
276+
let widenButton = await screen.findAllByLabelText('Widen pane');
277+
let narrowButton = await screen.queryAllByLabelText('Narrow pane');
278+
const panes = screen.getAllByRole('main');
279+
expect(widenButton.length).toBe(2);
280+
expect(narrowButton.length).toBe(0);
281+
expect(Number.parseInt(getComputedStyle(panes[0]).width, 10)).toBe(1000);
282+
expect(Number.parseInt(getComputedStyle(panes[1]).width, 10)).toBe(1000);
283+
const resizer = screen.getByRole('presentation');
284+
fireEvent.mouseDown(resizer, { buttons: 1 });
285+
fireEvent.mouseMove(resizer, { clientX: -700, buttons: 1 });
286+
fireEvent.mouseUp(resizer);
287+
widenButton = await screen.findAllByLabelText('Widen pane');
288+
narrowButton = await screen.queryAllByLabelText('Narrow pane');
289+
expect(widenButton.length).toBe(1);
290+
expect(narrowButton.length).toBe(1);
291+
// the autosizer is mocked so there is no actual resize here
292+
});
270293
});
271294

272295
describe('Handles document title changes', () => {
@@ -295,7 +318,7 @@ describe('Wrapper', () => {
295318
// to work
296319
await screen.findByText(`loki Editor input: { label="value"}`);
297320

298-
store.dispatch(splitOpen<any>({ datasourceUid: 'elastic', query: { expr: 'error' } }) as any);
321+
store.dispatch(mainState.splitOpen<any>({ datasourceUid: 'elastic', query: { expr: 'error' } }) as any);
299322
await waitFor(() => expect(document.title).toEqual('Explore - loki | elastic - Grafana'));
300323
});
301324
});

0 commit comments

Comments
 (0)