Suggestions: improve styling for new version (#114381)
* Suggestions: hashes on suggestions, update logic to select first suggestion * fix types * Suggestions: New UI style updates * update some styles * getting styles just right * remove grouping when not on flag * adjust minimum width for sidebar * CI cleanups * updates from ad hoc review * add loading and error states to suggestions * remove unused import * update header ui for panel editor * restore back button to vizpicker * fix e2e test * fix e2e * add i18n update * use new util for setVisualization operation * Apply suggestions from code review Co-authored-by: Torkel Ödegaard <torkel@grafana.com> * comments from review * updates from review --------- Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
co-authored by
Torkel Ödegaard
parent
227b596a46
commit
021e0c6da0
@@ -2,17 +2,18 @@ import { Locator } from '@playwright/test';
|
||||
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setVisualization } from './vizpicker-utils';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
canvasPanelPanZoom: true,
|
||||
},
|
||||
});
|
||||
|
||||
test.describe('Canvas Panel - Scene Tests', () => {
|
||||
test.beforeEach(async ({ page, gotoDashboardPage }) => {
|
||||
test.beforeEach(async ({ page, gotoDashboardPage, selectors }) => {
|
||||
const dashboardPage = await gotoDashboardPage({});
|
||||
const panelEditPage = await dashboardPage.addPanel();
|
||||
await panelEditPage.setVisualization('Canvas');
|
||||
await setVisualization(panelEditPage, 'Canvas', selectors);
|
||||
|
||||
// Wait for canvas panel to load
|
||||
await page.waitForSelector('[data-testid="canvas-scene-pan-zoom"]', { timeout: 10000 });
|
||||
|
||||
@@ -58,7 +58,7 @@ test.describe(
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Queries')).click();
|
||||
|
||||
// Check that Time series is chosen
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText(
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header)).toHaveText(
|
||||
'Time series'
|
||||
);
|
||||
|
||||
@@ -71,9 +71,10 @@ test.describe(
|
||||
|
||||
// Change to Text panel
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('All visualizations')).click(); // <-- should only need to do this once thanks to the session storage
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item('Text')).click();
|
||||
// Check current visualization shows Text
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText(
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header)).toHaveText(
|
||||
'Text'
|
||||
);
|
||||
|
||||
@@ -84,7 +85,7 @@ test.describe(
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item('Table')).click();
|
||||
// Check current visualization shows Table
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText(
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header)).toHaveText(
|
||||
'Table'
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { expect, E2ESelectorGroups, PanelEditPage } from '@grafana/plugin-e2e';
|
||||
|
||||
// this replaces the panelEditPage.setVisualization method used previously in tests, since it
|
||||
// does not know how to use the updated 12.4 viz picker UI to set the visualization
|
||||
export const setVisualization = async (panelEditPage: PanelEditPage, vizName: string, selectors: E2ESelectorGroups) => {
|
||||
const vizPicker = panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker);
|
||||
await expect(vizPicker, '"Change" button should be visible').toBeVisible();
|
||||
await vizPicker.click();
|
||||
|
||||
const allVizTabBtn = panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('All visualizations'));
|
||||
await expect(allVizTabBtn, '"All visualiations" button should be visible').toBeVisible();
|
||||
await allVizTabBtn.click();
|
||||
|
||||
const vizItem = panelEditPage.getByGrafanaSelector(selectors.components.PluginVisualization.item(vizName));
|
||||
await expect(vizItem, `"${vizName}" item should be visible`).toBeVisible();
|
||||
await vizItem.scrollIntoViewIfNeeded();
|
||||
await vizItem.click();
|
||||
|
||||
await expect(vizPicker, '"Change" button should be visible again').toBeVisible();
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header),
|
||||
'Panel header should have the new viz type name'
|
||||
).toHaveText(vizName);
|
||||
};
|
||||
+5
-4
@@ -1,5 +1,6 @@
|
||||
import { expect, test } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setVisualization } from '../../../panels-suite/vizpicker-utils';
|
||||
import { formatExpectError } from '../errors';
|
||||
import { successfulDataQuery } from '../mocks/queries';
|
||||
|
||||
@@ -24,10 +25,10 @@ test.describe(
|
||||
).toContainText(['Field', 'Max', 'Mean', 'Last']);
|
||||
});
|
||||
|
||||
test('table panel data assertions', async ({ panelEditPage }) => {
|
||||
test('table panel data assertions', async ({ panelEditPage, selectors }) => {
|
||||
await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200);
|
||||
await panelEditPage.datasource.set('gdev-testdata');
|
||||
await panelEditPage.setVisualization('Table');
|
||||
await setVisualization(panelEditPage, 'Table', selectors);
|
||||
await panelEditPage.refreshPanel();
|
||||
await expect(
|
||||
panelEditPage.panel.locator,
|
||||
@@ -43,10 +44,10 @@ test.describe(
|
||||
).toContainText(['val1', 'val2', 'val3', 'val4']);
|
||||
});
|
||||
|
||||
test('timeseries panel - table view assertions', async ({ panelEditPage }) => {
|
||||
test('timeseries panel - table view assertions', async ({ panelEditPage, selectors }) => {
|
||||
await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200);
|
||||
await panelEditPage.datasource.set('gdev-testdata');
|
||||
await panelEditPage.setVisualization('Time series');
|
||||
await setVisualization(panelEditPage, 'Time series', selectors);
|
||||
await panelEditPage.refreshPanel();
|
||||
await panelEditPage.toggleTableView();
|
||||
await expect(
|
||||
|
||||
+27
-26
@@ -1,5 +1,6 @@
|
||||
import { expect, test } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setVisualization } from '../../../panels-suite/vizpicker-utils';
|
||||
import { formatExpectError } from '../errors';
|
||||
import { successfulDataQuery } from '../mocks/queries';
|
||||
import { scenarios } from '../mocks/resources';
|
||||
@@ -53,10 +54,10 @@ test.describe(
|
||||
).toHaveText(scenarios.map((s) => s.name));
|
||||
});
|
||||
|
||||
test('mocked query data response', async ({ panelEditPage, page }) => {
|
||||
test('mocked query data response', async ({ panelEditPage, page, selectors }) => {
|
||||
await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200);
|
||||
await panelEditPage.datasource.set('gdev-testdata');
|
||||
await panelEditPage.setVisualization(TABLE_VIZ_NAME);
|
||||
await setVisualization(panelEditPage, TABLE_VIZ_NAME, selectors);
|
||||
await panelEditPage.refreshPanel();
|
||||
await expect(
|
||||
panelEditPage.panel.getErrorIcon(),
|
||||
@@ -75,9 +76,9 @@ test.describe(
|
||||
selectors,
|
||||
page,
|
||||
}) => {
|
||||
await panelEditPage.setVisualization(TABLE_VIZ_NAME);
|
||||
await setVisualization(panelEditPage, TABLE_VIZ_NAME, selectors);
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker),
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header),
|
||||
formatExpectError('Expected panel visualization to be set to table')
|
||||
).toHaveText(TABLE_VIZ_NAME);
|
||||
await panelEditPage.setPanelTitle(PANEL_TITLE);
|
||||
@@ -92,8 +93,8 @@ test.describe(
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Select time zone in timezone picker', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
test('Select time zone in timezone picker', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
const axisOptions = await panelEditPage.getCustomOptions('Axis');
|
||||
const timeZonePicker = axisOptions.getSelect('Time zone');
|
||||
|
||||
@@ -101,8 +102,8 @@ test.describe(
|
||||
await expect(timeZonePicker).toHaveSelected('Europe/Stockholm');
|
||||
});
|
||||
|
||||
test('select unit in unit picker', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
test('select unit in unit picker', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
const standardOptions = panelEditPage.getStandardOptions();
|
||||
const unitPicker = standardOptions.getUnitPicker('Unit');
|
||||
|
||||
@@ -111,8 +112,8 @@ test.describe(
|
||||
await expect(unitPicker).toHaveSelected('Pixels');
|
||||
});
|
||||
|
||||
test('enter value in number input', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
test('enter value in number input', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const lineWith = axisOptions.getNumberInput('Soft min');
|
||||
|
||||
@@ -121,8 +122,8 @@ test.describe(
|
||||
await expect(lineWith).toHaveValue('10');
|
||||
});
|
||||
|
||||
test('enter value in slider', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
test('enter value in slider', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
const graphOptions = panelEditPage.getCustomOptions('Graph styles');
|
||||
const lineWidth = graphOptions.getSliderInput('Line width');
|
||||
|
||||
@@ -131,8 +132,8 @@ test.describe(
|
||||
await expect(lineWidth).toHaveValue('10');
|
||||
});
|
||||
|
||||
test('select value in single value select', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
test('select value in single value select', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
const standardOptions = panelEditPage.getStandardOptions();
|
||||
const colorSchemeSelect = standardOptions.getSelect('Color scheme');
|
||||
|
||||
@@ -140,8 +141,8 @@ test.describe(
|
||||
await expect(colorSchemeSelect).toHaveSelected('Classic palette');
|
||||
});
|
||||
|
||||
test('clear input', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
test('clear input', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
const panelOptions = panelEditPage.getPanelOptions();
|
||||
const title = panelOptions.getTextInput('Title');
|
||||
|
||||
@@ -150,8 +151,8 @@ test.describe(
|
||||
await expect(title).toHaveValue('');
|
||||
});
|
||||
|
||||
test('enter value in input', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
test('enter value in input', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
const panelOptions = panelEditPage.getPanelOptions();
|
||||
const description = panelOptions.getTextInput('Description');
|
||||
|
||||
@@ -160,8 +161,8 @@ test.describe(
|
||||
await expect(description).toHaveValue('This is a panel');
|
||||
});
|
||||
|
||||
test('unchecking switch', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
test('unchecking switch', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const showBorder = axisOptions.getSwitch('Show border');
|
||||
|
||||
@@ -173,8 +174,8 @@ test.describe(
|
||||
await expect(showBorder).toBeChecked({ checked: false });
|
||||
});
|
||||
|
||||
test('checking switch', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
test('checking switch', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const showBorder = axisOptions.getSwitch('Show border');
|
||||
|
||||
@@ -183,8 +184,8 @@ test.describe(
|
||||
await expect(showBorder).toBeChecked();
|
||||
});
|
||||
|
||||
test('re-selecting value in radio button group', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
test('re-selecting value in radio button group', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const placement = axisOptions.getRadioGroup('Placement');
|
||||
|
||||
@@ -195,8 +196,8 @@ test.describe(
|
||||
await expect(placement).toHaveChecked('Auto');
|
||||
});
|
||||
|
||||
test('selecting value in radio button group', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
test('selecting value in radio button group', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const placement = axisOptions.getRadioGroup('Placement');
|
||||
|
||||
|
||||
@@ -45,7 +45,9 @@ test.describe(
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item(panel.name)).click();
|
||||
|
||||
// Verify panel type is selected
|
||||
await expect(vizPicker).toHaveText(panel.name, { timeout: 10000 });
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header)
|
||||
).toHaveText(panel.name, { timeout: 10000 });
|
||||
|
||||
// Wait for panel to finish rendering
|
||||
await expect(page.getByLabel('Panel loading bar')).toHaveCount(0, { timeout: 10000 });
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('Panels smokescreen', () => {
|
||||
e2e.components.PanelEditor.toggleVizPicker().click();
|
||||
e2e.components.PluginVisualization.item(panel.name).scrollIntoView().should('be.visible').click();
|
||||
|
||||
e2e.components.PanelEditor.toggleVizPicker().should((e) => expect(e).to.contain(panel.name));
|
||||
e2e.components.PanelEditor.OptionsPane.content().should((e) => expect(e).to.contain(panel.name));
|
||||
// TODO: Come up with better check / better failure messaging to clearly indicate which panel failed
|
||||
cy.contains('An unexpected error happened').should('not.exist');
|
||||
}
|
||||
|
||||
@@ -583,6 +583,9 @@ export const versionedComponents = {
|
||||
'11.1.0': 'data-testid Panel editor option pane content',
|
||||
[MIN_GRAFANA_VERSION]: 'Panel editor option pane content',
|
||||
},
|
||||
header: {
|
||||
'12.4.0': 'data-testid Panel editor OptionsPane header',
|
||||
},
|
||||
select: {
|
||||
[MIN_GRAFANA_VERSION]: 'Panel editor option pane select',
|
||||
},
|
||||
|
||||
@@ -257,6 +257,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
searchQuery: '',
|
||||
listMode: OptionFilter.All,
|
||||
isVizPickerOpen: isUnconfigured,
|
||||
isNewPanel: this.state.isNewPanel,
|
||||
}),
|
||||
isInitializing: false,
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { SceneComponentProps, VizPanel } from '@grafana/scenes';
|
||||
import { Button, Spinner, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
import { Button, Spinner, ToolbarButton, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { MIN_SUGGESTIONS_PANE_WIDTH } from 'app/features/panel/suggestions/constants';
|
||||
|
||||
import { useEditPaneCollapsed } from '../edit-pane/shared';
|
||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||
@@ -25,6 +26,8 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
||||
|
||||
const isScrollingLayout = useScrollReflowLimit();
|
||||
|
||||
const theme = useTheme2();
|
||||
const panePadding = useMemo(() => +theme.spacing(2).replace(/px$/, ''), [theme]);
|
||||
const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } =
|
||||
useSnappingSplitter({
|
||||
direction: 'row',
|
||||
@@ -32,7 +35,7 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
||||
initialSize: 330,
|
||||
usePixels: true,
|
||||
collapsed: isInitiallyCollapsed,
|
||||
collapseBelowPixels: 250,
|
||||
collapseBelowPixels: MIN_SUGGESTIONS_PANE_WIDTH + panePadding,
|
||||
disabled: isScrollingLayout,
|
||||
});
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
SelectableValue,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { config, locationService, reportInteraction } from '@grafana/runtime';
|
||||
import {
|
||||
DeepPartial,
|
||||
SceneComponentProps,
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
VizPanel,
|
||||
sceneGraph,
|
||||
} from '@grafana/scenes';
|
||||
import { Button, FilterInput, ScrollContainer, Stack, ToolbarButton, useStyles2, Field } from '@grafana/ui';
|
||||
import { Button, FilterInput, ScrollContainer, Stack, ToolbarButton, useStyles2, Text } from '@grafana/ui';
|
||||
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
|
||||
import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError';
|
||||
import { VizTypeChangeDetails } from 'app/features/panel/components/VizTypePicker/types';
|
||||
@@ -39,6 +39,8 @@ export interface PanelOptionsPaneState extends SceneObjectState {
|
||||
searchQuery: string;
|
||||
listMode: OptionFilter;
|
||||
panelRef: SceneObjectRef<VizPanel>;
|
||||
isNewPanel?: boolean;
|
||||
hasPickedViz?: boolean;
|
||||
}
|
||||
|
||||
interface PluginOptionsCache {
|
||||
@@ -50,11 +52,15 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
||||
private _cachedPluginOptions: Record<string, PluginOptionsCache | undefined> = {};
|
||||
|
||||
onToggleVizPicker = () => {
|
||||
const newState = !this.state.isVizPickerOpen;
|
||||
reportInteraction(INTERACTION_EVENT_NAME, {
|
||||
item: INTERACTION_ITEM.TOGGLE_DROPDOWN,
|
||||
open: !this.state.isVizPickerOpen,
|
||||
open: newState,
|
||||
});
|
||||
this.setState({
|
||||
isVizPickerOpen: newState,
|
||||
hasPickedViz: this.state.hasPickedViz || newState === false,
|
||||
});
|
||||
this.setState({ isVizPickerOpen: !this.state.isVizPickerOpen });
|
||||
};
|
||||
|
||||
onChangePanelPlugin = (options: VizTypeChangeDetails) => {
|
||||
@@ -131,7 +137,7 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
||||
}
|
||||
|
||||
function PanelOptionsPaneComponent({ model }: SceneComponentProps<PanelOptionsPane>) {
|
||||
const { isVizPickerOpen, searchQuery, listMode, panelRef } = model.useState();
|
||||
const { isVizPickerOpen, searchQuery, listMode, panelRef, isNewPanel, hasPickedViz } = model.useState();
|
||||
const panel = panelRef.resolve();
|
||||
const { pluginId } = panel.useState();
|
||||
const { data } = sceneGraph.getData(panel).useState();
|
||||
@@ -142,34 +148,65 @@ function PanelOptionsPaneComponent({ model }: SceneComponentProps<PanelOptionsPa
|
||||
const onlyOverrides = listMode === OptionFilter.Overrides;
|
||||
const isScrollingLayout = useScrollReflowLimit();
|
||||
|
||||
const pluginMeta: PanelPluginMeta = useMemo(() => {
|
||||
let meta = getAllPanelPluginMeta().filter((p) => p.id === pluginId)[0];
|
||||
if (!meta) {
|
||||
const notFound = getPanelPluginNotFound(`Panel plugin not found (${pluginId})`, true);
|
||||
meta = notFound.meta;
|
||||
}
|
||||
return meta;
|
||||
}, [pluginId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isVizPickerOpen && (
|
||||
<>
|
||||
<div className={styles.top}>
|
||||
<Field label={t('dashboard.panel-edit.visualization-button-label', 'Visualization')} noMargin>
|
||||
<Stack gap={1}>
|
||||
<VisualizationButton pluginId={pluginId} onOpen={model.onToggleVizPicker} />
|
||||
<Button
|
||||
icon="search"
|
||||
variant="secondary"
|
||||
onClick={setIsSearchingOptions}
|
||||
tooltip={t('dashboard.panel-edit.visualization-button-tooltip', 'Search options')}
|
||||
/>
|
||||
{hasFieldConfig && (
|
||||
<ToolbarButton
|
||||
icon="filter"
|
||||
tooltip={t('dashboard.panel-edit.only-overrides-button-tooltip', 'Show only overrides')}
|
||||
variant={onlyOverrides ? 'active' : 'canvas'}
|
||||
onClick={() => {
|
||||
model.onSetListMode(onlyOverrides ? OptionFilter.All : OptionFilter.Overrides);
|
||||
}}
|
||||
/>
|
||||
<Stack gap={1}>
|
||||
<img alt={pluginMeta.name} src={pluginMeta.info.logos.small} className={styles.pluginIcon} />
|
||||
<Text
|
||||
data-testid={selectors.components.PanelEditor.OptionsPane.header}
|
||||
element="h3"
|
||||
variant="body"
|
||||
weight="medium"
|
||||
truncate
|
||||
>
|
||||
{pluginMeta.name}
|
||||
</Text>
|
||||
<Button
|
||||
size="sm"
|
||||
fill="text"
|
||||
onClick={model.onToggleVizPicker}
|
||||
data-testid={selectors.components.PanelEditor.toggleVizPicker}
|
||||
aria-label={t(
|
||||
'dashboard-scene.visualization-button.aria-label-change-visualization',
|
||||
'Change visualization'
|
||||
)}
|
||||
</Stack>
|
||||
</Field>
|
||||
|
||||
{isSearchingOptions && (
|
||||
>
|
||||
<Trans i18nKey="dashboard-scene.visualization-button.text">Change</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack gap={1}>
|
||||
{hasFieldConfig && (
|
||||
<ToolbarButton
|
||||
icon="sliders-v-alt"
|
||||
tooltip={t('dashboard.panel-edit.only-overrides-button-tooltip', 'Show only overrides')}
|
||||
variant={onlyOverrides ? 'active' : 'canvas'}
|
||||
onClick={() => {
|
||||
model.onSetListMode(onlyOverrides ? OptionFilter.All : OptionFilter.Overrides);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
icon="search"
|
||||
variant="secondary"
|
||||
onClick={setIsSearchingOptions}
|
||||
tooltip={t('dashboard.panel-edit.visualization-button-tooltip', 'Search options')}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
{isSearchingOptions && (
|
||||
<div className={styles.searchWrapper}>
|
||||
<FilterInput
|
||||
className={styles.searchOptions}
|
||||
value={searchQuery}
|
||||
@@ -182,8 +219,8 @@ function PanelOptionsPaneComponent({ model }: SceneComponentProps<PanelOptionsPa
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ScrollContainer minHeight={isScrollingLayout ? 'max-content' : 0}>
|
||||
<PanelOptions panel={panel} searchQuery={searchQuery} listMode={listMode} data={data} />
|
||||
</ScrollContainer>
|
||||
@@ -195,6 +232,7 @@ function PanelOptionsPaneComponent({ model }: SceneComponentProps<PanelOptionsPa
|
||||
onChange={model.onChangePanelPlugin}
|
||||
onClose={model.onToggleVizPicker}
|
||||
data={data}
|
||||
showBackButton={config.featureToggles.newVizSuggestions ? hasPickedViz || !isNewPanel : true}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -205,63 +243,24 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
top: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: theme.spacing(1, 2, 2, 2),
|
||||
flexDirection: 'row',
|
||||
padding: theme.spacing(1, 2),
|
||||
gap: theme.spacing(2),
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
searchOptions: css({
|
||||
minHeight: theme.spacing(4),
|
||||
}),
|
||||
searchWrapper: css({
|
||||
padding: theme.spacing(2, 2, 2, 0),
|
||||
padding: theme.spacing(1, 2, 2, 2),
|
||||
}),
|
||||
rotateIcon: css({
|
||||
rotate: '180deg',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
interface VisualizationButtonProps {
|
||||
pluginId: string;
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
export function VisualizationButton({ pluginId, onOpen }: VisualizationButtonProps) {
|
||||
const styles = useStyles2(getVizButtonStyles);
|
||||
let pluginMeta: PanelPluginMeta | undefined = useMemo(
|
||||
() => getAllPanelPluginMeta().filter((p) => p.id === pluginId)[0],
|
||||
[pluginId]
|
||||
);
|
||||
|
||||
if (!pluginMeta) {
|
||||
const notFound = getPanelPluginNotFound(`Panel plugin not found (${pluginId})`, true);
|
||||
pluginMeta = notFound.meta;
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
className={styles.vizButton}
|
||||
tooltip={t(
|
||||
'dashboard-scene.visualization-button.tooltip-click-to-change-visualization',
|
||||
'Click to change visualization'
|
||||
)}
|
||||
imgSrc={pluginMeta.info.logos.small}
|
||||
onClick={onOpen}
|
||||
data-testid={selectors.components.PanelEditor.toggleVizPicker}
|
||||
aria-label={t('dashboard-scene.visualization-button.aria-label-change-visualization', 'Change visualization')}
|
||||
variant="canvas"
|
||||
isOpen={false}
|
||||
fullWidth
|
||||
>
|
||||
{pluginMeta.name}
|
||||
</ToolbarButton>
|
||||
);
|
||||
}
|
||||
|
||||
function getVizButtonStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
vizButton: css({
|
||||
textAlign: 'left',
|
||||
pluginIcon: css({
|
||||
height: '22px',
|
||||
width: '22px',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSessionStorage } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2, PanelData } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { FilterInput, ScrollContainer, Tab, TabContent, TabsBar, useStyles2 } from '@grafana/ui';
|
||||
import { Button, FilterInput, ScrollContainer, Stack, Tab, TabContent, TabsBar, useStyles2 } from '@grafana/ui';
|
||||
import { LS_VISUALIZATION_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';
|
||||
@@ -20,6 +21,7 @@ import { INTERACTION_EVENT_NAME, INTERACTION_ITEM } from './interaction';
|
||||
|
||||
export interface Props {
|
||||
data?: PanelData;
|
||||
showBackButton?: boolean;
|
||||
panel: VizPanel;
|
||||
onChange: (options: VizTypeChangeDetails) => void;
|
||||
onClose: () => void;
|
||||
@@ -39,7 +41,7 @@ const getTabs = (): Array<{ label: string; value: VisualizationSelectPaneTab }>
|
||||
: [allVisualizationsTab, suggestionsTab];
|
||||
};
|
||||
|
||||
export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
|
||||
export function PanelVizTypePicker({ panel, data, onChange, onClose, showBackButton }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const panelModel = useMemo(() => new PanelModelCompatibilityWrapper(panel), [panel]);
|
||||
|
||||
@@ -60,9 +62,6 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearchQuery(value);
|
||||
};
|
||||
|
||||
/** TABS */
|
||||
const tabs = useMemo(getTabs, []);
|
||||
@@ -84,15 +83,6 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{/*@TODO: Re-enable/move close button*/}
|
||||
{/*<Button*/}
|
||||
{/* aria-label={t('dashboard-scene.panel-viz-type-picker.title-close', 'Close')}*/}
|
||||
{/* variant="secondary"*/}
|
||||
{/* icon="angle-up"*/}
|
||||
{/* className={styles.closeButton}*/}
|
||||
{/* data-testid={selectors.components.PanelEditor.toggleVizPicker}*/}
|
||||
{/* onClick={onClose}*/}
|
||||
{/*/>*/}
|
||||
<TabsBar className={styles.tabs} hideBorder={true}>
|
||||
{tabs.map((tab) => (
|
||||
<Tab
|
||||
@@ -105,27 +95,39 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
|
||||
))}
|
||||
</TabsBar>
|
||||
<ScrollContainer>
|
||||
<TabContent>
|
||||
<TabContent className={styles.tabContent}>
|
||||
{listMode === VisualizationSelectPaneTab.Suggestions && (
|
||||
<VisualizationSuggestions onChange={onChange} panel={panelModel} data={data} />
|
||||
)}
|
||||
{listMode === VisualizationSelectPaneTab.Visualizations && (
|
||||
<>
|
||||
<div className={styles.searchRow}>
|
||||
<Stack gap={1} direction="column">
|
||||
<Stack direction="row" gap={1}>
|
||||
{showBackButton && (
|
||||
<Button
|
||||
aria-label={t('dashboard-scene.panel-viz-type-picker.title-close', 'Close')}
|
||||
fill="text"
|
||||
variant="secondary"
|
||||
icon="arrow-left"
|
||||
data-testid={selectors.components.PanelEditor.toggleVizPicker}
|
||||
onClick={onClose}
|
||||
>
|
||||
<Trans i18nKey="dashboard-scene.panel-viz-type-picker.button.close">Back</Trans>
|
||||
</Button>
|
||||
)}
|
||||
<FilterInput
|
||||
className={styles.filter}
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
onChange={setSearchQuery}
|
||||
placeholder={t('dashboard-scene.panel-viz-type-picker.placeholder-search-for', 'Search for...')}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
<VizTypePicker
|
||||
pluginId={panel.state.pluginId}
|
||||
searchQuery={searchQuery}
|
||||
trackSearch={trackSearch}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
</Stack>
|
||||
)}
|
||||
</TabContent>
|
||||
</ScrollContainer>
|
||||
@@ -138,7 +140,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
padding: theme.spacing(2, 1),
|
||||
height: '100%',
|
||||
gap: theme.spacing(2),
|
||||
}),
|
||||
@@ -154,6 +155,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
}),
|
||||
tabContent: css({
|
||||
paddingInline: theme.spacing(2),
|
||||
}),
|
||||
closeButton: css({
|
||||
marginLeft: 'auto',
|
||||
}),
|
||||
|
||||
+52
-38
@@ -1,8 +1,8 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { CSSProperties, HTMLAttributes } from 'react';
|
||||
import { CSSProperties, HTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
import { GrafanaTheme2, PanelData, PanelPluginVisualizationSuggestion } from '@grafana/data';
|
||||
import { colorManipulator, GrafanaTheme2, PanelData, PanelPluginVisualizationSuggestion } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Tooltip, useStyles2 } from '@grafana/ui';
|
||||
@@ -16,7 +16,14 @@ export interface Props extends HTMLAttributes<HTMLButtonElement> {
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export function VisualizationSuggestionCard({ data, suggestion, width, isSelected = false, onClick }: Props) {
|
||||
export function VisualizationSuggestionCard({
|
||||
data,
|
||||
suggestion,
|
||||
width,
|
||||
isSelected = false,
|
||||
className,
|
||||
...restProps
|
||||
}: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { innerStyles, outerStyles, renderWidth, renderHeight } = getPreviewDimensionsAndStyles(width);
|
||||
const cardOptions = suggestion.cardOptions ?? {};
|
||||
@@ -24,35 +31,30 @@ export function VisualizationSuggestionCard({ data, suggestion, width, isSelecte
|
||||
|
||||
const commonButtonProps = {
|
||||
'aria-label': suggestion.name,
|
||||
className: cx(styles.vizBox, isNewVizSuggestionsEnabled && isSelected && styles.selectedBox),
|
||||
className: cx(className, styles.vizBox),
|
||||
'data-testid': selectors.components.VisualizationPreview.card(suggestion.name),
|
||||
style: outerStyles,
|
||||
onClick,
|
||||
...restProps,
|
||||
};
|
||||
|
||||
let content: ReactNode;
|
||||
|
||||
if (cardOptions.imgSrc) {
|
||||
return (
|
||||
<Tooltip content={suggestion.description ?? suggestion.name}>
|
||||
<button
|
||||
{...commonButtonProps}
|
||||
className={cx(styles.vizBox, styles.imgBox, isNewVizSuggestionsEnabled && isSelected && styles.selectedBox)}
|
||||
>
|
||||
<div className={styles.name}>{suggestion.name}</div>
|
||||
<img className={styles.img} src={cardOptions.imgSrc} alt={suggestion.name} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
content = (
|
||||
<button {...commonButtonProps} className={cx(commonButtonProps.className, styles.imgBox)}>
|
||||
<div className={styles.name}>{suggestion.name}</div>
|
||||
<img className={styles.img} src={cardOptions.imgSrc} alt={suggestion.name} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let preview = suggestion;
|
||||
if (suggestion.cardOptions?.previewModifier) {
|
||||
preview = cloneDeep(suggestion);
|
||||
suggestion.cardOptions.previewModifier(preview);
|
||||
}
|
||||
|
||||
let preview = suggestion;
|
||||
if (suggestion.cardOptions?.previewModifier) {
|
||||
preview = cloneDeep(suggestion);
|
||||
suggestion.cardOptions.previewModifier(preview);
|
||||
}
|
||||
|
||||
return (
|
||||
<button {...commonButtonProps}>
|
||||
<Tooltip content={suggestion.name}>
|
||||
content = (
|
||||
<button {...commonButtonProps}>
|
||||
<div style={innerStyles} className={styles.renderContainer}>
|
||||
<PanelRenderer
|
||||
title=""
|
||||
@@ -63,22 +65,38 @@ export function VisualizationSuggestionCard({ data, suggestion, width, isSelecte
|
||||
options={preview.options}
|
||||
fieldConfig={preview.fieldConfig}
|
||||
/>
|
||||
<div className={styles.hoverPane} />
|
||||
{/* this prevents interaction with the underlying panel. */}
|
||||
<div className={cx(styles.hoverPane, isSelected && styles.hoverPaneSelected)} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</button>
|
||||
);
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNewVizSuggestionsEnabled) {
|
||||
return <Tooltip content={suggestion.description ?? suggestion.name}>{content}</Tooltip>;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
hoverPane: css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
borderRadius: theme.spacing(2),
|
||||
bottom: 0,
|
||||
top: -4,
|
||||
left: -4,
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
borderRadius: theme.spacing(0.5),
|
||||
background: 'transparent',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create(['background'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
hoverPaneSelected: css({
|
||||
background: colorManipulator.alpha(theme.colors.text.primary, 0.1),
|
||||
}),
|
||||
vizBox: css({
|
||||
position: 'relative',
|
||||
@@ -97,10 +115,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
background: theme.colors.background.secondary,
|
||||
},
|
||||
}),
|
||||
selectedBox: css({
|
||||
border: `2px solid ${theme.colors.primary.main}`,
|
||||
boxShadow: `0 0 0 1px ${theme.colors.primary.main}`,
|
||||
}),
|
||||
imgBox: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useAsync, useMeasure } from 'react-use';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { GrafanaTheme2, PanelData, PanelModel, PanelPluginVisualizationSuggestion } from '@grafana/data';
|
||||
import {
|
||||
GrafanaTheme2,
|
||||
PanelData,
|
||||
PanelModel,
|
||||
PanelPluginMeta,
|
||||
PanelPluginVisualizationSuggestion,
|
||||
} from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, Icon, Text, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, Button, Icon, Spinner, Text, useStyles2 } from '@grafana/ui';
|
||||
import { UNCONFIGURED_PANEL_PLUGIN_ID } from 'app/features/dashboard-scene/scene/UnconfiguredPanel';
|
||||
|
||||
import { getAllPanelPluginMeta } from '../../state/util';
|
||||
import { MIN_MULTI_COLUMN_SIZE } from '../../suggestions/constants';
|
||||
import { getAllSuggestions } from '../../suggestions/getAllSuggestions';
|
||||
import { hasData } from '../../suggestions/utils';
|
||||
|
||||
@@ -21,19 +28,35 @@ export interface Props {
|
||||
panel?: PanelModel;
|
||||
}
|
||||
|
||||
const MIN_COLUMN_SIZE = 260;
|
||||
|
||||
export function VisualizationSuggestions({ onChange, data, panel }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { value: suggestions } = useAsync(async () => await getAllSuggestions(data), [data]);
|
||||
const { value: suggestions, loading, error } = useAsync(() => getAllSuggestions(data), [data]);
|
||||
const [suggestionHash, setSuggestionHash] = useState<string | null>(null);
|
||||
const [firstCardRef, { width }] = useMeasure<HTMLDivElement>();
|
||||
const [firstCardHash, setFirstCardHash] = useState<string | null>(null);
|
||||
|
||||
const isNewVizSuggestionsEnabled = config.featureToggles.newVizSuggestions;
|
||||
|
||||
const isUnconfiguredPanel = panel?.type === UNCONFIGURED_PANEL_PLUGIN_ID;
|
||||
|
||||
const suggestionsByVizType = useMemo(() => {
|
||||
const meta = getAllPanelPluginMeta();
|
||||
const record: Record<string, PanelPluginMeta> = {};
|
||||
for (const m of meta) {
|
||||
record[m.id] = m;
|
||||
}
|
||||
|
||||
const result: Array<[PanelPluginMeta | undefined, PanelPluginVisualizationSuggestion[]]> = [];
|
||||
let currentVizType: PanelPluginMeta | undefined = undefined;
|
||||
for (const suggestion of suggestions || []) {
|
||||
const vizType = record[suggestion.pluginId];
|
||||
if (!currentVizType || currentVizType.id !== vizType?.id) {
|
||||
currentVizType = vizType;
|
||||
result.push([vizType, []]);
|
||||
}
|
||||
result[result.length - 1][1].push(suggestion);
|
||||
}
|
||||
return result;
|
||||
}, [suggestions]);
|
||||
|
||||
const applySuggestion = useCallback(
|
||||
(suggestion: PanelPluginVisualizationSuggestion, isPreview?: boolean) => {
|
||||
onChange({
|
||||
@@ -66,19 +89,35 @@ export function VisualizationSuggestions({ onChange, data, panel }: Props) {
|
||||
}
|
||||
}, [suggestions, suggestionHash, firstCardHash, isNewVizSuggestionsEnabled, isUnconfiguredPanel, applySuggestion]);
|
||||
|
||||
const renderEmptyState = () => (
|
||||
<div className={styles.emptyStateWrapper}>
|
||||
<Icon name="chart-line" size="xxxl" className={styles.emptyStateIcon} />
|
||||
<Text element="p" textAlignment="center" color="secondary">
|
||||
<Trans i18nKey="dashboard.new-panel.suggestions.empty-state-message">
|
||||
Run a query to start seeing suggested visualizations
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.loadingContainer}>
|
||||
<Spinner size="xxl" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert title={t('panel.visualization-suggestions.error-loading-suggestions.title', 'Error')} severity="error">
|
||||
<Trans i18nKey="panel.visualization-suggestions.error-loading-suggestions.message">
|
||||
An error occurred when loading visualization suggestions.
|
||||
</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNewVizSuggestionsEnabled && (!data || !hasData(data))) {
|
||||
return renderEmptyState();
|
||||
return (
|
||||
<div className={styles.emptyStateWrapper}>
|
||||
<Icon name="chart-line" size="xxxl" className={styles.emptyStateIcon} />
|
||||
<Text element="p" textAlignment="center" color="secondary">
|
||||
<Trans i18nKey="dashboard.new-panel.suggestions.empty-state-message">
|
||||
Run a query to start seeing suggested visualizations
|
||||
</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
@@ -86,28 +125,40 @@ export function VisualizationSuggestions({ onChange, data, panel }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
// This div is needed in some places to make AutoSizer work
|
||||
<div>
|
||||
<AutoSizer disableHeight style={{ width: '100%', height: '100%' }}>
|
||||
{() => (
|
||||
<div>
|
||||
<div className={styles.grid}>
|
||||
{suggestions?.map((suggestion, index) => {
|
||||
const isCardSelected = isNewVizSuggestionsEnabled && suggestionHash === suggestion.hash;
|
||||
|
||||
<div className={styles.grid}>
|
||||
{isNewVizSuggestionsEnabled
|
||||
? suggestionsByVizType.map(([vizType, vizTypeSuggestions]) => (
|
||||
<>
|
||||
<div className={styles.vizTypeHeader} key={vizType?.id || 'unknown-viz-type'}>
|
||||
<Text variant="body" weight="medium">
|
||||
{vizType?.info && <img className={styles.vizTypeLogo} src={vizType.info.logos.small} alt="" />}
|
||||
{vizType?.name || t('panel.visualization-suggestions.unknown-viz-type', 'Unknown visualization type')}
|
||||
</Text>
|
||||
</div>
|
||||
{vizTypeSuggestions?.map((suggestion, index) => {
|
||||
const isCardSelected = suggestionHash === suggestion.hash;
|
||||
return (
|
||||
<div key={index} className={styles.cardContainer} ref={index === 0 ? firstCardRef : undefined}>
|
||||
<div
|
||||
key={suggestion.hash}
|
||||
className={styles.cardContainer}
|
||||
ref={index === 0 ? firstCardRef : undefined}
|
||||
>
|
||||
{isCardSelected && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size={'md'}
|
||||
onClick={() => applySuggestion(suggestion)}
|
||||
className={styles.applySuggestionButton}
|
||||
aria-label={t(
|
||||
'panel.visualization-suggestions.apply-suggestion-aria-label',
|
||||
'Apply {{suggestionName}} visualization',
|
||||
{ suggestionName: suggestion.name }
|
||||
)}
|
||||
onClick={() =>
|
||||
onChange({
|
||||
pluginId: suggestion.pluginId,
|
||||
withModKey: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('panel.visualization-suggestions.use-this-suggestion', 'Use this suggestion')}
|
||||
</Button>
|
||||
@@ -117,21 +168,39 @@ export function VisualizationSuggestions({ onChange, data, panel }: Props) {
|
||||
suggestion={suggestion}
|
||||
width={width}
|
||||
isSelected={isCardSelected}
|
||||
onClick={() => applySuggestion(suggestion, isNewVizSuggestionsEnabled)}
|
||||
tabIndex={index}
|
||||
onClick={() => applySuggestion(suggestion, true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
))
|
||||
: suggestions?.map((suggestion, index) => (
|
||||
<div key={suggestion.hash} className={styles.cardContainer} ref={index === 0 ? firstCardRef : undefined}>
|
||||
<VisualizationSuggestionCard
|
||||
key={index}
|
||||
data={data}
|
||||
suggestion={suggestion}
|
||||
width={width}
|
||||
tabIndex={index}
|
||||
onClick={() => applySuggestion(suggestion)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
loadingContainer: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
marginTop: theme.spacing(6),
|
||||
}),
|
||||
filterRow: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
@@ -147,7 +216,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
grid: css({
|
||||
display: 'grid',
|
||||
gridGap: theme.spacing(1),
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(${MIN_COLUMN_SIZE}px, 1fr))`,
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(${MIN_MULTI_COLUMN_SIZE}px, 1fr))`,
|
||||
marginBottom: theme.spacing(1),
|
||||
justifyContent: 'space-evenly',
|
||||
}),
|
||||
@@ -167,13 +236,29 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
cardContainer: css({
|
||||
position: 'relative',
|
||||
}),
|
||||
vizTypeHeader: css({
|
||||
gridColumn: '1 / -1',
|
||||
marginBottom: theme.spacing(0.5),
|
||||
marginTop: theme.spacing(2),
|
||||
'&:first-of-type': {
|
||||
marginTop: 0,
|
||||
},
|
||||
}),
|
||||
vizTypeLogo: css({
|
||||
filter: 'grayscale(100%)',
|
||||
maxHeight: `${theme.typography.body.lineHeight}em`,
|
||||
width: `${theme.typography.body.lineHeight}em`,
|
||||
alignItems: 'center',
|
||||
display: 'inline-block',
|
||||
marginRight: theme.spacing(1),
|
||||
}),
|
||||
applySuggestionButton: css({
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
zIndex: 10,
|
||||
padding: '0 16px',
|
||||
padding: theme.spacing(0, 2),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// we overall want the suggestions pane to be at least 260px wide (plus padding, which we will
|
||||
// get from the theme).
|
||||
export const MIN_SUGGESTIONS_PANE_WIDTH = 260;
|
||||
|
||||
// when the layout expands to multi-column, this is the minimum width of each column.
|
||||
// this value ensures that the columns are never larger than 440px - the suggestions look
|
||||
// weird if they get too wide.
|
||||
export const MIN_MULTI_COLUMN_SIZE = 220;
|
||||
@@ -5184,7 +5184,6 @@
|
||||
},
|
||||
"only-overrides-button-tooltip": "Show only overrides",
|
||||
"placeholder-search-options": "Search options",
|
||||
"visualization-button-label": "Visualization",
|
||||
"visualization-button-tooltip": "Search options"
|
||||
},
|
||||
"panel-editor-table-view": {
|
||||
@@ -6223,6 +6222,9 @@
|
||||
}
|
||||
},
|
||||
"panel-viz-type-picker": {
|
||||
"button": {
|
||||
"close": "Back"
|
||||
},
|
||||
"placeholder-search-for": "Search for...",
|
||||
"radio-options": {
|
||||
"label": {
|
||||
@@ -6534,7 +6536,7 @@
|
||||
},
|
||||
"visualization-button": {
|
||||
"aria-label-change-visualization": "Change visualization",
|
||||
"tooltip-click-to-change-visualization": "Click to change visualization"
|
||||
"text": "Change"
|
||||
},
|
||||
"viz-and-data-pane": {
|
||||
"aria-label-open-query-pane": "Open query pane",
|
||||
@@ -11089,6 +11091,11 @@
|
||||
},
|
||||
"visualization-suggestions": {
|
||||
"apply-suggestion-aria-label": "Apply {{suggestionName}} visualization",
|
||||
"error-loading-suggestions": {
|
||||
"message": "An error occurred when loading visualization suggestions.",
|
||||
"title": "Error"
|
||||
},
|
||||
"unknown-viz-type": "Unknown visualization type",
|
||||
"use-this-suggestion": "Use this suggestion"
|
||||
},
|
||||
"viz-type-picker": {
|
||||
|
||||
Reference in New Issue
Block a user