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."
},