Explorar o código

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
Marcus Efraimsson %!s(int64=6) %!d(string=hai) anos
pai
achega
e6001f57a2

+ 1 - 1
public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx

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

+ 10 - 15
public/app/features/explore/Explore.tsx

@@ -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,
   };
 }
 

+ 76 - 2
public/app/features/explore/ExploreToolbar.tsx

@@ -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)(

+ 2 - 1
public/app/features/explore/QueryEditor.tsx

@@ -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);
   }
 

+ 1 - 0
public/app/features/explore/QueryRow.tsx

@@ -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;

+ 11 - 0
public/app/features/explore/state/actionTypes.ts

@@ -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.

+ 16 - 6
public/app/features/explore/state/actions.ts

@@ -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' }));
     }
 

+ 15 - 0
public/app/features/explore/state/reducers.test.ts

@@ -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()

+ 28 - 1
public/app/features/explore/state/reducers.ts

@@ -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,
       };
     },
   })

+ 1 - 0
public/app/plugins/datasource/testdata/plugin.json

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

+ 7 - 0
public/app/types/explore.ts

@@ -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 {

+ 19 - 0
public/sass/pages/_explore.scss

@@ -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;
+  }
+}