Commit df1d4316 by Torkel Ödegaard Committed by GitHub

NewPanelEditor: Panel editor tabs in state (url) (#22102)

* tabs & style tweaks

* Styling updates

* ok look

* tweaks

* Updated snapshots

* Moved transforms

* Updated
parent 8080bbc8
......@@ -123,7 +123,7 @@ export interface PluginConfigPage<T extends PluginMeta> {
export class GrafanaPlugin<T extends PluginMeta = PluginMeta> {
// Meta is filled in by the plugin loading system
meta?: T;
meta: T;
// This is set if the plugin system had errors loading the plugin
loadError?: boolean;
......@@ -142,4 +142,8 @@ export class GrafanaPlugin<T extends PluginMeta = PluginMeta> {
this.configPages.push(tab);
return this;
}
constructor() {
this.meta = {} as T;
}
}
......@@ -25,6 +25,7 @@ export const getSelectStyles = stylesFactory((theme: GrafanaTheme) => {
flex-direction: row;
white-space: nowrap;
cursor: pointer;
border-left: 2px solid transparent;
&:hover {
background: ${optionBgHover};
}
......
......@@ -8,21 +8,16 @@ import {
DynamicConfigValue,
VariableSuggestionsScope,
} from '@grafana/data';
import {
standardFieldConfigEditorRegistry,
Forms,
fieldMatchersUI,
ControlledCollapse,
ValuePicker,
} from '@grafana/ui';
import { standardFieldConfigEditorRegistry, Forms, fieldMatchersUI, ValuePicker } from '@grafana/ui';
import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
import { OptionsGroup } from './OptionsGroup';
interface Props {
config: FieldConfigSource;
custom?: FieldConfigEditorRegistry; // custom fields
include?: string[]; // Ordered list of which fields should be shown/included
onChange: (config: FieldConfigSource) => void;
// Helpful for IntelliSense
/* Helpful for IntelliSense */
data: DataFrame[];
}
......@@ -33,6 +28,7 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
private setDefaultValue = (name: string, value: any, custom: boolean) => {
const defaults = { ...this.props.config.defaults };
const remove = value === undefined || value === null || '';
if (custom) {
if (defaults.custom) {
if (remove) {
......@@ -236,17 +232,14 @@ export class FieldConfigEditor extends React.PureComponent<Props> {
render() {
return (
<div>
<ControlledCollapse label="Standard Field Configuration" collapsible>
{this.renderStandardConfigs()}
</ControlledCollapse>
{this.props.custom && (
<ControlledCollapse label="Standard Field Configuration">{this.renderCustomConfigs()}</ControlledCollapse>
)}
<ControlledCollapse label="Field Overrides" collapsible>
<OptionsGroup title="Field configuration">{this.renderStandardConfigs()}</OptionsGroup>
{this.props.custom && <OptionsGroup title="Visualization options">{this.renderCustomConfigs()}</OptionsGroup>}
<OptionsGroup title="Field Overrides">
{this.renderOverrides()}
{this.renderAddOverride()}
</ControlledCollapse>
</OptionsGroup>
</div>
);
}
......
import React, { useState, FC } from 'react';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { useTheme, Icon, stylesFactory } from '@grafana/ui';
interface Props {
title: string;
}
export const OptionsGroup: FC<Props> = ({ title, children }) => {
const [isExpanded, toggleExpand] = useState(false);
const theme = useTheme();
const styles = getStyles(theme);
return (
<div className={styles.box}>
<div className={styles.header} onClick={() => toggleExpand(!isExpanded)}>
{title}
<div className={styles.toggle}>
<Icon name={isExpanded ? 'chevron-down' : 'chevron-left'} />
</div>
</div>
{isExpanded && <div className={styles.body}>{children}</div>}
</div>
);
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
box: css`
border-bottom: 1px solid ${theme.colors.pageHeaderBorder};
`,
toggle: css`
font-size: ${theme.typography.size.lg};
`,
header: css`
display: flex;
cursor: pointer;
justify-content: space-between;
align-items: center;
padding: ${theme.spacing.sm} ${theme.spacing.md};
font-weight: ${theme.typography.weight.semibold};
`,
body: css`
padding: ${theme.spacing.md};
`,
};
});
import React, { useState } from 'react';
import React from 'react';
import { config } from 'app/core/config';
import { css } from 'emotion';
import AutoSizer from 'react-virtualized-auto-sizer';
import useMeasure from 'react-use/lib/useMeasure';
import { TabsBar, Tab, stylesFactory, TabContent } from '@grafana/ui';
import { EditorTab, allTabs } from './types';
import { TabsBar, Tab, stylesFactory, TabContent, TransformationsEditor } from '@grafana/ui';
import { DataTransformerConfig, LoadingState, PanelData } from '@grafana/data';
import { PanelEditorTab, PanelEditorTabId } from './types';
import { DashboardModel } from '../../state';
import { QueriesTab } from '../../panel_editor/QueriesTab';
import { PanelModel } from '../../state/PanelModel';
......@@ -12,60 +12,69 @@ import { AlertTab } from 'app/features/alerting/AlertTab';
interface PanelEditorTabsProps {
panel: PanelModel;
dashboard: DashboardModel;
tabs: PanelEditorTab[];
onChangeTab: (tab: PanelEditorTab) => void;
data: PanelData;
}
export const PanelEditorTabs: React.FC<PanelEditorTabsProps> = ({ panel, dashboard, tabs, data, onChangeTab }) => {
const styles = getPanelEditorTabsStyles();
const activeTab = tabs.find(item => item.active);
if (tabs.length === 0) {
return null;
}
const onTransformersChange = (transformers: DataTransformerConfig[]) => {
panel.setTransformations(transformers);
};
return (
<div className={styles.wrapper}>
<TabsBar className={styles.tabBar}>
{tabs.map(tab => {
return <Tab key={tab.id} label={tab.text} active={tab.active} onChangeTab={() => onChangeTab(tab)} />;
})}
</TabsBar>
<TabContent className={styles.tabContent}>
{activeTab.id === PanelEditorTabId.Queries && <QueriesTab panel={panel} dashboard={dashboard} />}
{activeTab.id === PanelEditorTabId.Alert && <AlertTab panel={panel} dashboard={dashboard} />}
{activeTab.id === PanelEditorTabId.Transform && data.state !== LoadingState.NotStarted && (
<TransformationsEditor
transformations={panel.transformations || []}
onChange={onTransformersChange}
dataFrames={data.series}
/>
)}
</TabContent>
</div>
);
};
const getPanelEditorTabsStyles = stylesFactory(() => {
const { theme } = config;
return {
wrapper: css`
display: flex;
flex-direction: column;
height: 100%;
`,
content: css`
tabBar: css`
padding: 0 ${theme.spacing.sm};
`,
tabContent: css`
padding: 0;
display: flex;
flex-direction: column;
flex-grow: 1;
min-height: 0;
background: ${theme.colors.pageBg};
border-right: 1px solid ${theme.colors.pageHeaderBorder};
.toolbar {
background: transparent;
}
`,
};
});
export const PanelEditorTabs: React.FC<PanelEditorTabsProps> = ({ panel, dashboard }) => {
const [activeTab, setActiveTab] = useState(EditorTab.Query);
const [tabsBarRef, tabsBarMeasurements] = useMeasure();
const styles = getPanelEditorTabsStyles();
return (
<div className={styles.wrapper}>
<div>
<TabsBar ref={tabsBarRef}>
{allTabs.map(t => {
if (t.show(panel)) {
return (
<Tab
label={t.label}
active={activeTab === t.tab}
onChangeTab={() => {
setActiveTab(t.tab);
}}
/>
);
}
return null;
})}
</TabsBar>
</div>
<div style={{ flexGrow: 1 }}>
<TabContent style={{ height: `calc(100% - ${tabsBarMeasurements.height}px)` }}>
<AutoSizer>
{({ width, height }) => {
return (
<div style={{ width, height }}>
{activeTab === EditorTab.Query && <QueriesTab panel={panel} dashboard={dashboard} />}
{activeTab === EditorTab.Alerts && <AlertTab panel={panel} dashboard={dashboard} />}
{activeTab === EditorTab.Transform && <div>TODO: Show Transform</div>}
</div>
);
}}
</AutoSizer>
</TabContent>
</div>
</div>
);
};
import memoizeOne from 'memoize-one';
import { LocationState } from 'app/types';
import { PanelPlugin } from '@grafana/data';
import { PanelEditorTab, PanelEditorTabId } from '../types';
export const getPanelEditorTabs = memoizeOne((location: LocationState, plugin?: PanelPlugin) => {
const tabs: PanelEditorTab[] = [];
if (!plugin) {
return tabs;
}
let defaultTab = PanelEditorTabId.Visualization;
if (!plugin.meta.skipDataQuery) {
defaultTab = PanelEditorTabId.Queries;
tabs.push({
id: PanelEditorTabId.Queries,
text: 'Queries',
active: false,
});
tabs.push({
id: PanelEditorTabId.Transform,
text: 'Transform',
active: false,
});
}
tabs.push({
id: PanelEditorTabId.Visualization,
text: 'Visualization',
active: false,
});
if (plugin.meta.id === 'graph') {
tabs.push({
id: PanelEditorTabId.Alert,
text: 'Alert',
active: false,
});
}
const activeTab = tabs.find(item => item.id === (location.query.tab || defaultTab));
activeTab.active = true;
return tabs;
});
import { PanelModel } from '../../state/PanelModel';
export interface PanelEditorTab {
id: string;
text: string;
active: boolean;
}
export enum PanelEditorTabId {
Queries = 'queries',
Transform = 'transform',
Visualization = 'visualization',
Alert = 'alert',
}
export enum DisplayMode {
Fill = 0,
......@@ -11,15 +22,3 @@ export const displayModes = [
{ value: DisplayMode.Fit, label: 'Fit', description: 'Fit in the space keeping ratio' },
{ value: DisplayMode.Exact, label: 'Exact', description: 'Same size as the dashboard' },
];
export enum EditorTab {
Query = 'query',
Alerts = 'alerts',
Transform = 'xform',
}
export const allTabs = [
{ tab: EditorTab.Query, label: 'Query', show: (panel: PanelModel) => true },
{ tab: EditorTab.Alerts, label: 'Alerts', show: (panel: PanelModel) => true },
{ tab: EditorTab.Transform, label: 'Transform', show: (panel: PanelModel) => true },
];
// Libraries
import React, { PureComponent } from 'react';
import _ from 'lodash';
import { css } from 'emotion';
// Components
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { QueryInspector } from './QueryInspector';
import { QueryOptions } from './QueryOptions';
import { PanelOptionsGroup, TransformationsEditor, AlphaNotice } from '@grafana/ui';
import { PanelOptionsGroup } from '@grafana/ui';
import { QueryEditorRows } from './QueryEditorRows';
// Services
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
......@@ -16,15 +15,7 @@ import config from 'app/core/config';
// Types
import { PanelModel } from '../state/PanelModel';
import { DashboardModel } from '../state/DashboardModel';
import {
LoadingState,
DataTransformerConfig,
DefaultTimeRange,
DataSourceSelectItem,
DataQuery,
PanelData,
PluginState,
} from '@grafana/data';
import { LoadingState, DefaultTimeRange, DataSourceSelectItem, DataQuery, PanelData } from '@grafana/data';
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import { addQuery } from 'app/core/utils/query';
import { Unsubscribable } from 'rxjs';
......@@ -219,11 +210,6 @@ export class QueriesTab extends PureComponent<Props, State> {
this.forceUpdate();
};
onTransformersChange = (transformers: DataTransformerConfig[]) => {
this.props.panel.setTransformations(transformers);
this.forceUpdate();
};
setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
const target = event.target as HTMLElement;
this.setState({ scrollTop: target.scrollTop });
......@@ -256,7 +242,7 @@ export class QueriesTab extends PureComponent<Props, State> {
};
render() {
const { scrollTop, data } = this.state;
const { scrollTop } = this.state;
const queryInspector: EditorToolbarView = {
title: 'Query Inspector',
render: this.renderQueryInspector,
......@@ -268,8 +254,6 @@ export class QueriesTab extends PureComponent<Props, State> {
render: this.renderHelp,
};
const enableTransformations = config.featureToggles.transformations;
return (
<EditorTabBody
heading="Query"
......@@ -278,33 +262,7 @@ export class QueriesTab extends PureComponent<Props, State> {
setScrollTop={this.setScrollTop}
scrollTop={scrollTop}
>
<>
{this.renderQueryBody()}
{enableTransformations && (
<PanelOptionsGroup
title={
<>
Query results
<AlphaNotice
state={PluginState.alpha}
className={css`
margin-left: 16px;
`}
/>
</>
}
>
{this.state.data.state !== LoadingState.NotStarted && (
<TransformationsEditor
transformations={this.props.panel.transformations || []}
onChange={this.onTransformersChange}
dataFrames={data.series}
/>
)}
</PanelOptionsGroup>
)}
</>
<>{this.renderQueryBody()}</>
</EditorTabBody>
);
}
......
......@@ -103,6 +103,7 @@ exports[`Render should render alpha info text 1`] = `
DataSourcePlugin {
"DataSourceClass": Object {},
"components": Object {},
"meta": Object {},
}
}
/>
......@@ -292,6 +293,7 @@ exports[`Render should render is ready only message 1`] = `
DataSourcePlugin {
"DataSourceClass": Object {},
"components": Object {},
"meta": Object {},
}
}
/>
......
import { DashboardAcl } from './acl';
import { DataQuery } from '@grafana/data';
import { AngularComponent } from '@grafana/runtime';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
export interface DashboardDTO {
......@@ -70,7 +69,6 @@ export interface QueriesToUpdateOnDashboardLoad {
export interface PanelState {
pluginId: string;
angularPanel?: AngularComponent;
}
export interface DashboardState {
......
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