diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx
index 36182de2d44..a2bfe9ca651 100644
--- a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx
+++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx
@@ -163,17 +163,17 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
describe('at the root level', () => {
it('displays "Dashboards" as the page title', async () => {
- render();
+ render();
expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument();
});
it('displays a search input', async () => {
- render();
+ render();
expect(await screen.findByPlaceholderText('Search for dashboards and folders')).toBeInTheDocument();
});
it('shows the "New" button', async () => {
- render();
+ render();
expect(await screen.findByRole('button', { name: 'New' })).toBeInTheDocument();
});
@@ -185,25 +185,25 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
canCreateFolders: false,
};
});
- render();
+ render();
expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument();
});
it('does not show "Folder actions"', async () => {
- render();
+ render();
expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument();
});
it('does not show an "Edit title" button', async () => {
- render();
+ render();
expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Edit title' })).not.toBeInTheDocument();
});
it('does not show any tabs', async () => {
- render();
+ render();
expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument();
expect(screen.queryByRole('tab', { name: 'Dashboards' })).not.toBeInTheDocument();
@@ -212,7 +212,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
});
it('displays the filters and hides the actions initially', async () => {
- render();
+ render();
await screen.findByPlaceholderText('Search for dashboards and folders');
expect(await screen.findByText('Sort')).toBeInTheDocument();
@@ -223,7 +223,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
});
it('selecting an item hides the filters and shows the actions instead', async () => {
- render();
+ render();
const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(dashbdD.item.uid));
await userEvent.click(checkbox);
@@ -238,7 +238,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
});
it('navigating into a child item resets the selected state', async () => {
- const { rerender } = render();
+ const { rerender } = render();
const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(folderA.item.uid));
await userEvent.click(checkbox);
@@ -248,7 +248,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument();
(useParams as jest.Mock).mockReturnValue({ uid: folderA.item.uid });
- rerender();
+ rerender();
// Check the filters are now visible again
expect(await screen.findByText('Filter by tag')).toBeInTheDocument();
@@ -266,17 +266,17 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
});
it('shows the folder name as the page title', async () => {
- render();
+ render();
expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument();
});
it('displays a search input', async () => {
- render();
+ render();
expect(await screen.findByPlaceholderText('Search for dashboards and folders')).toBeInTheDocument();
});
it('shows the "New" button', async () => {
- render();
+ render();
expect(await screen.findByRole('button', { name: 'New' })).toBeInTheDocument();
});
@@ -288,13 +288,13 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
canCreateFolders: false,
};
});
- render();
+ render();
expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument();
});
it('shows the "Folder actions" button', async () => {
- render();
+ render();
expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument();
});
@@ -308,13 +308,13 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
canViewPermissions: false,
};
});
- render();
+ render();
expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument();
});
it('shows an "Edit title" button', async () => {
- render();
+ render();
expect(await screen.findByRole('button', { name: 'Edit title' })).toBeInTheDocument();
});
@@ -325,13 +325,13 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
canEditFolders: false,
};
});
- render();
+ render();
expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Edit title' })).not.toBeInTheDocument();
});
it('displays all the folder tabs and shows the "Dashboards" tab as selected', async () => {
- render();
+ render();
expect(await screen.findByRole('tab', { name: 'Dashboards' })).toBeInTheDocument();
expect(await screen.findByRole('tab', { name: 'Dashboards' })).toHaveAttribute('aria-selected', 'true');
@@ -343,7 +343,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
});
it('displays the filters and hides the actions initially', async () => {
- render();
+ render();
await screen.findByPlaceholderText('Search for dashboards and folders');
expect(await screen.findByText('Sort')).toBeInTheDocument();
@@ -354,7 +354,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
});
it('selecting an item hides the filters and shows the actions instead', async () => {
- render();
+ render();
const checkbox = await screen.findByTestId(
selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid)
diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
index 76b7fe057b9..7a58ec579ae 100644
--- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
+++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
@@ -25,13 +25,14 @@ import { BrowseFilters } from './components/BrowseFilters';
import { BrowseView } from './components/BrowseView';
import CreateNewButton from './components/CreateNewButton';
import { FolderActionsButton } from './components/FolderActionsButton';
+import { ProvisionedFolderPreviewBanner } from './components/ProvisionedFolderPreviewBanner';
import { SearchView } from './components/SearchView';
import { getFolderPermissions } from './permissions';
import { useHasSelection } from './state/hooks';
import { setAllSelection } from './state/slice';
// New Browse/Manage/Search Dashboards views for nested folders
-const BrowseDashboardsPage = memo(() => {
+const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record }) => {
const { uid: folderUID } = useParams();
const dispatch = useDispatch();
@@ -159,6 +160,7 @@ const BrowseDashboardsPage = memo(() => {
}
>
+
{
(getAppEvents as jest.Mock).mockReturnValue(mockAppEvents);
// Mock usePullRequestParam
- (usePullRequestParam as jest.Mock).mockReturnValue(null);
+ (usePullRequestParam as jest.Mock).mockReturnValue({});
// Mock useCreateRepositoryFilesWithPathMutation
const mockCreate = jest.fn();
@@ -409,7 +409,7 @@ describe('NewProvisionedFolderForm', () => {
});
it('should show PR link when PR URL is available', () => {
- (usePullRequestParam as jest.Mock).mockReturnValue('https://github.com/grafana/grafana/pull/1234');
+ (usePullRequestParam as jest.Mock).mockReturnValue({ prURL: 'https://github.com/grafana/grafana/pull/1234' });
setup();
diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx
index ed03a23cb7a..10a9e429099 100644
--- a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx
+++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx
@@ -30,7 +30,7 @@ interface Props {
}
function FormContent({ initialValues, repository, workflowOptions, folder, isGitHub, onDismiss }: FormProps) {
- const prURL = usePullRequestParam();
+ const { prURL } = usePullRequestParam();
const navigate = useNavigate();
const [create, request] = useCreateRepositoryFilesWithPathMutation();
diff --git a/public/app/features/browse-dashboards/components/ProvisionedFolderPreviewBanner.tsx b/public/app/features/browse-dashboards/components/ProvisionedFolderPreviewBanner.tsx
new file mode 100644
index 00000000000..57443939fa6
--- /dev/null
+++ b/public/app/features/browse-dashboards/components/ProvisionedFolderPreviewBanner.tsx
@@ -0,0 +1,23 @@
+import { config } from '@grafana/runtime';
+import { CommonBannerProps } from 'app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner';
+import { PreviewBannerViewPR } from 'app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR';
+import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
+
+export function ProvisionedFolderPreviewBanner({ queryParams }: CommonBannerProps) {
+ const provisioningEnabled = config.featureToggles.provisioning;
+ const { prURL, newPrURL } = usePullRequestParam();
+
+ if (!provisioningEnabled || 'kiosk' in queryParams) {
+ return null;
+ }
+
+ if (prURL) {
+ return ;
+ }
+
+ if (newPrURL) {
+ return ;
+ }
+
+ return null;
+}
diff --git a/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx b/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx
index 1f7486a921f..ee306bad2f9 100644
--- a/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx
+++ b/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx
@@ -1,13 +1,14 @@
-import { textUtil } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { config } from '@grafana/runtime';
-import { Alert, Icon, Stack } from '@grafana/ui';
+import { Alert } from '@grafana/ui';
import { useGetRepositoryFilesWithPathQuery } from 'app/api/clients/provisioning/v0alpha1';
import { DashboardPageRouteSearchParams } from 'app/features/dashboard/containers/types';
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
import { DashboardRoutes } from 'app/types';
-interface CommonBannerProps {
+import { PreviewBannerViewPR } from './PreviewBannerViewPR';
+
+export interface CommonBannerProps {
queryParams: DashboardPageRouteSearchParams;
path?: string;
slug?: string;
@@ -19,13 +20,13 @@ interface DashboardPreviewBannerProps extends CommonBannerProps {
interface DashboardPreviewBannerContentProps extends Required> {}
-const commonAlertProps = {
+export const commonAlertProps = {
severity: 'info' as const,
style: { flex: 0 } as const,
};
function DashboardPreviewBannerContent({ queryParams, slug, path }: DashboardPreviewBannerContentProps) {
- const prParam = usePullRequestParam();
+ const { prURL } = usePullRequestParam();
const file = useGetRepositoryFilesWithPathQuery({ name: slug, path, ref: queryParams.ref });
if (file.data?.errors) {
@@ -43,56 +44,14 @@ function DashboardPreviewBannerContent({ queryParams, slug, path }: DashboardPre
}
// This page was loaded with a `pull_request_url` in the URL
- if (prParam?.length) {
- return (
-
-
- View pull request in GitHub
-
-
-
- }
- onRemove={() => window.open(textUtil.sanitizeUrl(prParam), '_blank')}
- >
-
- The value is not yet saved in the Grafana database
-
-
- );
+ if (prURL?.length) {
+ return ;
}
// Check if this is a GitHub link
const githubURL = file.data?.urls?.newPullRequestURL ?? file.data?.urls?.compareURL;
if (githubURL) {
- return (
-
-
- Open pull request in GitHub
-
-
-
- }
- onRemove={() => window.open(textUtil.sanitizeUrl(githubURL), '_blank')}
- >
-
- The value is not yet saved in the Grafana database
-
-
- );
+ return ;
}
return (
diff --git a/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx
new file mode 100644
index 00000000000..de716143ac0
--- /dev/null
+++ b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx
@@ -0,0 +1,125 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { textUtil } from '@grafana/data';
+
+import { PreviewBannerViewPR } from './PreviewBannerViewPR';
+
+jest.mock('@grafana/data', () => ({
+ ...jest.requireActual('@grafana/data'),
+ textUtil: {
+ sanitizeUrl: jest.fn(),
+ },
+}));
+
+jest.mock('@grafana/i18n', () => ({
+ t: jest.fn((key: string, defaultValue: string) => defaultValue),
+ Trans: ({ children }: { children: React.ReactNode }) => children,
+}));
+
+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 renderResult = render();
+
+ return { renderResult, props: defaultProps };
+}
+
+describe('PreviewBannerViewPR', () => {
+ let windowOpenSpy: jest.SpyInstance;
+
+ beforeAll(() => {
+ Object.defineProperty(window, 'open', {
+ writable: true,
+ value: jest.fn(),
+ });
+ windowOpenSpy = jest.spyOn(window, 'open');
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockTextUtil.sanitizeUrl.mockImplementation((url) => url);
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ afterAll(() => {
+ windowOpenSpy.mockRestore();
+ });
+
+ describe('Dashboard scenarios', () => {
+ it('should render correct text for new PR dashboard', () => {
+ setup({ prParam: 'test-url', isFolder: false, isNewPr: true });
+
+ expect(screen.getByRole('status')).toBeInTheDocument();
+ expect(screen.getByText('This dashboard is loaded from a branch in GitHub.')).toBeInTheDocument();
+ });
+
+ it('should render correct text for existing PR dashboard', () => {
+ setup({ prParam: 'test-url', isFolder: false, isNewPr: false });
+
+ expect(screen.getByRole('status')).toBeInTheDocument();
+ expect(screen.getByText('This dashboard is loaded from a pull request in GitHub.')).toBeInTheDocument();
+ });
+
+ it('should render correct button text for new PR dashboard', () => {
+ setup({ prParam: 'test-url', isFolder: false, 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 });
+
+ 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 });
+
+ expect(screen.getByRole('status')).toBeInTheDocument();
+ expect(screen.getByText('A new folder 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 });
+
+ expect(screen.getByRole('status')).toBeInTheDocument();
+ expect(screen.getByText('A new folder has been created in a pull request in GitHub.')).toBeInTheDocument();
+ });
+
+ it('should render correct button text for new PR folder', () => {
+ setup({ prParam: 'test-url', isFolder: true, 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 });
+
+ expect(screen.getByText('View pull request in GitHub')).toBeInTheDocument();
+ });
+ });
+
+ describe('Button functionality', () => {
+ it('should open URL in new tab when button is clicked', async () => {
+ const testUrl = 'https://github.com/test/repo/pull/123';
+ setup({ prParam: testUrl });
+
+ const button = screen.getByRole('button', { name: /close alert/i });
+ await userEvent.click(button);
+
+ expect(windowOpenSpy).toHaveBeenCalledWith(testUrl, '_blank');
+ });
+ });
+});
diff --git a/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx
new file mode 100644
index 00000000000..2188c4029a0
--- /dev/null
+++ b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx
@@ -0,0 +1,64 @@
+import { textUtil } from '@grafana/data';
+import { Trans, t } from '@grafana/i18n';
+import { Alert, Icon, Stack } from '@grafana/ui';
+
+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.
+
+interface Props {
+ prParam: string;
+ isFolder?: boolean;
+ isNewPr?: boolean;
+}
+
+/**
+ * @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, isFolder = false, isNewPr }: Props) {
+ const titleText = isFolder
+ ? isNewPr
+ ? t(
+ 'provisioned-resource-preview-banner.title-folder-created-branch-git-hub',
+ 'A new folder has been created in a branch in GitHub.'
+ )
+ : t(
+ 'provisioned-resource-preview-banner.title-folder-created-pull-request-git-hub',
+ 'A new folder has been created in a pull request in GitHub.'
+ )
+ : isNewPr
+ ? t(
+ 'provisioned-resource-preview-banner.title-dashboard-loaded-branch-git-hub',
+ 'This dashboard is loaded from a branch in GitHub.'
+ )
+ : t(
+ 'provisioned-resource-preview-banner.title-dashboard-loaded-pull-request-git-hub',
+ 'This dashboard is loaded from a pull request in GitHub.'
+ );
+
+ return (
+
+ {isNewPr
+ ? t(
+ 'provisioned-resource-preview-banner.preview-banner.open-pull-request-in-git-hub',
+ 'Open pull request in GitHub'
+ )
+ : t(
+ 'provisioned-resource-preview-banner.preview-banner.view-pull-request-in-git-hub',
+ 'View pull request in GitHub'
+ )}
+
+
+ }
+ onRemove={() => window.open(textUtil.sanitizeUrl(prParam), '_blank')}
+ >
+
+ The value is not yet saved in the Grafana database
+
+
+ );
+}
diff --git a/public/app/features/provisioning/hooks/usePullRequestParam.ts b/public/app/features/provisioning/hooks/usePullRequestParam.ts
index 911a8ce988a..e4432232900 100644
--- a/public/app/features/provisioning/hooks/usePullRequestParam.ts
+++ b/public/app/features/provisioning/hooks/usePullRequestParam.ts
@@ -4,10 +4,10 @@ import { useUrlParams } from 'app/core/navigation/hooks';
export const usePullRequestParam = () => {
const [params] = useUrlParams();
const prParam = params.get('pull_request_url');
+ const newPrParam = params.get('new_pull_request_url');
- if (!prParam) {
- return undefined;
- }
-
- return textUtil.sanitizeUrl(decodeURIComponent(prParam));
+ return {
+ prURL: prParam ? textUtil.sanitizeUrl(prParam) : undefined,
+ newPrURL: newPrParam ? textUtil.sanitizeUrl(newPrParam) : undefined,
+ };
};
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index ff317b5d624..4334abe4952 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -5472,15 +5472,9 @@
"type": "Type"
},
"dashboard-preview-banner": {
- "not-saved": "The value is not yet saved in the Grafana database",
"not-yet-saved": "The value is not saved in the Grafana database",
- "open-pull-request-in-git-hub": "Open pull request in GitHub",
- "title-dashboard-loaded-branch-git-hub": "This dashboard is loaded from a branch in GitHub.",
"title-dashboard-loaded-external-repository": "This dashboard is loaded from an external repository",
- "title-dashboard-loaded-request-git-hub": "This dashboard is loaded from a pull request in GitHub.",
- "title-error-loading-dashboard": "Error loading dashboard",
- "value-not-saved": "The value is not yet saved in the Grafana database",
- "view-pull-request-in-git-hub": "View pull request in GitHub"
+ "title-error-loading-dashboard": "Error loading dashboard"
},
"dashboard-scene": {
"text": {
@@ -10312,6 +10306,17 @@
"label-workflow": "Workflow"
}
},
+ "provisioned-resource-preview-banner": {
+ "preview-banner": {
+ "not-saved": "The value is not yet saved in the Grafana database",
+ "open-pull-request-in-git-hub": "Open pull request in GitHub",
+ "view-pull-request-in-git-hub": "View pull request in GitHub"
+ },
+ "title-dashboard-loaded-branch-git-hub": "This dashboard is loaded from a branch in GitHub.",
+ "title-dashboard-loaded-pull-request-git-hub": "This dashboard is loaded from a pull request in GitHub.",
+ "title-folder-created-branch-git-hub": "A new folder has been created in a branch in GitHub.",
+ "title-folder-created-pull-request-git-hub": "A new folder has been created in a pull request in GitHub."
+ },
"provisioning": {
"banner": {
"message": "This feature is currently under active development. For the best experience and latest improvements, we recommend using the <2>nightly build2> of Grafana."