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": [
|
||||
[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": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
|
@ -51,8 +51,7 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
||||
const { panelManager } = model;
|
||||
const { panel } = panelManager.state;
|
||||
const dataObject = sceneGraph.getData(panel);
|
||||
const rawData = dataObject.useState();
|
||||
const dataWithFieldConfig = panel.applyFieldConfig(rawData.data!);
|
||||
const { data } = dataObject.useState();
|
||||
const { pluginId, options, fieldConfig } = panel.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const panelFrameOptions = useMemo(() => getPanelFrameCategory2(panel), [panel]);
|
||||
@ -77,7 +76,7 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
||||
getFieldOverrideCategories(
|
||||
fieldConfig,
|
||||
panel.getPlugin()?.fieldConfigRegistry!,
|
||||
dataWithFieldConfig.series,
|
||||
data?.series ?? [],
|
||||
searchQuery,
|
||||
(newConfig) => {
|
||||
panel.setState({
|
||||
@ -135,7 +134,9 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{isVizPickerOpen && <PanelVizTypePicker panelManager={panelManager} onChange={model.onToggleVizPicker} />}
|
||||
{isVizPickerOpen && (
|
||||
<PanelVizTypePicker panelManager={panelManager} onChange={model.onToggleVizPicker} data={data} />
|
||||
)}
|
||||
{!isVizPickerOpen && (
|
||||
<>
|
||||
<div className={styles.top}>
|
||||
|
@ -1,26 +1,52 @@
|
||||
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 { SceneObjectState } from '@grafana/scenes';
|
||||
import { CustomScrollbar, FilterInput, useStyles2 } from '@grafana/ui';
|
||||
import { GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data';
|
||||
import { CustomScrollbar, Field, FilterInput, RadioButtonGroup, 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 { VizTypeChangeDetails } from 'app/features/panel/components/VizTypePicker/types';
|
||||
|
||||
import { PanelModelCompatibilityWrapper } from '../utils/PanelModelCompatibilityWrapper';
|
||||
|
||||
import { VizPanelManager } from './VizPanelManager';
|
||||
|
||||
export interface PanelVizTypePickerState extends SceneObjectState {}
|
||||
|
||||
export function PanelVizTypePicker({
|
||||
panelManager,
|
||||
onChange,
|
||||
}: {
|
||||
export interface Props {
|
||||
data?: PanelData;
|
||||
panelManager: VizPanelManager;
|
||||
onChange: () => void;
|
||||
}) {
|
||||
}
|
||||
|
||||
export function PanelVizTypePicker({ panelManager, data, onChange }: Props) {
|
||||
const { panel } = panelManager.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
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 (
|
||||
<div className={styles.wrapper}>
|
||||
<FilterInput
|
||||
@ -30,15 +56,24 @@ export function PanelVizTypePicker({
|
||||
autoFocus={true}
|
||||
placeholder="Search for..."
|
||||
/>
|
||||
<CustomScrollbar>
|
||||
<VizTypePicker
|
||||
pluginId={panel.state.pluginId}
|
||||
searchQuery={searchQuery}
|
||||
onChange={(options) => {
|
||||
panelManager.changePluginType(options.pluginId);
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
<Field className={styles.customFieldMargin}>
|
||||
<RadioButtonGroup options={radioOptions} value={listMode} onChange={setListMode} fullWidth />
|
||||
</Field>
|
||||
<CustomScrollbar autoHeightMin="100%">
|
||||
{listMode === VisualizationSelectPaneTab.Visualizations && (
|
||||
<VizTypePicker pluginId={panel.state.pluginId} searchQuery={searchQuery} onChange={onVizTypeChange} />
|
||||
)}
|
||||
{/* {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>
|
||||
</div>
|
||||
);
|
||||
@ -57,6 +92,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
borderBottom: 'none',
|
||||
borderTopLeftRadius: theme.shape.radius.default,
|
||||
}),
|
||||
customFieldMargin: css({
|
||||
marginBottom: theme.spacing(1),
|
||||
}),
|
||||
filter: css({
|
||||
minHeight: theme.spacing(4),
|
||||
}),
|
||||
|
@ -5,7 +5,6 @@ import { TimeRangeUpdatedEvent } from '@grafana/runtime';
|
||||
import {
|
||||
behaviors,
|
||||
SceneDataLayers,
|
||||
SceneDataTransformer,
|
||||
sceneGraph,
|
||||
SceneGridItem,
|
||||
SceneGridLayout,
|
||||
@ -13,14 +12,13 @@ import {
|
||||
SceneObject,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations';
|
||||
|
||||
import { PanelModelCompatibilityWrapper } from './PanelModelCompatibilityWrapper';
|
||||
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
|
||||
@ -107,7 +105,7 @@ export class DashboardModelCompatibilityWrapper {
|
||||
const panels = findAllObjects(this._scene, (o) => {
|
||||
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));
|
||||
if (vizPanel) {
|
||||
return new PanelCompatibilityWrapper(vizPanel);
|
||||
return new PanelModelCompatibilityWrapper(vizPanel);
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -171,7 +169,7 @@ export class DashboardModelCompatibilityWrapper {
|
||||
/**
|
||||
* 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));
|
||||
if (!vizPanel) {
|
||||
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) {
|
||||
let result: SceneObject[] = [];
|
||||
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);
|
||||
|
||||
return (
|
||||
<AutoSizer disableHeight style={{ width: '100%', height: '100%' }}>
|
||||
{({ width }) => {
|
||||
if (!width) {
|
||||
return null;
|
||||
}
|
||||
// This div is needed in some places to make AutoSizer work
|
||||
<div>
|
||||
<AutoSizer disableHeight style={{ width: '100%', height: '100%' }}>
|
||||
{({ width }) => {
|
||||
if (!width) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const columnCount = Math.floor(width / 170);
|
||||
const spaceBetween = 8 * (columnCount! - 1);
|
||||
const previewWidth = (width - spaceBetween) / columnCount!;
|
||||
width = width - 1;
|
||||
const columnCount = Math.floor(width / 200);
|
||||
const spaceBetween = 8 * (columnCount! - 1);
|
||||
const previewWidth = Math.floor((width - spaceBetween) / columnCount!);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.filterRow}>
|
||||
<div className={styles.infoText}>Based on current data</div>
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.filterRow}>
|
||||
<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 className={styles.grid} style={{ gridTemplateColumns: `repeat(auto-fill, ${previewWidth - 1}px)` }}>
|
||||
{filteredSuggestions.map((suggestion, index) => (
|
||||
<VisualizationSuggestionCard
|
||||
key={index}
|
||||
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>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user