Explorar o código

react-panel: Time range options moved to "Queries" tab

Johannes Schill %!s(int64=7) %!d(string=hai) anos
pai
achega
69ae3d2e6a

+ 43 - 0
public/app/core/components/Form/Element.tsx

@@ -0,0 +1,43 @@
+import React, { PureComponent, ReactNode, ReactElement } from 'react';
+import { Label } from './Label';
+import { uniqueId } from 'lodash';
+
+interface Props {
+  label?: ReactNode;
+  labelClassName?: string;
+  id?: string;
+  children: ReactElement<any>;
+}
+
+export class Element extends PureComponent<Props> {
+  elementId: string = this.props.id || uniqueId('form-element-');
+
+  get elementLabel() {
+    const { label, labelClassName } = this.props;
+
+    if (label) {
+      return (
+        <Label htmlFor={this.elementId} className={labelClassName}>
+          {label}
+        </Label>
+      );
+    }
+
+    return null;
+  }
+
+  get children() {
+    const { children } = this.props;
+
+    return React.cloneElement(children, { id: this.elementId });
+  }
+
+  render() {
+    return (
+      <div className="our-custom-wrapper-class">
+        {this.elementLabel}
+        {this.children}
+      </div>
+    );
+  }
+}

+ 96 - 0
public/app/core/components/Form/Input.tsx

@@ -0,0 +1,96 @@
+import React, { PureComponent } from 'react';
+import { ValidationRule } from 'app/types';
+
+export enum InputStatus {
+  Default = 'default',
+  Loading = 'loading',
+  Invalid = 'invalid',
+  Valid = 'valid',
+}
+
+export enum InputTypes {
+  Text = 'text',
+  Number = 'number',
+  Password = 'password',
+  Email = 'email',
+}
+
+interface Props {
+  status?: InputStatus;
+  validationRules: ValidationRule[];
+  hideErrorMessage?: boolean;
+  onBlurWithStatus?: (evt, status: InputStatus) => void;
+  emptyToNull?: boolean;
+}
+
+const validator = (value: string, validationRules: ValidationRule[]) => {
+  const errors = validationRules.reduce((acc, currRule) => {
+    if (!currRule.rule(value)) {
+      return acc.concat(currRule.errorMessage);
+    }
+    return acc;
+  }, []);
+  return errors.length > 0 ? errors : null;
+};
+
+export class Input extends PureComponent<Props & React.HTMLProps<HTMLInputElement>> {
+  state = {
+    error: null,
+  };
+
+  get status() {
+    const { error } = this.state;
+    if (error) {
+      return InputStatus.Invalid;
+    }
+    return InputStatus.Valid;
+  }
+
+  onBlurWithValidation = evt => {
+    const { validationRules, onBlurWithStatus, onBlur } = this.props;
+
+    let errors = null;
+    if (validationRules) {
+      errors = validator(evt.currentTarget.value, validationRules);
+      this.setState(prevState => {
+        return {
+          ...prevState,
+          error: errors ? errors[0] : null,
+        };
+      });
+    }
+
+    if (onBlurWithStatus) {
+      onBlurWithStatus(evt, errors ? InputStatus.Invalid : InputStatus.Valid);
+    }
+
+    if (onBlur) {
+      onBlur(evt);
+    }
+  };
+
+  render() {
+    const {
+      status,
+      validationRules,
+      onBlurWithStatus,
+      onBlur,
+      className,
+      hideErrorMessage,
+      emptyToNull,
+      ...restProps
+    } = this.props;
+
+    const { error } = this.state;
+
+    let inputClassName = 'gf-form-input';
+    inputClassName = this.status === InputStatus.Invalid ? inputClassName + ' invalid' : inputClassName;
+
+    return (
+      <div className="our-custom-wrapper-class">
+        <input {...restProps} onBlur={this.onBlurWithValidation} className={inputClassName} />
+        {error && !hideErrorMessage && <span>{error}</span>}
+      </div>
+    );
+  }
+}

+ 19 - 0
public/app/core/components/Form/Label.tsx

@@ -0,0 +1,19 @@
+import React, { PureComponent, ReactNode } from 'react';
+
+interface Props {
+  children: ReactNode;
+  htmlFor?: string;
+  className?: string;
+}
+
+export class Label extends PureComponent<Props> {
+  render() {
+    const { children, htmlFor, className } = this.props;
+
+    return (
+      <label className={`custom-label-class ${className || ''}`} htmlFor={htmlFor}>
+        {children}
+      </label>
+    );
+  }
+}

+ 3 - 0
public/app/core/components/Form/index.ts

@@ -0,0 +1,3 @@
+export { Element } from './Element';
+export { Input } from './Input';
+export { Label } from './Label';

+ 9 - 0
public/app/core/utils/rangeutil.ts

@@ -159,3 +159,12 @@ export function describeTimeRange(range: RawTimeRange): string {
 
   return range.from.toString() + ' to ' + range.to.toString();
 }
+
+export const isValidTimeSpan = (value: string) => {
+  if (value.indexOf('$') === 0 || value.indexOf('+$') === 0) {
+    return true;
+  }
+
+  const info = describeTextRange(value);
+  return info.invalid !== true;
+};

+ 99 - 2
public/app/features/dashboard/dashgrid/QueriesTab.tsx

@@ -8,6 +8,11 @@ import { DashboardModel } from '../dashboard_model';
 import './../../panel/metrics_tab';
 import config from 'app/core/config';
 import { QueryInspector } from './QueryInspector';
