From 1bb61660f1722cba473438ebf728c130715d86af Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Thu, 12 Nov 2020 16:21:09 +0000 Subject: [PATCH] Chore: Migrate Dashboard List panel to React (#28607) * Chore: Migrate Dashlist to React Closes #28491 --- .../grafana-data/src/panel/PanelPlugin.ts | 2 +- .../app/plugins/panel/dashlist/DashList.tsx | 156 ++++++++++++++++++ public/app/plugins/panel/dashlist/editor.html | 45 ----- public/app/plugins/panel/dashlist/module.html | 20 --- public/app/plugins/panel/dashlist/module.ts | 156 ------------------ public/app/plugins/panel/dashlist/module.tsx | 81 +++++++++ public/app/plugins/panel/dashlist/styles.ts | 57 +++++++ public/app/plugins/panel/dashlist/types.ts | 10 ++ 8 files changed, 305 insertions(+), 222 deletions(-) create mode 100644 public/app/plugins/panel/dashlist/DashList.tsx delete mode 100644 public/app/plugins/panel/dashlist/editor.html delete mode 100644 public/app/plugins/panel/dashlist/module.html delete mode 100644 public/app/plugins/panel/dashlist/module.ts create mode 100644 public/app/plugins/panel/dashlist/module.tsx create mode 100644 public/app/plugins/panel/dashlist/styles.ts create mode 100644 public/app/plugins/panel/dashlist/types.ts diff --git a/packages/grafana-data/src/panel/PanelPlugin.ts b/packages/grafana-data/src/panel/PanelPlugin.ts index e86a9fec18f..36f78a3e358 100644 --- a/packages/grafana-data/src/panel/PanelPlugin.ts +++ b/packages/grafana-data/src/panel/PanelPlugin.ts @@ -196,7 +196,7 @@ export class PanelPlugin) { this.onPanelMigration = handler; return this; } diff --git a/public/app/plugins/panel/dashlist/DashList.tsx b/public/app/plugins/panel/dashlist/DashList.tsx new file mode 100644 index 00000000000..472fd82c5bf --- /dev/null +++ b/public/app/plugins/panel/dashlist/DashList.tsx @@ -0,0 +1,156 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import take from 'lodash/take'; + +import { PanelProps } from '@grafana/data'; +import { CustomScrollbar, Icon, useStyles } from '@grafana/ui'; + +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import impressionSrv from 'app/core/services/impression_srv'; +import { DashboardSearchHit } from 'app/features/search/types'; +import { DashListOptions } from './types'; +import { getStyles } from './styles'; + +type Dashboard = DashboardSearchHit & { isSearchResult?: boolean; isRecent?: boolean }; + +interface DashboardGroup { + show: boolean; + header: string; + dashboards: Dashboard[]; +} + +async function fetchDashboards(options: DashListOptions) { + let starredDashboards: Promise = Promise.resolve([]); + if (options.showStarred) { + const params = { limit: options.maxItems, starred: 'true' }; + starredDashboards = getBackendSrv().search(params); + } + + let recentDashboards: Promise = Promise.resolve([]); + if (options.showRecentlyViewed) { + const dashIds = take(impressionSrv.getDashboardOpened(), options.maxItems); + recentDashboards = getBackendSrv().search({ dashboardIds: dashIds, limit: options.maxItems }); + } + + let searchedDashboards: Promise = Promise.resolve([]); + if (options.showSearch) { + const params = { + limit: options.maxItems, + query: options.query, + folderIds: options.folderId, + tag: options.tags, + type: 'dash-db', + }; + + searchedDashboards = getBackendSrv().search(params); + } + + const [starred, searched, recent] = await Promise.all([starredDashboards, searchedDashboards, recentDashboards]); + const dashMap = starred.reduce( + (acc, dash) => Object.assign(acc, { [dash.id]: dash }), + {} as Record + ); + + searched.forEach(dash => { + if (dashMap.hasOwnProperty(dash.id)) { + dashMap[dash.id].isSearchResult = true; + } else { + dashMap[dash.id] = { ...dash, isSearchResult: true }; + } + }); + + recent.forEach(dash => { + if (dashMap.hasOwnProperty(dash.id)) { + dashMap[dash.id].isRecent = true; + } else { + dashMap[dash.id] = { ...dash, isRecent: true }; + } + }); + + return dashMap; +} + +export function DashList(props: PanelProps) { + const [dashboards, setDashboards] = useState>({}); + useEffect(() => { + fetchDashboards(props.options).then(dashes => { + setDashboards(dashes); + }); + }, [ + props.options.showSearch, + props.options.showStarred, + props.options.showRecentlyViewed, + props.options.maxItems, + props.options.query, + props.options.tags, + props.options.folderId, + ]); + + const toggleDashboardStar = async (e: React.SyntheticEvent, dash: Dashboard) => { + e.preventDefault(); + e.stopPropagation(); + + const isStarred = await getDashboardSrv().starDashboard(dash.id.toString(), dash.isStarred); + setDashboards(Object.assign({}, dashboards, { [dash.id]: { ...dash, isStarred } })); + }; + + const [starredDashboards, recentDashboards, searchedDashboards] = useMemo(() => { + const dashboardList = Object.values(dashboards); + return [ + dashboardList.filter(dash => dash.isStarred), + dashboardList.filter(dash => dash.isRecent), + dashboardList.filter(dash => dash.isSearchResult), + ]; + }, [dashboards]); + + const { showStarred, showRecentlyViewed, showHeadings, showSearch } = props.options; + + const dashboardGroups: DashboardGroup[] = [ + { + header: 'Starred dashboards', + dashboards: starredDashboards, + show: showStarred, + }, + { + header: 'Recently viewed dashboards', + dashboards: recentDashboards, + show: showRecentlyViewed, + }, + { + header: 'Search', + dashboards: searchedDashboards, + show: showSearch, + }, + ]; + + const css = useStyles(getStyles); + return ( + + {dashboardGroups.map( + ({ show, header, dashboards }, i) => + show && ( +
+ {showHeadings &&
{header}
} +
    + {dashboards.map(dash => ( +
  • +
    +
    + + {dash.title} + + {dash.folderTitle &&
    {dash.folderTitle}
    } +
    + toggleDashboardStar(e, dash)}> + + +
    +
  • + ))} +
