PreviewBannerViewPR: Display different provider in preview banner (#108490)

* PreviewBannerViewPR: display different provider in preview banner, update copy
This commit is contained in:
Yunwen Zheng
2025-07-24 12:31:55 -04:00
committed by GitHub
parent 716ae2a266
commit 6b259435ee
15 changed files with 250 additions and 64 deletions
@@ -15,6 +15,7 @@ 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 { buildResourceBranchRedirectUrl } from 'app/features/dashboard-scene/settings/utils';
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
import { WorkflowOption } from 'app/features/provisioning/types';
import { useSelector } from 'app/types/store';
@@ -89,7 +90,13 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions
if (workflow === 'branch') {
onDismiss?.();
if (successState.repoUrl) {
navigate({ search: `?repo_url=${encodeURIComponent(successState.repoUrl)}` });
const url = buildResourceBranchRedirectUrl({
paramName: 'repo_url',
paramValue: successState.repoUrl,
repoType: repository.type,
});
navigate(url);
return;
}
window.location.reload();
@@ -297,9 +297,11 @@ describe('DeleteProvisionedFolderForm', () => {
const { mockNavigate } = setup({}, { ...defaultHookData, initialValues: branchFormData }, successState);
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith(
'/dashboards?new_pull_request_url=https://github.com/test/repo/pull/new'
);
const expectedParams = new URLSearchParams();
expectedParams.set('new_pull_request_url', 'https://github.com/test/repo/pull/new');
const expectedUrl = `/dashboards?${expectedParams.toString()}`;
expect(mockNavigate).toHaveBeenCalledWith(expectedUrl);
});
});
});
@@ -11,6 +11,7 @@ import { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/ap
import { AnnoKeySourcePath } from 'app/features/apiserver/types';
import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields';
import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared';
import { buildResourceBranchRedirectUrl } from 'app/features/dashboard-scene/settings/utils';
import { FolderDTO } from 'app/types/folders';
import { useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData';
@@ -61,7 +62,12 @@ function FormContent({ initialValues, parentFolder, repository, workflowOptions,
if (request.isSuccess && repository) {
const prUrl = request.data?.urls?.newPullRequestURL;
if (workflow === 'branch' && prUrl) {
navigate(`/dashboards?new_pull_request_url=${prUrl}`);
const url = buildResourceBranchRedirectUrl({
paramName: 'new_pull_request_url',
paramValue: prUrl,
repoType: request.data?.repository?.type,
});
navigate(url);
return;
}
@@ -12,6 +12,7 @@ import { RepositoryView, useCreateRepositoryFilesWithPathMutation } from 'app/ap
import { AnnoKeySourcePath, Resource } from 'app/features/apiserver/types';
import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields';
import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared';
import { buildResourceBranchRedirectUrl } from 'app/features/dashboard-scene/settings/utils';
import { PROVISIONING_URL } from 'app/features/provisioning/constants';
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
import { FolderDTO } from 'app/types/folders';
@@ -61,6 +62,17 @@ function FormContent({ initialValues, repository, workflowOptions, folder, onDis
],
});
const prUrl = request.data?.urls?.newPullRequestURL;
if (workflow === 'branch' && prUrl) {
const url = buildResourceBranchRedirectUrl({
paramName: 'new_pull_request_url',
paramValue: prUrl,
repoType: request.data?.repository?.type,
});
navigate(url);
return;
}
// TODO: Update when the upsert type is fixed
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const folder = request.data.resource?.upsert as Resource;
@@ -48,10 +48,10 @@ function DashboardPreviewBannerContent({ queryParams, slug, path }: DashboardPre
return <PreviewBannerViewPR prParam={prURL} />;
}
// Check if this is a GitHub link
const githubURL = file.data?.urls?.newPullRequestURL ?? file.data?.urls?.compareURL;
if (githubURL) {
return <PreviewBannerViewPR prParam={githubURL} isNewPr />;
// Check if this is a repo link
const repoUrl = file.data?.urls?.newPullRequestURL ?? file.data?.urls?.compareURL;
if (repoUrl) {
return <PreviewBannerViewPR prParam={repoUrl} isNewPr />;
}
return (
@@ -2,8 +2,10 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { textUtil } from '@grafana/data';
import { RepoType } from 'app/features/provisioning/Wizard/types';
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
import { PreviewBannerViewPR } from './PreviewBannerViewPR';
import { isValidRepoType, PreviewBannerViewPR } from './PreviewBannerViewPR';
jest.mock('@grafana/data', () => ({
...jest.requireActual('@grafana/data'),
@@ -12,23 +14,33 @@ jest.mock('@grafana/data', () => ({
},
}));
jest.mock('@grafana/i18n', () => ({
t: jest.fn((key: string, defaultValue: string) => defaultValue),
Trans: ({ children }: { children: React.ReactNode }) => children,
jest.mock('app/features/provisioning/hooks/usePullRequestParam', () => ({
usePullRequestParam: jest.fn(),
}));
const mockTextUtil = jest.mocked(textUtil);
function setup(props: { prParam: string; isFolder?: boolean; isNewPr?: boolean } = { prParam: 'test-url' }) {
const defaultProps = {
isFolder: false,
isNewPr: false,
...props,
const mockUsePullRequestParam = jest.mocked(usePullRequestParam);
function setup(
options: { prParam: string; isNewPr?: boolean; repoType?: RepoType } = { prParam: 'test-url', repoType: 'github' }
) {
const componentProps = {
prParam: options.prParam,
isNewPr: options.isNewPr || false,
};
const renderResult = render(<PreviewBannerViewPR {...defaultProps} />);
// Mock the hook BEFORE rendering the component
mockUsePullRequestParam.mockReturnValue({
prURL: undefined,
newPrURL: undefined,
repoURL: undefined,
repoType: options.repoType || 'github',
});
return { renderResult, props: defaultProps };
const renderResult = render(<PreviewBannerViewPR {...componentProps} />);
return { renderResult, props: componentProps };
}
describe('PreviewBannerViewPR', () => {
@@ -57,55 +69,63 @@ describe('PreviewBannerViewPR', () => {
describe('Dashboard scenarios', () => {
it('should render correct text for new PR dashboard', () => {
setup({ prParam: 'test-url', isFolder: false, isNewPr: true });
setup({ prParam: 'test-url', isNewPr: true });
expect(screen.getByRole('status')).toBeInTheDocument();
expect(screen.getByText('A new resource has been created in a branch in GitHub.')).toBeInTheDocument();
});
it('should render correct text for existing PR dashboard', () => {
setup({ prParam: 'test-url', isFolder: false, isNewPr: false });
setup({ prParam: 'test-url', isNewPr: false });
expect(screen.getByRole('status')).toBeInTheDocument();
expect(screen.getByText('This resource is loaded from a pull request in GitHub.')).toBeInTheDocument();
expect(
screen.getByText(
'The rest of Grafana users in your organization will still see the current version saved to configured default branch until this branch is merged'
)
).toBeInTheDocument();
});
it('should render correct button text for new PR dashboard', () => {
setup({ prParam: 'test-url', isFolder: false, isNewPr: true });
setup({ prParam: 'test-url', isNewPr: true });
expect(screen.getByText('Open pull request in GitHub')).toBeInTheDocument();
});
it('should render correct button text for existing PR dashboard', () => {
setup({ prParam: 'test-url', isFolder: false, isNewPr: false });
setup({ prParam: 'test-url', isNewPr: false });
expect(screen.getByText('View pull request in GitHub')).toBeInTheDocument();
});
});
describe('Folder scenarios', () => {
it('should render correct text for new PR folder', () => {
setup({ prParam: 'test-url', isFolder: true, isNewPr: true });
describe('Additional scenarios', () => {
it('should render correct text for new PR resource', () => {
setup({ prParam: 'test-url', isNewPr: true });
expect(screen.getByRole('status')).toBeInTheDocument();
expect(screen.getByText('A new resource has been created in a branch in GitHub.')).toBeInTheDocument();
});
it('should render correct text for existing PR folder', () => {
setup({ prParam: 'test-url', isFolder: true, isNewPr: false });
it('should render correct text for existing PR resource', () => {
setup({ prParam: 'test-url', isNewPr: false });
expect(screen.getByRole('status')).toBeInTheDocument();
expect(screen.getByText('This resource is loaded from a pull request in GitHub.')).toBeInTheDocument();
expect(
screen.getByText(
'This resource is loaded from the branch you just created in GitHub and it is only visible to you'
)
).toBeInTheDocument();
});
it('should render correct button text for new PR folder', () => {
setup({ prParam: 'test-url', isFolder: true, isNewPr: true });
it('should render correct button text for new PR resource', () => {
setup({ prParam: 'test-url', isNewPr: true });
expect(screen.getByText('Open pull request in GitHub')).toBeInTheDocument();
});
it('should render correct button text for existing PR folder', () => {
setup({ prParam: 'test-url', isFolder: true, isNewPr: false });
it('should render correct button text for existing PR resource', () => {
setup({ prParam: 'test-url', isNewPr: false });
expect(screen.getByText('View pull request in GitHub')).toBeInTheDocument();
});
@@ -113,7 +133,7 @@ describe('PreviewBannerViewPR', () => {
describe('Button functionality', () => {
it('should open URL in new tab when button is clicked', async () => {
const testUrl = 'https://github.com/test/repo/pull/123';
const testUrl = 'https://GitHub.com/test/repo/pull/123';
setup({ prParam: testUrl });
const button = screen.getByRole('button', { name: /close alert/i });
@@ -122,4 +142,46 @@ describe('PreviewBannerViewPR', () => {
expect(windowOpenSpy).toHaveBeenCalledWith(testUrl, '_blank');
});
});
describe('Different repository types', () => {
it('should handle GitLab repository type', () => {
setup({ prParam: 'test-url', isNewPr: false, repoType: 'gitlab' });
expect(screen.getByRole('status')).toBeInTheDocument();
expect(
screen.getByText(
'This resource is loaded from the branch you just created in GitLab and it is only visible to you'
)
).toBeInTheDocument();
});
it('should handle Bitbucket repository type', () => {
setup({ prParam: 'test-url', isNewPr: false, repoType: 'bitbucket' });
expect(screen.getByRole('status')).toBeInTheDocument();
expect(
screen.getByText(
'This resource is loaded from the branch you just created in Bitbucket and it is only visible to you'
)
).toBeInTheDocument();
});
});
});
describe('isValidRepoType', () => {
it('should return true for valid repo types', () => {
expect(isValidRepoType('github')).toBe(true);
expect(isValidRepoType('gitlab')).toBe(true);
expect(isValidRepoType('bitbucket')).toBe(true);
expect(isValidRepoType('git')).toBe(true);
});
it('should return false for invalid repo types', () => {
expect(isValidRepoType('unknown')).toBe(false);
expect(isValidRepoType('apple')).toBe(false);
expect(isValidRepoType('')).toBe(false);
expect(isValidRepoType(undefined)).toBe(false);
// @ts-expect-error testing invalid type
expect(isValidRepoType(null)).toBe(false);
});
});
@@ -1,6 +1,8 @@
import { textUtil } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { Alert, Icon, Stack } from '@grafana/ui';
import { RepoTypeDisplay, RepoType } from 'app/features/provisioning/Wizard/types';
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
import { commonAlertProps } from './DashboardPreviewBanner';
@@ -14,17 +16,27 @@ interface Props {
}
/**
* @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 repo.
*/
export function PreviewBannerViewPR({ prParam, isNewPr, behindBranch, repoUrl }: Props) {
const { repoType } = usePullRequestParam();
const capitalizedRepoType = isValidRepoType(repoType) ? RepoTypeDisplay[repoType] : 'repository';
const titleText = isNewPr
? t(
'provisioned-resource-preview-banner.title-created-branch-git-hub',
'A new resource has been created in a branch in GitHub.'
'provisioned-resource-preview-banner.title-created-branch-in-repo',
'A new resource has been created in a branch in {{repoType}}.',
{
repoType: capitalizedRepoType,
}
)
: t(
'provisioned-resource-preview-banner.title-loaded-pull-request-git-hub',
'This resource is loaded from a pull request in GitHub.'
'provisioned-resource-preview-banner.title-loaded-pull-request-in-repo',
'This resource is loaded from the branch you just created in {{repoType}} and it is only visible to you',
{
repoType: capitalizedRepoType,
}
);
if (behindBranch) {
@@ -33,18 +45,26 @@ export function PreviewBannerViewPR({ prParam, isNewPr, behindBranch, repoUrl }:
{...commonAlertProps}
buttonContent={
<Stack alignItems="center">
{t('provisioned-resource-preview-banner.preview-banner.open-git-hub', 'Open in Github')}
{t('provisioned-resource-preview-banner.preview-banner.open-in-repo-button', 'Open in {{repoType}}', {
repoType: capitalizedRepoType,
})}
<Icon name="external-link-alt" />
</Stack>
}
title={t(
'provisioned-resource-preview-banner.preview-banner.behind-branch',
'This resource is behind the branch in GitHub.'
'provisioned-resource-preview-banner.preview-banner.behind-branch-text',
'This resource is behind the branch in {{repoType}}.',
{
repoType: capitalizedRepoType,
}
)}
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
i18nKey="provisioned-resource-preview-banner.preview-banner.view-in-repo-button"
values={{ repoType: capitalizedRepoType }}
>
View it in {{ repoType }} to see the latest changes.
</Trans>
</Alert>
);
@@ -58,12 +78,14 @@ export function PreviewBannerViewPR({ prParam, isNewPr, behindBranch, repoUrl }:
<Stack alignItems="center">
{isNewPr
? t(
'provisioned-resource-preview-banner.preview-banner.open-pull-request-in-git-hub',
'Open pull request in GitHub'
'provisioned-resource-preview-banner.preview-banner.open-pull-request-in-repo',
'Open pull request in {{repoType}}',
{ repoType: capitalizedRepoType }
)
: t(
'provisioned-resource-preview-banner.preview-banner.view-pull-request-in-git-hub',
'View pull request in GitHub'
'provisioned-resource-preview-banner.preview-banner.view-pull-request-in-repo',
'View pull request in {{repoType}}',
{ repoType: capitalizedRepoType }
)}
<Icon name="external-link-alt" />
</Stack>
@@ -71,8 +93,16 @@ export function PreviewBannerViewPR({ prParam, isNewPr, behindBranch, repoUrl }:
onRemove={prParam ? () => window.open(textUtil.sanitizeUrl(prParam), '_blank') : undefined}
>
<Trans i18nKey="provisioned-resource-preview-banner.preview-banner.not-saved">
The value is not yet saved in the Grafana database
The rest of Grafana users in your organization will still see the current version saved to configured default
branch until this branch is merged
</Trans>
</Alert>
);
}
export function isValidRepoType(repoType: string | undefined): repoType is RepoType {
if (typeof repoType !== 'string') {
return false;
}
return repoType in RepoTypeDisplay;
}
@@ -16,6 +16,7 @@ import { PROVISIONING_URL } from 'app/features/provisioning/constants';
import { useCreateOrUpdateRepositoryFile } from 'app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile';
import { ResourceEditFormSharedFields } from '../../components/Provisioned/ResourceEditFormSharedFields';
import { buildResourceBranchRedirectUrl } from '../../settings/utils';
import { getDashboardUrl } from '../../utils/getDashboardUrl';
import { useProvisionedRequestHandler } from '../../utils/useProvisionedRequestHandler';
import { SaveDashboardFormCommonOptions } from '../SaveDashboardForm';
@@ -92,7 +93,14 @@ export function SaveProvisionedDashboardForm({
const onBranchSuccess = (ref: string, path: string) => {
panelEditor?.onDiscard();
drawer.onClose();
navigate(`${PROVISIONING_URL}/${defaultValues.repo}/dashboard/preview/${path}?ref=${ref}`);
const url = buildResourceBranchRedirectUrl({
baseUrl: `${PROVISIONING_URL}/${defaultValues.repo}/dashboard/preview/${path}`,
paramName: 'ref',
paramValue: ref,
repoType: request.data?.repository?.type,
});
navigate(url);
};
useProvisionedRequestHandler({
@@ -13,6 +13,8 @@ import { ProvisionedDashboardFormData } from '../saving/shared';
import { DashboardScene } from '../scene/DashboardScene';
import { useProvisionedRequestHandler } from '../utils/useProvisionedRequestHandler';
import { buildResourceBranchRedirectUrl } from './utils';
export interface Props {
dashboard: DashboardScene;
defaultValues: ProvisionedDashboardFormData;
@@ -82,9 +84,13 @@ export function DeleteProvisionedDashboardForm({
const onBranchSuccess = (path: string, urls?: Record<string, string>) => {
panelEditor?.onDiscard();
onDismiss();
navigate(
`${PROVISIONING_URL}/${defaultValues.repo}/dashboard/preview/${path}?pull_request_url=${urls?.newPullRequestURL}`
);
const url = buildResourceBranchRedirectUrl({
baseUrl: `${PROVISIONING_URL}/${defaultValues.repo}/dashboard/preview/${path}`,
paramName: 'pull_request_url',
paramValue: urls?.newPullRequestURL,
repoType: request.data?.repository?.type,
});
navigate(url);
};
useProvisionedRequestHandler({
@@ -27,6 +27,13 @@ jest.mock('app/api/clients/provisioning/v0alpha1', () => ({
useGetRepositoryFilesWithPathQuery: jest.fn(),
useCreateRepositoryFilesWithPathMutation: jest.fn(),
useDeleteRepositoryFilesWithPathMutation: jest.fn(),
provisioningAPIv0alpha1: {
endpoints: {
listRepository: {
select: jest.fn(() => () => ({ data: { items: [] } })),
},
},
},
}));
jest.mock('app/api/clients/folder/v1beta1', () => ({
@@ -20,6 +20,8 @@ import { ProvisionedDashboardFormData } from '../saving/shared';
import { DashboardScene } from '../scene/DashboardScene';
import { useProvisionedRequestHandler } from '../utils/useProvisionedRequestHandler';
import { buildResourceBranchRedirectUrl } from './utils';
export interface Props {
dashboard: DashboardScene;
defaultValues: ProvisionedDashboardFormData;
@@ -144,7 +146,12 @@ export function MoveProvisionedDashboardForm({
const onBranchSuccess = () => {
panelEditor?.onDiscard();
navigate(`/dashboards?new_pull_request_url=${createRequest.data?.urls?.newPullRequestURL}`);
const url = buildResourceBranchRedirectUrl({
paramName: 'new_pull_request_url',
paramValue: createRequest?.data?.urls?.newPullRequestURL,
repoType: createRequest?.data?.repository?.type,
});
navigate(url);
};
useProvisionedRequestHandler({
@@ -111,3 +111,30 @@ export function createDashboardEditViewFor(editview: string): DashboardEditView
return new GeneralSettingsEditView({});
}
}
export type ResourceBranchUrlOptions = {
baseUrl?: string;
paramName?: string;
paramValue?: string;
repoType?: string;
};
export function buildResourceBranchRedirectUrl({
baseUrl = '/dashboards',
paramName,
paramValue,
repoType,
}: ResourceBranchUrlOptions): string {
const params = new URLSearchParams();
if (paramName && paramValue) {
params.set(paramName, paramValue);
}
if (repoType) {
params.set('repo_type', repoType);
}
const queryString = params.toString();
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
}
@@ -26,6 +26,16 @@ export interface ModeOption {
subtitle: string;
}
export type StepStatus = 'idle' | 'running' | 'error' | 'success';
export const RepoTypeDisplay: { [key in RepoType]: string } = {
github: 'GitHub',
gitlab: 'GitLab',
bitbucket: 'Bitbucket',
git: 'Git',
local: 'Local',
};
export type StepStatusInfo =
| { status: 'idle' | 'running' | 'success' }
| { status: 'error'; error: string | ProvisioningErrorInfo }
@@ -6,10 +6,12 @@ export const usePullRequestParam = () => {
const prParam = params.get('pull_request_url');
const newPrParam = params.get('new_pull_request_url');
const repoUrl = params.get('repo_url');
const repoType = params.get('repo_type');
return {
prURL: prParam ? textUtil.sanitizeUrl(prParam) : undefined,
newPrURL: newPrParam ? textUtil.sanitizeUrl(newPrParam) : undefined,
repoURL: repoUrl ? textUtil.sanitizeUrl(repoUrl) : undefined,
repoType: repoType ? textUtil.sanitizeUrl(repoType) : undefined,
};
};
+8 -8
View File
@@ -10952,15 +10952,15 @@
},
"provisioned-resource-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",
"open-git-hub": "Open in Github",
"open-pull-request-in-git-hub": "Open pull request in GitHub",
"view-pull-request-in-git-hub": "View pull request in GitHub"
"behind-branch-text": "This resource is behind the branch in {{repoType}}.",
"not-saved": "The rest of Grafana users in your organization will still see the current version saved to configured default branch until this branch is merged",
"open-in-repo-button": "Open in {{repoType}}",
"open-pull-request-in-repo": "Open pull request in {{repoType}}",
"view-in-repo-button": "View it in {{repoType}} to see the latest changes.",
"view-pull-request-in-repo": "View pull request in {{repoType}}"
},
"title-created-branch-git-hub": "A new resource has been created in a branch in GitHub.",
"title-loaded-pull-request-git-hub": "This resource is loaded from a pull request in GitHub."
"title-created-branch-in-repo": "A new resource has been created in a branch in {{repoType}}.",
"title-loaded-pull-request-in-repo": "This resource is loaded from the branch you just created in {{repoType}} and it is only visible to you"
},
"provisioning": {
"banner": {