Commit 17059489 by Ryan McKinley Committed by GitHub

Inspector: move `Panel JSON` and query inspector to the inspector (#23354)

* move Panel JSON to inspector

* move Panel JSON to inspector

* update test

* use stats display options

* move query inspector to inspector

* open inspector from the queries section

* subscribe to results

* subscribe to results

* open the right tab

* apply review feedback

* update menus (inspect tabs)

* Dashboard: extend dashnav to add custom content (#23433)

* Dashlist: Fixed dashlist broken in edit mode (#23426)

* Chore: Fix bunch of strict null error to fix master CI (#23443)

* Fix bunch of null error

* Fix failing test

* Another test fix

* Docs: Add SQL region annotation examples (#23268)

Add region annotation examples for SQL data sources in docs.

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>

* Docs: Update contributing doc to install node@12. (#23450)

* NewPanelEdit: Minor style and description tweaks, AND PanelQueryRunner & autoMinMax (#23445)

* NewPanelEdit: Minor style and description tweaks

* Removed the worst snapshot of all time

* ReactTable: adds color text to field options (#23427)

* Feature: adds text color field config

* Refactor: created an extension point

* Refactor: uses HOC for extension instead

* Fix: fixes background styling from affecting cells without display.color

* Chore: export OptionsUIRegistryBuilder on grafana/data (#23444)

* export the ui registry

* add to utils index also

* DataLinks: Do not full page reload data links links (#23429)

* Templating: Fix global variable "__org.id" (#23362)

* Fixed global variable __org.id value

* correct orgId value

* reverted the change as variables moved to new file

* Chore: reduce null check errors to 788 (currently over 798) (#23449)

* Fixed ts errors so build will succeed

* Update packages/grafana-data/src/types/graph.ts

Co-Authored-By: Ryan McKinley <ryantxu@gmail.com>

* Feedback from code review

* Leaving out trivial typing's

* Fix error with color being undefined now.

* fix test with timezone issue

* Fixed test

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>

* Cloudwatch: prefer webIdentity over EC2 role (#23452)

* Plugins: add a signature status flag (#23420)

* Progress

* fixed button

* Final touches

* now works from edit mode

* fix layout

* show raw objects

* move query inspector buttons to the bottom

* update snapshot

* Updated design

* Made full page reload work

* Fixed minor style issue

* Updated

* More fixes

* Removed unused imports

* Updated

* Moved to data tab out to seperate component

* fixed ts issue

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>
Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>
Co-authored-by: Alexandre de Verteuil <alexandre@grafana.com>
Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Co-authored-by: Cyril Tovena <cyril.tovena@gmail.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: Vikky Omkar <vikkyomkar@gmail.com>
Co-authored-by: Stephanie Closson <srclosson@gmail.com>
Co-authored-by: Dário Nascimento <dfrnascimento@gmail.com>
parent cff70b66
...@@ -9,7 +9,7 @@ import { stylesFactory, useTheme } from '../../themes'; ...@@ -9,7 +9,7 @@ import { stylesFactory, useTheme } from '../../themes';
export interface Props { export interface Props {
children: ReactNode; children: ReactNode;
/** Title shown at the top of the drawer */ /** Title shown at the top of the drawer */
title?: (() => JSX.Element) | string; title?: JSX.Element | string;
/** Should the Drawer be closable by clicking on the mask */ /** Should the Drawer be closable by clicking on the mask */
closeOnMaskClick?: boolean; closeOnMaskClick?: boolean;
/** Render the drawer inside a container on the page */ /** Render the drawer inside a container on the page */
...@@ -98,7 +98,7 @@ export const Drawer: FC<Props> = ({ ...@@ -98,7 +98,7 @@ export const Drawer: FC<Props> = ({
</div> </div>
</div> </div>
)} )}
{typeof title === 'function' && title()} {typeof title !== 'string' && title}
<div className={drawerStyles.content}> <div className={drawerStyles.content}>
{!scrollableContent ? children : <CustomScrollbar>{children}</CustomScrollbar>} {!scrollableContent ? children : <CustomScrollbar>{children}</CustomScrollbar>}
</div> </div>
......
...@@ -91,7 +91,7 @@ const renderForm = (defaultValues?: Partial<FormDTO>) => ( ...@@ -91,7 +91,7 @@ const renderForm = (defaultValues?: Partial<FormDTO>) => (
</Field> </Field>
<Field label="Textarea" invalid={!!errors.text} error="Text is required"> <Field label="Textarea" invalid={!!errors.text} error="Text is required">
<TextArea name="text" placeholder="Long text" size="md" ref={register({ required: true })} /> <TextArea name="text" placeholder="Long text" ref={register({ required: true })} />
</Field> </Field>
<Field label="Checkbox" invalid={!!errors.checkbox} error="We need your consent"> <Field label="Checkbox" invalid={!!errors.checkbox} error="We need your consent">
......
import React from 'react'; import React from 'react';
import { TextArea } from './TextArea'; import { TextArea } from './TextArea';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { boolean, number, select, text } from '@storybook/addon-knobs'; import { boolean, number, text } from '@storybook/addon-knobs';
import mdx from './TextArea.mdx'; import mdx from './TextArea.mdx';
export default { export default {
...@@ -25,8 +25,6 @@ export const simple = () => { ...@@ -25,8 +25,6 @@ export const simple = () => {
// --- // ---
const placeholder = text('Placeholder', 'This is just a placeholder', VISUAL_GROUP); const placeholder = text('Placeholder', 'This is just a placeholder', VISUAL_GROUP);
const cols = number('Cols', 30, { range: true, min: 5, max: 50, step: 5 }, VISUAL_GROUP); const cols = number('Cols', 30, { range: true, min: 5, max: 50, step: 5 }, VISUAL_GROUP);
const size = select('Size', ['sm', 'md', 'lg', 'auto'], undefined, VISUAL_GROUP);
const CONTAINER_GROUP = 'Container options'; const CONTAINER_GROUP = 'Container options';
// --- // ---
const containerWidth = number( const containerWidth = number(
...@@ -43,7 +41,7 @@ export const simple = () => { ...@@ -43,7 +41,7 @@ export const simple = () => {
return ( return (
<div style={{ width: containerWidth }}> <div style={{ width: containerWidth }}>
<TextArea invalid={invalid} placeholder={placeholder} cols={cols} disabled={disabled} size={size} /> <TextArea invalid={invalid} placeholder={placeholder} cols={cols} disabled={disabled} />
</div> </div>
); );
}; };
...@@ -2,28 +2,19 @@ import React, { HTMLProps } from 'react'; ...@@ -2,28 +2,19 @@ import React, { HTMLProps } from 'react';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { stylesFactory, useTheme } from '../../themes'; import { stylesFactory, useTheme } from '../../themes';
import { getFocusStyle, inputSizes, sharedInputStyle } from '../Forms/commonStyles'; import { getFocusStyle, sharedInputStyle } from '../Forms/commonStyles';
import { FormInputSize } from '../Forms/types';
export interface Props extends Omit<HTMLProps<HTMLTextAreaElement>, 'size'> { export interface Props extends Omit<HTMLProps<HTMLTextAreaElement>, 'size'> {
/** Show an invalid state around the input */ /** Show an invalid state around the input */
invalid?: boolean; invalid?: boolean;
/** Choose a predefined size */
size?: FormInputSize;
} }
export const TextArea = React.forwardRef<HTMLTextAreaElement, Props>( export const TextArea = React.forwardRef<HTMLTextAreaElement, Props>(({ invalid, className, ...props }, ref) => {
({ invalid, size = 'auto', className, ...props }, ref) => { const theme = useTheme();
const theme = useTheme(); const styles = getTextAreaStyle(theme, invalid);
const styles = getTextAreaStyle(theme, invalid);
return ( return <textarea {...props} className={cx(styles.textarea, className)} ref={ref} />;
<div className={inputSizes()[size]}> });
<textarea {...props} className={cx(styles.textarea, className)} ref={ref} />
</div>
);
}
);
const getTextAreaStyle = stylesFactory((theme: GrafanaTheme, invalid = false) => { const getTextAreaStyle = stylesFactory((theme: GrafanaTheme, invalid = false) => {
return { return {
......
import './json_editor_ctrl';
import './invited_ctrl'; import './invited_ctrl';
import './signup_ctrl'; import './signup_ctrl';
import './reset_password_ctrl'; import './reset_password_ctrl';
......
import angular from 'angular';
import coreModule from '../core_module';
export class JsonEditorCtrl {
/** @ngInject */
constructor($scope: any) {
$scope.json = angular.toJson($scope.model.object, true);
$scope.canUpdate = $scope.model.updateHandler !== void 0 && $scope.model.canUpdate;
$scope.canCopy = $scope.model.enableCopy;
$scope.update = () => {
const newObject = angular.fromJson($scope.json);
$scope.model.updateHandler(newObject, $scope.model.object);
};
$scope.getContentForClipboard = () => $scope.json;
}
}
coreModule.controller('JsonEditorCtrl', JsonEditorCtrl);
...@@ -12,7 +12,6 @@ import 'mousetrap-global-bind'; ...@@ -12,7 +12,6 @@ import 'mousetrap-global-bind';
import { ContextSrv } from './context_srv'; import { ContextSrv } from './context_srv';
import { ILocationService, IRootScopeService, ITimeoutService } from 'angular'; import { ILocationService, IRootScopeService, ITimeoutService } from 'angular';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { getLocationSrv } from '@grafana/runtime';
import { DashboardModel } from '../../features/dashboard/state'; import { DashboardModel } from '../../features/dashboard/state';
import { ShareModal } from 'app/features/dashboard/components/ShareModal'; import { ShareModal } from 'app/features/dashboard/components/ShareModal';
import { SaveDashboardModalProxy } from '../../features/dashboard/components/SaveDashboard/SaveDashboardModalProxy'; import { SaveDashboardModalProxy } from '../../features/dashboard/components/SaveDashboard/SaveDashboardModalProxy';
...@@ -125,6 +124,13 @@ export class KeybindingSrv { ...@@ -125,6 +124,13 @@ export class KeybindingSrv {
return; return;
} }
if (search.inspect) {
delete search.inspect;
delete search.inspectTab;
this.$location.search(search);
return;
}
if (search.editPanel) { if (search.editPanel) {
delete search.editPanel; delete search.editPanel;
delete search.tab; delete search.tab;
...@@ -226,6 +232,13 @@ export class KeybindingSrv { ...@@ -226,6 +232,13 @@ export class KeybindingSrv {
} }
}); });
this.bind('i', () => {
if (dashboard.meta.focusPanelId) {
const search = _.extend(this.$location.search(), { inspect: dashboard.meta.focusPanelId });
this.$location.search(search);
}
});
// jump to explore if permissions allow // jump to explore if permissions allow
if (this.contextSrv.hasAccessToExplore()) { if (this.contextSrv.hasAccessToExplore()) {
this.bind('x', async () => { this.bind('x', async () => {
...@@ -279,13 +292,6 @@ export class KeybindingSrv { ...@@ -279,13 +292,6 @@ export class KeybindingSrv {
} }
}); });
// inspect panel
this.bind('p i', () => {
if (dashboard.meta.focusPanelId) {
getLocationSrv().update({ partial: true, query: { inspect: dashboard.meta.focusPanelId } });
}
});
// toggle panel legend // toggle panel legend
this.bind('p l', () => { this.bind('p l', () => {
if (dashboard.meta.focusPanelId) { if (dashboard.meta.focusPanelId) {
......
import React, { PureComponent } from 'react';
import { DataFrame, applyFieldOverrides, toCSV, SelectableValue } from '@grafana/data';
import { Button, Select, Icon, Table } from '@grafana/ui';
import { getPanelInspectorStyles } from './styles';
import { config } from 'app/core/config';
import AutoSizer from 'react-virtualized-auto-sizer';
import { saveAs } from 'file-saver';
interface Props {
data: DataFrame[];
dataFrameIndex: number;
isLoading: boolean;
onSelectedFrameChanged: (item: SelectableValue<number>) => void;
}
export class InspectDataTab extends PureComponent<Props> {
constructor(props: Props) {
super(props);
}
exportCsv = (dataFrame: DataFrame) => {
const dataFrameCsv = toCSV([dataFrame]);
const blob = new Blob([dataFrameCsv], {
type: 'application/csv;charset=utf-8',
});
saveAs(blob, dataFrame.name + '-' + new Date().getUTCDate() + '.csv');
};
render() {
const { data, dataFrameIndex, isLoading, onSelectedFrameChanged } = this.props;
const styles = getPanelInspectorStyles();
if (isLoading) {
return (
<div>
Loading <Icon name="fa fa-spinner" className="fa-spin" size="lg" />
</div>
);
}
if (!data || !data.length) {
return <div>No Data</div>;
}
const choices = data.map((frame, index) => {
return {
value: index,
label: `${frame.name} (${index})`,
};
});
const processed = applyFieldOverrides({
data,
theme: config.theme,
fieldConfig: { defaults: {}, overrides: [] },
replaceVariables: (value: string) => {
return value;
},
});
return (
<div className={styles.dataTabContent}>
<div className={styles.toolbar}>
{choices.length > 1 && (
<div className={styles.dataFrameSelect}>
<Select options={choices} value={dataFrameIndex} onChange={onSelectedFrameChanged} />
</div>
)}
<div className={styles.downloadCsv}>
<Button variant="primary" onClick={() => this.exportCsv(processed[dataFrameIndex])}>
Download CSV
</Button>
</div>
</div>
<div style={{ flexGrow: 1 }}>
<AutoSizer>
{({ width, height }) => {
if (width === 0) {
return null;
}
return (
<div style={{ width, height }}>
<Table width={width} height={height} data={processed[dataFrameIndex]} />
</div>
);
}}
</AutoSizer>
</div>
</div>
);
}
}
...@@ -32,11 +32,12 @@ export const InspectHeader: FC<Props> = ({ ...@@ -32,11 +32,12 @@ export const InspectHeader: FC<Props> = ({
return ( return (
<div className={styles.header}> <div className={styles.header}>
<div className={styles.actions}> <div className={styles.actions}>
<IconButton name="angle-left" size="xl" onClick={onToggleExpand} surface="header" /> {!isExpanded && <IconButton name="angle-left" size="xl" onClick={onToggleExpand} surface="header" />}
{isExpanded && <IconButton name="angle-right" size="xl" onClick={onToggleExpand} surface="header" />}
<IconButton name="times" size="xl" onClick={onClose} surface="header" /> <IconButton name="times" size="xl" onClick={onClose} surface="header" />
</div> </div>
<div className={styles.titleWrapper}> <div className={styles.titleWrapper}>
<h3>{panel.title}</h3> <h3>{panel.title || 'Panel inspect'}</h3>
<div className="muted">{formatStats(panelData)}</div> <div className="muted">{formatStats(panelData)}</div>
</div> </div>
<TabsBar className={styles.tabsBar}> <TabsBar className={styles.tabsBar}>
......
import React, { PureComponent } from 'react';
import { chain } from 'lodash';
import { PanelData, SelectableValue, AppEvents } from '@grafana/data';
import { TextArea, Button, Select, ClipboardButton, JSONFormatter, Field } from '@grafana/ui';
import { appEvents } from 'app/core/core';
import { PanelModel, DashboardModel } from '../../state';
import { getPanelInspectorStyles } from './styles';
enum ShowContent {
PanelJSON = 'panel',
PanelData = 'data',
DataStructure = 'structure',
}
const options: Array<SelectableValue<ShowContent>> = [
{
label: 'Panel JSON',
description: 'The model saved in the dashboard JSON that configures how everythign works.',
value: ShowContent.PanelJSON,
},
{
label: 'Panel data',
description: 'The raw model passed to the panel visualization',
value: ShowContent.PanelData,
},
{
label: 'DataFrame structure',
description: 'Response info without any values',
value: ShowContent.DataStructure,
},
];
interface Props {
dashboard: DashboardModel;
panel: PanelModel;
data: PanelData;
onClose: () => void;
}
interface State {
show: ShowContent;
text: string;
}
export class InspectJSONTab extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
show: ShowContent.PanelJSON,
text: getSaveModelJSON(props.panel),
};
}
onSelectChanged = (item: SelectableValue<ShowContent>) => {
let text = '';
if (item.value === ShowContent.PanelJSON) {
text = getSaveModelJSON(this.props.panel);
}
this.setState({ text, show: item.value });
};
onTextChanged = (e: React.FormEvent<HTMLTextAreaElement>) => {
const text = e.currentTarget.value;
this.setState({ text });
};
getJSONObject = (show: ShowContent): any => {
if (show === ShowContent.PanelData) {
return this.props.data;
}
if (show === ShowContent.DataStructure) {
const series = this.props.data?.series;
if (!series) {
return { note: 'Missing Response Data' };
}
return this.props.data.series.map(frame => {
const fields = frame.fields.map(field => {
return chain(field)
.omit('values')
.omit('calcs')
.omit('display')
.value();
});
return {
...frame,
fields,
};
});
}
if (show === ShowContent.PanelJSON) {
return this.props.panel.getSaveModel();
}
return { note: 'Unknown Object', show };
};
getClipboardText = () => {
const { show } = this.state;
const obj = this.getJSONObject(show);
return JSON.stringify(obj, null, 2);
};
onClipboardCopied = () => {
appEvents.emit(AppEvents.alertSuccess, ['Content copied to clipboard']);
alert('TODO... the notice is behind the inspector!');
};
onApplyPanelModel = () => {
const { panel, dashboard, onClose } = this.props;
try {
if (!dashboard.meta.canEdit) {
appEvents.emit(AppEvents.alertError, ['Unable to apply']);
} else {
const updates = JSON.parse(this.state.text);
panel.restoreModel(updates);
panel.refresh();
appEvents.emit(AppEvents.alertSuccess, ['Panel model updated']);
}
} catch (err) {
console.log('Error applyign updates', err);
appEvents.emit(AppEvents.alertError, ['Invalid JSON text']);
}
onClose();
};
renderPanelJSON(styles: any) {
return (
<TextArea spellCheck={false} value={this.state.text} onChange={this.onTextChanged} className={styles.editor} />
);
}
render() {
const { dashboard } = this.props;
const { show } = this.state;
const selected = options.find(v => v.value === show);
const isPanelJSON = show === ShowContent.PanelJSON;
const canEdit = dashboard.meta.canEdit;
const styles = getPanelInspectorStyles();
return (
<>
<div className={styles.toolbar}>
<Field label="Select source" className="flex-grow-1">
<Select options={options} value={selected} onChange={this.onSelectChanged} />
</Field>
<ClipboardButton
variant="secondary"
className={styles.toolbarItem}
getText={this.getClipboardText}
onClipboardCopy={this.onClipboardCopied}
>
Copy to clipboard
</ClipboardButton>
{isPanelJSON && canEdit && (
<Button className={styles.toolbarItem} onClick={this.onApplyPanelModel}>
Apply
</Button>
)}
</div>
<div className={styles.content}>
{isPanelJSON ? (
this.renderPanelJSON(styles)
) : (
<div className={styles.viewer}>
<JSONFormatter json={this.getJSONObject(show)} />
</div>
)}
</div>
</>
);
}
}
function getSaveModelJSON(panel: PanelModel): string {
return JSON.stringify(panel.getSaveModel(), null, 2);
}
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard'; import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
import { JSONFormatter, LoadingPlaceholder, Icon } from '@grafana/ui'; import { JSONFormatter, LoadingPlaceholder, Button } from '@grafana/ui';
import { CoreEvents } from 'app/types'; import { CoreEvents } from 'app/types';
import { AppEvents, PanelEvents } from '@grafana/data'; import { AppEvents, PanelEvents } from '@grafana/data';
import { PanelModel } from 'app/features/dashboard/state';
import { getPanelInspectorStyles } from './styles';
interface DsQuery { interface DsQuery {
isLoading: boolean; isLoading: boolean;
...@@ -11,7 +13,7 @@ interface DsQuery { ...@@ -11,7 +13,7 @@ interface DsQuery {
} }
interface Props { interface Props {
panel: any; panel: PanelModel;
} }
interface State { interface State {
...@@ -39,15 +41,15 @@ export class QueryInspector extends PureComponent<Props, State> { ...@@ -39,15 +41,15 @@ export class QueryInspector extends PureComponent<Props, State> {
} }
componentDidMount() { componentDidMount() {
const { panel } = this.props;
appEvents.on(CoreEvents.dsRequestResponse, this.onDataSourceResponse); appEvents.on(CoreEvents.dsRequestResponse, this.onDataSourceResponse);
appEvents.on(CoreEvents.dsRequestError, this.onRequestError); appEvents.on(CoreEvents.dsRequestError, this.onRequestError);
this.props.panel.events.on(PanelEvents.refresh, this.onPanelRefresh);
panel.events.on(PanelEvents.refresh, this.onPanelRefresh);
panel.refresh();
} }
onIssueNewQuery = () => {
this.props.panel.refresh();
};
componentWillUnmount() { componentWillUnmount() {
const { panel } = this.props; const { panel } = this.props;
...@@ -177,46 +179,58 @@ export class QueryInspector extends PureComponent<Props, State> { ...@@ -177,46 +179,58 @@ export class QueryInspector extends PureComponent<Props, State> {
})); }));
}; };
renderExpandCollapse = () => {
const { allNodesExpanded } = this.state;
const collapse = (
<>
<Icon name="minus-circle" /> Collapse All
</>
);
const expand = (
<>
<Icon name="plus-circle" /> Expand All
</>
);
return allNodesExpanded ? collapse : expand;
};
render() { render() {
const { allNodesExpanded } = this.state;
const { response, isLoading } = this.state.dsQuery; const { response, isLoading } = this.state.dsQuery;
const openNodes = this.getNrOfOpenNodes(); const openNodes = this.getNrOfOpenNodes();
const styles = getPanelInspectorStyles();
if (isLoading) { const haveData = Object.keys(response).length > 0;
return <LoadingPlaceholder text="Loading query inspector..." />;
}
return ( return (
<> <>
<div className="pull-right"> <div>
<button className="btn btn-transparent btn-p-x-0 m-r-1" onClick={this.onToggleExpand}> <h3 className="section-heading">Query inspector</h3>
{this.renderExpandCollapse()} <p className="small muted">
</button> Query inspector allows you to view raw request and response. To collect this data Grafana needs to issue a
<CopyToClipboard new query. Hit refresh button below to trigger a new query.
className="btn btn-transparent btn-p-x-0" </p>
text={this.getTextForClipboard} </div>
onSuccess={this.onClipboardSuccess} <div className={styles.toolbar}>
> <Button icon="sync" onClick={this.onIssueNewQuery}>
<Icon name="copy" /> Copy to Clipboard Refresh
</CopyToClipboard> </Button>
{haveData && allNodesExpanded && (
<Button icon="minus" variant="secondary" className={styles.toolbarItem} onClick={this.onToggleExpand}>
Collapse all
</Button>
)}
{haveData && !allNodesExpanded && (
<Button icon="plus" variant="secondary" className={styles.toolbarItem} onClick={this.onToggleExpand}>
Expand all
</Button>
)}
{haveData && (
<CopyToClipboard
text={this.getTextForClipboard}
onSuccess={this.onClipboardSuccess}
elType="div"
className={styles.toolbarItem}
>
<Button icon="copy" variant="secondary">
Copy to clipboard
</Button>
</CopyToClipboard>
)}
</div>
<div className={styles.contentQueryInspector}>
{isLoading && <LoadingPlaceholder text="Loading query inspector..." />}
{!isLoading && haveData && (
<JSONFormatter json={response} open={openNodes} onDidRender={this.setFormattedJson} />
)}
{!isLoading && !haveData && <p className="muted">No request & response collected yet. Hit refresh button</p>}
</div> </div>
<JSONFormatter json={response} open={openNodes} onDidRender={this.setFormattedJson} />
</> </>
); );
} }
......
import { css } from 'emotion';
import { config } from 'app/core/config';
import { stylesFactory } from '@grafana/ui';
export const getPanelInspectorStyles = stylesFactory(() => {
return {
wrap: css`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
flex: 1 1 0;
`,
toolbar: css`
display: flex;
width: 100%;
flex-grow: 0;
align-items: center;
justify-content: flex-end;
`,
toolbarItem: css`
margin-left: ${config.theme.spacing.md};
`,
content: css`
flex-grow: 1;
padding-bottom: 16px;
`,
contentQueryInspector: css`
flex-grow: 1;
padding: ${config.theme.spacing.md} 0;
`,
editor: css`
font-family: monospace;
height: 100%;
flex-grow: 1;
`,
viewer: css`
overflow: scroll;
`,
dataFrameSelect: css`
flex-grow: 2;
`,
downloadCsv: css`
margin-left: 16px;
`,
tabContent: css`
height: 100%;
display: flex;
flex-direction: column;
`,
dataTabContent: css`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
`,
};
});
...@@ -236,6 +236,22 @@ export class DashboardPage extends PureComponent<Props, State> { ...@@ -236,6 +236,22 @@ export class DashboardPage extends PureComponent<Props, State> {
); );
} }
getInspectPanel() {
const { dashboard, inspectPanelId } = this.props;
if (!dashboard || !inspectPanelId) {
return null;
}
const inspectPanel = dashboard.getPanelById(parseInt(inspectPanelId, 10));
// cannot inspect panels plugin is not already loaded
if (!inspectPanel) {
return null;
}
return inspectPanel;
}
render() { render() {
const { const {
dashboard, dashboard,
...@@ -243,7 +259,6 @@ export class DashboardPage extends PureComponent<Props, State> { ...@@ -243,7 +259,6 @@ export class DashboardPage extends PureComponent<Props, State> {
$injector, $injector,
isInitSlow, isInitSlow,
initError, initError,
inspectPanelId,
inspectTab, inspectTab,
isNewEditorOpen, isNewEditorOpen,
updateLocation, updateLocation,
...@@ -264,11 +279,9 @@ export class DashboardPage extends PureComponent<Props, State> { ...@@ -264,11 +279,9 @@ export class DashboardPage extends PureComponent<Props, State> {
'dashboard-container--has-submenu': dashboard.meta.submenuEnabled, 'dashboard-container--has-submenu': dashboard.meta.submenuEnabled,
}); });
// Find the panel to inspect
const inspectPanel = inspectPanelId ? dashboard.getPanelById(parseInt(inspectPanelId, 10)) : null;
// Only trigger render when the scroll has moved by 25 // Only trigger render when the scroll has moved by 25
const approximateScrollTop = Math.round(scrollTop / 25) * 25; const approximateScrollTop = Math.round(scrollTop / 25) * 25;
const inspectPanel = this.getInspectPanel();
return ( return (
<div> <div>
...@@ -297,7 +310,7 @@ export class DashboardPage extends PureComponent<Props, State> { ...@@ -297,7 +310,7 @@ export class DashboardPage extends PureComponent<Props, State> {
</CustomScrollbar> </CustomScrollbar>
</div> </div>
{inspectPanel && <PanelInspector dashboard={dashboard} panel={inspectPanel} selectedTab={inspectTab} />} {inspectPanel && <PanelInspector dashboard={dashboard} panel={inspectPanel} defaultTab={inspectTab} />}
{editPanel && <PanelEditor dashboard={dashboard} sourcePanel={editPanel} />} {editPanel && <PanelEditor dashboard={dashboard} sourcePanel={editPanel} />}
{editview && <DashboardSettings dashboard={dashboard} updateLocation={updateLocation} />} {editview && <DashboardSettings dashboard={dashboard} updateLocation={updateLocation} />}
</div> </div>
...@@ -319,7 +332,7 @@ export const mapStateToProps = (state: StoreState) => ({ ...@@ -319,7 +332,7 @@ export const mapStateToProps = (state: StoreState) => ({
isInitSlow: state.dashboard.isInitSlow, isInitSlow: state.dashboard.isInitSlow,
initError: state.dashboard.initError, initError: state.dashboard.initError,
dashboard: state.dashboard.getModel() as DashboardModel, dashboard: state.dashboard.getModel() as DashboardModel,
inspectTab: state.location.query.tab, inspectTab: state.location.query.inspectTab,
isNewEditorOpen: state.panelEditorNew.isOpen, isNewEditorOpen: state.panelEditorNew.isOpen,
}); });
......
...@@ -4,9 +4,9 @@ import _ from 'lodash'; ...@@ -4,9 +4,9 @@ import _ from 'lodash';
// Components // Components
import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { QueryInspector } from './QueryInspector';
import { QueryOptions } from './QueryOptions'; import { QueryOptions } from './QueryOptions';
import { PanelOptionsGroup } from '@grafana/ui'; import { PanelOptionsGroup } from '@grafana/ui';
import { getLocationSrv } from '@grafana/runtime';
import { QueryEditorRows } from './QueryEditorRows'; import { QueryEditorRows } from './QueryEditorRows';
// Services // Services
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
...@@ -119,9 +119,12 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -119,9 +119,12 @@ export class QueriesTab extends PureComponent<Props, State> {
}); });
}; };
renderQueryInspector = () => { openQueryInspector = () => {
const { panel } = this.props; const { panel } = this.props;
return <QueryInspector panel={panel} />; getLocationSrv().update({
query: { inspect: panel.id, inspectTab: 'query' },
partial: true,
});
}; };
renderHelp = () => { renderHelp = () => {
...@@ -165,7 +168,7 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -165,7 +168,7 @@ export class QueriesTab extends PureComponent<Props, State> {
<div className="flex-grow-1" /> <div className="flex-grow-1" />
{showAddButton && ( {showAddButton && (
<button className="btn navbar-button" onClick={this.onAddQueryClick}> <button className="btn navbar-button" onClick={this.onAddQueryClick}>
Add Query Add query
</button> </button>
)} )}
{isAddingMixed && this.renderMixedPicker()} {isAddingMixed && this.renderMixedPicker()}
...@@ -244,8 +247,8 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -244,8 +247,8 @@ export class QueriesTab extends PureComponent<Props, State> {
render() { render() {
const { scrollTop } = this.state; const { scrollTop } = this.state;
const queryInspector: EditorToolbarView = { const queryInspector: EditorToolbarView = {
title: 'Query Inspector', title: 'Query inspector',
render: this.renderQueryInspector, onClick: this.openQueryInspector,
}; };
const dsHelp: EditorToolbarView = { const dsHelp: EditorToolbarView = {
...@@ -256,7 +259,7 @@ export class QueriesTab extends PureComponent<Props, State> { ...@@ -256,7 +259,7 @@ export class QueriesTab extends PureComponent<Props, State> {
return ( return (
<EditorTabBody <EditorTabBody
heading="Query" heading="Data source"
renderToolbar={this.renderToolbar} renderToolbar={this.renderToolbar}
toolbarItems={[queryInspector, dsHelp]} toolbarItems={[queryInspector, dsHelp]}
setScrollTop={this.setScrollTop} setScrollTop={this.setScrollTop}
......
...@@ -367,6 +367,10 @@ export class DashboardModel { ...@@ -367,6 +367,10 @@ export class DashboardModel {
} }
getPanelById(id: number): PanelModel { getPanelById(id: number): PanelModel {
if (this.panelInEdit && this.panelInEdit.id === id) {
return this.panelInEdit;
}
for (const panel of this.panels) { for (const panel of this.panels) {
if (panel.id === id) { if (panel.id === id) {
return panel; return panel;
......
...@@ -30,8 +30,15 @@ describe('getPanelMenu', () => { ...@@ -30,8 +30,15 @@ describe('getPanelMenu', () => {
Object { Object {
"iconClassName": "info-circle", "iconClassName": "info-circle",
"onClick": [Function], "onClick": [Function],
"shortcut": "p i", "shortcut": "i",
"subMenu": Array [
Object {
"onClick": [Function],
"text": "Panel JSON",
},
],
"text": "Inspect", "text": "Inspect",
"type": "submenu",
}, },
Object { Object {
"iconClassName": "cube", "iconClassName": "cube",
...@@ -46,10 +53,6 @@ describe('getPanelMenu', () => { ...@@ -46,10 +53,6 @@ describe('getPanelMenu', () => {
"onClick": [Function], "onClick": [Function],
"text": "Copy", "text": "Copy",
}, },
Object {
"onClick": [Function],
"text": "Panel JSON",
},
], ],
"text": "More...", "text": "More...",
"type": "submenu", "type": "submenu",
......
...@@ -2,7 +2,7 @@ import { updateLocation } from 'app/core/actions'; ...@@ -2,7 +2,7 @@ import { updateLocation } from 'app/core/actions';
import { store } from 'app/store/store'; import { store } from 'app/store/store';
import { getDataSourceSrv, getLocationSrv, AngularComponent } from '@grafana/runtime'; import { getDataSourceSrv, getLocationSrv, AngularComponent } from '@grafana/runtime';
import { PanelMenuItem } from '@grafana/data'; import { PanelMenuItem } from '@grafana/data';
import { copyPanel, duplicatePanel, editPanelJson, removePanel, sharePanel } from 'app/features/dashboard/utils/panel'; import { copyPanel, duplicatePanel, removePanel, sharePanel } from 'app/features/dashboard/utils/panel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { contextSrv } from '../../../core/services/context_srv'; import { contextSrv } from '../../../core/services/context_srv';
...@@ -45,12 +45,14 @@ export function getPanelMenu( ...@@ -45,12 +45,14 @@ export function getPanelMenu(
sharePanel(dashboard, panel); sharePanel(dashboard, panel);
}; };
const onInspectPanel = (event: React.MouseEvent<any>) => { const onInspectPanel = (tab?: string) => {
event.preventDefault(); event.preventDefault();
getLocationSrv().update({ getLocationSrv().update({
partial: true, partial: true,
query: { query: {
inspect: panel.id, inspect: panel.id,
inspectTab: tab,
}, },
}); });
}; };
...@@ -69,11 +71,6 @@ export function getPanelMenu( ...@@ -69,11 +71,6 @@ export function getPanelMenu(
copyPanel(panel); copyPanel(panel);
}; };
const onEditPanelJson = (event: React.MouseEvent<any>) => {
event.preventDefault();
editPanelJson(dashboard, panel);
};
const onRemovePanel = (event: React.MouseEvent<any>) => { const onRemovePanel = (event: React.MouseEvent<any>) => {
event.preventDefault(); event.preventDefault();
removePanel(dashboard, panel, true); removePanel(dashboard, panel, true);
...@@ -119,11 +116,35 @@ export function getPanelMenu( ...@@ -119,11 +116,35 @@ export function getPanelMenu(
}); });
} }
const inspectMenu: PanelMenuItem[] = [];
// Only show these inspect actions for data plugins
if (panel.plugin && !panel.plugin.meta.skipDataQuery) {
inspectMenu.push({
text: 'Data',
onClick: (e: React.MouseEvent<any>) => onInspectPanel('data'),
});
if (dashboard.meta.canEdit) {
inspectMenu.push({
text: 'Query',
onClick: (e: React.MouseEvent<any>) => onInspectPanel('query'),
});
}
}
inspectMenu.push({
text: 'Panel JSON',
onClick: (e: React.MouseEvent<any>) => onInspectPanel('json'),
});
menu.push({ menu.push({
type: 'submenu',
text: 'Inspect', text: 'Inspect',
iconClassName: 'info-circle', iconClassName: 'info-circle',
onClick: onInspectPanel, onClick: (e: React.MouseEvent<any>) => onInspectPanel(),
shortcut: 'p i', shortcut: 'i',
subMenu: inspectMenu,
}); });
const subMenu: PanelMenuItem[] = []; const subMenu: PanelMenuItem[] = [];
...@@ -141,11 +162,6 @@ export function getPanelMenu( ...@@ -141,11 +162,6 @@ export function getPanelMenu(
}); });
} }
subMenu.push({
text: 'Panel JSON',
onClick: onEditPanelJson,
});
// add old angular panel options // add old angular panel options
if (angularComponent) { if (angularComponent) {
const scope = angularComponent.getScope(); const scope = angularComponent.getScope();
......
...@@ -3,7 +3,7 @@ import store from 'app/core/store'; ...@@ -3,7 +3,7 @@ import store from 'app/core/store';
// Models // Models
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel, panelRemoved, panelAdded } from 'app/features/dashboard/state/PanelModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { TimeRange, AppEvents } from '@grafana/data'; import { TimeRange, AppEvents } from '@grafana/data';
// Utils // Utils
...@@ -51,38 +51,6 @@ export const copyPanel = (panel: PanelModel) => { ...@@ -51,38 +51,6 @@ export const copyPanel = (panel: PanelModel) => {
appEvents.emit(AppEvents.alertSuccess, ['Panel copied. Open Add Panel to paste']); appEvents.emit(AppEvents.alertSuccess, ['Panel copied. Open Add Panel to paste']);
}; };
const replacePanel = (dashboard: DashboardModel, newPanel: PanelModel, oldPanel: PanelModel) => {
const index = dashboard.panels.findIndex(panel => {
return panel.id === oldPanel.id;
});
const deletedPanel = dashboard.panels.splice(index, 1)[0];
dashboard.events.emit(panelRemoved, deletedPanel);
newPanel = new PanelModel(newPanel);
newPanel.id = oldPanel.id;
dashboard.panels.splice(index, 0, newPanel);
dashboard.sortPanelsByGridPos();
dashboard.events.emit(panelAdded, newPanel);
};
export const editPanelJson = (dashboard: DashboardModel, panel: PanelModel) => {
const model = {
object: panel.getSaveModel(),
updateHandler: (newPanel: PanelModel, oldPanel: PanelModel) => {
replacePanel(dashboard, newPanel, oldPanel);
},
canUpdate: dashboard.meta.canEdit,
enableCopy: true,
};
appEvents.emit(CoreEvents.showModal, {
src: 'public/app/partials/edit_json.html',
model: model,
});
};
export const sharePanel = (dashboard: DashboardModel, panel: PanelModel) => { export const sharePanel = (dashboard: DashboardModel, panel: PanelModel) => {
appEvents.emit(CoreEvents.showModalReact, { appEvents.emit(CoreEvents.showModalReact, {
component: ShareModal, component: ShareModal,
......
...@@ -21,15 +21,18 @@ p { ...@@ -21,15 +21,18 @@ p {
// Ex: 14px base font * 85% = about 12px // Ex: 14px base font * 85% = about 12px
small { small {
font-size: 85%; font-size: $font-size-sm;
} }
strong { strong {
font-weight: $font-weight-semi-bold; font-weight: $font-weight-semi-bold;
} }
em { em {
font-style: italic; font-style: italic;
color: $headings-color; color: $headings-color;
} }
cite { cite {
font-style: normal; font-style: normal;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment