\n {headerGroups.map((headerGroup) => {\n const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps({\n style: { width },\n });\n\n return (\n
\n {headerGroup.headers.map((column) => {\n const { key, ...headerProps } = column.getHeaderProps();\n\n return (\n
\n {column.render('Header', { isSelected, onAllSelectionChange })}\n
\n );\n })}\n
\n );\n })}\n\n
\n \n {({ onItemsRendered, ref }) => (\n {\n ref(elem);\n listRef.current = elem;\n }}\n height={height - HEADER_HEIGHT}\n width={width}\n itemCount={items.length}\n itemData={virtualData}\n estimatedItemSize={ROW_HEIGHT}\n itemSize={getRowHeight}\n onItemsRendered={onItemsRendered}\n >\n {VirtualListRow}\n
\n )}\n \n
\n
\n );\n}\n\ninterface VirtualListRowProps {\n index: number;\n style: React.CSSProperties;\n data: {\n table: TableInstance;\n isSelected: DashboardsTreeCellProps['isSelected'];\n onAllSelectionChange: DashboardsTreeCellProps['onAllSelectionChange'];\n onItemSelectionChange: DashboardsTreeCellProps['onItemSelectionChange'];\n treeID: string;\n };\n}\n\nfunction VirtualListRow({ index, style, data }: VirtualListRowProps) {\n const styles = useStyles2(getStyles);\n const { table, isSelected, onItemSelectionChange, treeID } = data;\n const { rows, prepareRow } = table;\n\n const row = rows[index];\n prepareRow(row);\n\n const dashboardItem = row.original.item;\n\n if (dashboardItem.kind === 'ui' && dashboardItem.uiKind === 'divider') {\n return (\n \n
\n \n );\n }\n\n return (\n \n {row.cells.map((cell) => {\n const { key, ...cellProps } = cell.getCellProps();\n\n return (\n
\n {cell.render('Cell', { isSelected, onItemSelectionChange, treeID })}\n
\n );\n })}\n
\n );\n}\n\nconst getStyles = (theme: GrafanaTheme2) => {\n return {\n // Column flex properties (cell sizing) are set by customFlexTableLayout.ts\n\n row: css({\n gap: theme.spacing(1),\n }),\n\n divider: css({\n borderTop: `1px solid ${theme.colors.border.weak}`,\n width: '100%',\n margin: 0,\n }),\n\n headerRow: css({\n backgroundColor: theme.colors.background.secondary,\n height: HEADER_HEIGHT,\n }),\n\n bodyRow: css({\n height: ROW_HEIGHT,\n\n '&:hover': {\n backgroundColor: theme.colors.emphasize(theme.colors.background.primary, 0.03),\n },\n }),\n\n cell: css({\n padding: theme.spacing(1),\n overflow: 'hidden', // Required so flex children can do text-overflow: ellipsis\n display: 'flex',\n alignItems: 'center',\n }),\n\n link: css({\n '&:hover': {\n textDecoration: 'underline',\n },\n }),\n };\n};\n","import React, { useCallback } from 'react';\n\nimport { CallToActionCard } from '@grafana/ui';\nimport EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';\nimport { DashboardViewItem } from 'app/features/search/types';\nimport { useDispatch } from 'app/types';\n\nimport { PAGE_SIZE } from '../api/services';\nimport {\n useFlatTreeState,\n useCheckboxSelectionState,\n setFolderOpenState,\n setItemSelectionState,\n useChildrenByParentUIDState,\n setAllSelection,\n useBrowseLoadingStatus,\n useLoadNextChildrenPage,\n fetchNextChildrenPage,\n} from '../state';\nimport { BrowseDashboardsState, DashboardTreeSelection, SelectionState } from '../types';\n\nimport { DashboardsTree } from './DashboardsTree';\n\ninterface BrowseViewProps {\n height: number;\n width: number;\n folderUID: string | undefined;\n canSelect: boolean;\n}\n\nexport function BrowseView({ folderUID, width, height, canSelect }: BrowseViewProps) {\n const status = useBrowseLoadingStatus(folderUID);\n const dispatch = useDispatch();\n const flatTree = useFlatTreeState(folderUID);\n const selectedItems = useCheckboxSelectionState();\n const childrenByParentUID = useChildrenByParentUIDState();\n\n const handleFolderClick = useCallback(\n (clickedFolderUID: string, isOpen: boolean) => {\n dispatch(setFolderOpenState({ folderUID: clickedFolderUID, isOpen }));\n\n if (isOpen) {\n dispatch(fetchNextChildrenPage({ parentUID: clickedFolderUID, pageSize: PAGE_SIZE }));\n }\n },\n [dispatch]\n );\n\n const handleItemSelectionChange = useCallback(\n (item: DashboardViewItem, isSelected: boolean) => {\n dispatch(setItemSelectionState({ item, isSelected }));\n },\n [dispatch]\n );\n\n const isSelected = useCallback(\n (item: DashboardViewItem | '$all'): SelectionState => {\n if (item === '$all') {\n // We keep the boolean $all state up to date in redux, so we can short-circut\n // the logic if we know this has been selected\n if (selectedItems.$all) {\n return SelectionState.Selected;\n }\n\n // Otherwise, if we have any selected items, then it should be in 'mixed' state\n for (const selection of Object.values(selectedItems)) {\n if (typeof selection === 'boolean') {\n continue;\n }\n\n for (const uid in selection) {\n const isSelected = selection[uid];\n if (isSelected) {\n return SelectionState.Mixed;\n }\n }\n }\n\n // Otherwise otherwise, nothing is selected and header should be unselected\n return SelectionState.Unselected;\n }\n\n const isSelected = selectedItems[item.kind][item.uid];\n if (isSelected) {\n return SelectionState.Selected;\n }\n\n // Because if _all_ children, then the parent is selected (and bailed in the previous check),\n // this .some check will only return true if the children are partially selected\n const isMixed = hasSelectedDescendants(item, childrenByParentUID, selectedItems);\n if (isMixed) {\n return SelectionState.Mixed;\n }\n\n return SelectionState.Unselected;\n },\n [selectedItems, childrenByParentUID]\n );\n\n const isItemLoaded = useCallback(\n (itemIndex: number) => {\n const treeItem = flatTree[itemIndex];\n if (!treeItem) {\n return false;\n }\n const item = treeItem.item;\n const result = !(item.kind === 'ui' && item.uiKind === 'pagination-placeholder');\n\n return result;\n },\n [flatTree]\n );\n\n const handleLoadMore = useLoadNextChildrenPage();\n\n if (status === 'fulfilled' && flatTree.length === 0) {\n return (\n \n {canSelect ? (\n '}\n proTipLink={folderUID && 'dashboards'}\n proTipLinkTitle={folderUID && 'Browse dashboards'}\n proTipTarget=\"\"\n />\n ) : (\n This folder is empty} />\n )}\n
\n );\n }\n\n return (\n dispatch(setAllSelection({ isSelected: newState, folderUID }))}\n onItemSelectionChange={handleItemSelectionChange}\n isItemLoaded={isItemLoaded}\n requestLoadMore={handleLoadMore}\n />\n );\n}\n\nfunction hasSelectedDescendants(\n item: DashboardViewItem,\n childrenByParentUID: BrowseDashboardsState['childrenByParentUID'],\n selectedItems: DashboardTreeSelection\n): boolean {\n const collection = childrenByParentUID[item.uid];\n if (!collection) {\n return false;\n }\n\n return collection.items.some((v) => {\n const thisIsSelected = selectedItems[v.kind][v.uid];\n if (thisIsSelected) {\n return thisIsSelected;\n }\n\n return hasSelectedDescendants(v, childrenByParentUID, selectedItems);\n });\n}\n","import React from 'react';\n\nimport { selectors } from '@grafana/e2e-selectors';\nimport { Button, Input, Form, Field, HorizontalGroup } from '@grafana/ui';\nimport { Trans, t } from 'app/core/internationalization';\n\nimport { validationSrv } from '../../manage-dashboards/services/ValidationSrv';\n\ninterface Props {\n onConfirm: (folderName: string) => void;\n onCancel: () => void;\n}\n\ninterface FormModel {\n folderName: string;\n}\n\nconst initialFormModel: FormModel = { folderName: '' };\n\nexport function NewFolderForm({ onCancel, onConfirm }: Props) {\n const translatedFolderNameRequiredPhrase = t(\n 'browse-dashboards.action.new-folder-name-required-phrase',\n 'Folder name is required.'\n );\n const validateFolderName = async (folderName: string) => {\n try {\n await validationSrv.validateNewFolderName(folderName);\n return true;\n } catch (e) {\n if (e instanceof Error) {\n return e.message;\n } else {\n throw e;\n }\n }\n };\n\n const fieldNameLabel = t('browse-dashboards.new-folder-form.name-label', 'Folder name');\n\n return (\n \n );\n}\n","import React, { useState } from 'react';\nimport { useLocation } from 'react-router-dom';\n\nimport { config, reportInteraction } from '@grafana/runtime';\nimport { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';\nimport {\n getNewDashboardPhrase,\n getNewFolderPhrase,\n getImportPhrase,\n getNewPhrase,\n} from 'app/features/search/tempI18nPhrases';\nimport { FolderDTO } from 'app/types';\n\nimport { useNewFolderMutation } from '../api/browseDashboardsAPI';\n\nimport { NewFolderForm } from './NewFolderForm';\n\ninterface Props {\n parentFolder?: FolderDTO;\n canCreateFolder: boolean;\n canCreateDashboard: boolean;\n}\n\nexport default function CreateNewButton({ parentFolder, canCreateDashboard, canCreateFolder }: Props) {\n const [isOpen, setIsOpen] = useState(false);\n const location = useLocation();\n const [newFolder] = useNewFolderMutation();\n const [showNewFolderDrawer, setShowNewFolderDrawer] = useState(false);\n\n const onCreateFolder = async (folderName: string) => {\n try {\n await newFolder({\n title: folderName,\n parentUid: parentFolder?.uid,\n });\n const depth = parentFolder?.parents ? parentFolder.parents.length + 1 : 0;\n reportInteraction('grafana_manage_dashboards_folder_created', {\n is_subfolder: Boolean(parentFolder?.uid),\n folder_depth: depth,\n });\n } finally {\n setShowNewFolderDrawer(false);\n }\n };\n\n const newMenu = (\n \n );\n\n return (\n <>\n \n \n \n {showNewFolderDrawer && (\n setShowNewFolderDrawer(false)}\n size=\"sm\"\n >\n setShowNewFolderDrawer(false)} />\n \n )}\n >\n );\n}\n\n/**\n *\n * @param url without any parameters\n * @param folderUid folder id\n * @returns url with paramter if folder is present\n */\nfunction buildUrl(url: string, folderUid: string | undefined) {\n const baseUrl = folderUid ? url + '?folderUid=' + folderUid : url;\n return config.appSubUrl ? config.appSubUrl + baseUrl : baseUrl;\n}\n","import React, { useCallback } from 'react';\n\nimport { DataFrameView, toDataFrame } from '@grafana/data';\nimport { Button, Card } from '@grafana/ui';\nimport { Trans } from 'app/core/internationalization';\nimport { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection';\nimport { SearchResultsProps, SearchResultsTable } from 'app/features/search/page/components/SearchResultsTable';\nimport { useSearchStateManager } from 'app/features/search/state/SearchStateManager';\nimport { DashboardViewItemKind } from 'app/features/search/types';\nimport { useDispatch, useSelector } from 'app/types';\n\nimport { setAllSelection, setItemSelectionState, useHasSelection } from '../state';\n\ninterface SearchViewProps {\n height: number;\n width: number;\n canSelect: boolean;\n}\n\nconst NUM_PLACEHOLDER_ROWS = 50;\nconst initialLoadingView = {\n view: new DataFrameView(\n toDataFrame({\n fields: [\n { name: 'uid', display: true, values: Array(NUM_PLACEHOLDER_ROWS).fill(null) },\n { name: 'kind', display: true, values: Array(NUM_PLACEHOLDER_ROWS).fill('dashboard') },\n { name: 'name', display: true, values: Array(NUM_PLACEHOLDER_ROWS).fill('') },\n { name: 'location', display: true, values: Array(NUM_PLACEHOLDER_ROWS).fill('') },\n { name: 'tags', display: true, values: Array(NUM_PLACEHOLDER_ROWS).fill([]) },\n ],\n meta: {\n custom: {\n locationInfo: [],\n },\n },\n })\n ),\n loadMoreItems: () => Promise.resolve(),\n // this is key and controls whether to show the skeleton in generateColumns\n isItemLoaded: () => false,\n totalRows: NUM_PLACEHOLDER_ROWS,\n};\n\nexport function SearchView({ width, height, canSelect }: SearchViewProps) {\n const dispatch = useDispatch();\n const selectedItems = useSelector((wholeState) => wholeState.browseDashboards.selectedItems);\n const hasSelection = useHasSelection();\n\n const { keyboardEvents } = useKeyNavigationListener();\n const [searchState, stateManager] = useSearchStateManager();\n\n const value = searchState.result ?? initialLoadingView;\n\n const selectionChecker = useCallback(\n (kind: string | undefined, uid: string): boolean => {\n if (!kind) {\n return false;\n }\n\n // Currently, this indicates _some_ items are selected, not nessicarily all are\n // selected.\n if (kind === '*' && uid === '*') {\n return hasSelection;\n } else if (kind === '*') {\n // Unsure how this case can happen\n return false;\n }\n\n return selectedItems[assertDashboardViewItemKind(kind)][uid] ?? false;\n },\n [selectedItems, hasSelection]\n );\n\n const clearSelection = useCallback(() => {\n dispatch(setAllSelection({ isSelected: false, folderUID: undefined }));\n }, [dispatch]);\n\n const handleItemSelectionChange = useCallback(\n (kind: string, uid: string) => {\n const newIsSelected = !selectionChecker(kind, uid);\n\n dispatch(\n setItemSelectionState({ item: { kind: assertDashboardViewItemKind(kind), uid }, isSelected: newIsSelected })\n );\n },\n [selectionChecker, dispatch]\n );\n\n if (value.totalRows === 0) {\n return (\n \n \n \n No results found for your query.\n \n \n \n \n \n
\n );\n }\n\n const props: SearchResultsProps = {\n response: value,\n selection: canSelect ? selectionChecker : undefined,\n selectionToggle: canSelect ? handleItemSelectionChange : undefined,\n clearSelection,\n width: width,\n height: height,\n onTagSelected: stateManager.onAddTag,\n keyboardEvents,\n onDatasourceChange: searchState.datasource ? stateManager.onDatasourceChange : undefined,\n onClickItem: stateManager.onSearchItemClicked,\n };\n\n return ;\n}\n\nfunction assertDashboardViewItemKind(kind: string): DashboardViewItemKind {\n switch (kind) {\n case 'folder':\n return 'folder';\n case 'dashboard':\n return 'dashboard';\n case 'panel':\n return 'panel';\n }\n\n throw new Error('Unsupported kind' + kind);\n}\n","import { css } from '@emotion/css';\nimport React, { memo, useEffect, useMemo } from 'react';\nimport AutoSizer from 'react-virtualized-auto-sizer';\n\nimport { GrafanaTheme2 } from '@grafana/data';\nimport { reportInteraction } from '@grafana/runtime';\nimport { FilterInput, useStyles2 } from '@grafana/ui';\nimport { Page } from 'app/core/components/Page/Page';\nimport { GrafanaRouteComponentProps } from 'app/core/navigation/types';\nimport { useDispatch } from 'app/types';\n\nimport { buildNavModel, getDashboardsTabID } from '../folders/state/navModel';\nimport { useSearchStateManager } from '../search/state/SearchStateManager';\nimport { getSearchPlaceholder } from '../search/tempI18nPhrases';\n\nimport { skipToken, useGetFolderQuery, useSaveFolderMutation } from './api/browseDashboardsAPI';\nimport { BrowseActions } from './components/BrowseActions/BrowseActions';\nimport { BrowseFilters } from './components/BrowseFilters';\nimport { BrowseView } from './components/BrowseView';\nimport CreateNewButton from './components/CreateNewButton';\nimport { FolderActionsButton } from './components/FolderActionsButton';\nimport { SearchView } from './components/SearchView';\nimport { getFolderPermissions } from './permissions';\nimport { setAllSelection, useHasSelection } from './state';\n\nexport interface BrowseDashboardsPageRouteParams {\n uid?: string;\n slug?: string;\n}\n\nexport interface Props extends GrafanaRouteComponentProps {}\n\n// New Browse/Manage/Search Dashboards views for nested folders\n\nconst BrowseDashboardsPage = memo(({ match }: Props) => {\n const { uid: folderUID } = match.params;\n const dispatch = useDispatch();\n\n const styles = useStyles2(getStyles);\n const [searchState, stateManager] = useSearchStateManager();\n const isSearching = stateManager.hasSearchFilters();\n\n useEffect(() => {\n stateManager.initStateFromUrl(folderUID);\n\n // Clear selected state when folderUID changes\n dispatch(\n setAllSelection({\n isSelected: false,\n folderUID: undefined,\n })\n );\n }, [dispatch, folderUID, stateManager]);\n\n useEffect(() => {\n // Clear the search results when we leave SearchView to prevent old results flashing\n // when starting a new search\n if (!isSearching && searchState.result) {\n stateManager.setState({ result: undefined, includePanels: undefined });\n }\n }, [isSearching, searchState.result, stateManager]);\n\n const { data: folderDTO } = useGetFolderQuery(folderUID ?? skipToken);\n const [saveFolder] = useSaveFolderMutation();\n const navModel = useMemo(() => {\n if (!folderDTO) {\n return undefined;\n }\n const model = buildNavModel(folderDTO);\n\n // Set the \"Dashboards\" tab to active\n const dashboardsTabID = getDashboardsTabID(folderDTO.uid);\n const dashboardsTab = model.children?.find((child) => child.id === dashboardsTabID);\n if (dashboardsTab) {\n dashboardsTab.active = true;\n }\n return model;\n }, [folderDTO]);\n\n const hasSelection = useHasSelection();\n\n const { canEditFolders, canEditDashboards, canCreateDashboards, canCreateFolders } = getFolderPermissions(folderDTO);\n\n const showEditTitle = canEditFolders && folderUID;\n const canSelect = canEditFolders || canEditDashboards;\n const onEditTitle = async (newValue: string) => {\n if (folderDTO) {\n const result = await saveFolder({\n ...folderDTO,\n title: newValue,\n });\n if ('error' in result) {\n reportInteraction('grafana_browse_dashboards_page_edit_folder_name', {\n status: 'failed_with_error',\n error: result.error,\n });\n throw result.error;\n } else {\n reportInteraction('grafana_browse_dashboards_page_edit_folder_name', { status: 'success' });\n }\n } else {\n reportInteraction('grafana_browse_dashboards_page_edit_folder_name', { status: 'failed_no_folderDTO' });\n }\n };\n\n return (\n \n {folderDTO && }\n {(canCreateDashboards || canCreateFolders) && (\n \n )}\n >\n }\n >\n \n stateManager.onQueryChange(e)}\n />\n\n {hasSelection ? : }\n\n \n
\n {({ width, height }) =>\n isSearching ? (\n \n ) : (\n \n )\n }\n \n
\n \n \n );\n});\n\nconst getStyles = (theme: GrafanaTheme2) => ({\n pageContents: css({\n display: 'grid',\n gridTemplateRows: 'auto auto 1fr',\n height: '100%',\n rowGap: theme.spacing(1),\n }),\n\n // AutoSizer needs an element to measure the full height available\n subView: css({\n height: '100%',\n }),\n});\n\nBrowseDashboardsPage.displayName = 'BrowseDashboardsPage';\nexport default BrowseDashboardsPage;\n","import React, { useState } from 'react';\n\nimport { config } from '@grafana/runtime';\nimport { Alert, ConfirmModal, Text, Space } from '@grafana/ui';\nimport { Trans, t } from 'app/core/internationalization';\n\nimport { useGetAffectedItemsQuery } from '../../api/browseDashboardsAPI';\nimport { DashboardTreeSelection } from '../../types';\n\nimport { DescendantCount } from './DescendantCount';\n\nexport interface Props {\n isOpen: boolean;\n onConfirm: () => Promise;\n onDismiss: () => void;\n selectedItems: DashboardTreeSelection;\n}\n\nexport const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {\n const { data } = useGetAffectedItemsQuery(selectedItems);\n const deleteIsInvalid = !config.featureToggles.nestedFolders && data && (data.alertRule || data.libraryPanel);\n const [isDeleting, setIsDeleting] = useState(false);\n const onDelete = async () => {\n setIsDeleting(true);\n try {\n await onConfirm();\n setIsDeleting(false);\n onDismiss();\n } catch {\n setIsDeleting(false);\n }\n };\n\n return (\n \n \n \n This action will delete the following content:\n \n \n \n \n >\n }\n description={\n <>\n {deleteIsInvalid ? (\n \n \n One or more folders contain library panels or alert rules. Delete these first in order to proceed.\n \n \n ) : null}\n >\n }\n confirmationText=\"Delete\"\n confirmText={\n isDeleting\n ? t('browse-dashboards.action.deleting', 'Deleting...')\n : t('browse-dashboards.action.delete-button', 'Delete')\n }\n onDismiss={onDismiss}\n onConfirm={onDelete}\n title={t('browse-dashboards.action.delete-modal-title', 'Delete')}\n {...props}\n />\n );\n};\n","import React, { useState } from 'react';\n\nimport { Alert, Button, Field, Modal, Text, Space } from '@grafana/ui';\nimport { FolderPicker } from 'app/core/components/Select/FolderPicker';\nimport { t, Trans } from 'app/core/internationalization';\n\nimport { DashboardTreeSelection } from '../../types';\n\nimport { DescendantCount } from './DescendantCount';\n\nexport interface Props {\n isOpen: boolean;\n onConfirm: (targetFolderUid: string) => Promise;\n onDismiss: () => void;\n selectedItems: DashboardTreeSelection;\n}\n\nexport const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {\n const [moveTarget, setMoveTarget] = useState();\n const [isMoving, setIsMoving] = useState(false);\n const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);\n\n const onMove = async () => {\n if (moveTarget !== undefined) {\n setIsMoving(true);\n try {\n await onConfirm(moveTarget);\n setIsMoving(false);\n onDismiss();\n } catch {\n setIsMoving(false);\n }\n }\n };\n\n return (\n \n {selectedFolders.length > 0 && (\n \n )}\n\n \n This action will move the following content:\n \n\n \n\n \n\n \n \n \n\n \n \n \n \n \n );\n};\n","import React, { useState } from 'react';\n\nimport { config, locationService, reportInteraction } from '@grafana/runtime';\nimport { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';\nimport { Permissions } from 'app/core/components/AccessControl';\nimport { appEvents } from 'app/core/core';\nimport { t, Trans } from 'app/core/internationalization';\nimport { FolderDTO } from 'app/types';\nimport { ShowModalReactEvent } from 'app/types/events';\n\nimport { useDeleteFolderMutation, useMoveFolderMutation } from '../api/browseDashboardsAPI';\nimport { getFolderPermissions } from '../permissions';\n\nimport { DeleteModal } from './BrowseActions/DeleteModal';\nimport { MoveModal } from './BrowseActions/MoveModal';\n\ninterface Props {\n folder: FolderDTO;\n}\n\nexport function FolderActionsButton({ folder }: Props) {\n const [isOpen, setIsOpen] = useState(false);\n const [showPermissionsDrawer, setShowPermissionsDrawer] = useState(false);\n const [moveFolder] = useMoveFolderMutation();\n const [deleteFolder] = useDeleteFolderMutation();\n const { canEditFolders, canDeleteFolders, canViewPermissions, canSetPermissions } = getFolderPermissions(folder);\n // Can only move folders when nestedFolders is enabled\n const canMoveFolder = config.featureToggles.nestedFolders && canEditFolders;\n\n const onMove = async (destinationUID: string) => {\n await moveFolder({ folder, destinationUID });\n reportInteraction('grafana_manage_dashboards_item_moved', {\n item_counts: {\n folder: 1,\n dashboard: 0,\n },\n source: 'folder_actions',\n });\n };\n\n const onDelete = async () => {\n await deleteFolder(folder);\n reportInteraction('grafana_manage_dashboards_item_deleted', {\n item_counts: {\n folder: 1,\n dashboard: 0,\n },\n source: 'folder_actions',\n });\n const { parents } = folder;\n const parentUrl = parents && parents.length ? parents[parents.length - 1].url : '/dashboards';\n locationService.push(parentUrl);\n };\n\n const showMoveModal = () => {\n appEvents.publish(\n new ShowModalReactEvent({\n component: MoveModal,\n props: {\n selectedItems: {\n folder: { [folder.uid]: true },\n dashboard: {},\n panel: {},\n $all: false,\n },\n onConfirm: onMove,\n },\n })\n );\n };\n\n const showDeleteModal = () => {\n appEvents.publish(\n new ShowModalReactEvent({\n component: DeleteModal,\n props: {\n selectedItems: {\n folder: { [folder.uid]: true },\n dashboard: {},\n panel: {},\n $all: false,\n },\n onConfirm: onDelete,\n },\n })\n );\n };\n\n const managePermissionsLabel = t('browse-dashboards.folder-actions-button.manage-permissions', 'Manage permissions');\n const moveLabel = t('browse-dashboards.folder-actions-button.move', 'Move');\n const deleteLabel = t('browse-dashboards.folder-actions-button.delete', 'Delete');\n\n const menu = (\n \n );\n\n if (!canViewPermissions && !canMoveFolder && !canDeleteFolders) {\n return null;\n }\n\n return (\n <>\n \n \n \n {showPermissionsDrawer && (\n setShowPermissionsDrawer(false)}\n size=\"md\"\n >\n \n \n )}\n >\n );\n}\n","import { config } from '@grafana/runtime';\nimport { contextSrv } from 'app/core/core';\nimport { AccessControlAction, FolderDTO } from 'app/types';\n\nfunction checkFolderPermission(action: AccessControlAction, folderDTO?: FolderDTO) {\n return folderDTO ? contextSrv.hasPermissionInMetadata(action, folderDTO) : contextSrv.hasPermission(action);\n}\n\nexport function getFolderPermissions(folderDTO?: FolderDTO) {\n // Can only create a folder if we have permissions and either we're at root or nestedFolders is enabled\n const canCreateDashboards = checkFolderPermission(AccessControlAction.DashboardsCreate, folderDTO);\n const canCreateFolders = Boolean(\n (!folderDTO || config.featureToggles.nestedFolders) && checkFolderPermission(AccessControlAction.FoldersCreate)\n );\n const canDeleteFolders = checkFolderPermission(AccessControlAction.FoldersDelete, folderDTO);\n const canEditDashboards = checkFolderPermission(AccessControlAction.DashboardsWrite, folderDTO);\n const canEditFolders = checkFolderPermission(AccessControlAction.FoldersWrite, folderDTO);\n const canSetPermissions = checkFolderPermission(AccessControlAction.FoldersPermissionsWrite, folderDTO);\n const canViewPermissions = checkFolderPermission(AccessControlAction.FoldersPermissionsRead, folderDTO);\n\n return {\n canCreateDashboards,\n canCreateFolders,\n canDeleteFolders,\n canEditDashboards,\n canEditFolders,\n canSetPermissions,\n canViewPermissions,\n };\n}\n","// Temporary place to collect phrases we reuse between new and old browse/search\n// TODO: remove this when new Browse Dashboards UI is no longer feature flagged\n\nimport { t } from 'app/core/internationalization';\n\nexport function getSearchPlaceholder(includePanels = false) {\n return includePanels\n ? t('search.search-input.include-panels-placeholder', 'Search for dashboards, folders, and panels')\n : t('search.search-input.placeholder', 'Search for dashboards and folders');\n}\n\nexport function getNewDashboardPhrase() {\n return t('search.dashboard-actions.new-dashboard', 'New dashboard');\n}\n\nexport function getNewFolderPhrase() {\n return t('search.dashboard-actions.new-folder', 'New folder');\n}\n\nexport function getImportPhrase() {\n return t('search.dashboard-actions.import', 'Import');\n}\n\nexport function getNewPhrase() {\n return t('search.dashboard-actions.new', 'New');\n}\n"],"names":["defaultSortOptionsGetter","SortPicker","onChange","value","placeholder","filter","getSortOptions","isClearable","options","vals","v","isDesc","opt","initialState","defaultQueryParams","getLocalStorageLayout","SearchStateManager","StateManagerBase","query","tagToRemove","tag","tags","newTag","datasource","panel_type","e","starred","sort","layout","includePanels","store","folderUid","doInitialSearch","stateFromUrl","prevSort","stateManager","state","q","trackingInfo","searcher","searchTimestamp","result","error","getSearchStateManager","useSearchStateManager","BrowseActions","styles","getStyles","dispatch","selectedItems","deleteItems","moveItems","moveIsInvalid","config","isSearching","onActionComplete","onDelete","trackAction","onMove","destinationUID","showMoveModal","MoveModal","showDeleteModal","DeleteModal","moveButton","Button","Tooltip","theme","actionMap","action","selectedDashboards","uid","selectedFolders","getLayoutOptions","getValidQueryLayout","ActionRow","onLayoutChange","onSortChange","onStarredFilterChange","onTagFilterChange","getTagOptions","sortPlaceholder","onDatasourceChange","onPanelTypeChange","onSetIncludePanels","showStarredFilter","hideLayout","disabledOptions","TagFilter","Checkbox","RadioButtonGroup","change","BrowseFilters","searchState","SelectionState","CheckboxCell","row","isSelected","onItemSelectionChange","item","CheckboxSpacer","selectors","ev","CheckboxHeaderCell","onAllSelectionChange","CHEVRON_SIZE","ICON_SIZE","NameCell","data","onFolderClick","treeID","level","isOpen","childrenByParentUID","isLoading","iconName","Indent","Text","IconButton","Spinner","Icon","Link","TagsCell","TagList","HEADER_HEIGHT","ROW_HEIGHT","DIVIDER_HEIGHT","DashboardsTree","items","width","height","isItemLoaded","requestLoadMore","canSelect","infiniteLoaderRef","listRef","tableColumns","checkboxColumn","nameColumn","props","tagsColumns","table","getTableProps","getTableBodyProps","headerGroups","virtualData","handleIsItemLoaded","itemIndex","handleLoadMore","startIndex","endIndex","parentUID","getRowHeight","rowIndex","headerGroup","key","headerGroupProps","column","headerProps","onItemsRendered","ref","elem","VirtualListRow","index","style","rows","prepareRow","dashboardItem","cell","cellProps","BrowseView","folderUID","status","flatTree","handleFolderClick","clickedFolderUID","handleItemSelectionChange","selection","hasSelectedDescendants","treeItem","EmptyListCTA","CallToActionCard","newState","collection","thisIsSelected","initialFormModel","NewFolderForm","onCancel","onConfirm","translatedFolderNameRequiredPhrase","validateFolderName","folderName","fieldNameLabel","Form","form","register","errors","Field","Input","CreateNewButton","parentFolder","canCreateDashboard","canCreateFolder","setIsOpen","location","newFolder","showNewFolderDrawer","setShowNewFolderDrawer","onCreateFolder","depth","newMenu","Menu","MenuItem","buildUrl","Dropdown","Drawer","url","baseUrl","NUM_PLACEHOLDER_ROWS","initialLoadingView","DataFrameView","SearchView","wholeState","hasSelection","keyboardEvents","selectionChecker","kind","assertDashboardViewItemKind","clearSelection","newIsSelected","Card","SearchResultsTable","BrowseDashboardsPage","match","folderDTO","saveFolder","navModel","model","dashboardsTabID","dashboardsTab","child","canEditFolders","canEditDashboards","canCreateDashboards","canCreateFolders","showEditTitle","onEditTitle","newValue","Page","FolderActionsButton","FilterInput","onDismiss","deleteIsInvalid","isDeleting","setIsDeleting","moveTarget","setMoveTarget","isMoving","setIsMoving","folder","showPermissionsDrawer","setShowPermissionsDrawer","moveFolder","deleteFolder","canDeleteFolders","canViewPermissions","canSetPermissions","canMoveFolder","parents","parentUrl","managePermissionsLabel","moveLabel","deleteLabel","menu","checkFolderPermission","getFolderPermissions","getSearchPlaceholder","getNewDashboardPhrase","getNewFolderPhrase","getImportPhrase","getNewPhrase"],"sourceRoot":""}