Dashboards: POC split between visualizations and widgets (#70850)

This commit is contained in:
Juan Cabanas
2023-07-12 10:09:22 -03:00
committed by GitHub
parent d9057c010c
commit 88bb4a778d
23 changed files with 346 additions and 34 deletions

View File

@@ -4,6 +4,7 @@ import React from 'react';
import { PluginType } from '@grafana/data';
import { locationService, reportInteraction } from '@grafana/runtime';
import { defaultDashboard } from '@grafana/schema';
import config from 'app/core/config';
import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
import {
onCreateNewPanel,
@@ -46,6 +47,7 @@ function setup() {
}
beforeEach(() => {
config.featureToggles = { vizAndWidgetSplit: false };
jest.clearAllMocks();
});
@@ -136,3 +138,15 @@ it('adds a library panel when clicked on menu item Import from library', () => {
expect(locationService.partial).not.toHaveBeenCalled();
expect(onAddLibraryPanel).toHaveBeenCalled();
});
it('renders menu list without Widget button when feature flag is disabled', () => {
setup();
expect(screen.queryByText('Widget')).not.toBeInTheDocument();
});
it('renders menu list with Widget button when feature flag is enabled', () => {
config.featureToggles.vizAndWidgetSplit = true;
setup();
expect(screen.getByText('Widget')).toBeInTheDocument();
});

View File

@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { locationService, reportInteraction } from '@grafana/runtime';
import { config, locationService, reportInteraction } from '@grafana/runtime';
import { Menu } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { DashboardModel } from 'app/features/dashboard/state';
@@ -38,6 +38,17 @@ const AddPanelMenu = ({ dashboard }: Props) => {
dispatch(setInitialDatasource(undefined));
}}
/>
{config.featureToggles.vizAndWidgetSplit && (
<Menu.Item
key="add-widget"
testId={selectors.pages.AddDashboard.itemButton('Add new widget menu item')}
label={t('dashboard.add-menu.widget', 'Widget')}
onClick={() => {
reportInteraction('dashboards_toolbar_add_clicked', { item: 'add_widget' });
locationService.partial({ addWidget: true });
}}
/>
)}
<Menu.Item
key="add-row"
testId={selectors.pages.AddDashboard.itemButton('Add new row menu item')}

View File

@@ -0,0 +1,86 @@
import { css } from '@emotion/css';
import React, { useMemo, useState } from 'react';
import { GrafanaTheme2, PanelPluginMeta } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { CustomScrollbar, Icon, Input, Modal, useStyles2 } from '@grafana/ui';
import { onCreateNewWidgetPanel } from 'app/features/dashboard/utils/dashboard';
import { VizTypePickerPlugin } from 'app/features/panel/components/VizTypePicker/VizTypePickerPlugin';
import { filterPluginList, getWidgetPluginMeta } from 'app/features/panel/state/util';
import { useSelector } from 'app/types';
export const AddWidgetModal = () => {
const styles = useStyles2(getStyles);
const [searchQuery, setSearchQuery] = useState('');
const dashboard = useSelector((state) => state.dashboard.getModel());
const widgetsList: PanelPluginMeta[] = useMemo(() => {
return getWidgetPluginMeta();
}, []);
const filteredWidgetsTypes = useMemo((): PanelPluginMeta[] => {
return filterPluginList(widgetsList, searchQuery);
}, [widgetsList, searchQuery]);
const onDismiss = () => {
locationService.partial({ addWidget: null });
};
return (
<Modal
title="Select widget type"
closeOnEscape
closeOnBackdropClick
isOpen
className={styles.modal}
onClickBackdrop={onDismiss}
onDismiss={onDismiss}
>
<Input
type="search"
autoFocus
className={styles.searchInput}
value={searchQuery}
prefix={<Icon name="search" />}
placeholder="Search widget"
onChange={(e) => {
setSearchQuery(e.currentTarget.value);
}}
/>
<CustomScrollbar>
<div className={styles.grid}>
{filteredWidgetsTypes.map((plugin, index) => (
<VizTypePickerPlugin
disabled={false}
key={plugin.id}
isCurrent={false}
plugin={plugin}
onClick={(e) => {
const id = onCreateNewWidgetPanel(dashboard!, plugin.id);
locationService.partial({ editPanel: id, addWidget: null });
}}
/>
))}
</div>
</CustomScrollbar>
</Modal>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
modal: css`
width: 65%;
max-width: 960px;
${theme.breakpoints.down('md')} {
width: 100%;
}
`,
searchInput: css`
margin-bottom: ${theme.spacing(2)};
`,
grid: css`
display: grid;
grid-gap: ${theme.spacing(1)};
`,
});

View File

@@ -4,9 +4,10 @@ import { useLocalStorage } from 'react-use';
import { GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import { Button, CustomScrollbar, FilterInput, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { Field } from '@grafana/ui/src/components/Forms/Field';
import { LS_VISUALIZATION_SELECT_TAB_KEY } from 'app/core/constants';
import { LS_VISUALIZATION_SELECT_TAB_KEY, LS_WIDGET_SELECT_TAB_KEY } from 'app/core/constants';
import { PanelLibraryOptionsGroup } from 'app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup';
import { VisualizationSuggestions } from 'app/features/panel/components/VizTypePicker/VisualizationSuggestions';
import { VizTypeChangeDetails } from 'app/features/panel/components/VizTypePicker/types';
@@ -28,10 +29,15 @@ interface Props {
export const VisualizationSelectPane = ({ panel, data }: Props) => {
const plugin = useSelector(getPanelPluginWithFallback(panel.type));
const [searchQuery, setSearchQuery] = useState('');
const [listMode, setListMode] = useLocalStorage(
LS_VISUALIZATION_SELECT_TAB_KEY,
VisualizationSelectPaneTab.Visualizations
);
// Add support to show widgets in the visualization picker
const isWidget = !!plugin.meta.skipDataQuery;
const isWidgetEnabled = Boolean(isWidget && config.featureToggles.vizAndWidgetSplit);
const tabKey = isWidgetEnabled ? LS_WIDGET_SELECT_TAB_KEY : LS_VISUALIZATION_SELECT_TAB_KEY;
const defaultTab = isWidgetEnabled ? VisualizationSelectPaneTab.Widgets : VisualizationSelectPaneTab.Visualizations;
const [listMode, setListMode] = useLocalStorage(tabKey, defaultTab);
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
@@ -67,6 +73,15 @@ export const VisualizationSelectPane = ({ panel, data }: Props) => {
},
];
const radioOptionsWidgetFlow: Array<SelectableValue<VisualizationSelectPaneTab>> = [
{ label: 'Widgets', value: VisualizationSelectPaneTab.Widgets },
{
label: 'Library panels',
value: VisualizationSelectPaneTab.LibraryPanels,
description: 'Reusable panels you can share between multiple dashboards.',
},
];
return (
<div className={styles.openWrapper}>
<div className={styles.formBox}>
@@ -88,7 +103,12 @@ export const VisualizationSelectPane = ({ panel, data }: Props) => {
/>
</div>
<Field className={styles.customFieldMargin}>
<RadioButtonGroup options={radioOptions} value={listMode} onChange={setListMode} fullWidth />
<RadioButtonGroup
options={isWidgetEnabled ? radioOptionsWidgetFlow : radioOptions}
value={listMode}
onChange={setListMode}
fullWidth
/>
</Field>
</div>
<div className={styles.scrollWrapper}>
@@ -103,6 +123,17 @@ export const VisualizationSelectPane = ({ panel, data }: Props) => {
onClose={() => {}}
/>
)}
{listMode === VisualizationSelectPaneTab.Widgets && (
<VizTypePicker
current={plugin.meta}
onChange={onVizChange}
searchQuery={searchQuery}
data={data}
onClose={() => {}}
isWidget
/>
)}
{listMode === VisualizationSelectPaneTab.Suggestions && (
<VisualizationSuggestions
current={plugin.meta}
@@ -114,7 +145,12 @@ export const VisualizationSelectPane = ({ panel, data }: Props) => {
/>
)}
{listMode === VisualizationSelectPaneTab.LibraryPanels && (
<PanelLibraryOptionsGroup searchQuery={searchQuery} panel={panel} key="Panel Library" />
<PanelLibraryOptionsGroup
searchQuery={searchQuery}
panel={panel}
key="Panel Library"
isWidget={isWidget}
/>
)}
</div>
</CustomScrollbar>

View File

@@ -72,4 +72,5 @@ export enum VisualizationSelectPaneTab {
Visualizations,
LibraryPanels,
Suggestions,
Widgets,
}