mirror of https://github.com/grafana/grafana.git
parent
60a10be931
commit
20d6702e50
|
@ -37,3 +37,15 @@ export const handleError = (e: unknown, dispatch: ThunkDispatch, message: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function extractErrorMessage(error: unknown): string {
|
||||||
|
if (error && typeof error === 'object') {
|
||||||
|
if ('data' in error && error.data && typeof error.data === 'object' && 'message' in error.data) {
|
||||||
|
return String(error.data.message);
|
||||||
|
}
|
||||||
|
if ('message' in error) {
|
||||||
|
return String(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { useDeleteItemsMutation, useMoveItemsMutation } from '../../api/browseDa
|
||||||
import { useActionSelectionState } from '../../state/hooks';
|
import { useActionSelectionState } from '../../state/hooks';
|
||||||
import { setAllSelection } from '../../state/slice';
|
import { setAllSelection } from '../../state/slice';
|
||||||
import { DashboardTreeSelection } from '../../types';
|
import { DashboardTreeSelection } from '../../types';
|
||||||
|
import { BulkDeleteProvisionedResource } from '../BulkDeleteProvisionedResource';
|
||||||
|
|
||||||
import { DeleteModal } from './DeleteModal';
|
import { DeleteModal } from './DeleteModal';
|
||||||
import { MoveModal } from './MoveModal';
|
import { MoveModal } from './MoveModal';
|
||||||
|
@ -133,10 +134,14 @@ export function BrowseActions({ folderDTO }: Props) {
|
||||||
onClose={() => setShowBulkDeleteProvisionedResource(false)}
|
onClose={() => setShowBulkDeleteProvisionedResource(false)}
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
{/* TODO: Implement bulk delete for provisioned resources, PR will merge soon https://github.com/grafana/grafana/pull/107800 */}
|
<BulkDeleteProvisionedResource
|
||||||
<Trans i18nKey="browse-dashboards.action.bulk-delete-provisioned-resources-not-implemented">
|
selectedItems={selectedItems}
|
||||||
Bulk delete for provisioned resources is not implemented yet.
|
folderUid={folderDTO?.uid || ''}
|
||||||
</Trans>
|
onDismiss={() => {
|
||||||
|
setShowBulkDeleteProvisionedResource(false);
|
||||||
|
onActionComplete();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { t } from '@grafana/i18n';
|
||||||
|
import { Alert } from '@grafana/ui';
|
||||||
|
|
||||||
|
export type MoveResultFailed = {
|
||||||
|
status?: 'failed';
|
||||||
|
title?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BulkActionFailureBanner({ result, onDismiss }: { result: MoveResultFailed[]; onDismiss: () => void }) {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
title={t('browse-dashboards.bulk-action-resources-form.failed-alert', '{{count}} items failed', {
|
||||||
|
count: result.length,
|
||||||
|
})}
|
||||||
|
onRemove={onDismiss}
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
{result.map((item) => (
|
||||||
|
<li key={item.title}>
|
||||||
|
<strong>{item.title}</strong>
|
||||||
|
{item.errorMessage && `: ${item.errorMessage}`}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Trans } from '@grafana/i18n';
|
||||||
|
import { Box, Text } from '@grafana/ui';
|
||||||
|
import ProgressBar from 'app/features/provisioning/Shared/ProgressBar';
|
||||||
|
|
||||||
|
export interface ProgressState {
|
||||||
|
current: number;
|
||||||
|
total: number;
|
||||||
|
item: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BulkActionProgress({ progress }: { progress: ProgressState }) {
|
||||||
|
const progressPercentage = Math.round((progress.current / progress.total) * 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Text>
|
||||||
|
<Trans
|
||||||
|
i18nKey="browse-dashboards.bulk-move-resources-form.progress"
|
||||||
|
defaults="Progress: {{current}} of {{total}}"
|
||||||
|
values={{ current: progress.current, total: progress.total }}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
<ProgressBar progress={progressPercentage} />
|
||||||
|
<Text variant="bodySmall" color="secondary">
|
||||||
|
{progress.item}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,259 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||||
|
|
||||||
|
import { AppEvents } from '@grafana/data';
|
||||||
|
import { Trans, t } from '@grafana/i18n';
|
||||||
|
import { getAppEvents } from '@grafana/runtime';
|
||||||
|
import { Box, Button, Stack } from '@grafana/ui';
|
||||||
|
import {
|
||||||
|
DeleteRepositoryFilesWithPathApiArg,
|
||||||
|
DeleteRepositoryFilesWithPathApiResponse,
|
||||||
|
RepositoryView,
|
||||||
|
useDeleteRepositoryFilesWithPathMutation,
|
||||||
|
} from 'app/api/clients/provisioning/v0alpha1';
|
||||||
|
import { extractErrorMessage } from 'app/api/utils';
|
||||||
|
import { AnnoKeySourcePath } from 'app/features/apiserver/types';
|
||||||
|
import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields';
|
||||||
|
import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/dashboard-scene/saving/provisioned/defaults';
|
||||||
|
import { generateTimestamp } from 'app/features/dashboard-scene/saving/provisioned/utils/timestamp';
|
||||||
|
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
|
||||||
|
import { WorkflowOption } from 'app/features/provisioning/types';
|
||||||
|
import { useSelector } from 'app/types/store';
|
||||||
|
|
||||||
|
import { useChildrenByParentUIDState, rootItemsSelector } from '../state/hooks';
|
||||||
|
import { findItem } from '../state/utils';
|
||||||
|
import { DashboardTreeSelection } from '../types';
|
||||||
|
|
||||||
|
import { DescendantCount } from './BrowseActions/DescendantCount';
|
||||||
|
import { BulkActionFailureBanner, MoveResultFailed } from './BulkActionFailureBanner';
|
||||||
|
import { BulkActionProgress, ProgressState } from './BulkActionProgress';
|
||||||
|
import { collectSelectedItems, fetchProvisionedDashboardPath } from './utils';
|
||||||
|
|
||||||
|
interface BulkDeleteFormData {
|
||||||
|
comment: string;
|
||||||
|
ref: string;
|
||||||
|
workflow?: WorkflowOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormProps extends BulkDeleteProvisionResourceProps {
|
||||||
|
initialValues: BulkDeleteFormData;
|
||||||
|
repository: RepositoryView;
|
||||||
|
workflowOptions: Array<{ label: string; value: string }>;
|
||||||
|
isGitHub: boolean;
|
||||||
|
folderPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BulkDeleteProvisionResourceProps {
|
||||||
|
folderUid?: string;
|
||||||
|
selectedItems: Omit<DashboardTreeSelection, 'panel' | '$all'>;
|
||||||
|
onDismiss?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type BulkSuccessResponse = Array<{
|
||||||
|
index: number;
|
||||||
|
item: DeleteRepositoryFilesWithPathApiArg;
|
||||||
|
data: DeleteRepositoryFilesWithPathApiResponse;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
function FormContent({
|
||||||
|
initialValues,
|
||||||
|
selectedItems,
|
||||||
|
repository,
|
||||||
|
workflowOptions,
|
||||||
|
folderPath,
|
||||||
|
isGitHub,
|
||||||
|
onDismiss,
|
||||||
|
}: FormProps) {
|
||||||
|
const [deleteRepoFile, request] = useDeleteRepositoryFilesWithPathMutation();
|
||||||
|
const [progress, setProgress] = useState<ProgressState | null>(null);
|
||||||
|
const [failureResults, setFailureResults] = useState<MoveResultFailed[] | undefined>();
|
||||||
|
|
||||||
|
const methods = useForm<BulkDeleteFormData>({ defaultValues: initialValues });
|
||||||
|
const childrenByParentUID = useChildrenByParentUIDState();
|
||||||
|
const rootItems = useSelector(rootItemsSelector);
|
||||||
|
const { handleSubmit, watch } = methods;
|
||||||
|
const workflow = watch('workflow');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const getResourcePath = async (uid: string, isFolder: boolean): Promise<string | undefined> => {
|
||||||
|
const item = findItem(rootItems?.items || [], childrenByParentUID, uid);
|
||||||
|
if (!item) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return isFolder ? `${folderPath}/${item.title}/` : fetchProvisionedDashboardPath(uid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuccess = (successes: BulkSuccessResponse) => {
|
||||||
|
getAppEvents().publish({
|
||||||
|
type: AppEvents.alertSuccess.name,
|
||||||
|
payload: [
|
||||||
|
t('browse-dashboards.bulk-delete-resources-form.api-success', `Successfully deleted {{count}} items`, {
|
||||||
|
count: successes.length,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (workflow === 'branch') {
|
||||||
|
onDismiss?.();
|
||||||
|
const repoUrl = successes[0].data.urls?.repositoryURL;
|
||||||
|
if (repoUrl) {
|
||||||
|
navigate({ search: `?repo_url=${encodeURIComponent(repoUrl)}` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
onDismiss?.();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitForm = async (data: BulkDeleteFormData) => {
|
||||||
|
setFailureResults(undefined);
|
||||||
|
|
||||||
|
const targets = collectSelectedItems(selectedItems, childrenByParentUID, rootItems?.items || []);
|
||||||
|
|
||||||
|
if (targets.length > 0) {
|
||||||
|
setProgress({
|
||||||
|
current: 0,
|
||||||
|
total: targets.length,
|
||||||
|
item: targets[0].displayName || 'Unknown',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const successes: BulkSuccessResponse = [];
|
||||||
|
const failures: MoveResultFailed[] = [];
|
||||||
|
|
||||||
|
// Iterate through each selected item and delete it
|
||||||
|
// We want sequential processing to avoid overwhelming the API
|
||||||
|
for (let i = 0; i < targets.length; i++) {
|
||||||
|
const { uid, isFolder, displayName } = targets[i];
|
||||||
|
setProgress({
|
||||||
|
current: i,
|
||||||
|
total: targets.length,
|
||||||
|
item: displayName,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// get path in repository
|
||||||
|
const path = await getResourcePath(uid, isFolder);
|
||||||
|
if (!path) {
|
||||||
|
failures.push({
|
||||||
|
status: 'failed',
|
||||||
|
title: `${isFolder ? 'Folder' : 'Dashboard'}: ${displayName}`,
|
||||||
|
errorMessage: t('browse-dashboards.bulk-delete-resources-form.error-path-not-found', 'Path not found'),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// build params
|
||||||
|
const deleteParams: DeleteRepositoryFilesWithPathApiArg = {
|
||||||
|
name: repository.name,
|
||||||
|
path,
|
||||||
|
ref: workflow === 'write' ? undefined : data.ref,
|
||||||
|
message: data.comment || `Delete resource ${path}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// perform delete operation
|
||||||
|
const response = await deleteRepoFile(deleteParams).unwrap();
|
||||||
|
successes.push({ index: i, item: deleteParams, data: response });
|
||||||
|
} catch (error: unknown) {
|
||||||
|
failures.push({
|
||||||
|
status: 'failed',
|
||||||
|
title: `${isFolder ? 'Folder' : 'Dashboard'}: ${displayName}`,
|
||||||
|
errorMessage: extractErrorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgress({
|
||||||
|
current: i + 1,
|
||||||
|
total: targets.length,
|
||||||
|
item: targets[i + 1]?.displayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgress(null);
|
||||||
|
|
||||||
|
if (successes.length > 0 && failures.length === 0) {
|
||||||
|
handleSuccess(successes);
|
||||||
|
} else if (failures.length > 0) {
|
||||||
|
setFailureResults(failures);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form onSubmit={handleSubmit(handleSubmitForm)}>
|
||||||
|
<Stack direction="column" gap={2}>
|
||||||
|
<Box paddingBottom={2}>
|
||||||
|
<Trans i18nKey="browse-dashboards.bulk-delete-resources-form.delete-warning">
|
||||||
|
This will delete selected folders and their descendants. In total, this will affect:
|
||||||
|
</Trans>
|
||||||
|
<DescendantCount selectedItems={{ ...selectedItems, panel: {}, $all: false }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{failureResults && (
|
||||||
|
<BulkActionFailureBanner result={failureResults} onDismiss={() => setFailureResults(undefined)} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{progress && <BulkActionProgress progress={progress} />}
|
||||||
|
|
||||||
|
<ResourceEditFormSharedFields
|
||||||
|
resourceType="folder"
|
||||||
|
isNew={false}
|
||||||
|
workflow={workflow}
|
||||||
|
workflowOptions={workflowOptions}
|
||||||
|
isGitHub={isGitHub}
|
||||||
|
hidePath
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Button type="submit" disabled={request.isLoading || !!failureResults} variant="destructive">
|
||||||
|
{request.isLoading
|
||||||
|
? t('browse-dashboards.bulk-delete-resources-form.button-deleting', 'Deleting...')
|
||||||
|
: t('browse-dashboards.bulk-delete-resources-form.button-delete', 'Delete')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" fill="outline" onClick={onDismiss}>
|
||||||
|
<Trans i18nKey="browse-dashboards.bulk-delete-resources-form.button-cancel">Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BulkDeleteProvisionedResource({
|
||||||
|
folderUid,
|
||||||
|
selectedItems,
|
||||||
|
onDismiss,
|
||||||
|
}: BulkDeleteProvisionResourceProps) {
|
||||||
|
const { repository, folder } = useGetResourceRepositoryView({ folderName: folderUid });
|
||||||
|
|
||||||
|
const workflowOptions = getWorkflowOptions(repository);
|
||||||
|
const isGitHub = repository?.type === 'github';
|
||||||
|
const folderPath = folder?.metadata?.annotations?.[AnnoKeySourcePath] || '';
|
||||||
|
const timestamp = generateTimestamp();
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
comment: '',
|
||||||
|
ref: `bulk-delete/${timestamp}`,
|
||||||
|
workflow: getDefaultWorkflow(repository),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!repository) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormContent
|
||||||
|
selectedItems={selectedItems}
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
initialValues={initialValues}
|
||||||
|
repository={repository}
|
||||||
|
workflowOptions={workflowOptions}
|
||||||
|
isGitHub={isGitHub}
|
||||||
|
folderPath={folderPath}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -30,6 +30,13 @@ jest.mock('app/api/clients/provisioning/v0alpha1', () => ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
provisioningAPIv0alpha1: {
|
||||||
|
endpoints: {
|
||||||
|
listRepository: {
|
||||||
|
select: jest.fn(() => () => ({ data: { items: [] } })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../hooks/useProvisionedFolderFormData');
|
jest.mock('../hooks/useProvisionedFolderFormData');
|
||||||
|
|
|
@ -37,6 +37,13 @@ jest.mock('app/features/manage-dashboards/services/ValidationSrv', () => {
|
||||||
jest.mock('app/api/clients/provisioning/v0alpha1', () => {
|
jest.mock('app/api/clients/provisioning/v0alpha1', () => {
|
||||||
return {
|
return {
|
||||||
useCreateRepositoryFilesWithPathMutation: jest.fn(),
|
useCreateRepositoryFilesWithPathMutation: jest.fn(),
|
||||||
|
provisioningAPIv0alpha1: {
|
||||||
|
endpoints: {
|
||||||
|
listRepository: {
|
||||||
|
select: jest.fn().mockReturnValue(() => ({ data: { items: [] } })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequ
|
||||||
|
|
||||||
export function ProvisionedFolderPreviewBanner({ queryParams }: CommonBannerProps) {
|
export function ProvisionedFolderPreviewBanner({ queryParams }: CommonBannerProps) {
|
||||||
const provisioningEnabled = config.featureToggles.provisioning;
|
const provisioningEnabled = config.featureToggles.provisioning;
|
||||||
const { prURL, newPrURL } = usePullRequestParam();
|
const { prURL, newPrURL, repoURL } = usePullRequestParam();
|
||||||
|
|
||||||
if (!provisioningEnabled || 'kiosk' in queryParams) {
|
if (!provisioningEnabled || 'kiosk' in queryParams) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -19,5 +19,9 @@ export function ProvisionedFolderPreviewBanner({ queryParams }: CommonBannerProp
|
||||||
return <PreviewBannerViewPR prParam={newPrURL} isNewPr />;
|
return <PreviewBannerViewPR prParam={newPrURL} isNewPr />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (repoURL) {
|
||||||
|
return <PreviewBannerViewPR repoUrl={repoURL} behindBranch />;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
|
import { AnnoKeySourcePath } from 'app/features/apiserver/types';
|
||||||
|
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
|
||||||
|
import { DashboardViewItem } from 'app/features/search/types';
|
||||||
|
|
||||||
import { DashboardViewItemWithUIItems } from '../types';
|
import { useChildrenByParentUIDState } from '../state/hooks';
|
||||||
|
import { findItem } from '../state/utils';
|
||||||
|
import { DashboardTreeSelection, DashboardViewItemWithUIItems } from '../types';
|
||||||
|
|
||||||
export function makeRowID(baseId: string, item: DashboardViewItemWithUIItems) {
|
export function makeRowID(baseId: string, item: DashboardViewItemWithUIItems) {
|
||||||
return baseId + item.uid;
|
return baseId + item.uid;
|
||||||
|
@ -57,3 +62,46 @@ export function formatFolderName(folderName?: string): string {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch provisioned dashboard path in repository
|
||||||
|
export async function fetchProvisionedDashboardPath(uid: string): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
const dto = await getDashboardAPI().getDashboardDTO(uid);
|
||||||
|
const sourcePath =
|
||||||
|
'meta' in dto
|
||||||
|
? dto.meta.k8s?.annotations?.[AnnoKeySourcePath] || dto.meta.provisionedExternalId
|
||||||
|
: dto.metadata?.annotations?.[AnnoKeySourcePath];
|
||||||
|
return `${sourcePath}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching provisioned dashboard path:', error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect selected dashboard and folder from the DashboardTreeSelection
|
||||||
|
// This is used to prepare the items for bulk delete operation.
|
||||||
|
export function collectSelectedItems(
|
||||||
|
selectedItems: Omit<DashboardTreeSelection, 'panel' | '$all'>,
|
||||||
|
childrenByParentUID: ReturnType<typeof useChildrenByParentUIDState>,
|
||||||
|
rootItems: DashboardViewItem[] = []
|
||||||
|
) {
|
||||||
|
const targets: Array<{ uid: string; isFolder: boolean; displayName: string }> = [];
|
||||||
|
|
||||||
|
// folders
|
||||||
|
for (const [uid, selected] of Object.entries(selectedItems.folder)) {
|
||||||
|
if (selected) {
|
||||||
|
const item = findItem(rootItems, childrenByParentUID, uid);
|
||||||
|
targets.push({ uid, isFolder: true, displayName: item?.title || uid });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dashboards
|
||||||
|
for (const [uid, selected] of Object.entries(selectedItems.dashboard)) {
|
||||||
|
if (selected) {
|
||||||
|
const item = findItem(rootItems, childrenByParentUID, uid);
|
||||||
|
targets.push({ uid, isFolder: false, displayName: item?.title || uid });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
|
@ -7,14 +7,16 @@ import { commonAlertProps } from './DashboardPreviewBanner';
|
||||||
// TODO: We have this https://github.com/grafana/git-ui-sync-project/issues/166 to add more details about the PR.
|
// TODO: We have this https://github.com/grafana/git-ui-sync-project/issues/166 to add more details about the PR.
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
prParam: string;
|
prParam?: string;
|
||||||
isNewPr?: boolean;
|
isNewPr?: boolean;
|
||||||
|
behindBranch?: boolean;
|
||||||
|
repoUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description This component is used to display a banner when a provisioned dashboard/folder is created or loaded from a new branch in Github.
|
* @description This component is used to display a banner when a provisioned dashboard/folder is created or loaded from a new branch in Github.
|
||||||
*/
|
*/
|
||||||
export function PreviewBannerViewPR({ prParam, isNewPr }: Props) {
|
export function PreviewBannerViewPR({ prParam, isNewPr, behindBranch, repoUrl }: Props) {
|
||||||
const titleText = isNewPr
|
const titleText = isNewPr
|
||||||
? t(
|
? t(
|
||||||
'provisioned-resource-preview-banner.title-created-branch-git-hub',
|
'provisioned-resource-preview-banner.title-created-branch-git-hub',
|
||||||
|
@ -25,6 +27,29 @@ export function PreviewBannerViewPR({ prParam, isNewPr }: Props) {
|
||||||
'This resource is loaded from a pull request in GitHub.'
|
'This resource is loaded from a pull request in GitHub.'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (behindBranch) {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
{...commonAlertProps}
|
||||||
|
buttonContent={
|
||||||
|
<Stack alignItems="center">
|
||||||
|
{t('provisioned-resource-preview-banner.preview-banner.open-git-hub', 'Open in Github')}
|
||||||
|
<Icon name="external-link-alt" />
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
title={t(
|
||||||
|
'provisioned-resource-preview-banner.preview-banner.behind-branch',
|
||||||
|
'This resource is behind the branch in GitHub.'
|
||||||
|
)}
|
||||||
|
onRemove={repoUrl ? () => window.open(textUtil.sanitizeUrl(repoUrl), '_blank') : undefined}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="provisioned-resource-preview-banner.preview-banner.new-branch">
|
||||||
|
View it in GitHub to see the latest changes.
|
||||||
|
</Trans>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
{...commonAlertProps}
|
{...commonAlertProps}
|
||||||
|
@ -43,7 +68,7 @@ export function PreviewBannerViewPR({ prParam, isNewPr }: Props) {
|
||||||
<Icon name="external-link-alt" />
|
<Icon name="external-link-alt" />
|
||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
onRemove={() => window.open(textUtil.sanitizeUrl(prParam), '_blank')}
|
onRemove={prParam ? () => window.open(textUtil.sanitizeUrl(prParam), '_blank') : undefined}
|
||||||
>
|
>
|
||||||
<Trans i18nKey="provisioned-resource-preview-banner.preview-banner.not-saved">
|
<Trans i18nKey="provisioned-resource-preview-banner.preview-banner.not-saved">
|
||||||
The value is not yet saved in the Grafana database
|
The value is not yet saved in the Grafana database
|
||||||
|
|
|
@ -5,9 +5,11 @@ export const usePullRequestParam = () => {
|
||||||
const [params] = useUrlParams();
|
const [params] = useUrlParams();
|
||||||
const prParam = params.get('pull_request_url');
|
const prParam = params.get('pull_request_url');
|
||||||
const newPrParam = params.get('new_pull_request_url');
|
const newPrParam = params.get('new_pull_request_url');
|
||||||
|
const repoUrl = params.get('repo_url');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
prURL: prParam ? textUtil.sanitizeUrl(prParam) : undefined,
|
prURL: prParam ? textUtil.sanitizeUrl(prParam) : undefined,
|
||||||
newPrURL: newPrParam ? textUtil.sanitizeUrl(newPrParam) : undefined,
|
newPrURL: newPrParam ? textUtil.sanitizeUrl(newPrParam) : undefined,
|
||||||
|
repoURL: repoUrl ? textUtil.sanitizeUrl(repoUrl) : undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -3479,7 +3479,6 @@
|
||||||
"browse-dashboards": {
|
"browse-dashboards": {
|
||||||
"action": {
|
"action": {
|
||||||
"bulk-delete-provisioned-resources": "Bulk Delete Provisioned Resources",
|
"bulk-delete-provisioned-resources": "Bulk Delete Provisioned Resources",
|
||||||
"bulk-delete-provisioned-resources-not-implemented": "Bulk delete for provisioned resources is not implemented yet.",
|
|
||||||
"cancel-button": "Cancel",
|
"cancel-button": "Cancel",
|
||||||
"cannot-move-folders": "Folders cannot be moved",
|
"cannot-move-folders": "Folders cannot be moved",
|
||||||
"confirmation-text": "Delete",
|
"confirmation-text": "Delete",
|
||||||
|
@ -3512,6 +3511,22 @@
|
||||||
"browse-view": {
|
"browse-view": {
|
||||||
"this-folder-is-empty": "This folder is empty"
|
"this-folder-is-empty": "This folder is empty"
|
||||||
},
|
},
|
||||||
|
"bulk-action-resources-form": {
|
||||||
|
"failed-alert_one": "{{count}} item failed",
|
||||||
|
"failed-alert_other": "{{count}} items failed"
|
||||||
|
},
|
||||||
|
"bulk-delete-resources-form": {
|
||||||
|
"api-success_one": "Successfully deleted {{count}} item",
|
||||||
|
"api-success_other": "Successfully deleted {{count}} items",
|
||||||
|
"button-cancel": "Cancel",
|
||||||
|
"button-delete": "Delete",
|
||||||
|
"button-deleting": "Deleting...",
|
||||||
|
"delete-warning": "This will delete selected folders and their descendants. In total, this will affect:",
|
||||||
|
"error-path-not-found": "Path not found"
|
||||||
|
},
|
||||||
|
"bulk-move-resources-form": {
|
||||||
|
"progress": "Progress: {{current}} of {{total}}"
|
||||||
|
},
|
||||||
"counts": {
|
"counts": {
|
||||||
"alertRule_one": "{{count}} alert rule",
|
"alertRule_one": "{{count}} alert rule",
|
||||||
"alertRule_other": "{{count}} alert rule",
|
"alertRule_other": "{{count}} alert rule",
|
||||||
|
@ -10521,7 +10536,10 @@
|
||||||
},
|
},
|
||||||
"provisioned-resource-preview-banner": {
|
"provisioned-resource-preview-banner": {
|
||||||
"preview-banner": {
|
"preview-banner": {
|
||||||
|
"behind-branch": "This resource is behind the branch in GitHub.",
|
||||||
|
"new-branch": "View it in GitHub to see the latest changes.",
|
||||||
"not-saved": "The value is not yet saved in the Grafana database",
|
"not-saved": "The value is not yet saved in the Grafana database",
|
||||||
|
"open-git-hub": "Open in Github",
|
||||||
"open-pull-request-in-git-hub": "Open pull request in GitHub",
|
"open-pull-request-in-git-hub": "Open pull request in GitHub",
|
||||||
"view-pull-request-in-git-hub": "View pull request in GitHub"
|
"view-pull-request-in-git-hub": "View pull request in GitHub"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue