mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugin admin: Add a page to show where panel plugins are used in dashboards (#50909)
This commit is contained in:
parent
fd6c7d518d
commit
b6a9b9804d
@ -5025,11 +5025,11 @@ exports[`better eslint`] = {
|
||||
[264, 15, 56, "Do not use any type assertions.", "2674630592"],
|
||||
[266, 13, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
],
|
||||
"public/app/features/dashboard/containers/DashboardPage.tsx:707193595": [
|
||||
[107, 36, 40, "Do not use any type assertions.", "2547843745"],
|
||||
[107, 73, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[142, 32, 40, "Do not use any type assertions.", "2547843745"],
|
||||
[142, 69, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
"public/app/features/dashboard/containers/DashboardPage.tsx:3173260528": [
|
||||
[108, 36, 40, "Do not use any type assertions.", "2547843745"],
|
||||
[108, 73, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[144, 32, 40, "Do not use any type assertions.", "2547843745"],
|
||||
[144, 69, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
],
|
||||
"public/app/features/dashboard/containers/SoloPanelPage.test.tsx:1655234814": [
|
||||
[26, 29, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
@ -5366,8 +5366,8 @@ exports[`better eslint`] = {
|
||||
[143, 17, 41, "Do not use any type assertions.", "1363816804"],
|
||||
[145, 11, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
],
|
||||
"public/app/features/dashboard/state/initDashboard.ts:4291397377": [
|
||||
[215, 71, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
"public/app/features/dashboard/state/initDashboard.ts:1143357497": [
|
||||
[216, 84, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
],
|
||||
"public/app/features/dashboard/state/reducers.ts:2272523560": [
|
||||
[64, 9, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
@ -6420,8 +6420,8 @@ exports[`better eslint`] = {
|
||||
[32, 25, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[61, 22, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
],
|
||||
"public/app/features/plugins/admin/components/PluginDetailsBody.tsx:3199506957": [
|
||||
[45, 35, 25, "Do not use any type assertions.", "3298329852"]
|
||||
"public/app/features/plugins/admin/components/PluginDetailsBody.tsx:2575001007": [
|
||||
[46, 35, 25, "Do not use any type assertions.", "3298329852"]
|
||||
],
|
||||
"public/app/features/plugins/admin/components/PluginDetailsHeader.tsx:4233259": [
|
||||
[64, 48, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
@ -6462,7 +6462,7 @@ exports[`better eslint`] = {
|
||||
"public/app/features/plugins/admin/pages/PluginDetails.test.tsx:186575126": [
|
||||
[85, 15, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
],
|
||||
"public/app/features/plugins/admin/pages/PluginDetails.tsx:4160094062": [
|
||||
"public/app/features/plugins/admin/pages/PluginDetails.tsx:2558474094": [
|
||||
[43, 18, 32, "Do not use any type assertions.", "4000149916"],
|
||||
[87, 24, 20, "Do not use any type assertions.", "2850977225"]
|
||||
],
|
||||
@ -6478,8 +6478,8 @@ exports[`better eslint`] = {
|
||||
"public/app/features/plugins/admin/state/selectors.ts:345773501": [
|
||||
[62, 23, 27, "Do not use any type assertions.", "1285719276"]
|
||||
],
|
||||
"public/app/features/plugins/admin/types.ts:593250396": [
|
||||
[236, 10, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
"public/app/features/plugins/admin/types.ts:1638901340": [
|
||||
[238, 10, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
],
|
||||
"public/app/features/plugins/built_in_plugins.ts:2973583336": [
|
||||
[93, 22, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||
@ -6839,13 +6839,13 @@ exports[`better eslint`] = {
|
||||
[62, 12, 21, "Do not use any type assertions.", "976337522"],
|
||||
[63, 14, 26, "Do not use any type assertions.", "2909521803"]
|
||||
],
|
||||
"public/app/features/search/page/components/columns.tsx:282017932": [
|
||||
[33, 20, 70, "Do not use any type assertions.", "512413936"],
|
||||
[33, 21, 13, "Do not use any type assertions.", "3933930693"],
|
||||
[33, 31, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[48, 32, 21, "Do not use any type assertions.", "3454251755"],
|
||||
[48, 50, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[127, 15, 56, "Do not use any type assertions.", "1375039711"]
|
||||
"public/app/features/search/page/components/columns.tsx:2440349722": [
|
||||
[34, 20, 70, "Do not use any type assertions.", "512413936"],
|
||||
[34, 21, 13, "Do not use any type assertions.", "3933930693"],
|
||||
[34, 31, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[49, 32, 21, "Do not use any type assertions.", "3454251755"],
|
||||
[49, 50, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||
[145, 15, 56, "Do not use any type assertions.", "1375039711"]
|
||||
],
|
||||
"public/app/features/search/reducers/dashboardSearch.test.ts:1813679195": [
|
||||
[5, 66, 17, "Do not use any type assertions.", "3518965757"],
|
||||
|
@ -394,6 +394,12 @@ func doSearchQuery(
|
||||
hasConstraints = true
|
||||
}
|
||||
|
||||
// Panel type
|
||||
if q.PanelType != "" {
|
||||
fullQuery.AddMust(bluge.NewTermQuery(q.PanelType).SetField(documentFieldPanelType))
|
||||
hasConstraints = true
|
||||
}
|
||||
|
||||
// Datasource
|
||||
if q.Datasource != "" {
|
||||
fullQuery.AddMust(bluge.NewTermQuery(q.Datasource).SetField(documentFieldDSUID))
|
||||
|
@ -20,6 +20,7 @@ type DashboardQuery struct {
|
||||
Datasource string `json:"ds_uid,omitempty"` // "datasource" collides with the JSON value at the same leel :()
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Kind []string `json:"kind,omitempty"`
|
||||
PanelType string `json:"panel_type,omitempty"`
|
||||
UIDs []string `json:"uid,omitempty"`
|
||||
Explain bool `json:"explain,omitempty"` // adds details on why document matched
|
||||
Facet []FacetField `json:"facet,omitempty"`
|
||||
|
@ -46,6 +46,7 @@ export type DashboardPageRouteSearchParams = {
|
||||
editPanel?: string;
|
||||
viewPanel?: string;
|
||||
editview?: string;
|
||||
panelType?: string;
|
||||
inspect?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
@ -129,6 +130,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
urlUid: match.params.uid,
|
||||
urlType: match.params.type,
|
||||
urlFolderId: queryParams.folderId,
|
||||
panelType: queryParams.panelType,
|
||||
routeName: this.props.route.routeName,
|
||||
fixUrl: !isPublic,
|
||||
accessToken: match.params.accessToken,
|
||||
|
@ -24,7 +24,8 @@ export interface InitDashboardArgs {
|
||||
urlUid?: string;
|
||||
urlSlug?: string;
|
||||
urlType?: string;
|
||||
urlFolderId?: string | null;
|
||||
urlFolderId?: string;
|
||||
panelType?: string;
|
||||
accessToken?: string;
|
||||
routeName?: string;
|
||||
fixUrl: boolean;
|
||||
@ -84,7 +85,7 @@ async function fetchDashboard(
|
||||
return dashDTO;
|
||||
}
|
||||
case DashboardRoutes.New: {
|
||||
return getNewDashboardModelData(args.urlFolderId);
|
||||
return getNewDashboardModelData(args.urlFolderId, args.panelType);
|
||||
}
|
||||
default:
|
||||
throw { message: 'Unknown route ' + args.routeName };
|
||||
@ -213,7 +214,7 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
|
||||
};
|
||||
}
|
||||
|
||||
export function getNewDashboardModelData(urlFolderId?: string | null): any {
|
||||
export function getNewDashboardModelData(urlFolderId?: string, panelType?: string): any {
|
||||
const data = {
|
||||
meta: {
|
||||
canStar: false,
|
||||
@ -226,7 +227,7 @@ export function getNewDashboardModelData(urlFolderId?: string | null): any {
|
||||
title: 'New dashboard',
|
||||
panels: [
|
||||
{
|
||||
type: 'add-panel',
|
||||
type: panelType ?? 'add-panel',
|
||||
gridPos: { x: 0, y: 0, w: 12, h: 9 },
|
||||
title: 'Panel Title',
|
||||
},
|
||||
|
@ -10,6 +10,7 @@ import { CatalogPlugin, PluginTabIds } from '../types';
|
||||
|
||||
import { AppConfigCtrlWrapper } from './AppConfigWrapper';
|
||||
import { PluginDashboards } from './PluginDashboards';
|
||||
import { PluginUsage } from './PluginUsage';
|
||||
|
||||
type Props = {
|
||||
plugin: CatalogPlugin;
|
||||
@ -60,6 +61,14 @@ export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.E
|
||||
}
|
||||
}
|
||||
|
||||
if (pageId === PluginTabIds.USAGE && pluginConfig) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<PluginUsage plugin={pluginConfig?.meta} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (pageId === PluginTabIds.DASHBOARDS && pluginConfig) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@ -78,6 +87,7 @@ export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.E
|
||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css`
|
||||
padding: ${theme.spacing(3, 4)};
|
||||
height: 100%;
|
||||
`,
|
||||
readme: css`
|
||||
& img {
|
||||
|
90
public/app/features/plugins/admin/components/PluginUsage.tsx
Normal file
90
public/app/features/plugins/admin/components/PluginUsage.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { GrafanaTheme2, PluginMeta } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { SearchResultsTable } from 'app/features/search/page/components/SearchResultsTable';
|
||||
import { getGrafanaSearcher, SearchQuery } from 'app/features/search/service';
|
||||
|
||||
type Props = {
|
||||
plugin: PluginMeta;
|
||||
};
|
||||
|
||||
export function PluginUsage({ plugin }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const searchQuery = useMemo<SearchQuery>(() => {
|
||||
return {
|
||||
query: '*',
|
||||
panel_type: plugin.id,
|
||||
kind: ['panel'],
|
||||
};
|
||||
}, [plugin]);
|
||||
|
||||
const results = useAsync(() => {
|
||||
return getGrafanaSearcher().search(searchQuery);
|
||||
}, [searchQuery]);
|
||||
|
||||
const found = results.value;
|
||||
if (found?.totalRows) {
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.info}>
|
||||
{plugin.name} is used <b>{found.totalRows}</b> times.
|
||||
</div>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
return (
|
||||
<SearchResultsTable
|
||||
response={found}
|
||||
width={width}
|
||||
height={height}
|
||||
clearSelection={() => {}}
|
||||
keyboardEvents={of()}
|
||||
onTagSelected={() => {}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (!config.featureToggles.panelTitleSearch) {
|
||||
return (
|
||||
<Alert title="Missing feature toggle: panelTitleSearch">
|
||||
Plugin usage requires the new search index to find usage across dashboards
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EmptyListCTA
|
||||
title={`${plugin.name} is not used in any dashboards yet`}
|
||||
buttonIcon="plus"
|
||||
buttonTitle="Create Dashboard"
|
||||
buttonLink={`dashboard/new?panelType=${plugin.id}&editPanel=1`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
wrap: css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`,
|
||||
info: css`
|
||||
padding-bottom: 30px;
|
||||
`,
|
||||
};
|
||||
};
|
@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { PluginIncludeType, PluginType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { usePluginConfig } from '../hooks/usePluginConfig';
|
||||
import { isOrgAdmin } from '../permissions';
|
||||
@ -23,7 +24,6 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, defaultTabs: Plugin
|
||||
const canConfigurePlugins = isOrgAdmin();
|
||||
const tabs: PluginDetailsTab[] = [...defaultTabs];
|
||||
let defaultTab;
|
||||
|
||||
if (isPublished) {
|
||||
tabs.push({
|
||||
label: PluginTabLabels.VERSIONS,
|
||||
@ -39,6 +39,15 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, defaultTabs: Plugin
|
||||
return [tabs, defaultTab];
|
||||
}
|
||||
|
||||
if (config.featureToggles.panelTitleSearch && pluginConfig.meta.type === PluginType.panel) {
|
||||
tabs.push({
|
||||
label: PluginTabLabels.USAGE,
|
||||
icon: 'list-ul',
|
||||
id: PluginTabIds.USAGE,
|
||||
href: `${pathname}?page=${PluginTabIds.USAGE}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (canConfigurePlugins) {
|
||||
if (pluginConfig.meta.type === PluginType.app) {
|
||||
if (pluginConfig.angularConfigCtrl) {
|
||||
|
@ -114,6 +114,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
// Needed due to block formatting context
|
||||
tabContent: css`
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -212,6 +212,7 @@ export enum PluginTabLabels {
|
||||
VERSIONS = 'Version history',
|
||||
CONFIG = 'Config',
|
||||
DASHBOARDS = 'Dashboards',
|
||||
USAGE = 'Usage',
|
||||
}
|
||||
|
||||
export enum PluginTabIds {
|
||||
@ -219,6 +220,7 @@ export enum PluginTabIds {
|
||||
VERSIONS = 'version-history',
|
||||
CONFIG = 'config',
|
||||
DASHBOARDS = 'dashboards',
|
||||
USAGE = 'usage',
|
||||
}
|
||||
|
||||
export enum RequestStatus {
|
||||
|
@ -84,7 +84,8 @@ export const SearchResultsTable = React.memo(
|
||||
clearSelection,
|
||||
styles,
|
||||
onTagSelected,
|
||||
onDatasourceChange
|
||||
onDatasourceChange,
|
||||
response.view?.length >= response.totalRows
|
||||
);
|
||||
}, [response, width, styles, selection, selectionToggle, clearSelection, onTagSelected, onDatasourceChange]);
|
||||
|
||||
|
@ -25,7 +25,8 @@ export const generateColumns = (
|
||||
clearSelection: () => void,
|
||||
styles: { [key: string]: string },
|
||||
onTagSelected: (tag: string) => void,
|
||||
onDatasourceChange?: (datasource?: string) => void
|
||||
onDatasourceChange?: (datasource?: string) => void,
|
||||
showingEverything?: boolean
|
||||
): TableColumn[] => {
|
||||
const columns: TableColumn[] = [];
|
||||
const access = response.view.fields;
|
||||
@ -125,9 +126,26 @@ export const generateColumns = (
|
||||
columns.push(makeTypeColumn(access.kind, access.panel_type, width, styles));
|
||||
availableWidth -= width;
|
||||
|
||||
// Show datasources if we have any
|
||||
if (access.ds_uid && onDatasourceChange) {
|
||||
width = Math.min(availableWidth / 2.5, DATASOURCE_COLUMN_WIDTH);
|
||||
columns.push(
|
||||
makeDataSourceColumn(
|
||||
access.ds_uid,
|
||||
width,
|
||||
styles.typeIcon,
|
||||
styles.datasourceItem,
|
||||
styles.invalidDatasourceItem,
|
||||
onDatasourceChange
|
||||
)
|
||||
);
|
||||
availableWidth -= width;
|
||||
}
|
||||
|
||||
const showTags = !showingEverything || hasValue(response.view.fields.tags);
|
||||
const meta = response.view.dataFrame.meta?.custom as SearchResultMeta;
|
||||
if (meta?.locationInfo && availableWidth > 0) {
|
||||
width = Math.max(availableWidth / 1.75, 300);
|
||||
width = showTags ? Math.max(availableWidth / 1.75, 300) : availableWidth;
|
||||
availableWidth -= width;
|
||||
columns.push({
|
||||
Cell: (p) => {
|
||||
@ -154,23 +172,7 @@ export const generateColumns = (
|
||||
});
|
||||
}
|
||||
|
||||
// Show datasources if we have any
|
||||
if (access.ds_uid && onDatasourceChange) {
|
||||
width = DATASOURCE_COLUMN_WIDTH;
|
||||
columns.push(
|
||||
makeDataSourceColumn(
|
||||
access.ds_uid,
|
||||
width,
|
||||
styles.typeIcon,
|
||||
styles.datasourceItem,
|
||||
styles.invalidDatasourceItem,
|
||||
onDatasourceChange
|
||||
)
|
||||
);
|
||||
availableWidth -= width;
|
||||
}
|
||||
|
||||
if (availableWidth > 0) {
|
||||
if (availableWidth > 0 && showTags) {
|
||||
columns.push(makeTagsColumn(access.tags, availableWidth, styles.tagList, onTagSelected));
|
||||
}
|
||||
|
||||
@ -209,6 +211,15 @@ function getIconForKind(v: string): IconName {
|
||||
return 'question-circle';
|
||||
}
|
||||
|
||||
function hasValue(f: Field): boolean {
|
||||
for (let i = 0; i < f.values.length; i++) {
|
||||
if (f.values.get(i) != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function makeDataSourceColumn(
|
||||
field: Field<string[]>,
|
||||
width: number,
|
||||
|
@ -13,6 +13,7 @@ export interface SearchQuery {
|
||||
ds_uid?: string;
|
||||
tags?: string[];
|
||||
kind?: string[];
|
||||
panel_type?: string;
|
||||
uid?: string[];
|
||||
id?: number[];
|
||||
facet?: FacetField[];
|
||||
|
Loading…
Reference in New Issue
Block a user