Explore: Use PageToolbar component & some cleanup (#41318)

* Explore: Use PageToolbar component & some cleanup

* remove unneeded getStyles

* remove unneeded fragment

* restore cancelQueries action handler in reducer

* avoid nested ternary operator

* Clean up explore e2e
This commit is contained in:
Giordano Ricci 2021-11-12 10:09:25 +00:00 committed by GitHub
parent 3c3cf2eee9
commit 13db125558
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 121 additions and 468 deletions

View File

@ -146,9 +146,6 @@ export const Pages = {
table: 'Explore Table',
scrollBar: () => '.scrollbar-view',
},
Toolbar: {
navBar: () => '.explore-toolbar',
},
},
SoloPanel: {
url: (page: string) => `/d-solo/${page}`,

View File

@ -9,6 +9,5 @@ export const addPanel = (config?: Partial<PartialAddPanelConfig>) =>
panelTitle: `e2e-${uuidv4()}`,
...config,
isEdit: false,
isExplore: false,
})
);

View File

@ -2,7 +2,7 @@ import { e2e } from '../index';
import { getScenarioContext } from '../support/scenarioContext';
import { selectOption } from './selectOption';
import { setDashboardTimeRange } from './setDashboardTimeRange';
import { setTimeRange, TimeRangeConfig } from './setTimeRange';
import { TimeRangeConfig } from './setTimeRange';
interface AddPanelOverrides {
dataSourceName: string;
@ -33,12 +33,10 @@ interface ConfigurePanelOptional {
panelTitle?: string;
timeRange?: TimeRangeConfig;
visualizationName?: string;
matchExploreTable?: boolean;
}
interface ConfigurePanelRequired {
isEdit: boolean;
isExplore: boolean;
}
export type PartialConfigurePanelConfig = Partial<ConfigurePanelDefault> &
@ -74,8 +72,6 @@ export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelC
dashboardUid,
dataSourceName,
isEdit,
isExplore,
matchExploreTable,
matchScreenshot,
panelTitle,
queriesForm,
@ -85,32 +81,20 @@ export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelC
visualizationName,
} = fullConfig;
if (isEdit && isExplore) {
throw new TypeError('Invalid configuration');
if (visitDashboardAtStart) {
e2e.flows.openDashboard({ uid: dashboardUid });
}
if (isExplore) {
e2e.pages.Explore.visit();
if (isEdit) {
e2e.components.Panels.Panel.title(panelTitle).click();
e2e.components.Panels.Panel.headerItems('Edit').click();
} else {
if (visitDashboardAtStart) {
e2e.flows.openDashboard({ uid: dashboardUid });
}
if (isEdit) {
e2e.components.Panels.Panel.title(panelTitle).click();
e2e.components.Panels.Panel.headerItems('Edit').click();
} else {
e2e.components.PageToolbar.item('Add panel').click();
e2e.pages.AddDashboard.addNewPanel().click();
}
e2e.components.PageToolbar.item('Add panel').click();
e2e.pages.AddDashboard.addNewPanel().click();
}
if (timeRange) {
if (isExplore) {
e2e.pages.Explore.Toolbar.navBar().within(() => setTimeRange(timeRange));
} else {
setDashboardTimeRange(timeRange);
}
setDashboardTimeRange(timeRange);
}
// @todo alias '/**/*.js*' as '@pluginModule' when possible: https://github.com/cypress-io/cypress/issues/1296
@ -127,25 +111,23 @@ export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelC
// @todo instead wait for '@pluginModule' if not already loaded
e2e().wait(2000);
if (!isExplore) {
// `panelTitle` is needed to edit the panel, and unlikely to have its value changed at that point
const changeTitle = panelTitle && !isEdit;
// `panelTitle` is needed to edit the panel, and unlikely to have its value changed at that point
const changeTitle = panelTitle && !isEdit;
if (changeTitle || visualizationName) {
if (changeTitle && panelTitle) {
e2e.components.PanelEditor.OptionsPane.fieldLabel('Panel options Title').type(`{selectall}${panelTitle}`);
}
if (visualizationName) {
e2e.components.PluginVisualization.item(visualizationName).scrollIntoView().click();
// @todo wait for '@pluginModule' if not a core visualization and not already loaded
e2e().wait(2000);
}
} else {
// Consistently closed
closeOptions();
if (changeTitle || visualizationName) {
if (changeTitle && panelTitle) {
e2e.components.PanelEditor.OptionsPane.fieldLabel('Panel options Title').type(`{selectall}${panelTitle}`);
}
if (visualizationName) {
e2e.components.PluginVisualization.item(visualizationName).scrollIntoView().click();
// @todo wait for '@pluginModule' if not a core visualization and not already loaded
e2e().wait(2000);
}
} else {
// Consistently closed
closeOptions();
}
if (queriesForm) {
@ -164,11 +146,6 @@ export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelC
//e2e.components.QueryEditorRow.actionButton('Disable/enable query').click();
//e2e().wait('@chartData');
if (!isExplore) {
e2e().get('button[title="Apply changes and go back to dashboard"]').click();
e2e().url().should('include', `/d/${dashboardUid}`);
}
// Avoid annotations flakiness
e2e.components.RefreshPicker.runButton().should('be.visible').click();
@ -180,11 +157,7 @@ export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelC
if (matchScreenshot) {
let visualization;
if (isExplore) {
visualization = matchExploreTable ? e2e.pages.Explore.General.table() : e2e.pages.Explore.General.graph();
} else {
visualization = e2e.components.Panels.Panel.containerByTitle(panelTitle).find('.panel-content');
}
visualization = e2e.components.Panels.Panel.containerByTitle(panelTitle).find('.panel-content');
visualization.scrollIntoView().screenshot(screenshotName);
e2e().compareScreenshots(screenshotName);

View File

@ -4,5 +4,4 @@ export const editPanel = (config: Partial<PartialEditPanelConfig>) =>
configurePanel({
...config,
isEdit: true,
isExplore: false,
});

View File

@ -1,19 +0,0 @@
import { configurePanel, PartialConfigurePanelConfig } from './configurePanel';
import { getScenarioContext } from '../support/scenarioContext';
export const explore = (config: Partial<PartialConfigurePanelConfig>) =>
getScenarioContext().then(({ lastAddedDataSource }: any) =>
configurePanel({
dataSourceName: lastAddedDataSource,
screenshotName: 'explore-graph',
...config,
isEdit: false,
isExplore: true,
timeRange: {
from: '2020-01-01 00:00:00',
to: '2020-01-01 06:00:00',
zone: 'Coordinated Universal Time',
...config.timeRange,
},
})
);

View File

@ -5,7 +5,6 @@ export * from './assertSuccessNotification';
export * from './deleteDashboard';
export * from './deleteDataSource';
export * from './editPanel';
export * from './explore';
export * from './login';
export * from './openDashboard';
export * from './openPanelMenuItem';

View File

@ -2,7 +2,7 @@
import React, { PureComponent } from 'react';
// Components
import { ActionMeta, HorizontalGroup, PluginSignatureBadge, Select, stylesFactory } from '@grafana/ui';
import { ActionMeta, HorizontalGroup, PluginSignatureBadge, Select } from '@grafana/ui';
import {
DataSourceInstanceSettings,
DataSourceRef,
@ -12,7 +12,6 @@ import {
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { getDataSourceSrv } from '../services/dataSourceSrv';
import { css, cx } from '@emotion/css';
/**
* Component props description for the {@link DataSourcePicker}
@ -152,7 +151,6 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
const { error } = this.state;
const options = this.getDataSourceOptions();
const value = this.getCurrentValue();
const styles = getStyles();
const isClearable = typeof onClear === 'function';
return (
@ -161,7 +159,7 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
aria-label={selectors.components.DataSourcePicker.inputV2}
inputId="data-source-picker"
menuShouldPortal
className={styles.select}
className="ds-picker select-container"
isMulti={false}
isClearable={isClearable}
backspaceRemovesValue={false}
@ -191,13 +189,3 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
);
}
}
const getStyles = stylesFactory(() => ({
select: cx(
css({
minWidth: 200,
}),
'ds-picker',
'select-container'
),
}));

View File

@ -12,7 +12,7 @@ import { getFocusStyles } from '../../themes/mixins';
export interface Props {
pageIcon?: IconName;
title: string;
title?: string;
parent?: string;
onGoBack?: () => void;
titleHref?: string;
@ -80,7 +80,8 @@ export const PageToolbar: FC<Props> = React.memo(
)}
</>
)}
{titleHref && (
{title && titleHref && (
<h1 className={styles.h1Styles}>
<Link
aria-label="Search dashboard by name"
@ -91,7 +92,7 @@ export const PageToolbar: FC<Props> = React.memo(
</Link>
</h1>
)}
{!titleHref && <h1 className={styles.titleText}>{title}</h1>}
{title && !titleHref && <h1 className={styles.titleText}>{title}</h1>}
</nav>
{leftItems?.map((child, index) => (
<div className={styles.leftActionItem} key={index}>

View File

@ -1,10 +1,7 @@
import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import classNames from 'classnames';
import { css } from '@emotion/css';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { Icon, IconButton, SetInterval, ToolbarButton, ToolbarButtonRow, Tooltip } from '@grafana/ui';
import { PageToolbar, SetInterval, ToolbarButton, ToolbarButtonRow } from '@grafana/ui';
import { DataSourceInstanceSettings, RawTimeRange } from '@grafana/data';
import { DataSourcePicker } from '@grafana/runtime';
import { StoreState } from 'app/types/store';
@ -18,9 +15,10 @@ import { ExploreTimeControls } from './ExploreTimeControls';
import { LiveTailButton } from './LiveTailButton';
import { RunButton } from './RunButton';
import { LiveTailControls } from './useLiveTailControls';
import { cancelQueries, clearQueries, runQueries } from './state/query';
import { cancelQueries, runQueries } from './state/query';
import ReturnToDashboardButton from './ReturnToDashboardButton';
import { isSplit } from './state/selectors';
import { DashNavButton } from '../dashboard/components/DashNav/DashNavButton';
interface OwnProps {
exploreId: ExploreId;
@ -34,10 +32,6 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
this.props.changeDatasource(this.props.exploreId, dsSettings.uid, { importQueries: true });
};
onClearAll = () => {
this.props.clearAll(this.props.exploreId);
};
onRunQuery = (loading = false) => {
const { runQueries, cancelQueries, exploreId } = this.props;
if (loading) {
@ -83,131 +77,88 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
const showSmallTimePicker = splitted || containerWidth < 1210;
return (
<div className={splitted ? 'explore-toolbar splitted' : 'explore-toolbar'}>
<div className="explore-toolbar-item">
<div className="explore-toolbar-header">
<div className="explore-toolbar-header-title">
{exploreId === 'left' && (
<span className="navbar-page-btn">
<Icon
name="compass"
size="lg"
className={css`
margin-right: 6px;
margin-bottom: 3px;
`}
/>
Explore
</span>
)}
</div>
{splitted && (
<IconButton
title="Close split pane"
className="explore-toolbar-header-close"
onClick={() => closeSplit(exploreId)}
name="times"
/>
)}
</div>
</div>
<div className="explore-toolbar-item">
<div className="explore-toolbar-content">
{!datasourceMissing ? (
<div className="explore-toolbar-content-item">
<div
className={classNames(
'explore-ds-picker',
showSmallDataSourcePicker ? 'explore-ds-picker--small' : ''
)}
>
<DataSourcePicker
onChange={this.onChangeDatasource}
current={this.props.datasourceName}
hideTextValue={showSmallDataSourcePicker}
/>
</div>
</div>
) : null}
<ToolbarButtonRow>
<ReturnToDashboardButton exploreId={exploreId} />
<PageToolbar
title={exploreId === ExploreId.left ? 'Explore' : undefined}
pageIcon={exploreId === ExploreId.left ? 'compass' : undefined}
leftItems={[
exploreId === ExploreId.left && (
<DashNavButton
key="share"
tooltip="Copy shortened link"
icon="share-alt"
onClick={() => createAndCopyShortLink(window.location.href)}
aria-label="Copy shortened link"
/>
),
!datasourceMissing && (
<DataSourcePicker
key={`${exploreId}-ds-picker`}
onChange={this.onChangeDatasource}
current={this.props.datasourceName}
hideTextValue={showSmallDataSourcePicker}
width={showSmallDataSourcePicker ? 8 : undefined}
/>
),
].filter(Boolean)}
>
<ToolbarButtonRow>
<ReturnToDashboardButton exploreId={exploreId} />
{exploreId === 'left' && !splitted ? (
<ToolbarButton
iconOnly={splitted}
title="Split"
/* This way ToolbarButton doesn't add event as a parameter when invoking split function
* which breaks splitting functionality
*/
onClick={() => split()}
icon="columns"
disabled={isLive}
>
Split
</ToolbarButton>
) : null}
{!splitted ? (
<ToolbarButton title="Split" onClick={() => split()} icon="columns" disabled={isLive}>
Split
</ToolbarButton>
) : (
<ToolbarButton title="Close split pane" onClick={() => closeSplit(exploreId)} icon="times">
Close
</ToolbarButton>
)}
<Tooltip content={'Copy shortened link to the executed query'} placement="bottom">
<ToolbarButton
icon="share-alt"
onClick={() => createAndCopyShortLink(window.location.href)}
aria-label="Copy shortened link to the executed query"
/>
</Tooltip>
{!isLive && (
<ExploreTimeControls
exploreId={exploreId}
range={range}
timeZone={timeZone}
fiscalYearStartMonth={fiscalYearStartMonth}
onChangeTime={onChangeTime}
splitted={splitted}
syncedTimes={syncedTimes}
onChangeTimeSync={this.onChangeTimeSync}
hideText={showSmallTimePicker}
onChangeTimeZone={onChangeTimeZone}
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
/>
)}
{!isLive && (
<ExploreTimeControls
exploreId={exploreId}
range={range}
timeZone={timeZone}
fiscalYearStartMonth={fiscalYearStartMonth}
onChangeTime={onChangeTime}
<RunButton
refreshInterval={refreshInterval}
onChangeRefreshInterval={this.onChangeRefreshInterval}
isSmall={splitted || showSmallTimePicker}
isLive={isLive}
loading={loading || (isLive && !isPaused)}
onRun={this.onRunQuery}
showDropdown={!isLive}
/>
{refreshInterval && <SetInterval func={this.onRunQuery} interval={refreshInterval} loading={loading} />}
{hasLiveOption && (
<LiveTailControls exploreId={exploreId}>
{(controls) => (
<LiveTailButton
splitted={splitted}
syncedTimes={syncedTimes}
onChangeTimeSync={this.onChangeTimeSync}
hideText={showSmallTimePicker}
onChangeTimeZone={onChangeTimeZone}
onChangeFiscalYearStartMonth={onChangeFiscalYearStartMonth}
isLive={isLive}
isPaused={isPaused}
start={controls.start}
pause={controls.pause}
resume={controls.resume}
stop={controls.stop}
/>
)}
{!isLive && (
<ToolbarButton title="Clear all" onClick={this.onClearAll} icon="trash-alt" iconOnly={splitted}>
Clear all
</ToolbarButton>
)}
<RunButton
refreshInterval={refreshInterval}
onChangeRefreshInterval={this.onChangeRefreshInterval}
isSmall={splitted || showSmallTimePicker}
isLive={isLive}
loading={loading || (isLive && !isPaused)}
onRun={this.onRunQuery}
showDropdown={!isLive}
/>
{refreshInterval && <SetInterval func={this.onRunQuery} interval={refreshInterval} loading={loading} />}
{hasLiveOption && (
<LiveTailControls exploreId={exploreId}>
{(controls) => (
<LiveTailButton
splitted={splitted}
isLive={isLive}
isPaused={isPaused}
start={controls.start}
pause={controls.pause}
resume={controls.resume}
stop={controls.stop}
/>
)}
</LiveTailControls>
)}
</ToolbarButtonRow>
</div>
</div>
</div>
</LiveTailControls>
)}
</ToolbarButtonRow>
</PageToolbar>
);
}
}
@ -248,7 +199,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps) => {
const mapDispatchToProps = {
changeDatasource,
changeRefreshInterval,
clearAll: clearQueries,
cancelQueries,
runQueries,
closeSplit: splitClose,

View File

@ -37,7 +37,7 @@ import { localStorageFullAction, richHistoryLimitExceededAction, richHistoryUpda
import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit';
import { updateTime } from './time';
import { historyUpdatedAction } from './history';
import { createCacheKey, createEmptyQueryResponse, getResultsFromCache } from './utils';
import { createCacheKey, getResultsFromCache } from './utils';
import deepEqual from 'fast-deep-equal';
//
@ -64,18 +64,13 @@ export interface ChangeQueriesPayload {
}
export const changeQueriesAction = createAction<ChangeQueriesPayload>('explore/changeQueries');
/**
* Clear all queries and results.
*/
export interface ClearQueriesPayload {
exploreId: ExploreId;
}
export const clearQueriesAction = createAction<ClearQueriesPayload>('explore/clearQueries');
/**
* Cancel running queries.
*/
export const cancelQueriesAction = createAction<ClearQueriesPayload>('explore/cancelQueries');
export interface CancelQueriesPayload {
exploreId: ExploreId;
}
export const cancelQueriesAction = createAction<CancelQueriesPayload>('explore/cancelQueries');
export interface QueriesImportedPayload {
exploreId: ExploreId;
@ -222,17 +217,6 @@ export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<vo
};
}
/**
* Clear all queries and results.
*/
export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
return (dispatch) => {
dispatch(scanStopAction({ exploreId }));
dispatch(clearQueriesAction({ exploreId }));
dispatch(stateSave());
};
}
/**
* Cancel running queries
*/
@ -627,21 +611,6 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
};
}
if (clearQueriesAction.match(action)) {
const queries = ensureQueries();
stopQueryState(state.querySubscription);
return {
...state,
queries: queries.slice(),
graphResult: null,
tableResult: null,
logsResult: null,
queryKeys: getQueryKeys(queries, state.datasourceInstance),
queryResponse: createEmptyQueryResponse(),
loading: false,
};
}
if (cancelQueriesAction.match(action)) {
stopQueryState(state.querySubscription);

View File

@ -1,152 +1,16 @@
.icon-margin-right {
margin-right: 0.25em;
@media only screen and (max-width: 1320px) {
margin-right: 0;
}
}
.icon-brand-gradient {
color: linear-gradient(180deg, #f05a28 30%, #fbca0a 100%);
}
.icon-margin-left {
margin-left: 0.25em;
@media only screen and (max-width: 1320px) {
margin-left: 0;
}
}
.explore-active-button {
box-shadow: $btn-active-box-shadow;
border: 1px solid $orange-dark !important;
color: $orange-dark !important;
}
.explore-ds-picker {
min-width: 200px;
max-width: 300px;
}
.explore-ds-picker--small {
min-width: 60px;
max-width: 60px;
.ds-picker {
min-width: 60px;
max-width: 60px;
}
}
.explore-toolbar {
background: inherit;
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
height: auto;
padding: 0 $dashboard-padding;
border-bottom: 1px solid #0000;
transition-duration: 0.35s;
transition-timing-function: ease-in-out;
transition-property: box-shadow, border-bottom;
}
.explore-toolbar-item {
position: relative;
align-self: center;
&:first-child {
padding-left: 34px;
@include media-breakpoint-up(md) {
padding-left: 0;
}
}
}
.explore-toolbar.splitted {
.explore-toolbar-item {
flex: 1 1 100%;
}
.explore-toolbar-content-item:first-child {
padding-left: 0;
margin-right: auto;
}
}
.explore-toolbar-item:last-child {
flex: auto;
}
.explore-toolbar-header {
display: flex;
flex: 1 1 0;
flex-flow: row nowrap;
font-size: 18px;
min-height: $navbarHeight;
line-height: $navbarHeight;
justify-content: space-between;
align-items: center;
}
.explore-toolbar-header-close {
margin-left: auto;
color: $text-color-weak;
}
.explore-toolbar-content {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: flex-end;
}
.explore-toolbar-content-item {
display: flex;
padding: 0px 2px;
position: relative;
&:first-child {
padding-left: $dashboard-padding;
margin-right: auto;
}
}
@media only screen and (max-width: 1070px) {
.explore-toolbar-content {
justify-content: flex-start;
}
.explore-toolbar.splitted {
.explore-toolbar-content-item {
padding: 2px 0;
}
}
}
@media only screen and (max-width: 810px) {
.explore-toolbar {
.explore-toolbar-content-item {
&:first-child {
padding-left: 2px;
margin: 0 12px 0 16px;
}
}
}
}
// TODO: check if this is used
.explore {
display: flex;
flex: 1 1 auto;
flex-direction: column;
}
.explore.explore-live {
flex-direction: column-reverse;
}
.explore + .explore {
border-left: 1px dotted $table-border;
}
@ -156,10 +20,7 @@
flex: 1 1 auto;
flex-direction: column;
padding: $dashboard-padding;
}
.explore-container.explore-live {
flex-direction: column-reverse;
padding-top: 0;
}
.explore-wrapper {
@ -171,10 +32,12 @@
}
}
// TODO: this is used in Loki & Prometheus, move it
.explore-input-margin {
margin-right: 4px;
}
// TODO: move to public/app/features/explore/Time.tsx
.navbar .elapsed-time {
position: absolute;
left: 0;
@ -188,78 +51,12 @@
flex-wrap: wrap;
}
.query-row {
display: flex;
position: relative;
align-items: flex-start;
@include media-breakpoint-down(sm) {
flex-wrap: wrap;
}
& + & {
margin-top: $space-sm;
}
}
.query-row-tools {
white-space: nowrap;
}
.query-row-status {
position: relative;
top: 0;
right: 35px;
z-index: 1015;
display: flex;
flex-direction: column;
justify-content: center;
height: $input-height;
width: 0;
}
.query-row-field {
margin-right: 3px;
flex-grow: 1;
}
// TODO: move to Loki and Prometheus
.query-row-break {
flex-basis: 100%;
}
.query-transactions {
display: table;
}
.query-transaction {
display: table-row;
color: $text-color-weak;
line-height: 1.44;
}
.query-transaction--loading {
animation: query-loading-color-change 1s alternate 100;
}
@keyframes query-loading-color-change {
from {
color: $text-color-faint;
}
to {
color: $blue;
}
}
.query-transaction__type,
.query-transaction__duration {
display: table-cell;
font-size: $font-size-xs;
text-align: right;
padding-right: 0.25em;
}
// Prometheus-specifics, to be extracted to datasource soon
// TODO: Prometheus-specifics, to be extracted to datasource soon
.explore {
.prom-query-field-info {
margin: 0.25em 0.5em 0.5em;