mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
parent
cff70b6648
commit
17059489d8
@ -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> = ({
|
|||||||
</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>) => (
|
|||||||
</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">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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 = () => {
|
|||||||
// ---
|
// ---
|
||||||
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 = () => {
|
|||||||
|
|
||||||
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';
|
|||||||
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 {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import './json_editor_ctrl';
|
|
||||||
import './invited_ctrl';
|
import './invited_ctrl';
|
||||||
import './signup_ctrl';
|
import './signup_ctrl';
|
||||||
import './reset_password_ctrl';
|
import './reset_password_ctrl';
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
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';
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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) {
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
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> = ({
|
|||||||
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}>
|
||||||
|
@ -0,0 +1,178 @@
|
|||||||
|
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);
|
||||||
|
}
|
@ -1,99 +1,122 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import { Unsubscribable } from 'rxjs';
|
||||||
import { saveAs } from 'file-saver';
|
import { connect, MapStateToProps } from 'react-redux';
|
||||||
import { css } from 'emotion';
|
|
||||||
|
|
||||||
import { InspectHeader } from './InspectHeader';
|
import { InspectHeader } from './InspectHeader';
|
||||||
|
import { InspectJSONTab } from './InspectJSONTab';
|
||||||
|
import { QueryInspector } from './QueryInspector';
|
||||||
|
|
||||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||||
import {
|
import { JSONFormatter, Drawer, TabContent, CustomScrollbar } from '@grafana/ui';
|
||||||
JSONFormatter,
|
|
||||||
Drawer,
|
|
||||||
LegacyForms,
|
|
||||||
Table,
|
|
||||||
TabContent,
|
|
||||||
stylesFactory,
|
|
||||||
CustomScrollbar,
|
|
||||||
Button,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
const { Select } = LegacyForms;
|
|
||||||
import { getLocationSrv, getDataSourceSrv } from '@grafana/runtime';
|
import { getLocationSrv, getDataSourceSrv } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
SelectableValue,
|
SelectableValue,
|
||||||
applyFieldOverrides,
|
getDisplayProcessor,
|
||||||
toCSV,
|
|
||||||
DataQueryError,
|
DataQueryError,
|
||||||
PanelData,
|
PanelData,
|
||||||
getValueFormat,
|
FieldType,
|
||||||
formattedValueToString,
|
formattedValueToString,
|
||||||
QueryResultMetaStat,
|
QueryResultMetaStat,
|
||||||
|
LoadingState,
|
||||||
|
PanelPlugin,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
|
import { getPanelInspectorStyles } from './styles';
|
||||||
|
import { StoreState } from 'app/types';
|
||||||
|
import { InspectDataTab } from './InspectDataTab';
|
||||||
|
|
||||||
interface Props {
|
interface OwnProps {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
selectedTab: InspectTab;
|
defaultTab: InspectTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConnectedProps {
|
||||||
|
plugin?: PanelPlugin | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = OwnProps & ConnectedProps;
|
||||||
|
|
||||||
export enum InspectTab {
|
export enum InspectTab {
|
||||||
Data = 'data',
|
Data = 'data',
|
||||||
Request = 'request',
|
|
||||||
Issue = 'issue',
|
|
||||||
Meta = 'meta', // When result metadata exists
|
Meta = 'meta', // When result metadata exists
|
||||||
Error = 'error',
|
Error = 'error',
|
||||||
Stats = 'stats',
|
Stats = 'stats',
|
||||||
PanelJson = 'paneljson',
|
JSON = 'json',
|
||||||
|
Query = 'query',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
isLoading: boolean;
|
||||||
// The last raw response
|
// The last raw response
|
||||||
last: PanelData;
|
last: PanelData;
|
||||||
|
|
||||||
// Data from the last response
|
// Data from the last response
|
||||||
data: DataFrame[];
|
data: DataFrame[];
|
||||||
|
|
||||||
// The selected data frame
|
// The selected data frame
|
||||||
selected: number;
|
selectedDataFrame: number;
|
||||||
|
|
||||||
// The Selected Tab
|
// The Selected Tab
|
||||||
tab: InspectTab;
|
currentTab: InspectTab;
|
||||||
|
|
||||||
// If the datasource supports custom metadata
|
// If the datasource supports custom metadata
|
||||||
metaDS?: DataSourceApi;
|
metaDS?: DataSourceApi;
|
||||||
|
// drawer width
|
||||||
drawerWidth: string;
|
drawerWidth: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PanelInspector extends PureComponent<Props, State> {
|
export class PanelInspectorUnconnected extends PureComponent<Props, State> {
|
||||||
|
querySubscription?: Unsubscribable;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
isLoading: true,
|
||||||
last: {} as PanelData,
|
last: {} as PanelData,
|
||||||
data: [],
|
data: [],
|
||||||
selected: 0,
|
selectedDataFrame: 0,
|
||||||
tab: props.selectedTab || InspectTab.Data,
|
currentTab: props.defaultTab ?? InspectTab.Data,
|
||||||
drawerWidth: '50%',
|
drawerWidth: '50%',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
const { panel } = this.props;
|
const { plugin } = this.props;
|
||||||
|
|
||||||
if (!panel) {
|
if (plugin) {
|
||||||
this.onDismiss(); // Try to close the component
|
this.init();
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastResult = panel.getQueryRunner().getLastResult();
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
if (prevProps.plugin !== this.props.plugin) {
|
||||||
if (!lastResult) {
|
this.init();
|
||||||
this.onDismiss(); // Usually opened from refresh?
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This init process where we do not have a plugin to start with is to handle full page reloads with inspect url parameter
|
||||||
|
* When this inspect drawer loads the plugin is not yet loaded.
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
const { plugin, panel } = this.props;
|
||||||
|
|
||||||
|
if (plugin && !plugin.meta.skipDataQuery) {
|
||||||
|
this.querySubscription = panel
|
||||||
|
.getQueryRunner()
|
||||||
|
.getData()
|
||||||
|
.subscribe({
|
||||||
|
next: data => this.onUpdateData(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.querySubscription) {
|
||||||
|
this.querySubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onUpdateData(lastResult: PanelData) {
|
||||||
let metaDS: DataSourceApi;
|
let metaDS: DataSourceApi;
|
||||||
const data = lastResult.series;
|
const data = lastResult.series;
|
||||||
const error = lastResult.error;
|
const error = lastResult.error;
|
||||||
@ -117,16 +140,17 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
// Set last result, but no metadata inspector
|
// Set last result, but no metadata inspector
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
|
isLoading: lastResult.state === LoadingState.Loading,
|
||||||
last: lastResult,
|
last: lastResult,
|
||||||
data,
|
data,
|
||||||
metaDS,
|
metaDS,
|
||||||
tab: error ? InspectTab.Error : prevState.tab,
|
currentTab: error ? InspectTab.Error : prevState.currentTab,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
onDismiss = () => {
|
onClose = () => {
|
||||||
getLocationSrv().update({
|
getLocationSrv().update({
|
||||||
query: { inspect: null, tab: null },
|
query: { inspect: null, inspectTab: null },
|
||||||
partial: true,
|
partial: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -138,21 +162,11 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onSelectTab = (item: SelectableValue<InspectTab>) => {
|
onSelectTab = (item: SelectableValue<InspectTab>) => {
|
||||||
this.setState({ tab: item.value || InspectTab.Data });
|
this.setState({ currentTab: item.value || InspectTab.Data });
|
||||||
};
|
};
|
||||||
|
|
||||||
onSelectedFrameChanged = (item: SelectableValue<number>) => {
|
onSelectedFrameChanged = (item: SelectableValue<number>) => {
|
||||||
this.setState({ selected: item.value || 0 });
|
this.setState({ selectedDataFrame: item.value || 0 });
|
||||||
};
|
|
||||||
|
|
||||||
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');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
renderMetadataInspector() {
|
renderMetadataInspector() {
|
||||||
@ -164,63 +178,15 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderDataTab() {
|
renderDataTab() {
|
||||||
const { data, selected } = this.state;
|
const { last, isLoading, selectedDataFrame } = this.state;
|
||||||
const styles = getStyles();
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className={styles.dataTabContent}>
|
<InspectDataTab
|
||||||
<div className={styles.toolbar}>
|
data={last.series}
|
||||||
{choices.length > 1 && (
|
isLoading={isLoading}
|
||||||
<div className={styles.dataFrameSelect}>
|
dataFrameIndex={selectedDataFrame}
|
||||||
<Select
|
onSelectedFrameChanged={this.onSelectedFrameChanged}
|
||||||
options={choices}
|
|
||||||
value={choices.find(t => t.value === selected)}
|
|
||||||
onChange={this.onSelectedFrameChanged}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.downloadCsv}>
|
|
||||||
<Button variant="primary" onClick={() => this.exportCsv(processed[selected])}>
|
|
||||||
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[selected]} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,18 +205,6 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
return <div>{error.message}</div>;
|
return <div>{error.message}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRequestTab() {
|
|
||||||
return (
|
|
||||||
<CustomScrollbar>
|
|
||||||
<JSONFormatter json={this.state.last} open={2} />
|
|
||||||
</CustomScrollbar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderJsonModelTab() {
|
|
||||||
return <JSONFormatter json={this.props.panel.getSaveModel()} open={2} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderStatsTab() {
|
renderStatsTab() {
|
||||||
const { last } = this.state;
|
const { last } = this.state;
|
||||||
const { request } = last;
|
const { request } = last;
|
||||||
@ -285,12 +239,16 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.renderStatsTable('Stats', stats)}
|
{this.renderStatsTable('Stats', stats)}
|
||||||
{dataStats.length && this.renderStatsTable('Data source stats', dataStats)}
|
{this.renderStatsTable('Data source stats', dataStats)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderStatsTable(name: string, stats: QueryResultMetaStat[]) {
|
renderStatsTable(name: string, stats: QueryResultMetaStat[]) {
|
||||||
|
if (!stats || !stats.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ paddingBottom: '16px' }}>
|
<div style={{ paddingBottom: '16px' }}>
|
||||||
<div className="section-heading">{name}</div>
|
<div className="section-heading">{name}</div>
|
||||||
@ -300,7 +258,7 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<tr key={`${stat.title}-${index}`}>
|
<tr key={`${stat.title}-${index}`}>
|
||||||
<td>{stat.title}</td>
|
<td>{stat.title}</td>
|
||||||
<td style={{ textAlign: 'right' }}>{formatStat(stat.value, stat.unit)}</td>
|
<td style={{ textAlign: 'right' }}>{formatStat(stat)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -310,93 +268,109 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawerHeader = () => {
|
drawerHeader(tabs: Array<{ label: string; value: InspectTab }>, activeTab: InspectTab) {
|
||||||
const { tab, last } = this.state;
|
const { panel } = this.props;
|
||||||
|
const { last } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InspectHeader
|
||||||
|
tabs={tabs}
|
||||||
|
tab={activeTab}
|
||||||
|
panelData={last}
|
||||||
|
onSelectTab={this.onSelectTab}
|
||||||
|
onClose={this.onClose}
|
||||||
|
panel={panel}
|
||||||
|
onToggleExpand={this.onToggleExpand}
|
||||||
|
isExpanded={this.state.drawerWidth === '100%'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTabs() {
|
||||||
|
const { dashboard, plugin } = this.props;
|
||||||
|
const { last } = this.state;
|
||||||
const error = last?.error;
|
const error = last?.error;
|
||||||
const tabs = [];
|
const tabs = [];
|
||||||
|
|
||||||
if (last && last?.series?.length > 0) {
|
if (plugin && !plugin.meta.skipDataQuery) {
|
||||||
tabs.push({ label: 'Data', value: InspectTab.Data });
|
tabs.push({ label: 'Data', value: InspectTab.Data });
|
||||||
}
|
|
||||||
|
|
||||||
tabs.push({ label: 'Stats', value: InspectTab.Stats });
|
tabs.push({ label: 'Stats', value: InspectTab.Stats });
|
||||||
tabs.push({ label: 'Request', value: InspectTab.Request });
|
}
|
||||||
tabs.push({ label: 'Panel JSON', value: InspectTab.PanelJson });
|
|
||||||
|
|
||||||
if (this.state.metaDS) {
|
if (this.state.metaDS) {
|
||||||
tabs.push({ label: 'Meta Data', value: InspectTab.Meta });
|
tabs.push({ label: 'Meta Data', value: InspectTab.Meta });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tabs.push({ label: 'JSON', value: InspectTab.JSON });
|
||||||
|
|
||||||
if (error && error.message) {
|
if (error && error.message) {
|
||||||
tabs.push({ label: 'Error', value: InspectTab.Error });
|
tabs.push({ label: 'Error', value: InspectTab.Error });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (dashboard.meta.canEdit) {
|
||||||
<InspectHeader
|
tabs.push({ label: 'Query', value: InspectTab.Query });
|
||||||
tabs={tabs}
|
}
|
||||||
tab={tab}
|
return tabs;
|
||||||
panelData={last}
|
}
|
||||||
onSelectTab={this.onSelectTab}
|
|
||||||
onClose={this.onDismiss}
|
|
||||||
panel={this.props.panel}
|
|
||||||
onToggleExpand={this.onToggleExpand}
|
|
||||||
isExpanded={this.state.drawerWidth === '100%'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { last, tab, drawerWidth } = this.state;
|
const { panel, dashboard, plugin } = this.props;
|
||||||
const styles = getStyles();
|
const { currentTab } = this.state;
|
||||||
|
|
||||||
|
if (!plugin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { last, drawerWidth } = this.state;
|
||||||
|
const styles = getPanelInspectorStyles();
|
||||||
const error = last?.error;
|
const error = last?.error;
|
||||||
|
const tabs = this.getTabs();
|
||||||
|
|
||||||
|
// Validate that the active tab is actually valid and allowed
|
||||||
|
let activeTab = currentTab;
|
||||||
|
if (!tabs.find(item => item.value === currentTab)) {
|
||||||
|
activeTab = InspectTab.JSON;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer title={this.drawerHeader} width={drawerWidth} onClose={this.onDismiss}>
|
<Drawer title={this.drawerHeader(tabs, activeTab)} width={drawerWidth} onClose={this.onClose}>
|
||||||
<TabContent className={styles.tabContent}>
|
{activeTab === InspectTab.Data && this.renderDataTab()}
|
||||||
{tab === InspectTab.Data && this.renderDataTab()}
|
|
||||||
<CustomScrollbar autoHeightMin="100%">
|
<CustomScrollbar autoHeightMin="100%">
|
||||||
{tab === InspectTab.Meta && this.renderMetadataInspector()}
|
<TabContent className={styles.tabContent}>
|
||||||
{tab === InspectTab.Request && this.renderRequestTab()}
|
{activeTab === InspectTab.Meta && this.renderMetadataInspector()}
|
||||||
{tab === InspectTab.Error && this.renderErrorTab(error)}
|
{activeTab === InspectTab.JSON && (
|
||||||
{tab === InspectTab.Stats && this.renderStatsTab()}
|
<InspectJSONTab panel={panel} dashboard={dashboard} data={last} onClose={this.onClose} />
|
||||||
{tab === InspectTab.PanelJson && this.renderJsonModelTab()}
|
)}
|
||||||
</CustomScrollbar>
|
{activeTab === InspectTab.Error && this.renderErrorTab(error)}
|
||||||
|
{activeTab === InspectTab.Stats && this.renderStatsTab()}
|
||||||
|
{activeTab === InspectTab.Query && <QueryInspector panel={panel} />}
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
</CustomScrollbar>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatStat(value: any, unit?: string): string {
|
function formatStat(stat: QueryResultMetaStat): string {
|
||||||
if (unit) {
|
const display = getDisplayProcessor({
|
||||||
return formattedValueToString(getValueFormat(unit)(value));
|
field: {
|
||||||
} else {
|
type: FieldType.number,
|
||||||
return value;
|
config: stat,
|
||||||
}
|
},
|
||||||
|
theme: config.theme,
|
||||||
|
});
|
||||||
|
return formattedValueToString(display(stat.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = stylesFactory(() => {
|
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
|
||||||
|
const panelState = state.dashboard.panels[props.panel.id];
|
||||||
|
if (!panelState) {
|
||||||
|
return { plugin: null };
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toolbar: css`
|
plugin: panelState.plugin,
|
||||||
display: flex;
|
|
||||||
margin: 8px 0;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
`,
|
|
||||||
dataFrameSelect: css`
|
|
||||||
flex-grow: 2;
|
|
||||||
`,
|
|
||||||
downloadCsv: css`
|
|
||||||
margin-left: 16px;
|
|
||||||
`,
|
|
||||||
tabContent: css`
|
|
||||||
height: calc(100% - 32px);
|
|
||||||
`,
|
|
||||||
dataTabContent: css`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
`,
|
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export const PanelInspector = connect(mapStateToProps)(PanelInspectorUnconnected);
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
panel: any;
|
panel: PanelModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface 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> {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
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
|
||||||
|
new query. Hit refresh button below to trigger a new query.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.toolbar}>
|
||||||
|
<Button icon="sync" onClick={this.onIssueNewQuery}>
|
||||||
|
Refresh
|
||||||
|
</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
|
<CopyToClipboard
|
||||||
className="btn btn-transparent btn-p-x-0"
|
|
||||||
text={this.getTextForClipboard}
|
text={this.getTextForClipboard}
|
||||||
onSuccess={this.onClipboardSuccess}
|
onSuccess={this.onClipboardSuccess}
|
||||||
|
elType="div"
|
||||||
|
className={styles.toolbarItem}
|
||||||
>
|
>
|
||||||
<Icon name="copy" /> Copy to Clipboard
|
<Button icon="copy" variant="secondary">
|
||||||
|
Copy to clipboard
|
||||||
|
</Button>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.contentQueryInspector}>
|
||||||
|
{isLoading && <LoadingPlaceholder text="Loading query inspector..." />}
|
||||||
|
{!isLoading && haveData && (
|
||||||
<JSONFormatter json={response} open={openNodes} onDidRender={this.setFormattedJson} />
|
<JSONFormatter json={response} open={openNodes} onDidRender={this.setFormattedJson} />
|
||||||
|
)}
|
||||||
|
{!isLoading && !haveData && <p className="muted">No request & response collected yet. Hit refresh button</p>}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
58
public/app/features/dashboard/components/Inspector/styles.ts
Normal file
58
public/app/features/dashboard/components/Inspector/styles.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
|||||||
$injector,
|
$injector,
|
||||||
isInitSlow,
|
isInitSlow,
|
||||||
initError,
|
initError,
|
||||||
inspectPanelId,
|
|
||||||
inspectTab,
|
inspectTab,
|
||||||
isNewEditorOpen,
|
isNewEditorOpen,
|
||||||
updateLocation,
|
updateLocation,
|
||||||
@ -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> {
|
|||||||
</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) => ({
|
|||||||
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';
|
|||||||
// 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> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
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> {
|
|||||||
<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> {
|
|||||||
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> {
|
|||||||
|
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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', () => {
|
|||||||
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', () => {
|
|||||||
"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';
|
|||||||
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(
|
|||||||
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(
|
|||||||
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(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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';
|
|||||||
|
|
||||||
// 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) => {
|
|||||||
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 {
|
|||||||
|
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user