mirror of
				https://github.com/onepanelio/onepanel.git
				synced 2025-10-31 00:36:18 +08:00 
			
		
		
		
	feat: added support for sorting and filtering workspaces.
* Also added a LabelFilter interface
This commit is contained in:
		
							
								
								
									
										28
									
								
								pkg/filter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								pkg/filter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | package v1 | ||||||
|  |  | ||||||
|  | import sq "github.com/Masterminds/squirrel" | ||||||
|  |  | ||||||
|  | // LabelFilter represents a filter that has labels | ||||||
|  | type LabelFilter interface { | ||||||
|  | 	// GetLabels returns the labels to filter by. These are assumed to be ANDed together. | ||||||
|  | 	GetLabels() []*Label | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ApplyLabelSelectQuery returns a query builder that adds where statements to filter by labels in the filter, if there are any | ||||||
|  | // labelSelector is the database column that has the labels, such as "we.labels" for workflowExecutions aliased by "we". | ||||||
|  | func ApplyLabelSelectQuery(labelSelector string, sb sq.SelectBuilder, filter LabelFilter) (sq.SelectBuilder, error) { | ||||||
|  | 	labels := filter.GetLabels() | ||||||
|  |  | ||||||
|  | 	if len(labels) == 0 { | ||||||
|  | 		return sb, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	labelsJSON, err := LabelsToJSONString(labels) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return sb, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sb = sb.Where("%v @> ?", labelSelector, labelsJSON) | ||||||
|  |  | ||||||
|  | 	return sb, nil | ||||||
|  | } | ||||||
| @@ -65,24 +65,11 @@ func typeWorkflow(wf *wfv1.Workflow) (workflow *WorkflowExecution) { | |||||||
| // WorkflowExecutionFilter represents the available ways we can filter WorkflowExecutions | // WorkflowExecutionFilter represents the available ways we can filter WorkflowExecutions | ||||||
| type WorkflowExecutionFilter struct { | type WorkflowExecutionFilter struct { | ||||||
| 	Labels []*Label | 	Labels []*Label | ||||||
| 	Phase  string //empty string means none | 	Phase  string // empty string means none | ||||||
| } | } | ||||||
|  |  | ||||||
| // applyWorkflowExecutionLabelSelectQuery returns a query builder that adds where statements to filter by labels in the request, | func (wf *WorkflowExecutionFilter) GetLabels() []*Label { | ||||||
| // if there are any | 	return wf.Labels | ||||||
| func applyWorkflowExecutionLabelSelectQuery(sb sq.SelectBuilder, filter *WorkflowExecutionFilter) (sq.SelectBuilder, error) { |  | ||||||
| 	if len(filter.Labels) == 0 { |  | ||||||
| 		return sb, nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	labelsJSON, err := LabelsToJSONString(filter.Labels) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return sb, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	sb = sb.Where("we.labels @> ?", labelsJSON) |  | ||||||
|  |  | ||||||
| 	return sb, nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func applyWorkflowExecutionFilter(sb sq.SelectBuilder, request *request.Request) (sq.SelectBuilder, error) { | func applyWorkflowExecutionFilter(sb sq.SelectBuilder, request *request.Request) (sq.SelectBuilder, error) { | ||||||
| @@ -95,7 +82,7 @@ func applyWorkflowExecutionFilter(sb sq.SelectBuilder, request *request.Request) | |||||||
| 		return sb, nil | 		return sb, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	sb, err := applyWorkflowExecutionLabelSelectQuery(sb, &filter) | 	sb, err := ApplyLabelSelectQuery("we.labels", sb, &filter) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return sb, err | 		return sb, err | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import ( | |||||||
| 	"github.com/lib/pq" | 	"github.com/lib/pq" | ||||||
| 	"github.com/onepanelio/core/pkg/util" | 	"github.com/onepanelio/core/pkg/util" | ||||||
| 	"github.com/onepanelio/core/pkg/util/ptr" | 	"github.com/onepanelio/core/pkg/util/ptr" | ||||||
| 	"github.com/onepanelio/core/pkg/util/request/pagination" | 	"github.com/onepanelio/core/pkg/util/request" | ||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
| 	"google.golang.org/grpc/codes" | 	"google.golang.org/grpc/codes" | ||||||
| 	v1 "k8s.io/api/core/v1" | 	v1 "k8s.io/api/core/v1" | ||||||
| @@ -20,6 +20,35 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // WorkspaceFilter represents the available ways we can filter Workspaces | ||||||
|  | type WorkspaceFilter struct { | ||||||
|  | 	Labels []*Label | ||||||
|  | 	Phase  string // empty string means none | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetLabels gets the labels of the filter | ||||||
|  | func (wf *WorkspaceFilter) GetLabels() []*Label { | ||||||
|  | 	return wf.Labels | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func applyWorkspaceFilter(sb sq.SelectBuilder, request *request.Request) (sq.SelectBuilder, error) { | ||||||
|  | 	if request.Filter == nil { | ||||||
|  | 		return sb, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	filter, ok := request.Filter.(WorkspaceFilter) | ||||||
|  | 	if !ok { | ||||||
|  | 		return sb, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sb, err := ApplyLabelSelectQuery("w.labels", sb, &filter) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return sb, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return sb, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (c *Client) workspacesSelectBuilder(namespace string) sq.SelectBuilder { | func (c *Client) workspacesSelectBuilder(namespace string) sq.SelectBuilder { | ||||||
| 	sb := sb.Select(getWorkspaceColumns("w")...). | 	sb := sb.Select(getWorkspaceColumns("w")...). | ||||||
| 		Columns(getWorkspaceStatusColumns("w", "status")...). | 		Columns(getWorkspaceStatusColumns("w", "status")...). | ||||||
| @@ -562,13 +591,12 @@ func (c *Client) ListWorkspacesByTemplateID(namespace string, templateID uint64) | |||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
| func (c *Client) ListWorkspaces(namespace string, paginator *pagination.PaginationRequest) (workspaces []*Workspace, err error) { | func (c *Client) ListWorkspaces(namespace string, request *request.Request) (workspaces []*Workspace, err error) { | ||||||
| 	sb := sb.Select(getWorkspaceColumns("w")...). | 	sb := sb.Select(getWorkspaceColumns("w")...). | ||||||
| 		Columns(getWorkspaceStatusColumns("w", "status")...). | 		Columns(getWorkspaceStatusColumns("w", "status")...). | ||||||
| 		Columns(getWorkspaceTemplateColumns("wt", "workspace_template")...). | 		Columns(getWorkspaceTemplateColumns("wt", "workspace_template")...). | ||||||
| 		From("workspaces w"). | 		From("workspaces w"). | ||||||
| 		Join("workspace_templates wt ON wt.id = w.workspace_template_id"). | 		Join("workspace_templates wt ON wt.id = w.workspace_template_id"). | ||||||
| 		OrderBy("w.created_at DESC"). |  | ||||||
| 		Where(sq.And{ | 		Where(sq.And{ | ||||||
| 			sq.Eq{ | 			sq.Eq{ | ||||||
| 				"w.namespace": namespace, | 				"w.namespace": namespace, | ||||||
| @@ -577,7 +605,28 @@ func (c *Client) ListWorkspaces(namespace string, paginator *pagination.Paginati | |||||||
| 				"phase": WorkspaceTerminated, | 				"phase": WorkspaceTerminated, | ||||||
| 			}, | 			}, | ||||||
| 		}) | 		}) | ||||||
| 	sb = *paginator.ApplyToSelect(&sb) |  | ||||||
|  | 	if request.HasSorting() { | ||||||
|  | 		properties := getWorkspaceColumnsMap(true) | ||||||
|  | 		for _, order := range request.Sort.Properties { | ||||||
|  | 			if columnName, ok := properties[order.Property]; ok { | ||||||
|  | 				nullSort := "NULLS FIRST" | ||||||
|  | 				if order.Direction == "desc" { | ||||||
|  | 					nullSort = "NULLS LAST" // default in postgres, but let's be explicit | ||||||
|  | 				} | ||||||
|  | 				sb = sb.OrderBy(fmt.Sprintf("w.%v %v %v", columnName, order.Direction, nullSort)) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		sb = sb.OrderBy("w.created_at DESC") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sb, err = applyWorkspaceFilter(sb, request) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sb = *request.Pagination.ApplyToSelect(&sb) | ||||||
|  |  | ||||||
| 	if err := c.DB.Selectx(&workspaces, sb); err != nil { | 	if err := c.DB.Selectx(&workspaces, sb); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @@ -587,8 +636,8 @@ func (c *Client) ListWorkspaces(namespace string, paginator *pagination.Paginati | |||||||
| } | } | ||||||
|  |  | ||||||
| // CountWorkspaces returns the total number of workspaces in the given namespace that are not terminated | // CountWorkspaces returns the total number of workspaces in the given namespace that are not terminated | ||||||
| func (c *Client) CountWorkspaces(namespace string) (count int, err error) { | func (c *Client) CountWorkspaces(namespace string, request *request.Request) (count int, err error) { | ||||||
| 	err = sb.Select("COUNT( DISTINCT( w.id ))"). | 	query := sb.Select("COUNT( DISTINCT( w.id ))"). | ||||||
| 		From("workspaces w"). | 		From("workspaces w"). | ||||||
| 		Join("workspace_templates wt ON w.workspace_template_id = wt.id"). | 		Join("workspace_templates wt ON w.workspace_template_id = wt.id"). | ||||||
| 		Where(sq.And{ | 		Where(sq.And{ | ||||||
| @@ -598,8 +647,14 @@ func (c *Client) CountWorkspaces(namespace string) (count int, err error) { | |||||||
| 			sq.NotEq{ | 			sq.NotEq{ | ||||||
| 				"phase": WorkspaceTerminated, | 				"phase": WorkspaceTerminated, | ||||||
| 			}, | 			}, | ||||||
| 		}). | 		}) | ||||||
| 		RunWith(c.DB). |  | ||||||
|  | 	query, err = applyWorkspaceFilter(query, request) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = query.RunWith(c.DB). | ||||||
| 		QueryRow(). | 		QueryRow(). | ||||||
| 		Scan(&count) | 		Scan(&count) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -122,3 +122,27 @@ func WorkspacesToIDs(resources []*Workspace) (ids []uint64) { | |||||||
|  |  | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // getWorkspaceColumnsMap returns a map where the keys are the columns of the workspaces table | ||||||
|  | // the value is the raw column name as it is in the database | ||||||
|  | func getWorkspaceColumnsMap(camelCase bool) map[string]string { | ||||||
|  | 	result := map[string]string{ | ||||||
|  | 		"id":         "id", | ||||||
|  | 		"labels":     "labels", | ||||||
|  | 		"name":       "name", | ||||||
|  | 		"uid":        "uid", | ||||||
|  | 		"namespace": "namespace", | ||||||
|  | 		"phase":      "phase", | ||||||
|  | 		"parameters": "parameters", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if camelCase { | ||||||
|  | 		result["createdAt"] = "created_at" | ||||||
|  | 		result["modifiedAt"] = "modified_at" | ||||||
|  | 	} else { | ||||||
|  | 		result["created_at"] = "created_at" | ||||||
|  | 		result["modified_at"] = "modified_at" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return result | ||||||
|  | } | ||||||
|   | |||||||
| @@ -7,7 +7,9 @@ import ( | |||||||
| 	v1 "github.com/onepanelio/core/pkg" | 	v1 "github.com/onepanelio/core/pkg" | ||||||
| 	"github.com/onepanelio/core/pkg/util" | 	"github.com/onepanelio/core/pkg/util" | ||||||
| 	"github.com/onepanelio/core/pkg/util/ptr" | 	"github.com/onepanelio/core/pkg/util/ptr" | ||||||
|  | 	"github.com/onepanelio/core/pkg/util/request" | ||||||
| 	"github.com/onepanelio/core/pkg/util/request/pagination" | 	"github.com/onepanelio/core/pkg/util/request/pagination" | ||||||
|  | 	requestSort "github.com/onepanelio/core/pkg/util/request/sort" | ||||||
| 	"github.com/onepanelio/core/server/auth" | 	"github.com/onepanelio/core/server/auth" | ||||||
| 	"github.com/onepanelio/core/server/converter" | 	"github.com/onepanelio/core/server/converter" | ||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
| @@ -213,8 +215,24 @@ func (s *WorkspaceServer) ListWorkspaces(ctx context.Context, req *api.ListWorks | |||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	paginator := pagination.NewRequest(req.Page, req.PageSize) | 	labelFilter, err := v1.LabelsFromString(req.Labels) | ||||||
| 	workspaces, err := client.ListWorkspaces(req.Namespace, &paginator) | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	reqSort, err := requestSort.New(req.Order) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resourceRequest := &request.Request{ | ||||||
|  | 		Pagination: pagination.New(req.Page, req.PageSize), | ||||||
|  | 		Filter: v1.WorkspaceFilter{ | ||||||
|  | 			Labels: labelFilter, | ||||||
|  | 		}, | ||||||
|  | 		Sort: reqSort, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	workspaces, err := client.ListWorkspaces(req.Namespace, resourceRequest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| @@ -228,11 +246,12 @@ func (s *WorkspaceServer) ListWorkspaces(ctx context.Context, req *api.ListWorks | |||||||
| 		apiWorkspaces = append(apiWorkspaces, apiWorkspace(w, sysConfig)) | 		apiWorkspaces = append(apiWorkspaces, apiWorkspace(w, sysConfig)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	count, err := client.CountWorkspaces(req.Namespace) | 	count, err := client.CountWorkspaces(req.Namespace, resourceRequest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	paginator := resourceRequest.Pagination | ||||||
| 	return &api.ListWorkspaceResponse{ | 	return &api.ListWorkspaceResponse{ | ||||||
| 		Count:      int32(len(apiWorkspaces)), | 		Count:      int32(len(apiWorkspaces)), | ||||||
| 		Workspaces: apiWorkspaces, | 		Workspaces: apiWorkspaces, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Andrey Melnikov
					Andrey Melnikov