Просмотр исходного кода

Merge branch 'master' into fix/explore-do-not-clear-results

Torkel Ödegaard 7 лет назад
Родитель
Сommit
fd06e51776

+ 1 - 5
.circleci/config.yml

@@ -147,9 +147,6 @@ jobs:
       - run:
           name: sha-sum packages
           command: 'go run build.go sha-dist'
-      - run:
-          name: Build Grafana.com master publisher
-          command: 'go build -o scripts/publish scripts/build/publish.go'
       - run:
           name: Test and build Grafana.com release publisher
           command: 'cd scripts/build/release_publisher && go test . && go build -o release_publisher .'
@@ -158,7 +155,6 @@ jobs:
           paths:
             - dist/grafana*
             - scripts/*.sh
-            - scripts/publish
             - scripts/build/release_publisher/release_publisher
             - scripts/build/publish.sh
 
@@ -393,7 +389,7 @@ jobs:
           name: Publish to Grafana.com
           command: |
             rm dist/grafana-master-$(echo "${CIRCLE_SHA1}" | cut -b1-7).linux-x64.tar.gz
-            ./scripts/publish -apiKey ${GRAFANA_COM_API_KEY}
+            cd dist && ../scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -from-local
 
   deploy-release:
     docker:

+ 1 - 1
conf/defaults.ini

@@ -504,7 +504,7 @@ concurrent_render_limit = 5
 #################################### Explore #############################
 [explore]
 # Enable the Explore section
-enabled = false
+enabled = true
 
 #################################### Internal Grafana Metrics ############
 # Metrics available at HTTP API Url /metrics

+ 1 - 1
conf/sample.ini

@@ -429,7 +429,7 @@ log_queries =
 #################################### Explore #############################
 [explore]
 # Enable the Explore section
-;enabled = false
+;enabled = true
 
 #################################### Internal Grafana Metrics ##########################
 # Metrics available at HTTP API Url /metrics

+ 1 - 1
pkg/api/dashboard.go

@@ -336,7 +336,7 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
 		"id":   123123,
 		"gridPos": map[string]interface{}{
 			"x": 0,
-			"y": 0,
+			"y": 3,
 			"w": 24,
 			"h": 4,
 		},

+ 1 - 1
pkg/setting/setting.go

@@ -718,7 +718,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	AlertingNoDataOrNullValues = alerting.Key("nodata_or_nullvalues").MustString("no_data")
 
 	explore := iniFile.Section("explore")
-	ExploreEnabled = explore.Key("enabled").MustBool(false)
+	ExploreEnabled = explore.Key("enabled").MustBool(true)
 
 	panels := iniFile.Section("panels")
 	cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false)

+ 6 - 2
public/app/core/actions/location.ts

@@ -1,13 +1,17 @@
 import { LocationUpdate } from 'app/types';
 
+export enum CoreActionTypes {
+  UpdateLocation = 'UPDATE_LOCATION',
+}
+
 export type Action = UpdateLocationAction;
 
 export interface UpdateLocationAction {
-  type: 'UPDATE_LOCATION';
+  type: CoreActionTypes.UpdateLocation;
   payload: LocationUpdate;
 }
 
 export const updateLocation = (location: LocationUpdate): UpdateLocationAction => ({
-  type: 'UPDATE_LOCATION',
+  type: CoreActionTypes.UpdateLocation,
   payload: location,
 });

+ 3 - 5
public/app/core/reducers/location.ts

@@ -1,4 +1,4 @@
-import { Action } from 'app/core/actions/location';
+import { Action, CoreActionTypes } from 'app/core/actions/location';
 import { LocationState } from 'app/types';
 import { renderUrl } from 'app/core/utils/url';
 import _ from 'lodash';
@@ -12,7 +12,7 @@ export const initialState: LocationState = {
 
 export const locationReducer = (state = initialState, action: Action): LocationState => {
   switch (action.type) {
-    case 'UPDATE_LOCATION': {
+    case CoreActionTypes.UpdateLocation: {
       const { path, routeParams } = action.payload;
       let query = action.payload.query || state.query;
 
@@ -24,9 +24,7 @@ export const locationReducer = (state = initialState, action: Action): LocationS
       return {
         url: renderUrl(path || state.path, query),
         path: path || state.path,
-        query: {
-          ...query,
-        },
+        query: { ...query },
         routeParams: routeParams || state.routeParams,
       };
     }

+ 0 - 1
public/app/features/dashboard/services/DashboardViewStateSrv.test.ts

@@ -58,7 +58,6 @@ describe('when updating view state', () => {
     it('should remove params from query string', () => {
       viewState.update({ fullscreen: true, panelId: 1, edit: true });
       viewState.update({ fullscreen: false });
-      expect(viewState.dashboard.meta.fullscreen).toBe(false);
       expect(viewState.state.fullscreen).toBe(null);
     });
   });

+ 11 - 2
public/app/features/dashboard/services/DashboardViewStateSrv.ts

@@ -72,7 +72,6 @@ export class DashboardViewStateSrv {
     }
 
     _.extend(this.state, state);
-    this.dashboard.meta.fullscreen = this.state.fullscreen;
 
     if (!this.state.fullscreen) {
       this.state.fullscreen = null;
@@ -117,10 +116,20 @@ export class DashboardViewStateSrv {
   }
 
   syncState() {
-    if (this.dashboard.meta.fullscreen) {
+    if (this.state.fullscreen) {
       const panel = this.dashboard.getPanelById(this.state.panelId);
 
       if (!panel) {
+        this.state.fullscreen = null;
+        this.state.panelId = null;
+        this.state.edit = null;
+
+        this.update(this.state);
+
+        setTimeout(() => {
+          appEvents.emit('alert-error', ['Error', 'Panel not found']);
+        }, 100);
+
         return;
       }
 

+ 2 - 103
public/app/features/explore/Explore.tsx

@@ -9,8 +9,6 @@ import { AutoSizer } from 'react-virtualized';
 import store from 'app/core/store';
 
 // Components
-import { DataSourceSelectItem } from '@grafana/ui/src/types';
-import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
 import { Alert } from './Error';
 import ErrorBoundary from './ErrorBoundary';
 import GraphContainer from './GraphContainer';
@@ -21,18 +19,13 @@ import TimePicker, { parseTime } from './TimePicker';
 
 // Actions
 import {
-  changeDatasource,
   changeSize,
   changeTime,
-  clearQueries,
   initializeExplore,
   modifyQueries,
-  runQueries,
   scanStart,
   scanStop,
   setQueries,
-  splitClose,
-  splitOpen,
 } from './state/actions';
 
 // Types
@@ -41,27 +34,23 @@ import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/
 import { StoreState } from 'app/types';
 import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore';
 import { Emitter } from 'app/core/utils/emitter';
+import { ExploreToolbar } from './ExploreToolbar';
 
 interface ExploreProps {
   StartPage?: any;
-  changeDatasource: typeof changeDatasource;
   changeSize: typeof changeSize;
   changeTime: typeof changeTime;
-  clearQueries: typeof clearQueries;
   datasourceError: string;
   datasourceInstance: any;
   datasourceLoading: boolean | null;
   datasourceMissing: boolean;
-  exploreDatasources: DataSourceSelectItem[];
   exploreId: ExploreId;
   initialDatasource?: string;
   initialQueries: DataQuery[];
   initializeExplore: typeof initializeExplore;
   initialized: boolean;
-  loading: boolean;
   modifyQueries: typeof modifyQueries;
   range: RawTimeRange;
-  runQueries: typeof runQueries;
   scanner?: RangeScanner;
   scanning?: boolean;
   scanRange?: RawTimeRange;
@@ -69,8 +58,6 @@ interface ExploreProps {
   scanStop: typeof scanStop;
   setQueries: typeof setQueries;
   split: boolean;
-  splitClose: typeof splitClose;
-  splitOpen: typeof splitOpen;
   showingStartPage?: boolean;
   supportsGraph: boolean | null;
   supportsLogs: boolean | null;
@@ -145,10 +132,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
     this.el = el;
   };
 
-  onChangeDatasource = async option => {
-    this.props.changeDatasource(this.props.exploreId, option.value);
-  };
-
   onChangeTime = (range: TimeRange, changedByScanner?: boolean) => {
     if (this.props.scanning && !changedByScanner) {
       this.onStopScanning();
@@ -156,23 +139,11 @@ export class Explore extends React.PureComponent<ExploreProps> {
     this.props.changeTime(this.props.exploreId, range);
   };
 
-  onClickClear = () => {
-    this.props.clearQueries(this.props.exploreId);
-  };
-
-  onClickCloseSplit = () => {
-    this.props.splitClose();
-  };
-
   // Use this in help pages to set page to a single query
   onClickExample = (query: DataQuery) => {
     this.props.setQueries(this.props.exploreId, [query]);
   };
 
-  onClickSplit = () => {
-    this.props.splitOpen();
-  };
-
   onClickLabel = (key: string, value: string) => {
     this.onModifyQueries({ type: 'ADD_FILTER', key, value });
   };
@@ -204,10 +175,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
     this.props.scanStop(this.props.exploreId);
   };
 
-  onSubmit = () => {
-    this.props.runQueries(this.props.exploreId);
-  };
-
   render() {
     const {
       StartPage,
@@ -215,11 +182,8 @@ export class Explore extends React.PureComponent<ExploreProps> {
       datasourceError,
       datasourceLoading,
       datasourceMissing,
-      exploreDatasources,
       exploreId,
-      loading,
       initialQueries,
-      range,
       showingStartPage,
       split,
       supportsGraph,
@@ -227,64 +191,10 @@ export class Explore extends React.PureComponent<ExploreProps> {
       supportsTable,
     } = this.props;
     const exploreClass = split ? 'explore explore-split' : 'explore';
-    const selectedDatasource = datasourceInstance
-      ? exploreDatasources.find(d => d.name === datasourceInstance.name)
-      : undefined;
 
     return (
       <div className={exploreClass} ref={this.getRef}>
-        <div className="navbar">
-          {exploreId === 'left' ? (
-            <div>
-              <a className="navbar-page-btn">
-                <i className="fa fa-rocket" />
-                Explore
-              </a>
-            </div>
-          ) : (
-            <>
-              <div className="navbar-page-btn" />
-              <div className="navbar-buttons explore-first-button">
-                <button className="btn navbar-button" onClick={this.onClickCloseSplit}>
-                  Close Split
-                </button>
-              </div>
-            </>
-          )}
-          {!datasourceMissing ? (
-            <div className="navbar-buttons">
-              <DataSourcePicker
-                onChange={this.onChangeDatasource}
-                datasources={exploreDatasources}
-                current={selectedDatasource}
-              />
-            </div>
-          ) : null}
-          <div className="navbar__spacer" />
-          {exploreId === 'left' && !split ? (
-            <div className="navbar-buttons">
-              <button className="btn navbar-button" onClick={this.onClickSplit}>
-                Split
-              </button>
-            </div>
-          ) : null}
-          <TimePicker ref={this.timepickerRef} range={range} onChangeTime={this.onChangeTime} />
-          <div className="navbar-buttons">
-            <button className="btn navbar-button navbar-button--no-icon" onClick={this.onClickClear}>
-              Clear All
-            </button>
-          </div>
-          <div className="navbar-buttons relative">
-            <button className="btn navbar-button navbar-button--primary" onClick={this.onSubmit}>
-              Run Query{' '}
-              {loading ? (
-                <i className="fa fa-spinner fa-fw fa-spin run-icon" />
-              ) : (
-                <i className="fa fa-level-down fa-fw run-icon" />
-              )}
-            </button>
-          </div>
-        </div>
+        <ExploreToolbar exploreId={exploreId} timepickerRef={this.timepickerRef} onChangeTime={this.onChangeTime} />
         {datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
         {datasourceMissing ? (
           <div className="explore-container">Please add a datasource that supports Explore (e.g., Prometheus).</div>
@@ -341,30 +251,24 @@ function mapStateToProps(state: StoreState, { exploreId }) {
     datasourceInstance,
     datasourceLoading,
     datasourceMissing,
-    exploreDatasources,
     initialDatasource,
     initialQueries,
     initialized,
-    queryTransactions,
     range,
     showingStartPage,
     supportsGraph,
     supportsLogs,
     supportsTable,
   } = item;
-  const loading = queryTransactions.some(qt => !qt.done);
   return {
     StartPage,
     datasourceError,
     datasourceInstance,
     datasourceLoading,
     datasourceMissing,
-    exploreDatasources,
     initialDatasource,
     initialQueries,
     initialized,
-    loading,
-    queryTransactions,
     range,
     showingStartPage,
     split,
@@ -375,18 +279,13 @@ function mapStateToProps(state: StoreState, { exploreId }) {
 }
 
 const mapDispatchToProps = {
-  changeDatasource,
   changeSize,
   changeTime,
-  clearQueries,
   initializeExplore,
   modifyQueries,
-  runQueries,
   scanStart,
   scanStop,
   setQueries,
-  splitClose,
-  splitOpen,
 };
 
 export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Explore));

+ 191 - 0
public/app/features/explore/ExploreToolbar.tsx

@@ -0,0 +1,191 @@
+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, TimeRange } from '@grafana/ui';
+import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
+import { StoreState } from 'app/types/store';
+import { changeDatasource, clearQueries, splitClose, runQueries, splitOpen } from './state/actions';
+import TimePicker from './TimePicker';
+
+enum IconSide {
+  left = 'left',
+  right = 'right',
+}
+
+const createResponsiveButton = (options: {
+  splitted: boolean;
+  title: string;
+  onClick: () => void;
+  buttonClassName?: string;
+  iconClassName?: string;
+  iconSide?: IconSide;
+}) => {
+  const defaultOptions = {
+    iconSide: IconSide.left,
+  };
+  const props = { ...options, defaultOptions };
+  const { title, onClick, buttonClassName, iconClassName, splitted, iconSide } = props;
+
+  return (
+    <button className={`btn navbar-button ${buttonClassName ? buttonClassName : ''}`} onClick={onClick}>
+      {iconClassName && iconSide === IconSide.left ? <i className={`${iconClassName} icon-margin-right`} /> : null}
+      <span className="btn-title">{!splitted ? title : ''}</span>
+      {iconClassName && iconSide === IconSide.right ? <i className={`${iconClassName} icon-margin-left`} /> : null}
+    </button>
+  );
+};
+
+interface OwnProps {
+  exploreId: ExploreId;
+  timepickerRef: React.RefObject<TimePicker>;
+  onChangeTime: (range: TimeRange, changedByScanner?: boolean) => void;
+}
+
+interface StateProps {
+  datasourceMissing: boolean;
+  exploreDatasources: DataSourceSelectItem[];
+  loading: boolean;
+  range: RawTimeRange;
+  selectedDatasource: DataSourceSelectItem;
+  splitted: boolean;
+}
+
+interface DispatchProps {
+  changeDatasource: typeof changeDatasource;
+  clearAll: typeof clearQueries;
+  runQuery: typeof runQueries;
+  closeSplit: typeof splitClose;
+  split: typeof splitOpen;
+}
+
+type Props = StateProps & DispatchProps & OwnProps;
+
+export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
+  constructor(props) {
+    super(props);
+  }
+
+  onChangeDatasource = async option => {
+    this.props.changeDatasource(this.props.exploreId, option.value);
+  };
+
+  onClearAll = () => {
+    this.props.clearAll(this.props.exploreId);
+  };
+
+  onRunQuery = () => {
+    this.props.runQuery(this.props.exploreId);
+  };
+
+  render() {
+    const {
+      datasourceMissing,
+      exploreDatasources,
+      exploreId,
+      loading,
+      range,
+      selectedDatasource,
+      splitted,
+      timepickerRef,
+    } = this.props;
+
+    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' && (
+                <a className="navbar-page-btn">
+                  <i className="fa fa-rocket fa-fw" />
+                  Explore
+                </a>
+              )}
+            </div>
+            <div className="explore-toolbar-header-close">
+              {exploreId === 'right' && (
+                <a onClick={this.props.closeSplit}>
+                  <i className="fa fa-times fa-fw" />
+                </a>
+              )}
+            </div>
+          </div>
+        </div>
+        <div className="explore-toolbar-item">
+          <div className="explore-toolbar-content">
+            {!datasourceMissing ? (
+              <div className="explore-toolbar-content-item">
+                <div className="datasource-picker">
+                  <DataSourcePicker
+                    onChange={this.onChangeDatasource}
+                    datasources={exploreDatasources}
+                    current={selectedDatasource}
+                  />
+                </div>
+              </div>
+            ) : null}
+            {exploreId === 'left' && !splitted ? (
+              <div className="explore-toolbar-content-item">
+                {createResponsiveButton({
+                  splitted,
+                  title: 'Split',
+                  onClick: this.props.split,
+                  iconClassName: 'fa fa-fw fa-columns icon-margin-right',
+                  iconSide: IconSide.left,
+                })}
+              </div>
+            ) : null}
+            <div className="explore-toolbar-content-item timepicker">
+              <TimePicker ref={timepickerRef} range={range} onChangeTime={this.props.onChangeTime} />
+            </div>
+            <div className="explore-toolbar-content-item">
+              <button className="btn navbar-button navbar-button--no-icon" onClick={this.onClearAll}>
+                Clear All
+              </button>
+            </div>
+            <div className="explore-toolbar-content-item">
+              {createResponsiveButton({
+                splitted,
+                title: 'Run Query',
+                onClick: this.onRunQuery,
+                buttonClassName: 'navbar-button--primary',
+                iconClassName: loading ? 'fa fa-spinner fa-fw fa-spin run-icon' : 'fa fa-level-down fa-fw run-icon',
+                iconSide: IconSide.right,
+              })}
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+
+const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps => {
+  const splitted = state.explore.split;
+  const exploreItem = state.explore[exploreId];
+  const { datasourceInstance, datasourceMissing, exploreDatasources, queryTransactions, range } = exploreItem;
+  const selectedDatasource = datasourceInstance
+    ? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
+    : undefined;
+  const loading = queryTransactions.some(qt => !qt.done);
+
+  return {
+    datasourceMissing,
+    exploreDatasources,
+    loading,
+    range,
+    selectedDatasource,
+    splitted,
+  };
+};
+
+const mapDispatchToProps: DispatchProps = {
+  changeDatasource,
+  clearAll: clearQueries,
+  runQuery: runQueries,
+  closeSplit: splitClose,
+  split: splitOpen,
+};
+
+export const ExploreToolbar = hot(module)(connect(mapStateToProps, mapDispatchToProps)(UnConnectedExploreToolbar));

+ 12 - 7
public/app/features/explore/QueryField.tsx

@@ -73,6 +73,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
   placeholdersBuffer: PlaceholdersBuffer;
   plugins: any[];
   resetTimer: any;
+  mounted: boolean;
 
   constructor(props: QueryFieldProps, context) {
     super(props, context);
@@ -93,10 +94,12 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
   }
 
   componentDidMount() {
+    this.mounted = true;
     this.updateMenu();
   }
 
   componentWillUnmount() {
+    this.mounted = false;
     clearTimeout(this.resetTimer);
   }
 
@@ -347,13 +350,15 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
   };
 
   resetTypeahead = () => {
-    this.setState({
-      suggestions: [],
-      typeaheadIndex: 0,
-      typeaheadPrefix: '',
-      typeaheadContext: null,
-    });
-    this.resetTimer = null;
+    if (this.mounted) {
+      this.setState({
+        suggestions: [],
+        typeaheadIndex: 0,
+        typeaheadPrefix: '',
+        typeaheadContext: null,
+      });
+      this.resetTimer = null;
+    }
   };
 
   handleBlur = () => {

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

@@ -293,6 +293,7 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
 
   render() {
     const { isUtc, rangeString, refreshInterval } = this.state;
+
     return (
       <div className="timepicker">
         <div className="navbar-buttons">

+ 7 - 1
public/app/features/explore/Wrapper.tsx

@@ -7,7 +7,7 @@ import { StoreState } from 'app/types';
 import { ExploreId, ExploreUrlState } from 'app/types/explore';
 import { parseUrlState } from 'app/core/utils/explore';
 
-import { initializeExploreSplit } from './state/actions';
+import { initializeExploreSplit, resetExplore } from './state/actions';
 import ErrorBoundary from './ErrorBoundary';
 import Explore from './Explore';
 import { CustomScrollbar } from '@grafana/ui';
@@ -16,6 +16,7 @@ interface WrapperProps {
   initializeExploreSplit: typeof initializeExploreSplit;
   split: boolean;
   updateLocation: typeof updateLocation;
+  resetExplore: typeof resetExplore;
   urlStates: { [key: string]: string };
 }
 
@@ -42,6 +43,10 @@ export class Wrapper extends Component<WrapperProps> {
     }
   }
 
+  componentWillUnmount() {
+    this.props.resetExplore();
+  }
+
   render() {
     const { split } = this.props;
     const { leftState, rightState } = this.urlStates;
@@ -74,6 +79,7 @@ const mapStateToProps = (state: StoreState) => {
 const mapDispatchToProps = {
   initializeExploreSplit,
   updateLocation,
+  resetExplore,
 };
 
 export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper));

+ 9 - 2
public/app/features/explore/state/actionTypes.ts

@@ -1,6 +1,6 @@
 // Types
 import { Emitter } from 'app/core/core';
-import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem  } from '@grafana/ui/src/types';
+import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types';
 import {
   ExploreId,
   ExploreItemState,
@@ -41,6 +41,7 @@ export enum ActionTypes {
   ToggleGraph = 'explore/TOGGLE_GRAPH',
   ToggleLogs = 'explore/TOGGLE_LOGS',
   ToggleTable = 'explore/TOGGLE_TABLE',
+  ResetExplore = 'explore/RESET_EXPLORE',
 }
 
 export interface AddQueryRowAction {
@@ -270,6 +271,11 @@ export interface ToggleLogsAction {
   };
 }
 
+export interface ResetExploreAction {
+  type: ActionTypes.ResetExplore;
+  payload: {};
+}
+
 export type Action =
   | AddQueryRowAction
   | ChangeQueryAction
@@ -297,4 +303,5 @@ export type Action =
   | SplitOpenAction
   | ToggleGraphAction
   | ToggleLogsAction
-  | ToggleTableAction;
+  | ToggleTableAction
+  | ResetExploreAction;

+ 11 - 2
public/app/features/explore/state/actions.ts

@@ -21,7 +21,7 @@ import { updateLocation } from 'app/core/actions';
 
 // Types
 import { StoreState } from 'app/types';
-import { DataQuery, DataSourceSelectItem, QueryHint  } from '@grafana/ui/src/types';
+import { DataQuery, DataSourceSelectItem, QueryHint } from '@grafana/ui/src/types';
 import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
 import {
   ExploreId,
@@ -48,7 +48,6 @@ import {
   ScanStopAction,
 } from './actionTypes';
 
-
 type ThunkResult<R> = ThunkAction<R, StoreState, undefined, ThunkableAction>;
 
 /**
@@ -539,6 +538,7 @@ export function runQueries(exploreId: ExploreId) {
 
     if (!hasNonEmptyQuery(modifiedQueries)) {
       dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } });
+      dispatch(stateSave()); // Remember to saves to state and update location
       return;
     }
 
@@ -766,3 +766,12 @@ export function toggleTable(exploreId: ExploreId): ThunkResult<void> {
     }
   };
 }
+
+/**
+ * Resets state for explore.
+ */
+export function resetExplore(): ThunkResult<void> {
+  return dispatch => {
+    dispatch({ type: ActionTypes.ResetExplore, payload: {} });
+  };
+}

