From df1d43167af035c6819923ecce135056f37c79c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Feb 2020 10:42:57 +0100 Subject: [PATCH] NewPanelEditor: Panel editor tabs in state (url) (#22102) * tabs & style tweaks * Styling updates * ok look * tweaks * Updated snapshots * Moved transforms * Updated --- packages/grafana-data/src/types/plugin.ts | 6 +- .../Forms/Select/getSelectStyles.ts | 1 + .../PanelEditor/FieldConfigEditor.tsx | 27 ++-- .../components/PanelEditor/OptionsGroup.tsx | 48 ++++++ .../components/PanelEditor/PanelEditor.tsx | 144 ++++++++++++------ .../PanelEditor/PanelEditorTabs.tsx | 107 +++++++------ .../components/PanelEditor/state/selectors.ts | 49 ++++++ .../dashboard/components/PanelEditor/types.ts | 25 ++- .../dashboard/panel_editor/QueriesTab.tsx | 50 +----- .../DataSourceSettingsPage.test.tsx.snap | 2 + public/app/types/dashboard.ts | 2 - 11 files changed, 283 insertions(+), 178 deletions(-) create mode 100644 public/app/features/dashboard/components/PanelEditor/OptionsGroup.tsx create mode 100644 public/app/features/dashboard/components/PanelEditor/state/selectors.ts diff --git a/packages/grafana-data/src/types/plugin.ts b/packages/grafana-data/src/types/plugin.ts index 838787aadca..2f33b290851 100644 --- a/packages/grafana-data/src/types/plugin.ts +++ b/packages/grafana-data/src/types/plugin.ts @@ -123,7 +123,7 @@ export interface PluginConfigPage { export class GrafanaPlugin { // 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 { this.configPages.push(tab); return this; } + + constructor() { + this.meta = {} as T; + } } diff --git a/packages/grafana-ui/src/components/Forms/Select/getSelectStyles.ts b/packages/grafana-ui/src/components/Forms/Select/getSelectStyles.ts index 086f77f7e6c..ff8d09a5508 100644 --- a/packages/grafana-ui/src/components/Forms/Select/getSelectStyles.ts +++ b/packages/grafana-ui/src/components/Forms/Select/getSelectStyles.ts @@ -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}; } diff --git a/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx b/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx index bd7a8acc355..54d15a84725 100644 --- a/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx @@ -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 { 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 { render() { return (
- - {this.renderStandardConfigs()} - - {this.props.custom && ( - {this.renderCustomConfigs()} - )} + {this.renderStandardConfigs()} - + {this.props.custom && {this.renderCustomConfigs()}} + + {this.renderOverrides()} {this.renderAddOverride()} - +
); } diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsGroup.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsGroup.tsx new file mode 100644 index 00000000000..1dbe9836484 --- /dev/null +++ b/public/app/features/dashboard/components/PanelEditor/OptionsGroup.tsx @@ -0,0 +1,48 @@ +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 = ({ title, children }) => { + const [isExpanded, toggleExpand] = useState(false); + const theme = useTheme(); + const styles = getStyles(theme); + + return ( +
+
toggleExpand(!isExpanded)}> + {title} +
+ +
+
+ {isExpanded &&
{children}
} +
+ ); +}; + +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}; + `, + }; +}); diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 84ee4630075..b666065a34e 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { GrafanaTheme, FieldConfigSource, PanelData, PanelPlugin, SelectableValue } from '@grafana/data'; -import { stylesFactory, Forms, CustomScrollbar, selectThemeVariant, ControlledCollapse } from '@grafana/ui'; +import { stylesFactory, Forms, CustomScrollbar, selectThemeVariant } from '@grafana/ui'; import { css, cx } from 'emotion'; import config from 'app/core/config'; import AutoSizer from 'react-virtualized-auto-sizer'; @@ -15,7 +15,7 @@ import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux'; import { updateLocation } from '../../../../core/reducers/location'; import { Unsubscribable } from 'rxjs'; import { PanelTitle } from './PanelTitle'; -import { DisplayMode, displayModes } from './types'; +import { DisplayMode, displayModes, PanelEditorTab } from './types'; import { PanelEditorTabs } from './PanelEditorTabs'; import { DashNavTimeControls } from '../DashNav/DashNavTimeControls'; import { LocationState } from 'app/types'; @@ -23,6 +23,8 @@ import { calculatePanelSize } from './utils'; import { initPanelEditor, panelEditorCleanUp } from './state/actions'; import { setDisplayMode, toggleOptionsView, setDiscardChanges } from './state/reducers'; import { FieldConfigEditor } from './FieldConfigEditor'; +import { OptionsGroup } from './OptionsGroup'; +import { getPanelEditorTabs } from './state/selectors'; interface OwnProps { dashboard: DashboardModel; @@ -37,6 +39,7 @@ interface ConnectedProps { mode: DisplayMode; isPanelOptionsVisible: boolean; initDone: boolean; + tabs: PanelEditorTab[]; } interface DispatchProps { @@ -76,6 +79,10 @@ export class PanelEditorUnconnected extends PureComponent { }); }; + onChangeTab = (tab: PanelEditorTab) => { + this.props.updateLocation({ query: { tab: tab.id }, partial: true }); + }; + onFieldConfigsChange = (fieldOptions: FieldConfigSource) => { // NOTE: for now, assume this is from 'fieldOptions' -- TODO? put on panel model directly? const { panel } = this.props; @@ -135,7 +142,10 @@ export class PanelEditorUnconnected extends PureComponent { onDragFinished = () => { document.body.style.cursor = 'auto'; - console.log('TODO, save splitter settings'); + }; + + onDragStarted = () => { + document.body.style.cursor = 'row-resize'; }; onPanelTitleChange = (title: string) => { @@ -152,16 +162,17 @@ export class PanelEditorUnconnected extends PureComponent { }; renderHorizontalSplit(styles: any) { - const { dashboard, panel, mode } = this.props; + const { dashboard, panel, mode, tabs, data } = this.props; return ( (document.body.style.cursor = 'row-resize')} + onDragStarted={this.onDragStarted} onDragFinished={this.onDragFinished} >
@@ -188,7 +199,7 @@ export class PanelEditorUnconnected extends PureComponent {
- +
); @@ -212,22 +223,35 @@ export class PanelEditorUnconnected extends PureComponent {
- v.value === mode)} - options={displayModes} - onChange={this.onDiplayModeChange} - /> - - - Discard - - +
+ +
+
+ v.value === mode)} + options={displayModes} + onChange={this.onDiplayModeChange} + /> +
+
+ +
-
+
{isPanelOptionsVisible ? ( { onDragFinished={this.onDragFinished} > {this.renderHorizontalSplit(styles)} -
+
-
- {this.renderFieldOptions()} - - {this.renderVisSettings()} - -
+ {this.renderFieldOptions()} + {this.renderVisSettings()}
@@ -259,15 +279,20 @@ export class PanelEditorUnconnected extends PureComponent { } } -const mapStateToProps: MapStateToProps = (state, props) => ({ - location: state.location, - plugin: state.plugins.panels[props.sourcePanel.type], - panel: state.panelEditorNew.getPanel(), - mode: state.panelEditorNew.mode, - isPanelOptionsVisible: state.panelEditorNew.isPanelOptionsVisible, - data: state.panelEditorNew.getData(), - initDone: state.panelEditorNew.initDone, -}); +const mapStateToProps: MapStateToProps = (state, props) => { + const plugin = state.plugins.panels[props.sourcePanel.type]; + + return { + location: state.location, + plugin: plugin, + panel: state.panelEditorNew.getPanel(), + mode: state.panelEditorNew.mode, + isPanelOptionsVisible: state.panelEditorNew.isPanelOptionsVisible, + data: state.panelEditorNew.getData(), + initDone: state.panelEditorNew.initDone, + tabs: getPanelEditorTabs(state.location, plugin), + }; +}; const mapDispatchToProps: MapDispatchToProps = { updateLocation, @@ -284,20 +309,22 @@ export const PanelEditor = connect(mapStateToProps, mapDispatchToProps)(PanelEdi * Styles */ const getStyles = stylesFactory((theme: GrafanaTheme) => { - const handleColor = selectThemeVariant( - { - dark: theme.colors.dark9, - light: theme.colors.gray6, - }, - theme.type - ); + const handleColor = theme.colors.blueLight; + const background = selectThemeVariant({ light: theme.colors.white, dark: theme.colors.inputBlack }, theme.type); const resizer = css` - padding: 3px; font-style: italic; - background: ${theme.colors.panelBg}; + background: transparent; + border-top: 0; + border-right: 0; + border-bottom: 0; + border-left: 0; + border-color: transparent; + border-style: solid; + transition: 0.2s border-color ease-in-out; + &:hover { - background: ${handleColor}; + border-color: ${handleColor}; } `; @@ -311,9 +338,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { left: 0; right: 0; bottom: 0; - background: ${theme.colors.pageBg}; + background: ${background}; `, panelWrapper: css` + padding: 0 2px 2px ${theme.spacing.sm}; width: 100%; height: 100%; `, @@ -321,33 +349,49 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { resizer, css` cursor: col-resize; + width: 8px; + border-right-width: 1px; ` ), resizerH: cx( resizer, css` + height: 8px; cursor: row-resize; + position: relative; + top: 49px; + z-index: 1; + border-top-width: 1px; ` ), noScrollPaneContent: css` height: 100%; width: 100%; - overflow: hidden; + `, + panelOptionsPane: css` + height: 100%; + width: 100%; + background: ${theme.colors.pageBg}; + border-top: 1px solid ${theme.colors.pageHeaderBorder}; + border-left: 1px solid ${theme.colors.pageHeaderBorder}; `, toolbar: css` padding: ${theme.spacing.sm}; - height: 48px; + height: 55px; display: flex; justify-content: space-between; `, - panes: css` - height: calc(100% - 48px); + editorBody: css` + height: calc(100% - 55px); position: relative; `, toolbarLeft: css` display: flex; align-items: center; `, + toolbarItem: css` + margin-right: ${theme.spacing.sm}; + `, centeringContainer: css` display: flex; justify-content: center; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx index ddb63b459c5..69e468d8410 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx @@ -1,9 +1,9 @@ -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 = ({ 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 ( +
+ + {tabs.map(tab => { + return onChangeTab(tab)} />; + })} + + + {activeTab.id === PanelEditorTabId.Queries && } + {activeTab.id === PanelEditorTabId.Alert && } + {activeTab.id === PanelEditorTabId.Transform && data.state !== LoadingState.NotStarted && ( + + )} + +
+ ); +}; + 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 = ({ panel, dashboard }) => { - const [activeTab, setActiveTab] = useState(EditorTab.Query); - const [tabsBarRef, tabsBarMeasurements] = useMeasure(); - const styles = getPanelEditorTabsStyles(); - - return ( -
-
- - {allTabs.map(t => { - if (t.show(panel)) { - return ( - { - setActiveTab(t.tab); - }} - /> - ); - } - return null; - })} - -
-
- - - {({ width, height }) => { - return ( -
- {activeTab === EditorTab.Query && } - {activeTab === EditorTab.Alerts && } - {activeTab === EditorTab.Transform &&
TODO: Show Transform
} -
- ); - }} -
-
-
-
- ); -}; diff --git a/public/app/features/dashboard/components/PanelEditor/state/selectors.ts b/public/app/features/dashboard/components/PanelEditor/state/selectors.ts new file mode 100644 index 00000000000..a6e28cfdfd1 --- /dev/null +++ b/public/app/features/dashboard/components/PanelEditor/state/selectors.ts @@ -0,0 +1,49 @@ +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; +}); diff --git a/public/app/features/dashboard/components/PanelEditor/types.ts b/public/app/features/dashboard/components/PanelEditor/types.ts index 426ca71bafb..375df69ae8d 100644 --- a/public/app/features/dashboard/components/PanelEditor/types.ts +++ b/public/app/features/dashboard/components/PanelEditor/types.ts @@ -1,4 +1,15 @@ -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 }, -]; diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index 3fe4671a754..24ac56b5de8 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -1,13 +1,12 @@ // 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 { this.forceUpdate(); }; - onTransformersChange = (transformers: DataTransformerConfig[]) => { - this.props.panel.setTransformations(transformers); - this.forceUpdate(); - }; - setScrollTop = (event: React.MouseEvent) => { const target = event.target as HTMLElement; this.setState({ scrollTop: target.scrollTop }); @@ -256,7 +242,7 @@ export class QueriesTab extends PureComponent { }; 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 { render: this.renderHelp, }; - const enableTransformations = config.featureToggles.transformations; - return ( { setScrollTop={this.setScrollTop} scrollTop={scrollTop} > - <> - {this.renderQueryBody()} - - {enableTransformations && ( - - Query results - - - } - > - {this.state.data.state !== LoadingState.NotStarted && ( - - )} - - )} - + <>{this.renderQueryBody()} ); } diff --git a/public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap b/public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap index 057e0daf935..2575c724ae4 100644 --- a/public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap +++ b/public/app/features/datasources/settings/__snapshots__/DataSourceSettingsPage.test.tsx.snap @@ -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 {}, } } /> diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 4f4609c2c64..5b9a4463c4d 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -1,6 +1,5 @@ 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 {