@@ -128,7 +109,7 @@ export function DashboardsTree({
return (
- {column.render('Header', { selectedItems })}
+ {column.render('Header', { isSelected, onAllSelectionChange })}
);
})}
@@ -156,13 +137,15 @@ interface VirtualListRowProps {
style: React.CSSProperties;
data: {
table: TableInstance
;
- selectedItems: Record>;
+ isSelected: DashboardsTreeCellProps['isSelected'];
+ onAllSelectionChange: DashboardsTreeCellProps['onAllSelectionChange'];
+ onItemSelectionChange: DashboardsTreeCellProps['onItemSelectionChange'];
};
}
function VirtualListRow({ index, style, data }: VirtualListRowProps) {
const styles = useStyles2(getStyles);
- const { table, selectedItems } = data;
+ const { table, isSelected, onItemSelectionChange } = data;
const { rows, prepareRow } = table;
const row = rows[index];
@@ -179,7 +162,7 @@ function VirtualListRow({ index, style, data }: VirtualListRowProps) {
return (
- {cell.render('Cell', { selectedItems })}
+ {cell.render('Cell', { isSelected, onItemSelectionChange })}
);
})}
diff --git a/public/app/features/browse-dashboards/state/hooks.ts b/public/app/features/browse-dashboards/state/hooks.ts
index 3bd672b0946..097ef3ebb7e 100644
--- a/public/app/features/browse-dashboards/state/hooks.ts
+++ b/public/app/features/browse-dashboards/state/hooks.ts
@@ -44,6 +44,10 @@ export function useCheckboxSelectionState() {
return useSelector((wholeState: StoreState) => wholeState.browseDashboards.selectedItems);
}
+export function useChildrenByParentUIDState() {
+ return useSelector((wholeState: StoreState) => wholeState.browseDashboards.childrenByParentUID);
+}
+
export function useActionSelectionState() {
return useSelector((state) => selectedItemsForActionsSelector(state));
}
diff --git a/public/app/features/browse-dashboards/state/reducers.test.ts b/public/app/features/browse-dashboards/state/reducers.test.ts
index 32e5b3bbb99..1bf0ce0d41a 100644
--- a/public/app/features/browse-dashboards/state/reducers.test.ts
+++ b/public/app/features/browse-dashboards/state/reducers.test.ts
@@ -115,8 +115,10 @@ describe('browse-dashboards reducers', () => {
describe('setItemSelectionState', () => {
it('marks items as selected', () => {
+ const folder = wellFormedFolder(1).item;
+ const dashboard = wellFormedDashboard(2).item;
const state = createInitialState();
- const dashboard = wellFormedDashboard().item;
+ state.rootItems = [folder, dashboard];
setItemSelectionState(state, { type: 'setItemSelectionState', payload: { item: dashboard, isSelected: true } });
@@ -133,11 +135,13 @@ describe('browse-dashboards reducers', () => {
it('marks descendants as selected when the parent folder is selected', () => {
const state = createInitialState();
- const parentFolder = wellFormedFolder(1).item;
- const childDashboard = wellFormedDashboard(2, {}, { parentUID: parentFolder.uid }).item;
- const childFolder = wellFormedFolder(3, {}, { parentUID: parentFolder.uid }).item;
- const grandchildDashboard = wellFormedDashboard(4, {}, { parentUID: childFolder.uid }).item;
+ const rootDashboard = wellFormedDashboard(1).item;
+ const parentFolder = wellFormedFolder(2).item;
+ const childDashboard = wellFormedDashboard(3, {}, { parentUID: parentFolder.uid }).item;
+ const childFolder = wellFormedFolder(4, {}, { parentUID: parentFolder.uid }).item;
+ const grandchildDashboard = wellFormedDashboard(5, {}, { parentUID: childFolder.uid }).item;
+ state.rootItems = [parentFolder, rootDashboard];
state.childrenByParentUID[parentFolder.uid] = [childDashboard, childFolder];
state.childrenByParentUID[childFolder.uid] = [grandchildDashboard];
@@ -196,6 +200,105 @@ describe('browse-dashboards reducers', () => {
panel: {},
});
});
+
+ it('selects ancestors when all their children are now selected', () => {
+ const state = createInitialState();
+
+ const rootDashboard = wellFormedDashboard(1).item;
+ const parentFolder = wellFormedFolder(2).item;
+ const childDashboard = wellFormedDashboard(3, {}, { parentUID: parentFolder.uid }).item;
+ const childFolder = wellFormedFolder(4, {}, { parentUID: parentFolder.uid }).item;
+ const grandchildDashboard = wellFormedDashboard(5, {}, { parentUID: childFolder.uid }).item;
+
+ state.rootItems = [parentFolder, rootDashboard];
+ state.childrenByParentUID[parentFolder.uid] = [childDashboard, childFolder];
+ state.childrenByParentUID[childFolder.uid] = [grandchildDashboard];
+
+ // Selected the deepest grandchild dashboard
+ setItemSelectionState(state, {
+ type: 'setItemSelectionState',
+ payload: { item: grandchildDashboard, isSelected: true },
+ });
+
+ expect(state.selectedItems).toEqual({
+ $all: false,
+ dashboard: {
+ [grandchildDashboard.uid]: true,
+ },
+ folder: {
+ [parentFolder.uid]: false,
+ [childFolder.uid]: true, // is selected because all it's children (grandchildDashboard) is selected
+ },
+ panel: {},
+ });
+
+ setItemSelectionState(state, {
+ type: 'setItemSelectionState',
+ payload: { item: childDashboard, isSelected: true },
+ });
+
+ expect(state.selectedItems).toEqual({
+ $all: false,
+ dashboard: {
+ [childDashboard.uid]: true,
+ [grandchildDashboard.uid]: true,
+ },
+ folder: {
+ [parentFolder.uid]: true, // is now selected because we also selected its other child
+ [childFolder.uid]: true,
+ },
+ panel: {},
+ });
+ });
+
+ it('selects the $all header checkbox when all descendants are now selected', () => {
+ const state = createInitialState();
+
+ const rootDashboard = wellFormedDashboard(1).item;
+ const rootFolder = wellFormedFolder(2).item;
+ const childDashboardA = wellFormedDashboard(3, {}, { parentUID: rootFolder.uid }).item;
+ const childDashboardB = wellFormedDashboard(4, {}, { parentUID: rootFolder.uid }).item;
+
+ state.rootItems = [rootFolder, rootDashboard];
+ state.childrenByParentUID[rootFolder.uid] = [childDashboardA, childDashboardB];
+
+ state.selectedItems.dashboard = { [rootDashboard.uid]: true, [childDashboardA.uid]: true };
+
+ // Selected the deepest grandchild dashboard
+ setItemSelectionState(state, {
+ type: 'setItemSelectionState',
+ payload: { item: childDashboardB, isSelected: true },
+ });
+
+ expect(state.selectedItems.$all).toBeTruthy();
+ });
+
+ it('unselects the $all header checkbox a descendant is unselected', () => {
+ const state = createInitialState();
+
+ const rootDashboard = wellFormedDashboard(1).item;
+ const rootFolder = wellFormedFolder(2).item;
+ const childDashboardA = wellFormedDashboard(3, {}, { parentUID: rootFolder.uid }).item;
+ const childDashboardB = wellFormedDashboard(4, {}, { parentUID: rootFolder.uid }).item;
+
+ state.rootItems = [rootFolder, rootDashboard];
+ state.childrenByParentUID[rootFolder.uid] = [childDashboardA, childDashboardB];
+
+ state.selectedItems.dashboard = {
+ [rootDashboard.uid]: true,
+ [childDashboardA.uid]: true,
+ [childDashboardB.uid]: true,
+ };
+ state.selectedItems.folder = { [rootFolder.uid]: true };
+
+ // Selected the deepest grandchild dashboard
+ setItemSelectionState(state, {
+ type: 'setItemSelectionState',
+ payload: { item: childDashboardB, isSelected: false },
+ });
+
+ expect(state.selectedItems.$all).toBeFalsy();
+ });
});
describe('setAllSelection', () => {
diff --git a/public/app/features/browse-dashboards/state/reducers.ts b/public/app/features/browse-dashboards/state/reducers.ts
index ac79052360c..86414f423ec 100644
--- a/public/app/features/browse-dashboards/state/reducers.ts
+++ b/public/app/features/browse-dashboards/state/reducers.ts
@@ -45,6 +45,8 @@ export function setItemSelectionState(
) {
const { item, isSelected } = action.payload;
+ // 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) {
state.selectedItems[kind][uid] = isSelected;
@@ -60,26 +62,37 @@ export function setItemSelectionState(
markChildren(item.kind, item.uid);
- // If we're unselecting an item, unselect all ancestors (parent, grandparent, etc) also
- // so we can later show a UI-only 'mixed' checkbox
- if (!isSelected) {
- let nextParentUID = item.parentUID;
+ // 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;
- // this is like a recursive climb up the parents of the tree while we have a
- // parentUID (we've hit a root dashboard/folder)
- while (nextParentUID) {
- const parent = findItem(state.rootItems, state.childrenByParentUID, nextParentUID);
+ while (nextParentUID) {
+ const parent = findItem(state.rootItems, state.childrenByParentUID, nextParentUID);
- // This case should not happen, but a find can theortically return undefined, and it
- // helps limit infinite loops
- if (!parent) {
- break;
- }
-
- state.selectedItems[parent.kind][parent.uid] = false;
- nextParentUID = parent.parentUID;
+ // This case should not happen, but a find can theortically return undefined, and it
+ // helps limit infinite loops
+ if (!parent) {
+ break;
}
+
+ if (isSelected) {
+ // If we're selecting an item, check all ancestors and see if all their children are
+ // now selected and update them appropriately
+ const children = state.childrenByParentUID[parent.uid];
+
+ const allChildrenSelected = children?.every((v) => state.selectedItems[v.kind][v.uid]) ?? false;
+ state.selectedItems[parent.kind][parent.uid] = allChildrenSelected;
+ } else {
+ // A folder cannot be selected if any of it's children are unselected
+ state.selectedItems[parent.kind][parent.uid] = false;
+ }
+
+ nextParentUID = parent.parentUID;
}
+
+ // Check to see if we should mark the header checkbox selected if all root items are selected
+ state.selectedItems.$all = state.rootItems.every((v) => state.selectedItems[v.kind][v.uid]) ?? false;
}
export function setAllSelection(state: BrowseDashboardsState, action: PayloadAction<{ isSelected: boolean }>) {
diff --git a/public/app/features/browse-dashboards/types.ts b/public/app/features/browse-dashboards/types.ts
index 7ace5982127..aa97eb5d737 100644
--- a/public/app/features/browse-dashboards/types.ts
+++ b/public/app/features/browse-dashboards/types.ts
@@ -1,3 +1,5 @@
+import { CellProps, Column, HeaderProps } from 'react-table';
+
import { DashboardViewItem as DashboardViewItem, DashboardViewItemKind } from 'app/features/search/types';
export type DashboardTreeSelection = Record> & {
@@ -27,3 +29,21 @@ export interface DashboardsTreeItem SelectionState;
+ onAllSelectionChange?: (newState: boolean) => void;
+ onItemSelectionChange?: (item: DashboardViewItem, newState: boolean) => void;
+}
+
+export type DashboardsTreeColumn = Column;
+export type DashboardsTreeCellProps = CellProps & RendererUserProps;
+export type DashboardTreeHeaderProps = HeaderProps & RendererUserProps;
+
+export enum SelectionState {
+ Unselected,
+ Selected,
+ Mixed,
+}