+ 7 - 13
public/app/features/explore/state/reducers.ts

@@ -422,25 +422,19 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
 export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
   switch (action.type) {
     case ActionTypes.SplitClose: {
-      return {
-        ...state,
-        split: false,
-      };
+      return { ...state, split: false };
     }
 
     case ActionTypes.SplitOpen: {
-      return {
-        ...state,
-        split: true,
-        right: action.payload.itemState,
-      };
+      return { ...state, split: true, right: action.payload.itemState };
     }
 
     case ActionTypes.InitializeExploreSplit: {
-      return {
-        ...state,
-        split: true,
-      };
+      return { ...state, split: true };
+    }
+
+    case ActionTypes.ResetExplore: {
+      return initialExploreState;
     }
   }
 

+ 2 - 1
public/app/plugins/datasource/loki/language_provider.ts

@@ -173,8 +173,9 @@ export default class LokiLanguageProvider extends LanguageProvider {
         })
       );
     }
+    // Return a cleaned LokiQuery
     return queries.map(query => ({
-      ...query,
+      refId: query.refId,
       expr: '',
     }));
   }

+ 19 - 2
public/dashboards/home.json

@@ -10,6 +10,23 @@
   "id": null,
   "links": [],
   "panels": [
+    {
+      "content": "<div class=\"text-center dashboard-header\">\n  <span>Home Dashboard</span>\n</div>",
+      "editable": true,
+      "id": 1,
+      "links": [],
+      "mode": "html",
+      "style": {},
+      "title": "",
+      "transparent": true,
+      "type": "text",
+      "gridPos": {
+        "w": 24,
+        "h": 3,
+        "x": 0,
+        "y": 0
+      }
+    },
     {
       "folderId": 0,
       "headings": true,
@@ -28,7 +45,7 @@
         "w": 12,
         "h": 17,
         "x": 0,
-        "y": 1
+        "y": 6
       }
     },
     {
@@ -43,7 +60,7 @@
         "w": 12,
         "h": 17,
         "x": 12,
-        "y": 1
+        "y": 6
       }
     }
   ],

