mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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: '',
|
uri: '',
|
||||||
items: [],
|
items: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
|
slug: '',
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
...searchItem,
|
...searchItem,
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,9 @@ import React, { useCallback } from 'react';
|
|||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { Card, Icon, IconName, TagList, useStyles2 } from '@grafana/ui';
|
import { Card, Icon, IconName, TagList, useStyles2 } from '@grafana/ui';
|
||||||
|
import { t } from 'app/core/internationalization';
|
||||||
|
|
||||||
import { SEARCH_ITEM_HEIGHT } from '../constants';
|
import { SEARCH_ITEM_HEIGHT } from '../constants';
|
||||||
import { getIconForKind } from '../service/utils';
|
import { getIconForKind } from '../service/utils';
|
||||||
@ -55,6 +57,16 @@ export const SearchItem = ({ item, isSelected, editable, onToggleChecked, onTagS
|
|||||||
[item, onToggleChecked]
|
[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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
data-testid={selectors.dashboardItem(item.title)}
|
data-testid={selectors.dashboardItem(item.title)}
|
||||||
@ -64,6 +76,7 @@ export const SearchItem = ({ item, isSelected, editable, onToggleChecked, onTagS
|
|||||||
onClick={onClickItem}
|
onClick={onClickItem}
|
||||||
>
|
>
|
||||||
<Card.Heading>{item.title}</Card.Heading>
|
<Card.Heading>{item.title}</Card.Heading>
|
||||||
|
|
||||||
<Card.Figure align={'center'} className={styles.checkbox}>
|
<Card.Figure align={'center'} className={styles.checkbox}>
|
||||||
<SearchCheckbox
|
<SearchCheckbox
|
||||||
aria-label="Select dashboard"
|
aria-label="Select dashboard"
|
||||||
@ -72,11 +85,9 @@ export const SearchItem = ({ item, isSelected, editable, onToggleChecked, onTagS
|
|||||||
onClick={handleCheckboxClick}
|
onClick={handleCheckboxClick}
|
||||||
/>
|
/>
|
||||||
</Card.Figure>
|
</Card.Figure>
|
||||||
|
|
||||||
<Card.Meta separator={''}>
|
<Card.Meta separator={''}>
|
||||||
<span className={styles.metaContainer}>
|
<span className={styles.metaContainer}>{description}</span>
|
||||||
<Icon name={getIconForKind(item.parentKind ?? 'folder')} aria-hidden />
|
|
||||||
{item.parentTitle || 'General'}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{item.sortMetaName && (
|
{item.sortMetaName && (
|
||||||
<span className={styles.metaContainer}>
|
<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) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
container: css`
|
container: css`
|
||||||
|
@ -15,7 +15,7 @@ describe('FolderSection', () => {
|
|||||||
const mockSelectionToggle = jest.fn();
|
const mockSelectionToggle = jest.fn();
|
||||||
const mockSelection = jest.fn();
|
const mockSelection = jest.fn();
|
||||||
const mockSection: DashboardViewItem = {
|
const mockSection: DashboardViewItem = {
|
||||||
kind: 'folder' as const,
|
kind: 'folder',
|
||||||
uid: 'my-folder',
|
uid: 'my-folder',
|
||||||
title: 'My folder',
|
title: 'My folder',
|
||||||
};
|
};
|
||||||
|
@ -5,12 +5,15 @@ import { useAsync, useLocalStorage } from 'react-use';
|
|||||||
import { GrafanaTheme2, toIconName } from '@grafana/data';
|
import { GrafanaTheme2, toIconName } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { Card, Checkbox, CollapsableSection, Icon, Spinner, useStyles2 } from '@grafana/ui';
|
import { Card, Checkbox, CollapsableSection, Icon, Spinner, useStyles2 } from '@grafana/ui';
|
||||||
|
import { config } from 'app/core/config';
|
||||||
import { t } from 'app/core/internationalization';
|
import { t } from 'app/core/internationalization';
|
||||||
import { getSectionStorageKey } from 'app/features/search/utils';
|
import { getSectionStorageKey } from 'app/features/search/utils';
|
||||||
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
|
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
|
||||||
|
|
||||||
import { SearchItem } from '../..';
|
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 { queryResultToViewItem } from '../../service/utils';
|
||||||
import { DashboardViewItem } from '../../types';
|
import { DashboardViewItem } from '../../types';
|
||||||
import { SelectionChecker, SelectionToggle } from '../selection';
|
import { SelectionChecker, SelectionToggle } from '../selection';
|
||||||
@ -25,6 +28,27 @@ interface SectionHeaderProps {
|
|||||||
tags?: string[];
|
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 = ({
|
export const FolderSection = ({
|
||||||
section,
|
section,
|
||||||
selectionToggle,
|
selectionToggle,
|
||||||
@ -42,22 +66,10 @@ export const FolderSection = ({
|
|||||||
if (!sectionExpanded && !renderStandaloneBody) {
|
if (!sectionExpanded && !renderStandaloneBody) {
|
||||||
return Promise.resolve([]);
|
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 childItems = getChildren(section, tags);
|
||||||
const items = raw.view.map((v) => queryResultToViewItem(v, raw.view));
|
|
||||||
return items;
|
return childItems;
|
||||||
}, [sectionExpanded, tags]);
|
}, [sectionExpanded, tags]);
|
||||||
|
|
||||||
const onSectionExpand = () => {
|
const onSectionExpand = () => {
|
||||||
@ -72,8 +84,8 @@ export const FolderSection = ({
|
|||||||
selectionToggle(section.kind, section.uid);
|
selectionToggle(section.kind, section.uid);
|
||||||
const sub = results.value ?? [];
|
const sub = results.value ?? [];
|
||||||
for (const item of sub) {
|
for (const item of sub) {
|
||||||
if (selection('dashboard', item.uid!) !== checked) {
|
if (selection(item.kind, item.uid!) !== checked) {
|
||||||
selectionToggle('dashboard', item.uid!);
|
selectionToggle(item.kind, item.uid!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,14 +116,10 @@ export const FolderSection = ({
|
|||||||
key={item.uid}
|
key={item.uid}
|
||||||
item={item}
|
item={item}
|
||||||
onTagSelected={onTagSelected}
|
onTagSelected={onTagSelected}
|
||||||
onToggleChecked={(item) => {
|
onToggleChecked={(item) => selectionToggle?.(item.kind, item.uid)}
|
||||||
if (selectionToggle) {
|
|
||||||
selectionToggle('dashboard', item.uid!);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
editable={Boolean(selection != null)}
|
editable={Boolean(selection != null)}
|
||||||
onClickItem={onClickItem}
|
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}>
|
<div className={styles.text}>
|
||||||
<span id={labelId}>{section.title}</span>
|
<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}>
|
<a href={section.url} className={styles.link}>
|
||||||
<span className={styles.separator}>|</span> <Icon name="folder-upload" />{' '}
|
<span className={styles.separator}>|</span> <Icon name="folder-upload" />{' '}
|
||||||
{t('search.folder-view.go-to-folder', 'Go to folder')}
|
{t('search.folder-view.go-to-folder', 'Go to folder')}
|
||||||
@ -170,6 +178,7 @@ export const FolderSection = ({
|
|||||||
|
|
||||||
const getSectionHeaderStyles = (theme: GrafanaTheme2, editable: boolean) => {
|
const getSectionHeaderStyles = (theme: GrafanaTheme2, editable: boolean) => {
|
||||||
const sm = theme.spacing(1);
|
const sm = theme.spacing(1);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -9,7 +9,7 @@ import impressionSrv from '../../../../core/services/impression_srv';
|
|||||||
import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from '../../service';
|
import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from '../../service';
|
||||||
import { DashboardSearchItemType } from '../../types';
|
import { DashboardSearchItemType } from '../../types';
|
||||||
|
|
||||||
import { FolderView } from './FolderView';
|
import { RootFolderView } from './RootFolderView';
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
@ -18,7 +18,7 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('FolderView', () => {
|
describe('RootFolderView', () => {
|
||||||
let grafanaSearcherSpy: jest.SpyInstance;
|
let grafanaSearcherSpy: jest.SpyInstance;
|
||||||
const mockOnTagSelected = jest.fn();
|
const mockOnTagSelected = jest.fn();
|
||||||
const mockSelectionToggle = jest.fn();
|
const mockSelectionToggle = jest.fn();
|
||||||
@ -69,7 +69,11 @@ describe('FolderView', () => {
|
|||||||
grafanaSearcherSpy.mockImplementationOnce(() => promise);
|
grafanaSearcherSpy.mockImplementationOnce(() => promise);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
<RootFolderView
|
||||||
|
onTagSelected={mockOnTagSelected}
|
||||||
|
selection={mockSelection}
|
||||||
|
selectionToggle={mockSelectionToggle}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
|
expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
|
||||||
|
|
||||||
@ -84,7 +88,11 @@ describe('FolderView', () => {
|
|||||||
it('does not show the starred items if not signed in', async () => {
|
it('does not show the starred items if not signed in', async () => {
|
||||||
contextSrv.isSignedIn = false;
|
contextSrv.isSignedIn = false;
|
||||||
render(
|
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((await screen.findAllByTestId(selectors.components.Search.sectionV2))[0]).toBeInTheDocument();
|
||||||
expect(screen.queryByRole('button', { name: 'Starred' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: 'Starred' })).not.toBeInTheDocument();
|
||||||
@ -93,7 +101,11 @@ describe('FolderView', () => {
|
|||||||
it('shows the starred items if signed in', async () => {
|
it('shows the starred items if signed in', async () => {
|
||||||
contextSrv.isSignedIn = true;
|
contextSrv.isSignedIn = true;
|
||||||
render(
|
render(
|
||||||
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
<RootFolderView
|
||||||
|
onTagSelected={mockOnTagSelected}
|
||||||
|
selection={mockSelection}
|
||||||
|
selectionToggle={mockSelectionToggle}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
expect(await screen.findByRole('button', { name: 'Starred' })).toBeInTheDocument();
|
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 () => {
|
it('does not show the recent items if no dashboards have been opened recently', async () => {
|
||||||
jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue([]);
|
jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue([]);
|
||||||
render(
|
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((await screen.findAllByTestId(selectors.components.Search.sectionV2))[0]).toBeInTheDocument();
|
||||||
expect(screen.queryByRole('button', { name: 'Recent' })).not.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 () => {
|
it('shows the recent items if any dashboards have recently been opened', async () => {
|
||||||
jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue(['7MeksYbmk']);
|
jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue(['7MeksYbmk']);
|
||||||
render(
|
render(
|
||||||
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
<RootFolderView
|
||||||
|
onTagSelected={mockOnTagSelected}
|
||||||
|
selection={mockSelection}
|
||||||
|
selectionToggle={mockSelectionToggle}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
expect(await screen.findByRole('button', { name: 'Recent' })).toBeInTheDocument();
|
expect(await screen.findByRole('button', { name: 'Recent' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the general folder by default', async () => {
|
it('shows the general folder by default', async () => {
|
||||||
render(
|
render(
|
||||||
<FolderView onTagSelected={mockOnTagSelected} selection={mockSelection} selectionToggle={mockSelectionToggle} />
|
<RootFolderView
|
||||||
|
onTagSelected={mockOnTagSelected}
|
||||||
|
selection={mockSelection}
|
||||||
|
selectionToggle={mockSelectionToggle}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
expect(await screen.findByRole('button', { name: 'General' })).toBeInTheDocument();
|
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 () => {
|
it('does not show the starred items even if signed in', async () => {
|
||||||
contextSrv.isSignedIn = true;
|
contextSrv.isSignedIn = true;
|
||||||
render(
|
render(
|
||||||
<FolderView
|
<RootFolderView
|
||||||
hidePseudoFolders
|
hidePseudoFolders
|
||||||
onTagSelected={mockOnTagSelected}
|
onTagSelected={mockOnTagSelected}
|
||||||
selection={mockSelection}
|
selection={mockSelection}
|
||||||
@ -140,7 +164,7 @@ describe('FolderView', () => {
|
|||||||
it('does not show the recent items even if recent dashboards have been opened', async () => {
|
it('does not show the recent items even if recent dashboards have been opened', async () => {
|
||||||
jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue(['7MeksYbmk']);
|
jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue(['7MeksYbmk']);
|
||||||
render(
|
render(
|
||||||
<FolderView
|
<RootFolderView
|
||||||
hidePseudoFolders
|
hidePseudoFolders
|
||||||
onTagSelected={mockOnTagSelected}
|
onTagSelected={mockOnTagSelected}
|
||||||
selection={mockSelection}
|
selection={mockSelection}
|
||||||
@ -156,7 +180,11 @@ describe('FolderView', () => {
|
|||||||
// reject with a specific Error object
|
// reject with a specific Error object
|
||||||
grafanaSearcherSpy.mockRejectedValueOnce(new Error('Uh oh spagghettios!'));
|
grafanaSearcherSpy.mockRejectedValueOnce(new Error('Uh oh spagghettios!'));
|
||||||
render(
|
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();
|
expect(await screen.findByRole('alert', { name: 'Uh oh spagghettios!' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@ -165,7 +193,11 @@ describe('FolderView', () => {
|
|||||||
// reject with nothing
|
// reject with nothing
|
||||||
grafanaSearcherSpy.mockRejectedValueOnce(null);
|
grafanaSearcherSpy.mockRejectedValueOnce(null);
|
||||||
render(
|
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();
|
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 { selectors } from '@grafana/e2e-selectors';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { Alert, Spinner, useStyles2 } from '@grafana/ui';
|
import { Alert, Spinner, useStyles2 } from '@grafana/ui';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
|
||||||
import { contextSrv } from '../../../../core/services/context_srv';
|
import { contextSrv } from '../../../../core/services/context_srv';
|
||||||
import impressionSrv from '../../../../core/services/impression_srv';
|
import impressionSrv from '../../../../core/services/impression_srv';
|
||||||
import { GENERAL_FOLDER_UID } from '../../constants';
|
import { GENERAL_FOLDER_UID } from '../../constants';
|
||||||
import { getGrafanaSearcher } from '../../service';
|
import { getGrafanaSearcher } from '../../service';
|
||||||
|
import { getFolderChildren } from '../../service/folders';
|
||||||
import { queryResultToViewItem } from '../../service/utils';
|
import { queryResultToViewItem } from '../../service/utils';
|
||||||
import { DashboardViewItem } from '../../types';
|
|
||||||
import { SearchResultsProps } from '../components/SearchResultsTable';
|
|
||||||
|
|
||||||
import { FolderSection } from './FolderSection';
|
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'> & {
|
type Props = Pick<SearchResultsProps, 'selection' | 'selectionToggle' | 'onTagSelected' | 'onClickItem'> & {
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
hidePseudoFolders?: boolean;
|
hidePseudoFolders?: boolean;
|
||||||
};
|
};
|
||||||
|
export const RootFolderView = ({
|
||||||
export const FolderView = ({
|
|
||||||
selection,
|
selection,
|
||||||
selectionToggle,
|
selectionToggle,
|
||||||
onTagSelected,
|
onTagSelected,
|
||||||
@ -33,33 +49,22 @@ export const FolderView = ({
|
|||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const results = useAsync(async () => {
|
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) {
|
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) {
|
if (contextSrv.isSignedIn) {
|
||||||
const stars = await getBackendSrv().get('api/user/stars');
|
const stars = await getBackendSrv().get('api/user/stars');
|
||||||
if (stars.length > 0) {
|
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;
|
return folders;
|
@ -48,6 +48,7 @@ export const SearchResultsCards = React.memo(
|
|||||||
|
|
||||||
const item = response.view.get(rowIndex);
|
const item = response.view.get(rowIndex);
|
||||||
const searchItem = queryResultToViewItem(item, response.view);
|
const searchItem = queryResultToViewItem(item, response.view);
|
||||||
|
const isSelected = selectionToggle && selection?.(searchItem.kind, searchItem.uid);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style} key={item.uid} className={className} role="row">
|
<div style={style} key={item.uid} className={className} role="row">
|
||||||
@ -61,7 +62,7 @@ export const SearchResultsCards = React.memo(
|
|||||||
}}
|
}}
|
||||||
editable={Boolean(selection != null)}
|
editable={Boolean(selection != null)}
|
||||||
onClickItem={onClickItem}
|
onClickItem={onClickItem}
|
||||||
isSelected={selectionToggle && selection?.(searchItem.kind, searchItem.uid)}
|
isSelected={isSelected}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -5,6 +5,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { useStyles2, Spinner, Button } from '@grafana/ui';
|
import { useStyles2, Spinner, Button } from '@grafana/ui';
|
||||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
import { FolderDTO } from 'app/types';
|
import { FolderDTO } from 'app/types';
|
||||||
@ -12,13 +13,13 @@ import { FolderDTO } from 'app/types';
|
|||||||
import { PreviewsSystemRequirements } from '../../components/PreviewsSystemRequirements';
|
import { PreviewsSystemRequirements } from '../../components/PreviewsSystemRequirements';
|
||||||
import { getGrafanaSearcher } from '../../service';
|
import { getGrafanaSearcher } from '../../service';
|
||||||
import { getSearchStateManager } from '../../state/SearchStateManager';
|
import { getSearchStateManager } from '../../state/SearchStateManager';
|
||||||
import { SearchLayout } from '../../types';
|
import { SearchLayout, DashboardViewItem } from '../../types';
|
||||||
import { newSearchSelection, updateSearchSelection } from '../selection';
|
import { newSearchSelection, updateSearchSelection } from '../selection';
|
||||||
|
|
||||||
import { ActionRow, getValidQueryLayout } from './ActionRow';
|
import { ActionRow, getValidQueryLayout } from './ActionRow';
|
||||||
import { FolderSection } from './FolderSection';
|
import { FolderSection } from './FolderSection';
|
||||||
import { FolderView } from './FolderView';
|
|
||||||
import { ManageActions } from './ManageActions';
|
import { ManageActions } from './ManageActions';
|
||||||
|
import { RootFolderView } from './RootFolderView';
|
||||||
import { SearchResultsCards } from './SearchResultsCards';
|
import { SearchResultsCards } from './SearchResultsCards';
|
||||||
import { SearchResultsGrid } from './SearchResultsGrid';
|
import { SearchResultsGrid } from './SearchResultsGrid';
|
||||||
import { SearchResultsTable, SearchResultsProps } from './SearchResultsTable';
|
import { SearchResultsTable, SearchResultsProps } from './SearchResultsTable';
|
||||||
@ -86,11 +87,12 @@ export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardE
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selection = showManage ? searchSelection.isSelected : undefined;
|
const selection = showManage ? searchSelection.isSelected : undefined;
|
||||||
|
|
||||||
if (layout === SearchLayout.Folders) {
|
if (layout === SearchLayout.Folders) {
|
||||||
if (folderDTO) {
|
if (folderDTO) {
|
||||||
return (
|
return (
|
||||||
<FolderSection
|
<FolderSection
|
||||||
section={{ uid: folderDTO.uid, kind: 'folder', title: folderDTO.title }}
|
section={sectionForFolderView(folderDTO)}
|
||||||
selection={selection}
|
selection={selection}
|
||||||
selectionToggle={toggleSelection}
|
selectionToggle={toggleSelection}
|
||||||
onTagSelected={stateManager.onAddTag}
|
onTagSelected={stateManager.onAddTag}
|
||||||
@ -102,7 +104,7 @@ export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardE
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<FolderView
|
<RootFolderView
|
||||||
key={listKey}
|
key={listKey}
|
||||||
selection={selection}
|
selection={selection}
|
||||||
selectionToggle={toggleSelection}
|
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 (
|
return (
|
||||||
<EmptyListCTA
|
<EmptyListCTA
|
||||||
title="This folder doesn't have any dashboards yet"
|
title="This folder doesn't have any dashboards yet"
|
||||||
@ -215,3 +225,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
margin-top: ${theme.v1.spacing.md};
|
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 */
|
/** Gets the default sort used for the Folder view */
|
||||||
getFolderViewSort: () => string;
|
getFolderViewSort: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NestedFolderDTO {
|
||||||
|
uid: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
@ -170,7 +170,7 @@ export function DashList(props: PanelProps<PanelOptions>) {
|
|||||||
<ul className={css.gridContainer}>
|
<ul className={css.gridContainer}>
|
||||||
{dashboards.map((dash) => (
|
{dashboards.map((dash) => (
|
||||||
<li key={dash.uid}>
|
<li key={dash.uid}>
|
||||||
<SearchCard item={{ ...dash, kind: 'folder' }} />
|
<SearchCard item={{ ...dash, kind: 'dashboard' }} />
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -9,8 +9,7 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"change-theme": "",
|
"change-theme": "",
|
||||||
"dark-theme": "",
|
"dark-theme": "",
|
||||||
"light-theme": "",
|
"light-theme": ""
|
||||||
"search": ""
|
|
||||||
},
|
},
|
||||||
"search-box": {
|
"search-box": {
|
||||||
"placeholder": ""
|
"placeholder": ""
|
||||||
@ -84,7 +83,6 @@
|
|||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"add-panel": "Panel hinzufügen",
|
"add-panel": "Panel hinzufügen",
|
||||||
"comments-show": "Dashboard-Kommentare anzeigen",
|
|
||||||
"mark-favorite": "Als Favorit markieren",
|
"mark-favorite": "Als Favorit markieren",
|
||||||
"open-original": "Original-Dashboard öffnen",
|
"open-original": "Original-Dashboard öffnen",
|
||||||
"playlist-next": "Zum nächsten Dashboard",
|
"playlist-next": "Zum nächsten Dashboard",
|
||||||
@ -292,7 +290,6 @@
|
|||||||
"title": "Szenen"
|
"title": "Szenen"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Grafana durchsuchen",
|
|
||||||
"placeholderCommandPalette": ""
|
"placeholderCommandPalette": ""
|
||||||
},
|
},
|
||||||
"search-dashboards": {
|
"search-dashboards": {
|
||||||
@ -415,6 +412,11 @@
|
|||||||
"go-to-folder": "",
|
"go-to-folder": "",
|
||||||
"select-folder": ""
|
"select-folder": ""
|
||||||
},
|
},
|
||||||
|
"result-kind": {
|
||||||
|
"dashboard": "",
|
||||||
|
"folder": "",
|
||||||
|
"panel": ""
|
||||||
|
},
|
||||||
"results-table": {
|
"results-table": {
|
||||||
"datasource-header": "",
|
"datasource-header": "",
|
||||||
"location-header": "",
|
"location-header": "",
|
||||||
|
@ -9,8 +9,7 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"change-theme": "Change theme...",
|
"change-theme": "Change theme...",
|
||||||
"dark-theme": "Dark",
|
"dark-theme": "Dark",
|
||||||
"light-theme": "Light",
|
"light-theme": "Light"
|
||||||
"search": "Search"
|
|
||||||
},
|
},
|
||||||
"search-box": {
|
"search-box": {
|
||||||
"placeholder": "Search or jump to..."
|
"placeholder": "Search or jump to..."
|
||||||
@ -84,7 +83,6 @@
|
|||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"add-panel": "Add panel",
|
"add-panel": "Add panel",
|
||||||
"comments-show": "Show dashboard comments",
|
|
||||||
"mark-favorite": "Mark as favorite",
|
"mark-favorite": "Mark as favorite",
|
||||||
"open-original": "Open original dashboard",
|
"open-original": "Open original dashboard",
|
||||||
"playlist-next": "Go to next dashboard",
|
"playlist-next": "Go to next dashboard",
|
||||||
@ -292,7 +290,6 @@
|
|||||||
"title": "Scenes"
|
"title": "Scenes"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Search Grafana",
|
|
||||||
"placeholderCommandPalette": "Search or jump to..."
|
"placeholderCommandPalette": "Search or jump to..."
|
||||||
},
|
},
|
||||||
"search-dashboards": {
|
"search-dashboards": {
|
||||||
@ -415,6 +412,11 @@
|
|||||||
"go-to-folder": "Go to folder",
|
"go-to-folder": "Go to folder",
|
||||||
"select-folder": "Select folder"
|
"select-folder": "Select folder"
|
||||||
},
|
},
|
||||||
|
"result-kind": {
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"folder": "Folder",
|
||||||
|
"panel": "Panel"
|
||||||
|
},
|
||||||
"results-table": {
|
"results-table": {
|
||||||
"datasource-header": "Data source",
|
"datasource-header": "Data source",
|
||||||
"location-header": "Location",
|
"location-header": "Location",
|
||||||
|
@ -9,8 +9,7 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"change-theme": "",
|
"change-theme": "",
|
||||||
"dark-theme": "",
|
"dark-theme": "",
|
||||||
"light-theme": "",
|
"light-theme": ""
|
||||||
"search": ""
|
|
||||||
},
|
},
|
||||||
"search-box": {
|
"search-box": {
|
||||||
"placeholder": ""
|
"placeholder": ""
|
||||||
@ -84,7 +83,6 @@
|
|||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"add-panel": "Añadir panel",
|
"add-panel": "Añadir panel",
|
||||||
"comments-show": "Mostrar comentarios del panel de control",
|
|
||||||
"mark-favorite": "Marcar como favorito",
|
"mark-favorite": "Marcar como favorito",
|
||||||
"open-original": "Abrir el panel de control original",
|
"open-original": "Abrir el panel de control original",
|
||||||
"playlist-next": "Ir al siguiente panel de control",
|
"playlist-next": "Ir al siguiente panel de control",
|
||||||
@ -292,7 +290,6 @@
|
|||||||
"title": "Escenas"
|
"title": "Escenas"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Buscar Grafana",
|
|
||||||
"placeholderCommandPalette": ""
|
"placeholderCommandPalette": ""
|
||||||
},
|
},
|
||||||
"search-dashboards": {
|
"search-dashboards": {
|
||||||
@ -415,6 +412,11 @@
|
|||||||
"go-to-folder": "",
|
"go-to-folder": "",
|
||||||
"select-folder": ""
|
"select-folder": ""
|
||||||
},
|
},
|
||||||
|
"result-kind": {
|
||||||
|
"dashboard": "",
|
||||||
|
"folder": "",
|
||||||
|
"panel": ""
|
||||||
|
},
|
||||||
"results-table": {
|
"results-table": {
|
||||||
"datasource-header": "",
|
"datasource-header": "",
|
||||||
"location-header": "",
|
"location-header": "",
|
||||||
|
@ -9,8 +9,7 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"change-theme": "",
|
"change-theme": "",
|
||||||
"dark-theme": "",
|
"dark-theme": "",
|
||||||
"light-theme": "",
|
"light-theme": ""
|
||||||
"search": ""
|
|
||||||
},
|
},
|
||||||
"search-box": {
|
"search-box": {
|
||||||
"placeholder": ""
|
"placeholder": ""
|
||||||
@ -84,7 +83,6 @@
|
|||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"add-panel": "Ajouter un panneau",
|
"add-panel": "Ajouter un panneau",
|
||||||
"comments-show": "Afficher les commentaires du tableau de bord",
|
|
||||||
"mark-favorite": "Marquer comme favori",
|
"mark-favorite": "Marquer comme favori",
|
||||||
"open-original": "Ouvrir le tableau de bord d'origine",
|
"open-original": "Ouvrir le tableau de bord d'origine",
|
||||||
"playlist-next": "Accéder au tableau de bord suivant",
|
"playlist-next": "Accéder au tableau de bord suivant",
|
||||||
@ -292,7 +290,6 @@
|
|||||||
"title": "Scènes"
|
"title": "Scènes"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Rechercher dans Grafana",
|
|
||||||
"placeholderCommandPalette": ""
|
"placeholderCommandPalette": ""
|
||||||
},
|
},
|
||||||
"search-dashboards": {
|
"search-dashboards": {
|
||||||
@ -415,6 +412,11 @@
|
|||||||
"go-to-folder": "",
|
"go-to-folder": "",
|
||||||
"select-folder": ""
|
"select-folder": ""
|
||||||
},
|
},
|
||||||
|
"result-kind": {
|
||||||
|
"dashboard": "",
|
||||||
|
"folder": "",
|
||||||
|
"panel": ""
|
||||||
|
},
|
||||||
"results-table": {
|
"results-table": {
|
||||||
"datasource-header": "",
|
"datasource-header": "",
|
||||||
"location-header": "",
|
"location-header": "",
|
||||||
|
@ -9,8 +9,7 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"change-theme": "Cĥäʼnģę ŧĥęmę...",
|
"change-theme": "Cĥäʼnģę ŧĥęmę...",
|
||||||
"dark-theme": "Đäřĸ",
|
"dark-theme": "Đäřĸ",
|
||||||
"light-theme": "Ŀįģĥŧ",
|
"light-theme": "Ŀįģĥŧ"
|
||||||
"search": "Ŝęäřčĥ"
|
|
||||||
},
|
},
|
||||||
"search-box": {
|
"search-box": {
|
||||||
"placeholder": "Ŝęäřčĥ őř ĵūmp ŧő..."
|
"placeholder": "Ŝęäřčĥ őř ĵūmp ŧő..."
|
||||||
@ -84,7 +83,6 @@
|
|||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"add-panel": "Åđđ päʼnęľ",
|
"add-panel": "Åđđ päʼnęľ",
|
||||||
"comments-show": "Ŝĥőŵ đäşĥþőäřđ čőmmęʼnŧş",
|
|
||||||
"mark-favorite": "Mäřĸ äş ƒävőřįŧę",
|
"mark-favorite": "Mäřĸ äş ƒävőřįŧę",
|
||||||
"open-original": "Øpęʼn őřįģįʼnäľ đäşĥþőäřđ",
|
"open-original": "Øpęʼn őřįģįʼnäľ đäşĥþőäřđ",
|
||||||
"playlist-next": "Ğő ŧő ʼnęχŧ đäşĥþőäřđ",
|
"playlist-next": "Ğő ŧő ʼnęχŧ đäşĥþőäřđ",
|
||||||
@ -292,7 +290,6 @@
|
|||||||
"title": "Ŝčęʼnęş"
|
"title": "Ŝčęʼnęş"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Ŝęäřčĥ Ğřäƒäʼnä",
|
|
||||||
"placeholderCommandPalette": "Ŝęäřčĥ őř ĵūmp ŧő..."
|
"placeholderCommandPalette": "Ŝęäřčĥ őř ĵūmp ŧő..."
|
||||||
},
|
},
|
||||||
"search-dashboards": {
|
"search-dashboards": {
|
||||||
@ -415,6 +412,11 @@
|
|||||||
"go-to-folder": "Ğő ŧő ƒőľđęř",
|
"go-to-folder": "Ğő ŧő ƒőľđęř",
|
||||||
"select-folder": "Ŝęľęčŧ ƒőľđęř"
|
"select-folder": "Ŝęľęčŧ ƒőľđęř"
|
||||||
},
|
},
|
||||||
|
"result-kind": {
|
||||||
|
"dashboard": "Đäşĥþőäřđ",
|
||||||
|
"folder": "Főľđęř",
|
||||||
|
"panel": "Päʼnęľ"
|
||||||
|
},
|
||||||
"results-table": {
|
"results-table": {
|
||||||
"datasource-header": "Đäŧä şőūřčę",
|
"datasource-header": "Đäŧä şőūřčę",
|
||||||
"location-header": "Ŀőčäŧįőʼn",
|
"location-header": "Ŀőčäŧįőʼn",
|
||||||
|
@ -9,8 +9,7 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"change-theme": "",
|
"change-theme": "",
|
||||||
"dark-theme": "",
|
"dark-theme": "",
|
||||||
"light-theme": "",
|
"light-theme": ""
|
||||||
"search": ""
|
|
||||||
},
|
},
|
||||||
"search-box": {
|
"search-box": {
|
||||||
"placeholder": ""
|
"placeholder": ""
|
||||||
@ -84,7 +83,6 @@
|
|||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"add-panel": "添加面板",
|
"add-panel": "添加面板",
|
||||||
"comments-show": "显示仪表板备注",
|
|
||||||
"mark-favorite": "标记为收藏",
|
"mark-favorite": "标记为收藏",
|
||||||
"open-original": "打开原始仪表板",
|
"open-original": "打开原始仪表板",
|
||||||
"playlist-next": "前往下一个仪表板",
|
"playlist-next": "前往下一个仪表板",
|
||||||
@ -292,7 +290,6 @@
|
|||||||
"title": "场景"
|
"title": "场景"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "搜索 Grafana",
|
|
||||||
"placeholderCommandPalette": ""
|
"placeholderCommandPalette": ""
|
||||||
},
|
},
|
||||||
"search-dashboards": {
|
"search-dashboards": {
|
||||||
@ -415,6 +412,11 @@
|
|||||||
"go-to-folder": "",
|
"go-to-folder": "",
|
||||||
"select-folder": ""
|
"select-folder": ""
|
||||||
},
|
},
|
||||||
|
"result-kind": {
|
||||||
|
"dashboard": "",
|
||||||
|
"folder": "",
|
||||||
|
"panel": ""
|
||||||
|
},
|
||||||
"results-table": {
|
"results-table": {
|
||||||
"datasource-header": "",
|
"datasource-header": "",
|
||||||
"location-header": "",
|
"location-header": "",
|
||||||
|
Loading…
Reference in New Issue
Block a user