2025-04-01 10:38:02 +09:00

188 lines
5.7 KiB
TypeScript

import { useCallback } from 'react';
import { CallToActionCard, EmptyState, LinkButton, TextLink } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { DashboardViewItem } from 'app/features/search/types';
import { useDispatch } from 'app/types';
import { PAGE_SIZE } from '../api/services';
import {
useFlatTreeState,
useCheckboxSelectionState,
setFolderOpenState,
setItemSelectionState,
useChildrenByParentUIDState,
setAllSelection,
useBrowseLoadingStatus,
useLoadNextChildrenPage,
fetchNextChildrenPage,
} from '../state';
import { BrowseDashboardsState, DashboardTreeSelection, SelectionState } from '../types';
import { DashboardsTree } from './DashboardsTree';
interface BrowseViewProps {
height: number;
width: number;
folderUID: string | undefined;
canSelect: boolean;
}
export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewProps) {
const status = useBrowseLoadingStatus(folderUID);
const dispatch = useDispatch();
const flatTree = useFlatTreeState(folderUID);
const selectedItems = useCheckboxSelectionState();
const childrenByParentUID = useChildrenByParentUIDState();
const handleFolderClick = useCallback(
(clickedFolderUID: string, isOpen: boolean) => {
dispatch(setFolderOpenState({ folderUID: clickedFolderUID, isOpen }));
if (isOpen) {
dispatch(fetchNextChildrenPage({ parentUID: clickedFolderUID, pageSize: PAGE_SIZE }));
}
},
[dispatch]
);
const handleItemSelectionChange = useCallback(
(item: DashboardViewItem, isSelected: boolean) => {
dispatch(setItemSelectionState({ item, isSelected }));
},
[dispatch]
);
const isSelected = useCallback(
(item: DashboardViewItem | '$all'): SelectionState => {
if (item === '$all') {
// We keep the boolean $all state up to date in redux, so we can short-circut
// the logic if we know this has been selected
if (selectedItems.$all) {
return SelectionState.Selected;
}
// Otherwise, if we have any selected items, then it should be in 'mixed' state
for (const selection of Object.values(selectedItems)) {
if (typeof selection === 'boolean') {
continue;
}
for (const uid in selection) {
const isSelected = selection[uid];
if (isSelected) {
return SelectionState.Mixed;
}
}
}
// Otherwise otherwise, nothing is selected and header should be unselected
return SelectionState.Unselected;
}
const isSelected = selectedItems[item.kind][item.uid];
if (isSelected) {
return SelectionState.Selected;
}
// Because if _all_ children, then the parent is selected (and bailed in the previous check),
// this .some check will only return true if the children are partially selected
const isMixed = hasSelectedDescendants(item, childrenByParentUID, selectedItems);
if (isMixed) {
return SelectionState.Mixed;
}
return SelectionState.Unselected;
},
[selectedItems, childrenByParentUID]
);
const isItemLoaded = useCallback(
(itemIndex: number) => {
const treeItem = flatTree[itemIndex];
if (!treeItem) {
return false;
}
const item = treeItem.item;
const result = !(item.kind === 'ui' && item.uiKind === 'pagination-placeholder');
return result;
},
[flatTree]
);
const handleLoadMore = useLoadNextChildrenPage();
if (status === 'fulfilled' && flatTree.length === 0) {
return (
<div style={{ width }}>
{canSelect ? (
<EmptyState
variant="call-to-action"
button={
<LinkButton
href={folderUID ? `dashboard/new?folderUid=${folderUID}` : 'dashboard/new'}
icon="plus"
size="lg"
>
<Trans i18nKey="browse-dashboards.empty-state.button-title">Create dashboard</Trans>
</LinkButton>
}
message={
folderUID
? t('browse-dashboards.empty-state.title-folder', "This folder doesn't have any dashboards yet")
: t('browse-dashboards.empty-state.title', "You haven't created any dashboards yet")
}
>
{folderUID && (
<Trans i18nKey="browse-dashboards.empty-state.pro-tip">
Add/move dashboards to your folder at{' '}
<TextLink external={false} href="/dashboards">
Browse dashboards
</TextLink>
</Trans>
)}
</EmptyState>
) : (
<CallToActionCard callToActionElement={<span>This folder is empty</span>} />
)}
</div>
);
}
return (
<DashboardsTree
canSelect={canSelect}
items={flatTree}
width={width}
height={height}
isSelected={isSelected}
onFolderClick={handleFolderClick}
onAllSelectionChange={(newState) => dispatch(setAllSelection({ isSelected: newState, folderUID }))}
onItemSelectionChange={handleItemSelectionChange}
isItemLoaded={isItemLoaded}
requestLoadMore={handleLoadMore}
/>
);
}
function hasSelectedDescendants(
item: DashboardViewItem,
childrenByParentUID: BrowseDashboardsState['childrenByParentUID'],
selectedItems: DashboardTreeSelection
): boolean {
const collection = childrenByParentUID[item.uid];
if (!collection) {
return false;
}
return collection.items.some((v) => {
const thisIsSelected = selectedItems[v.kind][v.uid];
if (thisIsSelected) {
return thisIsSelected;
}
return hasSelectedDescendants(v, childrenByParentUID, selectedItems);
});
}