Add Push button on provisioning page to auto-select unmanaged resources
- Add Push button in RepositoryList that appears when unmanaged resources exist - Create useAutoSelectUnmanagedDashboards hook to programmatically select unmanaged dashboards - Update BrowseActions to handle autoPush URL parameter for auto-selection flow - When Push button is clicked, navigate to dashboards page with autoPush=true - Auto-select all unmanaged dashboards and open push drawer - Add translation for 'Push unmanaged resources' button
This commit is contained in:
@@ -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 }));
|
||||
|
||||
|
||||
@@ -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) {
|
||||
</>
|
||||
)}
|
||||
</Alert>
|
||||
{unmanagedCount > 0 && (
|
||||
<Box>
|
||||
<Button onClick={handlePushUnmanaged} variant="primary">
|
||||
<Trans i18nKey="provisioning.folder-repository-list.push-unmanaged-button">
|
||||
Push unmanaged resources
|
||||
</Trans>
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<boolean> => {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user