mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: Panel edit visualization suggestions (#82296)
* DashboardScene: Panel edit visualization suggestions * Tweak * tweak * Update betterer * Update
This commit is contained in:
parent
8a90c0f868
commit
f7a425d352
@ -2636,6 +2636,9 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts:5381": [
|
"public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
],
|
],
|
||||||
|
"public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.ts:5381": [
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
|
],
|
||||||
"public/app/features/dashboard-scene/utils/test-utils.ts:5381": [
|
"public/app/features/dashboard-scene/utils/test-utils.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||||
|
@ -51,8 +51,7 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
|||||||
const { panelManager } = model;
|
const { panelManager } = model;
|
||||||
const { panel } = panelManager.state;
|
const { panel } = panelManager.state;
|
||||||
const dataObject = sceneGraph.getData(panel);
|
const dataObject = sceneGraph.getData(panel);
|
||||||
const rawData = dataObject.useState();
|
const { data } = dataObject.useState();
|
||||||
const dataWithFieldConfig = panel.applyFieldConfig(rawData.data!);
|
|
||||||
const { pluginId, options, fieldConfig } = panel.useState();
|
const { pluginId, options, fieldConfig } = panel.useState();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const panelFrameOptions = useMemo(() => getPanelFrameCategory2(panel), [panel]);
|
const panelFrameOptions = useMemo(() => getPanelFrameCategory2(panel), [panel]);
|
||||||
@ -77,7 +76,7 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
|||||||
getFieldOverrideCategories(
|
getFieldOverrideCategories(
|
||||||
fieldConfig,
|
fieldConfig,
|
||||||
panel.getPlugin()?.fieldConfigRegistry!,
|
panel.getPlugin()?.fieldConfigRegistry!,
|
||||||
dataWithFieldConfig.series,
|
data?.series ?? [],
|
||||||
searchQuery,
|
searchQuery,
|
||||||
(newConfig) => {
|
(newConfig) => {
|
||||||
panel.setState({
|
panel.setState({
|
||||||
@ -135,7 +134,9 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{isVizPickerOpen && <PanelVizTypePicker panelManager={panelManager} onChange={model.onToggleVizPicker} />}
|
{isVizPickerOpen && (
|
||||||
|
<PanelVizTypePicker panelManager={panelManager} onChange={model.onToggleVizPicker} data={data} />
|
||||||
|
)}
|
||||||
{!isVizPickerOpen && (
|
{!isVizPickerOpen && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.top}>
|
<div className={styles.top}>
|
||||||
|
@ -1,26 +1,52 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { useLocalStorage } from 'react-use';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data';
|
||||||
import { SceneObjectState } from '@grafana/scenes';
|
import { CustomScrollbar, Field, FilterInput, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||||
import { CustomScrollbar, FilterInput, useStyles2 } from '@grafana/ui';
|
import { LS_VISUALIZATION_SELECT_TAB_KEY, LS_WIDGET_SELECT_TAB_KEY } from 'app/core/constants';
|
||||||
|
import { VisualizationSelectPaneTab } from 'app/features/dashboard/components/PanelEditor/types';
|
||||||
|
import { VisualizationSuggestions } from 'app/features/panel/components/VizTypePicker/VisualizationSuggestions';
|
||||||
import { VizTypePicker } from 'app/features/panel/components/VizTypePicker/VizTypePicker';
|
import { VizTypePicker } from 'app/features/panel/components/VizTypePicker/VizTypePicker';
|
||||||
|
import { VizTypeChangeDetails } from 'app/features/panel/components/VizTypePicker/types';
|
||||||
|
|
||||||
|
import { PanelModelCompatibilityWrapper } from '../utils/PanelModelCompatibilityWrapper';
|
||||||
|
|
||||||
import { VizPanelManager } from './VizPanelManager';
|
import { VizPanelManager } from './VizPanelManager';
|
||||||
|
|
||||||
export interface PanelVizTypePickerState extends SceneObjectState {}
|
export interface Props {
|
||||||
|
data?: PanelData;
|
||||||
export function PanelVizTypePicker({
|
|
||||||
panelManager,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
panelManager: VizPanelManager;
|
panelManager: VizPanelManager;
|
||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
export function PanelVizTypePicker({ panelManager, data, onChange }: Props) {
|
||||||
const { panel } = panelManager.useState();
|
const { panel } = panelManager.useState();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
const isWidgetEnabled = false;
|
||||||
|
const tabKey = isWidgetEnabled ? LS_WIDGET_SELECT_TAB_KEY : LS_VISUALIZATION_SELECT_TAB_KEY;
|
||||||
|
const defaultTab = isWidgetEnabled ? VisualizationSelectPaneTab.Widgets : VisualizationSelectPaneTab.Visualizations;
|
||||||
|
const panelModel = useMemo(() => new PanelModelCompatibilityWrapper(panel), [panel]);
|
||||||
|
|
||||||
|
const [listMode, setListMode] = useLocalStorage(tabKey, defaultTab);
|
||||||
|
|
||||||
|
const radioOptions: Array<SelectableValue<VisualizationSelectPaneTab>> = [
|
||||||
|
{ label: 'Visualizations', value: VisualizationSelectPaneTab.Visualizations },
|
||||||
|
{ label: 'Suggestions', value: VisualizationSelectPaneTab.Suggestions },
|
||||||
|
// {
|
||||||
|
// label: 'Library panels',
|
||||||
|
// value: VisualizationSelectPaneTab.LibraryPanels,
|
||||||
|
// description: 'Reusable panels you can share between multiple dashboards.',
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
const onVizTypeChange = (options: VizTypeChangeDetails) => {
|
||||||
|
panelManager.changePluginType(options.pluginId);
|
||||||
|
onChange();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<FilterInput
|
<FilterInput
|
||||||
@ -30,15 +56,24 @@ export function PanelVizTypePicker({
|
|||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
placeholder="Search for..."
|
placeholder="Search for..."
|
||||||
/>
|
/>
|
||||||
<CustomScrollbar>
|
<Field className={styles.customFieldMargin}>
|
||||||
<VizTypePicker
|
<RadioButtonGroup options={radioOptions} value={listMode} onChange={setListMode} fullWidth />
|
||||||
pluginId={panel.state.pluginId}
|
</Field>
|
||||||
searchQuery={searchQuery}
|
<CustomScrollbar autoHeightMin="100%">
|
||||||
onChange={(options) => {
|
{listMode === VisualizationSelectPaneTab.Visualizations && (
|
||||||
panelManager.changePluginType(options.pluginId);
|
<VizTypePicker pluginId={panel.state.pluginId} searchQuery={searchQuery} onChange={onVizTypeChange} />
|
||||||
onChange();
|
)}
|
||||||
}}
|
{/* {listMode === VisualizationSelectPaneTab.Widgets && (
|
||||||
/>
|
<VizTypePicker pluginId={plugin.meta.id} onChange={onVizChange} searchQuery={searchQuery} isWidget />
|
||||||
|
)} */}
|
||||||
|
{listMode === VisualizationSelectPaneTab.Suggestions && (
|
||||||
|
<VisualizationSuggestions
|
||||||
|
onChange={onVizTypeChange}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
panel={panelModel}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -57,6 +92,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
borderBottom: 'none',
|
borderBottom: 'none',
|
||||||
borderTopLeftRadius: theme.shape.radius.default,
|
borderTopLeftRadius: theme.shape.radius.default,
|
||||||
}),
|
}),
|
||||||
|
customFieldMargin: css({
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
}),
|
||||||
filter: css({
|
filter: css({
|
||||||
minHeight: theme.spacing(4),
|
minHeight: theme.spacing(4),
|
||||||
}),
|
}),
|
||||||
|
@ -5,7 +5,6 @@ import { TimeRangeUpdatedEvent } from '@grafana/runtime';
|
|||||||
import {
|
import {
|
||||||
behaviors,
|
behaviors,
|
||||||
SceneDataLayers,
|
SceneDataLayers,
|
||||||
SceneDataTransformer,
|
|
||||||
sceneGraph,
|
sceneGraph,
|
||||||
SceneGridItem,
|
SceneGridItem,
|
||||||
SceneGridLayout,
|
SceneGridLayout,
|
||||||
@ -13,14 +12,13 @@ import {
|
|||||||
SceneObject,
|
SceneObject,
|
||||||
VizPanel,
|
VizPanel,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { DataSourceRef } from '@grafana/schema';
|
|
||||||
|
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
|
||||||
import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations';
|
import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations';
|
||||||
|
|
||||||
|
import { PanelModelCompatibilityWrapper } from './PanelModelCompatibilityWrapper';
|
||||||
import { dashboardSceneGraph } from './dashboardSceneGraph';
|
import { dashboardSceneGraph } from './dashboardSceneGraph';
|
||||||
import { findVizPanelByKey, getPanelIdForVizPanel, getQueryRunnerFor, getVizPanelKeyForPanelId } from './utils';
|
import { findVizPanelByKey, getVizPanelKeyForPanelId } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will move this to make it the main way we remain somewhat compatible with getDashboardSrv().getCurrent
|
* Will move this to make it the main way we remain somewhat compatible with getDashboardSrv().getCurrent
|
||||||
@ -107,7 +105,7 @@ export class DashboardModelCompatibilityWrapper {
|
|||||||
const panels = findAllObjects(this._scene, (o) => {
|
const panels = findAllObjects(this._scene, (o) => {
|
||||||
return Boolean(o instanceof VizPanel);
|
return Boolean(o instanceof VizPanel);
|
||||||
});
|
});
|
||||||
return panels.map((p) => new PanelCompatibilityWrapper(p as VizPanel));
|
return panels.map((p) => new PanelModelCompatibilityWrapper(p as VizPanel));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -159,10 +157,10 @@ export class DashboardModelCompatibilityWrapper {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPanelById(id: number): PanelCompatibilityWrapper | null {
|
public getPanelById(id: number): PanelModelCompatibilityWrapper | null {
|
||||||
const vizPanel = findVizPanelByKey(this._scene, getVizPanelKeyForPanelId(id));
|
const vizPanel = findVizPanelByKey(this._scene, getVizPanelKeyForPanelId(id));
|
||||||
if (vizPanel) {
|
if (vizPanel) {
|
||||||
return new PanelCompatibilityWrapper(vizPanel);
|
return new PanelModelCompatibilityWrapper(vizPanel);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -171,7 +169,7 @@ export class DashboardModelCompatibilityWrapper {
|
|||||||
/**
|
/**
|
||||||
* Mainly implemented to support Getting started panel's dissmis button.
|
* Mainly implemented to support Getting started panel's dissmis button.
|
||||||
*/
|
*/
|
||||||
public removePanel(panel: PanelCompatibilityWrapper) {
|
public removePanel(panel: PanelModelCompatibilityWrapper) {
|
||||||
const vizPanel = findVizPanelByKey(this._scene, getVizPanelKeyForPanelId(panel.id));
|
const vizPanel = findVizPanelByKey(this._scene, getVizPanelKeyForPanelId(panel.id));
|
||||||
if (!vizPanel) {
|
if (!vizPanel) {
|
||||||
console.error('Trying to remove a panel that was not found in scene', panel);
|
console.error('Trying to remove a panel that was not found in scene', panel);
|
||||||
@ -237,65 +235,6 @@ export class DashboardModelCompatibilityWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PanelCompatibilityWrapper {
|
|
||||||
constructor(private _vizPanel: VizPanel) {}
|
|
||||||
|
|
||||||
public get id() {
|
|
||||||
const id = getPanelIdForVizPanel(
|
|
||||||
this._vizPanel.parent instanceof LibraryVizPanel ? this._vizPanel.parent : this._vizPanel
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isNaN(id)) {
|
|
||||||
console.error('VizPanel key could not be translated to a legacy numeric panel id', this._vizPanel);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get type() {
|
|
||||||
return this._vizPanel.state.pluginId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get title() {
|
|
||||||
return this._vizPanel.state.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get transformations() {
|
|
||||||
if (this._vizPanel.state.$data instanceof SceneDataTransformer) {
|
|
||||||
return this._vizPanel.state.$data.state.transformations;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public get targets() {
|
|
||||||
const queryRunner = getQueryRunnerFor(this._vizPanel);
|
|
||||||
if (!queryRunner) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return queryRunner.state.queries;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get datasource(): DataSourceRef | null | undefined {
|
|
||||||
const queryRunner = getQueryRunnerFor(this._vizPanel);
|
|
||||||
return queryRunner?.state.datasource;
|
|
||||||
}
|
|
||||||
|
|
||||||
public refresh() {
|
|
||||||
console.error('Scenes PanelCompatibilityWrapper.refresh no implemented (yet)');
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
console.error('Scenes PanelCompatibilityWrapper.render no implemented (yet)');
|
|
||||||
}
|
|
||||||
|
|
||||||
public getQueryRunner() {
|
|
||||||
console.error('Scenes PanelCompatibilityWrapper.getQueryRunner no implemented (yet)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function findAllObjects(root: SceneObject, check: (o: SceneObject) => boolean) {
|
function findAllObjects(root: SceneObject, check: (o: SceneObject) => boolean) {
|
||||||
let result: SceneObject[] = [];
|
let result: SceneObject[] = [];
|
||||||
root.forEachChild((child) => {
|
root.forEachChild((child) => {
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
import { PanelModel } from '@grafana/data';
|
||||||
|
import { SceneDataTransformer, VizPanel } from '@grafana/scenes';
|
||||||
|
import { DataSourceRef, DataTransformerConfig } from '@grafana/schema';
|
||||||
|
|
||||||
|
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||||
|
|
||||||
|
import { getPanelIdForVizPanel, getQueryRunnerFor } from './utils';
|
||||||
|
|
||||||
|
export class PanelModelCompatibilityWrapper implements PanelModel {
|
||||||
|
constructor(private _vizPanel: VizPanel) {}
|
||||||
|
|
||||||
|
public get id() {
|
||||||
|
const id = getPanelIdForVizPanel(
|
||||||
|
this._vizPanel.parent instanceof LibraryVizPanel ? this._vizPanel.parent : this._vizPanel
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNaN(id)) {
|
||||||
|
console.error('VizPanel key could not be translated to a legacy numeric panel id', this._vizPanel);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get description() {
|
||||||
|
return this._vizPanel.state.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get type() {
|
||||||
|
return this._vizPanel.state.pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get title() {
|
||||||
|
return this._vizPanel.state.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get transformations() {
|
||||||
|
if (this._vizPanel.state.$data instanceof SceneDataTransformer) {
|
||||||
|
return this._vizPanel.state.$data.state.transformations as DataTransformerConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public get targets() {
|
||||||
|
const queryRunner = getQueryRunnerFor(this._vizPanel);
|
||||||
|
if (!queryRunner) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryRunner.state.queries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get datasource(): DataSourceRef | null | undefined {
|
||||||
|
const queryRunner = getQueryRunnerFor(this._vizPanel);
|
||||||
|
return queryRunner?.state.datasource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get options() {
|
||||||
|
return this._vizPanel.state.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get fieldConfig() {
|
||||||
|
return this._vizPanel.state.fieldConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get pluginVersion() {
|
||||||
|
return this._vizPanel.state.pluginVersion;
|
||||||
|
}
|
||||||
|
}
|
@ -24,39 +24,43 @@ export function VisualizationSuggestions({ searchQuery, onChange, data, panel }:
|
|||||||
const filteredSuggestions = filterSuggestionsBySearch(searchQuery, suggestions);
|
const filteredSuggestions = filterSuggestionsBySearch(searchQuery, suggestions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoSizer disableHeight style={{ width: '100%', height: '100%' }}>
|
// This div is needed in some places to make AutoSizer work
|
||||||
{({ width }) => {
|
<div>
|
||||||
if (!width) {
|
<AutoSizer disableHeight style={{ width: '100%', height: '100%' }}>
|
||||||
return null;
|
{({ width }) => {
|
||||||
}
|
if (!width) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const columnCount = Math.floor(width / 170);
|
width = width - 1;
|
||||||
const spaceBetween = 8 * (columnCount! - 1);
|
const columnCount = Math.floor(width / 200);
|
||||||
const previewWidth = (width - spaceBetween) / columnCount!;
|
const spaceBetween = 8 * (columnCount! - 1);
|
||||||
|
const previewWidth = Math.floor((width - spaceBetween) / columnCount!);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.filterRow}>
|
<div className={styles.filterRow}>
|
||||||
<div className={styles.infoText}>Based on current data</div>
|
<div className={styles.infoText}>Based on current data</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.grid} style={{ gridTemplateColumns: `repeat(auto-fill, ${previewWidth}px)` }}>
|
||||||
|
{filteredSuggestions.map((suggestion, index) => (
|
||||||
|
<VisualizationSuggestionCard
|
||||||
|
key={index}
|
||||||
|
data={data!}
|
||||||
|
suggestion={suggestion}
|
||||||
|
onChange={onChange}
|
||||||
|
width={previewWidth - 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{searchQuery && filteredSuggestions.length === 0 && (
|
||||||
|
<div className={styles.infoText}>No results matched your query</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.grid} style={{ gridTemplateColumns: `repeat(auto-fill, ${previewWidth - 1}px)` }}>
|
);
|
||||||
{filteredSuggestions.map((suggestion, index) => (
|
}}
|
||||||
<VisualizationSuggestionCard
|
</AutoSizer>
|
||||||
key={index}
|
</div>
|
||||||
data={data!}
|
|
||||||
suggestion={suggestion}
|
|
||||||
onChange={onChange}
|
|
||||||
width={previewWidth}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{searchQuery && filteredSuggestions.length === 0 && (
|
|
||||||
<div className={styles.infoText}>No results matched your query</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</AutoSizer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user