Files
upstream-corteza/server/compose/service/page_layout.go
upstream-mirror 7e385fc126
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:22:23 +05:00

636 lines
17 KiB
Go

package service
import (
"context"
"reflect"
"github.com/cortezaproject/corteza/server/compose/service/event"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/pkg/handle"
"github.com/cortezaproject/corteza/server/pkg/label"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/store"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
type (
pageLayout struct {
actionlog actionlog.Recorder
ac pageLayoutAccessController
eventbus eventDispatcher
store store.Storer
locale ResourceTranslationsManagerService
pageLayoutSettings *pageLayoutSettings
}
pageLayoutSettings struct {
hideNew bool
hideEdit bool
hideSubmit bool
hideDelete bool
hideClone bool
hideBack bool
}
pageLayoutAccessController interface {
CanManageResourceTranslations(context.Context) bool
CanCreatePageLayoutOnPage(context.Context, *types.Page) bool
CanSearchPageLayoutsOnPage(context.Context, *types.Page) bool
CanReadPageLayout(context.Context, *types.PageLayout) bool
CanUpdatePageLayout(context.Context, *types.PageLayout) bool
CanDeletePageLayout(context.Context, *types.PageLayout) bool
}
pageLayoutUpdateHandler func(ctx context.Context, ns *types.Namespace, pg *types.Page, c *types.PageLayout) (pageLayoutChanges, error)
pageLayoutChanges uint8
)
const (
pageLayoutUnchanged pageLayoutChanges = 0
pageLayoutChanged pageLayoutChanges = 1
pageLayoutLabelsChanged pageLayoutChanges = 2
)
func PageLayout() *pageLayout {
return &pageLayout{
actionlog: DefaultActionlog,
ac: DefaultAccessControl,
eventbus: eventbus.Service(),
store: DefaultStore,
locale: DefaultResourceTranslation,
pageLayoutSettings: &pageLayoutSettings{},
}
}
func (svc pageLayout) FindByID(ctx context.Context, namespaceID, pageLayoutID uint64) (p *types.PageLayout, err error) {
return svc.lookup(ctx, namespaceID, func(aProps *pageLayoutActionProps) (*types.PageLayout, error) {
if pageLayoutID == 0 {
return nil, PageLayoutErrInvalidID()
}
aProps.pageLayout.ID = pageLayoutID
return store.LookupComposePageLayoutByID(ctx, svc.store, pageLayoutID)
})
}
func (svc pageLayout) FindByHandle(ctx context.Context, namespaceID uint64, h string) (c *types.PageLayout, err error) {
return svc.lookup(ctx, namespaceID, func(aProps *pageLayoutActionProps) (*types.PageLayout, error) {
if !handle.IsValid(h) {
return nil, PageLayoutErrInvalidHandle()
}
aProps.pageLayout.Handle = h
return store.LookupComposePageLayoutByNamespaceIDHandle(ctx, svc.store, namespaceID, h)
})
}
func (svc pageLayout) FindByPageLayoutID(ctx context.Context, namespaceID, pageLayoutID uint64) (p *types.PageLayout, err error) {
return svc.lookup(ctx, namespaceID, func(aProps *pageLayoutActionProps) (*types.PageLayout, error) {
if pageLayoutID == 0 {
return nil, PageLayoutErrInvalidID()
}
aProps.pageLayout.ID = pageLayoutID
return store.LookupComposePageLayoutByID(ctx, svc.store, pageLayoutID)
})
}
func checkPageLayout(ctx context.Context, ac pageLayoutAccessController) func(res *types.PageLayout) (bool, error) {
return func(res *types.PageLayout) (bool, error) {
if !ac.CanReadPageLayout(ctx, res) {
return false, nil
}
return true, nil
}
}
// search fn() orchestrates pageLayouts search, namespace preload and check
func (svc pageLayout) search(ctx context.Context, filter types.PageLayoutFilter) (set types.PageLayoutSet, f types.PageLayoutFilter, err error) {
var (
aProps = &pageLayoutActionProps{filter: &filter}
ns *types.Namespace
pg *types.Page
)
// For each fetched item, store backend will check if it is valid or not
filter.Check = checkPageLayout(ctx, svc.ac)
err = func() error {
if filter.PageID > 0 {
ns, pg, err = loadPageCombo(ctx, svc.store, filter.NamespaceID, filter.PageID)
if err != nil {
return err
}
if !svc.ac.CanSearchPageLayoutsOnPage(ctx, pg) {
return PageLayoutErrNotAllowedToSearch()
}
} else {
ns, err = loadNamespace(ctx, svc.store, filter.NamespaceID)
if err != nil {
return err
}
}
aProps.setNamespace(ns)
if len(filter.Labels) > 0 {
filter.LabeledIDs, err = label.Search(
ctx,
svc.store,
types.PageLayout{}.LabelResourceKind(),
filter.Labels,
)
if err != nil {
return err
}
// labels specified but no labeled resources found
if len(filter.LabeledIDs) == 0 {
return nil
}
}
if set, f, err = store.SearchComposePageLayouts(ctx, svc.store, filter); err != nil {
return err
}
if err = label.Load(ctx, svc.store, toLabeledPageLayouts(set)...); err != nil {
return err
}
// i18n
tag := locale.GetAcceptLanguageFromContext(ctx)
set.Walk(func(p *types.PageLayout) error {
p.DecodeTranslations(svc.locale.Locale().ResourceTranslations(tag, p.ResourceTranslation()))
return nil
})
return nil
}()
return set, f, svc.recordAction(ctx, aProps, PageLayoutActionSearch, err)
}
func (svc pageLayout) Find(ctx context.Context, filter types.PageLayoutFilter) (set types.PageLayoutSet, f types.PageLayoutFilter, err error) {
return svc.search(ctx, filter)
}
func (svc pageLayout) Create(ctx context.Context, new *types.PageLayout) (*types.PageLayout, error) {
var (
aProps = &pageLayoutActionProps{pageLayout: new}
ns *types.Namespace
pg *types.Page
)
new.ID = 0
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if !handle.IsValid(new.Handle) {
return PageLayoutErrInvalidID()
}
if ns, pg, err = loadPageCombo(ctx, s, new.NamespaceID, new.PageID); err != nil {
return err
}
// Allow users to manage their personal layouts regardless of RBAC (when enabled)
if !svc.ac.CanCreatePageLayoutOnPage(ctx, pg) {
if new.OwnedBy == 0 || !pg.Meta.AllowPersonalLayouts {
return PageLayoutErrNotAllowedToCreate()
}
}
aProps.setNamespace(ns)
if err = svc.eventbus.WaitFor(ctx, event.PageLayoutBeforeCreate(new, nil, ns, nil)); err != nil {
return err
}
if err = svc.uniqueCheck(ctx, new); err != nil {
return err
}
new.ID = nextID()
new.CreatedAt = *now()
new.UpdatedAt = nil
new.DeletedAt = nil
// Ensure pageLayout-block IDs
for i := range new.Blocks {
new.Blocks[i].BlockID = uint64(i) + 1
}
aProps.setChanged(new)
if err = store.CreateComposePageLayout(ctx, s, new); err != nil {
return err
}
if err = updateTranslations(ctx, svc.ac, svc.locale, new.EncodeTranslations()...); err != nil {
return
}
if err = label.Create(ctx, s, new); err != nil {
return
}
_ = svc.eventbus.WaitFor(ctx, event.PageLayoutAfterCreate(new, nil, ns, nil))
return err
})
return new, svc.recordAction(ctx, aProps, PageLayoutActionCreate, err)
}
func (svc pageLayout) Reorder(ctx context.Context, namespaceID, pageID uint64, pageLayoutIDs []uint64) (err error) {
var (
aProps = &pageLayoutActionProps{pageLayout: &types.PageLayout{ID: pageID}}
p *types.Page
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error {
// Get the page
if p, err = store.LookupComposePageByID(ctx, svc.store, pageID); errors.IsNotFound(err) {
return PageLayoutErrNotFound()
} else if err != nil {
return err
}
_ = p
// @note following the pattern of pages; should probably change this
// on both resources since it doesn't sound right.
if !svc.ac.CanCreatePageLayoutOnPage(ctx, p) {
return PageLayoutErrNotAllowedToUpdate()
}
return store.ReorderComposePageLayouts(ctx, s, namespaceID, pageID, pageLayoutIDs)
})
return svc.recordAction(ctx, aProps, PageLayoutActionReorder, err)
}
func (svc pageLayout) Update(ctx context.Context, upd *types.PageLayout) (c *types.PageLayout, err error) {
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
ns, pg, res, err := loadPageLayoutCombo(ctx, s, upd.NamespaceID, upd.PageID, upd.ID)
if err != nil {
return
}
c, err = svc.updater(ctx, svc.store, ns, pg, res, PageLayoutActionUpdate, svc.handleUpdate(ctx, upd))
return
})
return
}
func (svc pageLayout) DeleteByID(ctx context.Context, namespaceID, pageID, pageLayoutID uint64) error {
var (
ns *types.Namespace
pg *types.Page
res *types.PageLayout
)
return store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
// simply delete the pageLayout and ignore the subpageLayouts
ns, pg, res, err = loadPageLayoutCombo(ctx, s, namespaceID, pageID, pageLayoutID)
if err != nil {
return
}
_, err = svc.updater(ctx, svc.store, ns, pg, res, PageLayoutActionDelete, svc.handleDelete)
return
})
}
func (svc pageLayout) UndeleteByID(ctx context.Context, namespaceID, pageID, pageLayoutID uint64) error {
return store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
ns, pg, res, err := loadPageLayoutCombo(ctx, s, namespaceID, pageID, pageLayoutID)
if err != nil {
return
}
_, err = svc.updater(ctx, svc.store, ns, pg, res, PageLayoutActionUpdate, svc.handleUndelete)
return
})
}
func (svc pageLayout) updater(ctx context.Context, s store.Storer, ns *types.Namespace, pg *types.Page, res *types.PageLayout, action func(...*pageLayoutActionProps) *pageLayoutAction, fn pageLayoutUpdateHandler) (*types.PageLayout, error) {
var (
changes pageLayoutChanges
old *types.PageLayout
aProps = &pageLayoutActionProps{pageLayout: res}
err error
)
err = store.Tx(ctx, s, func(ctx context.Context, s store.Storer) (err error) {
if err = label.Load(ctx, svc.store, res); err != nil {
return err
}
old = res.Clone()
aProps.setNamespace(ns)
aProps.setChanged(res)
if res.DeletedAt == nil {
err = svc.eventbus.WaitFor(ctx, event.PageLayoutBeforeUpdate(old, res, ns, nil))
} else {
err = svc.eventbus.WaitFor(ctx, event.PageLayoutBeforeDelete(old, res, ns, nil))
}
if err != nil {
return
}
if changes, err = fn(ctx, ns, pg, res); err != nil {
return err
}
if changes&pageLayoutChanged > 0 {
if err = store.UpdateComposePageLayout(ctx, s, res); err != nil {
return err
}
}
if err = updateTranslations(ctx, svc.ac, svc.locale, res.EncodeTranslations()...); err != nil {
return
}
if changes&pageLayoutLabelsChanged > 0 {
if err = label.Update(ctx, s, res); err != nil {
return
}
}
if res.DeletedAt == nil {
err = svc.eventbus.WaitFor(ctx, event.PageLayoutAfterUpdate(res, res, ns, nil))
} else {
err = svc.eventbus.WaitFor(ctx, event.PageLayoutAfterDelete(nil, res, ns, nil))
}
return err
})
return res, svc.recordAction(ctx, aProps, action, err)
}
// lookup fn() orchestrates pageLayout lookup, namespace preload and check
func (svc pageLayout) lookup(ctx context.Context, namespaceID uint64, lookup func(*pageLayoutActionProps) (*types.PageLayout, error)) (p *types.PageLayout, err error) {
var aProps = &pageLayoutActionProps{pageLayout: &types.PageLayout{NamespaceID: namespaceID}}
err = func() error {
if ns, err := loadNamespace(ctx, svc.store, namespaceID); err != nil {
return err
} else {
aProps.setNamespace(ns)
}
if p, err = lookup(aProps); errors.IsNotFound(err) {
return PageLayoutErrNotFound()
} else if err != nil {
return err
}
p.DecodeTranslations(svc.locale.Locale().ResourceTranslations(locale.GetAcceptLanguageFromContext(ctx), p.ResourceTranslation()))
aProps.setPageLayout(p)
if !svc.ac.CanReadPageLayout(ctx, p) {
return PageLayoutErrNotAllowedToRead()
}
if err = label.Load(ctx, svc.store, p); err != nil {
return err
}
return nil
}()
return p, svc.recordAction(ctx, aProps, PageLayoutActionLookup, err)
}
func (svc pageLayout) uniqueCheck(ctx context.Context, p *types.PageLayout) (err error) {
if p.Handle != "" {
if e, _ := store.LookupComposePageLayoutByNamespaceIDPageIDHandle(ctx, svc.store, p.NamespaceID, p.PageID, p.Handle); e != nil && e.ID != p.ID {
return PageLayoutErrHandleNotUnique()
}
}
return nil
}
func (svc pageLayout) handleUpdate(ctx context.Context, upd *types.PageLayout) pageLayoutUpdateHandler {
return func(ctx context.Context, ns *types.Namespace, pg *types.Page, res *types.PageLayout) (changes pageLayoutChanges, err error) {
if isStale(upd.UpdatedAt, res.UpdatedAt, res.CreatedAt) {
return pageLayoutUnchanged, PageLayoutErrStaleData()
}
if upd.Handle != res.Handle && !handle.IsValid(upd.Handle) {
return pageLayoutUnchanged, PageLayoutErrInvalidHandle()
}
if err := svc.uniqueCheck(ctx, upd); err != nil {
return pageLayoutUnchanged, err
}
// Allow users to manage their personal layouts regardless of RBAC (when enabled)
if !svc.ac.CanUpdatePageLayout(ctx, res) {
if res.OwnedBy == 0 || !pg.Meta.AllowPersonalLayouts {
return pageLayoutUnchanged, PageLayoutErrNotAllowedToUpdate()
}
}
// Get max actionID for later use
actionID := uint64(0)
for _, a := range res.Config.Actions {
if a.ActionID > actionID {
actionID = a.ActionID
}
}
if res.ID != upd.ID {
res.ID = upd.ID
changes |= pageLayoutChanged
}
if res.PageID != upd.PageID {
res.PageID = upd.PageID
changes |= pageLayoutChanged
}
if res.ParentID != upd.ParentID {
res.ParentID = upd.ParentID
changes |= pageLayoutChanged
}
if res.NamespaceID != upd.NamespaceID {
res.NamespaceID = upd.NamespaceID
changes |= pageLayoutChanged
}
if !reflect.DeepEqual(res.Config, upd.Config) {
res.Config = upd.Config
changes |= pageLayoutChanged
}
if !reflect.DeepEqual(res.Blocks, upd.Blocks) {
res.Blocks = upd.Blocks
changes |= pageLayoutChanged
}
// Assure blockIDs
for i, a := range res.Config.Actions {
if a.ActionID == 0 {
actionID++
a.ActionID = actionID
res.Config.Actions[i] = a
changes |= pageLayoutChanged
}
}
if res.Meta.Title != upd.Meta.Title {
res.Meta.Title = upd.Meta.Title
changes |= pageLayoutChanged
}
if res.Handle != upd.Handle {
res.Handle = upd.Handle
changes |= pageLayoutChanged
}
if res.Meta.Description != upd.Meta.Description {
res.Meta.Description = upd.Meta.Description
changes |= pageLayoutChanged
}
if !reflect.DeepEqual(res.Meta.Style, upd.Meta.Style) {
res.Meta.Style = upd.Meta.Style
changes |= pageLayoutChanged
}
if res.Primary != upd.Primary {
res.Primary = upd.Primary
changes |= pageLayoutChanged
}
if upd.Labels != nil {
if label.Changed(res.Labels, upd.Labels) {
changes |= pageLayoutLabelsChanged
res.Labels = upd.Labels
}
}
if changes&pageLayoutChanged > 0 {
res.UpdatedAt = now()
}
return
}
}
func (svc pageLayout) handleDelete(ctx context.Context, ns *types.Namespace, pg *types.Page, m *types.PageLayout) (pageLayoutChanges, error) {
// Allow users to manage their personal layouts regardless of RBAC (when enabled)
if !svc.ac.CanDeletePageLayout(ctx, m) {
if m.OwnedBy == 0 || !pg.Meta.AllowPersonalLayouts {
return pageLayoutUnchanged, PageLayoutErrNotAllowedToDelete()
}
}
if m.DeletedAt != nil {
// pageLayout already deleted
return pageLayoutUnchanged, nil
}
m.DeletedAt = now()
return pageLayoutChanged, nil
}
func (svc pageLayout) handleUndelete(ctx context.Context, ns *types.Namespace, pg *types.Page, m *types.PageLayout) (pageLayoutChanges, error) {
// Allow users to manage their personal layouts regardless of RBAC (when enabled)
if !svc.ac.CanDeletePageLayout(ctx, m) {
if m.OwnedBy == 0 || !pg.Meta.AllowPersonalLayouts {
return pageLayoutUnchanged, PageLayoutErrNotAllowedToUndelete()
}
}
if m.DeletedAt == nil {
// pageLayout not deleted
return pageLayoutUnchanged, nil
}
m.DeletedAt = nil
return pageLayoutChanged, nil
}
func (svc *pageLayout) UpdateConfig(ss *systemTypes.AppSettings) {
a := ss.Compose.UI.RecordToolbar
svc.pageLayoutSettings = &pageLayoutSettings{
hideNew: a.HideNew,
hideEdit: a.HideEdit,
hideSubmit: a.HideSubmit,
hideDelete: a.HideDelete,
hideClone: a.HideClone,
hideBack: a.HideBack,
}
}
func loadPageLayoutCombo(ctx context.Context, s interface {
store.ComposePageLayouts
store.ComposeNamespaces
}, namespaceID, pageID, pageLayoutID uint64) (ns *types.Namespace, pg *types.Page, c *types.PageLayout, err error) {
ns, err = loadNamespace(ctx, s, namespaceID)
if err != nil {
return
}
c, err = loadPageLayout(ctx, s, namespaceID, pageID, pageLayoutID)
return
}
func loadPageLayout(ctx context.Context, s store.ComposePageLayouts, namespaceID, pageID, pageLayoutID uint64) (res *types.PageLayout, err error) {
if pageLayoutID == 0 || namespaceID == 0 {
return nil, PageLayoutErrInvalidID()
}
if res, err = store.LookupComposePageLayoutByID(ctx, s, pageLayoutID); errors.IsNotFound(err) {
err = PageLayoutErrNotFound()
}
if err == nil && namespaceID != res.NamespaceID {
// Make sure chart belongs to the right namespace
return nil, PageLayoutErrNotFound()
}
if err == nil && namespaceID != res.NamespaceID {
// Make sure pageLayout belongs to the right namespace
return nil, PageLayoutErrNotFound()
}
return
}
// toLabeledPageLayouts converts to []label.LabeledResource
//
// This function is auto-generated.
func toLabeledPageLayouts(set []*types.PageLayout) []label.LabeledResource {
if len(set) == 0 {
return nil
}
ll := make([]label.LabeledResource, len(set))
for i := range set {
ll[i] = set[i]
}
return ll
}