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:
@@ -2,16 +2,26 @@ import { css } from '@emotion/css';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, PanelPluginMeta, SelectableValue } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Icon, Button, MultiSelect, useStyles2 } from '@grafana/ui';
|
||||
import { getAllPanelPluginMeta } from 'app/features/panel/state/util';
|
||||
import { getAllPanelPluginMeta, getVizPluginMeta, getWidgetPluginMeta } from 'app/features/panel/state/util';
|
||||
|
||||
export interface Props {
|
||||
onChange: (plugins: PanelPluginMeta[]) => void;
|
||||
maxMenuHeight?: number;
|
||||
isWidget?: boolean;
|
||||
}
|
||||
|
||||
export const PanelTypeFilter = ({ onChange: propsOnChange, maxMenuHeight }: Props): JSX.Element => {
|
||||
const plugins = useMemo<PanelPluginMeta[]>(() => getAllPanelPluginMeta(), []);
|
||||
export const PanelTypeFilter = ({ onChange: propsOnChange, maxMenuHeight, isWidget = false }: Props): JSX.Element => {
|
||||
const getPluginMetaData = (): PanelPluginMeta[] => {
|
||||
if (config.featureToggles.vizAndWidgetSplit) {
|
||||
return isWidget ? getWidgetPluginMeta() : getVizPluginMeta();
|
||||
} else {
|
||||
return getAllPanelPluginMeta();
|
||||
}
|
||||
};
|
||||
|
||||
const plugins = useMemo<PanelPluginMeta[]>(getPluginMetaData, [isWidget]);
|
||||
const options = useMemo(
|
||||
() =>
|
||||
plugins
|
||||
|
||||
@@ -17,3 +17,5 @@ export const EDIT_PANEL_ID = 23763571993;
|
||||
export const DEFAULT_PER_PAGE_PAGINATION = 40;
|
||||
|
||||
export const LS_VISUALIZATION_SELECT_TAB_KEY = 'VisualizationSelectPane.ListMode';
|
||||
|
||||
export const LS_WIDGET_SELECT_TAB_KEY = 'WidgetSelectPane.ListMode';
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events';
|
||||
|
||||
import { cancelVariables, templateVarsChangedInUrl } from '../../variables/state/actions';
|
||||
import { findTemplateVarChanges } from '../../variables/utils';
|
||||
import { AddWidgetModal } from '../components/AddWidgetModal/AddWidgetModal';
|
||||
import { DashNav } from '../components/DashNav';
|
||||
import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed';
|
||||
import { DashboardLoading } from '../components/DashboardLoading/DashboardLoading';
|
||||
@@ -50,6 +51,7 @@ export type DashboardPageRouteSearchParams = {
|
||||
editPanel?: string;
|
||||
viewPanel?: string;
|
||||
editview?: string;
|
||||
addWidget?: boolean;
|
||||
panelType?: string;
|
||||
inspect?: string;
|
||||
from?: string;
|
||||
@@ -411,6 +413,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
sectionNav={sectionNav}
|
||||
/>
|
||||
)}
|
||||
{queryParams.addWidget && config.featureToggles.vizAndWidgetSplit && <AddWidgetModal />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
|
||||
import { locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { defaultDashboard } from '@grafana/schema';
|
||||
import config from 'app/core/config';
|
||||
|
||||
import { createDashboardModelFixture } from '../state/__fixtures__/dashboardFixtures';
|
||||
import { onCreateNewPanel, onCreateNewRow, onAddLibraryPanel } from '../utils/dashboard';
|
||||
@@ -41,6 +42,7 @@ function setup(options?: Partial<Props>) {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
config.featureToggles = { vizAndWidgetSplit: false };
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -101,3 +103,22 @@ it('adds a library panel when clicked Import library panel', () => {
|
||||
expect(locationService.partial).not.toHaveBeenCalled();
|
||||
expect(onAddLibraryPanel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders page without Add Widget button when feature flag is disabled', () => {
|
||||
setup();
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Add visualization' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Add row' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Import library panel' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Add widget' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders page with Add Widget button when feature flag is enabled', () => {
|
||||
config.featureToggles.vizAndWidgetSplit = true;
|
||||
setup();
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Add visualization' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Add row' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Import library panel' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Add widget' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { config, locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { H1, H3, P } from '@grafana/ui/src/unstable';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
@@ -58,6 +58,32 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => {
|
||||
</Button>
|
||||
</div>
|
||||
<div className={cx(styles.centeredContent, styles.others)}>
|
||||
{config.featureToggles.vizAndWidgetSplit && (
|
||||
<div className={cx(styles.containerBox, styles.centeredContent, styles.widgetContainer)}>
|
||||
<div className={styles.headerSmall}>
|
||||
<H3 textAlignment="center" weight="medium">
|
||||
<Trans i18nKey="dashboard.empty.add-widget-header">Add a widget</Trans>
|
||||
</H3>
|
||||
</div>
|
||||
<div className={styles.bodySmall}>
|
||||
<P textAlignment="center" color="secondary">
|
||||
<Trans i18nKey="dashboard.empty.add-widget-body">Create lists, markdowns and other widgets</Trans>
|
||||
</P>
|
||||
</div>
|
||||
<Button
|
||||
icon="plus"
|
||||
fill="outline"
|
||||
data-testid={selectors.pages.AddDashboard.itemButton('Create new widget button')}
|
||||
onClick={() => {
|
||||
reportInteraction('dashboards_emptydashboard_clicked', { item: 'add_widget' });
|
||||
locationService.partial({ addWidget: true });
|
||||
}}
|
||||
disabled={!canCreate}
|
||||
>
|
||||
<Trans i18nKey="dashboard.empty.add-widget-button">Add widget</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className={cx(styles.containerBox, styles.centeredContent, styles.rowContainer)}>
|
||||
<div className={styles.headerSmall}>
|
||||
<H3 textAlignment="center" weight="medium">
|
||||
@@ -148,22 +174,30 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
padding: theme.spacing.gridSize * 4,
|
||||
}),
|
||||
others: css({
|
||||
width: '100%',
|
||||
label: 'others-wrapper',
|
||||
alignItems: 'stretch',
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing.gridSize * 4,
|
||||
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
[theme.breakpoints.down('md')]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}),
|
||||
widgetContainer: css({
|
||||
label: 'widget-container',
|
||||
padding: theme.spacing.gridSize * 3,
|
||||
flex: 1,
|
||||
}),
|
||||
rowContainer: css({
|
||||
label: 'row-container',
|
||||
padding: theme.spacing.gridSize * 3,
|
||||
flex: 1,
|
||||
}),
|
||||
libraryContainer: css({
|
||||
label: 'library-container',
|
||||
padding: theme.spacing.gridSize * 3,
|
||||
flex: 1,
|
||||
}),
|
||||
headerBig: css({
|
||||
marginBottom: theme.spacing.gridSize * 2,
|
||||
|
||||
@@ -20,6 +20,19 @@ export function onCreateNewPanel(dashboard: DashboardModel, datasource?: string)
|
||||
return newPanel.id;
|
||||
}
|
||||
|
||||
export function onCreateNewWidgetPanel(dashboard: DashboardModel, widgetType: string): number | undefined {
|
||||
const newPanel: Partial<PanelModel> = {
|
||||
type: widgetType,
|
||||
title: 'Widget title',
|
||||
gridPos: calculateNewPanelGridPos(dashboard),
|
||||
datasource: null,
|
||||
isNew: true,
|
||||
};
|
||||
|
||||
dashboard.addPanel(newPanel);
|
||||
return newPanel.id;
|
||||
}
|
||||
|
||||
export function onCreateNewRow(dashboard: DashboardModel) {
|
||||
const newRow = {
|
||||
type: 'row',
|
||||
|
||||
@@ -21,6 +21,7 @@ interface LibraryPanelViewProps {
|
||||
panelFilter?: string[];
|
||||
folderFilter?: string[];
|
||||
perPage?: number;
|
||||
isWidget?: boolean;
|
||||
}
|
||||
|
||||
export const LibraryPanelsView = ({
|
||||
@@ -33,6 +34,7 @@ export const LibraryPanelsView = ({
|
||||
showSecondaryActions,
|
||||
currentPanelId: currentPanel,
|
||||
perPage: propsPerPage = 40,
|
||||
isWidget,
|
||||
}: LibraryPanelViewProps) => {
|
||||
const styles = useStyles2(getPanelViewStyles);
|
||||
const [{ libraryPanels, page, perPage, numberOfPages, loadingState, currentPanelId }, dispatch] = useReducer(
|
||||
@@ -55,6 +57,7 @@ export const LibraryPanelsView = ({
|
||||
page,
|
||||
perPage,
|
||||
currentPanelId,
|
||||
isWidget,
|
||||
})
|
||||
),
|
||||
300,
|
||||
|
||||
@@ -3,6 +3,11 @@ import { Dispatch } from 'react';
|
||||
import { from, merge, of, Subscription, timer } from 'rxjs';
|
||||
import { catchError, finalize, mapTo, mergeMap, share, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { PanelPluginMeta } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { LibraryPanel } from '@grafana/schema';
|
||||
import { getAllPanelPluginMeta } from 'app/features/panel/state/util';
|
||||
|
||||
import { deleteLibraryPanel as apiDeleteLibraryPanel, getLibraryPanels } from '../../state/api';
|
||||
|
||||
import { initialLibraryPanelsViewState, initSearch, searchCompleted } from './reducer';
|
||||
@@ -16,9 +21,29 @@ interface SearchArgs {
|
||||
panelFilter?: string[];
|
||||
folderFilterUIDs?: string[];
|
||||
currentPanelId?: string;
|
||||
isWidget?: boolean;
|
||||
}
|
||||
|
||||
export function searchForLibraryPanels(args: SearchArgs): DispatchResult {
|
||||
// Functions to support filtering out library panels per plugin type that have skipDataQuery set to true
|
||||
|
||||
const findPluginMeta = (pluginMeta: PanelPluginMeta, libraryPanel: LibraryPanel) =>
|
||||
pluginMeta.id === libraryPanel.type;
|
||||
|
||||
const filterLibraryPanels = (libraryPanels: LibraryPanel[], isWidget: boolean) => {
|
||||
const pluginMetaList = getAllPanelPluginMeta();
|
||||
|
||||
return libraryPanels.filter((libraryPanel) => {
|
||||
const matchingPluginMeta = pluginMetaList.find((pluginMeta) => findPluginMeta(pluginMeta, libraryPanel));
|
||||
// widget mode filter
|
||||
if (isWidget) {
|
||||
return !!matchingPluginMeta?.skipDataQuery;
|
||||
}
|
||||
// non-widget mode filter
|
||||
return !matchingPluginMeta?.skipDataQuery;
|
||||
});
|
||||
};
|
||||
|
||||
return function (dispatch) {
|
||||
const subscription = new Subscription();
|
||||
const dataObservable = from(
|
||||
@@ -32,6 +57,17 @@ export function searchForLibraryPanels(args: SearchArgs): DispatchResult {
|
||||
folderFilterUIDs: args.folderFilterUIDs,
|
||||
})
|
||||
).pipe(
|
||||
//filter out library panels per plugin type that have skipDataQuery set to true
|
||||
mergeMap((libraryPanelsResult) => {
|
||||
const { elements: libraryPanels } = libraryPanelsResult;
|
||||
|
||||
if (config.featureToggles.vizAndWidgetSplit && args.isWidget !== undefined) {
|
||||
const filteredLibraryPanels = filterLibraryPanels(libraryPanels, args.isWidget);
|
||||
return of({ ...libraryPanelsResult, elements: filteredLibraryPanels });
|
||||
}
|
||||
|
||||
return of({ ...libraryPanelsResult, elements: libraryPanels });
|
||||
}),
|
||||
mergeMap(({ perPage, elements: libraryPanels, page, totalCount }) =>
|
||||
of(searchCompleted({ libraryPanels, page, perPage, totalCount }))
|
||||
),
|
||||
|
||||
@@ -17,9 +17,10 @@ import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView';
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
searchQuery: string;
|
||||
isWidget?: boolean;
|
||||
}
|
||||
|
||||
export const PanelLibraryOptionsGroup = ({ panel, searchQuery }: Props) => {
|
||||
export const PanelLibraryOptionsGroup = ({ panel, searchQuery, isWidget = false }: Props) => {
|
||||
const [showingAddPanelModal, setShowingAddPanelModal] = useState(false);
|
||||
const [changeToPanel, setChangeToPanel] = useState<LibraryElementDTO | undefined>(undefined);
|
||||
const [panelFilter, setPanelFilter] = useState<string[]>([]);
|
||||
@@ -43,7 +44,6 @@ export const PanelLibraryOptionsGroup = ({ panel, searchQuery }: Props) => {
|
||||
|
||||
const onAddToPanelLibrary = () => setShowingAddPanelModal(true);
|
||||
const onDismissChangeToPanel = () => setChangeToPanel(undefined);
|
||||
|
||||
return (
|
||||
<VerticalGroup spacing="md">
|
||||
{!panel.libraryPanel && (
|
||||
@@ -54,7 +54,7 @@ export const PanelLibraryOptionsGroup = ({ panel, searchQuery }: Props) => {
|
||||
</VerticalGroup>
|
||||
)}
|
||||
|
||||
<PanelTypeFilter onChange={onPanelFilterChange} />
|
||||
<PanelTypeFilter onChange={onPanelFilterChange} isWidget={isWidget} />
|
||||
|
||||
<div className={styles.libraryPanelsView}>
|
||||
<LibraryPanelsView
|
||||
@@ -63,6 +63,7 @@ export const PanelLibraryOptionsGroup = ({ panel, searchQuery }: Props) => {
|
||||
panelFilter={panelFilter}
|
||||
onClickCard={setChangeToPanel}
|
||||
showSecondaryActions
|
||||
isWidget={isWidget}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ export const PanelTypeCard = ({
|
||||
<div
|
||||
className={cssClass}
|
||||
aria-label={selectors.components.PluginVisualization.item(plugin.name)}
|
||||
data-testid={selectors.components.PluginVisualization.item(plugin.name)}
|
||||
onClick={isDisabled ? undefined : onClick}
|
||||
title={isCurrent ? 'Click again to close this section' : plugin.name}
|
||||
>
|
||||
|
||||
@@ -2,9 +2,10 @@ import { css } from '@emotion/css';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { GrafanaTheme2, PanelData, PanelPluginMeta } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { EmptySearchResult, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { filterPluginList, getAllPanelPluginMeta } from '../../state/util';
|
||||
import { filterPluginList, getAllPanelPluginMeta, getVizPluginMeta, getWidgetPluginMeta } from '../../state/util';
|
||||
|
||||
import { VizTypePickerPlugin } from './VizTypePickerPlugin';
|
||||
import { VizTypeChangeDetails } from './types';
|
||||
@@ -15,13 +16,20 @@ export interface Props {
|
||||
onChange: (options: VizTypeChangeDetails) => void;
|
||||
searchQuery: string;
|
||||
onClose: () => void;
|
||||
isWidget?: boolean;
|
||||
}
|
||||
|
||||
export function VizTypePicker({ searchQuery, onChange, current, data }: Props) {
|
||||
export function VizTypePicker({ searchQuery, onChange, current, data, isWidget = false }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const pluginsList: PanelPluginMeta[] = useMemo(() => {
|
||||
if (config.featureToggles.vizAndWidgetSplit) {
|
||||
if (isWidget) {
|
||||
return getWidgetPluginMeta();
|
||||
}
|
||||
return getVizPluginMeta();
|
||||
}
|
||||
return getAllPanelPluginMeta();
|
||||
}, []);
|
||||
}, [isWidget]);
|
||||
|
||||
const filteredPluginTypes = useMemo((): PanelPluginMeta[] => {
|
||||
return filterPluginList(pluginsList, searchQuery, current);
|
||||
|
||||
@@ -10,15 +10,23 @@ export function getAllPanelPluginMeta(): PanelPluginMeta[] {
|
||||
.sort((a: PanelPluginMeta, b: PanelPluginMeta) => a.sort - b.sort);
|
||||
}
|
||||
|
||||
export function getWidgetPluginMeta(): PanelPluginMeta[] {
|
||||
return getAllPanelPluginMeta().filter((panel) => !!panel.skipDataQuery);
|
||||
}
|
||||
|
||||
export function getVizPluginMeta(): PanelPluginMeta[] {
|
||||
return getAllPanelPluginMeta().filter((panel) => !panel.skipDataQuery);
|
||||
}
|
||||
|
||||
export function filterPluginList(
|
||||
pluginsList: PanelPluginMeta[],
|
||||
searchQuery: string, // Note: this will be an escaped regex string as it comes from `FilterInput`
|
||||
current: PanelPluginMeta
|
||||
current?: PanelPluginMeta
|
||||
): PanelPluginMeta[] {
|
||||
if (!searchQuery.length) {
|
||||
return pluginsList.filter((p) => {
|
||||
if (p.state === PluginState.deprecated) {
|
||||
return current.id === p.id;
|
||||
return current?.id === p.id;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -30,7 +38,7 @@ export function filterPluginList(
|
||||
const isGraphQuery = 'graph'.startsWith(query);
|
||||
|
||||
for (const item of pluginsList) {
|
||||
if (item.state === PluginState.deprecated && current.id !== item.id) {
|
||||
if (item.state === PluginState.deprecated && current?.id !== item.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,8 @@
|
||||
"import": "Aus Bibliothek importieren",
|
||||
"paste-panel": "Panel einfügen",
|
||||
"row": "Zeile",
|
||||
"visualization": "Visualisierung"
|
||||
"visualization": "Visualisierung",
|
||||
"widget": ""
|
||||
},
|
||||
"empty": {
|
||||
"add-import-body": "Visualisierungen importieren, die mit anderen Dashboards geteilt werden.",
|
||||
@@ -131,7 +132,10 @@
|
||||
"add-row-header": "Eine Zeile hinzufügen",
|
||||
"add-visualization-body": "Wählen Sie eine Datenquelle aus und visualisieren und fragen Sie dann Ihre Daten mit Diagrammen, Statistiken und Tabellen ab oder erstellen Sie Listen, Markierungen und andere Widgets.",
|
||||
"add-visualization-button": "Visualisierung hinzufügen",
|
||||
"add-visualization-header": "Starten Sie Ihr neues Dashboard, indem Sie eine Visualisierung hinzufügen"
|
||||
"add-visualization-header": "Starten Sie Ihr neues Dashboard, indem Sie eine Visualisierung hinzufügen",
|
||||
"add-widget-body": "",
|
||||
"add-widget-button": "",
|
||||
"add-widget-header": ""
|
||||
},
|
||||
"inspect": {
|
||||
"data-tab": "Daten",
|
||||
|
||||
@@ -120,7 +120,8 @@
|
||||
"import": "Import from library",
|
||||
"paste-panel": "Paste panel",
|
||||
"row": "Row",
|
||||
"visualization": "Visualization"
|
||||
"visualization": "Visualization",
|
||||
"widget": "Widget"
|
||||
},
|
||||
"empty": {
|
||||
"add-import-body": "Import visualizations that are shared with other dashboards.",
|
||||
@@ -131,7 +132,10 @@
|
||||
"add-row-header": "Add a row",
|
||||
"add-visualization-body": "Select a data source and then query and visualize your data with charts, stats and tables or create lists, markdowns and other widgets.",
|
||||
"add-visualization-button": "Add visualization",
|
||||
"add-visualization-header": "Start your new dashboard by adding a visualization"
|
||||
"add-visualization-header": "Start your new dashboard by adding a visualization",
|
||||
"add-widget-body": "Create lists, markdowns and other widgets",
|
||||
"add-widget-button": "Add widget",
|
||||
"add-widget-header": "Add a widget"
|
||||
},
|
||||
"inspect": {
|
||||
"data-tab": "Data",
|
||||
|
||||
@@ -125,7 +125,8 @@
|
||||
"import": "Importar de la biblioteca",
|
||||
"paste-panel": "Pegar panel",
|
||||
"row": "Fila",
|
||||
"visualization": "Visualización"
|
||||
"visualization": "Visualización",
|
||||
"widget": ""
|
||||
},
|
||||
"empty": {
|
||||
"add-import-body": "Importar las visualizaciones que se compartan con otros paneles de control.",
|
||||
@@ -136,7 +137,10 @@
|
||||
"add-row-header": "Añadir una fila",
|
||||
"add-visualization-body": "Selecciona una fuente de datos y luego consulta y visualiza tus datos con gráficos, estadísticas y tablas o cree listas, anotaciones y otros widgets.",
|
||||
"add-visualization-button": "Añadir visualización",
|
||||
"add-visualization-header": "Comienza tu nuevo panel de control añadiendo una visualización"
|
||||
"add-visualization-header": "Comienza tu nuevo panel de control añadiendo una visualización",
|
||||
"add-widget-body": "",
|
||||
"add-widget-button": "",
|
||||
"add-widget-header": ""
|
||||
},
|
||||
"inspect": {
|
||||
"data-tab": "Datos",
|
||||
|
||||
@@ -125,7 +125,8 @@
|
||||
"import": "Importer depuis la bibliothèque",
|
||||
"paste-panel": "Coller le panneau",
|
||||
"row": "Ligne",
|
||||
"visualization": "Visualisation"
|
||||
"visualization": "Visualisation",
|
||||
"widget": ""
|
||||
},
|
||||
"empty": {
|
||||
"add-import-body": "Importer des visualisations partagées avec d'autres tableaux de bord.",
|
||||
@@ -136,7 +137,10 @@
|
||||
"add-row-header": "Ajouter une ligne",
|
||||
"add-visualization-body": "Sélectionnez une source de données, puis examinez et visualisez vos données avec des graphiques, des statistiques et des tableaux ou créez des listes, des markdowns et d'autres widgets.",
|
||||
"add-visualization-button": "Ajouter une visualisation",
|
||||
"add-visualization-header": "Commencez votre nouveau tableau de bord en ajoutant une visualisation"
|
||||
"add-visualization-header": "Commencez votre nouveau tableau de bord en ajoutant une visualisation",
|
||||
"add-widget-body": "",
|
||||
"add-widget-button": "",
|
||||
"add-widget-header": ""
|
||||
},
|
||||
"inspect": {
|
||||
"data-tab": "Données",
|
||||
|
||||
@@ -120,7 +120,8 @@
|
||||
"import": "Ĩmpőřŧ ƒřőm ľįþřäřy",
|
||||
"paste-panel": "Päşŧę päʼnęľ",
|
||||
"row": "Ŗőŵ",
|
||||
"visualization": "Vįşūäľįžäŧįőʼn"
|
||||
"visualization": "Vįşūäľįžäŧįőʼn",
|
||||
"widget": "Ŵįđģęŧ"
|
||||
},
|
||||
"empty": {
|
||||
"add-import-body": "Ĩmpőřŧ vįşūäľįžäŧįőʼnş ŧĥäŧ äřę şĥäřęđ ŵįŧĥ őŧĥęř đäşĥþőäřđş.",
|
||||
@@ -131,7 +132,10 @@
|
||||
"add-row-header": "Åđđ ä řőŵ",
|
||||
"add-visualization-body": "Ŝęľęčŧ ä đäŧä şőūřčę äʼnđ ŧĥęʼn qūęřy äʼnđ vįşūäľįžę yőūř đäŧä ŵįŧĥ čĥäřŧş, şŧäŧş äʼnđ ŧäþľęş őř čřęäŧę ľįşŧş, mäřĸđőŵʼnş äʼnđ őŧĥęř ŵįđģęŧş.",
|
||||
"add-visualization-button": "Åđđ vįşūäľįžäŧįőʼn",
|
||||
"add-visualization-header": "Ŝŧäřŧ yőūř ʼnęŵ đäşĥþőäřđ þy äđđįʼnģ ä vįşūäľįžäŧįőʼn"
|
||||
"add-visualization-header": "Ŝŧäřŧ yőūř ʼnęŵ đäşĥþőäřđ þy äđđįʼnģ ä vįşūäľįžäŧįőʼn",
|
||||
"add-widget-body": "Cřęäŧę ľįşŧş, mäřĸđőŵʼnş äʼnđ őŧĥęř ŵįđģęŧş",
|
||||
"add-widget-button": "Åđđ ŵįđģęŧ",
|
||||
"add-widget-header": "Åđđ ä ŵįđģęŧ"
|
||||
},
|
||||
"inspect": {
|
||||
"data-tab": "Đäŧä",
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
"import": "从库导入",
|
||||
"paste-panel": "粘贴面板",
|
||||
"row": "行",
|
||||
"visualization": "可视化"
|
||||
"visualization": "可视化",
|
||||
"widget": ""
|
||||
},
|
||||
"empty": {
|
||||
"add-import-body": "导入与其他仪表板共享的可视化。",
|
||||
@@ -126,7 +127,10 @@
|
||||
"add-row-header": "添加一行",
|
||||
"add-visualization-body": "选择一个数据源,然后用图表、统计信息和表格查询您的数据以及将其可视化,或创建列表、Markdown 和其他小部件。",
|
||||
"add-visualization-button": "添加可视化",
|
||||
"add-visualization-header": "通过添加可视化开始您的新仪表板"
|
||||
"add-visualization-header": "通过添加可视化开始您的新仪表板",
|
||||
"add-widget-body": "",
|
||||
"add-widget-button": "",
|
||||
"add-widget-header": ""
|
||||
},
|
||||
"inspect": {
|
||||
"data-tab": "数据",
|
||||
|
||||
Reference in New Issue
Block a user