Compare commits

...

2 Commits

Author SHA1 Message Date
Mihai Doarna
f7337c65d1 Merge branch 'main' into dmihai/team-search-missing-fields 2025-12-19 11:13:15 +02:00
Mihai Doarna
375c22c5b0 add the missing fields to the team search results 2025-12-11 17:52:03 +02:00
8 changed files with 123 additions and 13 deletions

View File

@@ -2,6 +2,8 @@ package v0alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana/pkg/services/team"
)
// +k8s:deepcopy-gen=true
@@ -27,9 +29,12 @@ type TeamSearchResults struct {
// +k8s:deepcopy-gen=true
type TeamHit struct {
Name string `json:"name"`
Title string `json:"title"`
Email string `json:"email,omitempty"`
Provisioned bool `json:"provisioned,omitempty"`
ExternalUID string `json:"externalUID,omitempty"`
Name string `json:"name"`
Title string `json:"title"`
Email string `json:"email,omitempty"`
Provisioned bool `json:"provisioned,omitempty"`
ExternalUID string `json:"externalUID,omitempty"`
MemberCount int64 `json:"memberCount"`
Permission team.PermissionType `json:"permission"`
AccessControl map[string]bool `json:"accessControl,omitempty"`
}

View File

@@ -2,6 +2,8 @@ package team
import (
"context"
"encoding/binary"
"encoding/json"
"fmt"
"log/slog"
"math"
@@ -78,7 +80,7 @@ func (c *LegacyTeamSearchClient) Search(ctx context.Context, req *resourcepb.Res
namespace := signedInUser.GetNamespace()
for _, t := range res.Teams {
cells := createCells(t, req.Fields)
cells := createCells(c.log, t, req.Fields)
list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{
Key: getResourceKey(t, namespace),
Cells: cells,
@@ -120,7 +122,7 @@ func getDefaultColumns() []*resourcepb.ResourceTableColumnDefinition {
}
}
func createCells(t *team.TeamDTO, fields []string) [][]byte {
func createCells(log *slog.Logger, t *team.TeamDTO, fields []string) [][]byte {
cells := createDefaultCells(t)
for _, field := range fields {
fieldName := strings.TrimPrefix(field, res.SEARCH_FIELD_PREFIX)
@@ -131,6 +133,22 @@ func createCells(t *team.TeamDTO, fields []string) [][]byte {
cells = append(cells, []byte(strconv.FormatBool(t.IsProvisioned)))
case builders.TEAM_SEARCH_EXTERNAL_UID:
cells = append(cells, []byte(t.ExternalUID))
case builders.TEAM_SEARCH_MEMBER_COUNT:
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(t.MemberCount))
cells = append(cells, b)
case builders.TEAM_SEARCH_PERMISSION:
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, uint32(t.Permission))
cells = append(cells, b)
case builders.TEAM_SEARCH_ACCESS_CONTROL:
accessControl, err := json.Marshal(t.AccessControl)
if err != nil {
log.Error("error marshalling access control", "error", err)
cells = append(cells, []byte(""))
continue
}
cells = append(cells, accessControl)
}
}
return cells

View File

@@ -2,6 +2,7 @@ package team
import (
"context"
"encoding/binary"
"errors"
"math"
"testing"
@@ -25,7 +26,7 @@ func TestLegacyTeamSearchClient_Search(t *testing.T) {
Limit: 10,
Page: 1,
Query: "test",
Fields: []string{"name", "email", "provisioned", "externalUID"},
Fields: []string{"name", "email", "provisioned", "externalUID", "memberCount", "permission", "accessControl"},
}
mockTeamService.ExpectedSearchTeamsResult = team.SearchTeamQueryResult{
@@ -36,6 +37,9 @@ func TestLegacyTeamSearchClient_Search(t *testing.T) {
Email: "test@example.com",
IsProvisioned: true,
ExternalUID: "testExternalUID",
MemberCount: 10,
Permission: team.PermissionTypeAdmin,
AccessControl: map[string]bool{"test": true},
},
},
TotalCount: 1,
@@ -48,7 +52,7 @@ func TestLegacyTeamSearchClient_Search(t *testing.T) {
require.NoError(t, err)
require.Equal(t, int64(1), resp.TotalHits)
require.Len(t, resp.Results.Rows, 1)
require.Len(t, resp.Results.Columns, 5)
require.Len(t, resp.Results.Columns, 8)
require.Equal(t, "default", resp.Results.Rows[0].Key.Namespace)
require.Equal(t, "iam.grafana.com", resp.Results.Rows[0].Key.Group)
require.Equal(t, "teams", resp.Results.Rows[0].Key.Resource)
@@ -58,6 +62,9 @@ func TestLegacyTeamSearchClient_Search(t *testing.T) {
require.Equal(t, "test@example.com", string(resp.Results.Rows[0].Cells[2]))
require.Equal(t, "true", string(resp.Results.Rows[0].Cells[3]))
require.Equal(t, "testExternalUID", string(resp.Results.Rows[0].Cells[4]))
require.Equal(t, 10, int(binary.BigEndian.Uint64(resp.Results.Rows[0].Cells[5])))
require.Equal(t, team.PermissionTypeAdmin, team.PermissionType(binary.BigEndian.Uint32(resp.Results.Rows[0].Cells[6])))
require.Equal(t, "{\"test\":true}", string(resp.Results.Rows[0].Cells[7]))
})
t.Run("returns error if page is negative", func(t *testing.T) {

View File

@@ -179,6 +179,9 @@ func (s *TeamSearchHandler) DoTeamSearch(w http.ResponseWriter, r *http.Request)
resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_EMAIL,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_PROVISIONED,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_EXTERNAL_UID,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_MEMBER_COUNT,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_PERMISSION,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_SEARCH_ACCESS_CONTROL,
},
}

View File

@@ -89,7 +89,7 @@ func TestTeamSearchHandler(t *testing.T) {
if mockClient.LastSearchRequest == nil {
t.Fatalf("expected Search to be called, but it was not")
}
expectedFields := []string{"title", "fields.email", "fields.provisioned", "fields.externalUID"}
expectedFields := []string{"title", "fields.email", "fields.provisioned", "fields.externalUID", "fields.memberCount", "fields.permission", "fields.accessControl"}
if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) {
t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields)
}

View File

@@ -1,9 +1,12 @@
package search
import (
"encoding/binary"
"encoding/json"
"fmt"
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/storage/unified/search/builders"
@@ -22,6 +25,9 @@ func ParseResults(result *resourcepb.ResourceSearchResponse, offset int64) (v0al
emailIDX := -1
provisionedIDX := -1
externalUIDIDX := -1
memberCountIDX := -1
permissionIDX := -1
accessControlIDX := -1
for i, v := range result.Results.Columns {
if v == nil {
@@ -37,6 +43,12 @@ func ParseResults(result *resourcepb.ResourceSearchResponse, offset int64) (v0al
provisionedIDX = i
case builders.TEAM_SEARCH_EXTERNAL_UID:
externalUIDIDX = i
case builders.TEAM_SEARCH_MEMBER_COUNT:
memberCountIDX = i
case builders.TEAM_SEARCH_PERMISSION:
permissionIDX = i
case builders.TEAM_SEARCH_ACCESS_CONTROL:
accessControlIDX = i
}
}
@@ -75,6 +87,25 @@ func ParseResults(result *resourcepb.ResourceSearchResponse, offset int64) (v0al
hit.ExternalUID = string(row.Cells[externalUIDIDX])
}
if memberCountIDX >= 0 && row.Cells[memberCountIDX] != nil {
memberCount := binary.BigEndian.Uint64(row.Cells[memberCountIDX])
hit.MemberCount = int64(memberCount)
}
if permissionIDX >= 0 && row.Cells[permissionIDX] != nil {
permission := binary.BigEndian.Uint32(row.Cells[permissionIDX])
hit.Permission = team.PermissionType(permission)
}
if accessControlIDX >= 0 && row.Cells[accessControlIDX] != nil {
var accessControl map[string]bool
err := json.Unmarshal(row.Cells[accessControlIDX], &accessControl)
if err != nil {
return v0alpha1.TeamSearchResults{}, fmt.Errorf("error parsing team search response: error unmarshalling access control: %w", err)
}
hit.AccessControl = accessControl
}
sr.Hits[i] = *hit
}

View File

@@ -1,10 +1,12 @@
package search
import (
"encoding/binary"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
@@ -29,6 +31,18 @@ func TestParseResults(t *testing.T) {
Name: "externalUID",
Type: resourcepb.ResourceTableColumnDefinition_STRING,
},
{
Name: "memberCount",
Type: resourcepb.ResourceTableColumnDefinition_INT64,
},
{
Name: "permission",
Type: resourcepb.ResourceTableColumnDefinition_INT32,
},
{
Name: "accessControl",
Type: resourcepb.ResourceTableColumnDefinition_OBJECT,
},
},
Rows: []*resourcepb.ResourceTableRow{
{
@@ -41,6 +55,17 @@ func TestParseResults(t *testing.T) {
[]byte("team1@example.com"),
[]byte("true"),
[]byte("team1-uid"),
func() []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, 10)
return b
}(),
func() []byte {
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, 4)
return b
}(),
[]byte("{\"test\":true}"),
},
},
},
@@ -55,6 +80,9 @@ func TestParseResults(t *testing.T) {
require.Equal(t, "team1@example.com", results.Hits[0].Email)
require.True(t, results.Hits[0].Provisioned)
require.Equal(t, "team1-uid", results.Hits[0].ExternalUID)
require.Equal(t, int64(10), results.Hits[0].MemberCount)
require.Equal(t, team.PermissionTypeAdmin, results.Hits[0].Permission)
require.Equal(t, map[string]bool{"test": true}, results.Hits[0].AccessControl)
})
t.Run("should handle nil result", func(t *testing.T) {

View File

@@ -14,9 +14,12 @@ import (
)
const (
TEAM_SEARCH_EMAIL = "email"
TEAM_SEARCH_PROVISIONED = "provisioned"
TEAM_SEARCH_EXTERNAL_UID = "externalUID"
TEAM_SEARCH_EMAIL = "email"
TEAM_SEARCH_PROVISIONED = "provisioned"
TEAM_SEARCH_EXTERNAL_UID = "externalUID"
TEAM_SEARCH_MEMBER_COUNT = "memberCount"
TEAM_SEARCH_PERMISSION = "permission"
TEAM_SEARCH_ACCESS_CONTROL = "accessControl"
)
var TeamSearchTableColumnDefinitions = map[string]*resourcepb.ResourceTableColumnDefinition{
@@ -35,6 +38,21 @@ var TeamSearchTableColumnDefinitions = map[string]*resourcepb.ResourceTableColum
Type: resourcepb.ResourceTableColumnDefinition_STRING,
Description: "External UID of the team",
},
TEAM_SEARCH_MEMBER_COUNT: {
Name: TEAM_SEARCH_MEMBER_COUNT,
Type: resourcepb.ResourceTableColumnDefinition_INT64,
Description: "Number of members in the team",
},
TEAM_SEARCH_PERMISSION: {
Name: TEAM_SEARCH_PERMISSION,
Type: resourcepb.ResourceTableColumnDefinition_INT32,
Description: "Permission of the team",
},
TEAM_SEARCH_ACCESS_CONTROL: {
Name: TEAM_SEARCH_ACCESS_CONTROL,
Type: resourcepb.ResourceTableColumnDefinition_OBJECT,
Description: "Access control of the team",
},
}
func GetTeamSearchBuilder() (resource.DocumentBuilderInfo, error) {