NestedFolderPicker: Migrate to app platform API (#106926)

* Add /children endpoint

* Update folder client

* Add comment

* Add feature toggle

* Add new version of useFoldersQuery

* Error handling

* Format

* Rename feature toggle

* Remove options and move root folder constant

* Fix feature toggle merge

* Add feature toggle again

* Rename useFoldersQuery files

* Update API spec

* Fix test

* Add test

* Better typings

---------

Co-authored-by: Clarity-89 <homes89@ukr.net>
This commit is contained in:
Andrej Ocenas
2025-07-03 16:15:23 +02:00
committed by GitHub
co-authored by Clarity-89
parent 185ce90a4b
commit e76f470b44
18 changed files with 1102 additions and 193 deletions
+5
View File
@@ -192,6 +192,11 @@ func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.API
storage[resourceInfo.StoragePath("counts")] = &subCountREST{searcher: b.searcher}
storage[resourceInfo.StoragePath("access")] = &subAccessREST{b.folderSvc, b.ac}
// Adds a path to return children of a given folder
storage[resourceInfo.StoragePath("children")] = &subChildrenREST{
lister: storage[resourceInfo.StoragePath()].(rest.Lister),
}
apiGroupInfo.VersionedResourcesStorageMap[folders.VERSION] = storage
b.storage = storage[resourceInfo.StoragePath()].(grafanarest.Storage)
return nil
+73
View File
@@ -0,0 +1,73 @@
package folders
import (
"context"
"fmt"
"net/http"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
)
type subChildrenREST struct {
lister rest.Lister
}
var _ = rest.Connecter(&subChildrenREST{})
var _ = rest.StorageMetadata(&subChildrenREST{})
// RootFolderName Hardcoded magic const to get root folders without parent.
var RootFolderName = "general"
func (r *subChildrenREST) New() runtime.Object {
return &folders.FolderList{}
}
func (r *subChildrenREST) Destroy() {
}
func (r *subChildrenREST) ProducesMIMETypes(verb string) []string {
return nil
}
func (r *subChildrenREST) ProducesObject(verb string) interface{} {
return &folders.FolderList{}
}
func (r *subChildrenREST) ConnectMethods() []string {
return []string{"GET"}
}
func (r *subChildrenREST) NewConnectOptions() (runtime.Object, bool, string) {
return nil, false, "" // true means you can use the trailing path as a variable
}
func (r *subChildrenREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
obj, err := r.lister.List(ctx, &internalversion.ListOptions{})
if err != nil {
return nil, err
}
allFolders, ok := obj.(*folders.FolderList)
if !ok {
return nil, fmt.Errorf("could not list folders")
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
children := &folders.FolderList{}
parentName := ""
if name != RootFolderName {
parentName = name
}
for _, folder := range allFolders.Items {
if parentName == getParent(&folder) {
children.Items = append(children.Items, folder)
}
}
responder.Object(http.StatusOK, children)
}), nil
}
+10
View File
@@ -1734,6 +1734,16 @@ var (
FrontendOnly: true,
Expression: "false", // extensions will be disabled by default
},
{
Name: "foldersAppPlatformAPI",
Description: "Enables use of app platform API for folders",
Stage: FeatureStageExperimental,
Owner: grafanaFrontendSearchNavOrganise,
HideFromAdminPage: true,
HideFromDocs: true,
FrontendOnly: true,
Expression: "false",
},
}
)
+1
View File
@@ -226,3 +226,4 @@ preferLibraryPanelTitle,privatePreview,@grafana/dashboards-squad,false,false,fal
tabularNumbers,GA,@grafana/grafana-frontend-platform,false,false,false
newInfluxDSConfigPageDesign,privatePreview,@grafana/partner-datasources,false,false,false
enableAppChromeExtensions,experimental,@grafana/plugins-platform-backend,false,false,true
foldersAppPlatformAPI,experimental,@grafana/grafana-search-navigate-organise,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
226 tabularNumbers GA @grafana/grafana-frontend-platform false false false
227 newInfluxDSConfigPageDesign privatePreview @grafana/partner-datasources false false false
228 enableAppChromeExtensions experimental @grafana/plugins-platform-backend false false true
229 foldersAppPlatformAPI experimental @grafana/grafana-search-navigate-organise false false true
+4
View File
@@ -914,4 +914,8 @@ const (
// FlagEnableAppChromeExtensions
// Set this to true to enable all app chrome extensions registered by plugins.
FlagEnableAppChromeExtensions = "enableAppChromeExtensions"
// FlagFoldersAppPlatformAPI
// Enables use of app platform API for folders
FlagFoldersAppPlatformAPI = "foldersAppPlatformAPI"
)
+16
View File
@@ -1257,6 +1257,22 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "foldersAppPlatformAPI",
"resourceVersion": "1751377081192",
"creationTimestamp": "2025-07-01T13:38:01Z"
},
"spec": {
"description": "Enables use of app platform API for folders",
"stage": "experimental",
"codeowner": "@grafana/grafana-search-navigate-organise",
"frontend": true,
"hideFromAdminPage": true,
"hideFromDocs": true,
"expression": "false"
}
},
{
"metadata": {
"name": "formatString",
+9
View File
@@ -88,6 +88,15 @@ func TestIntegrationFoldersApp(t *testing.T) {
"get"
]
},
{
"name": "folders/children",
"singularName": "",
"namespaced": true,
"kind": "FolderList",
"verbs": [
"get"
]
},
{
"name": "folders/counts",
"singularName": "",
@@ -915,6 +915,55 @@
}
]
},
"/apis/folder.grafana.app/v1beta1/namespaces/{namespace}/folders/{name}/children": {
"get": {
"tags": [
"Folder"
],
"description": "connect GET requests to children of Folder",
"operationId": "getFolderChildren",
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.folder.pkg.apis.folder.v1beta1.FolderList"
}
}
}
}
},
"x-kubernetes-action": "connect",
"x-kubernetes-group-version-kind": {
"group": "folder.grafana.app",
"version": "v1beta1",
"kind": "FolderList"
}
},
"parameters": [
{
"name": "name",
"in": "path",
"description": "name of the FolderList",
"required": true,
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "namespace",
"in": "path",
"description": "object name and auth scope, such as for teams and projects",
"required": true,
"schema": {
"type": "string",
"uniqueItems": true
}
}
]
},
"/apis/folder.grafana.app/v1beta1/namespaces/{namespace}/folders/{name}/counts": {
"get": {
"tags": [