diff --git a/.betterer.results b/.betterer.results
index ae90f276b56..43158964214 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -4360,10 +4360,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
- [0, 0, 0, "Unexpected any. Specify a different type.", "16"],
- [0, 0, 0, "Unexpected any. Specify a different type.", "17"],
- [0, 0, 0, "Unexpected any. Specify a different type.", "18"],
- [0, 0, 0, "Unexpected any. Specify a different type.", "19"]
+ [0, 0, 0, "Unexpected any. Specify a different type.", "16"]
],
"public/app/features/manage-dashboards/state/reducers.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@@ -4660,81 +4657,10 @@ exports[`better eslint`] = {
"public/app/features/sandbox/TestStuffPage.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
- "public/app/features/search/components/DashboardListPage.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"]
- ],
- "public/app/features/search/components/ManageDashboardsNew.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"],
- [0, 0, 0, "Styles should be written using objects.", "1"],
- [0, 0, 0, "Styles should be written using objects.", "2"],
- [0, 0, 0, "Styles should be written using objects.", "3"],
- [0, 0, 0, "Styles should be written using objects.", "4"]
- ],
- "public/app/features/search/components/SearchCard.tsx:5381": [
- [0, 0, 0, "Unexpected any. Specify a different type.", "0"],
- [0, 0, 0, "Do not use any type assertions.", "1"],
- [0, 0, 0, "Do not use any type assertions.", "2"],
- [0, 0, 0, "Styles should be written using objects.", "3"],
- [0, 0, 0, "Styles should be written using objects.", "4"],
- [0, 0, 0, "Styles should be written using objects.", "5"],
- [0, 0, 0, "Styles should be written using objects.", "6"],
- [0, 0, 0, "Styles should be written using objects.", "7"],
- [0, 0, 0, "Styles should be written using objects.", "8"],
- [0, 0, 0, "Styles should be written using objects.", "9"],
- [0, 0, 0, "Styles should be written using objects.", "10"],
- [0, 0, 0, "Styles should be written using objects.", "11"]
- ],
- "public/app/features/search/components/SearchCardExpanded.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"],
- [0, 0, 0, "Styles should be written using objects.", "1"],
- [0, 0, 0, "Styles should be written using objects.", "2"],
- [0, 0, 0, "Styles should be written using objects.", "3"],
- [0, 0, 0, "Styles should be written using objects.", "4"],
- [0, 0, 0, "Styles should be written using objects.", "5"],
- [0, 0, 0, "Styles should be written using objects.", "6"],
- [0, 0, 0, "Styles should be written using objects.", "7"],
- [0, 0, 0, "Styles should be written using objects.", "8"],
- [0, 0, 0, "Styles should be written using objects.", "9"],
- [0, 0, 0, "Styles should be written using objects.", "10"]
- ],
- "public/app/features/search/components/SearchItem.tsx:5381": [
- [0, 0, 0, "Unexpected any. Specify a different type.", "0"],
- [0, 0, 0, "Styles should be written using objects.", "1"],
- [0, 0, 0, "Styles should be written using objects.", "2"],
- [0, 0, 0, "Styles should be written using objects.", "3"]
- ],
"public/app/features/search/page/components/ActionRow.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
- "public/app/features/search/page/components/ConfirmDeleteModal.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"]
- ],
- "public/app/features/search/page/components/FolderSection.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"],
- [0, 0, 0, "Styles should be written using objects.", "1"],
- [0, 0, 0, "Styles should be written using objects.", "2"],
- [0, 0, 0, "Styles should be written using objects.", "3"],
- [0, 0, 0, "Styles should be written using objects.", "4"],
- [0, 0, 0, "Styles should be written using objects.", "5"],
- [0, 0, 0, "Styles should be written using objects.", "6"],
- [0, 0, 0, "Styles should be written using objects.", "7"],
- [0, 0, 0, "Styles should be written using objects.", "8"]
- ],
- "public/app/features/search/page/components/MoveToFolderModal.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"],
- [0, 0, 0, "Styles should be written using objects.", "1"]
- ],
- "public/app/features/search/page/components/RootFolderView.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"],
- [0, 0, 0, "Styles should be written using objects.", "1"],
- [0, 0, 0, "Styles should be written using objects.", "2"],
- [0, 0, 0, "Styles should be written using objects.", "3"]
- ],
- "public/app/features/search/page/components/SearchResultsCards.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"],
- [0, 0, 0, "Styles should be written using objects.", "1"]
- ],
"public/app/features/search/page/components/SearchResultsTable.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
@@ -4750,11 +4676,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "11"],
[0, 0, 0, "Styles should be written using objects.", "12"]
],
- "public/app/features/search/page/components/SearchView.tsx:5381": [
- [0, 0, 0, "Styles should be written using objects.", "0"],
- [0, 0, 0, "Styles should be written using objects.", "1"],
- [0, 0, 0, "Styles should be written using objects.", "2"]
- ],
"public/app/features/search/page/components/columns.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
index 12d33557fa0..3c0861d1837 100644
--- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
+++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
@@ -49,7 +49,6 @@ Some features are enabled by default. You can disable these feature by setting t
| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes |
| `transformationsRedesign` | Enables the transformations redesign | Yes |
| `prometheusConfigOverhaulAuth` | Update the Prometheus configuration page with the new auth component | Yes |
-| `newBrowseDashboards` | New browse/manage dashboards UI | Yes |
| `alertingInsights` | Show the new alerting insights landing page | Yes |
| `cloudWatchWildCardDimensionValues` | Fetches dimension values from CloudWatch to correctly label wildcard dimensions | Yes |
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index 681b48d49f4..5ca588e103e 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -116,7 +116,6 @@ export interface FeatureToggles {
angularDeprecationUI?: boolean;
dashgpt?: boolean;
reportingRetries?: boolean;
- newBrowseDashboards?: boolean;
sseGroupByDatasource?: boolean;
requestInstrumentationStatusSource?: boolean;
libraryPanelRBAC?: boolean;
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index 7b53db06e66..81791f22a9c 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -733,15 +733,6 @@ var (
Owner: grafanaSharingSquad,
RequiresRestart: true,
},
- {
- Name: "newBrowseDashboards",
- Description: "New browse/manage dashboards UI",
- Stage: FeatureStageGeneralAvailability,
- Owner: grafanaFrontendPlatformSquad,
- FrontendOnly: true,
- Expression: "true", // on by default
- AllowSelfServe: truePtr,
- },
{
Name: "sseGroupByDatasource",
Description: "Send query to the same datasource in a single request when using server side expressions",
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index b1cbe037f8d..8f9a770964e 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -97,7 +97,6 @@ alertingNoDataErrorExecution,privatePreview,@grafana/alerting-squad,false,false,
angularDeprecationUI,experimental,@grafana/plugins-platform-backend,false,false,false,true
dashgpt,preview,@grafana/dashboards-squad,false,false,false,true
reportingRetries,preview,@grafana/sharing-squad,false,false,true,false
-newBrowseDashboards,GA,@grafana/grafana-frontend-platform,false,false,false,true
sseGroupByDatasource,experimental,@grafana/observability-metrics,false,false,false,false
requestInstrumentationStatusSource,experimental,@grafana/plugins-platform-backend,false,false,false,false
libraryPanelRBAC,experimental,@grafana/dashboards-squad,false,false,true,false
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index bd7634f084e..cfef22fc19a 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -399,10 +399,6 @@ const (
// Enables rendering retries for the reporting feature
FlagReportingRetries = "reportingRetries"
- // FlagNewBrowseDashboards
- // New browse/manage dashboards UI
- FlagNewBrowseDashboards = "newBrowseDashboards"
-
// FlagSseGroupByDatasource
// Send query to the same datasource in a single request when using server side expressions
FlagSseGroupByDatasource = "sseGroupByDatasource"
diff --git a/public/app/core/components/AccessControl/Permissions.tsx b/public/app/core/components/AccessControl/Permissions.tsx
index 92dfb78e7bd..1fb44372b52 100644
--- a/public/app/core/components/AccessControl/Permissions.tsx
+++ b/public/app/core/components/AccessControl/Permissions.tsx
@@ -9,7 +9,6 @@ import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { Trans, t } from 'app/core/internationalization';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { DescendantCount } from 'app/features/browse-dashboards/components/BrowseActions/DescendantCount';
-import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
import { AddPermission } from './AddPermission';
import { PermissionList } from './PermissionList';
@@ -156,7 +155,7 @@ export const Permissions = ({
{canSetPermissions && (
<>
- {newBrowseDashboardsEnabled() && resource === 'folders' && (
+ {resource === 'folders' && (
<>
This will change permissions for this folder and all its descendants. In total, this will affect:
diff --git a/public/app/core/components/Select/OldFolderPicker.test.tsx b/public/app/core/components/Select/OldFolderPicker.test.tsx
index 2b27a1fbfd7..0be47d4847a 100644
--- a/public/app/core/components/Select/OldFolderPicker.test.tsx
+++ b/public/app/core/components/Select/OldFolderPicker.test.tsx
@@ -76,7 +76,7 @@ describe('OldFolderPicker', () => {
});
});
- it('should show the General folder by default for editors', async () => {
+ it('should show the Dashboards root by default for editors', async () => {
jest
.spyOn(api, 'searchFolders')
.mockResolvedValue([
@@ -94,10 +94,10 @@ describe('OldFolderPicker', () => {
const pickerOptions = await screen.findAllByLabelText('Select option');
- expect(pickerOptions[0]).toHaveTextContent('General');
+ expect(pickerOptions[0]).toHaveTextContent('Dashboards');
});
- it('should not show the General folder by default if showRoot is false', async () => {
+ it('should not show the Dashboards root by default if showRoot is false', async () => {
jest
.spyOn(api, 'searchFolders')
.mockResolvedValue([
@@ -115,10 +115,10 @@ describe('OldFolderPicker', () => {
const pickerOptions = await screen.findAllByLabelText('Select option');
- expect(pickerOptions[0]).not.toHaveTextContent('General');
+ expect(pickerOptions[0]).not.toHaveTextContent('Dashboards');
});
- it('should not show the General folder by default for not editors', async () => {
+ it('should not show the Dashboards root by default for not editors', async () => {
jest
.spyOn(api, 'searchFolders')
.mockResolvedValue([
@@ -136,7 +136,7 @@ describe('OldFolderPicker', () => {
const pickerOptions = await screen.findAllByLabelText('Select option');
- expect(pickerOptions[0]).not.toHaveTextContent('General');
+ expect(pickerOptions[0]).not.toHaveTextContent('Dashboards');
});
it('should return the correct search results when typing in the select', async () => {
diff --git a/public/app/core/components/Select/OldFolderPicker.tsx b/public/app/core/components/Select/OldFolderPicker.tsx
index 77fbf504c08..4a7d73d3484 100644
--- a/public/app/core/components/Select/OldFolderPicker.tsx
+++ b/public/app/core/components/Select/OldFolderPicker.tsx
@@ -10,7 +10,6 @@ import { useStyles2, ActionMeta, Input, InputActionMeta, AsyncVirtualizedSelect
import appEvents from 'app/core/app_events';
import { t } from 'app/core/internationalization';
import { contextSrv } from 'app/core/services/context_srv';
-import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
import { createFolder, getFolderByUid, searchFolders } from 'app/features/manage-dashboards/state/actions';
import { DashboardSearchHit } from 'app/features/search/types';
import { AccessControlAction, PermissionLevelString, SearchQueryType } from 'app/types';
@@ -81,7 +80,7 @@ export function OldFolderPicker(props: Props) {
folderWarning,
} = props;
- const rootName = rootNameProp ?? newBrowseDashboardsEnabled() ? 'Dashboards' : 'General';
+ const rootName = rootNameProp ?? 'Dashboards';
const [folder, setFolder] = useState(null);
const [isCreatingNew, setIsCreatingNew] = useState(false);
diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts
index f4f04de9994..fc758061db5 100644
--- a/public/app/core/selectors/navModel.ts
+++ b/public/app/core/selectors/navModel.ts
@@ -1,7 +1,6 @@
import memoizeOne from 'memoize-one';
import { NavModel, NavModelItem, NavIndex } from '@grafana/data';
-import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
import { FOLDER_ID } from 'app/features/folders/state/navModel';
import { HOME_NAV_ID } from '../reducers/navModel';
@@ -44,7 +43,7 @@ export const getNavModel = memoizeOne(
export function getRootSectionForNode(node: NavModelItem): NavModelItem {
// Don't recurse fully up the folder tree when nested folders is enabled
- if (newBrowseDashboardsEnabled() && node.id === FOLDER_ID) {
+ if (node.id === FOLDER_ID) {
return node;
} else {
return node.parentItem && node.parentItem.id !== HOME_NAV_ID ? getRootSectionForNode(node.parentItem) : node;
diff --git a/public/app/features/browse-dashboards/featureFlag.ts b/public/app/features/browse-dashboards/featureFlag.ts
deleted file mode 100644
index f2e424835d3..00000000000
--- a/public/app/features/browse-dashboards/featureFlag.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { config } from '@grafana/runtime';
-
-export function newBrowseDashboardsEnabled() {
- return config.featureToggles.nestedFolders || config.featureToggles.newBrowseDashboards;
-}
diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
index b8ec4022740..971d0599a00 100644
--- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
+++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
@@ -3,7 +3,6 @@ import { getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
import { updateNavIndex } from 'app/core/actions';
import { StateManagerBase } from 'app/core/services/StateManagerBase';
import { backendSrv } from 'app/core/services/backend_srv';
-import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { buildNavModel } from 'app/features/folders/state/navModel';
@@ -161,7 +160,7 @@ export class DashboardScenePageStateManager extends StateManagerBase {
}),
};
- const { folderTitle, folderUid } = meta;
+ const { folderUid } = meta;
if (folderUid) {
- if (newBrowseDashboardsEnabled()) {
- const folderNavModel = getNavModel(navIndex, `folder-dashboards-${folderUid}`).main;
- // If the folder hasn't loaded (maybe user doesn't have permission on it?) then
- // don't show the "page not found" breadcrumb
- if (folderNavModel.id !== 'not-found') {
- pageNav = {
- ...pageNav,
- parentItem: folderNavModel,
- };
- }
- } else {
- if (folderTitle) {
- pageNav = {
- ...pageNav,
- parentItem: {
- text: folderTitle,
- url: `/dashboards/f/${meta.folderUid}`,
- },
- };
- }
+ const folderNavModel = getNavModel(navIndex, `folder-dashboards-${folderUid}`).main;
+ // If the folder hasn't loaded (maybe user doesn't have permission on it?) then
+ // don't show the "page not found" breadcrumb
+ if (folderNavModel.id !== 'not-found') {
+ pageNav = {
+ ...pageNav,
+ parentItem: folderNavModel,
+ };
}
}
diff --git a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardButton.tsx b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardButton.tsx
index 12d51e7dd44..34107900acb 100644
--- a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardButton.tsx
+++ b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardButton.tsx
@@ -11,6 +11,7 @@ interface SaveDashboardButtonProps {
dashboard: DashboardModel;
onSaveSuccess?: () => void;
size?: ComponentSize;
+ onClick?: () => void;
}
export const SaveDashboardButton = ({ dashboard, onSaveSuccess, size }: SaveDashboardButtonProps) => {
@@ -39,7 +40,7 @@ export const SaveDashboardButton = ({ dashboard, onSaveSuccess, size }: SaveDash
type Props = SaveDashboardButtonProps & { variant?: ButtonVariant };
-export const SaveDashboardAsButton = ({ dashboard, onSaveSuccess, variant, size }: Props) => {
+export const SaveDashboardAsButton = ({ dashboard, onClick, onSaveSuccess, variant, size }: Props) => {
return (
{({ showModal, hideModal }) => {
@@ -48,6 +49,7 @@ export const SaveDashboardAsButton = ({ dashboard, onSaveSuccess, variant, size
size={size}
onClick={() => {
reportInteraction('grafana_dashboard_save_as_clicked');
+ onClick?.();
showModal(SaveDashboardDrawer, {
dashboard,
onSaveSuccess,
diff --git a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.test.tsx b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.test.tsx
index ee9bb67e9df..1303bcefd16 100644
--- a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.test.tsx
+++ b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.test.tsx
@@ -10,20 +10,19 @@ import { createDashboardModelFixture } from '../../state/__fixtures__/dashboardF
import { SaveDashboardDrawer } from './SaveDashboardDrawer';
+const saveDashboardMutationMock = jest.fn();
+
jest.mock('app/core/core', () => ({
...jest.requireActual('app/core/core'),
contextSrv: {},
}));
-jest.mock('@grafana/runtime', () => ({
- ...jest.requireActual('@grafana/runtime'),
- getBackendSrv: () => ({
- post: mockPost,
- }),
+jest.mock('app/features/browse-dashboards/api/browseDashboardsAPI', () => ({
+ ...jest.requireActual('app/features/browse-dashboards/api/browseDashboardsAPI'),
+ useSaveDashboardMutation: () => [saveDashboardMutationMock],
}));
const store = configureStore();
-const mockPost = jest.fn();
const buildMocks = () => ({
dashboard: createDashboardModelFixture({
uid: 'mockDashboardUid',
@@ -53,13 +52,15 @@ const setup = (options: CompProps) => waitFor(() => render( {
beforeEach(() => {
- mockPost.mockClear();
+ saveDashboardMutationMock.mockClear();
jest.spyOn(console, 'error').mockImplementation();
});
it("renders a modal if there's an unhandled error", async () => {
const { onDismiss, dashboard, error } = buildMocks();
- mockPost.mockRejectedValueOnce(error);
+ saveDashboardMutationMock.mockResolvedValue({
+ error,
+ });
await setup({ dashboard, onDismiss });
@@ -72,15 +73,20 @@ describe('SaveDashboardDrawer', () => {
it('should render corresponding save modal once the error is handled', async () => {
const { onDismiss, dashboard, error } = buildMocks();
- mockPost.mockRejectedValueOnce(error);
+ saveDashboardMutationMock.mockResolvedValue({
+ error,
+ });
const { rerender } = await setup({ dashboard, onDismiss });
await userEvent.click(screen.getByRole('button', { name: /save/i }));
rerender( );
- mockPost.mockClear();
- mockPost.mockRejectedValueOnce({ ...error, isHandled: true });
+ saveDashboardMutationMock.mockClear();
+ saveDashboardMutationMock.mockResolvedValue({
+ error,
+ isHandled: true,
+ });
await userEvent.click(screen.getByRole('button', { name: /save/i }));
diff --git a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.tsx b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.tsx
index 5b81085c3a0..1ee7e8aa335 100644
--- a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.tsx
+++ b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.tsx
@@ -19,6 +19,7 @@ export const SaveDashboardDrawer = ({ dashboard, onDismiss, onSaveSuccess, isCop
const previous = dashboard.getOriginalDashboard();
const isProvisioned = dashboard.meta.provisioned;
const isNew = dashboard.version === 0;
+ const [errorIsHandled, setErrorIsHandled] = useState(false);
const data = useMemo(() => {
const clone = dashboard.getSaveModelClone({
@@ -89,18 +90,14 @@ export const SaveDashboardDrawer = ({ dashboard, onDismiss, onSaveSuccess, isCop
);
};
- if (
- state.error &&
- isFetchError(state.error) &&
- !state.error.isHandled &&
- proxyHandlesError(state.error.data.status)
- ) {
+ if (state.error && !errorIsHandled && isFetchError(state.error) && proxyHandlesError(state.error.data.status)) {
return (
);
}
diff --git a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx
index 23882f5af60..60d5533e153 100644
--- a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx
+++ b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx
@@ -1,5 +1,5 @@
import { css } from '@emotion/css';
-import React, { useEffect } from 'react';
+import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { FetchError } from '@grafana/runtime';
@@ -19,6 +19,7 @@ interface SaveDashboardErrorProxyProps {
dashboardSaveModel: Dashboard;
error: FetchError;
onDismiss: () => void;
+ setErrorIsHandled: React.Dispatch>;
}
export const SaveDashboardErrorProxy = ({
@@ -26,15 +27,10 @@ export const SaveDashboardErrorProxy = ({
dashboardSaveModel,
error,
onDismiss,
+ setErrorIsHandled,
}: SaveDashboardErrorProxyProps) => {
const { onDashboardSave } = useDashboardSave();
- useEffect(() => {
- if (error.data && proxyHandlesError(error.data.status)) {
- error.isHandled = true;
- }
- }, [error]);
-
return (
<>
{error.data && error.data.status === 'version-mismatch' && (
@@ -73,7 +69,13 @@ export const SaveDashboardErrorProxy = ({
/>
)}
{error.data && error.data.status === 'plugin-dashboard' && (
-
+ {
+ setErrorIsHandled(true);
+ onDismiss();
+ }}
+ />
)}
>
);
@@ -96,7 +98,7 @@ const ConfirmPluginDashboardSaveModal = ({ onDismiss, dashboard }: SaveDashboard
Cancel
-
+
{
diff --git a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx
index da61281c296..8bf16da07e9 100644
--- a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx
+++ b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx
@@ -5,12 +5,9 @@ import { locationService, reportInteraction } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import appEvents from 'app/core/app_events';
import { useAppNotification } from 'app/core/copy/appNotification';
-import { contextSrv } from 'app/core/core';
import { updateDashboardName } from 'app/core/reducers/navBarTree';
import { useSaveDashboardMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
-import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
import { DashboardModel } from 'app/features/dashboard/state';
-import { saveDashboard as saveDashboardApiCall } from 'app/features/manage-dashboards/state/actions';
import { useDispatch } from 'app/types';
import { DashboardSavedEvent } from 'app/types/events';
@@ -24,30 +21,18 @@ const saveDashboard = async (
dashboard: DashboardModel,
saveDashboardRtkQuery: ReturnType[0]
) => {
- if (newBrowseDashboardsEnabled()) {
- const query = await saveDashboardRtkQuery({
- dashboard: saveModel,
- folderUid: options.folderUid ?? dashboard.meta.folderUid ?? saveModel.meta.folderUid,
- message: options.message,
- overwrite: options.overwrite,
- });
+ const query = await saveDashboardRtkQuery({
+ dashboard: saveModel,
+ folderUid: options.folderUid ?? dashboard.meta.folderUid ?? saveModel.meta?.folderUid,
+ message: options.message,
+ overwrite: options.overwrite,
+ });
- if ('error' in query) {
- throw query.error;
- }
-
- return query.data;
- } else {
- let folderUid = options.folderUid;
- if (folderUid === undefined) {
- folderUid = dashboard.meta.folderUid ?? saveModel.folderUid;
- }
-
- const result = await saveDashboardApiCall({ ...options, folderUid, dashboard: saveModel });
- // fetch updated access control permissions
- await contextSrv.fetchUserPermissions();
- return result;
+ if ('error' in query) {
+ throw query.error;
}
+
+ return query.data;
};
export const useDashboardSave = (isCopy = false) => {
diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx
index 8e20cf9a98b..8770bdec767 100644
--- a/public/app/features/dashboard/containers/DashboardPage.tsx
+++ b/public/app/features/dashboard/containers/DashboardPage.tsx
@@ -14,7 +14,6 @@ import { createErrorNotification } from 'app/core/copy/appNotification';
import { getKioskMode } from 'app/core/navigation/kiosk';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';
-import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
import { PanelModel } from 'app/features/dashboard/state';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { AngularDeprecationNotice } from 'app/features/plugins/angularDeprecation/AngularDeprecationNotice';
@@ -425,29 +424,16 @@ function updateStatePageNavFromProps(props: Props, state: State): State {
};
}
- const { folderTitle, folderUid } = dashboard.meta;
+ const { folderUid } = dashboard.meta;
if (folderUid && pageNav) {
- if (newBrowseDashboardsEnabled()) {
- const folderNavModel = getNavModel(navIndex, `folder-dashboards-${folderUid}`).main;
- // If the folder hasn't loaded (maybe user doesn't have permission on it?) then
- // don't show the "page not found" breadcrumb
- if (folderNavModel.id !== 'not-found') {
- pageNav = {
- ...pageNav,
- parentItem: folderNavModel,
- };
- }
- } else {
- // Check if folder changed
- if (folderTitle && pageNav.parentItem?.text !== folderTitle) {
- pageNav = {
- ...pageNav,
- parentItem: {
- text: folderTitle,
- url: `/dashboards/f/${dashboard.meta.folderUid}`,
- },
- };
- }
+ const folderNavModel = getNavModel(navIndex, `folder-dashboards-${folderUid}`).main;
+ // If the folder hasn't loaded (maybe user doesn't have permission on it?) then
+ // don't show the "page not found" breadcrumb
+ if (folderNavModel.id !== 'not-found') {
+ pageNav = {
+ ...pageNav,
+ parentItem: folderNavModel,
+ };
}
}
diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts
index 5e1b81415ec..e657299c681 100644
--- a/public/app/features/dashboard/state/initDashboard.ts
+++ b/public/app/features/dashboard/state/initDashboard.ts
@@ -6,7 +6,6 @@ import { createErrorNotification } from 'app/core/copy/appNotification';
import { backendSrv } from 'app/core/services/backend_srv';
import { KeybindingSrv } from 'app/core/services/keybindingSrv';
import store from 'app/core/store';
-import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { DashboardSrv, getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
@@ -100,7 +99,7 @@ async function fetchDashboard(
// only the folder API has information about ancestors
// get parent folder (if it exists) and put it in the store
// this will be used to populate the full breadcrumb trail
- if (newBrowseDashboardsEnabled() && dashDTO.meta.folderUid) {
+ if (dashDTO.meta.folderUid) {
try {
await dispatch(getFolderByUid(dashDTO.meta.folderUid));
} catch (err) {
@@ -128,7 +127,7 @@ async function fetchDashboard(
// only the folder API has information about ancestors
// get parent folder (if it exists) and put it in the store
// this will be used to populate the full breadcrumb trail
- if (newBrowseDashboardsEnabled() && args.urlFolderUid) {
+ if (args.urlFolderUid) {
await dispatch(getFolderByUid(args.urlFolderUid));
}
return getNewDashboardModelData(args.urlFolderUid, args.panelType);
diff --git a/public/app/features/folders/AccessControlFolderPermissions.tsx b/public/app/features/folders/AccessControlFolderPermissions.tsx
deleted file mode 100644
index a3db00633b9..00000000000
--- a/public/app/features/folders/AccessControlFolderPermissions.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import React, { useEffect } from 'react';
-import { connect, ConnectedProps } from 'react-redux';
-
-import { Permissions } from 'app/core/components/AccessControl';
-import { Page } from 'app/core/components/Page/Page';
-import { contextSrv } from 'app/core/core';
-import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
-import { getNavModel } from 'app/core/selectors/navModel';
-import { AccessControlAction, StoreState } from 'app/types';
-
-import { getFolderByUid } from './state/actions';
-import { getLoadingNav } from './state/navModel';
-
-interface RouteProps extends GrafanaRouteComponentProps<{ uid: string }> {}
-
-function mapStateToProps(state: StoreState, props: RouteProps) {
- const uid = props.match.params.uid;
- return {
- uid: uid,
- pageNav: getNavModel(state.navIndex, `folder-permissions-${uid}`, getLoadingNav(1)),
- };
-}
-
-const mapDispatchToProps = {
- getFolderByUid,
-};
-
-const connector = connect(mapStateToProps, mapDispatchToProps);
-export type Props = ConnectedProps;
-
-export const AccessControlFolderPermissions = ({ uid, getFolderByUid, pageNav }: Props) => {
- useEffect(() => {
- getFolderByUid(uid);
- }, [getFolderByUid, uid]);
-
- const canSetPermissions = contextSrv.hasPermission(AccessControlAction.FoldersPermissionsWrite);
-
- return (
-
-
-
-
-
- );
-};
-
-export default connector(AccessControlFolderPermissions);
diff --git a/public/app/features/folders/FolderAlerting.tsx b/public/app/features/folders/FolderAlerting.tsx
deleted file mode 100644
index 3e44468c1ba..00000000000
--- a/public/app/features/folders/FolderAlerting.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react';
-import { useAsync } from 'react-use';
-
-import { Page } from 'app/core/components/Page/Page';
-import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
-import { getNavModel } from 'app/core/selectors/navModel';
-import { useDispatch, useSelector } from 'app/types';
-
-import { AlertsFolderView } from '../alerting/unified/AlertsFolderView';
-
-import { getFolderByUid } from './state/actions';
-import { getLoadingNav } from './state/navModel';
-
-export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {}
-
-const FolderAlerting = ({ match }: OwnProps) => {
- const dispatch = useDispatch();
- const navIndex = useSelector((state) => state.navIndex);
- const folder = useSelector((state) => state.folder);
-
- const uid = match.params.uid;
- const pageNav = getNavModel(navIndex, `folder-alerting-${uid}`, getLoadingNav(1));
-
- const { loading } = useAsync(async () => dispatch(getFolderByUid(uid)), [getFolderByUid, uid]);
-
- return (
-
-
-
-
-
- );
-};
-
-export default FolderAlerting;
diff --git a/public/app/features/folders/FolderLibraryPanelsPage.tsx b/public/app/features/folders/FolderLibraryPanelsPage.tsx
deleted file mode 100644
index 3708da258d0..00000000000
--- a/public/app/features/folders/FolderLibraryPanelsPage.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React, { useState } from 'react';
-import { connect, ConnectedProps } from 'react-redux';
-import { useAsync } from 'react-use';
-
-import { Page } from 'app/core/components/Page/Page';
-
-import { GrafanaRouteComponentProps } from '../../core/navigation/types';
-import { getNavModel } from '../../core/selectors/navModel';
-import { StoreState } from '../../types';
-import { LibraryPanelsSearch } from '../library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch';
-import { OpenLibraryPanelModal } from '../library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal';
-import { LibraryElementDTO } from '../library-panels/types';
-
-import { getFolderByUid } from './state/actions';
-import { getLoadingNav } from './state/navModel';
-
-export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {}
-
-const mapStateToProps = (state: StoreState, props: OwnProps) => {
- const uid = props.match.params.uid;
- return {
- pageNav: getNavModel(state.navIndex, `folder-library-panels-${uid}`, getLoadingNav(1)),
- folderUid: uid,
- };
-};
-
-const mapDispatchToProps = {
- getFolderByUid,
-};
-
-const connector = connect(mapStateToProps, mapDispatchToProps);
-
-export type Props = OwnProps & ConnectedProps;
-
-export function FolderLibraryPanelsPage({ pageNav, getFolderByUid, folderUid }: Props): JSX.Element {
- const { loading } = useAsync(async () => await getFolderByUid(folderUid), [getFolderByUid, folderUid]);
- const [selected, setSelected] = useState(undefined);
-
- return (
-
-
-
- {selected ? setSelected(undefined)} libraryPanel={selected} /> : null}
-
-
- );
-}
-
-export default connector(FolderLibraryPanelsPage);
diff --git a/public/app/features/folders/FolderSettingsPage.test.tsx b/public/app/features/folders/FolderSettingsPage.test.tsx
deleted file mode 100644
index 21873f4d6dd..00000000000
--- a/public/app/features/folders/FolderSettingsPage.test.tsx
+++ /dev/null
@@ -1,189 +0,0 @@
-import { render, screen, within } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import React from 'react';
-import { mockToolkitActionCreator } from 'test/core/redux/mocks';
-import { TestProvider } from 'test/helpers/TestProvider';
-
-import { NavModel } from '@grafana/data';
-import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
-import { ModalManager } from 'app/core/services/ModalManager';
-
-import { FolderSettingsPage, Props } from './FolderSettingsPage';
-import { setFolderTitle } from './state/reducers';
-
-const setup = (propOverrides?: object) => {
- const props: Props = {
- ...getRouteComponentProps(),
- pageNav: {} as NavModel,
- folderUid: '1234',
- folder: {
- id: 0,
- uid: '1234',
- title: 'loading',
- canSave: true,
- canDelete: true,
- url: 'url',
- hasChanged: false,
- version: 1,
- },
- getFolderByUid: jest.fn(),
- setFolderTitle: mockToolkitActionCreator(setFolderTitle),
- saveFolder: jest.fn(),
- deleteFolder: jest.fn(),
- };
-
- Object.assign(props, propOverrides);
-
- render(
-
-
-
- );
-};
-
-describe('FolderSettingsPage', () => {
- it('should render without error', () => {
- expect(() => setup()).not.toThrow();
- });
-
- it('should enable save button when canSave is true and hasChanged is true', () => {
- setup({
- folder: {
- id: 1,
- uid: '1234',
- title: 'loading',
- canSave: true,
- canDelete: true,
- hasChanged: true,
- version: 1,
- },
- });
- const saveButton = screen.getByRole('button', { name: 'Save' });
- expect(saveButton).not.toBeDisabled();
- });
-
- it('should disable save button when canSave is false and hasChanged is false', () => {
- setup({
- folder: {
- id: 1,
- uid: '1234',
- title: 'loading',
- canSave: false,
- canDelete: true,
- hasChanged: false,
- version: 1,
- },
- });
- const saveButton = screen.getByRole('button', { name: 'Save' });
- expect(saveButton).toBeDisabled();
- });
-
- it('should disable save button when canSave is true and hasChanged is false', () => {
- setup({
- folder: {
- id: 1,
- uid: '1234',
- title: 'loading',
- canSave: true,
- canDelete: true,
- hasChanged: false,
- version: 1,
- },
- });
- const saveButton = screen.getByRole('button', { name: 'Save' });
- expect(saveButton).toBeDisabled();
- });
-
- it('should disable save button when canSave is false and hasChanged is true', () => {
- setup({
- folder: {
- id: 1,
- uid: '1234',
- title: 'loading',
- canSave: false,
- canDelete: true,
- hasChanged: true,
- version: 1,
- },
- });
- const saveButton = screen.getByRole('button', { name: 'Save' });
- expect(saveButton).toBeDisabled();
- });
-
- it('should call onSave when the saveButton is clicked', async () => {
- const mockSaveFolder = jest.fn();
- const mockFolder = {
- id: 1,
- uid: '1234',
- title: 'loading',
- canSave: true,
- canDelete: true,
- hasChanged: true,
- version: 1,
- };
- setup({
- folder: mockFolder,
- saveFolder: mockSaveFolder,
- });
- const saveButton = screen.getByRole('button', { name: 'Save' });
- await userEvent.click(saveButton);
- expect(mockSaveFolder).toHaveBeenCalledWith(mockFolder);
- });
-
- it('should disable delete button when canDelete is false', () => {
- setup({
- folder: {
- id: 1,
- uid: '1234',
- title: 'loading',
- canSave: true,
- canDelete: false,
- hasChanged: true,
- version: 1,
- },
- });
- const deleteButton = screen.getByRole('button', { name: 'Delete' });
- expect(deleteButton).toBeDisabled();
- });
-
- it('should enable delete button when canDelete is true', () => {
- setup({
- folder: {
- id: 1,
- uid: '1234',
- title: 'loading',
- canSave: true,
- canDelete: true,
- hasChanged: true,
- version: 1,
- },
- });
- const deleteButton = screen.getByRole('button', { name: 'Delete' });
- expect(deleteButton).not.toBeDisabled();
- });
-
- it('should call the publish event when the deleteButton is clicked', async () => {
- new ModalManager().init();
- const mockDeleteFolder = jest.fn();
- const mockFolder = {
- id: 1,
- uid: '1234',
- title: 'loading',
- canSave: true,
- canDelete: true,
- hasChanged: true,
- version: 1,
- };
- setup({
- folder: mockFolder,
- deleteFolder: mockDeleteFolder,
- });
- const deleteButton = screen.getByRole('button', { name: 'Delete' });
- await userEvent.click(deleteButton);
- const deleteModal = screen.getByRole('dialog', { name: 'Delete' });
- expect(deleteModal).toBeInTheDocument();
- const deleteButtonModal = within(deleteModal).getByRole('button', { name: 'Delete' });
- await userEvent.click(deleteButtonModal);
- expect(mockDeleteFolder).toHaveBeenCalledWith(mockFolder.uid);
- });
-});
diff --git a/public/app/features/folders/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx
deleted file mode 100644
index f24767c17da..00000000000
--- a/public/app/features/folders/FolderSettingsPage.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import React, { PureComponent } from 'react';
-import { connect, ConnectedProps } from 'react-redux';
-
-import { Field, Form, Button, Input, InputControl } from '@grafana/ui';
-import appEvents from 'app/core/app_events';
-import { Page } from 'app/core/components/Page/Page';
-import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
-import { getNavModel } from 'app/core/selectors/navModel';
-import { StoreState } from 'app/types';
-
-import { ShowConfirmModalEvent } from '../../types/events';
-
-import { deleteFolder, getFolderByUid, saveFolder } from './state/actions';
-import { getLoadingNav } from './state/navModel';
-import { setFolderTitle } from './state/reducers';
-
-export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {}
-
-const mapStateToProps = (state: StoreState, props: OwnProps) => {
- const uid = props.match.params.uid;
- return {
- pageNav: getNavModel(state.navIndex, `folder-settings-${uid}`, getLoadingNav(2)),
- folderUid: uid,
- folder: state.folder,
- };
-};
-
-const mapDispatchToProps = {
- getFolderByUid,
- saveFolder,
- setFolderTitle,
- deleteFolder,
-};
-
-const connector = connect(mapStateToProps, mapDispatchToProps);
-
-export type Props = OwnProps & ConnectedProps;
-
-export interface State {
- isLoading: boolean;
-}
-
-export class FolderSettingsPage extends PureComponent {
- constructor(props: Props) {
- super(props);
- this.state = {
- isLoading: false,
- };
- }
-
- componentDidMount() {
- this.props.getFolderByUid(this.props.folderUid);
- }
-
- onTitleChange = (evt: React.ChangeEvent) => {
- this.props.setFolderTitle(evt.target.value);
- };
-
- onSave = () => {
- this.setState({ isLoading: true });
- this.props.saveFolder(this.props.folder);
- this.setState({ isLoading: false });
- };
-
- onDelete = (evt: React.MouseEvent) => {
- evt.stopPropagation();
- evt.preventDefault();
-
- const confirmationText = `Do you want to delete this folder and all its dashboards and alerts?`;
- appEvents.publish(
- new ShowConfirmModalEvent({
- title: 'Delete',
- text: confirmationText,
- icon: 'trash-alt',
- yesText: 'Delete',
- onConfirm: () => {
- this.props.deleteFolder(this.props.folder.uid);
- },
- })
- );
- };
-
- render() {
- const { pageNav, folder } = this.props;
-
- return (
-
-
- Folder settings
-
-
-
- );
- }
-}
-
-export default connector(FolderSettingsPage);
diff --git a/public/app/features/folders/components/NewDashboardsFolder.tsx b/public/app/features/folders/components/NewDashboardsFolder.tsx
deleted file mode 100644
index 8844e162078..00000000000
--- a/public/app/features/folders/components/NewDashboardsFolder.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import React from 'react';
-import { connect, ConnectedProps } from 'react-redux';
-
-import { NavModelItem } from '@grafana/data';
-import { config } from '@grafana/runtime';
-import { Button, Input, Form, Field, HorizontalGroup, LinkButton } from '@grafana/ui';
-import { Page } from 'app/core/components/Page/Page';
-import { useQueryParams } from 'app/core/hooks/useQueryParams';
-
-import { validationSrv } from '../../manage-dashboards/services/ValidationSrv';
-import { createNewFolder } from '../state/actions';
-
-const mapDispatchToProps = {
- createNewFolder,
-};
-
-const connector = connect(null, mapDispatchToProps);
-
-interface OwnProps {}
-
-interface FormModel {
- folderName: string;
-}
-
-type Props = OwnProps & ConnectedProps;
-
-const initialFormModel: FormModel = { folderName: '' };
-
-const pageNav: NavModelItem = {
- text: 'Create a new folder',
- subTitle: 'Folders provide a way to group dashboards and alert rules.',
-};
-
-function NewDashboardsFolder({ createNewFolder }: Props) {
- const [queryParams] = useQueryParams();
- const onSubmit = (formData: FormModel) => {
- const folderUid = typeof queryParams['folderUid'] === 'string' ? queryParams['folderUid'] : undefined;
-
- createNewFolder(formData.folderName, folderUid);
- };
-
- const validateFolderName = (folderName: string) => {
- return validationSrv
- .validateNewFolderName(folderName)
- .then(() => {
- return true;
- })
- .catch((e) => {
- return e.message;
- });
- };
-
- return (
-
-
-
-
-
- );
-}
-
-export default connector(NewDashboardsFolder);
diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts
index 3f9c869289f..e79c97f97f5 100644
--- a/public/app/features/folders/state/actions.ts
+++ b/public/app/features/folders/state/actions.ts
@@ -1,10 +1,6 @@
-import { locationUtil } from '@grafana/data';
-import { getBackendSrv, locationService } from '@grafana/runtime';
-import { notifyApp, updateNavIndex } from 'app/core/actions';
-import { createSuccessNotification } from 'app/core/copy/appNotification';
-import { contextSrv } from 'app/core/core';
+import { updateNavIndex } from 'app/core/actions';
import { backendSrv } from 'app/core/services/backend_srv';
-import { FolderDTO, FolderState, ThunkResult } from 'app/types';
+import { FolderDTO, ThunkResult } from 'app/types';
import { buildNavModel } from './navModel';
import { loadFolder } from './reducers';
@@ -17,32 +13,3 @@ export function getFolderByUid(uid: string): ThunkResult> {
return folder;
};
}
-
-export function saveFolder(folder: FolderState): ThunkResult {
- return async (dispatch) => {
- const res = await backendSrv.put(`/api/folders/${folder.uid}`, {
- title: folder.title,
- version: folder.version,
- });
-
- dispatch(notifyApp(createSuccessNotification('Folder saved')));
- dispatch(loadFolder(res));
- locationService.push(locationUtil.stripBaseFromUrl(`${res.url}/settings`));
- };
-}
-
-export function deleteFolder(uid: string): ThunkResult {
- return async () => {
- await backendSrv.delete(`/api/folders/${uid}?forceDeleteRules=false`);
- locationService.push('/dashboards');
- };
-}
-
-export function createNewFolder(folderName: string, uid?: string): ThunkResult {
- return async (dispatch) => {
- const newFolder = await getBackendSrv().post('/api/folders', { title: folderName, parentUid: uid });
- await contextSrv.fetchUserPermissions();
- dispatch(notifyApp(createSuccessNotification('Folder Created', 'OK')));
- locationService.push(locationUtil.stripBaseFromUrl(newFolder.url));
- };
-}
diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts
index 7e714ffeb21..5c7e0d4ffb4 100644
--- a/public/app/features/folders/state/navModel.ts
+++ b/public/app/features/folders/state/navModel.ts
@@ -3,7 +3,6 @@ import { config } from '@grafana/runtime';
import { t } from 'app/core/internationalization';
import { contextSrv } from 'app/core/services/context_srv';
import { getNavSubTitle } from 'app/core/utils/navBarItem-translations';
-import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
import { AccessControlAction, FolderDTO } from 'app/types';
export const FOLDER_ID = 'manage-folder';
@@ -56,28 +55,6 @@ export function buildNavModel(folder: FolderDTO, parents = folder.parents): NavM
});
}
- if (!newBrowseDashboardsEnabled()) {
- if (folder.canAdmin) {
- model.children!.push({
- active: false,
- icon: 'lock',
- id: getPermissionsTabID(folder.uid),
- text: t('browse-dashboards.manage-folder-nav.permissions', 'Permissions'),
- url: `${folder.url}/permissions`,
- });
- }
-
- if (folder.canSave) {
- model.children!.push({
- active: false,
- icon: 'cog',
- id: getSettingsTabID(folder.uid),
- text: t('browse-dashboards.manage-folder-nav.settings', 'Settings'),
- url: `${folder.url}/settings`,
- });
- }
- }
-
return model;
}
diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts
index 5474afacf21..15193dc2f23 100644
--- a/public/app/features/manage-dashboards/state/actions.ts
+++ b/public/app/features/manage-dashboards/state/actions.ts
@@ -4,7 +4,7 @@ import { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
-import { DashboardDTO, FolderInfo, PermissionLevelString, SearchQueryType, ThunkResult } from 'app/types';
+import { FolderInfo, PermissionLevelString, SearchQueryType, ThunkResult } from 'app/types';
import {
Input,
@@ -282,60 +282,6 @@ export async function moveFolders(folderUIDs: string[], toFolder: FolderInfo) {
return result;
}
-export function moveDashboards(dashboardUids: string[], toFolder: FolderInfo) {
- const tasks = [];
-
- for (const uid of dashboardUids) {
- tasks.push(createTask(moveDashboard, true, uid, toFolder));
- }
-
- return executeInOrder(tasks).then((result: any) => {
- return {
- totalCount: result.length,
- successCount: result.filter((res: any) => res.succeeded).length,
- alreadyInFolderCount: result.filter((res: any) => res.alreadyInFolder).length,
- };
- });
-}
-
-async function moveDashboard(uid: string, toFolder: FolderInfo) {
- const fullDash: DashboardDTO = await getBackendSrv().get(`/api/dashboards/uid/${uid}`);
-
- if (
- ((fullDash.meta.folderUid === undefined || fullDash.meta.folderUid === null) && toFolder.uid === '') ||
- fullDash.meta.folderUid === toFolder.uid
- ) {
- return { alreadyInFolder: true };
- }
-
- const options = {
- dashboard: fullDash.dashboard,
- folderUid: toFolder.uid,
- overwrite: false,
- };
-
- try {
- await saveDashboard(options);
- return { succeeded: true };
- } catch (err) {
- if (isFetchError(err)) {
- if (err.data?.status !== 'plugin-dashboard') {
- return { succeeded: false };
- }
-
- err.isHandled = true;
- }
- options.overwrite = true;
-
- try {
- await saveDashboard(options);
- return { succeeded: true };
- } catch (e) {
- return { succeeded: false };
- }
- }
-}
-
function createTask(fn: (...args: any[]) => Promise, ignoreRejections: boolean, ...args: any[]) {
return async (result: any) => {
try {
diff --git a/public/app/features/search/components/DashboardActions.tsx b/public/app/features/search/components/DashboardActions.tsx
deleted file mode 100644
index 12dc125b50e..00000000000
--- a/public/app/features/search/components/DashboardActions.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import React, { useMemo, useState } from 'react';
-
-import { config, reportInteraction } from '@grafana/runtime';
-import { Menu, Dropdown, Button, Icon, HorizontalGroup } from '@grafana/ui';
-import { FolderDTO } from 'app/types';
-
-import { MoveToFolderModal } from '../page/components/MoveToFolderModal';
-import { getImportPhrase, getNewDashboardPhrase, getNewFolderPhrase, getNewPhrase } from '../tempI18nPhrases';
-
-export interface Props {
- folder: FolderDTO | undefined;
- canCreateFolders?: boolean;
- canCreateDashboards?: boolean;
-}
-
-export const DashboardActions = ({ folder, canCreateFolders = false, canCreateDashboards = false }: Props) => {
- const [isMoveModalOpen, setIsMoveModalOpen] = useState(false);
- const canMove = config.featureToggles.nestedFolders && (folder?.canSave ?? false);
-
- const moveSelection = useMemo(
- () => new Map>([['folder', new Set(folder?.uid ? [folder.uid] : [])]]),
- [folder]
- );
-
- const actionUrl = (type: string) => {
- let url = `dashboard/${type}`;
- const isTypeNewFolder = type === 'new_folder';
-
- if (isTypeNewFolder) {
- url = `dashboards/folder/new/`;
- }
-
- if (folder?.uid) {
- url += `?folderUid=${folder.uid}`;
- }
-
- return url;
- };
-
- const MenuActions = () => {
- return (
-
- {canCreateDashboards && (
-
- reportInteraction('grafana_menu_item_clicked', { url: actionUrl('new'), from: '/dashboards' })
- }
- />
- )}
- {canCreateFolders && (config.featureToggles.nestedFolders || !folder?.uid) && (
-
- reportInteraction('grafana_menu_item_clicked', { url: actionUrl('new_folder'), from: '/dashboards' })
- }
- />
- )}
- {canCreateDashboards && (
-
- reportInteraction('grafana_menu_item_clicked', { url: actionUrl('import'), from: '/dashboards' })
- }
- />
- )}
-
- );
- };
-
- return (
- <>
-
-
- {canMove && (
- setIsMoveModalOpen(true)} icon="exchange-alt" variant="secondary">
- Move
-
- )}
-
-
- {getNewPhrase()}
-
-
-
-
-
-
- {canMove && isMoveModalOpen && (
- {}} results={moveSelection} onDismiss={() => setIsMoveModalOpen(false)} />
- )}
- >
- );
-};
diff --git a/public/app/features/search/components/DashboardListPage.tsx b/public/app/features/search/components/DashboardListPage.tsx
deleted file mode 100644
index 48a744064e5..00000000000
--- a/public/app/features/search/components/DashboardListPage.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { css } from '@emotion/css';
-import React, { memo } from 'react';
-import { useAsync } from 'react-use';
-
-import { locationUtil, NavModelItem } from '@grafana/data';
-import { locationService } from '@grafana/runtime';
-import { Page } from 'app/core/components/Page/Page';
-import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
-import NewBrowseDashboardsPage from 'app/features/browse-dashboards/BrowseDashboardsPage';
-import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
-import { FolderDTO } from 'app/types';
-
-import { loadFolderPage } from '../loaders';
-
-import ManageDashboardsNew from './ManageDashboardsNew';
-
-export interface DashboardListPageRouteParams {
- uid?: string;
- slug?: string;
-}
-
-interface Props extends GrafanaRouteComponentProps {}
-
-export const DashboardListPageFeatureToggle = memo((props: Props) => {
- if (newBrowseDashboardsEnabled()) {
- return ;
- }
-
- return ;
-});
-DashboardListPageFeatureToggle.displayName = 'DashboardListPageFeatureToggle';
-
-const DashboardListPage = memo(({ match, location }: Props) => {
- const { loading, value } = useAsync<() => Promise<{ folder?: FolderDTO; pageNav?: NavModelItem }>>(() => {
- const uid = match.params.uid;
- const url = location.pathname;
-
- if (!uid || !url.startsWith('/dashboards')) {
- return Promise.resolve({});
- }
-
- return loadFolderPage(uid!).then(({ folder, folderNav }) => {
- const path = locationUtil.stripBaseFromUrl(folder.url);
-
- if (path !== location.pathname) {
- locationService.replace(path);
- }
-
- return { folder, pageNav: folderNav };
- });
- }, [match.params.uid]);
-
- return (
-
-
-
-
-
- );
-});
-
-DashboardListPage.displayName = 'DashboardListPage';
-
-export default DashboardListPageFeatureToggle;
diff --git a/public/app/features/search/components/ManageDashboards.test.tsx b/public/app/features/search/components/ManageDashboards.test.tsx
deleted file mode 100644
index e07cd425f10..00000000000
--- a/public/app/features/search/components/ManageDashboards.test.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { render, screen, waitFor } from '@testing-library/react';
-import React from 'react';
-
-import { contextSrv } from 'app/core/services/context_srv';
-import { FolderDTO } from 'app/types';
-
-import ManageDashboardsNew from './ManageDashboardsNew';
-
-jest.mock('app/core/services/context_srv', () => {
- const originMock = jest.requireActual('app/core/services/context_srv');
-
- return {
- ...originMock,
- contextSrv: {
- ...originMock.context_srv,
- user: {},
- hasPermission: jest.fn(() => false),
- },
- };
-});
-
-const setup = async (options?: { folder?: FolderDTO }) => {
- const { folder = {} as FolderDTO } = options || {};
-
- const { rerender } = await waitFor(() => render( ));
-
- return { rerender };
-};
-
-jest.spyOn(console, 'error').mockImplementation();
-
-describe('ManageDashboards', () => {
- beforeEach(() => {
- (contextSrv.hasPermission as jest.Mock).mockClear();
- });
-
- it("should hide and show dashboard actions based on user's permissions", async () => {
- (contextSrv.hasPermission as jest.Mock).mockReturnValue(false);
-
- const { rerender } = await setup();
-
- expect(screen.queryByRole('button', { name: /new/i })).not.toBeInTheDocument();
-
- (contextSrv.hasPermission as jest.Mock).mockReturnValue(true);
- await waitFor(() => rerender( ));
-
- expect(screen.getByRole('button', { name: /new/i })).toBeInTheDocument();
- });
-});
diff --git a/public/app/features/search/components/ManageDashboardsNew.tsx b/public/app/features/search/components/ManageDashboardsNew.tsx
deleted file mode 100644
index 6dd4643276e..00000000000
--- a/public/app/features/search/components/ManageDashboardsNew.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { css, cx } from '@emotion/css';
-import React, { useEffect } from 'react';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { useStyles2, FilterInput } from '@grafana/ui';
-import { contextSrv } from 'app/core/services/context_srv';
-import { FolderDTO, AccessControlAction } from 'app/types';
-
-import { useKeyNavigationListener } from '../hooks/useSearchKeyboardSelection';
-import { SearchView } from '../page/components/SearchView';
-import { getSearchStateManager } from '../state/SearchStateManager';
-import { getSearchPlaceholder } from '../tempI18nPhrases';
-
-import { DashboardActions } from './DashboardActions';
-
-export interface Props {
- folder?: FolderDTO;
-}
-
-export const ManageDashboardsNew = React.memo(({ folder }: Props) => {
- const styles = useStyles2(getStyles);
- // since we don't use "query" from use search... it is not actually loaded from the URL!
- const stateManager = getSearchStateManager();
- const state = stateManager.useState();
- const { onKeyDown, keyboardEvents } = useKeyNavigationListener();
-
- // TODO: we need to refactor DashboardActions to use folder.uid instead
-
- const folderUid = folder?.uid;
- const canSave = folder?.canSave;
- const { isEditor } = contextSrv;
- const hasEditPermissionInFolders = folder ? canSave : contextSrv.hasEditPermissionInFolders;
- const canCreateFolders = contextSrv.hasPermission(AccessControlAction.FoldersCreate);
- const canCreateDashboards = folderUid
- ? contextSrv.hasPermissionInMetadata(AccessControlAction.DashboardsCreate, folder)
- : contextSrv.hasPermission(AccessControlAction.DashboardsCreate);
- const viewActions = (folder === undefined && canCreateFolders) || canCreateDashboards;
-
- useEffect(() => stateManager.initStateFromUrl(folder?.uid), [folder?.uid, stateManager]);
-
- return (
- <>
-
-
- stateManager.onQueryChange(e)}
- onKeyDown={onKeyDown}
- // eslint-disable-next-line jsx-a11y/no-autofocus
- autoFocus
- spellCheck={false}
- placeholder={getSearchPlaceholder(state.includePanels)}
- escapeRegex={false}
- className={styles.searchInput}
- />
-
- {viewActions && (
-
- )}
-
-
-
- >
- );
-});
-
-ManageDashboardsNew.displayName = 'ManageDashboardsNew';
-
-export default ManageDashboardsNew;
-
-const getStyles = (theme: GrafanaTheme2) => ({
- actionBar: css`
- ${theme.breakpoints.down('sm')} {
- flex-wrap: wrap;
- }
- `,
- inputWrapper: css`
- ${theme.breakpoints.down('sm')} {
- margin-right: 0 !important;
- }
- `,
- searchInput: css`
- margin-bottom: 6px;
- min-height: ${theme.spacing(4)};
- `,
- unsupported: css`
- padding: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- font-size: 18px;
- `,
- noResults: css`
- padding: ${theme.v1.spacing.md};
- background: ${theme.v1.colors.bg2};
- font-style: italic;
- margin-top: ${theme.v1.spacing.md};
- `,
-});
diff --git a/public/app/features/search/components/SearchCard.tsx b/public/app/features/search/components/SearchCard.tsx
deleted file mode 100644
index 4219f5d5a99..00000000000
--- a/public/app/features/search/components/SearchCard.tsx
+++ /dev/null
@@ -1,262 +0,0 @@
-import { css } from '@emotion/css';
-import { Placement, Rect } from '@popperjs/core';
-import React, { useCallback, useRef, useState } from 'react';
-import SVG from 'react-inlinesvg';
-import { usePopper } from 'react-popper';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { selectors } from '@grafana/e2e-selectors';
-import { Icon, Portal, TagList, useTheme2 } from '@grafana/ui';
-import { backendSrv } from 'app/core/services/backend_srv';
-
-import { DashboardViewItem, OnToggleChecked } from '../types';
-
-import { SearchCardExpanded } from './SearchCardExpanded';
-import { SearchCheckbox } from './SearchCheckbox';
-
-const DELAY_BEFORE_EXPANDING = 500;
-
-export interface Props {
- editable?: boolean;
- item: DashboardViewItem;
- isSelected?: boolean;
- onTagSelected?: (name: string) => any;
- onToggleChecked?: OnToggleChecked;
- onClick?: (event: React.MouseEvent) => void;
-}
-
-export function getThumbnailURL(uid: string, isLight?: boolean) {
- return `/api/dashboards/uid/${uid}/img/thumb/${isLight ? 'light' : 'dark'}`;
-}
-
-export function SearchCard({ editable, item, isSelected, onTagSelected, onToggleChecked, onClick }: Props) {
- const [hasImage, setHasImage] = useState(true);
- const [lastUpdated, setLastUpdated] = useState(null);
- const [showExpandedView, setShowExpandedView] = useState(false);
- const timeout = useRef(null);
-
- // Popper specific logic
- const offsetCallback = useCallback(
- ({ placement, reference, popper }: { placement: Placement; reference: Rect; popper: Rect }) => {
- let result: [number, number] = [0, 0];
- if (placement === 'bottom' || placement === 'top') {
- result = [0, -(reference.height + popper.height) / 2];
- } else if (placement === 'left' || placement === 'right') {
- result = [-(reference.width + popper.width) / 2, 0];
- }
- return result;
- },
- []
- );
- const [markerElement, setMarkerElement] = React.useState(null);
- const [popperElement, setPopperElement] = React.useState(null);
- const { styles: popperStyles, attributes } = usePopper(markerElement, popperElement, {
- modifiers: [
- {
- name: 'offset',
- options: {
- offset: offsetCallback,
- },
- },
- ],
- });
-
- const theme = useTheme2();
- const imageSrc = getThumbnailURL(item.uid!, theme.isLight);
- const styles = getStyles(
- theme,
- markerElement?.getBoundingClientRect().width,
- popperElement?.getBoundingClientRect().width
- );
-
- const onShowExpandedView = async () => {
- setShowExpandedView(true);
- if (item.uid && !lastUpdated) {
- const dashboard = await backendSrv.getDashboardByUid(item.uid);
- const { updated } = dashboard.meta;
- if (updated) {
- setLastUpdated(new Date(updated).toLocaleString());
- } else {
- setLastUpdated(null);
- }
- }
- };
-
- const onMouseEnter = () => {
- timeout.current = window.setTimeout(onShowExpandedView, DELAY_BEFORE_EXPANDING);
- };
-
- const onMouseMove = () => {
- if (timeout.current) {
- window.clearTimeout(timeout.current);
- }
- timeout.current = window.setTimeout(onShowExpandedView, DELAY_BEFORE_EXPANDING);
- };
-
- const onMouseLeave = () => {
- if (timeout.current) {
- window.clearTimeout(timeout.current);
- }
- setShowExpandedView(false);
- };
-
- const onCheckboxClick = (ev: React.MouseEvent) => {
- ev.stopPropagation();
- ev.preventDefault();
-
- onToggleChecked?.(item);
- };
-
- const onTagClick = (tag: string, ev: React.MouseEvent) => {
- ev.stopPropagation();
- ev.preventDefault();
-
- onTagSelected?.(tag);
- };
-
- return (
- setMarkerElement(ref as unknown as HTMLDivElement)}
- onMouseEnter={onMouseEnter}
- onMouseLeave={onMouseLeave}
- onMouseMove={onMouseMove}
- onClick={onClick}
- >
-
-
- {hasImage ? (
-
setHasImage(false)}
- />
- ) : (
-
- {item.icon ? (
-
- ) : (
-
- )}
-
- )}
-
-
- {showExpandedView && (
-
-
-
-
-
- )}
-
- );
-}
-
-const getStyles = (theme: GrafanaTheme2, markerWidth = 0, popperWidth = 0) => {
- const IMAGE_HORIZONTAL_MARGIN = theme.spacing(4);
-
- return {
- card: css`
- background-color: ${theme.colors.background.secondary};
- border: 1px solid ${theme.colors.border.medium};
- border-radius: ${theme.shape.radius.default};
- display: flex;
- flex-direction: column;
-
- &:hover {
- background-color: ${theme.colors.emphasize(theme.colors.background.secondary, 0.03)};
- }
- `,
- checkbox: css`
- left: 0;
- margin: ${theme.spacing(1)};
- position: absolute;
- top: 0;
- `,
- expandedView: css`
- @keyframes expand {
- 0% {
- transform: scale(${markerWidth / popperWidth});
- }
- 100% {
- transform: scale(1);
- }
- }
-
- animation: expand ${theme.transitions.duration.shortest}ms ease-in-out 0s 1 normal;
- background-color: ${theme.colors.emphasize(theme.colors.background.secondary, 0.03)};
- `,
- image: css`
- aspect-ratio: 4 / 3;
- box-shadow: ${theme.shadows.z1};
- margin: ${theme.spacing(1)} ${IMAGE_HORIZONTAL_MARGIN} 0;
- width: calc(100% - (2 * ${IMAGE_HORIZONTAL_MARGIN}));
- `,
- imageContainer: css`
- flex: 1;
- position: relative;
-
- &:after {
- background: linear-gradient(180deg, rgba(196, 196, 196, 0) 0%, rgba(127, 127, 127, 0.25) 100%);
- bottom: 0;
- content: '';
- left: 0;
- margin: ${theme.spacing(1)} ${IMAGE_HORIZONTAL_MARGIN} 0;
- position: absolute;
- right: 0;
- top: 0;
- }
- `,
- imagePlaceholder: css`
- align-items: center;
- aspect-ratio: 4 / 3;
- color: ${theme.colors.text.secondary};
- display: flex;
- justify-content: center;
- margin: ${theme.spacing(1)} ${IMAGE_HORIZONTAL_MARGIN} 0;
- width: calc(100% - (2 * ${IMAGE_HORIZONTAL_MARGIN}));
- `,
- info: css`
- align-items: center;
- background-color: ${theme.colors.background.canvas};
- border-bottom-left-radius: ${theme.shape.radius.default};
- border-bottom-right-radius: ${theme.shape.radius.default};
- display: flex;
- height: ${theme.spacing(7)};
- gap: ${theme.spacing(1)};
- padding: 0 ${theme.spacing(2)};
- z-index: 1;
- `,
- portal: css`
- pointer-events: none;
- `,
- title: css`
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- `,
- };
-};
diff --git a/public/app/features/search/components/SearchCardExpanded.tsx b/public/app/features/search/components/SearchCardExpanded.tsx
deleted file mode 100644
index 86028c590cf..00000000000
--- a/public/app/features/search/components/SearchCardExpanded.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-import { css } from '@emotion/css';
-import classNames from 'classnames';
-import React, { useState } from 'react';
-import SVG from 'react-inlinesvg';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { Icon, Spinner, TagList, useTheme2 } from '@grafana/ui';
-
-import { DashboardViewItem } from '../types';
-
-import { getThumbnailURL } from './SearchCard';
-
-export interface Props {
- className?: string;
- imageHeight: number;
- imageWidth: number;
- item: DashboardViewItem;
- lastUpdated?: string | null;
- onClick?: (event: React.MouseEvent) => void;
-}
-
-export function SearchCardExpanded({ className, imageHeight, imageWidth, item, lastUpdated, onClick }: Props) {
- const theme = useTheme2();
- const [hasImage, setHasImage] = useState(true);
- const imageSrc = getThumbnailURL(item.uid!, theme.isLight);
- const styles = getStyles(theme, imageHeight, imageWidth);
-
- const folderTitle = item.parentTitle || 'General';
-
- return (
-
-
- {hasImage ? (
-
setHasImage(true)}
- onError={() => setHasImage(false)}
- />
- ) : (
-
- {item.icon ? (
-
- ) : (
-
- )}
-
- )}
-
-
-
-
-
{item.title}
-
-
- {folderTitle}
-
-
- {lastUpdated !== null && (
-
-
Last updated
- {lastUpdated ?
{lastUpdated}
:
}
-
- )}
-
-
-
-
-
-
- );
-}
-
-const getStyles = (theme: GrafanaTheme2, imageHeight: Props['imageHeight'], imageWidth: Props['imageWidth']) => {
- const IMAGE_HORIZONTAL_MARGIN = theme.spacing(4);
-
- return {
- card: css`
- background-color: ${theme.colors.background.secondary};
- border: 1px solid ${theme.colors.border.medium};
- border-radius: 4px;
- box-shadow: ${theme.shadows.z3};
- display: flex;
- flex-direction: column;
- height: 100%;
- max-width: calc(${imageWidth}px + (${IMAGE_HORIZONTAL_MARGIN} * 2))};
- width: 100%;
- `,
- folder: css`
- align-items: center;
- color: ${theme.colors.text.secondary};
- display: flex;
- font-size: ${theme.typography.size.sm};
- gap: ${theme.spacing(0.5)};
- `,
- image: css`
- box-shadow: ${theme.shadows.z2};
- height: ${imageHeight}px;
- margin: ${theme.spacing(1)} calc(${IMAGE_HORIZONTAL_MARGIN} - 1px) 0;
- width: ${imageWidth}px;
- `,
- imageContainer: css`
- flex: 1;
- position: relative;
-
- &:after {
- background: linear-gradient(180deg, rgba(196, 196, 196, 0) 0%, rgba(127, 127, 127, 0.25) 100%);
- bottom: 0;
- content: '';
- left: 0;
- margin: ${theme.spacing(1)} calc(${IMAGE_HORIZONTAL_MARGIN} - 1px) 0;
- position: absolute;
- right: 0;
- top: 0;
- }
- `,
- imagePlaceholder: css`
- align-items: center;
- color: ${theme.colors.text.secondary};
- display: flex;
- height: ${imageHeight}px;
- justify-content: center;
- margin: ${theme.spacing(1)} ${IMAGE_HORIZONTAL_MARGIN} 0;
- width: ${imageWidth}px;
- `,
- info: css`
- background-color: ${theme.colors.background.canvas};
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
- display: flex;
- flex-direction: column;
- min-height: ${theme.spacing(7)};
- gap: ${theme.spacing(1)};
- padding: ${theme.spacing(1)} ${theme.spacing(2)};
- z-index: 1;
- `,
- infoHeader: css`
- display: flex;
- gap: ${theme.spacing(1)};
- justify-content: space-between;
- `,
- tagList: css`
- justify-content: flex-start;
- `,
- titleContainer: css`
- display: flex;
- flex-direction: column;
- gap: ${theme.spacing(0.5)};
- `,
- updateContainer: css`
- align-items: flex-end;
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
- font-size: ${theme.typography.bodySmall.fontSize};
- gap: ${theme.spacing(0.5)};
- `,
- update: css`
- color: ${theme.colors.text.secondary};
- text-align: right;
- `,
- };
-};
diff --git a/public/app/features/search/components/SearchCheckbox.tsx b/public/app/features/search/components/SearchCheckbox.tsx
deleted file mode 100644
index c80ea84db75..00000000000
--- a/public/app/features/search/components/SearchCheckbox.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React, { memo } from 'react';
-
-import { Checkbox } from '@grafana/ui';
-
-interface Props {
- checked?: boolean;
- onClick?: React.MouseEventHandler;
- className?: string;
- editable?: boolean;
- 'aria-label'?: string;
-}
-
-export const SearchCheckbox = memo(
- ({ onClick, className, checked = false, editable = false, 'aria-label': ariaLabel }: Props) => {
- return editable ? (
-
- ) : null;
- }
-);
-
-SearchCheckbox.displayName = 'SearchCheckbox';
diff --git a/public/app/features/search/components/SearchItem.test.tsx b/public/app/features/search/components/SearchItem.test.tsx
deleted file mode 100644
index 50c92b69f1a..00000000000
--- a/public/app/features/search/components/SearchItem.test.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import { fireEvent, render, screen } from '@testing-library/react';
-import React from 'react';
-
-import { selectors } from '@grafana/e2e-selectors';
-
-import { DashboardViewItem } from '../types';
-
-import { Props, SearchItem } from './SearchItem';
-
-beforeEach(() => {
- jest.clearAllMocks();
-});
-
-const data: DashboardViewItem = {
- kind: 'dashboard' as const,
- uid: 'lBdLINUWk',
- title: 'Test 1',
- url: '/d/lBdLINUWk/test1',
- tags: ['Tag1', 'Tag2'],
-};
-
-const setup = (propOverrides?: Partial) => {
- const props: Props = {
- item: data,
- onTagSelected: jest.fn(),
- editable: false,
- };
-
- Object.assign(props, propOverrides);
-
- render( );
-};
-
-describe('SearchItem', () => {
- it('should render the item', () => {
- setup();
- expect(screen.getAllByTestId(selectors.components.Search.dashboardItem('Test 1'))).toHaveLength(1);
- expect(screen.getAllByText('Test 1')).toHaveLength(1);
- });
-
- it('should toggle items when checked', () => {
- const mockedOnToggleChecked = jest.fn();
- setup({ editable: true, onToggleChecked: mockedOnToggleChecked });
- const checkbox = screen.getByRole('checkbox');
- expect(checkbox).not.toBeChecked();
- fireEvent.click(checkbox);
- expect(mockedOnToggleChecked).toHaveBeenCalledTimes(1);
- expect(mockedOnToggleChecked).toHaveBeenCalledWith(data);
- });
-
- it('should mark items as checked', () => {
- setup({ editable: true, isSelected: true });
- expect(screen.getByRole('checkbox')).toBeChecked();
- });
-
- it("should render item's tags", () => {
- setup();
- expect(screen.getAllByText(/tag/i)).toHaveLength(2);
- });
-
- it('should select the tag on tag click', () => {
- const mockOnTagSelected = jest.fn();
- setup({ onTagSelected: mockOnTagSelected });
- fireEvent.click(screen.getByText('Tag1'));
- expect(mockOnTagSelected).toHaveBeenCalledTimes(1);
- expect(mockOnTagSelected).toHaveBeenCalledWith('Tag1');
- });
-});
diff --git a/public/app/features/search/components/SearchItem.tsx b/public/app/features/search/components/SearchItem.tsx
deleted file mode 100644
index 60de8daf986..00000000000
--- a/public/app/features/search/components/SearchItem.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import { css } from '@emotion/css';
-import React, { useCallback } from 'react';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
-import { config } from '@grafana/runtime';
-import { Card, Icon, IconName, TagList, useStyles2 } from '@grafana/ui';
-import { t } from 'app/core/internationalization';
-
-import { SEARCH_ITEM_HEIGHT } from '../constants';
-import { getIconForKind } from '../service/utils';
-import { DashboardViewItem, OnToggleChecked } from '../types';
-
-import { SearchCheckbox } from './SearchCheckbox';
-
-export interface Props {
- item: DashboardViewItem;
- isSelected?: boolean;
- editable?: boolean;
- onTagSelected: (name: string) => any;
- onToggleChecked?: OnToggleChecked;
- onClickItem?: (event: React.MouseEvent) => void;
-}
-
-const selectors = e2eSelectors.components.Search;
-
-const getIconFromMeta = (meta = ''): IconName => {
- const metaIconMap = new Map([
- ['errors', 'info-circle'],
- ['views', 'eye'],
- ]);
-
- return metaIconMap.has(meta) ? metaIconMap.get(meta)! : 'sort-amount-down';
-};
-
-/** @deprecated */
-export const SearchItem = ({ item, isSelected, editable, onToggleChecked, onTagSelected, onClickItem }: Props) => {
- const styles = useStyles2(getStyles);
- const tagSelected = useCallback(
- (tag: string, event: React.MouseEvent) => {
- event.stopPropagation();
- event.preventDefault();
- onTagSelected(tag);
- },
- [onTagSelected]
- );
-
- const handleCheckboxClick = useCallback(
- (ev: React.MouseEvent) => {
- ev.stopPropagation();
-
- if (onToggleChecked) {
- onToggleChecked(item);
- }
- },
- [item, onToggleChecked]
- );
-
- const description = config.featureToggles.nestedFolders ? (
- <>
- {kindName(item.kind)}
- >
- ) : (
- <>
- {item.parentTitle || 'General'}
- >
- );
-
- return (
-
-
-
-
- {item.title}
-
-
- {description}
-
- {item.sortMetaName && (
-
-
- {item.sortMeta} {item.sortMetaName}
-
- )}
-
-
- `Filter by tag "${tag}"`} />
-
-
-
- );
-};
-
-function kindName(kind: DashboardViewItem['kind']) {
- switch (kind) {
- case 'folder':
- return t('search.result-kind.folder', 'Folder');
- case 'dashboard':
- return t('search.result-kind.dashboard', 'Dashboard');
- case 'panel':
- return t('search.result-kind.panel', 'Panel');
- }
-}
-
-const getStyles = (theme: GrafanaTheme2) => {
- return {
- cardContainer: css`
- display: flex;
- align-items: center;
- margin-bottom: ${theme.spacing(0.75)};
- `,
- card: css`
- padding: ${theme.spacing(1)} ${theme.spacing(2)};
- margin-bottom: 0;
- `,
- checkbox: css({
- marginRight: theme.spacing(1),
- }),
- metaContainer: css`
- display: flex;
- align-items: center;
- margin-right: ${theme.spacing(1)};
-
- svg {
- margin-right: ${theme.spacing(0.5)};
- }
- `,
- };
-};
diff --git a/public/app/features/search/index.ts b/public/app/features/search/index.ts
deleted file mode 100644
index f8282672f91..00000000000
--- a/public/app/features/search/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { SearchItem } from './components/SearchItem';
diff --git a/public/app/features/search/page/components/ConfirmDeleteModal.test.tsx b/public/app/features/search/page/components/ConfirmDeleteModal.test.tsx
deleted file mode 100644
index a782e526a7a..00000000000
--- a/public/app/features/search/page/components/ConfirmDeleteModal.test.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import React from 'react';
-
-import { config } from 'app/core/config';
-
-import { ConfirmDeleteModal } from './ConfirmDeleteModal';
-
-describe('ConfirmModal', () => {
- it('should render correct title, body, dismiss-, cancel- and delete-text', () => {
- const selectedItems = new Map([['dashboard', new Set(['uid1', 'uid2'])]]);
-
- render( {}} results={selectedItems} onDismiss={() => {}} />);
-
- expect(screen.getByRole('heading', { name: 'Delete' })).toBeInTheDocument();
- expect(screen.getByText('Do you want to delete the 2 selected dashboards?')).toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument();
-
- expect(screen.queryByPlaceholderText('Type "delete" to confirm')).not.toBeInTheDocument();
- });
-
- describe('with nestedFolders feature flag', () => {
- let originalNestedFoldersValue = config.featureToggles.nestedFolders;
-
- beforeAll(() => {
- originalNestedFoldersValue = config.featureToggles.nestedFolders;
- config.featureToggles.nestedFolders = true;
- });
-
- afterAll(() => {
- config.featureToggles.nestedFolders = originalNestedFoldersValue;
- });
-
- it("should ask to type 'delete' to confirm when a folder is selected", async () => {
- const selectedItems = new Map([
- ['dashboard', new Set(['uid1', 'uid2'])],
- ['folder', new Set(['uid3'])],
- ]);
-
- render( {}} results={selectedItems} onDismiss={() => {}} />);
-
- expect(screen.getByPlaceholderText('Type "delete" to confirm')).toBeInTheDocument();
- });
- });
-});
diff --git a/public/app/features/search/page/components/ConfirmDeleteModal.tsx b/public/app/features/search/page/components/ConfirmDeleteModal.tsx
deleted file mode 100644
index ec7b7808b8e..00000000000
--- a/public/app/features/search/page/components/ConfirmDeleteModal.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { css } from '@emotion/css';
-import React from 'react';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { ConfirmModal, useStyles2 } from '@grafana/ui';
-import { config } from 'app/core/config';
-import { deleteFoldersAndDashboards } from 'app/features/manage-dashboards/state/actions';
-
-import { OnMoveOrDeleleSelectedItems } from '../../types';
-
-interface Props {
- onDeleteItems: OnMoveOrDeleleSelectedItems;
- results: Map>;
- onDismiss: () => void;
-}
-
-export const ConfirmDeleteModal = ({ results, onDeleteItems, onDismiss }: Props) => {
- const styles = useStyles2(getStyles);
-
- const dashboards = Array.from(results.get('dashboard') ?? []);
- const folders = Array.from(results.get('folder') ?? []);
-
- const folderCount = folders.length;
- const dashCount = dashboards.length;
-
- let text = 'Do you want to delete the ';
- let subtitle;
- const dashEnding = dashCount === 1 ? '' : 's';
- const folderEnding = folderCount === 1 ? '' : 's';
-
- if (folderCount > 0 && dashCount > 0) {
- text += `selected folder${folderEnding} and dashboard${dashEnding}?\n`;
- subtitle = `All dashboards and alerts of the selected folder${folderEnding} will also be deleted`;
- } else if (folderCount > 0) {
- text += `selected folder${folderEnding} and all ${folderCount === 1 ? 'its' : 'their'} dashboards and alerts?`;
- } else {
- text += `${dashCount} selected dashboard${dashEnding}?`;
- }
-
- const deleteItems = () => {
- deleteFoldersAndDashboards(folders, dashboards).then(() => {
- onDeleteItems();
- onDismiss();
- });
- };
-
- const requireDoubleConfirm = config.featureToggles.nestedFolders && folderCount > 0;
-
- return (
-
- {text} {subtitle && {subtitle}
}
- >
- }
- confirmText="Delete"
- confirmationText={requireDoubleConfirm ? 'delete' : undefined}
- onConfirm={deleteItems}
- onDismiss={onDismiss}
- />
- );
-};
-
-const getStyles = (theme: GrafanaTheme2) => ({
- subtitle: css`
- font-size: ${theme.typography.fontSize}px;
- padding-top: ${theme.spacing(2)};
- `,
-});
diff --git a/public/app/features/search/page/components/FolderSection.test.tsx b/public/app/features/search/page/components/FolderSection.test.tsx
deleted file mode 100644
index cd2d3483715..00000000000
--- a/public/app/features/search/page/components/FolderSection.test.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-import { render, screen, act } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import React from 'react';
-
-import { DataFrame, DataFrameView, FieldType } from '@grafana/data';
-
-import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from '../../service';
-import { DashboardSearchItemType, DashboardViewItem } from '../../types';
-
-import { FolderSection } from './FolderSection';
-
-describe('FolderSection', () => {
- let grafanaSearcherSpy: jest.SpyInstance;
- const mockOnTagSelected = jest.fn();
- const mockSelectionToggle = jest.fn();
- const mockSelection = jest.fn();
- const mockSection: DashboardViewItem = {
- kind: 'folder',
- uid: 'my-folder',
- title: 'My folder',
- };
-
- // need to make sure we clear localStorage
- // otherwise tests can interfere with each other and the starting expanded state of the component
- afterEach(() => {
- window.localStorage.clear();
- });
-
- describe('when there are no results', () => {
- const emptySearchData: DataFrame = {
- fields: [
- { name: 'kind', type: FieldType.string, config: {}, values: [] },
- { name: 'name', type: FieldType.string, config: {}, values: [] },
- { name: 'uid', type: FieldType.string, config: {}, values: [] },
- { name: 'url', type: FieldType.string, config: {}, values: [] },
- { name: 'tags', type: FieldType.other, config: {}, values: [] },
- { name: 'location', type: FieldType.string, config: {}, values: [] },
- ],
- length: 0,
- };
-
- const mockSearchResult: QueryResponse = {
- isItemLoaded: jest.fn(),
- loadMoreItems: jest.fn(),
- totalRows: emptySearchData.length,
- view: new DataFrameView(emptySearchData),
- };
-
- beforeAll(() => {
- grafanaSearcherSpy = jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockSearchResult);
- });
-
- it('shows the folder title as the header', async () => {
- render( );
- expect(await screen.findByRole('button', { name: mockSection.title })).toBeInTheDocument();
- });
-
- describe('when renderStandaloneBody is set', () => {
- it('shows a "No results found" message and does not show the folder title header', async () => {
- render( );
- expect(await screen.findByText('No results found')).toBeInTheDocument();
- expect(screen.queryByRole('button', { name: mockSection.title })).not.toBeInTheDocument();
- });
-
- it('renders a loading spinner whilst waiting for the results', async () => {
- // mock the query promise so we can resolve manually
- let promiseResolver: (arg0: QueryResponse) => void;
- const promise = new Promise((resolve) => {
- promiseResolver = resolve;
- });
- grafanaSearcherSpy.mockImplementationOnce(() => promise);
-
- render( );
- expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
-
- // resolve the promise
- await act(async () => {
- promiseResolver(mockSearchResult);
- });
-
- expect(screen.queryByTestId('Spinner')).not.toBeInTheDocument();
- expect(await screen.findByText('No results found')).toBeInTheDocument();
- });
- });
-
- it('shows a "No results found" message when expanding the folder', async () => {
- render( );
-
- await userEvent.click(await screen.findByRole('button', { name: mockSection.title }));
- expect(getGrafanaSearcher().search).toHaveBeenCalled();
- expect(await screen.findByText('No results found')).toBeInTheDocument();
- });
- });
-
- describe('when there are results', () => {
- const searchData: DataFrame = {
- fields: [
- { name: 'kind', type: FieldType.string, config: {}, values: [DashboardSearchItemType.DashDB] },
- { name: 'name', type: FieldType.string, config: {}, values: ['My dashboard 1'] },
- { name: 'uid', type: FieldType.string, config: {}, values: ['my-dashboard-1'] },
- { name: 'url', type: FieldType.string, config: {}, values: ['/my-dashboard-1'] },
- { name: 'tags', type: FieldType.other, config: {}, values: [['foo', 'bar']] },
- { name: 'location', type: FieldType.string, config: {}, values: ['my-folder-1'] },
- ],
- meta: {
- custom: {
- locationInfo: {
- 'my-folder-1': {
- name: 'My folder 1',
- kind: 'folder',
- url: '/my-folder-1',
- },
- },
- },
- },
- length: 1,
- };
-
- const mockSearchResult: QueryResponse = {
- isItemLoaded: jest.fn(),
- loadMoreItems: jest.fn(),
- totalRows: searchData.length,
- view: new DataFrameView(searchData),
- };
-
- beforeAll(() => {
- grafanaSearcherSpy = jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockSearchResult);
- });
-
- it('shows the folder title as the header', async () => {
- render( );
- expect(await screen.findByRole('button', { name: mockSection.title })).toBeInTheDocument();
- });
-
- describe('when renderStandaloneBody is set', () => {
- it('shows the folder children and does not render the folder title', async () => {
- render( );
- expect(await screen.findByText('My dashboard 1')).toBeInTheDocument();
- expect(screen.queryByRole('button', { name: mockSection.title })).not.toBeInTheDocument();
- });
-
- it('renders a loading spinner whilst waiting for the results', async () => {
- // mock the query promise so we can resolve manually
- let promiseResolver: (arg0: QueryResponse) => void;
- const promise = new Promise((resolve) => {
- promiseResolver = resolve;
- });
- grafanaSearcherSpy.mockImplementationOnce(() => promise);
-
- render( );
- expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
-
- // resolve the promise
- await act(async () => {
- promiseResolver(mockSearchResult);
- });
-
- expect(screen.queryByTestId('Spinner')).not.toBeInTheDocument();
- expect(await screen.findByText('My dashboard 1')).toBeInTheDocument();
- });
- });
-
- it('shows the folder contents when expanding the folder', async () => {
- render( );
-
- await userEvent.click(await screen.findByRole('button', { name: mockSection.title }));
- expect(getGrafanaSearcher().search).toHaveBeenCalled();
- expect(await screen.findByText('My dashboard 1')).toBeInTheDocument();
- });
-
- describe('when clicking the checkbox', () => {
- it('does not expand the section', async () => {
- render(
-
- );
-
- await userEvent.click(await screen.findByRole('checkbox', { name: 'Select folder' }));
- expect(screen.queryByText('My dashboard 1')).not.toBeInTheDocument();
- });
-
- it('selects only the folder if the folder is not expanded', async () => {
- render(
-
- );
-
- await userEvent.click(await screen.findByRole('checkbox', { name: 'Select folder' }));
- expect(mockSelectionToggle).toHaveBeenCalledWith('folder', 'my-folder');
- expect(mockSelectionToggle).not.toHaveBeenCalledWith('dashboard', 'my-dashboard-1');
- });
-
- it('selects the folder and all children when the folder is expanded', async () => {
- render(
-
- );
-
- await userEvent.click(await screen.findByRole('button', { name: mockSection.title }));
- expect(getGrafanaSearcher().search).toHaveBeenCalled();
-
- await userEvent.click(await screen.findByRole('checkbox', { name: 'Select folder' }));
- expect(mockSelectionToggle).toHaveBeenCalledWith('folder', 'my-folder');
- expect(mockSelectionToggle).toHaveBeenCalledWith('dashboard', 'my-dashboard-1');
- });
- });
-
- describe('when in a pseudo-folder (i.e. Starred/Recent)', () => {
- const mockRecentSection: DashboardViewItem = {
- kind: 'folder',
- uid: '__recent',
- title: 'Recent',
- itemsUIDs: ['my-dashboard-1'],
- };
-
- it('shows the correct folder name next to the dashboard', async () => {
- render( );
-
- await userEvent.click(await screen.findByRole('button', { name: mockRecentSection.title }));
- expect(getGrafanaSearcher().search).toHaveBeenCalled();
- expect(await screen.findByText('My dashboard 1')).toBeInTheDocument();
- expect(await screen.findByText('My folder 1')).toBeInTheDocument();
- });
- });
- });
-});
diff --git a/public/app/features/search/page/components/FolderSection.tsx b/public/app/features/search/page/components/FolderSection.tsx
deleted file mode 100644
index 7183143594e..00000000000
--- a/public/app/features/search/page/components/FolderSection.tsx
+++ /dev/null
@@ -1,258 +0,0 @@
-import { css } from '@emotion/css';
-import React, { useId, useState } from 'react';
-import { useAsync } from 'react-use';
-
-import { GrafanaTheme2, toIconName } from '@grafana/data';
-import { selectors } from '@grafana/e2e-selectors';
-import { Card, Checkbox, CollapsableSection, Icon, Spinner, useStyles2 } from '@grafana/ui';
-import { config } from 'app/core/config';
-import { t } from 'app/core/internationalization';
-
-import { SearchItem } from '../..';
-import { GENERAL_FOLDER_UID, SEARCH_EXPANDED_FOLDER_STORAGE_KEY } from '../../constants';
-import { getGrafanaSearcher } from '../../service';
-import { getFolderChildren } from '../../service/folders';
-import { queryResultToViewItem } from '../../service/utils';
-import { DashboardViewItem } from '../../types';
-import { SelectionChecker, SelectionToggle } from '../selection';
-
-interface SectionHeaderProps {
- selection?: SelectionChecker;
- selectionToggle?: SelectionToggle;
- onClickItem?: (e: React.MouseEvent) => void;
- onTagSelected: (tag: string) => void;
- section: DashboardViewItem;
- renderStandaloneBody?: boolean; // render the body on its own
- tags?: string[];
-}
-
-async function getChildren(section: DashboardViewItem, tags: string[] | undefined): Promise {
- if (config.featureToggles.nestedFolders) {
- return getFolderChildren(section.uid, section.title);
- }
-
- const query = section.itemsUIDs
- ? {
- uid: section.itemsUIDs,
- }
- : {
- query: '*',
- kind: ['dashboard'],
- location: section.uid,
- sort: 'name_sort',
- limit: 1000, // this component does not have infinite scroll, so we need to load everything upfront
- };
-
- const raw = await getGrafanaSearcher().search({ ...query, tags });
- return raw.view.map((v) => queryResultToViewItem(v, raw.view));
-}
-
-export const FolderSection = ({
- section,
- selectionToggle,
- onClickItem,
- onTagSelected,
- selection,
- renderStandaloneBody,
- tags,
-}: SectionHeaderProps) => {
- const uid = section.uid;
- const editable = selectionToggle != null;
-
- const styles = useStyles2(getSectionHeaderStyles, editable);
- const [sectionExpanded, setSectionExpanded] = useState(() => {
- const lastExpandedFolder = window.localStorage.getItem(SEARCH_EXPANDED_FOLDER_STORAGE_KEY);
- return lastExpandedFolder === uid;
- });
-
- const results = useAsync(async () => {
- if (!sectionExpanded && !renderStandaloneBody) {
- return Promise.resolve([]);
- }
-
- const childItems = getChildren(section, tags);
-
- return childItems;
- }, [sectionExpanded, tags]);
-
- const onSectionExpand = () => {
- const newExpandedValue = !sectionExpanded;
-
- if (newExpandedValue) {
- // If we've just expanded the section, remember it to local storage
- window.localStorage.setItem(SEARCH_EXPANDED_FOLDER_STORAGE_KEY, uid);
- } else {
- // Else, when closing a section, remove it from local storage only if this folder was the most recently opened
- const lastExpandedFolder = window.localStorage.getItem(SEARCH_EXPANDED_FOLDER_STORAGE_KEY);
- if (lastExpandedFolder === uid) {
- window.localStorage.removeItem(SEARCH_EXPANDED_FOLDER_STORAGE_KEY);
- }
- }
-
- setSectionExpanded(newExpandedValue);
- };
-
- const onToggleFolder = (evt: React.FormEvent) => {
- evt.preventDefault();
- evt.stopPropagation();
- if (selectionToggle && selection) {
- const checked = !selection(section.kind, section.uid);
- selectionToggle(section.kind, section.uid);
- const sub = results.value ?? [];
- for (const item of sub) {
- if (selection(item.kind, item.uid!) !== checked) {
- selectionToggle(item.kind, item.uid!);
- }
- }
- }
- };
-
- const id = useId();
- const labelId = `section-header-label-${id}`;
-
- let icon = toIconName(section.icon ?? '');
- if (!icon) {
- icon = sectionExpanded ? 'folder-open' : 'folder';
- }
-
- const renderResults = () => {
- if (!results.value) {
- return null;
- } else if (results.value.length === 0 && !results.loading) {
- return (
-
- No results found
-
- );
- }
-
- return results.value.map((item) => {
- return (
- selectionToggle?.(item.kind, item.uid)}
- editable={Boolean(selection != null)}
- onClickItem={onClickItem}
- isSelected={selection?.(item.kind, item.uid)}
- />
- );
- });
- };
-
- // Skip the folder wrapper
- if (renderStandaloneBody) {
- return (
-
- {!results.value?.length && results.loading ? : renderResults()}
-
- );
- }
-
- return (
-
- {selectionToggle && selection && (
- // TODO: fix keyboard a11y
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
-
-
-
- )}
-
-
-
-
-
-
- >
- }
- >
- {results.value && }
-
- );
-};
-
-const getSectionHeaderStyles = (theme: GrafanaTheme2, editable: boolean) => {
- const sm = theme.spacing(1);
-
- return {
- wrapper: css`
- align-items: center;
- font-size: ${theme.typography.size.base};
- padding: 12px;
- border-bottom: none;
- color: ${theme.colors.text.secondary};
- z-index: 1;
-
- &:hover,
- &.selected {
- color: ${theme.colors.text};
- }
-
- &:hover,
- &:focus-visible,
- &:focus-within {
- a {
- opacity: 1;
- }
- }
- `,
- sectionItems: css`
- margin: 0 24px 0 32px;
- `,
- icon: css`
- padding: 0 ${sm} 0 ${editable ? 0 : sm};
- `,
- folderViewResults: css`
- overflow: auto;
- `,
- text: css`
- flex-grow: 1;
- line-height: 24px;
- `,
- link: css`
- padding: 2px 10px 0;
- color: ${theme.colors.text.secondary};
- opacity: 0;
- transition: opacity 150ms ease-in-out;
- `,
- separator: css`
- margin-right: 6px;
- `,
- content: css`
- padding-top: 0px;
- padding-bottom: 0px;
- `,
- spinner: css`
- display: grid;
- place-content: center;
- padding-bottom: 1rem;
- `,
- checkbox: css({
- marginRight: theme.spacing(1),
- }),
- };
-};
diff --git a/public/app/features/search/page/components/ManageActions.test.tsx b/public/app/features/search/page/components/ManageActions.test.tsx
deleted file mode 100644
index cd78a0c0899..00000000000
--- a/public/app/features/search/page/components/ManageActions.test.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import React from 'react';
-import { Provider } from 'react-redux';
-import configureMockStore from 'redux-mock-store';
-
-import { contextSrv } from 'app/core/services/context_srv';
-
-import { ManageActions } from './ManageActions';
-
-jest.mock('app/core/services/context_srv', () => ({
- contextSrv: {
- hasEditPermissionInFolders: false,
- },
-}));
-
-jest.mock('app/core/components/Select/OldFolderPicker', () => {
- return {
- OldFolderPicker: () => null,
- };
-});
-
-describe('ManageActions', () => {
- describe('when user has edit permission in folders', () => {
- // Permissions
- contextSrv.hasEditPermissionInFolders = true;
-
- // Mock selected dashboards
- const mockItemsSelected = new Map();
- const mockDashboardsUIDsSelected = new Set();
- mockDashboardsUIDsSelected.add('uid1');
- mockDashboardsUIDsSelected.add('uid2');
- mockItemsSelected.set('dashboard', mockDashboardsUIDsSelected);
-
- //Mock store redux for old MoveDashboards state action
- const mockStore = configureMockStore();
- const store = mockStore({ dashboard: { panels: [] } });
-
- const onChange = jest.fn();
- const clearSelection = jest.fn();
-
- it('should show move when user click the move button', async () => {
- render(
-
-
-
- );
- expect(screen.getByTestId('manage-actions')).toBeInTheDocument();
- expect(await screen.findByRole('button', { name: 'Move', hidden: true })).not.toBeDisabled();
- expect(await screen.findByRole('button', { name: 'Delete', hidden: true })).not.toBeDisabled();
-
- // open Move modal
- await userEvent.click(screen.getByRole('button', { name: 'Move', hidden: true }));
- expect(screen.getByText(/Move 2 dashboards to:/i)).toBeInTheDocument();
- });
-
- it('should show delete modal when user click the delete button', async () => {
- render(
-
-
-
- );
- expect(screen.getByTestId('manage-actions')).toBeInTheDocument();
- // open Delete modal
- await userEvent.click(screen.getByRole('button', { name: 'Delete', hidden: true }));
- expect(screen.getByText(/Do you want to delete the 2 selected dashboards\?/i)).toBeInTheDocument();
- });
- });
-
- describe('when user has not edit permission in folders', () => {
- it('should have disabled the Move button', async () => {
- contextSrv.hasEditPermissionInFolders = false;
- const mockItemsSelected = new Map();
- const mockDashboardsUIDsSelected = new Set();
- mockDashboardsUIDsSelected.add('uid1');
- mockDashboardsUIDsSelected.add('uid2');
- mockItemsSelected.set('dashboard', mockDashboardsUIDsSelected);
-
- //Mock store
- const mockStore = configureMockStore();
- const store = mockStore({ dashboard: { panels: [] } });
-
- const onChange = jest.fn();
- const clearSelection = jest.fn();
-
- render(
-
-
-
- );
- expect(screen.getByTestId('manage-actions')).toBeInTheDocument();
- expect(await screen.findByRole('button', { name: 'Move', hidden: true })).toBeDisabled();
- await userEvent.click(screen.getByRole('button', { name: 'Move', hidden: true }));
- expect(screen.queryByText(/Choose Dashboard Folder/i)).toBeNull();
- });
- });
- describe('When user has selected General folder', () => {
- contextSrv.hasEditPermissionInFolders = true;
- const mockItemsSelected = new Map();
- const mockFolderUIDSelected = new Set();
- mockFolderUIDSelected.add('general');
- mockItemsSelected.set('folder', mockFolderUIDSelected);
-
- //Mock store
- const mockStore = configureMockStore();
- const store = mockStore({ dashboard: { panels: [] } });
-
- const onChange = jest.fn();
- const clearSelection = jest.fn();
-
- it('should disable the Delete button', async () => {
- render(
-
-
-
- );
- expect(screen.getByTestId('manage-actions')).toBeInTheDocument();
- expect(await screen.findByRole('button', { name: 'Delete', hidden: true })).toBeDisabled();
- });
- });
-});
diff --git a/public/app/features/search/page/components/ManageActions.tsx b/public/app/features/search/page/components/ManageActions.tsx
deleted file mode 100644
index 2e68ac9b6cb..00000000000
--- a/public/app/features/search/page/components/ManageActions.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import React, { useState } from 'react';
-
-import { Button, HorizontalGroup, IconButton, useStyles2 } from '@grafana/ui';
-import { contextSrv } from 'app/core/services/context_srv';
-import { FolderDTO } from 'app/types';
-
-import { GENERAL_FOLDER_UID } from '../../constants';
-import { OnMoveOrDeleleSelectedItems } from '../../types';
-
-import { getStyles } from './ActionRow';
-import { ConfirmDeleteModal } from './ConfirmDeleteModal';
-import { MoveToFolderModal } from './MoveToFolderModal';
-
-type Props = {
- items: Map>;
- folder?: FolderDTO; // when we are loading in folder page
- onChange: OnMoveOrDeleleSelectedItems;
- clearSelection: () => void;
-};
-
-export function ManageActions({ items, folder, onChange, clearSelection }: Props) {
- const styles = useStyles2(getStyles);
-
- const canSave = folder?.canSave;
- const hasEditPermissionInFolders = folder ? canSave : contextSrv.hasEditPermissionInFolders;
-
- const canMove = hasEditPermissionInFolders;
-
- const selectedFolders = Array.from(items.get('folder') ?? []);
- const includesGeneralFolder = selectedFolders.find((result) => result === GENERAL_FOLDER_UID);
-
- const canDelete = hasEditPermissionInFolders && !includesGeneralFolder;
- const [isMoveModalOpen, setIsMoveModalOpen] = useState(false);
- const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
-
- const onMove = () => {
- setIsMoveModalOpen(true);
- };
-
- const onDelete = () => {
- setIsDeleteModalOpen(true);
- };
-
- return (
-
-
-
-
- Move
-
-
- Delete
-
-
-
- {isDeleteModalOpen && (
- setIsDeleteModalOpen(false)} />
- )}
-
- {isMoveModalOpen && (
- setIsMoveModalOpen(false)} />
- )}
-
- );
-}
diff --git a/public/app/features/search/page/components/MoveToFolderModal.test.tsx b/public/app/features/search/page/components/MoveToFolderModal.test.tsx
deleted file mode 100644
index 3da2968e8d2..00000000000
--- a/public/app/features/search/page/components/MoveToFolderModal.test.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import React from 'react';
-import { Provider } from 'react-redux';
-import configureMockStore from 'redux-mock-store';
-import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
-
-import { selectors } from '@grafana/e2e-selectors';
-import config from 'app/core/config';
-import * as api from 'app/features/manage-dashboards/state/actions';
-
-import { DashboardSearchHit, DashboardSearchItemType } from '../../types';
-
-import { MoveToFolderModal } from './MoveToFolderModal';
-
-function makeSelections(dashboardUIDs: string[] = [], folderUIDs: string[] = []) {
- const dashboards = new Set(dashboardUIDs);
- const folders = new Set(folderUIDs);
-
- return new Map([
- ['dashboard', dashboards],
- ['folder', folders],
- ]);
-}
-
-function makeDashboardSearchHit(title: string, uid: string, type = DashboardSearchItemType.DashDB): DashboardSearchHit {
- return { title, uid, tags: [], type, url: `/d/${uid}` };
-}
-
-describe('MoveToFolderModal', () => {
- jest
- .spyOn(api, 'searchFolders')
- .mockResolvedValue([
- makeDashboardSearchHit('General', '', DashboardSearchItemType.DashFolder),
- makeDashboardSearchHit('Folder 1', 'folder-uid-1', DashboardSearchItemType.DashFolder),
- makeDashboardSearchHit('Folder 2', 'folder-uid-1', DashboardSearchItemType.DashFolder),
- makeDashboardSearchHit('Folder 3', 'folder-uid-3', DashboardSearchItemType.DashFolder),
- ]);
-
- it('should render correct title, body, dismiss-, cancel- and move-text', async () => {
- const items = makeSelections(['dash-uid-1', 'dash-uid-2']);
-
- const mockStore = configureMockStore();
- const store = mockStore({ dashboard: { panels: [] } });
- const onMoveItems = jest.fn();
-
- render(
-
- {}} />
-
- );
-
- // Wait for folder picker to finish rendering
- await screen.findByText('Choose');
-
- expect(screen.getByRole('heading', { name: 'Choose Dashboard Folder' })).toBeInTheDocument();
- expect(screen.getByText('Move 2 dashboards to:')).toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument();
- });
-
- it('should move dashboards, but not folders', async () => {
- const moveDashboardsMock = jest.spyOn(api, 'moveDashboards').mockResolvedValue({
- successCount: 2,
- totalCount: 2,
- alreadyInFolderCount: 0,
- });
-
- const moveFoldersMock = jest.spyOn(api, 'moveFolders').mockResolvedValue({
- successCount: 1,
- totalCount: 1,
- });
-
- const items = makeSelections(['dash-uid-1', 'dash-uid-2'], ['folder-uid-1']);
-
- const mockStore = configureMockStore();
- const store = mockStore({ dashboard: { panels: [] } });
- const onMoveItems = jest.fn();
-
- render(
-
- {}} />
-
- );
-
- // Wait for folder picker to finish rendering
- await screen.findByText('Choose');
-
- const folderPicker = screen.getByLabelText(selectors.components.FolderPicker.input);
- await selectOptionInTest(folderPicker, 'Folder 3');
-
- const moveButton = screen.getByText('Move');
- await userEvent.click(moveButton);
-
- expect(moveDashboardsMock).toHaveBeenCalledWith(['dash-uid-1', 'dash-uid-2'], {
- title: 'Folder 3',
- uid: 'folder-uid-3',
- });
-
- expect(moveFoldersMock).not.toHaveBeenCalled();
- });
-
- describe('with nestedFolders feature flag', () => {
- let originalNestedFoldersValue = config.featureToggles.nestedFolders;
-
- beforeAll(() => {
- originalNestedFoldersValue = config.featureToggles.nestedFolders;
- config.featureToggles.nestedFolders = true;
- });
-
- afterAll(() => {
- config.featureToggles.nestedFolders = originalNestedFoldersValue;
- });
-
- it('should move folders and dashboards', async () => {
- const moveDashboardsMock = jest.spyOn(api, 'moveDashboards').mockResolvedValue({
- successCount: 2,
- totalCount: 2,
- alreadyInFolderCount: 0,
- });
-
- const moveFoldersMock = jest.spyOn(api, 'moveFolders').mockResolvedValue({
- successCount: 1,
- totalCount: 1,
- });
-
- const items = makeSelections(['dash-uid-1', 'dash-uid-2'], ['folder-uid-1']);
-
- const mockStore = configureMockStore();
- const store = mockStore({ dashboard: { panels: [] } });
- const onMoveItems = jest.fn();
-
- render(
-
- {}} />
-
- );
-
- // Wait for folder picker to finish rendering
- await screen.findByText('Choose');
-
- const folderPicker = screen.getByLabelText(selectors.components.FolderPicker.input);
- await selectOptionInTest(folderPicker, 'Folder 3');
-
- const moveButton = screen.getByRole('button', { name: 'Move' });
- await userEvent.click(moveButton);
-
- expect(moveDashboardsMock).toHaveBeenCalledWith(['dash-uid-1', 'dash-uid-2'], {
- title: 'Folder 3',
- uid: 'folder-uid-3',
- });
-
- expect(moveFoldersMock).toHaveBeenCalledWith(['folder-uid-1'], {
- title: 'Folder 3',
- uid: 'folder-uid-3',
- });
- });
- });
-});
diff --git a/public/app/features/search/page/components/MoveToFolderModal.tsx b/public/app/features/search/page/components/MoveToFolderModal.tsx
deleted file mode 100644
index e0451bf6e6c..00000000000
--- a/public/app/features/search/page/components/MoveToFolderModal.tsx
+++ /dev/null
@@ -1,193 +0,0 @@
-import { css } from '@emotion/css';
-import React, { useCallback, useState } from 'react';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { Alert, Button, HorizontalGroup, Modal, useStyles2 } from '@grafana/ui';
-import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker';
-import config from 'app/core/config';
-import { useAppNotification } from 'app/core/copy/appNotification';
-import { moveDashboards, moveFolders } from 'app/features/manage-dashboards/state/actions';
-import { FolderInfo } from 'app/types';
-
-import { GENERAL_FOLDER_UID } from '../../constants';
-import { OnMoveOrDeleleSelectedItems } from '../../types';
-
-interface Props {
- onMoveItems: OnMoveOrDeleleSelectedItems;
- results: Map>;
- onDismiss: () => void;
-}
-
-export const MoveToFolderModal = ({ results, onMoveItems, onDismiss }: Props) => {
- const [folder, setFolder] = useState(null);
- const styles = useStyles2(getStyles);
- const notifyApp = useAppNotification();
- const [moving, setMoving] = useState(false);
-
- const nestedFoldersEnabled = config.featureToggles.nestedFolders;
-
- const selectedDashboards = Array.from(results.get('dashboard') ?? []);
- const selectedFolders = nestedFoldersEnabled
- ? Array.from(results.get('folder') ?? []).filter((v) => v !== GENERAL_FOLDER_UID)
- : [];
-
- const handleFolderChange = useCallback(
- (newFolder: FolderInfo) => {
- setFolder(newFolder);
- },
- [setFolder]
- );
-
- const moveTo = async () => {
- if (!folder) {
- return;
- }
-
- if (nestedFoldersEnabled) {
- setMoving(true);
- let totalCount = 0;
- let successCount = 0;
-
- if (selectedDashboards.length) {
- const moveDashboardsResult = await moveDashboards(selectedDashboards, folder);
-
- totalCount += moveDashboardsResult.totalCount;
- successCount += moveDashboardsResult.successCount;
- }
-
- if (selectedFolders.length) {
- const moveFoldersResult = await moveFolders(selectedFolders, folder);
-
- totalCount += moveFoldersResult.totalCount;
- successCount += moveFoldersResult.successCount;
- }
-
- const destTitle = folder.title ?? 'General';
- notifyNestedMoveResult(notifyApp, destTitle, {
- selectedDashboardsCount: selectedDashboards.length,
- selectedFoldersCount: selectedFolders.length,
- totalCount,
- successCount,
- });
-
- onMoveItems();
- setMoving(false);
- onDismiss();
-
- return;
- }
-
- if (selectedDashboards.length) {
- const folderTitle = folder.title ?? 'General';
- setMoving(true);
- moveDashboards(selectedDashboards, folder).then((result) => {
- if (result.successCount > 0) {
- const ending = result.successCount === 1 ? '' : 's';
- const header = `Dashboard${ending} Moved`;
- const msg = `${result.successCount} dashboard${ending} moved to ${folderTitle}`;
- notifyApp.success(header, msg);
- }
-
- if (result.totalCount === result.alreadyInFolderCount) {
- notifyApp.error('Error', `Dashboard already belongs to folder ${folderTitle}`);
- } else {
- //update the list
- onMoveItems();
- }
-
- setMoving(false);
- onDismiss();
- });
- }
- };
-
- const thingsMoving = [
- ['folder', 'folders', selectedFolders.length] as const,
- ['dashboard', 'dashboards', selectedDashboards.length] as const,
- ]
- .filter(([single, plural, count]) => count > 0)
- .map(([single, plural, count]) => `${count.toLocaleString()} ${count === 1 ? single : plural}`)
- .join(' and ');
-
- return (
-
- <>
-
- {nestedFoldersEnabled && selectedFolders.length > 0 && (
-
- )}
-
-
Move {thingsMoving} to:
-
-
-
-
-
-
- Cancel
-
-
- Move
-
-
- >
-
- );
-};
-
-interface NotifyCounts {
- selectedDashboardsCount: number;
- selectedFoldersCount: number;
- totalCount: number;
- successCount: number;
-}
-
-function notifyNestedMoveResult(
- notifyApp: ReturnType,
- destinationName: string,
- { selectedDashboardsCount, selectedFoldersCount, totalCount, successCount }: NotifyCounts
-) {
- let objectMoving: string | undefined;
- const plural = successCount === 1 ? '' : 's';
- const failedCount = totalCount - successCount;
-
- if (selectedDashboardsCount && selectedFoldersCount) {
- objectMoving = `Item${plural}`;
- } else if (selectedDashboardsCount) {
- objectMoving = `Dashboard${plural}`;
- } else if (selectedFoldersCount) {
- objectMoving = `Folder${plural}`;
- }
-
- if (objectMoving) {
- const objectLower = objectMoving?.toLocaleLowerCase();
-
- if (totalCount === successCount) {
- notifyApp.success(`${objectMoving} moved`, `Moved ${successCount} ${objectLower} to ${destinationName}`);
- } else if (successCount === 0) {
- notifyApp.error(`Failed to move ${objectLower}`, `Could not move ${totalCount} ${objectLower} due to an error`);
- } else {
- notifyApp.warning(
- `Partially moved ${objectLower}`,
- `Failed to move ${failedCount} ${objectLower} to ${destinationName}`
- );
- }
- }
-}
-
-const getStyles = (theme: GrafanaTheme2) => {
- return {
- modal: css`
- width: 500px;
- `,
- content: css`
- margin-bottom: ${theme.spacing(3)};
- `,
- };
-};
diff --git a/public/app/features/search/page/components/RootFolderView.test.tsx b/public/app/features/search/page/components/RootFolderView.test.tsx
deleted file mode 100644
index d49324a7bc1..00000000000
--- a/public/app/features/search/page/components/RootFolderView.test.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import { render, screen, act } from '@testing-library/react';
-import React from 'react';
-
-import { DataFrame, DataFrameView, FieldType } from '@grafana/data';
-import { selectors } from '@grafana/e2e-selectors';
-
-import { ContextSrv, setContextSrv } from '../../../../core/services/context_srv';
-import impressionSrv from '../../../../core/services/impression_srv';
-import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from '../../service';
-import { DashboardSearchItemType } from '../../types';
-
-import { RootFolderView } from './RootFolderView';
-
-jest.mock('@grafana/runtime', () => ({
- ...jest.requireActual('@grafana/runtime'),
- getBackendSrv: () => ({
- get: jest.fn().mockResolvedValue(['foo']),
- }),
-}));
-
-describe('RootFolderView', () => {
- let grafanaSearcherSpy: jest.SpyInstance;
- const mockOnTagSelected = jest.fn();
- const mockSelectionToggle = jest.fn();
- const mockSelection = jest.fn();
-
- const folderData: DataFrame = {
- fields: [
- {
- name: 'kind',
- type: FieldType.string,
- config: {},
- values: [DashboardSearchItemType.DashFolder],
- },
- { name: 'name', type: FieldType.string, config: {}, values: ['My folder 1'] },
- { name: 'uid', type: FieldType.string, config: {}, values: ['my-folder-1'] },
- { name: 'url', type: FieldType.string, config: {}, values: ['/my-folder-1'] },
- ],
- length: 1,
- };
-
- const mockSearchResult: QueryResponse = {
- isItemLoaded: jest.fn(),
- loadMoreItems: jest.fn(),
- totalRows: folderData.length,
- view: new DataFrameView(folderData),
- };
-
- let contextSrv: ContextSrv;
-
- beforeAll(() => {
- contextSrv = new ContextSrv();
- setContextSrv(contextSrv);
- grafanaSearcherSpy = jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockSearchResult);
- });
-
- // need to make sure we clear localStorage
- // otherwise tests can interfere with each other and the starting expanded state of the component
- afterEach(() => {
- window.localStorage.clear();
- });
-
- it('shows a spinner whilst the results are loading', async () => {
- // mock the query promise so we can resolve manually
- let promiseResolver: (arg0: QueryResponse) => void;
- const promise = new Promise((resolve) => {
- promiseResolver = resolve;
- });
- grafanaSearcherSpy.mockImplementationOnce(() => promise);
-
- render(
-
- );
- expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
-
- // resolve the promise
- await act(async () => {
- promiseResolver(mockSearchResult);
- });
-
- expect(screen.queryByTestId('Spinner')).not.toBeInTheDocument();
- });
-
- it('does not show the starred items if not signed in', async () => {
- contextSrv.isSignedIn = false;
- render(
-
- );
- expect((await screen.findAllByTestId(selectors.components.Search.sectionV2))[0]).toBeInTheDocument();
- expect(screen.queryByRole('button', { name: 'Starred' })).not.toBeInTheDocument();
- });
-
- it('shows the starred items if signed in', async () => {
- contextSrv.isSignedIn = true;
- render(
-
- );
- expect(await screen.findByRole('button', { name: 'Starred' })).toBeInTheDocument();
- });
-
- it('does not show the recent items if no dashboards have been opened recently', async () => {
- jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue([]);
- render(
-
- );
- expect((await screen.findAllByTestId(selectors.components.Search.sectionV2))[0]).toBeInTheDocument();
- expect(screen.queryByRole('button', { name: 'Recent' })).not.toBeInTheDocument();
- });
-
- it('shows the recent items if any dashboards have recently been opened', async () => {
- jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue(['7MeksYbmk']);
- render(
-
- );
- expect(await screen.findByRole('button', { name: 'Recent' })).toBeInTheDocument();
- });
-
- it('shows the general folder by default', async () => {
- render(
-
- );
- expect(await screen.findByRole('button', { name: 'General' })).toBeInTheDocument();
- });
-
- describe('when hidePseudoFolders is set', () => {
- it('does not show the starred items even if signed in', async () => {
- contextSrv.isSignedIn = true;
- render(
-
- );
- expect(await screen.findByRole('button', { name: 'General' })).toBeInTheDocument();
- expect(screen.queryByRole('button', { name: 'Starred' })).not.toBeInTheDocument();
- });
-
- it('does not show the recent items even if recent dashboards have been opened', async () => {
- jest.spyOn(impressionSrv, 'getDashboardOpened').mockResolvedValue(['7MeksYbmk']);
- render(
-
- );
- expect(await screen.findByRole('button', { name: 'General' })).toBeInTheDocument();
- expect(screen.queryByRole('button', { name: 'Recent' })).not.toBeInTheDocument();
- });
- });
-
- it('shows an error state if any of the calls reject for a specific reason', async () => {
- // reject with a specific Error object
- grafanaSearcherSpy.mockRejectedValueOnce(new Error('Uh oh spagghettios!'));
- render(
-
- );
- expect(await screen.findByRole('alert', { name: 'Uh oh spagghettios!' })).toBeInTheDocument();
- });
-
- it('shows a general error state if any of the calls reject', async () => {
- // reject with nothing
- grafanaSearcherSpy.mockRejectedValueOnce(null);
- render(
-
- );
- expect(await screen.findByRole('alert', { name: 'Something went wrong' })).toBeInTheDocument();
- });
-});
diff --git a/public/app/features/search/page/components/RootFolderView.tsx b/public/app/features/search/page/components/RootFolderView.tsx
deleted file mode 100644
index 1a311e0ac68..00000000000
--- a/public/app/features/search/page/components/RootFolderView.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { css } from '@emotion/css';
-import React from 'react';
-import { useAsync } from 'react-use';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { selectors } from '@grafana/e2e-selectors';
-import { getBackendSrv } from '@grafana/runtime';
-import { Alert, Spinner, useStyles2 } from '@grafana/ui';
-import config from 'app/core/config';
-
-import { contextSrv } from '../../../../core/services/context_srv';
-import impressionSrv from '../../../../core/services/impression_srv';
-import { GENERAL_FOLDER_UID } from '../../constants';
-import { getGrafanaSearcher } from '../../service';
-import { getFolderChildren } from '../../service/folders';
-import { queryResultToViewItem } from '../../service/utils';
-
-import { FolderSection } from './FolderSection';
-import { SearchResultsProps } from './SearchResultsTable';
-
-async function getChildren() {
- if (config.featureToggles.nestedFolders) {
- return getFolderChildren();
- }
-
- const searcher = getGrafanaSearcher();
- const results = await searcher.search({
- query: '*',
- kind: ['folder'],
- sort: searcher.getFolderViewSort(),
- limit: 1000,
- });
-
- return results.view.map((v) => queryResultToViewItem(v, results.view));
-}
-
-type Props = Pick & {
- tags?: string[];
- hidePseudoFolders?: boolean;
-};
-export const RootFolderView = ({
- selection,
- selectionToggle,
- onTagSelected,
- tags,
- hidePseudoFolders,
- onClickItem,
-}: Props) => {
- const styles = useStyles2(getStyles);
-
- const results = useAsync(async () => {
- const folders = await getChildren();
-
- folders.unshift({ title: 'General', url: '/dashboards', kind: 'folder', uid: GENERAL_FOLDER_UID });
-
- if (!hidePseudoFolders) {
- const itemsUIDs = await impressionSrv.getDashboardOpened();
- if (itemsUIDs.length) {
- folders.unshift({ title: 'Recent', icon: 'clock-nine', kind: 'folder', uid: '__recent', itemsUIDs });
- }
-
- if (contextSrv.isSignedIn) {
- const stars = await getBackendSrv().get('api/user/stars');
- if (stars.length > 0) {
- folders.unshift({ title: 'Starred', icon: 'star', kind: 'folder', uid: '__starred', itemsUIDs: stars });
- }
- }
- }
-
- return folders;
- }, []);
-
- const renderResults = () => {
- if (results.loading) {
- return ;
- } else if (!results.value) {
- return ;
- } else {
- return results.value.map((section) => (
-
- {section.title && (
-
- )}
-
- ));
- }
- };
-
- return {renderResults()}
;
-};
-
-const getStyles = (theme: GrafanaTheme2) => {
- return {
- wrapper: css`
- display: flex;
- flex-direction: column;
- overflow: auto;
-
- > ul {
- list-style: none;
- }
-
- border: solid 1px ${theme.v1.colors.border2};
- `,
- section: css`
- display: flex;
- flex-direction: column;
- background: ${theme.v1.colors.panelBg};
-
- &:not(:last-child) {
- border-bottom: solid 1px ${theme.v1.colors.border2};
- }
- `,
- spinner: css`
- align-items: center;
- display: flex;
- justify-content: center;
- min-height: 100px;
- `,
- error: css`
- margin: ${theme.spacing(4)} auto;
- `,
- };
-};
diff --git a/public/app/features/search/page/components/SearchResultsCards.test.tsx b/public/app/features/search/page/components/SearchResultsCards.test.tsx
deleted file mode 100644
index 941a8035464..00000000000
--- a/public/app/features/search/page/components/SearchResultsCards.test.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import React from 'react';
-import { Subject } from 'rxjs';
-
-import { DataFrame, DataFrameView, FieldType } from '@grafana/data';
-
-import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from '../../service';
-import { DashboardSearchItemType } from '../../types';
-
-import { SearchResultsCards } from './SearchResultsCards';
-
-describe('SearchResultsCards', () => {
- const mockOnTagSelected = jest.fn();
- const mockClearSelection = jest.fn();
- const mockSelectionToggle = jest.fn();
- const mockSelection = jest.fn();
- const mockKeyboardEvents = new Subject();
-
- describe('when there is data', () => {
- const searchData: DataFrame = {
- fields: [
- { name: 'kind', type: FieldType.string, config: {}, values: [DashboardSearchItemType.DashDB] },
- { name: 'uid', type: FieldType.string, config: {}, values: ['my-dashboard-1'] },
- { name: 'name', type: FieldType.string, config: {}, values: ['My dashboard 1'] },
- { name: 'panel_type', type: FieldType.string, config: {}, values: [''] },
- { name: 'url', type: FieldType.string, config: {}, values: ['/my-dashboard-1'] },
- { name: 'tags', type: FieldType.other, config: {}, values: [['foo', 'bar']] },
- { name: 'ds_uid', type: FieldType.other, config: {}, values: [''] },
- { name: 'location', type: FieldType.string, config: {}, values: ['folder0/my-dashboard-1'] },
- ],
- meta: {
- custom: {
- locationInfo: {
- folder0: { name: 'Folder 0', uid: 'f0' },
- },
- },
- },
- length: 1,
- };
-
- const mockSearchResult: QueryResponse = {
- isItemLoaded: () => true,
- loadMoreItems: () => Promise.resolve(),
- totalRows: searchData.length,
- view: new DataFrameView(searchData),
- };
-
- beforeAll(() => {
- jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockSearchResult);
- });
-
- it('shows the list with the correct accessible label', () => {
- render(
-
- );
- expect(screen.getByRole('list', { name: 'Search results list' })).toBeInTheDocument();
- });
-
- it('displays the data correctly in the table', () => {
- render(
-
- );
-
- const rows = screen.getAllByRole('row');
- expect(rows).toHaveLength(searchData.length);
- expect(screen.getByText('My dashboard 1')).toBeInTheDocument();
- expect(screen.getByText('foo')).toBeInTheDocument();
- expect(screen.getByText('bar')).toBeInTheDocument();
- });
- });
-
- describe('when there is no data', () => {
- const emptySearchData: DataFrame = {
- fields: [
- { name: 'kind', type: FieldType.string, config: {}, values: [] },
- { name: 'name', type: FieldType.string, config: {}, values: [] },
- { name: 'uid', type: FieldType.string, config: {}, values: [] },
- { name: 'url', type: FieldType.string, config: {}, values: [] },
- { name: 'tags', type: FieldType.other, config: {}, values: [] },
- { name: 'location', type: FieldType.string, config: {}, values: [] },
- ],
- length: 0,
- };
-
- const mockEmptySearchResult: QueryResponse = {
- isItemLoaded: jest.fn(),
- loadMoreItems: jest.fn(),
- totalRows: emptySearchData.length,
- view: new DataFrameView(emptySearchData),
- };
-
- beforeAll(() => {
- jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockEmptySearchResult);
- });
-
- it('shows a "No data" message', () => {
- render(
-
- );
- expect(screen.queryByRole('list', { name: 'Search results list' })).not.toBeInTheDocument();
- expect(screen.getByText('No data')).toBeInTheDocument();
- });
- });
-});
diff --git a/public/app/features/search/page/components/SearchResultsCards.tsx b/public/app/features/search/page/components/SearchResultsCards.tsx
deleted file mode 100644
index 0bd0c509001..00000000000
--- a/public/app/features/search/page/components/SearchResultsCards.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-/* eslint-disable react/jsx-no-undef */
-import { css } from '@emotion/css';
-import React, { useEffect, useRef, useCallback, useState, CSSProperties } from 'react';
-import { FixedSizeList } from 'react-window';
-import InfiniteLoader from 'react-window-infinite-loader';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { useStyles2 } from '@grafana/ui';
-
-import { SearchItem } from '../../components/SearchItem';
-import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection';
-import { queryResultToViewItem } from '../../service/utils';
-
-import { SearchResultsProps } from './SearchResultsTable';
-
-export const SearchResultsCards = React.memo(
- ({
- response,
- width,
- height,
- selection,
- selectionToggle,
- onTagSelected,
- keyboardEvents,
- onClickItem,
- }: SearchResultsProps) => {
- const styles = useStyles2(getStyles);
- const infiniteLoaderRef = useRef(null);
- const [listEl, setListEl] = useState(null);
- const highlightIndex = useSearchKeyboardNavigation(keyboardEvents, 0, response);
-
- // Scroll to the top and clear loader cache when the query results change
- useEffect(() => {
- if (infiniteLoaderRef.current) {
- infiniteLoaderRef.current.resetloadMoreItemsCache();
- }
- if (listEl) {
- listEl.scrollTo(0);
- }
- }, [response, listEl]);
-
- const RenderRow = useCallback(
- ({ index: rowIndex, style }: { index: number; style: CSSProperties }) => {
- let className = '';
- if (rowIndex === highlightIndex.y) {
- className += ' ' + styles.selectedRow;
- }
-
- const item = response.view.get(rowIndex);
- const searchItem = queryResultToViewItem(item, response.view);
- const isSelected = selectionToggle && selection?.(searchItem.kind, searchItem.uid);
-
- return (
-
- {
- if (selectionToggle) {
- selectionToggle('dashboard', item.uid!);
- }
- }}
- editable={Boolean(selection != null)}
- onClickItem={onClickItem}
- isSelected={isSelected}
- />
-
- );
- },
- [response.view, highlightIndex, styles, onTagSelected, selection, selectionToggle, onClickItem]
- );
-
- if (!response.totalRows) {
- return (
-
- No data
-
- );
- }
-
- return (
-
-
- {({ onItemsRendered, ref }) => (
- {
- ref(innerRef);
- setListEl(innerRef);
- }}
- onItemsRendered={onItemsRendered}
- height={height}
- itemCount={response.totalRows}
- itemSize={72}
- width="100%"
- style={{ overflow: 'hidden auto' }}
- >
- {RenderRow}
-
- )}
-
-
- );
- }
-);
-SearchResultsCards.displayName = 'SearchResultsCards';
-
-const getStyles = (theme: GrafanaTheme2) => {
- return {
- noData: css`
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- `,
- selectedRow: css`
- border-left: 3px solid ${theme.colors.primary.border};
- `,
- };
-};
diff --git a/public/app/features/search/page/components/SearchView.test.tsx b/public/app/features/search/page/components/SearchView.test.tsx
deleted file mode 100644
index e2808e1a62d..00000000000
--- a/public/app/features/search/page/components/SearchView.test.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-import { render, screen, waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import React from 'react';
-import { Provider } from 'react-redux';
-import configureMockStore from 'redux-mock-store';
-import { Observable } from 'rxjs';
-
-import { DataFrame, DataFrameView, FieldType } from '@grafana/data';
-import { config } from '@grafana/runtime';
-
-import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from '../../service';
-import { getSearchStateManager, initialState } from '../../state/SearchStateManager';
-import { DashboardSearchItemType, SearchLayout, SearchState } from '../../types';
-
-import { SearchView, SearchViewProps } from './SearchView';
-
-jest.mock('@grafana/runtime', () => {
- const originalModule = jest.requireActual('@grafana/runtime');
- return {
- ...originalModule,
- reportInteraction: jest.fn(),
- };
-});
-
-const stateManager = getSearchStateManager();
-
-const setup = (propOverrides?: Partial, stateOverrides?: Partial) => {
- const props: SearchViewProps = {
- showManage: false,
- keyboardEvents: {} as Observable,
- ...propOverrides,
- };
-
- stateManager.setState({ ...initialState, ...stateOverrides });
-
- const mockStore = configureMockStore();
- const store = mockStore({ searchQuery: { ...initialState } });
-
- render(
-
-
-
- );
-};
-
-describe('SearchView', () => {
- const folderData: DataFrame = {
- fields: [
- {
- name: 'kind',
- type: FieldType.string,
- config: {},
- values: [DashboardSearchItemType.DashFolder],
- },
- { name: 'name', type: FieldType.string, config: {}, values: ['My folder 1'] },
- { name: 'uid', type: FieldType.string, config: {}, values: ['my-folder-1'] },
- { name: 'url', type: FieldType.string, config: {}, values: ['/my-folder-1'] },
- ],
- length: 1,
- };
-
- const mockSearchResult: QueryResponse = {
- isItemLoaded: jest.fn(),
- loadMoreItems: jest.fn(),
- totalRows: folderData.length,
- view: new DataFrameView(folderData),
- };
-
- beforeAll(() => {
- jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue(mockSearchResult);
- });
-
- beforeEach(() => {
- config.featureToggles.panelTitleSearch = false;
- });
-
- it('does not show checkboxes or manage actions if showManage is false', async () => {
- setup();
- await waitFor(() => expect(screen.queryAllByRole('checkbox')).toHaveLength(0));
- expect(screen.queryByTestId('manage-actions')).not.toBeInTheDocument();
- });
-
- it('shows checkboxes if showManage is true', async () => {
- setup({ showManage: true });
- await waitFor(() => expect(screen.queryAllByRole('checkbox')).toHaveLength(2));
- });
-
- it('shows the manage actions if show manage is true and the user clicked a checkbox', async () => {
- setup({ showManage: true });
- await waitFor(() => userEvent.click(screen.getAllByRole('checkbox')[0]));
-
- expect(screen.queryByTestId('manage-actions')).toBeInTheDocument();
- });
-
- it('shows an empty state if no data returned', async () => {
- jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue({
- ...mockSearchResult,
- totalRows: 0,
- view: new DataFrameView({ fields: [], length: 0 }),
- });
-
- setup(undefined, { query: 'asdfasdfasdf' });
-
- await waitFor(() => expect(screen.queryByText('No results found for your query.')).toBeInTheDocument());
- expect(screen.getByRole('button', { name: 'Clear search and filters' })).toBeInTheDocument();
- });
-
- it('shows an empty state if no starred dashboard returned', async () => {
- jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue({
- ...mockSearchResult,
- totalRows: 0,
- view: new DataFrameView({ fields: [], length: 0 }),
- });
-
- setup(undefined, { starred: true });
-
- await waitFor(() => expect(screen.queryByText('No results found for your query.')).toBeInTheDocument());
- expect(screen.getByRole('button', { name: 'Clear search and filters' })).toBeInTheDocument();
- });
-
- it('shows empty folder cta for empty folder', async () => {
- jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue({
- ...mockSearchResult,
- totalRows: 0,
- view: new DataFrameView({ fields: [], length: 0 }),
- });
-
- setup(
- {
- folderDTO: {
- id: 1,
- uid: 'abc',
- title: 'morning coffee',
- url: '/morningcoffee',
- version: 1,
- canSave: true,
- canEdit: true,
- canAdmin: true,
- canDelete: true,
- created: '',
- createdBy: '',
- hasAcl: false,
- updated: '',
- updatedBy: '',
- },
- },
- undefined
- );
-
- await waitFor(() => expect(screen.queryByText("This folder doesn't have any dashboards yet")).toBeInTheDocument());
- });
-
- describe('include panels', () => {
- it('should be enabled when layout is list', async () => {
- config.featureToggles.panelTitleSearch = true;
- setup({}, { layout: SearchLayout.List });
-
- await waitFor(() => expect(screen.getByLabelText(/include panels/i)).toBeInTheDocument());
- expect(screen.getByTestId('include-panels')).toBeEnabled();
- });
-
- it('should be disabled when layout is folder', async () => {
- config.featureToggles.panelTitleSearch = true;
- setup({}, { layout: SearchLayout.Folders });
-
- await waitFor(() => expect(screen.getByLabelText(/include panels/i)).toBeInTheDocument());
- expect(screen.getByTestId('include-panels')).toBeDisabled();
- });
- });
-});
diff --git a/public/app/features/search/page/components/SearchView.tsx b/public/app/features/search/page/components/SearchView.tsx
deleted file mode 100644
index bc89febeb22..00000000000
--- a/public/app/features/search/page/components/SearchView.tsx
+++ /dev/null
@@ -1,221 +0,0 @@
-import { css } from '@emotion/css';
-import React, { useCallback, useState } from 'react';
-import { useDebounce } from 'react-use';
-import AutoSizer from 'react-virtualized-auto-sizer';
-import { Observable } from 'rxjs';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { useStyles2, Spinner, Button } from '@grafana/ui';
-import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
-import { Trans } from 'app/core/internationalization';
-import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
-import { FolderDTO } from 'app/types';
-
-import { getGrafanaSearcher } from '../../service';
-import { getSearchStateManager } from '../../state/SearchStateManager';
-import { SearchLayout, DashboardViewItem } from '../../types';
-import { newSearchSelection, updateSearchSelection } from '../selection';
-
-import { ActionRow, getValidQueryLayout } from './ActionRow';
-import { FolderSection } from './FolderSection';
-import { ManageActions } from './ManageActions';
-import { RootFolderView } from './RootFolderView';
-import { SearchResultsCards } from './SearchResultsCards';
-import { SearchResultsTable, SearchResultsProps } from './SearchResultsTable';
-
-export type SearchViewProps = {
- showManage: boolean;
- folderDTO?: FolderDTO;
- hidePseudoFolders?: boolean; // Recent + starred
- keyboardEvents: Observable;
-};
-
-export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardEvents }: SearchViewProps) => {
- const styles = useStyles2(getStyles);
- const stateManager = getSearchStateManager(); // State is initialized from URL by parent component
- const state = stateManager.useState();
-
- const [searchSelection, setSearchSelection] = useState(newSearchSelection());
- const layout = getValidQueryLayout(state);
- const isFolders = layout === SearchLayout.Folders;
-
- const [listKey, setListKey] = useState(Date.now());
-
- // Search usage reporting
- useDebounce(stateManager.onReportSearchUsage, 1000, []);
-
- const clearSelection = useCallback(() => {
- searchSelection.items.clear();
- setSearchSelection({ ...searchSelection });
- }, [searchSelection]);
-
- const toggleSelection = useCallback(
- (kind: string, uid: string) => {
- const current = searchSelection.isSelected(kind, uid);
- setSearchSelection(updateSearchSelection(searchSelection, !current, kind, [uid]));
- },
- [searchSelection]
- );
-
- // function to update items when dashboards or folders are moved or deleted
- const onChangeItemsList = async () => {
- // clean up search selection
- clearSelection();
- setListKey(Date.now());
- // trigger again the search to the backend
- stateManager.onQueryChange(state.query);
- };
-
- const renderResults = () => {
- const value = state.result;
-
- if ((!value || !value.totalRows) && !isFolders) {
- if (state.loading && !value) {
- return ;
- }
-
- return (
-
-
- No results found for your query.
-
-
-
- Clear search and filters
-
-
- );
- }
-
- const selection = showManage ? searchSelection.isSelected : undefined;
-
- if (layout === SearchLayout.Folders) {
- if (folderDTO) {
- return (
-
- );
- }
- return (
-
- );
- }
-
- return (
-
-
- {({ width, height }) => {
- const props: SearchResultsProps = {
- response: value!,
- selection,
- selectionToggle: toggleSelection,
- clearSelection,
- width: width,
- height: height,
- onTagSelected: stateManager.onAddTag,
- keyboardEvents,
- onDatasourceChange: state.datasource ? stateManager.onDatasourceChange : undefined,
- onClickItem: stateManager.onSearchItemClicked,
- };
-
- if (width < 800) {
- return ;
- }
-
- return ;
- }}
-
-
- );
- };
-
- if (
- folderDTO &&
- // With nested folders, SearchView doesn't know if it's fetched all children
- // of a folder so don't show empty state here.
- !newBrowseDashboardsEnabled() &&
- !state.loading &&
- !state.result?.totalRows &&
- !stateManager.hasSearchFilters()
- ) {
- return (
-
- );
- }
-
- return (
- <>
- {Boolean(searchSelection.items.size > 0) ? (
-
- ) : (
-
- )}
-
- {renderResults()}
- >
- );
-};
-
-const getStyles = (theme: GrafanaTheme2) => ({
- searchInput: css`
- margin-bottom: 6px;
- min-height: ${theme.spacing(4)};
- `,
- unsupported: css`
- padding: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- font-size: 18px;
- `,
- noResults: css`
- padding: ${theme.v1.spacing.md};
- background: ${theme.v1.colors.bg2};
- font-style: italic;
- margin-top: ${theme.v1.spacing.md};
- `,
-});
-
-function sectionForFolderView(folderDTO: FolderDTO): DashboardViewItem {
- return { uid: folderDTO.uid, kind: 'folder', title: folderDTO.title };
-}
diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx
index 415c0fb9af9..ad175440b2e 100644
--- a/public/app/routes/routes.tsx
+++ b/public/app/routes/routes.tsx
@@ -10,7 +10,6 @@ import { contextSrv } from 'app/core/services/context_srv';
import UserAdminPage from 'app/features/admin/UserAdminPage';
import LdapPage from 'app/features/admin/ldap/LdapPage';
import { getAlertingRoutes } from 'app/features/alerting/routes';
-import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
import { ConnectionsRedirectNotice } from 'app/features/connections/components/ConnectionsRedirectNotice';
import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants';
import { getRoutes as getDataConnectionsRoutes } from 'app/features/connections/routes';
@@ -139,38 +138,19 @@ export function getAppRoutes(): RouteDescriptor[] {
{
path: '/dashboards',
component: SafeDynamicImport(
- () => import(/* webpackChunkName: "DashboardListPage"*/ 'app/features/search/components/DashboardListPage')
- ),
- },
- {
- path: '/dashboards/folder/new',
- roles: () => contextSrv.evaluatePermission([AccessControlAction.FoldersCreate]),
- component: SafeDynamicImport(
- () => import(/* webpackChunkName: "NewDashboardsFolder"*/ 'app/features/folders/components/NewDashboardsFolder')
- ),
- },
- !newBrowseDashboardsEnabled() && {
- path: '/dashboards/f/:uid/:slug/permissions',
- component: SafeDynamicImport(
- () => import(/* webpackChunkName: "FolderPermissions"*/ 'app/features/folders/AccessControlFolderPermissions')
- ),
- },
- !newBrowseDashboardsEnabled() && {
- path: '/dashboards/f/:uid/:slug/settings',
- component: SafeDynamicImport(
- () => import(/* webpackChunkName: "FolderSettingsPage"*/ 'app/features/folders/FolderSettingsPage')
+ () => import(/* webpackChunkName: "DashboardListPage"*/ 'app/features/browse-dashboards/BrowseDashboardsPage')
),
},
{
path: '/dashboards/f/:uid/:slug',
component: SafeDynamicImport(
- () => import(/* webpackChunkName: "DashboardListPage"*/ 'app/features/search/components/DashboardListPage')
+ () => import(/* webpackChunkName: "DashboardListPage"*/ 'app/features/browse-dashboards/BrowseDashboardsPage')
),
},
{
path: '/dashboards/f/:uid',
component: SafeDynamicImport(
- () => import(/* webpackChunkName: "DashboardListPage"*/ 'app/features/search/components/DashboardListPage')
+ () => import(/* webpackChunkName: "DashboardListPage"*/ 'app/features/browse-dashboards/BrowseDashboardsPage')
),
},
{
@@ -454,23 +434,17 @@ export function getAppRoutes(): RouteDescriptor[] {
{
path: '/dashboards/f/:uid/:slug/library-panels',
component: SafeDynamicImport(
- newBrowseDashboardsEnabled()
- ? () =>
- import(
- /* webpackChunkName: "FolderLibraryPanelsPage"*/ 'app/features/browse-dashboards/BrowseFolderLibraryPanelsPage'
- )
- : () =>
- import(/* webpackChunkName: "FolderLibraryPanelsPage"*/ 'app/features/folders/FolderLibraryPanelsPage')
+ () =>
+ import(
+ /* webpackChunkName: "FolderLibraryPanelsPage"*/ 'app/features/browse-dashboards/BrowseFolderLibraryPanelsPage'
+ )
),
},
{
path: '/dashboards/f/:uid/:slug/alerting',
roles: () => contextSrv.evaluatePermission([AccessControlAction.AlertingRuleRead]),
component: SafeDynamicImport(
- newBrowseDashboardsEnabled()
- ? () =>
- import(/* webpackChunkName: "FolderAlerting"*/ 'app/features/browse-dashboards/BrowseFolderAlertingPage')
- : () => import(/* webpackChunkName: "FolderAlerting"*/ 'app/features/folders/FolderAlerting')
+ () => import(/* webpackChunkName: "FolderAlerting"*/ 'app/features/browse-dashboards/BrowseFolderAlertingPage')
),
},
{
diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json
index 81bc2812917..4b9e4b2fa0d 100644
--- a/public/locales/de-DE/grafana.json
+++ b/public/locales/de-DE/grafana.json
@@ -81,9 +81,7 @@
"manage-folder-nav": {
"alert-rules": "Warnregeln",
"dashboards": "Dashboards",
- "panels": "Fenster",
- "permissions": "Berechtigungen",
- "settings": "Einstellungen"
+ "panels": "Fenster"
},
"new-folder-form": {
"cancel-label": "Abbrechen",
@@ -1085,15 +1083,6 @@
"new-dashboard": "Neues Dashboard",
"new-folder": "Neuer Ordner"
},
- "folder-view": {
- "go-to-folder": "Zum Ordner",
- "select-folder": "Ordner auswählen"
- },
- "result-kind": {
- "dashboard": "Dashboard",
- "folder": "Ordner",
- "panel": "Panel"
- },
"results-table": {
"datasource-header": "Datenquelle",
"location-header": "Standort",
@@ -1108,12 +1097,6 @@
"placeholder": "Nach Dashboards suchen"
}
},
- "search-view": {
- "no-results": {
- "clear": "Suche und Filter löschen",
- "text": "Keine Ergebnisse für Ihre Abfrage gefunden."
- }
- },
"share-modal": {
"dashboard": {
"title": "Teilen"
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index db5fa55c05e..33ef5f29eaf 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -81,9 +81,7 @@
"manage-folder-nav": {
"alert-rules": "Alert rules",
"dashboards": "Dashboards",
- "panels": "Panels",
- "permissions": "Permissions",
- "settings": "Settings"
+ "panels": "Panels"
},
"new-folder-form": {
"cancel-label": "Cancel",
@@ -1085,15 +1083,6 @@
"new-dashboard": "New dashboard",
"new-folder": "New folder"
},
- "folder-view": {
- "go-to-folder": "Go to folder",
- "select-folder": "Select folder"
- },
- "result-kind": {
- "dashboard": "Dashboard",
- "folder": "Folder",
- "panel": "Panel"
- },
"results-table": {
"datasource-header": "Data source",
"location-header": "Location",
@@ -1108,12 +1097,6 @@
"placeholder": "Search for dashboards and folders"
}
},
- "search-view": {
- "no-results": {
- "clear": "Clear search and filters",
- "text": "No results found for your query."
- }
- },
"share-modal": {
"dashboard": {
"title": "Share"
diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json
index 8ca611c38c7..c79d6c2442e 100644
--- a/public/locales/es-ES/grafana.json
+++ b/public/locales/es-ES/grafana.json
@@ -86,9 +86,7 @@
"manage-folder-nav": {
"alert-rules": "Reglas de alerta",
"dashboards": "Paneles de control",
- "panels": "Paneles",
- "permissions": "Permisos",
- "settings": "Configuración"
+ "panels": "Paneles"
},
"new-folder-form": {
"cancel-label": "Cancelar",
@@ -1091,15 +1089,6 @@
"new-dashboard": "Nuevo panel de control",
"new-folder": "Nueva carpeta"
},
- "folder-view": {
- "go-to-folder": "Ir a la carpeta",
- "select-folder": "Seleccionar carpeta"
- },
- "result-kind": {
- "dashboard": "Panel de control",
- "folder": "Carpeta",
- "panel": "Panel"
- },
"results-table": {
"datasource-header": "Fuente de datos",
"location-header": "Ubicación",
@@ -1114,12 +1103,6 @@
"placeholder": "Buscar paneles de control"
}
},
- "search-view": {
- "no-results": {
- "clear": "Borrar la búsqueda y los filtros",
- "text": "No se ha encontrado ningún resultado para su consulta."
- }
- },
"share-modal": {
"dashboard": {
"title": "Compartir"
diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json
index 6dc7d3cae03..bd64c9a5295 100644
--- a/public/locales/fr-FR/grafana.json
+++ b/public/locales/fr-FR/grafana.json
@@ -86,9 +86,7 @@
"manage-folder-nav": {
"alert-rules": "Règles d'alerte",
"dashboards": "Tableaux de bord",
- "panels": "Panneaux",
- "permissions": "Permissions",
- "settings": "Paramètres"
+ "panels": "Panneaux"
},
"new-folder-form": {
"cancel-label": "Annuler",
@@ -1091,15 +1089,6 @@
"new-dashboard": "Nouveau tableau de bord",
"new-folder": "Nouveau dossier"
},
- "folder-view": {
- "go-to-folder": "Aller dans le dossier",
- "select-folder": "Sélectionner un dossier"
- },
- "result-kind": {
- "dashboard": "Tableau de bord",
- "folder": "Dossier",
- "panel": "Panneau"
- },
"results-table": {
"datasource-header": "Source de données",
"location-header": "Emplacement",
@@ -1114,12 +1103,6 @@
"placeholder": "Rechercher des tableaux de bord"
}
},
- "search-view": {
- "no-results": {
- "clear": "Effacer la recherche et les filtres",
- "text": "Aucun résultat trouvé pour votre requête."
- }
- },
"share-modal": {
"dashboard": {
"title": "Partager"
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index bff14de3d53..77be386cb16 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -81,9 +81,7 @@
"manage-folder-nav": {
"alert-rules": "Åľęřŧ řūľęş",
"dashboards": "Đäşĥþőäřđş",
- "panels": "Päʼnęľş",
- "permissions": "Pęřmįşşįőʼnş",
- "settings": "Ŝęŧŧįʼnģş"
+ "panels": "Päʼnęľş"
},
"new-folder-form": {
"cancel-label": "Cäʼnčęľ",
@@ -1085,15 +1083,6 @@
"new-dashboard": "Ńęŵ đäşĥþőäřđ",
"new-folder": "Ńęŵ ƒőľđęř"
},
- "folder-view": {
- "go-to-folder": "Ğő ŧő ƒőľđęř",
- "select-folder": "Ŝęľęčŧ ƒőľđęř"
- },
- "result-kind": {
- "dashboard": "Đäşĥþőäřđ",
- "folder": "Főľđęř",
- "panel": "Päʼnęľ"
- },
"results-table": {
"datasource-header": "Đäŧä şőūřčę",
"location-header": "Ŀőčäŧįőʼn",
@@ -1108,12 +1097,6 @@
"placeholder": "Ŝęäřčĥ ƒőř đäşĥþőäřđş äʼnđ ƒőľđęřş"
}
},
- "search-view": {
- "no-results": {
- "clear": "Cľęäř şęäřčĥ äʼnđ ƒįľŧęřş",
- "text": "Ńő řęşūľŧş ƒőūʼnđ ƒőř yőūř qūęřy."
- }
- },
"share-modal": {
"dashboard": {
"title": "Ŝĥäřę"
diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json
index 1cd5fe36376..a6594545607 100644
--- a/public/locales/zh-Hans/grafana.json
+++ b/public/locales/zh-Hans/grafana.json
@@ -76,9 +76,7 @@
"manage-folder-nav": {
"alert-rules": "警报规则",
"dashboards": "仪表板",
- "panels": "面板",
- "permissions": "权限",
- "settings": "设置"
+ "panels": "面板"
},
"new-folder-form": {
"cancel-label": "取消",
@@ -1079,15 +1077,6 @@
"new-dashboard": "新建仪表板",
"new-folder": "新建文件夹"
},
- "folder-view": {
- "go-to-folder": "前往文件夹",
- "select-folder": "选择文件夹"
- },
- "result-kind": {
- "dashboard": "仪表板",
- "folder": "文件夹",
- "panel": "面板"
- },
"results-table": {
"datasource-header": "数据源",
"location-header": "位置",
@@ -1102,12 +1091,6 @@
"placeholder": "搜索仪表板"
}
},
- "search-view": {
- "no-results": {
- "clear": "清除搜索和筛选条件",
- "text": "您的查询未找到结果。"
- }
- },
"share-modal": {
"dashboard": {
"title": "分享"