mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 20:24:18 -06:00
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:
parent
be66ed9dab
commit
e6001f57a2
@ -39,7 +39,7 @@ export const ToggleButton: FC<ToggleButtonProps> = ({
|
||||
}) => {
|
||||
const onClick = (event: React.SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
if (onChange) {
|
||||
if (!selected && onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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)(
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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' }));
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
};
|
||||
},
|
||||
})
|
||||
|
@ -4,6 +4,7 @@
|
||||
"id": "testdata",
|
||||
|
||||
"metrics": true,
|
||||
"logs": true,
|
||||
"alerting": true,
|
||||
"annotations": true,
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user