mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboards: POC split between visualizations and widgets (#70850)
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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)};
|
||||
`,
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -72,4 +72,5 @@ export enum VisualizationSelectPaneTab {
|
||||
Visualizations,
|
||||
LibraryPanels,
|
||||
Suggestions,
|
||||
Widgets,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user