+ 266 - 122
public/sass/pages/_explore.scss

@@ -1,178 +1,322 @@
-.explore {
-  flex: 1 1 auto;
+.icon-margin-right {
+  margin-right: 0.25em;
+}
 
-  &-container {
-    padding: $dashboard-padding;
-  }
+.icon-margin-left {
+  margin-left: 0.25em;
+}
 
-  &-wrapper {
-    display: flex;
+.run-icon {
+  transform: rotate(90deg);
+}
 
-    > .explore-split {
-      width: 50%;
-    }
-  }
+.timepicker {
+  display: flex;
+}
 
-  // Push split button a bit
-  .explore-first-button {
-    margin-left: 15px;
+.timepicker-rangestring {
+  margin-left: 0.5em;
+}
+
+.datasource-picker {
+  .ds-picker {
+    min-width: 200px;
+    max-width: 200px;
   }
+}
 
-  .explore-panel {
-    margin-top: $panel-margin;
+.sidemenu-open {
+  .explore-toolbar-header {
+    padding: 0;
+    margin-left: 0;
   }
 
-  .explore-panel__body {
-    padding: $panel-padding;
+  .explore-toolbar-header-title {
+    .navbar-page-btn {
+      padding-left: 0;
+    }
   }
+}
 
-  .explore-panel__header {
-    padding: $panel-padding;
-    padding-top: 5px;
-    padding-bottom: 0;
-    display: flex;
-    cursor: pointer;
-    margin-bottom: 5px;
-    transition: all 0.1s linear;
+.explore-toolbar {
+  background: inherit;
+  display: flex;
+  flex-flow: row wrap;
+  justify-content: flex-start;
+  height: auto;
+  padding: 0px $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;
+}
+
+.explore-toolbar.splitted {
+  .explore-toolbar-item {
+    flex: 1 1 100%;
   }
 
-  .explore-panel__header-label {
-    font-weight: 500;
-    margin-right: $panel-margin;
-    font-size: $font-size-h6;
-    box-shadow: $text-shadow-faint;
+  .explore-toolbar-content-item:first-child {
+    padding-left: 0;
+    margin-right: auto;
   }
+}
+
+.explore-toolbar-item:last-child {
+  flex: auto;
+}
 
-  .explore-panel__header-buttons {
-    margin-right: $panel-margin;
-    font-size: $font-size-lg;
-    line-height: $font-size-h6;
+.explore-toolbar-header {
+  display: flex;
+  flex: 1 1 0;
+  flex-flow: row nowrap;
+  font-size: 18px;
+  min-height: 55px;
+  line-height: 55px;
+  justify-content: space-between;
+  margin-left: $panel-margin * 3;
+}
+
+.explore-toolbar-header {
+  justify-content: space-between;
+  align-items: center;
+}
+
+.explore-toolbar-header-title {
+  color: darken($link-color, 5%);
+
+  .navbar-page-btn {
+    padding-left: $dashboard-padding;
   }
 
-  // Make sure wrap buttons around on small screens
-  .navbar {
-    flex-wrap: wrap;
-    height: auto;
+  .fa {
+    font-size: 100%;
+    opacity: 0.75;
+    margin-right: 0.5em;
   }
+}
 
-  .navbar-page-btn {
-    margin-right: 1rem;
+.explore-toolbar-header-close {
+  margin-left: auto;
+}
+
+.explore-toolbar-content {
+  display: flex;
+  flex-flow: row wrap;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.explore-toolbar-content-item {
+  padding: 10px 2px;
+}
+
+.explore-toolbar-content-item:first-child {
+  padding-left: $dashboard-padding;
+  margin-right: auto;
+}
 
-    // Explore icon in header
-    .fa {
-      font-size: 100%;
-      opacity: 0.75;
-      margin-right: 0.5em;
+@media only screen and (max-width: 1545px) {
+  .explore-toolbar.splitted {
+    .timepicker-rangestring {
+      display: none;
     }
   }
+}
 
-  // Toggle mode
-  .navbar-button.active {
-    color: $btn-active-text-color;
-    background-color: $btn-active-bg;
+@media only screen and (max-width: 1070px) {
+  .timepicker {
+    .timepicker-rangestring {
+      display: none;
+    }
   }
 
-  .navbar-button--no-icon {
-    line-height: 18px;
+  .explore-toolbar-content {
+    justify-content: flex-start;
   }
 
-  .result-options {
-    margin: 2 * $panel-margin 0;
+  .explore-toolbar.splitted {
+    .explore-toolbar-content-item {
+      padding: 2px 0;
+      margin: 0;
+    }
   }
 
-  .time-series-disclaimer {
-    width: 300px;
-    margin: $panel-margin auto;
-    padding: 10px 0;
-    border-radius: $border-radius;
-    text-align: center;
-    background-color: $panel-bg;
-
-    .disclaimer-icon {
-      color: $yellow;
-      margin-right: $panel-margin/2;
-    }
+  .explore-toolbar-content-item {
+    padding: 2px 2px;
+  }
+}
 
-    .show-all-time-series {
-      cursor: pointer;
-      color: $external-link-color;
+@media only screen and (max-width: 803px) {
+  .sidemenu-open {
+    .explore-toolbar-header-title {
+      .navbar-page-btn {
+        padding-left: 0;
+        margin-left: 0;
+      }
     }
   }
 
-  .navbar .elapsed-time {
-    position: absolute;
-    left: 0;
-    right: 0;
-    top: 3.5rem;
-    text-align: center;
-    font-size: 0.8rem;
+  .explore-toolbar-header-title {
+    .navbar-page-btn {
+      padding-left: 0;
+      margin-left: $dashboard-padding;
+    }
   }
 
-  .graph-legend {
-    flex-wrap: wrap;
+  .btn-title {
+    display: none;
   }
+}
 
-  .explore-panel__loader {
-    height: 2px;
-    position: relative;
-    overflow: hidden;
-    background: none;
-    margin: $panel-margin / 2;
+@media only screen and (max-width: 702px) {
+  .explore-toolbar-content-item:first-child {
+    padding-left: 2px;
+    margin-right: 0;
   }
+}
 
-  .explore-panel__loader--active:after {
-    content: ' ';
-    display: block;
-    width: 25%;
-    top: 0;
-    top: -50%;
-    height: 250%;
-    position: absolute;
-    animation: loader 2s cubic-bezier(0.17, 0.67, 0.83, 0.67) 500ms;
-    animation-iteration-count: 100;
-    left: -25%;
-    background: $blue;
+@media only screen and (max-width: 544px) {
+  .sidemenu-open {
+    .explore-toolbar-header-title {
+      .navbar-page-btn {
+        padding-left: 0;
+        margin-left: $dashboard-padding;
+      }
+    }
   }
 
-  @keyframes loader {
-    from {
-      left: -25%;
-      opacity: .1;
-    }
-    to {
-      opacity: 1;
-      left: 100%;
+  .explore-toolbar-header-title {
+    .navbar-page-btn {
+      padding-left: 0;
+      margin-left: $dashboard-padding;
     }
   }
+}
 
-  .datasource-picker {
-    min-width: 200px;
-  }
+.explore {
+  flex: 1 1 auto;
+}
 
-  .timepicker {
-    display: flex;
+.explore + .explore {
+  border-left: 1px dotted $table-border;
+}
 
-    &-rangestring {
-      margin-left: 0.5em;
-    }
-  }
+.explore-container {
+  padding: $dashboard-padding;
+}
+
+.explore-wrapper {
+  display: flex;
 
-  .run-icon {
-    margin-left: 0.25em;
-    transform: rotate(90deg);
+  > .explore-split {
+    width: 50%;
   }
+}
+
+.explore-panel {
+  margin-top: $panel-margin;
+}
+
+.explore-panel__body {
+  padding: $panel-padding;
+}
+
+.explore-panel__header {
+  padding: $panel-padding;
+  padding-top: 5px;
+  padding-bottom: 0;
+  display: flex;
+  cursor: pointer;
+  margin-bottom: 5px;
+  transition: all 0.1s linear;
+}
 
-  .relative {
-    position: relative;
+.explore-panel__header-label {
+  font-weight: 500;
+  margin-right: $panel-margin;
+  font-size: $font-size-h6;
+  box-shadow: $text-shadow-faint;
+}
+
+.explore-panel__header-buttons {
+  margin-right: $panel-margin;
+  font-size: $font-size-lg;
+  line-height: $font-size-h6;
+}
+
+.result-options {
+  margin: 2 * $panel-margin 0;
+}
+
+.time-series-disclaimer {
+  width: 300px;
+  margin: $panel-margin auto;
+  padding: 10px 0;
+  border-radius: $border-radius;
+  text-align: center;
+  background-color: $panel-bg;
+
+  .disclaimer-icon {
+    color: $yellow;
+    margin-right: $panel-margin/2;
   }
 
-  .link {
-    text-decoration: underline;
+  .show-all-time-series {
+    cursor: pointer;
+    color: $external-link-color;
   }
 }
 
-.explore + .explore {
-  border-left: 1px dotted $table-border;
+.navbar .elapsed-time {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 3.5rem;
+  text-align: center;
+  font-size: 0.8rem;
+}
+
+.graph-legend {
+  flex-wrap: wrap;
+}
+
+.explore-panel__loader {
+  height: 2px;
+  position: relative;
+  overflow: hidden;
+  background: none;
+  margin: $panel-margin / 2;
+}
+
+.explore-panel__loader--active:after {
+  content: ' ';
+  display: block;
+  width: 25%;
+  top: 0;
+  top: -50%;
+  height: 250%;
+  position: absolute;
+  animation: loader 2s cubic-bezier(0.17, 0.67, 0.83, 0.67) 500ms;
+  animation-iteration-count: 100;
+  left: -25%;
+  background: $blue;
+}
+
+@keyframes loader {
+  from {
+    left: -25%;
+    opacity: 0.1;
+  }
+  to {
+    left: 100%;
+    opacity: 1;
+  }
 }
 
 .query-row {

+ 0 - 194
scripts/build/publish.go

@@ -1,194 +0,0 @@
-package main
-
-import (
-	"bytes"
-	"encoding/json"
-	"flag"
-	"fmt"
-	"io/ioutil"
-	"log"
-	"net/http"
-	"os"
-	"path/filepath"
-	"regexp"
-	"strings"
-	"time"
-)
-
-var apiURL = flag.String("apiUrl", "https://grafana.com/api", "api url")
-var apiKey = flag.String("apiKey", "", "api key")
-var version = ""
-var versionRe = regexp.MustCompile(`grafana-(.*)(\.|_)(arm64|armhfp|aarch64|armv7|darwin|linux|windows|x86_64)`)
-var debVersionRe = regexp.MustCompile(`grafana_(.*)_(arm64|armv7|armhf|amd64)\.deb`)
-var builds = []build{}
-var architectureMapping = map[string]string{
-	"armv7":   "armv7",
-	"armhfp":  "armv7",
-	"armhf":   "armv7",
-	"arm64":   "arm64",
-	"aarch64": "arm64",
-	"amd64":   "amd64",
-	"x86_64":  "amd64",
-}
-
-func main() {
-	flag.Parse()
-	if *apiKey == "" {
-		log.Fatalf("Require apiKey command line parameters")
-	}
-
-	err := filepath.Walk("dist", packageWalker)
-	if err != nil {
-		log.Fatalf("Cannot find any packages to publish, %v", err)
-	}
-
-	if version == "" {
-		log.Fatalf("No version found")
-	}
-
-	if len(builds) == 0 {
-		log.Fatalf("No builds found")
-	}
-
-	nightly := release{
-		Version:         version,
-		ReleaseDate:     time.Now(),
-		Stable:          false,
-		Nightly:         true,
-		Beta:            false,
-		WhatsNewURL:     "",
-		ReleaseNotesURL: "",
-		Builds:          builds,
-	}
-
-	postRequest("/grafana/versions", nightly, fmt.Sprintf("Create Release %s", nightly.Version))
-	postRequest("/grafana/versions/"+nightly.Version, nightly, fmt.Sprintf("Update Release %s", nightly.Version))
-
-	for _, b := range nightly.Builds {
-		postRequest(fmt.Sprintf("/grafana/versions/%s/packages", nightly.Version), b, fmt.Sprintf("Create Build %s %s", b.Os, b.Arch))
-		postRequest(fmt.Sprintf("/grafana/versions/%s/packages/%s/%s", nightly.Version, b.Arch, b.Os), b, fmt.Sprintf("Update Build %s %s", b.Os, b.Arch))
-	}
-}
-
-func mapPackage(path string, name string, shaBytes []byte) (build, error) {
-	log.Printf("Finding package file %s", name)
-	result := versionRe.FindSubmatch([]byte(name))
-	debResult := debVersionRe.FindSubmatch([]byte(name))
-
-	if len(result) > 0 {
-		version = string(result[1])
-		log.Printf("Version detected: %v", version)
-	} else if len(debResult) > 0 {
-		version = string(debResult[1])
-	} else {
-		return build{}, fmt.Errorf("Unable to figure out version from '%v'", name)
-	}
-
-	os := ""
-	if strings.Contains(name, "linux") {
-		os = "linux"
-	}
-	if strings.HasSuffix(name, "windows-amd64.zip") {
-		os = "win"
-	}
-	if strings.HasSuffix(name, "darwin-amd64.tar.gz") {
-		os = "darwin"
-	}
-	if strings.HasSuffix(name, ".rpm") {
-		os = "rhel"
-	}
-	if strings.HasSuffix(name, ".deb") {
-		os = "deb"
-	}
-	if os == "" {
-		return build{}, fmt.Errorf("Unable to figure out os from '%v'", name)
-	}
-
-	arch := ""
-	for archListed, archReal := range architectureMapping {
-		if strings.Contains(name, archListed) {
-			arch = archReal
-			break
-		}
-	}
-	if arch == "" {
-		return build{}, fmt.Errorf("Unable to figure out arch from '%v'", name)
-	}
-
-	return build{
-		Os:     os,
-		Arch:   arch,
-		URL:    "https://s3-us-west-2.amazonaws.com/grafana-releases/master/" + name,
-		Sha256: string(shaBytes),
-	}, nil
-}
-
-func packageWalker(path string, f os.FileInfo, err error) error {
-	if err != nil {
-		log.Printf("error: %v", err)
-	}
-	if f.Name() == "dist" || strings.Contains(f.Name(), "sha256") || strings.Contains(f.Name(), "latest") {
-		return nil
-	}
-
-	shaBytes, err := ioutil.ReadFile(path + ".sha256")
-	if err != nil {
-		log.Fatalf("Failed to read sha256 file %v", err)
-	}
-
-	build, err := mapPackage(path, f.Name(), shaBytes)
-	if err != nil {
-		log.Printf("Could not map metadata from package: %v", err)
-		return nil
-	}
-
-	builds = append(builds, build)
-	return nil
-}
-
-func postRequest(url string, obj interface{}, desc string) {
-	jsonBytes, _ := json.Marshal(obj)
-	req, _ := http.NewRequest(http.MethodPost, (*apiURL)+url, bytes.NewReader(jsonBytes))
-	req.Header.Add("Authorization", "Bearer "+(*apiKey))
-	req.Header.Add("Content-Type", "application/json")
-
-	res, err := http.DefaultClient.Do(req)
-	if err != nil {
-		log.Fatalf("error: %v", err)
-	}
-
-	if res.StatusCode == http.StatusOK {
-		log.Printf("Action: %s \t OK", desc)
-	} else {
-
-		if res.Body != nil {
-			defer res.Body.Close()
-			body, _ := ioutil.ReadAll(res.Body)
-			if strings.Contains(string(body), "already exists") || strings.Contains(string(body), "Nothing to update") {
-				log.Printf("Action: %s \t Already exists", desc)
-			} else {
-				log.Printf("Action: %s \t Failed - Status: %v", desc, res.Status)
-				log.Printf("Resp: %s", body)
-				log.Fatalf("Quitting")
-			}
-		}
-	}
-}
-
-type release struct {
-	Version         string    `json:"version"`
-	ReleaseDate     time.Time `json:"releaseDate"`
-	Stable          bool      `json:"stable"`
-	Beta            bool      `json:"beta"`
-	Nightly         bool      `json:"nightly"`
-	WhatsNewURL     string    `json:"whatsNewUrl"`
-	ReleaseNotesURL string    `json:"releaseNotesUrl"`
-	Builds          []build   `json:"-"`
-}
-
-type build struct {
-	Os     string `json:"os"`
-	URL    string `json:"url"`
-	Sha256 string `json:"sha256"`
-	Arch   string `json:"arch"`
-}

+ 10 - 0
scripts/build/release_publisher/publisher.go

@@ -127,11 +127,21 @@ var completeBuildArtifactConfigurations = []buildArtifact{
 		arch:       "armv7",
 		urlPostfix: "_armhf.deb",
 	},
+	{
+		os:         "deb",
+		arch:       "armv6",
+		urlPostfix: "_armel.deb",
+	},
 	{
 		os:         "rhel",
 		arch:       "armv7",
 		urlPostfix: ".armhfp.rpm",
 	},
+	{
+		os:         "linux",
+		arch:       "armv6",
+		urlPostfix: ".linux-armv6.tar.gz",
+	},
 	{
 		os:         "linux",
 		arch:       "armv7",