Skip to content

Commit 4fcabad

Browse files
committed
feature: Create UI for user and team management
(cherry picked from commit 20c33c10528dcff3eda272659ec5c1ea393818d6) chore (upstream): Fix build error in org plugin (team filter) (cherry picked from commit 0746adc456ce8de59392a50b9dfb32d1719f936a)
1 parent 3a1cf9c commit 4fcabad

25 files changed

Lines changed: 2713 additions & 23 deletions

File tree

backend/plugins/org/api/store.go

Lines changed: 618 additions & 1 deletion
Large diffs are not rendered by default.

backend/plugins/org/api/team.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ limitations under the License.
1818
package api
1919

2020
import (
21+
"net/http"
22+
2123
"github.com/apache/incubator-devlake/core/errors"
2224
"github.com/apache/incubator-devlake/core/models/domainlayer/crossdomain"
2325
"github.com/apache/incubator-devlake/core/plugin"
24-
"net/http"
2526

2627
"github.com/gocarina/gocsv"
2728
)
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
import (
21+
"net/http"
22+
"strconv"
23+
24+
"github.com/apache/incubator-devlake/core/errors"
25+
"github.com/apache/incubator-devlake/core/models/domainlayer"
26+
"github.com/apache/incubator-devlake/core/models/domainlayer/crossdomain"
27+
"github.com/apache/incubator-devlake/core/plugin"
28+
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
29+
"github.com/google/uuid"
30+
)
31+
32+
type paginatedTeams struct {
33+
Count int64 `json:"count"`
34+
Teams []teamTree `json:"teams"`
35+
}
36+
37+
type teamTree struct {
38+
Id string `json:"id"`
39+
Name string `json:"name"`
40+
Alias string `json:"alias"`
41+
ParentId string `json:"parentId"`
42+
SortingIndex int `json:"sortingIndex"`
43+
UserCount int `json:"userCount"`
44+
Children []teamTree `json:"children,omitempty"`
45+
}
46+
47+
// ListTeams returns teams with pagination support
48+
// @Summary List teams
49+
// @Description GET /plugins/org/teams?page=1&pageSize=50
50+
// @Tags plugins/org
51+
// @Produce json
52+
// @Param page query int false "page number (default 1)"
53+
// @Param pageSize query int false "page size (default 50)"
54+
// @Param name query string false "filter by name (case-insensitive, partial match)"
55+
// @Param grouped query bool false "when true, returns parent teams with nested children"
56+
// @Success 200 {object} paginatedTeams
57+
// @Failure 500 {object} shared.ApiBody "Internal Error"
58+
// @Router /plugins/org/teams [get]
59+
func (h *Handlers) ListTeams(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
60+
page, pageSize := 1, 50
61+
if p := input.Query.Get("page"); p != "" {
62+
if v, e := strconv.Atoi(p); e == nil && v > 0 {
63+
page = v
64+
}
65+
}
66+
if ps := input.Query.Get("pageSize"); ps != "" {
67+
if v, e := strconv.Atoi(ps); e == nil && v > 0 {
68+
pageSize = v
69+
}
70+
}
71+
grouped := false
72+
if groupedParam := input.Query.Get("grouped"); groupedParam != "" {
73+
groupedValue, parseErr := strconv.ParseBool(groupedParam)
74+
if parseErr != nil {
75+
return nil, errors.BadInput.Wrap(parseErr, "grouped must be a boolean value")
76+
}
77+
grouped = groupedValue
78+
}
79+
nameFilter := input.Query.Get("name")
80+
teams, count, err := h.store.findTeamsPaginated(page, pageSize, nameFilter, grouped)
81+
if err != nil {
82+
return nil, err
83+
}
84+
return &plugin.ApiResourceOutput{
85+
Body: paginatedTeams{Count: count, Teams: teams},
86+
Status: http.StatusOK,
87+
}, nil
88+
}
89+
90+
// GetTeamById returns a single team by ID
91+
// @Summary Get a team
92+
// @Description get a team by ID
93+
// @Tags plugins/org
94+
// @Produce json
95+
// @Param teamId path string true "team ID"
96+
// @Success 200 {object} team
97+
// @Failure 400 {object} shared.ApiBody "Bad Request"
98+
// @Failure 404 {object} shared.ApiBody "Not Found"
99+
// @Failure 500 {object} shared.ApiBody "Internal Error"
100+
// @Router /plugins/org/teams/{teamId} [get]
101+
func (h *Handlers) GetTeamById(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
102+
teamId := input.Params["teamId"]
103+
if teamId == "" {
104+
return nil, errors.BadInput.New("teamId is required")
105+
}
106+
t, err := h.store.findTeamById(teamId)
107+
if err != nil {
108+
return nil, err
109+
}
110+
return &plugin.ApiResourceOutput{
111+
Body: team{
112+
Id: t.Id,
113+
Name: t.Name,
114+
Alias: t.Alias,
115+
ParentId: t.ParentId,
116+
SortingIndex: t.SortingIndex,
117+
},
118+
Status: http.StatusOK,
119+
}, nil
120+
}
121+
122+
type createTeamsRequest struct {
123+
Teams []team `json:"teams"`
124+
}
125+
126+
// CreateTeams creates one or more teams
127+
// @Summary Create teams
128+
// @Description create one or more teams
129+
// @Tags plugins/org
130+
// @Accept json
131+
// @Produce json
132+
// @Param body body createTeamsRequest true "teams to create"
133+
// @Success 201 {array} team
134+
// @Failure 400 {object} shared.ApiBody "Bad Request"
135+
// @Failure 500 {object} shared.ApiBody "Internal Error"
136+
// @Router /plugins/org/teams [post]
137+
func (h *Handlers) CreateTeams(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
138+
var req createTeamsRequest
139+
err := helper.Decode(input.Body, &req, nil)
140+
if err != nil {
141+
return nil, errors.BadInput.Wrap(err, "invalid request body")
142+
}
143+
if len(req.Teams) == 0 {
144+
return nil, errors.BadInput.New("at least one team is required")
145+
}
146+
var created []team
147+
for _, t := range req.Teams {
148+
id := uuid.New().String()
149+
domainTeam := &crossdomain.Team{
150+
DomainEntity: domainlayer.DomainEntity{Id: id},
151+
Name: t.Name,
152+
Alias: t.Alias,
153+
ParentId: t.ParentId,
154+
SortingIndex: t.SortingIndex,
155+
}
156+
if err := h.store.createTeam(domainTeam); err != nil {
157+
return nil, err
158+
}
159+
t.Id = id
160+
created = append(created, t)
161+
}
162+
return &plugin.ApiResourceOutput{Body: created, Status: http.StatusCreated}, nil
163+
}
164+
165+
// UpdateTeamById updates a team by ID
166+
// @Summary Update a team
167+
// @Description update a team by ID
168+
// @Tags plugins/org
169+
// @Accept json
170+
// @Produce json
171+
// @Param teamId path string true "team ID"
172+
// @Param body body team true "team fields to update"
173+
// @Success 200 {object} team
174+
// @Failure 400 {object} shared.ApiBody "Bad Request"
175+
// @Failure 404 {object} shared.ApiBody "Not Found"
176+
// @Failure 500 {object} shared.ApiBody "Internal Error"
177+
// @Router /plugins/org/teams/{teamId} [put]
178+
func (h *Handlers) UpdateTeamById(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
179+
teamId := input.Params["teamId"]
180+
if teamId == "" {
181+
return nil, errors.BadInput.New("teamId is required")
182+
}
183+
existing, err := h.store.findTeamById(teamId)
184+
if err != nil {
185+
return nil, err
186+
}
187+
var t team
188+
if e := helper.Decode(input.Body, &t, nil); e != nil {
189+
return nil, errors.BadInput.Wrap(e, "invalid request body")
190+
}
191+
existing.Name = t.Name
192+
existing.Alias = t.Alias
193+
existing.ParentId = t.ParentId
194+
existing.SortingIndex = t.SortingIndex
195+
if err := h.store.updateTeam(existing); err != nil {
196+
return nil, err
197+
}
198+
return &plugin.ApiResourceOutput{
199+
Body: team{
200+
Id: existing.Id,
201+
Name: existing.Name,
202+
Alias: existing.Alias,
203+
ParentId: existing.ParentId,
204+
SortingIndex: existing.SortingIndex,
205+
},
206+
Status: http.StatusOK,
207+
}, nil
208+
}
209+
210+
// DeleteTeamById deletes a team by ID and its associated team_users
211+
// @Summary Delete a team
212+
// @Description delete a team by ID (cascades to team_users)
213+
// @Tags plugins/org
214+
// @Param teamId path string true "team ID"
215+
// @Success 200
216+
// @Failure 400 {object} shared.ApiBody "Bad Request"
217+
// @Failure 500 {object} shared.ApiBody "Internal Error"
218+
// @Router /plugins/org/teams/{teamId} [delete]
219+
func (h *Handlers) DeleteTeamById(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
220+
teamId := input.Params["teamId"]
221+
if teamId == "" {
222+
return nil, errors.BadInput.New("teamId is required")
223+
}
224+
if err := h.store.deleteTeam(teamId); err != nil {
225+
return nil, err
226+
}
227+
return &plugin.ApiResourceOutput{Status: http.StatusOK}, nil
228+
}

0 commit comments

Comments
 (0)