diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx index 927a8ea7a79..bcdc073f900 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx @@ -1,13 +1,15 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import { useLocation } from 'react-router-dom-v5-compat'; import { Trans, t } from '@grafana/i18n'; -import { config, reportInteraction } from '@grafana/runtime'; +import { config, locationService, reportInteraction } from '@grafana/runtime'; import { Button, Drawer, Stack, Text } from '@grafana/ui'; import { appEvents } from 'app/core/app_events'; import { ManagerKind } from 'app/features/apiserver/types'; import { BulkDeleteProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkDeleteProvisionedResource'; import { BulkPushProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkPushProvisionedResource'; import { BulkMoveProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkMoveProvisionedResource'; +import { useAutoSelectUnmanagedDashboards } from 'app/features/provisioning/hooks/useAutoSelectUnmanagedDashboards'; import { useSelectionProvisioningStatus } from 'app/features/provisioning/hooks/useSelectionProvisioningStatus'; import { useSelectionUnmanagedStatus } from 'app/features/provisioning/hooks/useSelectionUnmanagedStatus'; import { useSearchStateManager } from 'app/features/search/state/SearchStateManager'; @@ -45,6 +47,8 @@ export function BrowseActions({ folderDTO }: Props) { const [moveDashboards] = useMoveDashboardsMutation(); const [, stateManager] = useSearchStateManager(); const provisioningEnabled = config.featureToggles.provisioning; + const location = useLocation(); + const selectAllUnmanagedDashboards = useAutoSelectUnmanagedDashboards(); const { hasProvisioned, hasNonProvisioned } = useSelectionProvisioningStatus( selectedItems, @@ -54,6 +58,37 @@ export function BrowseActions({ folderDTO }: Props) { const isSearching = stateManager.hasSearchFilters(); + // Handle autoPush URL parameter + useEffect(() => { + const searchParams = new URLSearchParams(location.search); + if (searchParams.get('autoPush') === 'true' && provisioningEnabled) { + // Remove the parameter from URL + searchParams.delete('autoPush'); + const newSearch = searchParams.toString(); + locationService.replace({ + pathname: location.pathname, + search: newSearch ? `?${newSearch}` : '', + }); + + // Wait for dashboards to load, then auto-select unmanaged dashboards and open push drawer + const attemptAutoSelect = async () => { + // Try multiple times with delays to ensure dashboards are loaded + for (let i = 0; i < 5; i++) { + await selectAllUnmanagedDashboards(); + await new Promise((resolve) => setTimeout(resolve, 300)); + } + + // Open the drawer after attempting to select + setShowBulkPushProvisionedResource(true); + }; + + // Start after a short delay to allow page to render + setTimeout(() => { + attemptAutoSelect(); + }, 500); + } + }, [location.search, location.pathname, provisioningEnabled, selectAllUnmanagedDashboards]); + const onActionComplete = () => { dispatch(setAllSelection({ isSelected: false, folderUID: undefined })); diff --git a/public/app/features/provisioning/Shared/RepositoryList.tsx b/public/app/features/provisioning/Shared/RepositoryList.tsx index 038359c6e8b..54609e0b944 100644 --- a/public/app/features/provisioning/Shared/RepositoryList.tsx +++ b/public/app/features/provisioning/Shared/RepositoryList.tsx @@ -1,7 +1,8 @@ import { useState } from 'react'; import { t, Trans } from '@grafana/i18n'; -import { Alert, Box, EmptyState, FilterInput, Icon, Stack, TextLink } from '@grafana/ui'; +import { locationService } from '@grafana/runtime'; +import { Alert, Box, Button, EmptyState, FilterInput, Icon, Stack, TextLink } from '@grafana/ui'; import { Repository } from 'app/api/clients/provisioning/v0alpha1'; import { RepositoryListItem } from '../Repository/RepositoryListItem'; @@ -23,6 +24,11 @@ export function RepositoryList({ items }: Props) { const filteredItems = items.filter((item) => item.metadata?.name?.includes(query)); const { instanceConnected } = checkSyncSettings(items); + const handlePushUnmanaged = () => { + // Navigate to dashboards page with autoPush parameter + locationService.push('/dashboards?autoPush=true'); + }; + const getResourceCountSection = () => { if (isProvisionedInstance) { return ( @@ -68,6 +74,15 @@ export function RepositoryList({ items }: Props) { )} + {unmanagedCount > 0 && ( + + + + )} ); } diff --git a/public/app/features/provisioning/hooks/useAutoSelectUnmanagedDashboards.ts b/public/app/features/provisioning/hooks/useAutoSelectUnmanagedDashboards.ts new file mode 100644 index 00000000000..0384843fc0f --- /dev/null +++ b/public/app/features/provisioning/hooks/useAutoSelectUnmanagedDashboards.ts @@ -0,0 +1,135 @@ +import { useCallback } from 'react'; + +import { config } from '@grafana/runtime'; +import { ManagerKind } from 'app/features/apiserver/types'; +import { isProvisionedDashboard as isProvisionedDashboardFromMeta } from 'app/features/browse-dashboards/api/isProvisioned'; +import { findItem } from 'app/features/browse-dashboards/state/utils'; +import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; +import { DashboardViewItem } from 'app/features/search/types'; +import { useSearchStateManager } from 'app/features/search/state/SearchStateManager'; +import { useDispatch, useSelector } from 'app/types/store'; + +import { setItemSelectionState } from '../../browse-dashboards/state/slice'; +import { BrowseDashboardsState } from '../../browse-dashboards/types'; + +/** + * Hook to auto-select all unmanaged dashboards when triggered + * Returns a function that can be called to select all unmanaged dashboards + */ +export function useAutoSelectUnmanagedDashboards() { + const dispatch = useDispatch(); + const browseState = useSelector((state) => state.browseDashboards); + const [, stateManager] = useSearchStateManager(); + const isSearching = stateManager.hasSearchFilters(); + const provisioningEnabled = config.featureToggles.provisioning; + + const findItemInState = useCallback( + (uid: string, state: BrowseDashboardsState) => { + const item = findItem(state.rootItems?.items || [], state.childrenByParentUID, uid); + return item ? { parentUID: item.parentUID, managedBy: item.managedBy } : undefined; + }, + [] + ); + + const getAllDashboards = useCallback( + (state: BrowseDashboardsState): DashboardViewItem[] => { + const dashboards: DashboardViewItem[] = []; + + // Helper to recursively collect dashboards from a collection + const collectDashboards = (collection: typeof state.rootItems, parentUID?: string) => { + if (!collection) { + return; + } + + for (const item of collection.items) { + if (item.kind === 'dashboard') { + dashboards.push(item); + } else if (item.kind === 'folder') { + // Recursively collect from children + const children = state.childrenByParentUID[item.uid]; + if (children) { + collectDashboards(children, item.uid); + } + } + } + }; + + // Start from root items + collectDashboards(state.rootItems); + + return dashboards; + }, + [] + ); + + const checkDashboardUnmanaged = useCallback( + async (dashboard: DashboardViewItem, state: BrowseDashboardsState): Promise => { + if (isSearching) { + // In search mode, fetch dashboard metadata + try { + const dto = await getDashboardAPI().getDashboardDTO(dashboard.uid); + return !isProvisionedDashboardFromMeta(dto); + } catch { + return false; + } + } + + // Check parent folder first + if (dashboard.parentUID) { + const parent = findItemInState(dashboard.parentUID, state); + if (parent?.managedBy === ManagerKind.Repo) { + // If parent is managed, dashboard is managed + return false; + } + } + + // Check dashboard itself + const item = findItemInState(dashboard.uid, state); + return item?.managedBy !== ManagerKind.Repo; + }, + [isSearching, findItemInState] + ); + + const selectAllUnmanagedDashboards = useCallback(async () => { + if (!provisioningEnabled) { + return; + } + + // Get current state at the time of execution + const currentState = browseState; + const allDashboards = getAllDashboards(currentState); + + if (allDashboards.length === 0) { + // No dashboards loaded yet, wait a bit and try again + return; + } + + const unmanagedDashboards: DashboardViewItem[] = []; + + // Check each dashboard to see if it's unmanaged + for (const dashboard of allDashboards) { + const isUnmanaged = await checkDashboardUnmanaged(dashboard, currentState); + if (isUnmanaged) { + unmanagedDashboards.push(dashboard); + } + } + + // Select all unmanaged dashboards + for (const dashboard of unmanagedDashboards) { + dispatch( + setItemSelectionState({ + item: { + kind: dashboard.kind, + uid: dashboard.uid, + parentUID: dashboard.parentUID, + managedBy: dashboard.managedBy, + }, + isSelected: true, + }) + ); + } + }, [provisioningEnabled, browseState, getAllDashboards, checkDashboardUnmanaged, dispatch]); + + return selectAllUnmanagedDashboards; +} + diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 432b1367d01..ae63276e237 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11750,6 +11750,7 @@ "no-results-matching-your-query": "No results matching your query", "partial-managed": "{{managedCount}}/{{resourceCount}} resources managed by Git sync.", "placeholder-search": "Search", + "push-unmanaged-button": "Push unmanaged resources", "unmanaged-resources_one": "{{count}} resource isn't managed by git sync.", "unmanaged-resources_other": "{{count}} resources aren't managed by git sync." },