ryan 6 سال پیش
والد
کامیت
152aa76648
50فایلهای تغییر یافته به همراه744 افزوده شده و 277 حذف شده
  1. 1 0
      conf/defaults.ini
  2. 1 0
      conf/sample.ini
  3. 11 3
      docs/sources/installation/configuration.md
  4. 2 0
      packages/grafana-ui/package.json
  5. 3 3
      packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx
  6. 2 2
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx
  7. 4 4
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx
  8. 1 1
      packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss
  9. 25 0
      packages/grafana-ui/src/components/Table/TableInputCSV.story.tsx
  10. 22 0
      packages/grafana-ui/src/components/Table/TableInputCSV.test.tsx
  11. 95 0
      packages/grafana-ui/src/components/Table/TableInputCSV.tsx
  12. 24 0
      packages/grafana-ui/src/components/Table/_TableInputCSV.scss
  13. 1 0
      packages/grafana-ui/src/components/index.scss
  14. 33 47
      packages/grafana-ui/src/themes/_variables.scss.tmpl.ts
  15. 14 11
      packages/grafana-ui/src/themes/default.ts
  16. 5 2
      packages/grafana-ui/src/types/theme.ts
  17. 66 0
      packages/grafana-ui/src/utils/__snapshots__/processTableData.test.ts.snap
  18. 20 0
      packages/grafana-ui/src/utils/processTableData.test.ts
  19. 133 0
      packages/grafana-ui/src/utils/processTableData.ts
  20. 1 0
      pkg/api/login.go
  21. 1 1
      pkg/services/alerting/test_notification.go
  22. 6 1
      pkg/services/sqlstore/datasource.go
  23. 2 0
      pkg/setting/setting.go
  24. 5 4
      pkg/tsdb/cloudwatch/cloudwatch.go
  25. 1 1
      pkg/tsdb/elasticsearch/client/index_pattern.go
  26. 9 0
      pkg/tsdb/elasticsearch/client/index_pattern_test.go
  27. 2 2
      public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx
  28. 0 16
      public/app/core/components/sidemenu/SideMenu.test.tsx
  29. 5 15
      public/app/core/components/sidemenu/SideMenu.tsx
  30. 3 3
      public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap
  31. 1 0
      public/app/core/config.ts
  32. 1 0
      public/app/core/controllers/login_ctrl.ts
  33. 3 2
      public/app/core/logs_model.ts
  34. 0 9
      public/app/core/services/context_srv.ts
  35. 28 0
      public/app/core/specs/logs_model.test.ts
  36. 14 3
      public/app/features/explore/LogLabels.tsx
  37. 1 1
      public/app/features/explore/LogRow.tsx
  38. 108 0
      public/app/features/explore/state/selectors.test.ts
  39. 1 1
      public/app/partials/login.html
  40. 3 19
      public/app/routes/GrafanaCtrl.ts
  41. 14 28
      public/sass/_variables.generated.scss
  42. 0 23
      public/sass/base/_type.scss
  43. 2 8
      public/sass/components/_navbar.scss
  44. 25 4
      public/sass/components/_panel_logs.scss
  45. 16 18
      public/sass/components/_sidemenu.scss
  46. 15 0
      public/sass/components/_view_states.scss
  47. 0 9
      public/sass/mixins/_mixins.scss
  48. 1 35
      public/sass/pages/_explore.scss
  49. 1 1
      scripts/cli/utils/useSpinner.ts
  50. 12 0
      yarn.lock

+ 1 - 0
conf/defaults.ini

@@ -231,6 +231,7 @@ verify_email_enabled = false
 
 # Background text for the user field on the login page
 login_hint = email or username
+password_hint = password
 
 # Default UI theme ("dark" or "light")
 default_theme = dark

+ 1 - 0
conf/sample.ini

@@ -211,6 +211,7 @@ log_queries =
 
 # Background text for the user field on the login page
 ;login_hint = email or username
+;password_hint = password
 
 # Default UI theme ("dark" or "light")
 ;default_theme = dark

+ 11 - 3
docs/sources/installation/configuration.md

@@ -162,9 +162,9 @@ executed with working directory set to the installation path.
 
 ### enable_gzip
 
-Set this option to `true` to enable HTTP compression, this can improve 
-transfer speed and bandwidth utilization. It is recommended that most 
-users set it to `true`. By default it is set to `false` for compatibility 
+Set this option to `true` to enable HTTP compression, this can improve
+transfer speed and bandwidth utilization. It is recommended that most
+users set it to `true`. By default it is set to `false` for compatibility
 reasons.
 
 ### cert_file
@@ -342,6 +342,14 @@ options are `Admin` and `Editor`. e.g. :
 Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
 Defaults to `false`.
 
+### login_hint
+
+Text used as placeholder text on login page for login/username input.
+
+### password_hint
+
+Text used as placeholder text on login page for password input.
+
 <hr>
 
 ## [auth]

+ 2 - 0
packages/grafana-ui/package.json

@@ -24,6 +24,7 @@
     "jquery": "^3.2.1",
     "lodash": "^4.17.10",
     "moment": "^2.22.2",
+    "papaparse": "^4.6.3",
     "react": "^16.6.3",
     "react-color": "^2.17.0",
     "react-custom-scrollbars": "^4.2.1",
@@ -46,6 +47,7 @@
     "@types/jquery": "^1.10.35",
     "@types/lodash": "^4.14.119",
     "@types/node": "^10.12.18",
+    "@types/papaparse": "^4.5.9",
     "@types/react": "^16.7.6",
     "@types/react-custom-scrollbars": "^4.0.5",
     "@types/react-test-renderer": "^16.0.3",

+ 3 - 3
packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx

@@ -39,7 +39,7 @@ class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
     this.props.onChange(color);
   };
 
-  handleChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+  onChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
     const newColor = tinycolor(event.currentTarget.value);
 
     this.setState({
@@ -51,7 +51,7 @@ class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
     }
   };
 
-  handleBlur = () => {
+  onBlur = () => {
     const newColor = tinycolor(this.state.value);
 
     if (!newColor.isValid()) {
@@ -84,7 +84,7 @@ class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
             flexGrow: 1,
           }}
         >
-          <input className="gf-form-input" value={value} onChange={this.handleChange} onBlur={this.handleBlur} />
+          <input className="gf-form-input" value={value} onChange={this.onChange} onBlur={this.onBlur} />
         </div>
       </div>
     );

+ 2 - 2
packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx

@@ -15,7 +15,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
     static displayName = displayName;
     pickerTriggerRef = createRef<HTMLDivElement>();
 
