From 5df33c0dc1a673e1a9ed6128b7cf4b4649371e5f Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 12 Apr 2023 10:44:01 +0100 Subject: [PATCH] NestedFolders: New Browse Dashboards views (#66003) * scaffold new browse routes * a part of rtk query * load nested data * . * link nested dashboards items * add comment about bad code, update codeowners * tidies --- .github/CODEOWNERS | 1 + public/app/core/reducers/root.ts | 2 + .../BrowseDashboardsPage.tsx | 47 ++++++++ .../api/browseDashboardsAPI.ts | 40 +++++++ .../components/BrowseActions.tsx | 39 +++++++ .../components/BrowseView.tsx | 101 ++++++++++++++++++ .../components/SearchView.tsx | 17 +++ .../search/components/DashboardListPage.tsx | 1 + public/app/features/search/service/folders.ts | 10 +- public/app/routes/routes.tsx | 25 ++++- public/app/store/configureStore.ts | 4 +- 11 files changed, 282 insertions(+), 5 deletions(-) create mode 100644 public/app/features/browse-dashboards/BrowseDashboardsPage.tsx create mode 100644 public/app/features/browse-dashboards/api/browseDashboardsAPI.ts create mode 100644 public/app/features/browse-dashboards/components/BrowseActions.tsx create mode 100644 public/app/features/browse-dashboards/components/BrowseView.tsx create mode 100644 public/app/features/browse-dashboards/components/SearchView.tsx diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 456d9e4f8f4..fe7ad32d3ab 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -377,6 +377,7 @@ lerna.json @grafana/frontend-ops /public/app/features/query/ @grafana/dashboards-squad /public/app/features/sandbox/ @grafana/grafana-frontend-platform /public/app/features/scenes/ @grafana/dashboards-squad +/public/app/features/browse-dashboards/ @grafana/grafana-frontend-platform /public/app/features/search/ @grafana/grafana-frontend-platform /public/app/features/serviceaccounts/ @grafana/grafana-authnz-team /public/app/features/storage/ @grafana/grafana-app-platform-squad diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index 07e871a703e..9d0f89de4f9 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -4,6 +4,7 @@ import sharedReducers from 'app/core/reducers'; import ldapReducers from 'app/features/admin/state/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import apiKeysReducers from 'app/features/api-keys/state/reducers'; +import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { publicDashboardApi } from 'app/features/dashboard/api/publicDashboardApi'; import panelEditorReducers from 'app/features/dashboard/components/PanelEditor/state/reducers'; import dashboardReducers from 'app/features/dashboard/state/reducers'; @@ -48,6 +49,7 @@ const rootReducers = { plugins: pluginsReducer, [alertingApi.reducerPath]: alertingApi.reducer, [publicDashboardApi.reducerPath]: publicDashboardApi.reducer, + [browseDashboardsAPI.reducerPath]: browseDashboardsAPI.reducer, }; const addedReducers = {}; diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx new file mode 100644 index 00000000000..114396465de --- /dev/null +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -0,0 +1,47 @@ +import React, { memo, useMemo } from 'react'; + +import { locationSearchToObject } from '@grafana/runtime'; +import { Page } from 'app/core/components/Page/Page'; +import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; + +import { buildNavModel } from '../folders/state/navModel'; +import { parseRouteParams } from '../search/utils'; + +import { skipToken, useGetFolderQuery } from './api/browseDashboardsAPI'; +import { BrowseActions } from './components/BrowseActions'; +import { BrowseView } from './components/BrowseView'; +import { SearchView } from './components/SearchView'; + +export interface BrowseDashboardsPageRouteParams { + uid?: string; + slug?: string; +} + +interface Props extends GrafanaRouteComponentProps {} + +// New Browse/Manage/Search Dashboards views for nested folders + +export const BrowseDashboardsPage = memo(({ match, location }: Props) => { + const { uid: folderUID } = match.params; + + const searchState = useMemo(() => { + return parseRouteParams(locationSearchToObject(location.search)); + }, [location.search]); + + const { data: folderDTO } = useGetFolderQuery(folderUID ?? skipToken); + const navModel = useMemo(() => (folderDTO ? buildNavModel(folderDTO) : undefined), [folderDTO]); + + return ( + + + + + {folderDTO &&
{JSON.stringify(folderDTO, null, 2)}
} + + {searchState.query ? : } +
+
+ ); +}); + +BrowseDashboardsPage.displayName = 'BrowseDashboardsPage'; diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts new file mode 100644 index 00000000000..5f4fbe6f68e --- /dev/null +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -0,0 +1,40 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; + +import { FolderDTO } from 'app/types'; + +// interface RequestOptions extends BackendSrvRequest { +// manageError?: (err: unknown) => { error: unknown }; +// showErrorAlert?: boolean; +// } + +// function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn { +// async function backendSrvBaseQuery(requestOptions: RequestOptions) { +// try { +// const { data: responseData, ...meta } = await lastValueFrom( +// getBackendSrv().fetch({ +// ...requestOptions, +// url: baseURL + requestOptions.url, +// showErrorAlert: requestOptions.showErrorAlert, +// }) +// ); +// return { data: responseData, meta }; +// } catch (error) { +// return requestOptions.manageError ? requestOptions.manageError(error) : { error }; +// } +// } + +// return backendSrvBaseQuery; +// } + +export const browseDashboardsAPI = createApi({ + reducerPath: 'browse-dashboards', + baseQuery: fetchBaseQuery({ baseUrl: '/api' }), + endpoints: (builder) => ({ + getFolder: builder.query({ + query: (folderUID) => `/folders/${folderUID}`, + }), + }), +}); + +export const { useGetFolderQuery } = browseDashboardsAPI; +export { skipToken } from '@reduxjs/toolkit/query/react'; diff --git a/public/app/features/browse-dashboards/components/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions.tsx new file mode 100644 index 00000000000..cf05c7d856e --- /dev/null +++ b/public/app/features/browse-dashboards/components/BrowseActions.tsx @@ -0,0 +1,39 @@ +import React, { useMemo } from 'react'; + +import { Input } from '@grafana/ui'; +import { ActionRow } from 'app/features/search/page/components/ActionRow'; +import { SearchLayout } from 'app/features/search/types'; + +export function BrowseActions() { + const fakeState = useMemo(() => { + return { + query: '', + tag: [], + starred: false, + layout: SearchLayout.Folders, + eventTrackingNamespace: 'manage_dashboards' as const, + }; + }, []); + + return ( +
+ + +
+ + Promise.resolve([])} + getSortOptions={() => Promise.resolve([])} + onLayoutChange={() => {}} + onSortChange={() => {}} + onStarredFilterChange={() => {}} + onTagFilterChange={() => {}} + onDatasourceChange={() => {}} + onPanelTypeChange={() => {}} + onSetIncludePanels={() => {}} + /> +
+ ); +} diff --git a/public/app/features/browse-dashboards/components/BrowseView.tsx b/public/app/features/browse-dashboards/components/BrowseView.tsx new file mode 100644 index 00000000000..9ea9b49b30f --- /dev/null +++ b/public/app/features/browse-dashboards/components/BrowseView.tsx @@ -0,0 +1,101 @@ +import React, { useCallback, useEffect, useState } from 'react'; + +import { Icon, IconButton, Link } from '@grafana/ui'; +import { getFolderChildren } from 'app/features/search/service/folders'; +import { DashboardViewItem } from 'app/features/search/types'; + +type NestedData = Record; + +interface BrowseViewProps { + folderUID: string | undefined; +} + +export function BrowseView({ folderUID }: BrowseViewProps) { + const [nestedData, setNestedData] = useState({}); + + // Note: entire implementation of this component must be replaced. + // This is just to show proof of concept for fetching and showing the data + + useEffect(() => { + const folderKey = folderUID ?? '$$root'; + + getFolderChildren(folderUID, undefined, true).then((children) => { + setNestedData((v) => ({ ...v, [folderKey]: children })); + }); + }, [folderUID]); + + const items = nestedData[folderUID ?? '$$root'] ?? []; + + const handleNodeClick = useCallback( + (uid: string) => { + if (nestedData[uid]) { + setNestedData((v) => ({ ...v, [uid]: undefined })); + return; + } + + getFolderChildren(uid).then((children) => { + setNestedData((v) => ({ ...v, [uid]: children })); + }); + }, + [nestedData] + ); + + return ( +
+

Browse view

+ +
    + {items.map((item) => { + return ( +
  • + +
  • + ); + })} +
+
+ ); +} + +function BrowseItem({ + item, + nestedData, + onFolderClick, +}: { + item: DashboardViewItem; + nestedData: NestedData; + onFolderClick: (uid: string) => void; +}) { + const childItems = nestedData[item.uid]; + + return ( + <> +
+ {item.kind === 'folder' ? ( + onFolderClick(item.uid)} name={childItems ? 'angle-down' : 'angle-right'} /> + ) : ( + + )} + {' '} + {item.title} +
+ + {childItems && ( +
    + {childItems.length === 0 && ( +
  • + Empty folder +
  • + )} + {childItems.map((childItem) => { + return ( +
  • + {' '} +
  • + ); + })} +
+ )} + + ); +} diff --git a/public/app/features/browse-dashboards/components/SearchView.tsx b/public/app/features/browse-dashboards/components/SearchView.tsx new file mode 100644 index 00000000000..b789d99a6e9 --- /dev/null +++ b/public/app/features/browse-dashboards/components/SearchView.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import { parseRouteParams } from 'app/features/search/utils'; + +interface SearchViewProps { + searchState: ReturnType; +} + +export function SearchView({ searchState }: SearchViewProps) { + return ( +
+

SearchView

+ +
{JSON.stringify(searchState, null, 2)}
+
+ ); +} diff --git a/public/app/features/search/components/DashboardListPage.tsx b/public/app/features/search/components/DashboardListPage.tsx index 846b3351da2..8c21db76909 100644 --- a/public/app/features/search/components/DashboardListPage.tsx +++ b/public/app/features/search/components/DashboardListPage.tsx @@ -23,6 +23,7 @@ export const DashboardListPage = memo(({ match, location }: Props) => { const { loading, value } = useAsync<() => Promise<{ folder?: FolderDTO; pageNav?: NavModelItem }>>(() => { const uid = match.params.uid; const url = location.pathname; + if (!uid || !url.startsWith('/dashboards')) { return Promise.resolve({}); } diff --git a/public/app/features/search/service/folders.ts b/public/app/features/search/service/folders.ts index c71609a21de..f6b3c39eb5d 100644 --- a/public/app/features/search/service/folders.ts +++ b/public/app/features/search/service/folders.ts @@ -7,13 +7,17 @@ import { getGrafanaSearcher } from './searcher'; import { NestedFolderDTO } from './types'; import { queryResultToViewItem } from './utils'; -export async function getFolderChildren(parentUid?: string, parentTitle?: string): Promise { +export async function getFolderChildren( + parentUid?: string, + parentTitle?: string, + dashboardsAtRoot = false +): Promise { if (!config.featureToggles.nestedFolders) { console.error('getFolderChildren requires nestedFolders feature toggle'); return []; } - if (!parentUid) { + if (!dashboardsAtRoot && !parentUid) { // We don't show dashboards at root in folder view yet - they're shown under a dummy 'general' // folder that FolderView adds in const folders = await getChildFolders(); @@ -24,7 +28,7 @@ export async function getFolderChildren(parentUid?: string, parentTitle?: string const dashboardsResults = await searcher.search({ kind: ['dashboard'], query: '*', - location: parentUid, + location: parentUid ?? 'general', limit: 1000, }); diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index f9b3e6ed981..a46873526f4 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Redirect } from 'react-router-dom'; +import { isTruthy } from '@grafana/data'; import { NavLandingPage } from 'app/core/components/AppChrome/NavLandingPage'; import { ErrorPage } from 'app/core/components/ErrorPage/ErrorPage'; import { LoginPage } from 'app/core/components/Login/LoginPage'; @@ -168,6 +169,9 @@ export function getAppRoutes(): RouteDescriptor[] { ) ), }, + + ...(config.featureToggles.nestedFolders ? getNestedFoldersRoutes() : []), + { path: '/dashboards', component: SafeDynamicImport( @@ -513,7 +517,7 @@ export function getAppRoutes(): RouteDescriptor[] { }, // TODO[Router] // ...playlistRoutes, - ]; + ].filter(isTruthy); } export function getSupportBundleRoutes(cfg = config): RouteDescriptor[] { @@ -565,3 +569,22 @@ export function getDynamicDashboardRoutes(cfg = config): RouteDescriptor[] { }, ]; } + +function getNestedFoldersRoutes(): RouteDescriptor[] { + return [ + { + path: '/nested-dashboards', + component: SafeDynamicImport(() => import('app/features/browse-dashboards/BrowseDashboardsPage')), + }, + + { + path: '/nested-dashboards/f/:uid', + component: SafeDynamicImport(() => import('app/features/browse-dashboards/BrowseDashboardsPage')), + }, + + { + path: '/nested-dashboards/f/:uid/:slug', + component: SafeDynamicImport(() => import('app/features/browse-dashboards/BrowseDashboardsPage')), + }, + ]; +} diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 30e2c0a7164..ed36b9340f1 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -1,5 +1,6 @@ import { configureStore as reduxConfigureStore } from '@reduxjs/toolkit'; +import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { publicDashboardApi } from 'app/features/dashboard/api/publicDashboardApi'; import { StoreState } from 'app/types/store'; @@ -22,7 +23,8 @@ export function configureStore(initialState?: Partial) { middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: true, serializableCheck: false, immutableCheck: false }).concat( alertingApi.middleware, - publicDashboardApi.middleware + publicDashboardApi.middleware, + browseDashboardsAPI.middleware ), devTools: process.env.NODE_ENV !== 'production', preloadedState: {