+import { Switch } from 'app/core/components/Switch/Switch';
+import { Input } from 'app/core/components/Form';
+import { InputStatus } from 'app/core/components/Form/Input';
+import { isValidTimeSpan } from 'app/core/utils/rangeutil';
+import { ValidationRule } from 'app/types';
 
 // Services
 import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
@@ -29,6 +34,7 @@ interface Help {
 interface State {
   currentDatasource: DataSourceSelectItem;
   help: Help;
+  hideTimeOverride: boolean;
 }
 
 interface LoadingPlaceholderProps {
@@ -36,6 +42,17 @@ interface LoadingPlaceholderProps {
 }
 
 const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => <h2>{text}</h2>;
+const validationRules: ValidationRule[] = [
+  {
+    rule: value => {
+      if (!value) {
+        return true;
+      }
+      return isValidTimeSpan(value);
+    },
+    errorMessage: 'Not a valid timespan',
+  },
+];
 
 export class QueriesTab extends PureComponent<Props, State> {
   element: any;
@@ -53,6 +70,7 @@ export class QueriesTab extends PureComponent<Props, State> {
         isLoading: false,
         helpHtml: null,
       },
+      hideTimeOverride: false,
     };
   }
 
@@ -215,9 +233,40 @@ export class QueriesTab extends PureComponent<Props, State> {
     return isLoading ? <LoadingPlaceholder text="Loading help..." /> : helpHtml;
   };
 
+  emptyToNull = (value: string) => {
+    return value === '' ? null : value;
+  };
+
+  onOverrideTime = (evt, status: InputStatus) => {
+    const { value } = evt.target;
+    const { panel } = this.props;
+    const emptyToNullValue = this.emptyToNull(value);
+    if (status === InputStatus.Valid && panel.timeFrom !== emptyToNullValue) {
+      panel.timeFrom = emptyToNullValue;
+      panel.refresh();
+    }
+  };
+
+  onTimeShift = (evt, status: InputStatus) => {
+    const { value } = evt.target;
+    const { panel } = this.props;
+    const emptyToNullValue = this.emptyToNull(value);
+    if (status === InputStatus.Valid && panel.timeShift !== emptyToNullValue) {
+      panel.timeShift = emptyToNullValue;
+      panel.refresh();
+    }
+  };
+
+  onToggleTimeOverride = () => {
+    const { panel } = this.props;
+    panel.hideTimeOverride = !panel.hideTimeOverride;
+    panel.refresh();
+  };
+
   render() {
     const { currentDatasource } = this.state;
-
+    const hideTimeOverride = this.props.panel.hideTimeOverride;
+    console.log('hideTimeOverride', hideTimeOverride);
     const { hasQueryHelp, queryOptions } = currentDatasource.meta;
     const hasQueryOptions = !!queryOptions;
     const dsInformation = {
@@ -256,7 +305,55 @@ export class QueriesTab extends PureComponent<Props, State> {
 
     return (
       <EditorTabBody heading="Queries" main={dsInformation} toolbarItems={[options, queryInspector, dsHelp]}>
-        <div ref={element => (this.element = element)} style={{ width: '100%' }} />
+        <>
+          <div ref={element => (this.element = element)} style={{ width: '100%' }} />
+
+          <h5 className="section-heading">Time Range</h5>
+
+          <div className="gf-form-group">
+            <div className="gf-form">
+              <span className="gf-form-label">
+                <i className="fa fa-clock-o" />
+              </span>
+
+              <span className="gf-form-label width-12">Override relative time</span>
+              <span className="gf-form-label width-6">Last</span>
+              <Input
+                type="text"
+                className="gf-form-input max-width-8"
+                placeholder="1h"
+                onBlurWithStatus={this.onOverrideTime}
+                validationRules={validationRules}
+                hideErrorMessage={true}
+              />
+            </div>
+
+            <div className="gf-form">
+              <span className="gf-form-label">
+                <i className="fa fa-clock-o" />
+              </span>
+              <span className="gf-form-label width-12">Add time shift</span>
+              <span className="gf-form-label width-6">Amount</span>
+              <Input
+                type="text"
+                className="gf-form-input max-width-8"
+                placeholder="1h"
+                onBlurWithStatus={this.onTimeShift}
+                validationRules={validationRules}
+                hideErrorMessage={true}
+              />
+            </div>
+
+            <div className="gf-form-inline">
+              <div className="gf-form">
+                <span className="gf-form-label">
+                  <i className="fa fa-clock-o" />
+                </span>
+              </div>
+              <Switch label="Hide time override info" checked={hideTimeOverride} onChange={this.onToggleTimeOverride} />
+            </div>
+          </div>
+        </>
       </EditorTabBody>
     );
   }

+ 4 - 0
public/app/types/form.ts

@@ -0,0 +1,4 @@
+export interface ValidationRule {
+  rule: (value: string) => boolean;
+  errorMessage: string;
+}

+ 2 - 1
public/app/types/index.ts

@@ -30,7 +30,7 @@ import {
   AppNotificationTimeout,
 } from './appNotifications';
 import { DashboardSearchHit } from './search';
-
+import { ValidationRule } from './form';
 export {
   Team,
   TeamsState,
@@ -89,6 +89,7 @@ export {
   AppNotificationTimeout,
   DashboardSearchHit,
   UserState,
+  ValidationRule,
 };
 
 export interface StoreState {

+ 5 - 1
public/sass/utils/_validation.scss

@@ -1,7 +1,11 @@
-input[type="text"].ng-dirty.ng-invalid {
+input[type='text'].ng-dirty.ng-invalid {
 }
 
 input.validation-error,
 input.ng-dirty.ng-invalid {
   box-shadow: inset 0 0px 5px $red;
 }
+
+input.invalid {
+  box-shadow: inset 0 0px 5px $red;
+}