Plugin admin: Add a page to show where panel plugins are used in dashboards (#50909)

This commit is contained in:
Ryan McKinley 2022-06-27 17:23:43 -07:00 committed by GitHub
parent fd6c7d518d
commit b6a9b9804d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 179 additions and 44 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
`,
};
};

View File

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

View File

@ -114,6 +114,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
// Needed due to block formatting context
tabContent: css`
overflow: auto;
height: 100%;
`,
};
};

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ export interface SearchQuery {
ds_uid?: string;
tags?: string[];
kind?: string[];
panel_type?: string;
uid?: string[];
id?: number[];
facet?: FacetField[];