mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #14198 from grafana/panel-edit-in-react-query-inspector
Panel edit in react query inspector
This commit is contained in:
commit
6cbbffffb9
@ -0,0 +1,67 @@
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import ClipboardJS from 'clipboard';
|
||||
|
||||
interface Props {
|
||||
text: () => string;
|
||||
elType?: string;
|
||||
onSuccess?: (evt: any) => void;
|
||||
onError?: (evt: any) => void;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export class CopyToClipboard extends PureComponent<Props> {
|
||||
clipboardjs: any;
|
||||
myRef: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.myRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { text, onSuccess, onError } = this.props;
|
||||
|
||||
this.clipboardjs = new ClipboardJS(this.myRef.current, {
|
||||
text: text,
|
||||
});
|
||||
|
||||
if (onSuccess) {
|
||||
this.clipboardjs.on('success', evt => {
|
||||
evt.clearSelection();
|
||||
onSuccess(evt);
|
||||
});
|
||||
}
|
||||
|
||||
if (onError) {
|
||||
this.clipboardjs.on('error', evt => {
|
||||
console.error('Action:', evt.action);
|
||||
console.error('Trigger:', evt.trigger);
|
||||
onError(evt);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.clipboardjs) {
|
||||
this.clipboardjs.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
getElementType = () => {
|
||||
return this.props.elType || 'button';
|
||||
};
|
||||
|
||||
render() {
|
||||
const { elType, text, children, onError, onSuccess, ...restProps } = this.props;
|
||||
|
||||
return React.createElement(
|
||||
this.getElementType(),
|
||||
{
|
||||
ref: this.myRef,
|
||||
...restProps,
|
||||
},
|
||||
this.props.children
|
||||
);
|
||||
}
|
||||
}
|
43
public/app/core/components/Form/Element.tsx
Normal file
43
public/app/core/components/Form/Element.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
86
public/app/core/components/Form/Input.tsx
Normal file
86
public/app/core/components/Form/Input.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { ValidationEvents, ValidationRule } from 'app/types';
|
||||
import { validate } from 'app/core/utils/validate';
|
||||
|
||||
export enum InputStatus {
|
||||
Invalid = 'invalid',
|
||||
Valid = 'valid',
|
||||
}
|
||||
|
||||
export enum InputTypes {
|
||||
Text = 'text',
|
||||
Number = 'number',
|
||||
Password = 'password',
|
||||
Email = 'email',
|
||||
}
|
||||
|
||||
export enum EventsWithValidation {
|
||||
onBlur = 'onBlur',
|
||||
onFocus = 'onFocus',
|
||||
onChange = 'onChange',
|
||||
}
|
||||
|
||||
interface Props extends React.HTMLProps<HTMLInputElement> {
|
||||
validationEvents: ValidationEvents;
|
||||
hideErrorMessage?: boolean;
|
||||
|
||||
// Override event props and append status as argument
|
||||
onBlur?: (event: React.FocusEvent<HTMLInputElement>, status?: InputStatus) => void;
|
||||
onFocus?: (event: React.FocusEvent<HTMLInputElement>, status?: InputStatus) => void;
|
||||
onChange?: (event: React.FormEvent<HTMLInputElement>, status?: InputStatus) => void;
|
||||
}
|
||||
|
||||
export class Input extends PureComponent<Props> {
|
||||
state = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
get status() {
|
||||
return this.state.error ? InputStatus.Invalid : InputStatus.Valid;
|
||||
}
|
||||
|
||||
get isInvalid() {
|
||||
return this.status === InputStatus.Invalid;
|
||||
}
|
||||
|
||||
validatorAsync = (validationRules: ValidationRule[]) => {
|
||||
return evt => {
|
||||
const errors = validate(evt.currentTarget.value, validationRules);
|
||||
this.setState(prevState => {
|
||||
return {
|
||||
...prevState,
|
||||
error: errors ? errors[0] : null,
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
populateEventPropsWithStatus = (restProps, validationEvents: ValidationEvents) => {
|
||||
const inputElementProps = { ...restProps };
|
||||
Object.keys(EventsWithValidation).forEach(eventName => {
|
||||
inputElementProps[eventName] = async evt => {
|
||||
if (validationEvents[eventName]) {
|
||||
await this.validatorAsync(validationEvents[eventName]).apply(this, [evt]);
|
||||
}
|
||||
if (restProps[eventName]) {
|
||||
restProps[eventName].apply(null, [evt, this.status]);
|
||||
}
|
||||
};
|
||||
});
|
||||
return inputElementProps;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { validationEvents, className, hideErrorMessage, ...restProps } = this.props;
|
||||
const { error } = this.state;
|
||||
const inputClassName = 'gf-form-input' + (this.isInvalid ? ' invalid' : '');
|
||||
const inputElementProps = this.populateEventPropsWithStatus(restProps, validationEvents);
|
||||
|
||||
return (
|
||||
<div className="our-custom-wrapper-class">
|
||||
<input {...inputElementProps} className={inputClassName} />
|
||||
{error && !hideErrorMessage && <span>{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
19
public/app/core/components/Form/Label.tsx
Normal file
19
public/app/core/components/Form/Label.tsx
Normal file
@ -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
public/app/core/components/Form/index.ts
Normal file
3
public/app/core/components/Form/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { Element } from './Element';
|
||||
export { Input } from './Input';
|
||||
export { Label } from './Label';
|
51
public/app/core/components/JSONFormatter/JSONFormatter.tsx
Normal file
51
public/app/core/components/JSONFormatter/JSONFormatter.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import React, { PureComponent, createRef } from 'react';
|
||||
// import JSONFormatterJS, { JSONFormatterConfiguration } from 'json-formatter-js';
|
||||
import { JsonExplorer } from 'app/core/core'; // We have made some monkey-patching of json-formatter-js so we can't switch right now
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
json: {};
|
||||
config?: any;
|
||||
open?: number;
|
||||
onDidRender?: (formattedJson: any) => void;
|
||||
}
|
||||
|
||||
export class JSONFormatter extends PureComponent<Props> {
|
||||
private wrapperRef = createRef<HTMLDivElement>();
|
||||
|
||||
static defaultProps = {
|
||||
open: 3,
|
||||
config: {
|
||||
animateOpen: true,
|
||||
},
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.renderJson();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.renderJson();
|
||||
}
|
||||
|
||||
renderJson = () => {
|
||||
const { json, config, open, onDidRender } = this.props;
|
||||
const wrapperEl = this.wrapperRef.current;
|
||||
const formatter = new JsonExplorer(json, open, config);
|
||||
const hasChildren: boolean = wrapperEl.hasChildNodes();
|
||||
if (hasChildren) {
|
||||
wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild);
|
||||
} else {
|
||||
wrapperEl.appendChild(formatter.render());
|
||||
}
|
||||
|
||||
if (onDidRender) {
|
||||
onDidRender(formatter.json);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
return <div className={className} ref={this.wrapperRef} />;
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
||||
|
11
public/app/core/utils/validate.ts
Normal file
11
public/app/core/utils/validate.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { ValidationRule } from 'app/types';
|
||||
|
||||
export const validate = (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;
|
||||
};
|
@ -1,13 +1,18 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { SFC, PureComponent } from 'react';
|
||||
import DataSourceOption from './DataSourceOption';
|
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||
import { EditorTabBody } from './EditorTabBody';
|
||||
import { DataSourcePicker } from './DataSourcePicker';
|
||||
|
||||
import { PanelModel } from '../panel_model';
|
||||
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, EventsWithValidation } from 'app/core/components/Form/Input';
|
||||
import { isValidTimeSpan } from 'app/core/utils/rangeutil';
|
||||
import { ValidationEvents } from 'app/types';
|
||||
|
||||
// Services
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
@ -29,8 +34,29 @@ interface Help {
|
||||
interface State {
|
||||
currentDatasource: DataSourceSelectItem;
|
||||
help: Help;
|
||||
hideTimeOverride: boolean;
|
||||
}
|
||||
|
||||
interface LoadingPlaceholderProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => <h2>{text}</h2>;
|
||||
|
||||
const timeRangeValidationEvents: ValidationEvents = {
|
||||
[EventsWithValidation.onBlur]: [
|
||||
{
|
||||
rule: value => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
return isValidTimeSpan(value);
|
||||
},
|
||||
errorMessage: 'Not a valid timespan',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export class QueriesTab extends PureComponent<Props, State> {
|
||||
element: any;
|
||||
component: AngularComponent;
|
||||
@ -47,6 +73,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
isLoading: false,
|
||||
helpHtml: null,
|
||||
},
|
||||
hideTimeOverride: false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -199,9 +226,50 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
renderQueryInspector = () => {
|
||||
const { panel } = this.props;
|
||||
return <QueryInspector panel={panel} LoadingPlaceholder={LoadingPlaceholder} />;
|
||||
};
|
||||
|
||||
renderHelp = () => {
|
||||
const { helpHtml, isLoading } = this.state.help;
|
||||
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 { helpHtml } = this.state.help;
|
||||
const hideTimeOverride = this.props.panel.hideTimeOverride;
|
||||
console.log('hideTimeOverride', hideTimeOverride);
|
||||
const { hasQueryHelp, queryOptions } = currentDatasource.meta;
|
||||
const hasQueryOptions = !!queryOptions;
|
||||
const dsInformation = {
|
||||
@ -220,7 +288,7 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
|
||||
const queryInspector = {
|
||||
title: 'Query Inspector',
|
||||
render: () => <h2>hello</h2>,
|
||||
render: this.renderQueryInspector,
|
||||
};
|
||||
|
||||
const dsHelp = {
|
||||
@ -228,18 +296,67 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
icon: 'fa fa-question',
|
||||
disabled: !hasQueryHelp,
|
||||
onClick: this.loadHelp,
|
||||
render: () => helpHtml,
|
||||
render: this.renderHelp,
|
||||
};
|
||||
|
||||
const options = {
|
||||
title: 'Options',
|
||||
title: '',
|
||||
icon: 'fa fa-cog',
|
||||
disabled: !hasQueryOptions,
|
||||
render: this.renderOptions,
|
||||
};
|
||||
|
||||
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"
|
||||
onBlur={this.onOverrideTime}
|
||||
validationEvents={timeRangeValidationEvents}
|
||||
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"
|
||||
onBlur={this.onTimeShift}
|
||||
validationEvents={timeRangeValidationEvents}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
226
public/app/features/dashboard/dashgrid/QueryInspector.tsx
Normal file
226
public/app/features/dashboard/dashgrid/QueryInspector.tsx
Normal file
@ -0,0 +1,226 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
|
||||
|
||||
interface DsQuery {
|
||||
isLoading: boolean;
|
||||
response: {};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
panel: any;
|
||||
LoadingPlaceholder: any;
|
||||
}
|
||||
|
||||
interface State {
|
||||
allNodesExpanded: boolean;
|
||||
isMocking: boolean;
|
||||
mockedResponse: string;
|
||||
dsQuery: DsQuery;
|
||||
}
|
||||
|
||||
export class QueryInspector extends PureComponent<Props, State> {
|
||||
formattedJson: any;
|
||||
clipboard: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
allNodesExpanded: null,
|
||||
isMocking: false,
|
||||
mockedResponse: '',
|
||||
dsQuery: {
|
||||
isLoading: false,
|
||||
response: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { panel } = this.props;
|
||||
panel.events.on('refresh', this.onPanelRefresh);
|
||||
appEvents.on('ds-request-response', this.onDataSourceResponse);
|
||||
panel.refresh();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { panel } = this.props;
|
||||
appEvents.off('ds-request-response', this.onDataSourceResponse);
|
||||
panel.events.off('refresh', this.onPanelRefresh);
|
||||
}
|
||||
|
||||
handleMocking(response) {
|
||||
const { mockedResponse } = this.state;
|
||||
let mockedData;
|
||||
try {
|
||||
mockedData = JSON.parse(mockedResponse);
|
||||
} catch (err) {
|
||||
appEvents.emit('alert-error', ['R: Failed to parse mocked response']);
|
||||
return;
|
||||
}
|
||||
|
||||
response.data = mockedData;
|
||||
}
|
||||
|
||||
onPanelRefresh = () => {
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
dsQuery: {
|
||||
isLoading: true,
|
||||
response: {},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
onDataSourceResponse = (response: any = {}) => {
|
||||
if (this.state.isMocking) {
|
||||
this.handleMocking(response);
|
||||
return;
|
||||
}
|
||||
|
||||
response = { ...response }; // clone - dont modify the response
|
||||
|
||||
if (response.headers) {
|
||||
delete response.headers;
|
||||
}
|
||||
|
||||
if (response.config) {
|
||||
response.request = response.config;
|
||||
delete response.config;
|
||||
delete response.request.transformRequest;
|
||||
delete response.request.transformResponse;
|
||||
delete response.request.paramSerializer;
|
||||
delete response.request.jsonpCallbackParam;
|
||||
delete response.request.headers;
|
||||
delete response.request.requestId;
|
||||
delete response.request.inspect;
|
||||
delete response.request.retry;
|
||||
delete response.request.timeout;
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
response.response = response.data;
|
||||
|
||||
delete response.data;
|
||||
delete response.status;
|
||||
delete response.statusText;
|
||||
delete response.$$config;
|
||||
}
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
dsQuery: {
|
||||
isLoading: false,
|
||||
response: response,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
setFormattedJson = formattedJson => {
|
||||
this.formattedJson = formattedJson;
|
||||
};
|
||||
|
||||
getTextForClipboard = () => {
|
||||
return JSON.stringify(this.formattedJson, null, 2);
|
||||
};
|
||||
|
||||
onClipboardSuccess = () => {
|
||||
appEvents.emit('alert-success', ['Content copied to clipboard']);
|
||||
};
|
||||
|
||||
onToggleExpand = () => {
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
allNodesExpanded: !this.state.allNodesExpanded,
|
||||
}));
|
||||
};
|
||||
|
||||
onToggleMocking = () => {
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
isMocking: !this.state.isMocking,
|
||||
}));
|
||||
};
|
||||
|
||||
getNrOfOpenNodes = () => {
|
||||
if (this.state.allNodesExpanded === null) {
|
||||
return 3; // 3 is default, ie when state is null
|
||||
} else if (this.state.allNodesExpanded) {
|
||||
return 20;
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
|
||||
setMockedResponse = evt => {
|
||||
const mockedResponse = evt.target.value;
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
mockedResponse,
|
||||
}));
|
||||
};
|
||||
|
||||
renderExpandCollapse = () => {
|
||||
const { allNodesExpanded } = this.state;
|
||||
|
||||
const collapse = (
|
||||
<>
|
||||
<i className="fa fa-minus-square-o" /> Collapse All
|
||||
</>
|
||||
);
|
||||
const expand = (
|
||||
<>
|
||||
<i className="fa fa-plus-square-o" /> Expand All
|
||||
</>
|
||||
);
|
||||
return allNodesExpanded ? collapse : expand;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { response, isLoading } = this.state.dsQuery;
|
||||
const { LoadingPlaceholder } = this.props;
|
||||
const { isMocking } = this.state;
|
||||
const openNodes = this.getNrOfOpenNodes();
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingPlaceholder text="Loading query inspector..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{/*
|
||||
<button className="btn btn-transparent btn-p-x-0 m-r-1" onClick={this.onToggleMocking}>
|
||||
Mock response
|
||||
</button>
|
||||
*/}
|
||||
<button className="btn btn-transparent btn-p-x-0 m-r-1" onClick={this.onToggleExpand}>
|
||||
{this.renderExpandCollapse()}
|
||||
</button>
|
||||
|
||||
<CopyToClipboard
|
||||
className="btn btn-transparent btn-p-x-0"
|
||||
text={this.getTextForClipboard}
|
||||
onSuccess={this.onClipboardSuccess}
|
||||
>
|
||||
<i className="fa fa-clipboard" /> Copy to Clipboard
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
|
||||
{!isMocking && <JSONFormatter json={response} open={openNodes} onDidRender={this.setFormattedJson} />}
|
||||
{isMocking && (
|
||||
<div className="query-troubleshooter__body">
|
||||
<div className="gf-form p-l-1 gf-form--v-stretch">
|
||||
<textarea
|
||||
className="gf-form-input"
|
||||
style={{ width: '95%' }}
|
||||
rows={10}
|
||||
onInput={this.setMockedResponse}
|
||||
placeholder="JSON"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
8
public/app/types/form.ts
Normal file
8
public/app/types/form.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface ValidationRule {
|
||||
rule: (valueToValidate: string) => boolean;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
export interface ValidationEvents {
|
||||
[eventName: string]: ValidationRule[];
|
||||
}
|
@ -30,7 +30,7 @@ import {
|
||||
AppNotificationTimeout,
|
||||
} from './appNotifications';
|
||||
import { DashboardSearchHit } from './search';
|
||||
|
||||
import { ValidationEvents, ValidationRule } from './form';
|
||||
export {
|
||||
Team,
|
||||
TeamsState,
|
||||
@ -89,6 +89,8 @@ export {
|
||||
AppNotificationTimeout,
|
||||
DashboardSearchHit,
|
||||
UserState,
|
||||
ValidationEvents,
|
||||
ValidationRule,
|
||||
};
|
||||
|
||||
export interface StoreState {
|
||||
|
@ -172,6 +172,12 @@
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
// No horizontal padding
|
||||
.btn-p-x-0 {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
// External services
|
||||
// Usage:
|
||||
// <div class="btn btn-service btn-service--facebook">Button text</div>
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user