The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/browse-dashboards/state/reducers.ts

202 lines
6.3 KiB

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';
import { findItem } from './utils';
type FetchNextChildrenPageFulfilledAction = ReturnType<typeof fetchNextChildrenPage.fulfilled>;
type RefetchChildrenFulfilledAction = ReturnType<typeof refetchChildren.fulfilled>;
export function refetchChildrenFulfilled(state: BrowseDashboardsState, action: RefetchChildrenFulfilledAction) {
const { children, page, kind, lastPageOfKind } = action.payload;
const { parentUID } = action.meta.arg;
const newCollection = {
items: children,
lastFetchedKind: kind,
lastFetchedPage: page,
lastKindHasMoreItems: !lastPageOfKind,
isFullyLoaded: kind === 'dashboard' && lastPageOfKind,
};
if (parentUID) {
state.childrenByParentUID[parentUID] = newCollection;
} else {
state.rootItems = newCollection;
}
}
export function fetchNextChildrenPageFulfilled(
state: BrowseDashboardsState,
action: FetchNextChildrenPageFulfilledAction
) {
const payload = action.payload;
if (!payload) {
// If not additional pages to load, the action returns undefined
return;
}
const { children, page, kind, lastPageOfKind } = payload;
const { parentUID, excludeKinds = [] } = action.meta.arg;
const collection = parentUID ? state.childrenByParentUID[parentUID] : state.rootItems;
const prevItems = collection?.items ?? [];
const newCollection = {
items: prevItems.concat(children),
lastFetchedKind: kind,
lastFetchedPage: page,
lastKindHasMoreItems: !lastPageOfKind,
isFullyLoaded: !excludeKinds.includes('dashboard') ? kind === 'dashboard' && lastPageOfKind : lastPageOfKind,
};
if (!parentUID) {
state.rootItems = newCollection;
return;
}
state.childrenByParentUID[parentUID] = newCollection;
// If the parent of the items we've loaded are selected, we must select all these items also
const parentIsSelected = state.selectedItems.folder[parentUID];
if (parentIsSelected) {
for (const child of children) {
state.selectedItems[child.kind][child.uid] = true;
}
}
}
export function setFolderOpenState(
state: BrowseDashboardsState,
action: PayloadAction<{ folderUID: string; isOpen: boolean }>
) {
const { folderUID, isOpen } = action.payload;
state.openFolders[folderUID] = isOpen;
}
export function setItemSelectionState(
state: BrowseDashboardsState,
// SearchView doesn't use DashboardViewItemKind (yet), so we pick just the specific properties
// we're interested in
action: PayloadAction<{ item: Pick<DashboardViewItem, 'kind' | 'uid' | 'parentUID'>; isSelected: boolean }>
) {
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) {
state.selectedItems[kind][uid] = isSelected;
if (kind !== 'folder') {
return;
}
let collection = state.childrenByParentUID[uid];
for (const child of collection?.items ?? []) {
markChildren(child.kind, child.uid);
}
}
markChildren(item.kind, item.uid);
// 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);
// This case should not happen, but a find can theortically return undefined, and it
// helps limit infinite loops
if (!parent) {
break;
}
// 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?.items?.every((v) => state.selectedItems[v.kind][v.uid]) ?? false;
}
export function setAllSelection(
state: BrowseDashboardsState,
action: PayloadAction<{ isSelected: boolean; folderUID: string | undefined }>
) {
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...
// In search:
// - When "Selecting all", it sends individual state updates with setItemSelectionState.
// - When "Deselecting all", it uses this setAllSelection. Search results aren't stored in
// redux, so we just need to iterate over the selected items to flip them to false
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)
if (!collection) {
return;
}
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') {
continue;
}
selectChildrenOfFolder(child.uid);
}
}
selectChildrenOfFolder(folderUIDArg);
} else {
// if deselecting only need to loop over what we've already selected
for (const kind in state.selectedItems) {
if (!(kind === 'dashboard' || kind === 'panel' || kind === 'folder')) {
continue;
}
const selection = state.selectedItems[kind];
for (const uid in selection) {
selection[uid] = isSelected;
}
}
}
}