mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 06:22:13 +08:00
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:
@ -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;
|
||||
|
@ -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.",
|
||||
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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 () => {
|
||||
|
@ -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 && (
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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}
|
||||
|
@ -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'}>
|
||||
|
Reference in New Issue
Block a user