mirror of https://github.com/grafana/grafana
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 * tidiespull/66361/head v0.0.0-cloud
parent
52f39e6fa0
commit
5df33c0dc1
@ -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<BrowseDashboardsPageRouteParams> {} |
||||
|
||||
// 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 ( |
||||
<Page navId="dashboards/browse" pageNav={navModel}> |
||||
<Page.Contents> |
||||
<BrowseActions /> |
||||
|
||||
{folderDTO && <pre>{JSON.stringify(folderDTO, null, 2)}</pre>} |
||||
|
||||
{searchState.query ? <SearchView searchState={searchState} /> : <BrowseView folderUID={folderUID} />} |
||||
</Page.Contents> |
||||
</Page> |
||||
); |
||||
}); |
||||
|
||||
BrowseDashboardsPage.displayName = 'BrowseDashboardsPage'; |
||||
@ -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<RequestOptions> {
|
||||
// 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<FolderDTO, string>({ |
||||
query: (folderUID) => `/folders/${folderUID}`, |
||||
}), |
||||
}), |
||||
}); |
||||
|
||||
export const { useGetFolderQuery } = browseDashboardsAPI; |
||||
export { skipToken } from '@reduxjs/toolkit/query/react'; |
||||
@ -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 ( |
||||
<div> |
||||
<Input placeholder="Search box" /> |
||||
|
||||
<br /> |
||||
|
||||
<ActionRow |
||||
includePanels={false} |
||||
state={fakeState} |
||||
getTagOptions={() => Promise.resolve([])} |
||||
getSortOptions={() => Promise.resolve([])} |
||||
onLayoutChange={() => {}} |
||||
onSortChange={() => {}} |
||||
onStarredFilterChange={() => {}} |
||||
onTagFilterChange={() => {}} |
||||
onDatasourceChange={() => {}} |
||||
onPanelTypeChange={() => {}} |
||||
onSetIncludePanels={() => {}} |
||||
/> |
||||
</div> |
||||
); |
||||
} |
||||
@ -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<string, DashboardViewItem[] | undefined>; |
||||
|
||||
interface BrowseViewProps { |
||||
folderUID: string | undefined; |
||||
} |
||||
|
||||
export function BrowseView({ folderUID }: BrowseViewProps) { |
||||
const [nestedData, setNestedData] = useState<NestedData>({}); |
||||
|
||||
// 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 ( |
||||
<div> |
||||
<p>Browse view</p> |
||||
|
||||
<ul style={{ marginLeft: 16 }}> |
||||
{items.map((item) => { |
||||
return ( |
||||
<li key={item.uid}> |
||||
<BrowseItem item={item} nestedData={nestedData} onFolderClick={handleNodeClick} /> |
||||
</li> |
||||
); |
||||
})} |
||||
</ul> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
function BrowseItem({ |
||||
item, |
||||
nestedData, |
||||
onFolderClick, |
||||
}: { |
||||
item: DashboardViewItem; |
||||
nestedData: NestedData; |
||||
onFolderClick: (uid: string) => void; |
||||
}) { |
||||
const childItems = nestedData[item.uid]; |
||||
|
||||
return ( |
||||
<> |
||||
<div> |
||||
{item.kind === 'folder' ? ( |
||||
<IconButton onClick={() => onFolderClick(item.uid)} name={childItems ? 'angle-down' : 'angle-right'} /> |
||||
) : ( |
||||
<span style={{ paddingRight: 20 }} /> |
||||
)} |
||||
<Icon name={item.kind === 'folder' ? (childItems ? 'folder-open' : 'folder') : 'apps'} />{' '} |
||||
<Link href={item.kind === 'folder' ? `/nested-dashboards/f/${item.uid}` : `/d/${item.uid}`}>{item.title}</Link> |
||||
</div> |
||||
|
||||
{childItems && ( |
||||
<ul style={{ marginLeft: 16 }}> |
||||
{childItems.length === 0 && ( |
||||
<li> |
||||
<em>Empty folder</em> |
||||
</li> |
||||
)} |
||||
{childItems.map((childItem) => { |
||||
return ( |
||||
<li key={childItem.uid}> |
||||
<BrowseItem item={childItem} nestedData={nestedData} onFolderClick={onFolderClick} />{' '} |
||||
</li> |
||||
); |
||||
})} |
||||
</ul> |
||||
)} |
||||
</> |
||||
); |
||||
} |
||||
@ -0,0 +1,17 @@ |
||||
import React from 'react'; |
||||
|
||||
import { parseRouteParams } from 'app/features/search/utils'; |
||||
|
||||
interface SearchViewProps { |
||||
searchState: ReturnType<typeof parseRouteParams>; |
||||
} |
||||
|
||||
export function SearchView({ searchState }: SearchViewProps) { |
||||
return ( |
||||
<div> |
||||
<p>SearchView</p> |
||||
|
||||
<pre>{JSON.stringify(searchState, null, 2)}</pre> |
||||
</div> |
||||
); |
||||
} |
||||
Loading…
Reference in new issue