Folders: Update folder using app platform APIs (#110449)

This commit is contained in:
Tom Ratcliffe
2025-09-03 11:29:26 +01:00
committed by GitHub
parent 6c517f82ed
commit 95080d9d56
16 changed files with 234 additions and 171 deletions
@@ -11,6 +11,7 @@ const collator = new Intl.Collator();
const mockAccessControl = {
'dashboards.permissions:write': true,
'dashboards:create': true,
'folders:write': true,
};
const additionalProperties = {
canAdmin: true,
@@ -108,6 +109,19 @@ const createFolderHandler = () =>
});
});
const handlers = [listFoldersHandler(), getFolderHandler(), createFolderHandler()];
const saveFolderHandler = () =>
http.put<{ uid: string }, { title: string; version: number }>('/api/folders/:uid', async ({ params, request }) => {
const { uid } = params;
const body = await request.json();
const folder = mockTree.find((v) => v.item.uid === uid);
if (!folder) {
return HttpResponse.json({ message: 'folder not found' }, { status: 404 });
}
return HttpResponse.json({ ...folder.item, title: body.title });
});
const handlers = [listFoldersHandler(), getFolderHandler(), createFolderHandler(), saveFolderHandler()];
export default handlers;
@@ -0,0 +1,17 @@
/** Expected constant response from `/api/search/sorting` */
export const SORT_OPTIONS = {
sortOptions: [
{
description: 'Sort results in an alphabetically ascending order',
displayName: 'Alphabetically (A–Z)',
meta: '',
name: 'alpha-asc',
},
{
description: 'Sort results in an alphabetically descending order',
displayName: 'Alphabetically (Z–A)',
meta: '',
name: 'alpha-desc',
},
],
};
@@ -3,6 +3,8 @@ import { HttpResponse, http } from 'msw';
import { wellFormedTree } from '../../../fixtures/folders';
import { SORT_OPTIONS } from './constants';
const [mockTree] = wellFormedTree();
type FilterArray = Array<(v: (typeof mockTree)[number]) => boolean>;
@@ -70,4 +72,6 @@ const getLegacySearchHandler = () =>
return HttpResponse.json(response);
});
export default [getLegacySearchHandler()];
const getSearchSortingHandler = () => http.get('/api/search/sorting', () => HttpResponse.json(SORT_OPTIONS));
export default [getLegacySearchHandler(), getSearchSortingHandler()];
@@ -7,32 +7,42 @@ const [mockTree] = wellFormedTree();
type FilterArray = Array<(v: (typeof mockTree)[number]) => boolean>;
const typeMap: Record<string, string> = {
folder: 'folders',
dashboard: 'dashboards',
};
const getSearchHandler = () =>
http.get('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/search', ({ request }) => {
http.get('/apis/dashboard.grafana.app/v0alpha1/namespaces/:namespace/search', ({ request }) => {
const folderFilter = new URL(request.url).searchParams.get('folder') || null;
const typeFilter = new URL(request.url).searchParams.get('type') || null;
const response = mockTree
.filter((filterItem) => {
const filters: FilterArray = [];
if (folderFilter && folderFilter !== 'general') {
filters.push(({ item }) => item.kind === 'folder' && item.parentUID === folderFilter);
}
if (folderFilter === 'general') {
filters.push(({ item }) => item.kind === 'folder' && item.parentUID === undefined);
}
if (typeFilter) {
filters.push(({ item }) => item.kind === typeFilter);
}
if (folderFilter && folderFilter !== 'general') {
filters.push(
({ item }) => (item.kind === 'folder' || item.kind === 'dashboard') && item.parentUID === folderFilter
);
}
if (folderFilter === 'general') {
filters.push(
({ item }) => (item.kind === 'folder' || item.kind === 'dashboard') && item.parentUID === undefined
);
}
return filters.every((filterPredicate) => filterPredicate(filterItem));
})
.map(({ item }) => {
const random = Chance(item.uid);
return {
resource: 'folders',
resource: typeMap[item.kind],
name: item.uid,
title: item.title,
field: {
@@ -11,6 +11,32 @@ const baseResponse = {
apiVersion: 'folder.grafana.app/v1beta1',
};
const folderToAppPlatform = (folder: (typeof mockTree)[number]['item'], id?: number, namespace?: string) => {
return {
...baseResponse,
metadata: {
name: folder.uid,
namespace: namespace ?? 'default',
uid: folder.uid,
creationTimestamp: '2023-01-01T00:00:00Z',
annotations: {
// TODO: Generalise annotations in fixture data
'grafana.app/createdBy': 'user:1',
'grafana.app/updatedBy': 'user:2',
'grafana.app/managedBy': 'user',
'grafana.app/updatedTimestamp': '2024-01-01T00:00:00Z',
'grafana.app/folder': folder.kind === 'folder' ? folder.parentUID : undefined,
},
labels: {
'grafana.app/deprecatedInternalID': id ?? '123',
},
},
spec: { title: folder.title, description: '' },
status: {},
};
};
const folderNotFoundError = getErrorResponse('folder not found', 404);
const getFolderHandler = () =>
@@ -26,28 +52,9 @@ const getFolderHandler = () =>
return HttpResponse.json(folderNotFoundError, { status: 404 });
}
return HttpResponse.json({
...baseResponse,
metadata: {
name: response.item.uid,
namespace,
uid: response.item.uid,
creationTimestamp: '2023-01-01T00:00:00Z',
annotations: {
// TODO: Generalise annotations in fixture data
'grafana.app/createdBy': 'user:1',
'grafana.app/updatedBy': 'user:2',
'grafana.app/managedBy': 'user',
'grafana.app/updatedTimestamp': '2024-01-01T00:00:00Z',
'grafana.app/folder': response.item.kind === 'folder' ? response.item.parentUID : undefined,
},
labels: {
'grafana.app/deprecatedInternalID': '123',
},
},
spec: { title: response.item.title, description: '' },
status: {},
});
const appPlatformFolder = folderToAppPlatform(response.item, undefined, namespace);
return HttpResponse.json(appPlatformFolder);
}
);
@@ -121,35 +128,40 @@ const createFolderHandler = () =>
const parentUid = body?.metadata?.annotations?.['grafana.app/folder'];
const random = Chance(title);
const name = random.string({ length: 10 });
const uid = random.string({ length: 45 });
const id = random.integer({ min: 1, max: 1000 });
return HttpResponse.json({
...baseResponse,
metadata: {
name,
namespace,
uid,
resourceVersion: '1756207979831',
generation: 1,
creationTimestamp: '2025-08-26T11:32:59Z',
labels: {
'grafana.app/deprecatedInternalID': id,
},
annotations: {
'grafana.app/createdBy': 'user:1',
'grafana.app/folder': parentUid,
'grafana.app/updatedBy': 'user:1',
'grafana.app/updatedTimestamp': '2025-08-26T11:32:59Z',
},
},
spec: {
title,
description: '',
},
status: {},
});
const appPlatformFolder = folderToAppPlatform(
{ uid, title, parentUID: parentUid, kind: 'folder' },
id,
namespace
);
return HttpResponse.json(appPlatformFolder);
}
);
export default [getFolderHandler(), getFolderParentsHandler(), createFolderHandler()];
const replaceFolderHandler = () =>
http.put<{ folderUid: string; namespace: string }, PartialFolderPayload>(
'/apis/folder.grafana.app/v1beta1/namespaces/:namespace/folders/:folderUid',
async ({ params, request }) => {
const body = await request.json();
const { folderUid } = params;
const response = mockTree.find(({ item }) => {
return item.uid === folderUid;
});
if (!response) {
return HttpResponse.json(folderNotFoundError, { status: 404 });
}
const modifiedFolder = {
...response.item,
title: body.spec.title,
};
const appPlatformFolder = folderToAppPlatform(modifiedFolder);
return HttpResponse.json(appPlatformFolder);
}
);
export default [getFolderHandler(), getFolderParentsHandler(), createFolderHandler(), replaceFolderHandler()];