FolderPicker: Remove old folder picker (#105374)

* Remove newFolderPicker toggle

* Remove usage of the old folder picker

* fix some tests

* remove old test
This commit is contained in:
Josh Hunt
2025-06-10 16:01:48 +02:00
committed by GitHub
parent 7e6eefc8cd
commit cbb828202a
14 changed files with 94 additions and 161 deletions

View File

@ -456,10 +456,6 @@ export interface FeatureToggles {
*/
alertingQueryOptimization?: boolean;
/**
* Enables the nested folder picker without having nested folders enabled
*/
newFolderPicker?: boolean;
/**
* Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group. Disables sequential evaluation if enabled.
*/
jitterAlertRulesWithinGroups?: boolean;

View File

@ -1,10 +1,9 @@
// To change feature flags, edit:
// pkg/services/featuremgmt/registry.go
// Then run tests in:
// pkg/services/featuremgmt/toggles_gen_test.go
// twice to generate and validate the feature flag files
// To change feature flags edit this file and run:
// make gen-feature-toggles
//
// Alternatively, use `make gen-feature-toggles`
// Alternatively run tests in:
// pkg/services/featuremgmt/toggles_gen_test.go
// twice to generate and validate the feature flag files
package featuremgmt
@ -766,13 +765,6 @@ var (
AllowSelfServe: false,
Expression: "false",
},
{
Name: "newFolderPicker",
Description: "Enables the nested folder picker without having nested folders enabled",
Stage: FeatureStageExperimental,
Owner: grafanaFrontendPlatformSquad,
FrontendOnly: true,
},
{
Name: "jitterAlertRulesWithinGroups",
Description: "Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group. Disables sequential evaluation if enabled.",

View File

@ -100,7 +100,6 @@ lokiQueryHints,GA,@grafana/observability-logs,false,false,true
kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true
cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false
alertingQueryOptimization,GA,@grafana/alerting-squad,false,false,false
newFolderPicker,experimental,@grafana/grafana-frontend-platform,false,false,true
jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false
onPremToCloudMigrations,GA,@grafana/grafana-operator-experience-squad,false,false,false
secretsManagementAppPlatform,experimental,@grafana/grafana-operator-experience-squad,false,false,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
100 kubernetesFeatureToggles experimental @grafana/grafana-operator-experience-squad false false true
101 cloudRBACRoles preview @grafana/identity-access-team false true false
102 alertingQueryOptimization GA @grafana/alerting-squad false false false
newFolderPicker experimental @grafana/grafana-frontend-platform false false true
103 jitterAlertRulesWithinGroups preview @grafana/alerting-squad false true false
104 onPremToCloudMigrations GA @grafana/grafana-operator-experience-squad false false false
105 secretsManagementAppPlatform experimental @grafana/grafana-operator-experience-squad false false false

View File

@ -411,10 +411,6 @@ const (
// Optimizes eligible queries in order to reduce load on datasources
FlagAlertingQueryOptimization = "alertingQueryOptimization"
// FlagNewFolderPicker
// Enables the nested folder picker without having nested folders enabled
FlagNewFolderPicker = "newFolderPicker"
// FlagJitterAlertRulesWithinGroups
// Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group. Disables sequential evaluation if enabled.
FlagJitterAlertRulesWithinGroups = "jitterAlertRulesWithinGroups"

View File

@ -2287,7 +2287,8 @@
"metadata": {
"name": "newFolderPicker",
"resourceVersion": "1743693517832",
"creationTimestamp": "2024-01-15T11:43:19Z"
"creationTimestamp": "2024-01-15T11:43:19Z",
"deletionTimestamp": "2025-05-14T09:11:28Z"
},
"spec": {
"description": "Enables the nested folder picker without having nested folders enabled",

View File

@ -1,70 +1,3 @@
import { useCallback } from 'react';
import { NestedFolderPicker } from '../NestedFolderPicker/NestedFolderPicker';
import { config } from '@grafana/runtime';
import { NestedFolderPicker, NestedFolderPickerProps } from '../NestedFolderPicker/NestedFolderPicker';
import { OldFolderPicker } from './OldFolderPicker';
interface FolderPickerProps extends NestedFolderPickerProps {
// These props are only used by the old folder picker, and should be removed when old picker is removed
/** @deprecated */
initialTitle?: string;
/** @deprecated */
inputId?: string;
/** @deprecated */
dashboardId?: number | string;
/** @deprecated */
enableCreateNew?: boolean;
/** @deprecated */
skipInitialLoad?: boolean;
}
// Temporary wrapper component to switch between the NestedFolderPicker and the old flat
// FolderPicker depending on feature flags
export function FolderPicker(props: FolderPickerProps) {
const nestedEnabled = config.featureToggles.newFolderPicker || config.featureToggles.nestedFolders;
const { initialTitle, dashboardId, enableCreateNew, ...newFolderPickerProps } = props;
return nestedEnabled ? <NestedFolderPicker {...newFolderPickerProps} /> : <OldFolderPickerWrapper {...props} />;
}
// Converts new NestedFolderPicker props to old non-nested folder picker props
// Seperate component so the hooks aren't created if not used
function OldFolderPickerWrapper({
value,
showRootFolder,
onChange,
initialTitle,
dashboardId,
enableCreateNew,
inputId,
skipInitialLoad,
}: FolderPickerProps) {
const handleOnChange = useCallback(
(newFolder: { title: string; uid: string }) => {
if (onChange) {
onChange(newFolder.uid, newFolder.title);
}
},
[onChange]
);
return (
<OldFolderPicker
onChange={handleOnChange}
showRoot={showRootFolder}
initialFolderUid={value}
initialTitle={initialTitle}
inputId={inputId}
skipInitialLoad={skipInitialLoad}
dashboardId={dashboardId}
enableCreateNew={enableCreateNew}
/>
);
}
export const FolderPicker = NestedFolderPicker;

View File

@ -1,35 +1,79 @@
import { render as rtlRender, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TestProvider } from 'test/helpers/TestProvider';
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { HttpResponse, http } from 'msw';
import { SetupServer, setupServer } from 'msw/node';
import { render, screen } from 'test/test-utils';
import { selectors } from '@grafana/e2e-selectors';
import { setBackendSrv } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/__mocks__/backend_srv';
import * as api from 'app/features/manage-dashboards/state/actions';
import { DashboardSearchHit } from 'app/features/search/types';
import { backendSrv } from 'app/core/services/backend_srv';
import { treeViewersCanEdit, wellFormedTree } from '../../fixtures/dashboardsTreeItem.fixture';
import { MoveModal, Props } from './MoveModal';
function render(...[ui, options]: Parameters<typeof rtlRender>) {
rtlRender(<TestProvider>{ui}</TestProvider>, options);
}
const [mockTree, { folderA }] = wellFormedTree();
const [mockTreeThatViewersCanEdit /* shares folders with wellFormedTree */] = treeViewersCanEdit();
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => backendSrv,
}));
describe('browse-dashboards MoveModal', () => {
const mockOnDismiss = jest.fn();
const mockOnConfirm = jest.fn();
const mockFolders = [
{ title: 'Dashboards', uid: '' } as DashboardSearchHit,
{ title: 'Folder 1', uid: 'wfTJJL5Wz' } as DashboardSearchHit,
];
let props: Props;
let server: SetupServer;
window.HTMLElement.prototype.scrollIntoView = () => {};
beforeAll(() => {
setBackendSrv(backendSrv);
jest.spyOn(backendSrv, 'get').mockResolvedValue({
dashboard: 0,
folder: 0,
});
server = setupServer(
http.get('/api/folders/:uid', () => {
return HttpResponse.json({
title: folderA.item.title,
uid: folderA.item.uid,
});
}),
http.get('/api/folders/:uid/counts', () => {
return HttpResponse.json({
folder: 1,
dashboard: 2,
librarypanel: 3,
alertrule: 4,
});
}),
http.get('/apis/provisioning.grafana.app/v0alpha1/namespaces/default/settings', () => {
return HttpResponse.json({
items: [],
});
}),
http.get('/api/folders', ({ request }) => {
const url = new URL(request.url);
const parentUid = url.searchParams.get('parentUid') ?? undefined;
const permission = url.searchParams.get('permission');
const limit = parseInt(url.searchParams.get('limit') ?? '1000', 10);
const page = parseInt(url.searchParams.get('page') ?? '1', 10);
const tree = permission === 'Edit' ? mockTreeThatViewersCanEdit : mockTree;
// reconstruct a folder API response from the flat tree fixture
const folders = tree
.filter((v) => v.item.kind === 'folder' && v.item.parentUID === parentUid)
.map((folder) => {
return {
uid: folder.item.uid,
title: folder.item.kind === 'folder' ? folder.item.title : "invalid - this shouldn't happen",
};
})
.slice(limit * (page - 1), limit * page);
return HttpResponse.json(folders);
})
);
server.listen();
});
beforeEach(() => {
@ -44,9 +88,10 @@ describe('browse-dashboards MoveModal', () => {
panel: {},
},
};
});
// mock the searchFolders api call so the folder picker has some folders in it
jest.spyOn(api, 'searchFolders').mockResolvedValue(mockFolders);
afterAll(() => {
server.close();
});
it('renders a dialog with the correct title', async () => {
@ -70,7 +115,7 @@ describe('browse-dashboards MoveModal', () => {
it('displays a folder picker', async () => {
render(<MoveModal {...props} />);
expect(await screen.findByTestId(selectors.components.FolderPicker.input)).toBeInTheDocument();
expect(await screen.findByRole('button', { name: 'Select folder' })).toBeInTheDocument();
});
it('displays a warning about permissions if a folder is selected', async () => {
@ -84,23 +129,24 @@ describe('browse-dashboards MoveModal', () => {
).toBeInTheDocument();
});
it('only enables the `Move` button if a folder is selected', async () => {
it('enables the `Move` button once a folder is selected', async () => {
render(<MoveModal {...props} />);
expect(await screen.findByRole('button', { name: 'Move' })).toBeDisabled();
const folderPicker = await screen.findByTestId(selectors.components.FolderPicker.input);
await selectOptionInTest(folderPicker, mockFolders[1].title);
expect(await screen.findByRole('button', { name: 'Move' })).toBeEnabled();
});
// Open the picker and wait for children to load
const folderPicker = await screen.findByRole('button', { name: 'Select folder' });
await userEvent.click(folderPicker);
await screen.findByLabelText(folderA.item.title);
it('calls onConfirm when clicking the `Move` button', async () => {
render(<MoveModal {...props} />);
const folderPicker = await screen.findByTestId(selectors.components.FolderPicker.input);
// Select the folder
await userEvent.click(screen.getByLabelText(folderA.item.title));
await selectOptionInTest(folderPicker, mockFolders[1].title);
await userEvent.click(await screen.findByRole('button', { name: 'Move' }));
expect(mockOnConfirm).toHaveBeenCalledWith(mockFolders[1].uid);
const moveButton = await screen.findByRole('button', { name: 'Move' });
expect(moveButton).toBeEnabled();
await userEvent.click(moveButton);
expect(mockOnConfirm).toHaveBeenCalledWith(folderA.item.uid);
});
it('calls onDismiss when clicking the `Cancel` button', async () => {

View File

@ -43,7 +43,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
},
});
const { errors, isValid, defaultValues } = formState;
const { errors, isValid } = formState;
const formValues = watch();
const { state, onSaveDashboard } = useSaveDashboard(false);
@ -158,11 +158,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
},
});
}}
// Old folder picker fields
value={formValues.folder?.uid}
initialTitle={defaultValues!.folder!.title}
dashboardId={dashboard.state.id ?? undefined}
enableCreateNew
/>
</Field>
{!changeInfo.isNew && (

View File

@ -214,7 +214,6 @@ export function SaveProvisionedDashboardForm({
render={({ field: { ref, value, onChange, ...field } }) => {
return (
<FolderPicker
inputId="dashboard-folder"
onChange={async (uid?: string, title?: string) => {
onChange({ uid, title });
// Update folderUid URL param

View File

@ -227,15 +227,7 @@ export class GeneralSettingsEditView
{dashboard.isManagedRepository() ? (
<Input readOnly value={meta.folderTitle} />
) : (
<FolderPicker
value={meta.folderUid}
onChange={model.onFolderChange}
// TODO deprecated props that can be removed once NestedFolderPicker is enabled by default
initialTitle={meta.folderTitle}
inputId="dashboard-folder-input"
enableCreateNew
skipInitialLoad
/>
<FolderPicker value={meta.folderUid} onChange={model.onFolderChange} />
)}
</Field>

View File

@ -165,16 +165,7 @@ export function GeneralSettingsUnconnected({
</Field>
<Field label={t('dashboard-settings.general.folder-label', 'Folder')}>
<FolderPicker
value={dashboard.meta.folderUid}
onChange={onFolderChange}
// TODO deprecated props that can be removed once NestedFolderPicker is enabled by default
initialTitle={dashboard.meta.folderTitle}
inputId="dashboard-folder-input"
enableCreateNew
dashboardId={dashboard.id}
skipInitialLoad
/>
<FolderPicker value={dashboard.meta.folderUid} onChange={onFolderChange} />
</Field>
<Field

View File

@ -1,5 +1,5 @@
import { screen, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render, screen } from 'test/test-utils';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import * as api from 'app/features/manage-dashboards/state/actions';

View File

@ -179,10 +179,6 @@ export const SaveDashboardAsForm = ({
{...field}
onChange={(uid: string | undefined, title: string | undefined) => field.onChange({ uid, title })}
value={field.value?.uid}
// Old folder picker fields
initialTitle={dashboard.meta.folderTitle}
dashboardId={dashboard.id}
enableCreateNew
/>
)}
control={control}

View File

@ -86,11 +86,7 @@ export const AddLibraryPanelContents = ({
'Library panel permissions are derived from the folder permissions'
)}
>
<FolderPicker
onChange={(uid) => setFolderUid(uid)}
value={folderUid}
inputId="share-panel-library-panel-folder-picker"
/>
<FolderPicker onChange={(uid) => setFolderUid(uid)} value={folderUid} />
</Field>
{config.featureToggles.newDashboardSharingComponent ? (
<Stack gap={1} justifyContent={'start'}>