Explore: Allow switching between metrics and logs (#16959)

Adds basic support for switching between Metrics and Logs in Explore. 
Currently only test datasource that supports both Metrics and Logs.
Summary of changes:
* Moves mode (Metric, Logs) selection to the left of datasource 
picker and add some quick styling.
* Only trigger change in ToggleButton if not selected
* Set correct mode if datasource only supports logs

Closes #16808
This commit is contained in:
Marcus Efraimsson 2019-05-16 09:52:22 +02:00 committed by GitHub
parent be66ed9dab
commit e6001f57a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 187 additions and 26 deletions

View File

@ -39,7 +39,7 @@ export const ToggleButton: FC<ToggleButtonProps> = ({
}) => {
const onClick = (event: React.SyntheticEvent) => {
event.stopPropagation();
if (onChange) {
if (!selected && onChange) {
onChange(value);
}
};

View File

@ -39,6 +39,7 @@ import {
ExploreId,
ExploreUpdateState,
ExploreUIState,
ExploreMode,
} from 'app/types/explore';
import { StoreState } from 'app/types';
import {
@ -79,15 +80,13 @@ interface ExploreProps {
setQueries: typeof setQueries;
split: boolean;
showingStartPage?: boolean;
supportsGraph: boolean | null;
supportsLogs: boolean | null;
supportsTable: boolean | null;
queryKeys: string[];
initialDatasource: string;
initialQueries: DataQuery[];
initialRange: RawTimeRange;
initialUI: ExploreUIState;
queryErrors: DataQueryError[];
mode: ExploreMode;
}
/**
@ -234,11 +233,9 @@ export class Explore extends React.PureComponent<ExploreProps> {
exploreId,
showingStartPage,
split,
supportsGraph,
supportsLogs,
supportsTable,
queryKeys,
queryErrors,
mode,
} = this.props;
const exploreClass = split ? 'explore explore-split' : 'explore';
@ -273,9 +270,11 @@ export class Explore extends React.PureComponent<ExploreProps> {
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
{!showingStartPage && (
<>
{supportsGraph && !supportsLogs && <GraphContainer width={width} exploreId={exploreId} />}
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
{supportsLogs && (
{mode === ExploreMode.Metrics && <GraphContainer width={width} exploreId={exploreId} />}
{mode === ExploreMode.Metrics && (
<TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />
)}
{mode === ExploreMode.Logs && (
<LogsContainer
width={width}
exploreId={exploreId}
@ -311,13 +310,11 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
datasourceMissing,
initialized,
showingStartPage,
supportsGraph,
supportsLogs,
supportsTable,
queryKeys,
urlState,
update,
queryErrors,
mode,
} = item;
const { datasource, queries, range: urlRange, ui } = (urlState || {}) as ExploreUrlState;
@ -335,9 +332,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
initialized,
showingStartPage,
split,
supportsGraph,
supportsLogs,
supportsTable,
queryKeys,
update,
initialDatasource,
@ -345,6 +339,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
initialRange,
initialUI,
queryErrors,
mode,
};
}

View File

@ -2,8 +2,15 @@ import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
import { ExploreId } from 'app/types/explore';
import { DataSourceSelectItem, RawTimeRange, ClickOutsideWrapper, TimeZone, TimeRange } from '@grafana/ui';
import { ExploreId, ExploreMode } from 'app/types/explore';
import {
DataSourceSelectItem,
RawTimeRange,
ClickOutsideWrapper,
TimeZone,
TimeRange,
SelectOptionItem,
} from '@grafana/ui';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { StoreState } from 'app/types/store';
import {
@ -13,10 +20,12 @@ import {
runQueries,
splitOpen,
changeRefreshInterval,
changeMode,
} from './state/actions';
import TimePicker from './TimePicker';
import { getTimeZone } from '../profile/state/selectors';
import { RefreshPicker, SetInterval } from '@grafana/ui';
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
enum IconSide {
left = 'left',
@ -61,6 +70,8 @@ interface StateProps {
selectedDatasource: DataSourceSelectItem;
splitted: boolean;
refreshInterval: string;
supportedModeOptions: Array<SelectOptionItem<ExploreMode>>;
selectedModeOption: SelectOptionItem<ExploreMode>;
}
interface DispatchProps {
@ -70,6 +81,7 @@ interface DispatchProps {
closeSplit: typeof splitClose;
split: typeof splitOpen;
changeRefreshInterval: typeof changeRefreshInterval;
changeMode: typeof changeMode;
}
type Props = StateProps & DispatchProps & OwnProps;
@ -100,6 +112,11 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
changeRefreshInterval(exploreId, item);
};
onModeChange = (mode: ExploreMode) => {
const { changeMode, exploreId } = this.props;
changeMode(exploreId, mode);
};
render() {
const {
datasourceMissing,
@ -115,6 +132,8 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
refreshInterval,
onChangeTime,
split,
supportedModeOptions,
selectedModeOption,
} = this.props;
return (
@ -147,8 +166,31 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
current={selectedDatasource}
/>
</div>
{supportedModeOptions.length > 1 ? (
<div className="query-type-toggle">
<ToggleButtonGroup label="" transparent={true}>
<ToggleButton
key={ExploreMode.Metrics}
value={ExploreMode.Metrics}
onChange={this.onModeChange}
selected={selectedModeOption.value === ExploreMode.Metrics}
>
{'Metrics'}
</ToggleButton>
<ToggleButton
key={ExploreMode.Logs}
value={ExploreMode.Logs}
onChange={this.onModeChange}
selected={selectedModeOption.value === ExploreMode.Logs}
>
{'Logs'}
</ToggleButton>
</ToggleButtonGroup>
</div>
) : null}
</div>
) : null}
{exploreId === 'left' && !splitted ? (
<div className="explore-toolbar-content-item">
{createResponsiveButton({
@ -208,12 +250,41 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
graphIsLoading,
logIsLoading,
tableIsLoading,
supportedModes,
mode,
} = exploreItem;
const selectedDatasource = datasourceInstance
? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
: undefined;
const loading = graphIsLoading || logIsLoading || tableIsLoading;
const supportedModeOptions: Array<SelectOptionItem<ExploreMode>> = [];
let selectedModeOption = null;
for (const supportedMode of supportedModes) {
switch (supportedMode) {
case ExploreMode.Metrics:
const option1 = {
value: ExploreMode.Metrics,
label: ExploreMode.Metrics,
};
supportedModeOptions.push(option1);
if (mode === ExploreMode.Metrics) {
selectedModeOption = option1;
}
break;
case ExploreMode.Logs:
const option2 = {
value: ExploreMode.Logs,
label: ExploreMode.Logs,
};
supportedModeOptions.push(option2);
if (mode === ExploreMode.Logs) {
selectedModeOption = option2;
}
break;
}
}
return {
datasourceMissing,
exploreDatasources,
@ -223,6 +294,8 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
selectedDatasource,
splitted,
refreshInterval,
supportedModeOptions,
selectedModeOption,
};
};
@ -233,6 +306,7 @@ const mapDispatchToProps: DispatchProps = {
runQueries,
closeSplit: splitClose,
split: splitOpen,
changeMode: changeMode,
};
export const ExploreToolbar = hot(module)(

View File

@ -35,7 +35,7 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
const loader = getAngularLoader();
const template = '<plugin-component type="query-ctrl"> </plugin-component>';
const target = { datasource: datasource.name, ...initialQuery };
const target = { ...initialQuery };
const scopeProps = {
ctrl: {
datasource,
@ -60,6 +60,7 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
this.component = loader.load(this.element, scopeProps, template);
setTimeout(() => {
this.props.onQueryChange(target);
this.props.onExecuteQuery();
}, 1);
}

View File

@ -38,6 +38,7 @@ interface QueryRowProps extends PropsFromParent {
addQueryRow: typeof addQueryRow;
changeQuery: typeof changeQuery;
className?: string;
exploreId: ExploreId;
datasourceInstance: ExploreDataSourceApi;
datasourceStatus: DataSourceStatus;
highlightLogsExpressionAction: typeof highlightLogsExpressionAction;

View File

@ -18,6 +18,7 @@ import {
ResultType,
QueryTransaction,
ExploreUIState,
ExploreMode,
} from 'app/types/explore';
import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
@ -49,6 +50,11 @@ export interface AddQueryRowPayload {
query: DataQuery;
}
export interface ChangeModePayload {
exploreId: ExploreId;
mode: ExploreMode;
}
export interface ChangeQueryPayload {
exploreId: ExploreId;
query: DataQuery;
@ -245,6 +251,11 @@ export const addQueryRowAction = actionCreatorFactory<AddQueryRowPayload>('explo
*/
export const changeDatasourceAction = noPayloadActionCreatorFactory('explore/CHANGE_DATASOURCE').create();
/**
* Change the mode of Explore.
*/
export const changeModeAction = actionCreatorFactory<ChangeModePayload>('explore/CHANGE_MODE').create();
/**
* Query change handler for the query row with the given index.
* If `override` is reset the query modifications and run the queries. Use this to set queries via a link.

View File

@ -44,6 +44,7 @@ import {
QueryOptions,
ExploreUIState,
QueryTransaction,
ExploreMode,
} from 'app/types/explore';
import {
updateDatasourceInstanceAction,
@ -85,6 +86,7 @@ import {
queryStartAction,
historyUpdatedAction,
resetQueryErrorAction,
changeModeAction,
} from './actionTypes';
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
import { LogsDedupStrategy } from 'app/core/logs_model';
@ -140,6 +142,16 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun
};
}
/**
* Change the display mode in Explore.
*/
export function changeMode(exploreId: ExploreId, mode: ExploreMode): ThunkResult<void> {
return dispatch => {
dispatch(changeModeAction({ exploreId, mode }));
dispatch(runQueries(exploreId));
};
}
/**
* Query change handler for the query row with the given index.
* If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
@ -509,11 +521,9 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
showingLogs,
showingGraph,
showingTable,
supportsGraph,
supportsLogs,
supportsTable,
datasourceError,
containerWidth,
mode,
} = getState().explore[exploreId];
if (datasourceError) {
@ -533,7 +543,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
dispatch(runQueriesAction({ exploreId }));
// Keep table queries first since they need to return quickly
if ((ignoreUIState || showingTable) && supportsTable) {
if ((ignoreUIState || showingTable) && mode === ExploreMode.Metrics) {
dispatch(
runQueriesForType(exploreId, 'Table', {
interval,
@ -543,7 +553,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
})
);
}
if ((ignoreUIState || showingGraph) && supportsGraph) {
if ((ignoreUIState || showingGraph) && mode === ExploreMode.Metrics) {
dispatch(
runQueriesForType(exploreId, 'Graph', {
interval,
@ -553,7 +563,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
})
);
}
if ((ignoreUIState || showingLogs) && supportsLogs) {
if ((ignoreUIState || showingLogs) && mode === ExploreMode.Logs) {
dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
}

View File

@ -12,6 +12,7 @@ import {
ExploreState,
QueryTransaction,
RangeScanner,
ExploreMode,
} from 'app/types/explore';
import { reducerTester } from 'test/core/redux/reducerTester';
import {
@ -23,6 +24,7 @@ import {
updateDatasourceInstanceAction,
splitOpenAction,
splitCloseAction,
changeModeAction,
} from './actionTypes';
import { Reducer } from 'redux';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
@ -122,6 +124,17 @@ describe('Explore item reducer', () => {
.thenStateShouldEqual(expectedState);
});
});
describe('when changeDataType is dispatched', () => {
it('then it should set correct state', () => {
reducerTester()
.givenReducer(itemReducer, {})
.whenActionIsDispatched(changeModeAction({ exploreId: ExploreId.left, mode: ExploreMode.Logs }))
.thenStateShouldEqual({
mode: ExploreMode.Logs,
});
});
});
});
describe('changing datasource', () => {
@ -160,6 +173,8 @@ describe('Explore item reducer', () => {
showingStartPage: true,
queries,
queryKeys,
supportedModes: [ExploreMode.Metrics, ExploreMode.Logs],
mode: ExploreMode.Metrics,
};
reducerTester()

View File

@ -8,7 +8,7 @@ import {
DEFAULT_UI_STATE,
generateNewKeyAndAddRefIdIfMissing,
} from 'app/core/utils/explore';
import { ExploreItemState, ExploreState, ExploreId, ExploreUpdateState } from 'app/types/explore';
import { ExploreItemState, ExploreState, ExploreId, ExploreUpdateState, ExploreMode } from 'app/types/explore';
import { DataQuery } from '@grafana/ui/src/types';
import {
HigherOrderAction,
@ -22,6 +22,7 @@ import {
runQueriesAction,
historyUpdatedAction,
resetQueryErrorAction,
changeModeAction,
} from './actionTypes';
import { reducerFactory } from 'app/core/redux';
import {
@ -107,6 +108,8 @@ export const makeExploreItemState = (): ExploreItemState => ({
update: makeInitialUpdateState(),
queryErrors: [],
latency: 0,
supportedModes: [],
mode: null,
});
/**
@ -165,6 +168,13 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
return { ...state, containerWidth };
},
})
.addMapper({
filter: changeModeAction,
mapper: (state, action): ExploreItemState => {
const mode = action.payload.mode;
return { ...state, mode };
},
})
.addMapper({
filter: changeTimeAction,
mapper: (state, action): ExploreItemState => {
@ -226,6 +236,21 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
const supportsLogs = datasourceInstance.meta.logs;
const supportsTable = datasourceInstance.meta.tables;
let mode = ExploreMode.Metrics;
const supportedModes: ExploreMode[] = [];
if (supportsGraph) {
supportedModes.push(ExploreMode.Metrics);
}
if (supportsLogs) {
supportedModes.push(ExploreMode.Logs);
}
if (supportedModes.length === 1) {
mode = supportedModes[0];
}
// Custom components
const StartPage = datasourceInstance.components.ExploreStartPage;
@ -243,6 +268,8 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
StartPage,
showingStartPage: Boolean(StartPage),
queryKeys: getQueryKeys(state.queries, datasourceInstance),
supportedModes,
mode,
};
},
})

View File

@ -4,6 +4,7 @@
"id": "testdata",
"metrics": true,
"logs": true,
"alerting": true,
"annotations": true,

View File

@ -17,6 +17,11 @@ import { Emitter, TimeSeries } from 'app/core/core';
import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model';
import TableModel from 'app/core/table_model';
export enum ExploreMode {
Metrics = 'Metrics',
Logs = 'Logs',
}
export interface CompletionItem {
/**
* The label of this completion item. By default
@ -258,6 +263,8 @@ export interface ExploreItemState {
queryErrors: DataQueryError[];
latency: number;
supportedModes: ExploreMode[];
mode: ExploreMode;
}
export interface ExploreUpdateState {

View File

@ -92,6 +92,7 @@
.explore-toolbar-content-item:first-child {
padding-left: $dashboard-padding;
margin-right: auto;
display: flex;
}
@media only screen and (max-width: 1545px) {
@ -413,3 +414,21 @@
margin: $space-xs 0;
cursor: pointer;
}
.query-type-toggle {
margin-left: 5px;
.toggle-button-group {
padding-top: 2px;
}
.btn.active {
background-color: $input-bg;
background-image: none;
background-clip: padding-box;
border: $input-border;
border-radius: $input-border-radius;
@include box-shadow($input-box-shadow);
color: $input-color;
}
}