feat: added support for sorting and filtering workspaces.

* Also added a LabelFilter interface
This commit is contained in:
Andrey Melnikov
2020-09-28 12:09:16 -07:00
parent 1b7e96cab9
commit 8b4a70d958
5 changed files with 141 additions and 28 deletions

28
pkg/filter.go Normal file
View 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
}

View File

@@ -65,24 +65,11 @@ func typeWorkflow(wf *wfv1.Workflow) (workflow *WorkflowExecution) {
// WorkflowExecutionFilter represents the available ways we can filter WorkflowExecutions
type WorkflowExecutionFilter struct {
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,
// if there are any
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 (wf *WorkflowExecutionFilter) GetLabels() []*Label {
return wf.Labels
}
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
}
sb, err := applyWorkflowExecutionLabelSelectQuery(sb, &filter)
sb, err := ApplyLabelSelectQuery("we.labels", sb, &filter)
if err != nil {
return sb, err
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/lib/pq"
"github.com/onepanelio/core/pkg/util"
"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"
"google.golang.org/grpc/codes"
v1 "k8s.io/api/core/v1"
@@ -20,6 +20,35 @@ import (
"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 {
sb := sb.Select(getWorkspaceColumns("w")...).
Columns(getWorkspaceStatusColumns("w", "status")...).
@@ -562,13 +591,12 @@ func (c *Client) ListWorkspacesByTemplateID(namespace string, templateID uint64)
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")...).
Columns(getWorkspaceStatusColumns("w", "status")...).
Columns(getWorkspaceTemplateColumns("wt", "workspace_template")...).
From("workspaces w").
Join("workspace_templates wt ON wt.id = w.workspace_template_id").
OrderBy("w.created_at DESC").
Where(sq.And{
sq.Eq{
"w.namespace": namespace,
@@ -577,7 +605,28 @@ func (c *Client) ListWorkspaces(namespace string, paginator *pagination.Paginati
"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 {
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
func (c *Client) CountWorkspaces(namespace string) (count int, err error) {
err = sb.Select("COUNT( DISTINCT( w.id ))").
func (c *Client) CountWorkspaces(namespace string, request *request.Request) (count int, err error) {
query := sb.Select("COUNT( DISTINCT( w.id ))").
From("workspaces w").
Join("workspace_templates wt ON w.workspace_template_id = wt.id").
Where(sq.And{
@@ -598,8 +647,14 @@ func (c *Client) CountWorkspaces(namespace string) (count int, err error) {
sq.NotEq{
"phase": WorkspaceTerminated,
},
}).
RunWith(c.DB).
})
query, err = applyWorkspaceFilter(query, request)
if err != nil {
return 0, err
}
err = query.RunWith(c.DB).
QueryRow().
Scan(&count)

View File

@@ -122,3 +122,27 @@ func WorkspacesToIDs(resources []*Workspace) (ids []uint64) {
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
}

View File

@@ -7,7 +7,9 @@ import (
v1 "github.com/onepanelio/core/pkg"
"github.com/onepanelio/core/pkg/util"
"github.com/onepanelio/core/pkg/util/ptr"
"github.com/onepanelio/core/pkg/util/request"
"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/converter"
log "github.com/sirupsen/logrus"
@@ -213,8 +215,24 @@ func (s *WorkspaceServer) ListWorkspaces(ctx context.Context, req *api.ListWorks
return nil, err
}
paginator := pagination.NewRequest(req.Page, req.PageSize)
workspaces, err := client.ListWorkspaces(req.Namespace, &paginator)
labelFilter, err := v1.LabelsFromString(req.Labels)
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 {
return nil, err
}
@@ -228,11 +246,12 @@ func (s *WorkspaceServer) ListWorkspaces(ctx context.Context, req *api.ListWorks
apiWorkspaces = append(apiWorkspaces, apiWorkspace(w, sysConfig))
}
count, err := client.CountWorkspaces(req.Namespace)
count, err := client.CountWorkspaces(req.Namespace, resourceRequest)
if err != nil {
return nil, err
}
paginator := resourceRequest.Pagination
return &api.ListWorkspaceResponse{
Count: int32(len(apiWorkspaces)),
Workspaces: apiWorkspaces,