-    handleColorChange = (color: string) => {
+    onColorChange = (color: string) => {
       const { onColorChange, onChange } = this.props;
       const changeHandler = (onColorChange || onChange) as ColorPickerChangeHandler;
 
@@ -25,7 +25,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
     render() {
       const popoverElement = React.createElement(popover, {
         ...this.props,
-        onChange: this.handleColorChange,
+        onChange: this.onColorChange,
       });
       const { theme, children } = this.props;
 

+ 4 - 4
packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx

@@ -60,7 +60,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
     changeHandler(getColorFromHexRgbOrName(color, theme.type));
   };
 
-  handleTabChange = (tab: PickerType | keyof T) => {
+  onTabChange = (tab: PickerType | keyof T) => {
     return () => this.setState({ activePicker: tab });
   };
 
@@ -104,7 +104,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
       <>
         {Object.keys(customPickers).map(key => {
           return (
-            <div className={this.getTabClassName(key)} onClick={this.handleTabChange(key)} key={key}>
+            <div className={this.getTabClassName(key)} onClick={this.onTabChange(key)} key={key}>
               {customPickers[key].name}
             </div>
           );
@@ -119,10 +119,10 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
     return (
       <div className={`ColorPickerPopover ColorPickerPopover--${colorPickerTheme}`}>
         <div className="ColorPickerPopover__tabs">
-          <div className={this.getTabClassName('palette')} onClick={this.handleTabChange('palette')}>
+          <div className={this.getTabClassName('palette')} onClick={this.onTabChange('palette')}>
             Colors
           </div>
-          <div className={this.getTabClassName('spectrum')} onClick={this.handleTabChange('spectrum')}>
+          <div className={this.getTabClassName('spectrum')} onClick={this.onTabChange('spectrum')}>
             Custom
           </div>
           {this.renderCustomPickerTabs()}

+ 1 - 1
packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss

@@ -53,7 +53,7 @@
 }
 
 .panel-options-group__title {
-  font-size: 1.1rem;
+  font-size: 16px;
   position: relative;
   top: 1px;
 }

+ 25 - 0
packages/grafana-ui/src/components/Table/TableInputCSV.story.tsx

@@ -0,0 +1,25 @@
+import React from 'react';
+
+import { storiesOf } from '@storybook/react';
+import TableInputCSV from './TableInputCSV';
+import { action } from '@storybook/addon-actions';
+import { TableData } from '../../types/data';
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+
+const TableInputStories = storiesOf('UI/Table/Input', module);
+
+TableInputStories.addDecorator(withCenteredStory);
+
+TableInputStories.add('default', () => {
+  return (
+    <div style={{ width: '90%', height: '90vh' }}>
+      <TableInputCSV
+        text={'a,b,c\n1,2,3'}
+        onTableParsed={(table: TableData, text: string) => {
+          console.log('Table', table, text);
+          action('Table')(table, text);
+        }}
+      />
+    </div>
+  );
+});

+ 22 - 0
packages/grafana-ui/src/components/Table/TableInputCSV.test.tsx

@@ -0,0 +1,22 @@
+import React from 'react';
+
+import renderer from 'react-test-renderer';
+import TableInputCSV from './TableInputCSV';
+import { TableData } from '../../types/data';
+
+describe('TableInputCSV', () => {
+  it('renders correctly', () => {
+    const tree = renderer
+      .create(
+        <TableInputCSV
+          text={'a,b,c\n1,2,3'}
+          onTableParsed={(table: TableData, text: string) => {
+            // console.log('Table:', table, 'from:', text);
+          }}
+        />
+      )
+      .toJSON();
+    //expect(tree).toMatchSnapshot();
+    expect(tree).toBeDefined();
+  });
+});

+ 95 - 0
packages/grafana-ui/src/components/Table/TableInputCSV.tsx

@@ -0,0 +1,95 @@
+import React from 'react';
+import debounce from 'lodash/debounce';
+import { parseCSV, TableParseOptions, TableParseDetails } from '../../utils/processTableData';
+import { TableData } from '../../types/data';
+import { AutoSizer } from 'react-virtualized';
+
+interface Props {
+  options?: TableParseOptions;
+  text: string;
+  onTableParsed: (table: TableData, text: string) => void;
+}
+
+interface State {
+  text: string;
+  table: TableData;
+  details: TableParseDetails;
+}
+
+/**
+ * Expects the container div to have size set and will fill it 100%
+ */
+class TableInputCSV extends React.PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+
+    // Shoud this happen in onComponentMounted?
+    const { text, options, onTableParsed } = props;
+    const details = {};
+    const table = parseCSV(text, options, details);
+    this.state = {
+      text,
+      table,
+      details,
+    };
+    onTableParsed(table, text);
+  }
+
+  readCSV = debounce(() => {
+    const details = {};
+    const table = parseCSV(this.state.text, this.props.options, details);
+    this.setState({ table, details });
+  }, 150);
+
+  componentDidUpdate(prevProps: Props, prevState: State) {
+    const { text } = this.state;
+    if (text !== prevState.text || this.props.options !== prevProps.options) {
+      this.readCSV();
+    }
+    // If the props text has changed, replace our local version
+    if (this.props.text !== prevProps.text && this.props.text !== text) {
+      this.setState({ text: this.props.text });
+    }
+
+    if (this.state.table !== prevState.table) {
+      this.props.onTableParsed(this.state.table, this.state.text);
+    }
+  }
+
+  onFooterClicked = (event: any) => {
+    console.log('Errors', this.state);
+    const message = this.state.details
+      .errors!.map(err => {
+        return err.message;
+      })
+      .join('\n');
+    alert('CSV Parsing Errors:\n' + message);
+  };
+
+  onTextChange = (event: any) => {
+    this.setState({ text: event.target.value });
+  };
+
+  render() {
+    const { table, details } = this.state;
+
+    const hasErrors = details.errors && details.errors.length > 0;
+    const footerClassNames = hasErrors ? 'gf-table-input-csv-err' : '';
+
+    return (
+      <AutoSizer>
+        {({ height, width }) => (
+          <div className="gf-table-input-csv" style={{ width, height }}>
+            <textarea placeholder="Enter CSV here..." value={this.state.text} onChange={this.onTextChange} />
+            <footer onClick={this.onFooterClicked} className={footerClassNames}>
+              Rows:{table.rows.length}, Columns:{table.columns.length} &nbsp;
+              {hasErrors ? <i className="fa fa-exclamation-triangle" /> : <i className="fa fa-check-circle" />}
+            </footer>
+          </div>
+        )}
+      </AutoSizer>
+    );
+  }
+}
+
+export default TableInputCSV;

+ 24 - 0
packages/grafana-ui/src/components/Table/_TableInputCSV.scss

@@ -0,0 +1,24 @@
+.gf-table-input-csv {
+  position: relative;
+}
+
+.gf-table-input-csv textarea {
+  height: 100%;
+  width: 100%;
+  resize: none;
+}
+
+.gf-table-input-csv footer {
+  position: absolute;
+  bottom: 15px;
+  right: 15px;
+  border: 1px solid #222;
+  background: #ccc;
+  padding: 1px 4px;
+  font-size: 80%;
+  cursor: pointer;
+}
+
+.gf-table-input-csv footer.gf-table-input-csv-err {
+  background: yellow;
+}

+ 1 - 0
packages/grafana-ui/src/components/index.scss

@@ -2,6 +2,7 @@
 @import 'DeleteButton/DeleteButton';
 @import 'ThresholdsEditor/ThresholdsEditor';
 @import 'Table/Table';
+@import 'Table/TableInputCSV';
 @import 'Tooltip/Tooltip';
 @import 'Select/Select';
 @import 'PanelOptionsGroup/PanelOptionsGroup';

+ 33 - 47
packages/grafana-ui/src/themes/_variables.scss.tmpl.ts

@@ -17,7 +17,7 @@ $enable-hover-media-query: false !default;
 // Control the default styling of most Bootstrap elements by modifying these
 // variables. Mostly focused on spacing.
 
-$spacer: 1rem !default;
+$spacer: ${theme.spacing.m} !default;
 $spacer-x: $spacer !default;
 $spacer-y: $spacer !default;
 $spacers: (
@@ -46,7 +46,7 @@ $spacers: (
     ),
   ),
 ) !default;
-$border-width: 1px !default;
+$border-width: ${theme.border.width.s} !default;
 
 // Grid breakpoints
 //
@@ -54,11 +54,11 @@ $border-width: 1px !default;
 // adapting to different screen sizes, for use in media queries.
 
 $grid-breakpoints: (
-  xs: 0,
-  sm: 544px,
-  md: 768px,
-  lg: 992px,
-  xl: 1200px,
+  xs: ${theme.breakpoints.xs},
+  sm: ${theme.breakpoints.s},
+  md: ${theme.breakpoints.m},
+  lg: ${theme.breakpoints.l},
+  xl: ${theme.breakpoints.xl},
 ) !default;
 
 // Grid containers
@@ -84,46 +84,32 @@ $enable-flex: true;
 // Typography
 // -------------------------
 
-$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
-$font-family-serif: Georgia, 'Times New Roman', Times, serif;
-$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
+$font-family-sans-serif: ${theme.typography.fontFamily.sansSerif};
+$font-family-monospace: ${theme.typography.fontFamily.monospace};
 $font-family-base: $font-family-sans-serif !default;
 
-$font-size-root: 14px !default;
-$font-size-base: 13px !default;
+$font-size-root: ${theme.typography.size.root} !default;
+$font-size-base: ${theme.typography.size.base} !default;
 
-$font-size-lg: 18px !default;
-$font-size-md: 14px !default;
-$font-size-sm: 12px !default;
-$font-size-xs: 10px !default;
+$font-size-lg: ${theme.typography.size.l} !default;
+$font-size-md: ${theme.typography.size.m} !default;
+$font-size-sm: ${theme.typography.size.s} !default;
+$font-size-xs: ${theme.typography.size.xs} !default;
 
-$line-height-base: 1.5 !default;
-$font-weight-semi-bold: 500;
+$line-height-base: ${theme.typography.lineHeight.l} !default;
+$font-weight-semi-bold: ${theme.typography.weight.semibold};
 
-$font-size-h1: 2rem !default;
-$font-size-h2: 1.75rem !default;
-$font-size-h3: 1.5rem !default;
-$font-size-h4: 1.3rem !default;
-$font-size-h5: 1.2rem !default;
-$font-size-h6: 1rem !default;
-
-$display1-size: 6rem !default;
-$display2-size: 5.5rem !default;
-$display3-size: 4.5rem !default;
-$display4-size: 3.5rem !default;
-
-$display1-weight: 400 !default;
-$display2-weight: 400 !default;
-$display3-weight: 400 !default;
-$display4-weight: 400 !default;
-
-$lead-font-size: 1.25rem !default;
-$lead-font-weight: 300 !default;
+$font-size-h1: ${theme.typography.heading.h1} !default;
+$font-size-h2: ${theme.typography.heading.h2} !default;
+$font-size-h3: ${theme.typography.heading.h3} !default;
+$font-size-h4: ${theme.typography.heading.h4} !default;
+$font-size-h5: ${theme.typography.heading.h5} !default;
+$font-size-h6: ${theme.typography.heading.h6} !default;
 
 $headings-margin-bottom: ($spacer / 2) !default;
 $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-$headings-font-weight: 400 !default;
-$headings-line-height: 1.1 !default;
+$headings-font-weight: ${theme.typography.weight.normal} !default;
+$headings-line-height: ${theme.typography.lineHeight.s} !default;
 
 $hr-border-width: $border-width !default;
 $dt-font-weight: bold !default;
@@ -141,8 +127,8 @@ $border-radius-sm: 2px !default;
 
 // Page
 
-$page-sidebar-width: 11rem;
-$page-sidebar-margin: 4rem;
+$page-sidebar-width: 154px;
+$page-sidebar-margin: 56px;
 
 // Links
 // -------------------------
@@ -174,7 +160,7 @@ $input-padding-y-lg: 10px !default;
 
 $input-height: 35px !default;
 
-$gf-form-margin: 0.2rem;
+$gf-form-margin: 3px;
 $gf-form-input-height: 35px;
 
 $cursor-disabled: not-allowed !default;
@@ -199,13 +185,13 @@ $zindex-typeahead: 1060;
 // Buttons
 //
 
-$btn-padding-x: 1rem !default;
-$btn-padding-y: 0.7rem !default;
+$btn-padding-x: 14px !default;
+$btn-padding-y: 10px !default;
 $btn-line-height: 1 !default;
-$btn-font-weight: 500 !default;
+$btn-font-weight: ${theme.typography.weight.semibold} !default;
 
-$btn-padding-x-sm: 0.5rem !default;
-$btn-padding-y-sm: 0.25rem !default;
+$btn-padding-x-sm: 7px !default;
+$btn-padding-y-sm: 4px !default;
 
 $btn-padding-x-lg: 21px !default;
 $btn-padding-y-lg: 11px !default;

+ 14 - 11
packages/grafana-ui/src/themes/default.ts

@@ -5,10 +5,10 @@ const theme: GrafanaThemeCommons = {
   typography: {
     fontFamily: {
       sansSerif: "'Roboto', Helvetica, Arial, sans-serif",
-      serif: "Georgia, 'Times New Roman', Times, serif",
       monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace",
     },
     size: {
+      root: '14px',
       base: '13px',
       xs: '10px',
       s: '12px',
@@ -16,12 +16,12 @@ const theme: GrafanaThemeCommons = {
       l: '18px',
     },
     heading: {
-      h1: '2rem',
-      h2: '1.75rem',
-      h3: '1.5rem',
-      h4: '1.3rem',
-      h5: '1.2rem',
-      h6: '1rem',
+      h1: '28px',
+      h2: '24px',
+      h3: '21px',
+      h4: '18px',
+      h5: '16px',
+      h6: '14px',
     },
     weight: {
       light: 300,
@@ -35,7 +35,7 @@ const theme: GrafanaThemeCommons = {
       l: 1.5,
     },
   },
-  brakpoints: {
+  breakpoints: {
     xs: '0',
     s: '544px',
     m: '768px',
@@ -44,9 +44,9 @@ const theme: GrafanaThemeCommons = {
   },
   spacing: {
     xs: '0',
-    s: '0.2rem',
-    m: '1rem',
-    l: '1.5rem',
+    s: '3px',
+    m: '14px',
+    l: '21px',
     gutter: '30px',
   },
   border: {
@@ -55,6 +55,9 @@ const theme: GrafanaThemeCommons = {
       s: '3px',
       m: '5px',
     },
+    width: {
+      s: '1px',
+    },
   },
 };
 

+ 5 - 2
packages/grafana-ui/src/types/theme.ts

@@ -6,7 +6,7 @@ export enum GrafanaThemeType {
 export interface GrafanaThemeCommons {
   name: string;
   // TODO: not sure if should be a part of theme
-  brakpoints: {
+  breakpoints: {
     xs: string;
     s: string;
     m: string;
@@ -16,10 +16,10 @@ export interface GrafanaThemeCommons {
   typography: {
     fontFamily: {
       sansSerif: string;
-      serif: string;
       monospace: string;
     };
     size: {
+      root: string;
       base: string;
       xs: string;
       s: string;
@@ -60,6 +60,9 @@ export interface GrafanaThemeCommons {
       s: string;
       m: string;
     };
+    width: {
+      s: string;
+    };
   };
 }
 

+ 66 - 0
packages/grafana-ui/src/utils/__snapshots__/processTableData.test.ts.snap

@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`processTableData basic processing should generate a header and fix widths 1`] = `
+Object {
+  "columnMap": Object {},
+  "columns": Array [
+    Object {
+      "text": "Column 1",
+    },
+    Object {
+      "text": "Column 2",
+    },
+    Object {
+      "text": "Column 3",
+    },
+  ],
+  "rows": Array [
+    Array [
+      1,
+      null,
+      null,
+    ],
+    Array [
+      2,
+      3,
+      4,
+    ],
+    Array [
+      5,
+      6,
+      null,
+    ],
+  ],
+  "type": "table",
+}
+`;
+
+exports[`processTableData basic processing should read header and two rows 1`] = `
+Object {
+  "columnMap": Object {},
+  "columns": Array [
+    Object {
+      "text": "a",
+    },
+    Object {
+      "text": "b",
+    },
+    Object {
+      "text": "c",
+    },
+  ],
+  "rows": Array [
+    Array [
+      1,
+      2,
+      3,
+    ],
+    Array [
+      4,
+      5,
+      6,
+    ],
+  ],
+  "type": "table",
+}
+`;

+ 20 - 0
packages/grafana-ui/src/utils/processTableData.test.ts

@@ -0,0 +1,20 @@
+import { parseCSV } from './processTableData';
+
+describe('processTableData', () => {
+  describe('basic processing', () => {
+    it('should read header and two rows', () => {
+      const text = 'a,b,c\n1,2,3\n4,5,6';
+      expect(parseCSV(text)).toMatchSnapshot();
+    });
+
+    it('should generate a header and fix widths', () => {
+      const text = '1\n2,3,4\n5,6';
+      const table = parseCSV(text, {
+        headerIsFirstLine: false,
+      });
+      expect(table.rows.length).toBe(3);
+
+      expect(table).toMatchSnapshot();
+    });
+  });
+});

+ 133 - 0
packages/grafana-ui/src/utils/processTableData.ts

@@ -0,0 +1,133 @@
+import { TableData, Column } from '../types/index';
+
+import Papa, { ParseError, ParseMeta } from 'papaparse';
+
+// Subset of all parse options
+export interface TableParseOptions {
+  headerIsFirstLine?: boolean; // Not a papa-parse option
+  delimiter?: string; // default: ","
+  newline?: string; // default: "\r\n"
+  quoteChar?: string; // default: '"'
+  encoding?: string; // default: ""
+  comments?: boolean | string; // default: false
+}
+
+export interface TableParseDetails {
+  meta?: ParseMeta;
+  errors?: ParseError[];
+}
+
+/**
+ * This makes sure the header and all rows have equal length.
+ *
+ * @param table (immutable)
+ * @returns a new table that has equal length rows, or the same
+ * table if no changes were needed
+ */
+export function matchRowSizes(table: TableData): TableData {
+  const { rows } = table;
+  let { columns } = table;
+
+  let sameSize = true;
+  let size = columns.length;
+  rows.forEach(row => {
+    if (size !== row.length) {
+      sameSize = false;
+      size = Math.max(size, row.length);
+    }
+  });
+  if (sameSize) {
+    return table;
+  }
+
+  // Pad Columns
+  if (size !== columns.length) {
+    const diff = size - columns.length;
+    columns = [...columns];
+    for (let i = 0; i < diff; i++) {
+      columns.push({
+        text: 'Column ' + (columns.length + 1),
+      });
+    }
+  }
+
+  // Pad Rows
+  const fixedRows: any[] = [];
+  rows.forEach(row => {
+    const diff = size - row.length;
+    if (diff > 0) {
+      row = [...row];
+      for (let i = 0; i < diff; i++) {
+        row.push(null);
+      }
+    }
+    fixedRows.push(row);
+  });
+
+  return {
+    columns,
+    rows: fixedRows,
+    type: table.type,
+    columnMap: table.columnMap,
+  };
+}
+
+function makeColumns(values: any[]): Column[] {
+  return values.map((value, index) => {
+    if (!value) {
+      value = 'Column ' + (index + 1);
+    }
+    return {
+      text: value.toString().trim(),
+    };
+  });
+}
+
+/**
+ * Convert CSV text into a valid TableData object
+ *
+ * @param text
+ * @param options
+ * @param details, if exists the result will be filled with debugging details
+ */
+export function parseCSV(text: string, options?: TableParseOptions, details?: TableParseDetails): TableData {
+  const results = Papa.parse(text, { ...options, dynamicTyping: true, skipEmptyLines: true });
+  const { data, meta, errors } = results;
+
+  // Fill the parse details for debugging
+  if (details) {
+    details.errors = errors;
+    details.meta = meta;
+  }
+
+  if (!data || data.length < 1) {
+    // Show a more reasonable warning on empty input text
+    if (details && !text) {
+      errors.length = 0;
+      errors.push({
+        code: 'empty',
+        message: 'Empty input text',
+        type: 'warning',
+        row: 0,
+      });
+      details.errors = errors;
+    }
+    return {
+      columns: [],
+      rows: [],
+      type: 'table',
+      columnMap: {},
+    };
+  }
+
+  // Assume the first line is the header unless the config says its not
+  const headerIsNotFirstLine = options && options.headerIsFirstLine === false;
+  const header = headerIsNotFirstLine ? [] : results.data.shift();
+
+  return matchRowSizes({
+    columns: makeColumns(header),
+    rows: results.data,
+    type: 'table',
+    columnMap: {},
+  });
+}

+ 1 - 0
pkg/api/login.go

@@ -36,6 +36,7 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
 	viewData.Settings["oauth"] = enabledOAuths
 	viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
 	viewData.Settings["loginHint"] = setting.LoginHint
+	viewData.Settings["passwordHint"] = setting.PasswordHint
 	viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
 
 	if loginError, ok := tryGetEncryptedCookie(c, LoginErrorCookieName); ok {

+ 1 - 1
pkg/services/alerting/test_notification.go

@@ -56,7 +56,7 @@ func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext {
 
 	ctx := NewEvalContext(context.Background(), testRule)
 	if cmd.Settings.Get("uploadImage").MustBool(true) {
-		ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
+		ctx.ImagePublicUrl = "https://grafana.com/assets/img/blog/mixed_styles.png"
 	}
 	ctx.IsTestRun = true
 	ctx.Firing = true

+ 6 - 1
pkg/services/sqlstore/datasource.go

@@ -174,6 +174,11 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
 			Version:           cmd.Version + 1,
 		}
 
+		sess.UseBool("is_default")
+		sess.UseBool("basic_auth")
+		sess.UseBool("with_credentials")
+		sess.UseBool("read_only")
+
 		var updateSession *xorm.Session
 		if cmd.Version != 0 {
 			// the reason we allow cmd.version > db.version is make it possible for people to force
@@ -185,7 +190,7 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
 			updateSession = sess.Where("id=? and org_id=?", ds.Id, ds.OrgId)
 		}
 
-		affected, err := updateSession.AllCols().Omit("created").Update(ds)
+		affected, err := updateSession.Update(ds)
 		if err != nil {
 			return err
 		}

+ 2 - 0
pkg/setting/setting.go

@@ -109,6 +109,7 @@ var (
 	AutoAssignOrgRole       string
 	VerifyEmailEnabled      bool
 	LoginHint               string
+	PasswordHint            string
 	DefaultTheme            string
 	DisableLoginForm        bool
 	DisableSignoutMenu      bool
@@ -656,6 +657,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"})
 	VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
 	LoginHint = users.Key("login_hint").String()
+	PasswordHint = users.Key("password_hint").String()
 	DefaultTheme = users.Key("default_theme").String()
 	ExternalUserMngLinkUrl = users.Key("external_manage_link_url").String()
 	ExternalUserMngLinkName = users.Key("external_manage_link_name").String()

+ 5 - 4
pkg/tsdb/cloudwatch/cloudwatch.go

@@ -496,9 +496,6 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
 	}
 
 	alias := model.Get("alias").MustString()
-	if alias == "" {
-		alias = "{{metric}}_{{stat}}"
-	}
 
 	returnData := model.Get("returnData").MustBool(false)
 	highResolution := model.Get("highResolution").MustBool(false)
@@ -521,7 +518,11 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
 
 func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]string) string {
 	if len(query.Id) > 0 && len(query.Expression) > 0 {
-		return query.Id
+		if len(query.Alias) > 0 {
+			return query.Alias
+		} else {
+			return query.Id
+		}
 	}
 
 	data := map[string]string{}

+ 1 - 1
pkg/tsdb/elasticsearch/client/index_pattern.go

@@ -279,7 +279,7 @@ func formatDate(t time.Time, pattern string) string {
 		isoYearShort := fmt.Sprintf("%d", isoYear)[2:4]
 		formatted = strings.Replace(formatted, "<stdIsoYear>", fmt.Sprintf("%d", isoYear), -1)
 		formatted = strings.Replace(formatted, "<stdIsoYearShort>", isoYearShort, -1)
-		formatted = strings.Replace(formatted, "<stdWeekOfYear>", fmt.Sprintf("%d", isoWeek), -1)
+		formatted = strings.Replace(formatted, "<stdWeekOfYear>", fmt.Sprintf("%02d", isoWeek), -1)
 
 		formatted = strings.Replace(formatted, "<stdUnix>", fmt.Sprintf("%d", t.Unix()), -1)
 

+ 9 - 0
pkg/tsdb/elasticsearch/client/index_pattern_test.go

@@ -76,6 +76,15 @@ func TestIndexPattern(t *testing.T) {
 			So(indices, ShouldHaveLength, 1)
 			So(indices[0], ShouldEqual, "2018-data")
 		})
+
+		Convey("Should return 01 week", func() {
+			from = fmt.Sprintf("%d", time.Date(2018, 1, 15, 17, 50, 0, 0, time.UTC).UnixNano()/int64(time.Millisecond))
+			to = fmt.Sprintf("%d", time.Date(2018, 1, 15, 17, 55, 0, 0, time.UTC).UnixNano()/int64(time.Millisecond))
+			indexPatternScenario(intervalWeekly, "[data-]GGGG.WW", tsdb.NewTimeRange(from, to), func(indices []string) {
+				So(indices, ShouldHaveLength, 1)
+				So(indices[0], ShouldEqual, "data-2018.03")
+			})
+		})
 	})
 
 	Convey("Hourly interval", t, func() {

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

@@ -37,7 +37,7 @@ export const ToggleButton: FC<ToggleButtonProps> = ({
   tooltip,
   onChange,
 }) => {
-  const handleChange = event => {
+  const onClick = event => {
     event.stopPropagation();
     if (onChange) {
       onChange(value);
@@ -46,7 +46,7 @@ export const ToggleButton: FC<ToggleButtonProps> = ({
 
   const btnClassName = `btn ${className} ${selected ? 'active' : ''}`;
   const button = (
-    <button className={btnClassName} onClick={handleChange}>
+    <button className={btnClassName} onClick={onClick}>
       <span>{children}</span>
     </button>
   );

+ 0 - 16
public/app/core/components/sidemenu/SideMenu.test.tsx

@@ -2,7 +2,6 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import { SideMenu } from './SideMenu';
 import appEvents from '../../app_events';
-import { contextSrv } from 'app/core/services/context_srv';
 
 jest.mock('../../app_events', () => ({
   emit: jest.fn(),
@@ -26,7 +25,6 @@ jest.mock('app/core/services/context_srv', () => ({
     isGrafanaAdmin: false,
     isEditor: false,
     hasEditPermissionFolders: false,
-    toggleSideMenu: jest.fn(),
   },
 }));
 
@@ -54,20 +52,6 @@ describe('Render', () => {
 });
 
 describe('Functions', () => {
-  describe('toggle side menu', () => {
-    const wrapper = setup();
-    const instance = wrapper.instance() as SideMenu;
-    instance.toggleSideMenu();
-
-    it('should call contextSrv.toggleSideMenu', () => {
-      expect(contextSrv.toggleSideMenu).toHaveBeenCalled();
-    });
-
-    it('should emit toggle sidemenu event', () => {
-      expect(appEvents.emit).toHaveBeenCalledWith('toggle-sidemenu');
-    });
-  });
-
   describe('toggle side menu on mobile', () => {
     const wrapper = setup();
     const instance = wrapper.instance() as SideMenu;

+ 5 - 15
public/app/core/components/sidemenu/SideMenu.tsx

@@ -1,31 +1,21 @@
 import React, { PureComponent } from 'react';
 import appEvents from '../../app_events';
-import { contextSrv } from 'app/core/services/context_srv';
 import TopSection from './TopSection';
 import BottomSection from './BottomSection';
-import { store } from 'app/store/store';
+import config from 'app/core/config';
 
-export class SideMenu extends PureComponent {
-  toggleSideMenu = () => {
-    // ignore if we just made a location change, stops hiding sidemenu on double clicks of back button
-    const timeSinceLocationChanged = new Date().getTime() - store.getState().location.lastUpdated;
-    if (timeSinceLocationChanged < 1000) {
-      return;
-    }
-
-    contextSrv.toggleSideMenu();
-    appEvents.emit('toggle-sidemenu');
-  };
+const homeUrl = config.appSubUrl || '/';
 
+export class SideMenu extends PureComponent {
   toggleSideMenuSmallBreakpoint = () => {
     appEvents.emit('toggle-sidemenu-mobile');
   };
 
   render() {
     return [
-      <div className="sidemenu__logo" onClick={this.toggleSideMenu} key="logo">
+      <a href={homeUrl} className="sidemenu__logo" key="logo">
         <img src="public/img/grafana_icon.svg" alt="Grafana" />
-      </div>,
+      </a>,
       <div className="sidemenu__logo_small_breakpoint" onClick={this.toggleSideMenuSmallBreakpoint} key="hamburger">
         <i className="fa fa-bars" />
         <span className="sidemenu__close">

+ 3 - 3
public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap

@@ -2,16 +2,16 @@
 
 exports[`Render should render component 1`] = `
 Array [
-  <div
+  <a
     className="sidemenu__logo"
+    href="/"
     key="logo"
-    onClick={[Function]}
   >
     <img
       alt="Grafana"
       src="public/img/grafana_icon.svg"
     />
-  </div>,
+  </a>,
   <div
     className="sidemenu__logo_small_breakpoint"
     key="hamburger"

+ 1 - 0
public/app/core/config.ts

@@ -34,6 +34,7 @@ export class Settings {
   oauth: any;
   disableUserSignUp: boolean;
   loginHint: any;
+  passwordHint: any;
   loginError: any;
   viewersCanEdit: boolean;
   editorsCanOwn: boolean;

+ 1 - 0
public/app/core/controllers/login_ctrl.ts

@@ -25,6 +25,7 @@ export class LoginCtrl {
     $scope.disableLoginForm = config.disableLoginForm;
     $scope.disableUserSignUp = config.disableUserSignUp;
     $scope.loginHint = config.loginHint;
+    $scope.passwordHint = config.passwordHint;
 
     $scope.loginMode = true;
     $scope.submitBtnText = 'Log in';

+ 3 - 2
public/app/core/logs_model.ts

@@ -245,12 +245,13 @@ export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): Logs
   }
 
   const dedupedRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => {
+    const rowCopy = { ...row };
     const previous = result[result.length - 1];
     if (index > 0 && isDuplicateRow(row, previous, strategy)) {
       previous.duplicates++;
     } else {
-      row.duplicates = 0;
-      result.push(row);
+      rowCopy.duplicates = 0;
+      result.push(rowCopy);
     }
     return result;
   }, []);

+ 0 - 9
public/app/core/services/context_srv.ts

@@ -1,7 +1,6 @@
 import config from 'app/core/config';
 import _ from 'lodash';
 import coreModule from 'app/core/core_module';
-import store from 'app/core/store';
 
 export class User {
   isGrafanaAdmin: any;
@@ -29,13 +28,10 @@ export class ContextSrv {
   isSignedIn: any;
   isGrafanaAdmin: any;
   isEditor: any;
-  sidemenu: any;
   sidemenuSmallBreakpoint = false;
   hasEditPermissionInFolders: boolean;
 
   constructor() {
-    this.sidemenu = store.getBool('grafana.sidemenu', true);
-
     if (!config.bootData) {
       config.bootData = { user: {}, settings: {} };
     }
@@ -55,11 +51,6 @@ export class ContextSrv {
     return !!(document.visibilityState === undefined || document.visibilityState === 'visible');
   }
 
-  toggleSideMenu() {
-    this.sidemenu = !this.sidemenu;
-    store.set('grafana.sidemenu', this.sidemenu);
-  }
-
   hasAccessToExplore() {
     return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled;
   }

+ 28 - 0
public/app/core/specs/logs_model.test.ts

@@ -113,6 +113,34 @@ describe('dedupLogRows()', () => {
       },
     ]);
   });
+
+  test('should return to non-deduped state on same log result', () => {
+    const logs = {
+      rows: [
+        {
+          entry: 'INFO 123',
+        },
+        {
+          entry: 'WARN 123',
+        },
+        {
+          entry: 'WARN 123',
+        },
+      ],
+    };
+    expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.exact).rows).toEqual([
+      {
+        duplicates: 0,
+        entry: 'INFO 123',
+      },
+      {
+        duplicates: 1,
+        entry: 'WARN 123',
+      },
+    ]);
+
+    expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.none).rows).toEqual(logs.rows);
+  });
 });
 
 describe('calculateFieldStats()', () => {

+ 14 - 3
public/app/features/explore/LogLabels.tsx

@@ -13,8 +13,19 @@ interface Props {
 export class LogLabels extends PureComponent<Props> {
   render() {
     const { getRows, labels, onClickLabel, plain } = this.props;
-    return Object.keys(labels).map(key => (
-      <LogLabel key={key} getRows={getRows} label={key} value={labels[key]} plain={plain} onClickLabel={onClickLabel} />
-    ));
+    return (
+      <span className="logs-labels">
+        {Object.keys(labels).map(key => (
+          <LogLabel
+            key={key}
+            getRows={getRows}
+            label={key}
+            value={labels[key]}
+            plain={plain}
+            onClickLabel={onClickLabel}
+          />
+        ))}
+      </span>
+    );
   }
 }

+ 1 - 1
public/app/features/explore/LogRow.tsx

@@ -150,7 +150,7 @@ export class LogRow extends PureComponent<Props, State> {
           </div>
         )}
         {showLocalTime && (
-          <div className="logs-row__time" title={`${row.timestamp} (${row.timeFromNow})`}>
+          <div className="logs-row__localtime" title={`${row.timestamp} (${row.timeFromNow})`}>
             {row.timeLocal}
           </div>
         )}

+ 108 - 0
public/app/features/explore/state/selectors.test.ts

@@ -0,0 +1,108 @@
+import { deduplicatedLogsSelector } from './selectors';
+import { LogsDedupStrategy } from 'app/core/logs_model';
+import { ExploreItemState } from 'app/types';
+
+const state = {
+  logsResult: {
+    rows: [
+      {
+        entry: '2019-03-05T11:00:56Z sntpc sntpc[1]: offset=-0.033938, delay=0.000649',
+      },
+      {
+        entry: '2019-03-05T11:00:26Z sntpc sntpc[1]: offset=-0.033730, delay=0.000581',
+      },
+      {
+        entry: '2019-03-05T10:59:56Z sntpc sntpc[1]: offset=-0.034184, delay=0.001089',
+      },
+      {
+        entry: '2019-03-05T10:59:26Z sntpc sntpc[1]: offset=-0.033972, delay=0.000582',
+      },
+      {
+        entry: '2019-03-05T10:58:56Z sntpc sntpc[1]: offset=-0.033955, delay=0.000606',
+      },
+      {
+        entry: '2019-03-05T10:58:26Z sntpc sntpc[1]: offset=-0.034067, delay=0.000616',
+      },
+      {
+        entry: '2019-03-05T10:57:56Z sntpc sntpc[1]: offset=-0.034155, delay=0.001021',
+      },
+      {
+        entry: '2019-03-05T10:57:26Z sntpc sntpc[1]: offset=-0.035797, delay=0.000883',
+      },
+      {
+        entry: '2019-03-05T10:56:56Z sntpc sntpc[1]: offset=-0.046818, delay=0.000605',
+      },
+      {
+        entry: '2019-03-05T10:56:26Z sntpc sntpc[1]: offset=-0.049200, delay=0.000584',
+      },
+    ],
+  },
+  hiddenLogLevels: undefined,
+  dedupStrategy: LogsDedupStrategy.none,
+};
+
+describe('Deduplication selector', () => {
+  it('should correctly deduplicate log rows when changing strategy multiple times', () => {
+    // Simulating sequence of UI actions that was causing a problem with deduplication counter being visible when unnecessary.
+    // The sequence was changing dedup strategy: (none -> exact -> numbers -> signature -> none) *2 -> exact. After that the first
+    // row contained information that was deduped, while it shouldn't be.
+    // Problem was caused by mutating the log results entries in redux state. The memoisation hash for deduplicatedLogsSelector
+    // was changing depending on duplicates information from log row state, while should be dependand on log row only.
+
+    let dedups = deduplicatedLogsSelector(state as ExploreItemState);
+    expect(dedups.rows.length).toBe(10);
+
+    deduplicatedLogsSelector({
+      ...state,
+      dedupStrategy: LogsDedupStrategy.none,
+    } as ExploreItemState);
+
+    deduplicatedLogsSelector({
+      ...state,
+      dedupStrategy: LogsDedupStrategy.exact,
+    } as ExploreItemState);
+
+    deduplicatedLogsSelector({
+      ...state,
+      dedupStrategy: LogsDedupStrategy.numbers,
+    } as ExploreItemState);
+
+    deduplicatedLogsSelector({
+      ...state,
+      dedupStrategy: LogsDedupStrategy.signature,
+    } as ExploreItemState);
+
+    deduplicatedLogsSelector({
+      ...state,
+      dedupStrategy: LogsDedupStrategy.none,
+    } as ExploreItemState);
+
+    deduplicatedLogsSelector({
+      ...state,
+      dedupStrategy: LogsDedupStrategy.exact,
+    } as ExploreItemState);
+
+    deduplicatedLogsSelector({
+      ...state,
+      dedupStrategy: LogsDedupStrategy.numbers,
+    } as ExploreItemState);
+
+    deduplicatedLogsSelector({
+      ...state,
+      dedupStrategy: LogsDedupStrategy.signature,
+    } as ExploreItemState);
+
+    deduplicatedLogsSelector({
+      ...state,
+      dedupStrategy: LogsDedupStrategy.none,
+    } as ExploreItemState);
+
+    dedups = deduplicatedLogsSelector({
+      ...state,
+      dedupStrategy: LogsDedupStrategy.exact,
+    } as ExploreItemState);
+
+    // Expecting that no row has duplicates now
+    expect(dedups.rows.reduce((acc, row) => acc + row.duplicates, 0)).toBe(0);
+  });
+});

+ 1 - 1
public/app/partials/login.html

@@ -13,7 +13,7 @@
           </div>
           <div class="login-form">
             <input type="password" name="password" class="gf-form-input login-form-input" required ng-model="formModel.password" id="inputPassword"
-              placeholder="password">
+              placeholder="{{passwordHint}}">
           </div>
           <div class="login-button-group">
             <button type="submit" class="btn btn-large p-x-2" ng-if="!loggingIn" ng-click="submit();" ng-class="{'btn-inverse': !loginForm.$valid, 'btn-primary': loginForm.$valid}">

+ 3 - 19
public/app/routes/GrafanaCtrl.ts

@@ -75,27 +75,22 @@ export class GrafanaCtrl {
   }
 }
 
-function setViewModeBodyClass(body: JQuery, mode: KioskUrlValue, sidemenuOpen: boolean) {
+function setViewModeBodyClass(body: JQuery, mode: KioskUrlValue) {
   body.removeClass('view-mode--tv');
   body.removeClass('view-mode--kiosk');
   body.removeClass('view-mode--inactive');
 
   switch (mode) {
     case 'tv': {
-      body.removeClass('sidemenu-open');
       body.addClass('view-mode--tv');
       break;
     }
     // 1 & true for legacy states
     case '1':
     case true: {
-      body.removeClass('sidemenu-open');
       body.addClass('view-mode--kiosk');
       break;
     }
-    default: {
-      body.toggleClass('sidemenu-open', sidemenuOpen);
-    }
   }
 }
 
@@ -105,7 +100,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
     restrict: 'E',
     controller: GrafanaCtrl,
     link: (scope, elem) => {
-      let sidemenuOpen;
       const body = $('body');
 
       // see https://github.com/zenorocha/clipboard.js/issues/155
@@ -113,14 +107,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
 
       $('.preloader').remove();
 
-      sidemenuOpen = scope.contextSrv.sidemenu;
-      body.toggleClass('sidemenu-open', sidemenuOpen);
-
-      appEvents.on('toggle-sidemenu', () => {
-        sidemenuOpen = scope.contextSrv.sidemenu;
-        body.toggleClass('sidemenu-open');
-      });
-
       appEvents.on('toggle-sidemenu-mobile', () => {
         body.toggleClass('sidemenu-open--xs');
       });
@@ -163,7 +149,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
         $('#tooltip, .tooltip').remove();
 
         // check for kiosk url param
-        setViewModeBodyClass(body, data.params.kiosk, sidemenuOpen);
+        setViewModeBodyClass(body, data.params.kiosk);
 
         // close all drops
         for (const drop of Drop.drops) {
@@ -198,7 +184,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
         }
 
         $timeout(() => $location.search(search));
-        setViewModeBodyClass(body, search.kiosk, sidemenuOpen);
+        setViewModeBodyClass(body, search.kiosk);
       });
 
       // handle in active view state class
@@ -218,7 +204,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
         if (new Date().getTime() - lastActivity > inActiveTimeLimit) {
           activeUser = false;
           body.addClass('view-mode--inactive');
-          body.removeClass('sidemenu-open');
         }
       }
 
@@ -227,7 +212,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
         if (!activeUser) {
           activeUser = true;
           body.removeClass('view-mode--inactive');
-          body.toggleClass('sidemenu-open', sidemenuOpen);
         }
       }
 

+ 14 - 28
public/sass/_variables.generated.scss

@@ -20,7 +20,7 @@ $enable-hover-media-query: false !default;
 // Control the default styling of most Bootstrap elements by modifying these
 // variables. Mostly focused on spacing.
 
-$spacer: 1rem !default;
+$spacer: 14px !default;
 $spacer-x: $spacer !default;
 $spacer-y: $spacer !default;
 $spacers: (
@@ -88,7 +88,6 @@ $enable-flex: true;
 // -------------------------
 
 $font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
-$font-family-serif: Georgia, 'Times New Roman', Times, serif;
 $font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
 $font-family-base: $font-family-sans-serif !default;
 
@@ -103,25 +102,12 @@ $font-size-xs: 10px !default;
 $line-height-base: 1.5 !default;
 $font-weight-semi-bold: 500;
 
-$font-size-h1: 2rem !default;
-$font-size-h2: 1.75rem !default;
-$font-size-h3: 1.5rem !default;
-$font-size-h4: 1.3rem !default;
-$font-size-h5: 1.2rem !default;
-$font-size-h6: 1rem !default;
-
-$display1-size: 6rem !default;
-$display2-size: 5.5rem !default;
-$display3-size: 4.5rem !default;
-$display4-size: 3.5rem !default;
-
-$display1-weight: 400 !default;
-$display2-weight: 400 !default;
-$display3-weight: 400 !default;
-$display4-weight: 400 !default;
-
-$lead-font-size: 1.25rem !default;
-$lead-font-weight: 300 !default;
+$font-size-h1: 28px !default;
+$font-size-h2: 24px !default;
+$font-size-h3: 21px !default;
+$font-size-h4: 18px !default;
+$font-size-h5: 16px !default;
+$font-size-h6: 14px !default;
 
 $headings-margin-bottom: ($spacer / 2) !default;
 $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
@@ -144,8 +130,8 @@ $border-radius-sm: 2px !default;
 
 // Page
 
-$page-sidebar-width: 11rem;
-$page-sidebar-margin: 4rem;
+$page-sidebar-width: 154px;
+$page-sidebar-margin: 56px;
 
 // Links
 // -------------------------
@@ -177,7 +163,7 @@ $input-padding-y-lg: 10px !default;
 
 $input-height: 35px !default;
 
-$gf-form-margin: 0.2rem;
+$gf-form-margin: 3px;
 $gf-form-input-height: 35px;
 
 $cursor-disabled: not-allowed !default;
@@ -202,13 +188,13 @@ $zindex-typeahead: 1060;
 // Buttons
 //
 
-$btn-padding-x: 1rem !default;
-$btn-padding-y: 0.7rem !default;
+$btn-padding-x: 14px !default;
+$btn-padding-y: 10px !default;
 $btn-line-height: 1 !default;
 $btn-font-weight: 500 !default;
 
-$btn-padding-x-sm: 0.5rem !default;
-$btn-padding-y-sm: 0.25rem !default;
+$btn-padding-x-sm: 7px !default;
+$btn-padding-y-sm: 4px !default;
 
 $btn-padding-x-lg: 21px !default;
 $btn-padding-y-lg: 11px !default;

+ 0 - 23
public/sass/base/_type.scss

@@ -141,29 +141,6 @@ h6,
   font-size: $font-size-h6;
 }
 
-.lead {
-  font-size: $lead-font-size;
-  font-weight: $lead-font-weight;
-}
-
-// Type display classes
-.display-1 {
-  font-size: $display1-size;
-  font-weight: $display1-weight;
-}
-.display-2 {
-  font-size: $display2-size;
-  font-weight: $display2-weight;
-}
-.display-3 {
-  font-size: $display3-size;
-  font-weight: $display3-weight;
-}
-.display-4 {
-  font-size: $display4-size;
-  font-weight: $display4-weight;
-}
-
 //
 // Horizontal rules
 //

+ 2 - 8
public/sass/components/_navbar.scss

@@ -157,14 +157,8 @@
 
 @include media-breakpoint-up(sm) {
   .navbar {
-    padding-left: 60px;
-  }
-
-  .sidemenu-open {
-    .navbar {
-      padding-left: 25px;
-      margin-left: 0;
-    }
+    padding-left: 20px;
+    margin-left: 0;
   }
 
   .navbar-page-btn {

+ 25 - 4
public/sass/components/_panel_logs.scss

@@ -63,6 +63,7 @@ $column-horizontal-spacing: 10px;
   font-size: $font-size-sm;
   display: table;
   table-layout: fixed;
+  width: 100%;
 }
 
 .logs-row {
@@ -83,16 +84,22 @@ $column-horizontal-spacing: 10px;
 
 .logs-row__time {
   white-space: nowrap;
+  width: 19em;
+}
+
+.logs-row__localtime {
+  white-space: nowrap;
+  width: 12.5em;
 }
 
 .logs-row__labels {
-  max-width: 20%;
+  width: 20%;
   line-height: 1.2;
+  position: relative;
 }
 
 .logs-row__message {
   word-break: break-all;
-  min-width: 80%;
 }
 
 .logs-row__match-highlight {
@@ -112,6 +119,7 @@ $column-horizontal-spacing: 10px;
 
 .logs-row__level {
   position: relative;
+  width: 10px;
 
   &::after {
     content: '';
@@ -165,6 +173,7 @@ $column-horizontal-spacing: 10px;
 
 .logs-row__duplicates {
   text-align: right;
+  width: 4.5em;
 }
 
 .logs-row__field-highlight {
@@ -193,15 +202,20 @@ $column-horizontal-spacing: 10px;
   }
 }
 
+.logs-labels {
+  display: flex;
+  flex-wrap: wrap;
+}
+
 .logs-label {
-  display: inline-block;
+  display: flex;
   padding: 0 2px;
   background-color: $btn-inverse-bg;
   border-radius: $border-radius;
   margin: 0 4px 2px 0;
   text-overflow: ellipsis;
   white-space: nowrap;
-  position: relative;
+  overflow: hidden;
 }
 
 .logs-label__icon {
@@ -211,6 +225,13 @@ $column-horizontal-spacing: 10px;
   margin-left: 2px;
 }
 
+.logs-label__value {
+  display: inline-block;
+  max-width: 20em;
+  text-overflow: ellipsis;
+  overflow: hidden;
+}
+
 .logs-label__stats {
   position: absolute;
   top: 1.25em;

+ 16 - 18
public/sass/components/_sidemenu.scss

@@ -16,6 +16,14 @@
   .sidemenu__close {
     display: none;
   }
+
+  @include media-breakpoint-up(sm) {
+    background: $side-menu-bg;
+    height: auto;
+    box-shadow: $side-menu-shadow;
+    position: relative;
+    z-index: $zindex-sidemenu;
+  }
 }
 
 // body class that hides sidemenu
@@ -25,32 +33,22 @@
   }
 }
 
-@include media-breakpoint-up(sm) {
-  .sidemenu-open {
-    .sidemenu {
-      background: $side-menu-bg;
-      height: auto;
-      box-shadow: $side-menu-shadow;
-      position: relative;
-      z-index: $zindex-sidemenu;
-    }
-
-    .sidemenu__top,
-    .sidemenu__bottom {
-      display: block;
-    }
-  }
-}
-
 .sidemenu__top {
   padding-top: 3rem;
   flex-grow: 1;
-  display: none;
 }
 
 .sidemenu__bottom {
   padding-bottom: $spacer;
+}
+
+.sidemenu__top,
+.sidemenu__bottom {
   display: none;
+
+  @include media-breakpoint-up(sm) {
+    display: block;
+  }
 }
 
 .sidemenu-item {

+ 15 - 0
public/sass/components/_view_states.scss

@@ -29,6 +29,21 @@
 .view-mode--tv {
   @extend .view-mode--inactive;
 
+  .sidemenu {
+    position: fixed;
+    background-color: transparent;
+    box-shadow: none;
+
+    .sidemenu__top,
+    .sidemenu__bottom {
+      display: none;
+    }
+  }
+
+  .navbar {
+    padding-left: $side-menu-width;
+  }
+
   .submenu-controls {
     display: none;
   }

+ 0 - 9
public/sass/mixins/_mixins.scss

@@ -79,10 +79,6 @@
 // FONTS
 // --------------------------------------------------
 
-@mixin font-family-serif() {
-  font-family: $font-family-serif;
-}
-
 @mixin font-family-sans-serif() {
   font-family: $font-family-sans-serif;
 }
@@ -97,11 +93,6 @@
   line-height: $lineHeight;
 }
 
-@mixin font-serif($size: $font-size-base, $weight: normal, $lineHeight: $line-height-base) {
-  @include font-family-serif();
-  @include font-shorthand($size, $weight, $lineHeight);
-}
-
 @mixin font-sans-serif($size: $font-size-base, $weight: normal, $lineHeight: $line-height-base) {
   @include font-family-sans-serif();
   @include font-shorthand($size, $weight, $lineHeight);

+ 1 - 35
public/sass/pages/_explore.scss

@@ -25,20 +25,13 @@
   }
 }
 
-.sidemenu-open {
-  .explore-toolbar-header {
-    padding: 0;
-    margin-left: 0;
-  }
-}
-
 .explore-toolbar {
   background: inherit;
   display: flex;
   flex-flow: row wrap;
   justify-content: flex-start;
   height: auto;
-  padding: 0px $dashboard-padding 0 25px;
+  padding: 0 $dashboard-padding;
   border-bottom: 1px solid #0000;
   transition-duration: 0.35s;
   transition-timing-function: ease-in-out;
@@ -72,11 +65,6 @@
   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;
 }
@@ -134,20 +122,6 @@
 }
 
 @media only screen and (max-width: 803px) {
-  .sidemenu-open {
-    .explore-toolbar-header-title {
-      .navbar-page-btn {
-        margin-left: 0;
-      }
-    }
-  }
-
-  .explore-toolbar-header-title {
-    .navbar-page-btn {
-      margin-left: $dashboard-padding;
-    }
-  }
-
   .btn-title {
     display: none;
   }
@@ -161,14 +135,6 @@
 }
 
 @media only screen and (max-width: 544px) {
-  .sidemenu-open {
-    .explore-toolbar-header-title {
-      .navbar-page-btn {
-        margin-left: $dashboard-padding;
-      }
-    }
-  }
-
   .explore-toolbar-header-title {
     .navbar-page-btn {
       margin-left: $dashboard-padding;

+ 1 - 1
scripts/cli/utils/useSpinner.ts

@@ -4,7 +4,7 @@ type FnToSpin<T> = (options: T) => Promise<void>;
 
 export const useSpinner = <T>(spinnerLabel: string, fn: FnToSpin<T>, killProcess = true) => {
   return async (options: T) => {
-    const spinner = new ora(spinnerLabel);
+    const spinner = ora(spinnerLabel);
     spinner.start();
     try {
       await fn(options);

+ 12 - 0
yarn.lock

@@ -1801,6 +1801,13 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.40.tgz#4314888d5cd537945d73e9ce165c04cc550144a4"
   integrity sha512-RRSjdwz63kS4u7edIwJUn8NqKLLQ6LyqF/X4+4jp38MBT3Vwetewi2N4dgJEshLbDwNgOJXNYoOwzVZUSSLhkQ==
 
+"@types/papaparse@^4.5.9":
+  version "4.5.9"
+  resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-4.5.9.tgz#ff887bd362f57cd0c87320d2de38ac232bb55e81"
+  integrity sha512-8Pmxp2IEd/y58tOIsiZkCbAkcKI7InYVpwZFVKJyweCVnqnVahKXVjfSo6gvxUVykQsJvtWB+s6Kc60znVfQVw==
+  dependencies:
+    "@types/node" "*"
+
 "@types/prop-types@*":
   version "15.5.8"
   resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.8.tgz#8ae4e0ea205fe95c3901a5a1df7f66495e3a56ce"
@@ -12993,6 +13000,11 @@ pako@~1.0.5:
   resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.8.tgz#6844890aab9c635af868ad5fecc62e8acbba3ea4"
   integrity sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==
 
+papaparse@^4.6.3:
+  version "4.6.3"
+  resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-4.6.3.tgz#742e5eaaa97fa6c7e1358d2934d8f18f44aee781"
+  integrity sha512-LRq7BrHC2kHPBYSD50aKuw/B/dGcg29omyJbKWY3KsYUZU69RKwaBHu13jGmCYBtOc4odsLCrFyk6imfyNubJQ==
+
 parallel-transform@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06"