3
0

Rework system/application sorting via weight

This commit is contained in:
Tomaž Jerman 2021-02-26 11:19:55 +01:00
parent 77c754b81b
commit 1e44e4299b
18 changed files with 401 additions and 10 deletions

View File

@ -62,6 +62,7 @@ func (c *application) MarshalYAML() (interface{}, error) {
nn, err := makeMap(
"name", c.res.Name,
"enabled", c.res.Enabled,
"weight", c.res.Weight,
"unify", c.res.Unify,

View File

@ -33,6 +33,9 @@ type (
// ApplicationMetrics (custom function)
ApplicationMetrics(ctx context.Context) (*types.ApplicationMetrics, error)
// ReorderApplications (custom function)
ReorderApplications(ctx context.Context, _order []uint64) error
}
)
@ -84,3 +87,7 @@ func TruncateApplications(ctx context.Context, s Applications) error {
func ApplicationMetrics(ctx context.Context, s Applications) (*types.ApplicationMetrics, error) {
return s.ApplicationMetrics(ctx)
}
func ReorderApplications(ctx context.Context, s Applications, _order []uint64) error {
return s.ReorderApplications(ctx, _order)
}

View File

@ -6,6 +6,7 @@ fields:
- { field: Name, sortable: true }
- { field: OwnerID }
- { field: Enabled }
- { field: Weight, sortable: true }
- { field: Unify }
- { field: CreatedAt, sortable: true }
- { field: UpdatedAt, sortable: true }
@ -22,6 +23,12 @@ functions:
- name: ApplicationMetrics
return: [ "*types.ApplicationMetrics", "error" ]
- name: ReorderApplications
arguments:
- { name: order, type: "[]uint64" }
return: [ "error" ]
rdbms:
alias: app
table: applications

View File

@ -435,6 +435,7 @@ func (s Store) internalApplicationRowScanner(row rowScanner) (res *types.Applica
&res.Name,
&res.OwnerID,
&res.Enabled,
&res.Weight,
&res.Unify,
&res.CreatedAt,
&res.UpdatedAt,
@ -482,6 +483,7 @@ func (Store) applicationColumns(aa ...string) []string {
alias + "name",
alias + "rel_owner",
alias + "enabled",
alias + "weight",
alias + "unify",
alias + "created_at",
alias + "updated_at",
@ -496,7 +498,7 @@ func (Store) applicationColumns(aa ...string) []string {
// With optional string arg, all columns are returned aliased
func (Store) sortableApplicationColumns() map[string]string {
return map[string]string{
"id": "id", "name": "name", "created_at": "created_at",
"id": "id", "name": "name", "weight": "weight", "created_at": "created_at",
"createdat": "created_at",
"updated_at": "updated_at",
"updatedat": "updated_at",
@ -515,6 +517,7 @@ func (s Store) internalApplicationEncoder(res *types.Application) store.Payload
"name": res.Name,
"rel_owner": res.OwnerID,
"enabled": res.Enabled,
"weight": res.Weight,
"unify": res.Unify,
"created_at": res.CreatedAt,
"updated_at": res.UpdatedAt,
@ -551,6 +554,9 @@ func (s Store) collectApplicationCursorValues(res *types.Application, cc ...*fil
case "name":
cursor.Set(c.Column, res.Name, c.Descending)
case "weight":
cursor.Set(c.Column, res.Weight, c.Descending)
case "created_at":
cursor.Set(c.Column, res.CreatedAt, c.Descending)

View File

@ -2,8 +2,10 @@ package rdbms
import (
"context"
"github.com/Masterminds/squirrel"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/types"
)
@ -56,3 +58,56 @@ func (s Store) ApplicationMetrics(ctx context.Context) (*types.ApplicationMetric
return rval, nil
}
func (s Store) ReorderApplications(ctx context.Context, order []uint64) (err error) {
var (
apps types.ApplicationSet
appMap = map[uint64]bool{}
weight = 1
f = types.ApplicationFilter{}
)
if apps, _, err = s.SearchApplications(ctx, f); err != nil {
return
}
for _, app := range apps {
appMap[app.ID] = true
}
// honor parameter first
for _, pageID := range order {
if appMap[pageID] {
appMap[pageID] = false
err = s.execUpdateApplications(ctx,
squirrel.Eq{"app.id": pageID},
store.Payload{"weight": weight})
if err != nil {
return
}
weight++
}
}
for pageID, update := range appMap {
if !update {
continue
}
err = s.execUpdateApplications(ctx,
squirrel.Eq{"app.id": pageID},
store.Payload{"weight": weight})
if err != nil {
return
}
weight++
}
return
}

View File

@ -3,6 +3,7 @@ package rdbms
import (
"context"
"fmt"
"github.com/cortezaproject/corteza-server/store/rdbms/ddl"
"go.uber.org/zap"
)
@ -59,6 +60,10 @@ func (g genericUpgrades) Upgrade(ctx context.Context, t *ddl.Table) error {
return g.all(ctx,
g.AlterActionlogAddID,
)
case "applications":
return g.all(ctx,
g.AddWeightField,
)
case "users":
return g.all(ctx,
g.AlterUsersDropOrganisation,
@ -243,6 +248,17 @@ func (g genericUpgrades) AlterActionlogAddID(ctx context.Context) (err error) {
return nil
}
func (g genericUpgrades) AddWeightField(ctx context.Context) error {
_, err := g.u.AddColumn(ctx, "applications", &ddl.Column{
Name: "weight",
Type: ddl.ColumnType{Type: ddl.ColumnTypeInteger},
IsNull: false,
DefaultValue: "0",
})
return err
}
func (g genericUpgrades) RenameReminders(ctx context.Context) error {
return g.RenameTable(ctx, "sys_reminder", "reminders")
}

View File

@ -221,6 +221,7 @@ func (Schema) Applications() *Table {
ID,
ColumnDef("name", ColumnTypeText),
ColumnDef("enabled", ColumnTypeBoolean, DefaultValue("true")),
ColumnDef("weight", ColumnTypeInteger, DefaultValue("0")),
ColumnDef("unify", ColumnTypeJson),
ColumnDef("rel_owner", ColumnTypeIdentifier),
CUDTimestamps,

View File

@ -744,6 +744,10 @@ endpoints:
type: bool
required: false
title: Enabled
- name: weight
type: int
required: false
title: Weight for sorting
- name: unify
type: sqlxTypes.JSONText
required: false
@ -775,6 +779,10 @@ endpoints:
type: bool
required: false
title: Enabled
- name: weight
type: int
required: false
title: Weight for sorting
- name: unify
type: sqlxTypes.JSONText
required: false
@ -832,6 +840,17 @@ endpoints:
type: string
title: Script to execute
required: true
- name: reorder
method: POST
title: Reorder applications
path: "/reorder"
parameters:
post:
- name: applicationIDs
type: "[]string"
required: true
title: Application order
- title: Permissions
parameters: {}
entrypoint: permissions

View File

@ -2,6 +2,8 @@ package rest
import (
"context"
"strconv"
"github.com/cortezaproject/corteza-server/pkg/api"
"github.com/cortezaproject/corteza-server/pkg/corredor"
"github.com/cortezaproject/corteza-server/pkg/filter"
@ -27,6 +29,7 @@ type (
Update(ctx context.Context, upd *types.Application) (app *types.Application, err error)
Delete(ctx context.Context, ID uint64) (err error)
Undelete(ctx context.Context, ID uint64) (err error)
Reorder(ctx context.Context, order []uint64) (err error)
}
applicationAccessController interface {
@ -87,6 +90,7 @@ func (ctrl *Application) Create(ctx context.Context, r *request.ApplicationCreat
app = &types.Application{
Name: r.Name,
Enabled: r.Enabled,
Weight: r.Weight,
Labels: r.Labels,
}
)
@ -109,6 +113,7 @@ func (ctrl *Application) Update(ctx context.Context, r *request.ApplicationUpdat
ID: r.ApplicationID,
Name: r.Name,
Enabled: r.Enabled,
Weight: r.Weight,
Labels: r.Labels,
}
)
@ -151,6 +156,21 @@ func (ctrl *Application) TriggerScript(ctx context.Context, r *request.Applicati
return application, err
}
func (ctrl *Application) Reorder(ctx context.Context, r *request.ApplicationReorder) (interface{}, error) {
order := make([]uint64, len(r.ApplicationIDs))
for i, aid := range r.ApplicationIDs {
parsed, err := strconv.ParseUint(aid, 10, 64)
if err != nil {
return nil, err
}
order[i] = parsed
}
return api.OK(), ctrl.application.Reorder(ctx, order)
}
func (ctrl Application) makePayload(ctx context.Context, m *types.Application, err error) (*applicationPayload, error) {
if err != nil || m == nil {
return nil, err

View File

@ -26,6 +26,7 @@ type (
Delete(context.Context, *request.ApplicationDelete) (interface{}, error)
Undelete(context.Context, *request.ApplicationUndelete) (interface{}, error)
TriggerScript(context.Context, *request.ApplicationTriggerScript) (interface{}, error)
Reorder(context.Context, *request.ApplicationReorder) (interface{}, error)
}
// HTTP API interface
@ -37,6 +38,7 @@ type (
Delete func(http.ResponseWriter, *http.Request)
Undelete func(http.ResponseWriter, *http.Request)
TriggerScript func(http.ResponseWriter, *http.Request)
Reorder func(http.ResponseWriter, *http.Request)
}
)
@ -152,6 +154,22 @@ func NewApplication(h ApplicationAPI) *Application {
return
}
api.Send(w, r, value)
},
Reorder: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewApplicationReorder()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Reorder(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
@ -167,5 +185,6 @@ func (h Application) MountRoutes(r chi.Router, middlewares ...func(http.Handler)
r.Delete("/application/{applicationID}", h.Delete)
r.Post("/application/{applicationID}/undelete", h.Undelete)
r.Post("/application/{applicationID}/trigger", h.TriggerScript)
r.Post("/application/reorder", h.Reorder)
})
}

View File

@ -79,6 +79,11 @@ type (
// Enabled
Enabled bool
// Weight POST parameter
//
// Weight for sorting
Weight int
// Unify POST parameter
//
// Unify properties
@ -111,6 +116,11 @@ type (
// Enabled
Enabled bool
// Weight POST parameter
//
// Weight for sorting
Weight int
// Unify POST parameter
//
// Unify properties
@ -159,6 +169,13 @@ type (
// Script to execute
Script string
}
ApplicationReorder struct {
// ApplicationIDs POST parameter
//
// Application order
ApplicationIDs []string
}
)
// NewApplicationList request
@ -293,6 +310,7 @@ func (r ApplicationCreate) Auditable() map[string]interface{} {
return map[string]interface{}{
"name": r.Name,
"enabled": r.Enabled,
"weight": r.Weight,
"unify": r.Unify,
"config": r.Config,
"labels": r.Labels,
@ -309,6 +327,11 @@ func (r ApplicationCreate) GetEnabled() bool {
return r.Enabled
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationCreate) GetWeight() int {
return r.Weight
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationCreate) GetUnify() sqlxTypes.JSONText {
return r.Unify
@ -358,6 +381,13 @@ func (r *ApplicationCreate) Fill(req *http.Request) (err error) {
}
}
if val, ok := req.Form["weight"]; ok && len(val) > 0 {
r.Weight, err = payload.ParseInt(val[0]), nil
if err != nil {
return err
}
}
if val, ok := req.Form["unify"]; ok && len(val) > 0 {
r.Unify, err = payload.ParseJSONTextWithErr(val[0])
if err != nil {
@ -399,6 +429,7 @@ func (r ApplicationUpdate) Auditable() map[string]interface{} {
"applicationID": r.ApplicationID,
"name": r.Name,
"enabled": r.Enabled,
"weight": r.Weight,
"unify": r.Unify,
"config": r.Config,
"labels": r.Labels,
@ -420,6 +451,11 @@ func (r ApplicationUpdate) GetEnabled() bool {
return r.Enabled
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationUpdate) GetWeight() int {
return r.Weight
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationUpdate) GetUnify() sqlxTypes.JSONText {
return r.Unify
@ -469,6 +505,13 @@ func (r *ApplicationUpdate) Fill(req *http.Request) (err error) {
}
}
if val, ok := req.Form["weight"]; ok && len(val) > 0 {
r.Weight, err = payload.ParseInt(val[0]), nil
if err != nil {
return err
}
}
if val, ok := req.Form["unify"]; ok && len(val) > 0 {
r.Unify, err = payload.ParseJSONTextWithErr(val[0])
if err != nil {
@ -711,3 +754,51 @@ func (r *ApplicationTriggerScript) Fill(req *http.Request) (err error) {
return err
}
// NewApplicationReorder request
func NewApplicationReorder() *ApplicationReorder {
return &ApplicationReorder{}
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationReorder) Auditable() map[string]interface{} {
return map[string]interface{}{
"applicationIDs": r.ApplicationIDs,
}
}
// Auditable returns all auditable/loggable parameters
func (r ApplicationReorder) GetApplicationIDs() []string {
return r.ApplicationIDs
}
// Fill processes request and fills internal variables
func (r *ApplicationReorder) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
//if val, ok := req.Form["applicationIDs[]"]; ok && len(val) > 0 {
// r.ApplicationIDs, err = val, nil
// if err != nil {
// return err
// }
//}
}
return err
}

View File

@ -2,6 +2,7 @@ package service
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/label"
@ -182,6 +183,7 @@ func (svc *application) Update(ctx context.Context, upd *types.Application) (app
// Assign changed values after afterUpdate events are emitted
app.Name = upd.Name
app.Enabled = upd.Enabled
app.Weight = upd.Weight
app.UpdatedAt = now()
if upd.Unify != nil {
@ -282,6 +284,31 @@ func (svc *application) Undelete(ctx context.Context, ID uint64) (err error) {
return svc.recordAction(ctx, aaProps, ApplicationActionUndelete, err)
}
func (svc *application) Reorder(ctx context.Context, order []uint64) (err error) {
var (
aProps = &applicationActionProps{}
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error {
for _, id := range order {
// This access control creates an aux application so we don't have to fetch them
// from the store; the ID is the only thing that matters...
auxApp := &types.Application{
ID: id,
}
if !svc.ac.CanUpdateApplication(ctx, auxApp) {
aProps.application = auxApp
return ApplicationErrNotAllowedToUpdate(aProps)
}
}
return store.ReorderApplications(ctx, s, order)
})
return svc.recordAction(ctx, aProps, ApplicationActionReorder, err)
}
// toLabeledApplications converts to []label.LabeledResource
//
// This function is auto-generated.

View File

@ -265,6 +265,26 @@ func ApplicationActionSearch(props ...*applicationActionProps) *applicationActio
return a
}
// ApplicationActionReorder returns "system:application.reorder" action
//
// This function is auto-generated.
//
func ApplicationActionReorder(props ...*applicationActionProps) *applicationAction {
a := &applicationAction{
timestamp: time.Now(),
resource: "system:application",
action: "reorder",
log: "reordered applications",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// ApplicationActionLookup returns "system:application.lookup" action
//
// This function is auto-generated.

View File

@ -31,6 +31,9 @@ actions:
log: "searched for applications"
severity: info
- action: reorder
log: "reordered applications"
- action: lookup
log: "looked-up for a {application}"
severity: info

View File

@ -3,9 +3,10 @@ package types
import (
"database/sql/driver"
"encoding/json"
"github.com/cortezaproject/corteza-server/pkg/filter"
"time"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/rbac"
@ -17,6 +18,7 @@ type (
Name string `json:"name"`
OwnerID uint64 `json:"ownerID"`
Enabled bool `json:"enabled"`
Weight int `json:"weight"`
Unify *ApplicationUnify `json:"unify,omitempty"`
@ -34,7 +36,6 @@ type (
Logo string `json:"logo"`
Url string `json:"url"`
Config string `json:"config"`
Order uint `json:"order"`
}
ApplicationFilter struct {

View File

@ -514,7 +514,6 @@ func TestStoreYaml_base(t *testing.T) {
req.Equal("logo", app.Unify.Logo)
req.Equal("url", app.Unify.Url)
req.Equal("{\"config\": \"config\"}", app.Unify.Config)
req.Equal(uint(0), app.Unify.Order)
req.Equal(createdAt.Format(time.RFC3339), app.CreatedAt.Format(time.RFC3339))
req.Equal(updatedAt.Format(time.RFC3339), app.UpdatedAt.Format(time.RFC3339))
},

View File

@ -66,7 +66,6 @@ func sTestApplication(ctx context.Context, t *testing.T, s store.Storer, usrID u
Logo: "logo",
Url: "url",
Config: "{\"config\": \"config\"}",
Order: 0,
},
CreatedAt: createdAt,
UpdatedAt: &updatedAt,

View File

@ -3,17 +3,18 @@ package system
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"time"
"github.com/cortezaproject/corteza-server/pkg/id"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/service"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/cortezaproject/corteza-server/tests/helpers"
"github.com/steinfletcher/apitest-jsonpath"
jsonpath "github.com/steinfletcher/apitest-jsonpath"
"github.com/stretchr/testify/require"
"net/http"
"net/url"
"testing"
"time"
)
func (h helper) clearApplications() {
@ -44,6 +45,17 @@ func (h helper) lookupApplicationByID(ID uint64) *types.Application {
return res
}
func (h helper) lookupApplicationByName(name string) *types.Application {
res, _, err := store.SearchApplications(context.Background(), service.DefaultStore, types.ApplicationFilter{
Name: name,
})
h.noError(err)
if len(res) == 0 {
return nil
}
return res[0]
}
func TestApplicationRead(t *testing.T) {
h := newHelper(t)
@ -122,6 +134,25 @@ func TestApplicationCreate(t *testing.T) {
End()
}
func TestApplicationCreate_weight(t *testing.T) {
h := newHelper(t)
h.allow(types.SystemRBACResource, "application.create")
name := "name_weight_create_" + rs()
h.apiInit().
Post("/application/").
FormData("name", name).
FormData("weight", "10").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res := h.lookupApplicationByName(name)
h.a.NotNil(res)
h.a.Equal(10, res.Weight)
}
func TestApplicationUpdateForbidden(t *testing.T) {
h := newHelper(t)
u := h.repoMakeApplication()
@ -159,6 +190,75 @@ func TestApplicationUpdate(t *testing.T) {
h.a.Equal(newName, res.Name)
}
func TestApplicationUpdate_weight(t *testing.T) {
h := newHelper(t)
res := h.repoMakeApplication()
h.allow(types.ApplicationRBACResource.AppendWildcard(), "update")
newName := "updated-" + rs()
newHandle := "updated-" + rs()
h.apiInit().
Put(fmt.Sprintf("/application/%d", res.ID)).
Header("Accept", "application/json").
FormData("name", newName).
FormData("handle", newHandle).
FormData("weight", "20").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupApplicationByID(res.ID)
h.a.NotNil(res)
h.a.Equal(20, res.Weight)
}
func TestApplicationReorder_forbiden(t *testing.T) {
h := newHelper(t)
h.allow(types.ApplicationRBACResource.AppendWildcard(), "update")
a := h.repoMakeApplication()
b := h.repoMakeApplication()
c := h.repoMakeApplication()
h.deny(types.ApplicationRBACResource.AppendID(b.ID), "update")
h.apiInit().
Post("/application/reorder").
Header("Accept", "application/json").
JSON(fmt.Sprintf(`{ "applicationIDs": ["%d", "%d", "%d"] }`, b.ID, a.ID, c.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("not allowed to update this application")).
End()
}
func TestApplicationReorder(t *testing.T) {
h := newHelper(t)
h.allow(types.ApplicationRBACResource.AppendWildcard(), "update")
a := h.repoMakeApplication()
b := h.repoMakeApplication()
c := h.repoMakeApplication()
h.apiInit().
Post("/application/reorder").
Header("Accept", "application/json").
JSON(fmt.Sprintf(`{ "applicationIDs": ["%d", "%d", "%d"] }`, b.ID, a.ID, c.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
check := func(app uint64, w int) {
res := h.lookupApplicationByID(app)
h.a.NotNil(res)
h.a.Equal(w, res.Weight)
}
check(b.ID, 1)
check(a.ID, 2)
check(c.ID, 3)
}
func TestApplicationDeleteForbidden(t *testing.T) {
h := newHelper(t)
u := h.repoMakeApplication()