+
+ ) + )} +
+ ); +} diff --git a/public/app/plugins/panel/dashlist/editor.html b/public/app/plugins/panel/dashlist/editor.html deleted file mode 100644 index a04d3db54a5..00000000000 --- a/public/app/plugins/panel/dashlist/editor.html +++ /dev/null @@ -1,45 +0,0 @@ -
-
-
Options
- - - - - - - -
- Max items - -
-
- -
-
Search
- -
- Query - -
- -
- - -
- -
- Tags - - -
-
- -
diff --git a/public/app/plugins/panel/dashlist/module.html b/public/app/plugins/panel/dashlist/module.html deleted file mode 100644 index 7a28e65b7b4..00000000000 --- a/public/app/plugins/panel/dashlist/module.html +++ /dev/null @@ -1,20 +0,0 @@ -
- -
diff --git a/public/app/plugins/panel/dashlist/module.ts b/public/app/plugins/panel/dashlist/module.ts deleted file mode 100644 index c6dbde4a85d..00000000000 --- a/public/app/plugins/panel/dashlist/module.ts +++ /dev/null @@ -1,156 +0,0 @@ -import _ from 'lodash'; -import { PanelCtrl } from 'app/plugins/sdk'; -import impressionSrv from 'app/core/services/impression_srv'; -import { auto, IScope } from 'angular'; -import { backendSrv } from 'app/core/services/backend_srv'; -import { DashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { PanelEvents } from '@grafana/data'; -import { promiseToDigest } from '../../../core/utils/promiseToDigest'; - -class DashListCtrl extends PanelCtrl { - static templateUrl = 'module.html'; - static scrollable = true; - - groups: any[]; - modes: any[]; - - panelDefaults: any = { - query: '', - limit: 10, - tags: [], - recent: false, - search: false, - starred: true, - headings: true, - folderId: null, - }; - - /** @ngInject */ - constructor($scope: IScope, $injector: auto.IInjectorService, private dashboardSrv: DashboardSrv) { - super($scope, $injector); - _.defaults(this.panel, this.panelDefaults); - - if (this.panel.tag) { - this.panel.tags = [this.panel.tag]; - delete this.panel.tag; - } - - this.events.on(PanelEvents.refresh, this.onRefresh.bind(this)); - this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this)); - - this.groups = [ - { list: [], show: false, header: 'Starred dashboards' }, - { list: [], show: false, header: 'Recently viewed dashboards' }, - { list: [], show: false, header: 'Search' }, - ]; - - // update capability - if (this.panel.mode) { - if (this.panel.mode === 'starred') { - this.panel.starred = true; - this.panel.headings = false; - } - if (this.panel.mode === 'recently viewed') { - this.panel.recent = true; - this.panel.starred = false; - this.panel.headings = false; - } - if (this.panel.mode === 'search') { - this.panel.search = true; - this.panel.starred = false; - this.panel.headings = false; - } - delete this.panel.mode; - } - } - - onInitEditMode() { - this.modes = ['starred', 'search', 'recently viewed']; - this.addEditorTab('Options', 'public/app/plugins/panel/dashlist/editor.html'); - } - - onRefresh() { - const promises = []; - - promises.push(this.getRecentDashboards()); - promises.push(this.getStarred()); - promises.push(this.getSearch()); - - return Promise.all(promises).then(this.renderingCompleted.bind(this)); - } - - getSearch() { - this.groups[2].show = this.panel.search; - if (!this.panel.search) { - return Promise.resolve(); - } - - const params = { - limit: this.panel.limit, - query: this.panel.query, - tag: this.panel.tags, - folderIds: this.panel.folderId, - type: 'dash-db', - }; - - return promiseToDigest(this.$scope)( - backendSrv.search(params).then(result => { - this.groups[2].list = result; - }) - ); - } - - getStarred() { - this.groups[0].show = this.panel.starred; - if (!this.panel.starred) { - return Promise.resolve(); - } - - const params = { limit: this.panel.limit, starred: 'true' }; - return promiseToDigest(this.$scope)( - backendSrv.search(params).then(result => { - this.groups[0].list = result; - }) - ); - } - - starDashboard(dash: any, evt: any) { - this.dashboardSrv.starDashboard(dash.id, dash.isStarred).then((newState: any) => { - dash.isStarred = newState; - }); - - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - } - - getRecentDashboards() { - this.groups[1].show = this.panel.recent; - if (!this.panel.recent) { - return Promise.resolve(); - } - - const dashIds = _.take(impressionSrv.getDashboardOpened(), this.panel.limit); - return promiseToDigest(this.$scope)( - backendSrv.search({ dashboardIds: dashIds, limit: this.panel.limit }).then(result => { - this.groups[1].list = dashIds - .map(orderId => { - return _.find(result, dashboard => { - return dashboard.id === orderId; - }); - }) - .filter(el => { - return el !== undefined; - }); - }) - ); - } - - onFolderChange = (folder: any) => { - this.panel.folderId = folder.id; - this.refresh(); - }; -} - -export { DashListCtrl, DashListCtrl as PanelCtrl }; diff --git a/public/app/plugins/panel/dashlist/module.tsx b/public/app/plugins/panel/dashlist/module.tsx new file mode 100644 index 00000000000..c70e2f2ca51 --- /dev/null +++ b/public/app/plugins/panel/dashlist/module.tsx @@ -0,0 +1,81 @@ +import _ from 'lodash'; +import { PanelModel, PanelPlugin } from '@grafana/data'; +import { DashList } from './DashList'; +import { DashListOptions } from './types'; +import { FolderPicker } from 'app/core/components/Select/FolderPicker'; +import React from 'react'; +import { TagsInput } from '@grafana/ui'; + +export const plugin = new PanelPlugin(DashList) + .setPanelOptions(builder => { + builder + .addBooleanSwitch({ + path: 'showStarred', + name: 'Starred', + defaultValue: true, + }) + .addBooleanSwitch({ + path: 'showRecentlyViewed', + name: 'Recently viewed', + defaultValue: false, + }) + .addBooleanSwitch({ + path: 'showSearch', + name: 'Search', + defaultValue: false, + }) + .addBooleanSwitch({ + path: 'showHeadings', + name: 'Show headings', + defaultValue: true, + }) + .addNumberInput({ + path: 'maxItems', + name: 'Max items', + defaultValue: 10, + }) + .addTextInput({ + path: 'query', + name: 'Query', + defaultValue: '', + }) + .addCustomEditor({ + path: 'folderId', + name: 'Folder', + id: 'folderId', + defaultValue: null, + editor: props => { + return props.onChange(id)} />; + }, + }) + .addCustomEditor({ + id: 'tags', + path: 'tags', + name: 'Tags', + description: '', + defaultValue: [], + editor: props => { + return ; + }, + }); + }) + .setMigrationHandler((panel: PanelModel & Record) => { + const newOptions = { + showStarred: panel.options.showStarred ?? panel.starred, + showRecentlyViewed: panel.options.showRecentlyViewed ?? panel.recent, + showSearch: panel.options.showSearch ?? panel.search, + showHeadings: panel.options.showHeadings ?? panel.headings, + maxItems: panel.options.maxItems ?? panel.limit, + query: panel.options.query ?? panel.query, + folderId: panel.options.folderId ?? panel.folderId, + tags: panel.options.tags ?? panel.tags, + }; + + const previousVersion = parseFloat(panel.pluginVersion || '6.1'); + if (previousVersion < 6.3) { + const oldProps = ['starred', 'recent', 'search', 'headings', 'limit', 'query', 'folderId']; + oldProps.forEach(prop => delete panel[prop]); + } + + return newOptions; + }); diff --git a/public/app/plugins/panel/dashlist/styles.ts b/public/app/plugins/panel/dashlist/styles.ts new file mode 100644 index 00000000000..4b80127cd90 --- /dev/null +++ b/public/app/plugins/panel/dashlist/styles.ts @@ -0,0 +1,57 @@ +import { css } from 'emotion'; + +import { GrafanaTheme } from '@grafana/data'; +import { styleMixins, stylesFactory } from '@grafana/ui'; + +export const getStyles = stylesFactory((theme: GrafanaTheme) => ({ + dashlistSectionHeader: css` + margin-bottom: ${theme.spacing.d}; + color: ${theme.colors.textWeak}; + `, + + dashlistSection: css` + margin-bottom: ${theme.spacing.d}; + padding-top: 3px; + `, + + dashlistLink: css` + ${styleMixins.listItem(theme)} + display: flex; + cursor: pointer; + margin: 3px; + padding: 7px; + `, + + dashlistStar: css` + display: flex; + align-items: center; + color: ${theme.colors.textWeak}; + cursor: pointer; + z-index: 1; + `, + + dashlistFolder: css` + color: ${theme.colors.textWeak}; + font-size: ${theme.typography.size.xs}; + `, + + dashlistTitle: css` + &::after { + position: absolute; + content: ''; + left: 0; + top: 0; + bottom: 0; + right: 0; + } + `, + + dashlistLinkBody: css` + flex-grow: 1; + `, + + dashlistItem: css` + position: relative; + list-style: none; + `, +})); diff --git a/public/app/plugins/panel/dashlist/types.ts b/public/app/plugins/panel/dashlist/types.ts new file mode 100644 index 00000000000..7456b335869 --- /dev/null +++ b/public/app/plugins/panel/dashlist/types.ts @@ -0,0 +1,10 @@ +export interface DashListOptions { + showStarred: boolean; + showRecentlyViewed: boolean; + showSearch: boolean; + showHeadings: boolean; + maxItems: number; + query: string; + folderId: number; + tags: string[]; +}