mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 06:02:49 +08:00
NestedFolders: Support Shared with me folder for showing items you've been granted access to (#80141)
* start shared with me frontend tweaks * prevent linking to sharedwithme folder * tests * make divider take up 0 height * Prevent sharedwithme from being selected * test git push * pr feedback * prevent setting url for sharedwithme * split iconForItem/kind functions * Hide sharedwithme in nested folder picker * fix test fixture
This commit is contained in:
@ -166,6 +166,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
|
||||
tokenExpirationDayLimit: undefined;
|
||||
disableFrontendSandboxForPlugins: string[] = [];
|
||||
sharedWithMeFolderUID: string | undefined;
|
||||
|
||||
constructor(options: GrafanaBootConfig) {
|
||||
this.bootData = options.bootData;
|
||||
|
@ -4,6 +4,7 @@ import React, { useCallback, useId, useMemo, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert, Icon, Input, LoadingBar, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { skipToken, useGetFolderQuery } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||
@ -164,6 +165,10 @@ export function NestedFolderPicker({
|
||||
return createFlatTree(undefined, searchCollection, childrenCollections, {}, 0, EXCLUDED_KINDS, excludeUIDs);
|
||||
}
|
||||
|
||||
const allExcludedUIDs = config.sharedWithMeFolderUID
|
||||
? [...(excludeUIDs || []), config.sharedWithMeFolderUID]
|
||||
: excludeUIDs;
|
||||
|
||||
let flatTree = createFlatTree(
|
||||
undefined,
|
||||
rootCollection,
|
||||
@ -171,7 +176,7 @@ export function NestedFolderPicker({
|
||||
folderOpenState,
|
||||
0,
|
||||
EXCLUDED_KINDS,
|
||||
excludeUIDs
|
||||
allExcludedUIDs
|
||||
);
|
||||
|
||||
if (showRootFolder) {
|
||||
|
@ -57,6 +57,7 @@ export const browseDashboardsAPI = createApi({
|
||||
providesTags: (_result, _error, folderUID) => [{ type: 'getFolder', id: folderUID }],
|
||||
query: (folderUID) => ({ url: `/folders/${folderUID}`, params: { accesscontrol: true } }),
|
||||
}),
|
||||
|
||||
// create a new folder
|
||||
newFolder: builder.mutation<FolderDTO, { title: string; parentUid?: string }>({
|
||||
query: ({ title, parentUid }) => ({
|
||||
@ -81,6 +82,7 @@ export const browseDashboardsAPI = createApi({
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
// save an existing folder (e.g. rename)
|
||||
saveFolder: builder.mutation<FolderDTO, FolderDTO>({
|
||||
// because the getFolder calls contain the parents, renaming a parent/grandparent/etc needs to invalidate all child folders
|
||||
|
@ -6,6 +6,7 @@ import { DashboardViewItem } from 'app/features/search/types';
|
||||
|
||||
import { contextSrv } from '../../../core/core';
|
||||
import { AccessControlAction } from '../../../types';
|
||||
import { isSharedWithMe } from '../components/utils';
|
||||
|
||||
export const PAGE_SIZE = 50;
|
||||
|
||||
@ -36,7 +37,7 @@ export async function listFolders(
|
||||
title: item.title,
|
||||
parentTitle,
|
||||
parentUID,
|
||||
url: `/dashboards/f/${item.uid}/`,
|
||||
url: isSharedWithMe(item.uid) ? undefined : `/dashboards/f/${item.uid}/`,
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -8,26 +8,31 @@ import { t } from 'app/core/internationalization';
|
||||
|
||||
import { DashboardsTreeCellProps, SelectionState } from '../types';
|
||||
|
||||
import { isSharedWithMe } from './utils';
|
||||
|
||||
export default function CheckboxCell({
|
||||
row: { original: row },
|
||||
isSelected,
|
||||
onItemSelectionChange,
|
||||
}: DashboardsTreeCellProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const item = row.item;
|
||||
|
||||
if (!isSelected) {
|
||||
return <span className={styles.checkboxSpacer} />;
|
||||
return <CheckboxSpacer />;
|
||||
}
|
||||
|
||||
if (item.kind === 'ui') {
|
||||
if (item.uiKind === 'pagination-placeholder') {
|
||||
return <Checkbox disabled value={false} />;
|
||||
} else {
|
||||
return <span className={styles.checkboxSpacer} />;
|
||||
return <CheckboxSpacer />;
|
||||
}
|
||||
}
|
||||
|
||||
if (isSharedWithMe(item.uid)) {
|
||||
return <CheckboxSpacer />;
|
||||
}
|
||||
|
||||
const state = isSelected(item);
|
||||
|
||||
return (
|
||||
@ -41,6 +46,11 @@ export default function CheckboxCell({
|
||||
);
|
||||
}
|
||||
|
||||
function CheckboxSpacer() {
|
||||
const styles = useStyles2(getStyles);
|
||||
return <span className={styles.checkboxSpacer} />;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
// Should be the same size as the <IconButton /> so Dashboard name is aligned to Folder name siblings
|
||||
checkboxSpacer: css({
|
||||
|
@ -5,8 +5,14 @@ import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { assertIsDefined } from 'test/helpers/asserts';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { wellFormedDashboard, wellFormedEmptyFolder, wellFormedFolder } from '../fixtures/dashboardsTreeItem.fixture';
|
||||
import {
|
||||
sharedWithMeFolder,
|
||||
wellFormedDashboard,
|
||||
wellFormedEmptyFolder,
|
||||
wellFormedFolder,
|
||||
} from '../fixtures/dashboardsTreeItem.fixture';
|
||||
import { SelectionState } from '../types';
|
||||
|
||||
import { DashboardsTree } from './DashboardsTree';
|
||||
@ -27,6 +33,10 @@ describe('browse-dashboards DashboardsTree', () => {
|
||||
const allItemsAreLoaded = () => true;
|
||||
const requestLoadMore = () => Promise.resolve();
|
||||
|
||||
beforeAll(() => {
|
||||
config.sharedWithMeFolderUID = 'sharedwithme';
|
||||
});
|
||||
|
||||
it('renders a dashboard item', () => {
|
||||
render(
|
||||
<DashboardsTree
|
||||
@ -82,9 +92,73 @@ describe('browse-dashboards DashboardsTree', () => {
|
||||
requestLoadMore={requestLoadMore}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(folder.item.title)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a folder link', () => {
|
||||
render(
|
||||
<DashboardsTree
|
||||
canSelect
|
||||
items={[folder]}
|
||||
isSelected={isSelected}
|
||||
width={WIDTH}
|
||||
height={HEIGHT}
|
||||
onFolderClick={noop}
|
||||
onItemSelectionChange={noop}
|
||||
onAllSelectionChange={noop}
|
||||
isItemLoaded={allItemsAreLoaded}
|
||||
requestLoadMore={requestLoadMore}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(folder.item.title)).toHaveAttribute('href', folder.item.url);
|
||||
});
|
||||
|
||||
it("doesn't link to the sharedwithme pseudo-folder", () => {
|
||||
const sharedWithMe = sharedWithMeFolder(2);
|
||||
|
||||
render(
|
||||
<DashboardsTree
|
||||
canSelect
|
||||
items={[sharedWithMe, folder]}
|
||||
isSelected={isSelected}
|
||||
width={WIDTH}
|
||||
height={HEIGHT}
|
||||
onFolderClick={noop}
|
||||
onItemSelectionChange={noop}
|
||||
onAllSelectionChange={noop}
|
||||
isItemLoaded={allItemsAreLoaded}
|
||||
requestLoadMore={requestLoadMore}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(sharedWithMe.item.title)).not.toHaveAttribute('href');
|
||||
});
|
||||
|
||||
it("doesn't render a checkbox for the sharedwithme pseudo-folder", () => {
|
||||
const sharedWithMe = sharedWithMeFolder(2);
|
||||
|
||||
render(
|
||||
<DashboardsTree
|
||||
canSelect
|
||||
items={[sharedWithMe, folder]}
|
||||
isSelected={isSelected}
|
||||
width={WIDTH}
|
||||
height={HEIGHT}
|
||||
onFolderClick={noop}
|
||||
onItemSelectionChange={noop}
|
||||
onAllSelectionChange={noop}
|
||||
isItemLoaded={allItemsAreLoaded}
|
||||
requestLoadMore={requestLoadMore}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId(selectors.pages.BrowseDashboards.table.checkbox(sharedWithMe.item.uid))
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onFolderClick when a folder button is clicked', async () => {
|
||||
const handler = jest.fn();
|
||||
render(
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { useCallback, useEffect, useId, useMemo, useRef } from 'react';
|
||||
import { TableInstance, useTable } from 'react-table';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import InfiniteLoader from 'react-window-infinite-loader';
|
||||
|
||||
import { GrafanaTheme2, isTruthy } from '@grafana/data';
|
||||
@ -35,6 +35,7 @@ interface DashboardsTreeProps {
|
||||
|
||||
const HEADER_HEIGHT = 36;
|
||||
const ROW_HEIGHT = 36;
|
||||
const DIVIDER_HEIGHT = 0; // Yes - make it appear as a border on the row rather than a row itself
|
||||
|
||||
export function DashboardsTree({
|
||||
items,
|
||||
@ -51,6 +52,7 @@ export function DashboardsTree({
|
||||
const treeID = useId();
|
||||
|
||||
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
||||
const listRef = useRef<List | null>(null);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
useEffect(() => {
|
||||
@ -60,6 +62,10 @@ export function DashboardsTree({
|
||||
if (infiniteLoaderRef.current) {
|
||||
infiniteLoaderRef.current.resetloadMoreItemsCache(true);
|
||||
}
|
||||
|
||||
if (listRef.current) {
|
||||
listRef.current.resetAfterIndex(0);
|
||||
}
|
||||
}, [items]);
|
||||
|
||||
const tableColumns = useMemo(() => {
|
||||
@ -123,6 +129,18 @@ export function DashboardsTree({
|
||||
[requestLoadMore, items]
|
||||
);
|
||||
|
||||
const getRowHeight = useCallback(
|
||||
(rowIndex: number) => {
|
||||
const row = items[rowIndex];
|
||||
if (row.item.kind === 'ui' && row.item.uiKind === 'divider') {
|
||||
return DIVIDER_HEIGHT;
|
||||
}
|
||||
|
||||
return ROW_HEIGHT;
|
||||
},
|
||||
[items]
|
||||
);
|
||||
|
||||
return (
|
||||
<div {...getTableProps()} role="table">
|
||||
{headerGroups.map((headerGroup) => {
|
||||
@ -154,12 +172,16 @@ export function DashboardsTree({
|
||||
>
|
||||
{({ onItemsRendered, ref }) => (
|
||||
<List
|
||||
ref={ref}
|
||||
ref={(elem) => {
|
||||
ref(elem);
|
||||
listRef.current = elem;
|
||||
}}
|
||||
height={height - HEADER_HEIGHT}
|
||||
width={width}
|
||||
itemCount={items.length}
|
||||
itemData={virtualData}
|
||||
itemSize={ROW_HEIGHT}
|
||||
estimatedItemSize={ROW_HEIGHT}
|
||||
itemSize={getRowHeight}
|
||||
onItemsRendered={onItemsRendered}
|
||||
>
|
||||
{VirtualListRow}
|
||||
@ -191,13 +213,23 @@ function VirtualListRow({ index, style, data }: VirtualListRowProps) {
|
||||
const row = rows[index];
|
||||
prepareRow(row);
|
||||
|
||||
const dashboardItem = row.original.item;
|
||||
|
||||
if (dashboardItem.kind === 'ui' && dashboardItem.uiKind === 'divider') {
|
||||
return (
|
||||
<div {...row.getRowProps({ style })}>
|
||||
<hr className={styles.divider} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...row.getRowProps({ style })}
|
||||
className={cx(styles.row, styles.bodyRow)}
|
||||
aria-labelledby={makeRowID(treeID, row.original.item)}
|
||||
aria-labelledby={makeRowID(treeID, dashboardItem)}
|
||||
data-testid={selectors.pages.BrowseDashboards.table.row(
|
||||
'title' in row.original.item ? row.original.item.title : row.original.item.uid
|
||||
'title' in dashboardItem ? dashboardItem.title : dashboardItem.uid
|
||||
)}
|
||||
>
|
||||
{row.cells.map((cell) => {
|
||||
@ -221,6 +253,12 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
|
||||
divider: css({
|
||||
borderTop: `1px solid ${theme.colors.border.weak}`,
|
||||
width: '100%',
|
||||
margin: 0,
|
||||
}),
|
||||
|
||||
headerRow: css({
|
||||
backgroundColor: theme.colors.background.secondary,
|
||||
height: HEADER_HEIGHT,
|
||||
|
@ -7,7 +7,7 @@ import { reportInteraction } from '@grafana/runtime';
|
||||
import { Icon, IconButton, Link, Spinner, useStyles2, Text } from '@grafana/ui';
|
||||
import { getSvgSize } from '@grafana/ui/src/components/Icon/utils';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { getIconForKind } from 'app/features/search/service/utils';
|
||||
import { getIconForItem } from 'app/features/search/service/utils';
|
||||
|
||||
import { Indent } from '../../../core/components/Indent/Indent';
|
||||
import { useChildrenByParentUIDState } from '../state';
|
||||
@ -27,7 +27,7 @@ export function NameCell({ row: { original: data }, onFolderClick, treeID }: Nam
|
||||
const { item, level, isOpen } = data;
|
||||
const childrenByParentUID = useChildrenByParentUIDState();
|
||||
const isLoading = isOpen && !childrenByParentUID[item.uid];
|
||||
const iconName = getIconForKind(data.item.kind, isOpen);
|
||||
const iconName = getIconForItem(data.item, isOpen);
|
||||
|
||||
if (item.kind === 'ui') {
|
||||
return (
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { DashboardViewItemWithUIItems } from '../types';
|
||||
|
||||
export function makeRowID(baseId: string, item: DashboardViewItemWithUIItems) {
|
||||
return baseId + item.uid;
|
||||
}
|
||||
|
||||
export function isSharedWithMe(uid: string) {
|
||||
return uid === config.sharedWithMeFolderUID;
|
||||
}
|
||||
|
@ -65,6 +65,14 @@ export function wellFormedFolder(
|
||||
};
|
||||
}
|
||||
|
||||
export function sharedWithMeFolder(seed = 1): DashboardsTreeItem<DashboardViewItem> {
|
||||
const folder = wellFormedFolder(seed, undefined, {
|
||||
uid: 'sharedwithme',
|
||||
url: undefined,
|
||||
});
|
||||
return folder;
|
||||
}
|
||||
|
||||
export function wellFormedTree() {
|
||||
let seed = 1;
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { DashboardViewItem } from 'app/features/search/types';
|
||||
import { useSelector, StoreState, useDispatch } from 'app/types';
|
||||
|
||||
import { PAGE_SIZE } from '../api/services';
|
||||
import { isSharedWithMe } from '../components/utils';
|
||||
import {
|
||||
BrowseDashboardsState,
|
||||
DashboardsTreeItem,
|
||||
@ -130,7 +131,7 @@ export function useLoadNextChildrenPage(
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of items, with level indicating it's 'nested' in the tree structure
|
||||
* Creates a list of items, with level indicating it's nesting in the tree structure
|
||||
*
|
||||
* @param folderUID The UID of the folder being viewed, or undefined if at root Browse Dashboards page
|
||||
* @param rootItems Array of loaded items at the root level (without a parent). If viewing a folder, we expect this to be empty and unused
|
||||
@ -180,7 +181,22 @@ export function createFlatTree(
|
||||
isOpen,
|
||||
};
|
||||
|
||||
return [thisItem, ...mappedChildren];
|
||||
const items = [thisItem, ...mappedChildren];
|
||||
|
||||
if (isSharedWithMe(thisItem.item.uid)) {
|
||||
items.push({
|
||||
item: {
|
||||
kind: 'ui',
|
||||
uiKind: 'divider',
|
||||
uid: 'shared-with-me-divider',
|
||||
},
|
||||
parentUID,
|
||||
level: level + 1,
|
||||
isOpen: false,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
const isOpen = (folderUID && openFolders[folderUID]) || level === 0;
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { wellFormedDashboard, wellFormedFolder } from '../fixtures/dashboardsTreeItem.fixture';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { sharedWithMeFolder, wellFormedDashboard, wellFormedFolder } from '../fixtures/dashboardsTreeItem.fixture';
|
||||
import { fullyLoadedViewItemCollection } from '../fixtures/state.fixtures';
|
||||
import { BrowseDashboardsState } from '../types';
|
||||
|
||||
@ -19,6 +21,10 @@ function createInitialState(): BrowseDashboardsState {
|
||||
}
|
||||
|
||||
describe('browse-dashboards reducers', () => {
|
||||
beforeAll(() => {
|
||||
config.sharedWithMeFolderUID = 'sharedwithme';
|
||||
});
|
||||
|
||||
describe('fetchNextChildrenPageFulfilled', () => {
|
||||
it('loads first page of root items', () => {
|
||||
const pageSize = 50;
|
||||
@ -321,11 +327,34 @@ describe('browse-dashboards reducers', () => {
|
||||
|
||||
expect(state.selectedItems.$all).toBeFalsy();
|
||||
});
|
||||
|
||||
it('does not allow the sharedwithme folder to be selected', () => {
|
||||
let seed = 1;
|
||||
const folder = wellFormedFolder(seed++).item;
|
||||
const dashboard = wellFormedDashboard(seed++).item;
|
||||
const sharedWithMe = sharedWithMeFolder(seed++).item;
|
||||
const sharedWithMeDashboard = wellFormedDashboard(seed++, {}, { parentUID: sharedWithMe.uid }).item;
|
||||
|
||||
const state = createInitialState();
|
||||
state.rootItems = fullyLoadedViewItemCollection([sharedWithMe, folder, dashboard]);
|
||||
state.childrenByParentUID[sharedWithMe.uid] = fullyLoadedViewItemCollection([sharedWithMeDashboard]);
|
||||
|
||||
setItemSelectionState(state, {
|
||||
type: 'setItemSelectionState',
|
||||
payload: { item: sharedWithMe, isSelected: true },
|
||||
});
|
||||
|
||||
expect(state.selectedItems.folder[sharedWithMe.uid]).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAllSelection', () => {
|
||||
let seed = 1;
|
||||
const topLevelDashboard = wellFormedDashboard(seed++).item;
|
||||
|
||||
const sharedWithMe = sharedWithMeFolder(seed++).item;
|
||||
const sharedWithMeDashboard = wellFormedDashboard(seed++, {}, { parentUID: sharedWithMe.uid }).item;
|
||||
|
||||
const topLevelFolder = wellFormedFolder(seed++).item;
|
||||
const childDashboard = wellFormedDashboard(seed++, {}, { parentUID: topLevelFolder.uid }).item;
|
||||
const childFolder = wellFormedFolder(seed++, {}, { parentUID: topLevelFolder.uid }).item;
|
||||
@ -407,5 +436,35 @@ describe('browse-dashboards reducers', () => {
|
||||
panel: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't select the sharedwithme folder when selecting all", () => {
|
||||
const state = createInitialState();
|
||||
|
||||
state.rootItems = fullyLoadedViewItemCollection([topLevelFolder, sharedWithMe]);
|
||||
state.childrenByParentUID[sharedWithMe.uid] = fullyLoadedViewItemCollection([sharedWithMeDashboard]);
|
||||
state.childrenByParentUID[topLevelFolder.uid] = fullyLoadedViewItemCollection([childDashboard, childFolder]);
|
||||
|
||||
setAllSelection(state, { type: 'setAllSelection', payload: { isSelected: true, folderUID: undefined } });
|
||||
|
||||
expect(state.selectedItems.folder[sharedWithMe.uid]).toBeFalsy();
|
||||
expect(state.selectedItems.dashboard[sharedWithMeDashboard.uid]).toBeFalsy();
|
||||
});
|
||||
|
||||
it("doesn't select anything when on the sharedwithme folder page", () => {
|
||||
const state = createInitialState();
|
||||
|
||||
state.rootItems = fullyLoadedViewItemCollection([topLevelFolder, topLevelDashboard]);
|
||||
state.childrenByParentUID[topLevelFolder.uid] = fullyLoadedViewItemCollection([childDashboard, childFolder]);
|
||||
state.childrenByParentUID[childFolder.uid] = fullyLoadedViewItemCollection([grandchildDashboard]);
|
||||
|
||||
setAllSelection(state, { type: 'setAllSelection', payload: { isSelected: true, folderUID: sharedWithMe.uid } });
|
||||
|
||||
expect(state.selectedItems).toEqual({
|
||||
$all: false,
|
||||
dashboard: {},
|
||||
folder: {},
|
||||
panel: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2,6 +2,7 @@ import { PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { DashboardViewItem, DashboardViewItemKind } from 'app/features/search/types';
|
||||
|
||||
import { isSharedWithMe } from '../components/utils';
|
||||
import { BrowseDashboardsState } from '../types';
|
||||
|
||||
import { fetchNextChildrenPage, refetchChildren } from './actions';
|
||||
@ -86,6 +87,11 @@ export function setItemSelectionState(
|
||||
) {
|
||||
const { item, isSelected } = action.payload;
|
||||
|
||||
// UI shouldn't allow it, but also prevent sharedwithme from being selected
|
||||
if (isSharedWithMe(item.uid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Selecting a folder selects all children, and unselecting a folder deselects all children
|
||||
// so propagate the new selection state to all descendants
|
||||
function markChildren(kind: DashboardViewItemKind, uid: string) {
|
||||
@ -103,26 +109,24 @@ export function setItemSelectionState(
|
||||
|
||||
markChildren(item.kind, item.uid);
|
||||
|
||||
// If all children of a folder are selected, then the folder is also selected.
|
||||
// If *any* child of a folder is unselelected, then the folder is alo unselected.
|
||||
// Reconcile all ancestors to make sure they're in the correct state.
|
||||
let nextParentUID = item.parentUID;
|
||||
// If we're unselecting a child, we also need to unselect all ancestors.
|
||||
if (!isSelected) {
|
||||
let nextParentUID = item.parentUID;
|
||||
|
||||
while (nextParentUID) {
|
||||
const parent = findItem(state.rootItems?.items ?? [], state.childrenByParentUID, nextParentUID);
|
||||
while (nextParentUID) {
|
||||
const parent = findItem(state.rootItems?.items ?? [], state.childrenByParentUID, nextParentUID);
|
||||
|
||||
// This case should not happen, but a find can theortically return undefined, and it
|
||||
// helps limit infinite loops
|
||||
if (!parent) {
|
||||
break;
|
||||
}
|
||||
// This case should not happen, but a find can theortically return undefined, and it
|
||||
// helps limit infinite loops
|
||||
if (!parent) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isSelected) {
|
||||
// A folder cannot be selected if any of it's children are unselected
|
||||
state.selectedItems[parent.kind][parent.uid] = false;
|
||||
}
|
||||
|
||||
nextParentUID = parent.parentUID;
|
||||
nextParentUID = parent.parentUID;
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if we should mark the header checkbox selected if all root items are selected
|
||||
@ -135,6 +139,12 @@ export function setAllSelection(
|
||||
) {
|
||||
const { isSelected, folderUID: folderUIDArg } = action.payload;
|
||||
|
||||
// If we're in the folder view for sharedwith me (currently not supported)
|
||||
// bail and don't select anything
|
||||
if (folderUIDArg && isSharedWithMe(folderUIDArg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.selectedItems.$all = isSelected;
|
||||
|
||||
// Search works a bit differently so the state here does different things...
|
||||
@ -146,6 +156,11 @@ export function setAllSelection(
|
||||
if (isSelected) {
|
||||
// Recursively select the children of the folder in view
|
||||
function selectChildrenOfFolder(folderUID: string | undefined) {
|
||||
// Don't descend into the sharedwithme folder
|
||||
if (folderUID && isSharedWithMe(folderUID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const collection = folderUID ? state.childrenByParentUID[folderUID] : state.rootItems;
|
||||
|
||||
// Bail early if the collection isn't found (not loaded yet)
|
||||
@ -154,6 +169,11 @@ export function setAllSelection(
|
||||
}
|
||||
|
||||
for (const child of collection.items) {
|
||||
// Don't traverse into the sharedwithme folder
|
||||
if (isSharedWithMe(child.uid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
state.selectedItems[child.kind][child.uid] = isSelected;
|
||||
|
||||
if (child.kind !== 'folder') {
|
||||
|
@ -29,7 +29,7 @@ export interface BrowseDashboardsState {
|
||||
|
||||
export interface UIDashboardViewItem {
|
||||
kind: 'ui';
|
||||
uiKind: 'empty-folder' | 'pagination-placeholder';
|
||||
uiKind: 'empty-folder' | 'pagination-placeholder' | 'divider';
|
||||
uid: string;
|
||||
}
|
||||
|
||||
|
@ -178,6 +178,7 @@ export const generateColumns = (
|
||||
return info ? (
|
||||
<a key={p} href={info.url} className={styles.locationItem}>
|
||||
<Icon name={getIconForKind(info.kind)} />
|
||||
|
||||
<Text variant="body" truncate>
|
||||
{info.name}
|
||||
</Text>
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { DataFrameView, IconName } from '@grafana/data';
|
||||
import { isSharedWithMe } from 'app/features/browse-dashboards/components/utils';
|
||||
import { DashboardViewItemWithUIItems } from 'app/features/browse-dashboards/types';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
|
||||
import { DashboardViewItem, DashboardViewItemKind } from '../types';
|
||||
@ -50,6 +52,33 @@ export function getIconForKind(kind: string, isOpen?: boolean): IconName {
|
||||
return 'question-circle';
|
||||
}
|
||||
|
||||
export function getIconForItem(item: DashboardViewItemWithUIItems, isOpen?: boolean): IconName {
|
||||
if (item && isSharedWithMe(item.uid)) {
|
||||
return 'users-alt';
|
||||
} else {
|
||||
return getIconForKind(item.kind, isOpen);
|
||||
}
|
||||
}
|
||||
|
||||
// export function getIconForItem(itemOrKind: string | DashboardViewItemWithUIItems, isOpen?: boolean): IconName {
|
||||
// const kind = typeof itemOrKind === 'string' ? itemOrKind : itemOrKind.kind;
|
||||
// const item = typeof itemOrKind === 'string' ? undefined : itemOrKind;
|
||||
|
||||
// if (kind === 'dashboard') {
|
||||
// return 'apps';
|
||||
// }
|
||||
|
||||
// if (item && isSharedWithMe(item.uid)) {
|
||||
// return 'users-alt';
|
||||
// }
|
||||
|
||||
// if (kind === 'folder') {
|
||||
// return isOpen ? 'folder-open' : 'folder';
|
||||
// }
|
||||
|
||||
// return 'question-circle';
|
||||
// }
|
||||
|
||||
function parseKindString(kind: string): DashboardViewItemKind {
|
||||
switch (kind) {
|
||||
case 'dashboard':
|
||||
|
Reference in New Issue
Block a user