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

@@ -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

View File

@@ -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';

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,
}

View File

@@ -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 />}
</>
);
}

View File

@@ -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();
});

View File

@@ -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,

View File

@@ -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',

View File

@@ -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,

View File

@@ -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 }))
),

View File

@@ -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>

View File

@@ -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}
>

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "Đäŧä",

View File

@@ -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": "数据",