mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 15:45:43 -06:00
NestedFolders: Show nested folders in Browse folder view (#63746)
* dirty dirty code for showing nested folders in folder view refactor to NestedFolderItem Update dashboard grid view to new types update tests REBASE OUT OF THIS BRANCH - joshhunt/star-by-uid merged into this Squashed commit of the following: commitd0f046ccd3
Author: joshhunt <josh@trtr.co> Date: Wed Feb 8 18:35:56 2023 +0000 undo async commitabe2777a1f
Author: joshhunt <josh@trtr.co> Date: Wed Feb 8 18:34:11 2023 +0000 Dashboards: Star dashboards by UID add type for dashboard search dto clean DashboardSearchItem type simplify DashboardSearchHit type remove unused properties from DashboardSearchHit make uid non-optional rename + move NestedFolderItem type to DashboardViewItem clean up * wip * fix checkbox selection of nested folders * show folder's parent correctly * Add dashboard result kind * don't render folder empty view in SearchView * call nested folders api only if feature flag enabled * remove unused import * un-rename variable to reduce PR diff * fix typo in comment * fix order of pseudoFolders * Fix General folder not showing in browse * rename folder view tests --------- Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
parent
da97139489
commit
d13488a435
@ -255,6 +255,7 @@ function mockDashboardSearchItem(searchItem: Partial<DashboardSearchItem>) {
|
||||
uri: '',
|
||||
items: [],
|
||||
tags: [],
|
||||
slug: '',
|
||||
isStarred: false,
|
||||
...searchItem,
|
||||
};
|
||||
|
@ -3,7 +3,9 @@ import React, { useCallback } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Card, Icon, IconName, TagList, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { SEARCH_ITEM_HEIGHT } from '../constants';
|
||||
import { getIconForKind } from '../service/utils';
|
||||
@ -55,6 +57,16 @@ export const SearchItem = ({ item, isSelected, editable, onToggleChecked, onTagS
|
||||
[item, onToggleChecked]
|
||||
);
|
||||
|
||||
const description = config.featureToggles.nestedFolders ? (
|
||||
<>
|
||||
<Icon name={getIconForKind(item.kind)} aria-hidden /> {kindName(item.kind)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name={getIconForKind(item.parentKind ?? 'folder')} aria-hidden /> {item.parentTitle || 'General'}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
data-testid={selectors.dashboardItem(item.title)}
|
||||
@ -64,6 +76,7 @@ export const SearchItem = ({ item, isSelected, editable, onToggleChecked, onTagS
|
||||
onClick={onClickItem}
|
||||
>
|
||||
<Card.Heading>{item.title}</Card.Heading>
|
||||
|
||||
<Card.Figure align={'center'} className={styles.checkbox}>
|
||||
<SearchCheckbox
|
||||
aria-label="Select dashboard"
|
||||
@ -72,11 +85,9 @@ export const SearchItem = ({ item, isSelected, editable, onToggleChecked, onTagS
|
||||
onClick={handleCheckboxClick}
|
||||
/>
|
||||
</Card.Figure>
|
||||
|
||||
<Card.Meta separator={''}>
|
||||
<span className={styles.metaContainer}>
|
||||
<Icon name={getIconForKind(item.parentKind ?? 'folder')} aria-hidden />
|
||||
{item.parentTitle || 'General'}
|
||||
</span>
|
||||
<span className={styles.metaContainer}>{description}</span>
|
||||
|
||||
{item.sortMetaName && (
|
||||
<span className={styles.metaContainer}>
|
||||
@ -92,6 +103,17 @@ export const SearchItem = ({ item, isSelected, editable, onToggleChecked, onTagS
|
||||
);
|
||||
};
|
||||
|
||||
function kindName(kind: DashboardViewItem['kind']) {
|
||||
switch (kind) {
|
||||
case 'folder':
|
||||
return t('search.result-kind.folder', 'Folder');
|
||||
case 'dashboard':
|
||||
return t('search.result-kind.dashboard', 'Dashboard');
|
||||
case 'panel':
|
||||
return t('search.result-kind.panel', 'Panel');
|
||||
}
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css`
|
||||
|
@ -15,7 +15,7 @@ describe('FolderSection', () => {
|
||||
const mockSelectionToggle = jest.fn();
|
||||
const mockSelection = jest.fn();
|
||||
const mockSection: DashboardViewItem = {
|
||||
kind: 'folder' as const,
|
||||
kind: 'folder',
|
||||
uid: 'my-folder',
|
||||
title: 'My folder',
|
||||
};
|
||||
|
@ -5,12 +5,15 @@ import { useAsync, useLocalStorage } from 'react-use';
|
||||
import { GrafanaTheme2, toIconName } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Card, Checkbox, CollapsableSection, Icon, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { getSectionStorageKey } from 'app/features/search/utils';
|
||||
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
|
||||
|
||||
import { SearchItem } from '../..';
|
||||
import { getGrafanaSearcher, SearchQuery } from '../../service';
|
||||
import { GENERAL_FOLDER_UID } from '../../constants';
|
||||
import { getGrafanaSearcher } from '../../service';
|
||||
import { getFolderChildren } from '../../service/folders';
|
||||
import { queryResultToViewItem } from '../../service/utils';
|
||||
import { DashboardViewItem } from '../../types';
|
||||
import { SelectionChecker, SelectionToggle } from '../selection';
|
||||
@ -25,6 +28,27 @@ interface SectionHeaderProps {
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
async function getChildren(section: DashboardViewItem, tags: string[] | undefined): Promise<DashboardViewItem[]> {
|
||||
if (config.featureToggles.nestedFolders) {
|
||||
return getFolderChildren(section.uid, section.title);
|
||||
}
|
||||
|
||||
const query = section.itemsUIDs
|
||||
? {
|
||||
uid: section.itemsUIDs,
|
||||
}
|
||||
: {
|
||||
query: '*',
|
||||
kind: ['dashboard'],
|
||||
location: section.uid,
|
||||
sort: 'name_sort',
|
||||
limit: 1000, // this component does not have infinite scroll, so we need to load everything upfront
|
||||
};
|
||||
|
||||
const raw = await getGrafanaSearcher().search({ ...query, tags });
|
||||
return raw.view.map((v) => queryResultToViewItem(v, raw.view));
|
||||
}
|
||||
|
||||
export const FolderSection = ({
|
||||
section,
|
||||
selectionToggle,
|
||||
@ -42,22 +66,10 @@ export const FolderSection = ({
|
||||
if (!sectionExpanded && !renderStandaloneBody) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let query: SearchQuery = {
|
||||
query: '*',
|
||||
kind: ['dashboard'],
|
||||
location: section.uid,
|
||||
sort: 'name_sort',
|
||||
limit: 1000, // this component does not have infinate scroll, so we need to load everything upfront
|
||||
};
|
||||
if (section.itemsUIDs) {
|
||||
query = {
|
||||
uid: section.itemsUIDs, // array of UIDs
|
||||
};
|
||||
}
|
||||
|
||||
const raw = await getGrafanaSearcher().search({ ...query, tags });
|
||||
const items = raw.view.map((v) => queryResultToViewItem(v, raw.view));
|
||||
return items;
|
||||
const childItems = getChildren(section, tags);
|
||||
|
||||
return childItems;
|
||||
}, [sectionExpanded, tags]);
|
||||
|
||||
const onSectionExpand = () => {
|
||||
@ -72,8 +84,8 @@ export const FolderSection = ({
|
||||
selectionToggle(section.kind, section.uid);
|
||||
const sub = results.value ?? [];
|
||||
for (const item of sub) {
|
||||
if (selection('dashboard', item.uid!) !== checked) {
|
||||
selectionToggle('dashboard', item.uid!);
|
||||
if (selection(item.kind, item.uid!) !== checked) {
|
||||
selectionToggle(item.kind, item.uid!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -104,14 +116,10 @@ export const FolderSection = ({
|
||||
key={item.uid}
|
||||
item={item}
|
||||
onTagSelected={onTagSelected}
|
||||
onToggleChecked={(item) => {
|
||||
if (selectionToggle) {
|
||||
selectionToggle('dashboard', item.uid!);
|
||||
}
|
||||
}}
|
||||
onToggleChecked={(item) => selectionToggle?.(item.kind, item.uid)}
|
||||
editable={Boolean(selection != null)}
|
||||
onClickItem={onClickItem}
|
||||
isSelected={selectionToggle && selection?.(item.kind, item.uid)}
|
||||
isSelected={selection?.(item.kind, item.uid)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -153,7 +161,7 @@ export const FolderSection = ({
|
||||
|
||||
<div className={styles.text}>
|
||||
<span id={labelId}>{section.title}</span>
|
||||
{section.url && section.uid !== 'general' && (
|
||||
{section.url && section.uid !== GENERAL_FOLDER_UID && (
|
||||
<a href={section.url} className={styles.link}>
|
||||
<span className={styles.separator}>|</span> <Icon name="folder-upload" />{' '}
|
||||
{t('search.folder-view.go-to-folder', 'Go to folder')}
|
||||
@ -170,6 +178,7 @@ export const FolderSection = ({
|
||||
|
||||
const getSectionHeaderStyles = (theme: GrafanaTheme2, editable: boolean) => {
|
||||
const sm = theme.spacing(1);
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
align-items: center;
|
||||
|
@ -9,7 +9,7 @@ import impressionSrv from '../../../../core/services/impression_srv';
|
||||
import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from '../../service';
|
||||
import { DashboardSearchItemType } from '../../types';
|
||||
|
||||
import { FolderView } from './FolderView';
|
||||
import { RootFolderView } from './RootFolderView';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
@ -18,7 +18,7 @@ jest.mock('@grafana/runtime', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('FolderView', () => {
|
||||
describe('RootFolderView', () => {
|
||||
let grafanaSearcherSpy: jest.SpyInstance;
|
||||
const mockOnTagSelected = jest.fn();
|
||||
const mockSelectionToggle = jest.fn();
|
||||
@ -69,7 +69,11 @@ describe('FolderView', () => {
|
||||
grafanaSearcherSpy.mockImplementationOnce(() => promise);
|
||||
|
||||
render(
|
||||
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||
<RootFolderView
|
||||
onTagSelected={mockOnTagSelected}
|
||||
selection={mockSelection}
|
||||
selectionToggle={mockSelectionToggle}
|
||||
/>
|
||||
);
|
||||
expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
|
||||
|
||||
@ -84,7 +88,11 @@ describe('FolderView', () => {
|
||||
it('does not show the starred items if not signed in', async () => {
|
||||
contextSrv.isSignedIn = false;
|
||||
render(
|
||||
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||
<RootFolderView
|
||||
onTagSelected={mockOnTagSelected}
|
||||
selection={mockSelection}
|
||||
selectionToggle={mockSelectionToggle}
|
||||
/>
|
||||
);
|
||||
expect((await screen.findAllByTestId(selectors.components.Search.sectionV2))[0]).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Starred' })).not.toBeInTheDocument();
|
||||
@ -93,7 +101,11 @@ describe('FolderView', () => {
|
||||
it('shows the starred items if signed in', async () => {
|
||||
contextSrv.isSignedIn = true;
|
||||
render(
|
||||
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||
<RootFolderView
|
||||
onTagSelected={mockOnTagSelected}
|
||||
selection={mockSelection}
|
||||
selectionToggle={mockSelectionToggle}
|
||||
/>
|
||||
);
|
||||
expect(await screen.findByRole('button', { name: 'Starred' })).toBeInTheDocument();
|
||||
});
|
||||
@ -101,7 +113,11 @@ describe('FolderView', () => {
|
||||
it('does not show the recent items if no dashboards have been opened recently', async () => {
|
||||
jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue([]);
|
||||
render(
|
||||
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||
<RootFolderView
|
||||
onTagSelected={mockOnTagSelected}
|
||||
selection={mockSelection}
|
||||
selectionToggle={mockSelectionToggle}
|
||||
/>
|
||||
);
|
||||
expect((await screen.findAllByTestId(selectors.components.Search.sectionV2))[0]).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Recent' })).not.toBeInTheDocument();
|
||||
@ -110,14 +126,22 @@ describe('FolderView', () => {
|
||||
it('shows the recent items if any dashboards have recently been opened', async () => {
|
||||
jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue(['7MeksYbmk']);
|
||||
render(
|
||||
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||
<RootFolderView
|
||||
onTagSelected={mockOnTagSelected}
|
||||
selection={mockSelection}
|
||||
selectionToggle={mockSelectionToggle}
|
||||
/>
|
||||
);
|
||||
expect(await screen.findByRole('button', { name: 'Recent' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the general folder by default', async () => {
|
||||
render(
|
||||
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||
<RootFolderView
|
||||
onTagSelected={mockOnTagSelected}
|
||||
selection={mockSelection}
|
||||
selectionToggle={mockSelectionToggle}
|
||||
/>
|
||||
);
|
||||
expect(await screen.findByRole('button', { name: 'General' })).toBeInTheDocument();
|
||||
});
|
||||
@ -126,7 +150,7 @@ describe('FolderView', () => {
|
||||
it('does not show the starred items even if signed in', async () => {
|
||||
contextSrv.isSignedIn = true;
|
||||
render(
|
||||
<FolderView
|
||||
<RootFolderView
|
||||
hidePseudoFolders
|
||||
onTagSelected={mockOnTagSelected}
|
||||
selection={mockSelection}
|
||||
@ -140,7 +164,7 @@ describe('FolderView', () => {
|
||||
it('does not show the recent items even if recent dashboards have been opened', async () => {
|
||||
jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue(['7MeksYbmk']);
|
||||
render(
|
||||
<FolderView
|
||||
<RootFolderView
|
||||
hidePseudoFolders
|
||||
onTagSelected={mockOnTagSelected}
|
||||
selection={mockSelection}
|
||||
@ -156,7 +180,11 @@ describe('FolderView', () => {
|
||||
// reject with a specific Error object
|
||||
grafanaSearcherSpy.mockRejectedValueOnce(new Error('Uh oh spagghettios!'));
|
||||
render(
|
||||
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||
<RootFolderView
|
||||
onTagSelected={mockOnTagSelected}
|
||||
selection={mockSelection}
|
||||
selectionToggle={mockSelectionToggle}
|
||||
/>
|
||||
);
|
||||
expect(await screen.findByRole('alert', { name: 'Uh oh spagghettios!' })).toBeInTheDocument();
|
||||
});
|
||||
@ -165,7 +193,11 @@ describe('FolderView', () => {
|
||||
// reject with nothing
|
||||
grafanaSearcherSpy.mockRejectedValueOnce(null);
|
||||
render(
|
||||
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
||||
<RootFolderView
|
||||
onTagSelected={mockOnTagSelected}
|
||||
selection={mockSelection}
|
||||
selectionToggle={mockSelectionToggle}
|
||||
/>
|
||||
);
|
||||
expect(await screen.findByRole('alert', { name: 'Something went wrong' })).toBeInTheDocument();
|
||||
});
|
@ -6,23 +6,39 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { Alert, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import config from 'app/core/config';
|
||||
|
||||
import { contextSrv } from '../../../../core/services/context_srv';
|
||||
import impressionSrv from '../../../../core/services/impression_srv';
|
||||
import { GENERAL_FOLDER_UID } from '../../constants';
|
||||
import { getGrafanaSearcher } from '../../service';
|
||||
import { getFolderChildren } from '../../service/folders';
|
||||
import { queryResultToViewItem } from '../../service/utils';
|
||||
import { DashboardViewItem } from '../../types';
|
||||
import { SearchResultsProps } from '../components/SearchResultsTable';
|
||||
|
||||
import { FolderSection } from './FolderSection';
|
||||
import { SearchResultsProps } from './SearchResultsTable';
|
||||
|
||||
async function getChildren() {
|
||||
if (config.featureToggles.nestedFolders) {
|
||||
return getFolderChildren();
|
||||
}
|
||||
|
||||
const searcher = getGrafanaSearcher();
|
||||
const results = await searcher.search({
|
||||
query: '*',
|
||||
kind: ['folder'],
|
||||
sort: searcher.getFolderViewSort(),
|
||||
limit: 1000,
|
||||
});
|
||||
|
||||
return results.view.map((v) => queryResultToViewItem(v, results.view));
|
||||
}
|
||||
|
||||
type Props = Pick<SearchResultsProps, 'selection' | 'selectionToggle' | 'onTagSelected' | 'onClickItem'> & {
|
||||
tags?: string[];
|
||||
hidePseudoFolders?: boolean;
|
||||
};
|
||||
|
||||
export const FolderView = ({
|
||||
export const RootFolderView = ({
|
||||
selection,
|
||||
selectionToggle,
|
||||
onTagSelected,
|
||||
@ -33,33 +49,22 @@ export const FolderView = ({
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const results = useAsync(async () => {
|
||||
const folders: DashboardViewItem[] = [];
|
||||
const folders = await getChildren();
|
||||
|
||||
folders.unshift({ title: 'General', url: '/dashboards', kind: 'folder', uid: GENERAL_FOLDER_UID });
|
||||
|
||||
if (!hidePseudoFolders) {
|
||||
const itemsUIDs = await impressionSrv.getDashboardOpened();
|
||||
if (itemsUIDs.length) {
|
||||
folders.unshift({ title: 'Recent', icon: 'clock-nine', kind: 'folder', uid: '__recent', itemsUIDs });
|
||||
}
|
||||
|
||||
if (contextSrv.isSignedIn) {
|
||||
const stars = await getBackendSrv().get('api/user/stars');
|
||||
if (stars.length > 0) {
|
||||
folders.push({ title: 'Starred', icon: 'star', kind: 'folder', uid: '__starred', itemsUIDs: stars });
|
||||
folders.unshift({ title: 'Starred', icon: 'star', kind: 'folder', uid: '__starred', itemsUIDs: stars });
|
||||
}
|
||||
}
|
||||
|
||||
const itemsUIDs = await impressionSrv.getDashboardOpened();
|
||||
if (itemsUIDs.length) {
|
||||
folders.push({ title: 'Recent', icon: 'clock-nine', kind: 'folder', uid: '__recent', itemsUIDs });
|
||||
}
|
||||
}
|
||||
|
||||
folders.push({ title: 'General', url: '/dashboards', kind: 'folder', uid: GENERAL_FOLDER_UID });
|
||||
|
||||
const searcher = getGrafanaSearcher();
|
||||
const results = await searcher.search({
|
||||
query: '*',
|
||||
kind: ['folder'],
|
||||
sort: searcher.getFolderViewSort(),
|
||||
limit: 1000,
|
||||
});
|
||||
for (const row of results.view) {
|
||||
folders.push(queryResultToViewItem(row, results.view));
|
||||
}
|
||||
|
||||
return folders;
|
@ -48,6 +48,7 @@ export const SearchResultsCards = React.memo(
|
||||
|
||||
const item = response.view.get(rowIndex);
|
||||
const searchItem = queryResultToViewItem(item, response.view);
|
||||
const isSelected = selectionToggle && selection?.(searchItem.kind, searchItem.uid);
|
||||
|
||||
return (
|
||||
<div style={style} key={item.uid} className={className} role="row">
|
||||
@ -61,7 +62,7 @@ export const SearchResultsCards = React.memo(
|
||||
}}
|
||||
editable={Boolean(selection != null)}
|
||||
onClickItem={onClickItem}
|
||||
isSelected={selectionToggle && selection?.(searchItem.kind, searchItem.uid)}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -5,6 +5,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { useStyles2, Spinner, Button } from '@grafana/ui';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { FolderDTO } from 'app/types';
|
||||
@ -12,13 +13,13 @@ import { FolderDTO } from 'app/types';
|
||||
import { PreviewsSystemRequirements } from '../../components/PreviewsSystemRequirements';
|
||||
import { getGrafanaSearcher } from '../../service';
|
||||
import { getSearchStateManager } from '../../state/SearchStateManager';
|
||||
import { SearchLayout } from '../../types';
|
||||
import { SearchLayout, DashboardViewItem } from '../../types';
|
||||
import { newSearchSelection, updateSearchSelection } from '../selection';
|
||||
|
||||
import { ActionRow, getValidQueryLayout } from './ActionRow';
|
||||
import { FolderSection } from './FolderSection';
|
||||
import { FolderView } from './FolderView';
|
||||
import { ManageActions } from './ManageActions';
|
||||
import { RootFolderView } from './RootFolderView';
|
||||
import { SearchResultsCards } from './SearchResultsCards';
|
||||
import { SearchResultsGrid } from './SearchResultsGrid';
|
||||
import { SearchResultsTable, SearchResultsProps } from './SearchResultsTable';
|
||||
@ -86,11 +87,12 @@ export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardE
|
||||
}
|
||||
|
||||
const selection = showManage ? searchSelection.isSelected : undefined;
|
||||
|
||||
if (layout === SearchLayout.Folders) {
|
||||
if (folderDTO) {
|
||||
return (
|
||||
<FolderSection
|
||||
section={{ uid: folderDTO.uid, kind: 'folder', title: folderDTO.title }}
|
||||
section={sectionForFolderView(folderDTO)}
|
||||
selection={selection}
|
||||
selectionToggle={toggleSelection}
|
||||
onTagSelected={stateManager.onAddTag}
|
||||
@ -102,7 +104,7 @@ export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardE
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FolderView
|
||||
<RootFolderView
|
||||
key={listKey}
|
||||
selection={selection}
|
||||
selectionToggle={toggleSelection}
|
||||
@ -146,7 +148,15 @@ export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardE
|
||||
);
|
||||
};
|
||||
|
||||
if (folderDTO && !state.loading && !state.result?.totalRows && !stateManager.hasSearchFilters()) {
|
||||
if (
|
||||
folderDTO &&
|
||||
// With nested folders, SearchView doesn't know if it's fetched all children
|
||||
// of a folder so don't show empty state here.
|
||||
!config.featureToggles.nestedFolders &&
|
||||
!state.loading &&
|
||||
!state.result?.totalRows &&
|
||||
!stateManager.hasSearchFilters()
|
||||
) {
|
||||
return (
|
||||
<EmptyListCTA
|
||||
title="This folder doesn't have any dashboards yet"
|
||||
@ -215,3 +225,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
margin-top: ${theme.v1.spacing.md};
|
||||
`,
|
||||
});
|
||||
|
||||
function sectionForFolderView(folderDTO: FolderDTO): DashboardViewItem {
|
||||
return { uid: folderDTO.uid, kind: 'folder', title: folderDTO.title };
|
||||
}
|
||||
|
52
public/app/features/search/service/folders.ts
Normal file
52
public/app/features/search/service/folders.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import config from 'app/core/config';
|
||||
|
||||
import { DashboardViewItem } from '../types';
|
||||
|
||||
import { getGrafanaSearcher } from './searcher';
|
||||
import { NestedFolderDTO } from './types';
|
||||
import { queryResultToViewItem } from './utils';
|
||||
|
||||
export async function getFolderChildren(parentUid?: string, parentTitle?: string): Promise<DashboardViewItem[]> {
|
||||
if (!config.featureToggles.nestedFolders) {
|
||||
console.error('getFolderChildren requires nestedFolders feature toggle');
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!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();
|
||||
return folders;
|
||||
}
|
||||
|
||||
const searcher = getGrafanaSearcher();
|
||||
const dashboardsResults = await searcher.search({
|
||||
kind: ['dashboard'],
|
||||
query: '*',
|
||||
location: parentUid,
|
||||
limit: 1000,
|
||||
});
|
||||
|
||||
const dashboardItems = dashboardsResults.view.map((item) => {
|
||||
return queryResultToViewItem(item, dashboardsResults.view);
|
||||
});
|
||||
|
||||
const folders = await getChildFolders(parentUid, parentTitle);
|
||||
|
||||
return [...folders, ...dashboardItems];
|
||||
}
|
||||
|
||||
async function getChildFolders(parentUid?: string, parentTitle?: string): Promise<DashboardViewItem[]> {
|
||||
const backendSrv = getBackendSrv();
|
||||
|
||||
const folders = await backendSrv.get<NestedFolderDTO[]>('/api/folders', { parentUid });
|
||||
|
||||
return folders.map((item) => ({
|
||||
kind: 'folder',
|
||||
uid: item.uid,
|
||||
title: item.title,
|
||||
parentTitle,
|
||||
url: `/dashboards/f/${item.uid}/`,
|
||||
}));
|
||||
}
|
@ -81,3 +81,8 @@ export interface GrafanaSearcher {
|
||||
/** Gets the default sort used for the Folder view */
|
||||
getFolderViewSort: () => string;
|
||||
}
|
||||
|
||||
export interface NestedFolderDTO {
|
||||
uid: string;
|
||||
title: string;
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ export function DashList(props: PanelProps<PanelOptions>) {
|
||||
<ul className={css.gridContainer}>
|
||||
{dashboards.map((dash) => (
|
||||
<li key={dash.uid}>
|
||||
<SearchCard item={{ ...dash, kind: 'folder' }} />
|
||||
<SearchCard item={{ ...dash, kind: 'dashboard' }} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
@ -9,8 +9,7 @@
|
||||
"action": {
|
||||
"change-theme": "",
|
||||
"dark-theme": "",
|
||||
"light-theme": "",
|
||||
"search": ""
|
||||
"light-theme": ""
|
||||
},
|
||||
"search-box": {
|
||||
"placeholder": ""
|
||||
@ -84,7 +83,6 @@
|
||||
},
|
||||
"toolbar": {
|
||||
"add-panel": "Panel hinzufügen",
|
||||
"comments-show": "Dashboard-Kommentare anzeigen",
|
||||
"mark-favorite": "Als Favorit markieren",
|
||||
"open-original": "Original-Dashboard öffnen",
|
||||
"playlist-next": "Zum nächsten Dashboard",
|
||||
@ -292,7 +290,6 @@
|
||||
"title": "Szenen"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Grafana durchsuchen",
|
||||
"placeholderCommandPalette": ""
|
||||
},
|
||||
"search-dashboards": {
|
||||
@ -415,6 +412,11 @@
|
||||
"go-to-folder": "",
|
||||
"select-folder": ""
|
||||
},
|
||||
"result-kind": {
|
||||
"dashboard": "",
|
||||
"folder": "",
|
||||
"panel": ""
|
||||
},
|
||||
"results-table": {
|
||||
"datasource-header": "",
|
||||
"location-header": "",
|
||||
|
@ -9,8 +9,7 @@
|
||||
"action": {
|
||||
"change-theme": "Change theme...",
|
||||
"dark-theme": "Dark",
|
||||
"light-theme": "Light",
|
||||
"search": "Search"
|
||||
"light-theme": "Light"
|
||||
},
|
||||
"search-box": {
|
||||
"placeholder": "Search or jump to..."
|
||||
@ -84,7 +83,6 @@
|
||||
},
|
||||
"toolbar": {
|
||||
"add-panel": "Add panel",
|
||||
"comments-show": "Show dashboard comments",
|
||||
"mark-favorite": "Mark as favorite",
|
||||
"open-original": "Open original dashboard",
|
||||
"playlist-next": "Go to next dashboard",
|
||||
@ -292,7 +290,6 @@
|
||||
"title": "Scenes"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search Grafana",
|
||||
"placeholderCommandPalette": "Search or jump to..."
|
||||
},
|
||||
"search-dashboards": {
|
||||
@ -415,6 +412,11 @@
|
||||
"go-to-folder": "Go to folder",
|
||||
"select-folder": "Select folder"
|
||||
},
|
||||
"result-kind": {
|
||||
"dashboard": "Dashboard",
|
||||
"folder": "Folder",
|
||||
"panel": "Panel"
|
||||
},
|
||||
"results-table": {
|
||||
"datasource-header": "Data source",
|
||||
"location-header": "Location",
|
||||
|
@ -9,8 +9,7 @@
|
||||
"action": {
|
||||
"change-theme": "",
|
||||
"dark-theme": "",
|
||||
"light-theme": "",
|
||||
"search": ""
|
||||
"light-theme": ""
|
||||
},
|
||||
"search-box": {
|
||||
"placeholder": ""
|
||||
@ -84,7 +83,6 @@
|
||||
},
|
||||
"toolbar": {
|
||||
"add-panel": "Añadir panel",
|
||||
"comments-show": "Mostrar comentarios del panel de control",
|
||||
"mark-favorite": "Marcar como favorito",
|
||||
"open-original": "Abrir el panel de control original",
|
||||
"playlist-next": "Ir al siguiente panel de control",
|
||||
@ -292,7 +290,6 @@
|
||||
"title": "Escenas"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Buscar Grafana",
|
||||
"placeholderCommandPalette": ""
|
||||
},
|
||||
"search-dashboards": {
|
||||
@ -415,6 +412,11 @@
|
||||
"go-to-folder": "",
|
||||
"select-folder": ""
|
||||
},
|
||||
"result-kind": {
|
||||
"dashboard": "",
|
||||
"folder": "",
|
||||
"panel": ""
|
||||
},
|
||||
"results-table": {
|
||||
"datasource-header": "",
|
||||
"location-header": "",
|
||||
|
@ -9,8 +9,7 @@
|
||||
"action": {
|
||||
"change-theme": "",
|
||||
"dark-theme": "",
|
||||
"light-theme": "",
|
||||
"search": ""
|
||||
"light-theme": ""
|
||||
},
|
||||
"search-box": {
|
||||
"placeholder": ""
|
||||
@ -84,7 +83,6 @@
|
||||
},
|
||||
"toolbar": {
|
||||
"add-panel": "Ajouter un panneau",
|
||||
"comments-show": "Afficher les commentaires du tableau de bord",
|
||||
"mark-favorite": "Marquer comme favori",
|
||||
"open-original": "Ouvrir le tableau de bord d'origine",
|
||||
"playlist-next": "Accéder au tableau de bord suivant",
|
||||
@ -292,7 +290,6 @@
|
||||
"title": "Scènes"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Rechercher dans Grafana",
|
||||
"placeholderCommandPalette": ""
|
||||
},
|
||||
"search-dashboards": {
|
||||
@ -415,6 +412,11 @@
|
||||
"go-to-folder": "",
|
||||
"select-folder": ""
|
||||
},
|
||||
"result-kind": {
|
||||
"dashboard": "",
|
||||
"folder": "",
|
||||
"panel": ""
|
||||
},
|
||||
"results-table": {
|
||||
"datasource-header": "",
|
||||
"location-header": "",
|
||||
|
@ -9,8 +9,7 @@
|
||||
"action": {
|
||||
"change-theme": "Cĥäʼnģę ŧĥęmę...",
|
||||
"dark-theme": "Đäřĸ",
|
||||
"light-theme": "Ŀįģĥŧ",
|
||||
"search": "Ŝęäřčĥ"
|
||||
"light-theme": "Ŀįģĥŧ"
|
||||
},
|
||||
"search-box": {
|
||||
"placeholder": "Ŝęäřčĥ őř ĵūmp ŧő..."
|
||||
@ -84,7 +83,6 @@
|
||||
},
|
||||
"toolbar": {
|
||||
"add-panel": "Åđđ päʼnęľ",
|
||||
"comments-show": "Ŝĥőŵ đäşĥþőäřđ čőmmęʼnŧş",
|
||||
"mark-favorite": "Mäřĸ äş ƒävőřįŧę",
|
||||
"open-original": "Øpęʼn őřįģįʼnäľ đäşĥþőäřđ",
|
||||
"playlist-next": "Ğő ŧő ʼnęχŧ đäşĥþőäřđ",
|
||||
@ -292,7 +290,6 @@
|
||||
"title": "Ŝčęʼnęş"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Ŝęäřčĥ Ğřäƒäʼnä",
|
||||
"placeholderCommandPalette": "Ŝęäřčĥ őř ĵūmp ŧő..."
|
||||
},
|
||||
"search-dashboards": {
|
||||
@ -415,6 +412,11 @@
|
||||
"go-to-folder": "Ğő ŧő ƒőľđęř",
|
||||
"select-folder": "Ŝęľęčŧ ƒőľđęř"
|
||||
},
|
||||
"result-kind": {
|
||||
"dashboard": "Đäşĥþőäřđ",
|
||||
"folder": "Főľđęř",
|
||||
"panel": "Päʼnęľ"
|
||||
},
|
||||
"results-table": {
|
||||
"datasource-header": "Đäŧä şőūřčę",
|
||||
"location-header": "Ŀőčäŧįőʼn",
|
||||
|
@ -9,8 +9,7 @@
|
||||
"action": {
|
||||
"change-theme": "",
|
||||
"dark-theme": "",
|
||||
"light-theme": "",
|
||||
"search": ""
|
||||
"light-theme": ""
|
||||
},
|
||||
"search-box": {
|
||||
"placeholder": ""
|
||||
@ -84,7 +83,6 @@
|
||||
},
|
||||
"toolbar": {
|
||||
"add-panel": "添加面板",
|
||||
"comments-show": "显示仪表板备注",
|
||||
"mark-favorite": "标记为收藏",
|
||||
"open-original": "打开原始仪表板",
|
||||
"playlist-next": "前往下一个仪表板",
|
||||
@ -292,7 +290,6 @@
|
||||
"title": "场景"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索 Grafana",
|
||||
"placeholderCommandPalette": ""
|
||||
},
|
||||
"search-dashboards": {
|
||||
@ -415,6 +412,11 @@
|
||||
"go-to-folder": "",
|
||||
"select-folder": ""
|
||||
},
|
||||
"result-kind": {
|
||||
"dashboard": "",
|
||||
"folder": "",
|
||||
"panel": ""
|
||||
},
|
||||
"results-table": {
|
||||
"datasource-header": "",
|
||||
"location-header": "",
|
||||
|
Loading…
Reference in New Issue
Block a user