QOR Admin

repository·master·Indexed 21 days ago

https://github.com/qor/admin

A framework for creating configurable, cross-platform Admin Interfaces and RESTful JSON APIs for managing data, primarily designed for GORM-backed models in Go. It provides features for resource management, custom actions, versioned records with composite primary keys, and flexible template resolution.

Tokens
23.9K
Snippets
109
Records
125
Agent score
75%

What's inside qor-admin

  1. Understand QOR Admin ViewPath resolution

    master

    QOR Admin resolves template files using a specific priority order to support both modern go mod environments and legacy GOPATH setups. The priority is:

    1. vendor/ directory (if present).
    2. $GOPATH/pkg/mod/github.com/qor/admin@v0.x/views (version is automatically detected via go.mod).
    3. $GOPATH/src/github.com/qor/admin/views.

    Note: If you want to ensure templates are loaded from pkg/mod, ensure that $GOPATH/src/github.com/qor/admin is absent from your system.

  2. Quick Start with QOR Admin

    master

    To set up a basic QOR Admin interface, initialize a new admin.Admin instance with a GORM database connection, add your models using AddResource, and mount the admin interface to an HTTP multiplexer using MountTo.

    package main
    
    import (
      "fmt"
      "net/http"
      "github.com/jinzhu/gorm"
      _ "github.com/mattn/go-sqlite3"
      "github.com/qor/admin"
    )
    
    // Create a GORM-backend model
    type User struct {
      gorm.Model
      Name string
    }
    
    // Create another GORM-backend model
    type Product struct {
      gorm.Model
      Name        string
      Description string
    }
    
    func main() {
      DB, _ := gorm.Open("sqlite3", "demo.db")
      DB.AutoMigrate(&User{}, &Product{})
    
      // Initialize
      Admin := admin.New(&admin.AdminConfig{DB: DB})
    
      // Allow to use Admin to manage User, Product
      Admin.AddResource(&User{})
      Admin.AddResource(&Product{})
    
      // initialize an HTTP request multiplexer
      mux := http.NewServeMux()
    
      // Mount admin interface to mux
      Admin.MountTo("/admin", mux)
    
      fmt.Println("Listening on: 9000")
      http.ListenAndServe(":9000", mux)
    }
  3. How to use remoteSelector with publish2.version for Has One relationships

    master

    For a has one relationship (e.g., Factory has one Manager), the 'one' side (Manager) must include resource.CompositePrimaryKeyField. The parent struct must also explicitly include the versioned ID fields (e.g., ManagerID and ManagerVersionName).

    Configure the remote selector's ID meta field with a Valuer using resource.GenCompositePrimaryKey. Apply it to the parent resource using admin.SelectOneConfig.

    // 1. Define models
    type Factory struct {
    	gorm.Model
    	Name string
    	publish2.Version
    
    	ManagerID          uint
    	ManagerVersionName string // Required. in "xxxVersionName" format.
    	Manager            Manager
    }
    
    type Manager struct {
    	gorm.Model
    	Name string
    	publish2.Version
    
    	// github.com/qor/qor/resource
    	resource.CompositePrimaryKeyField // Required
    }
    
    // 2. Define remote selector
    func generateRemoteManagerSelector(adm *admin.Admin) (res *admin.Resource) {
    	res = adm.AddResource(&Manager{}, &admin.Config{Name: "ManagerSelector"})
    	res.IndexAttrs("ID", "Name")
    
    	res.Meta(&admin.Meta{
    	Name: "ID",
    	Valuer: func(value interface{}, ctx *qor.Context) interface{} {
    		if r, ok := value.(*Manager); ok {
    			return resource.GenCompositePrimaryKey(r.ID, r.GetVersionName())
    		}
    		return ""
    	},
    	})
    
    	return res
    }
    
    // 3. Use in Factory resource
    managerSelector := generateRemoteManagerSelector(adm)
    factoryRes.Meta(&admin.Meta{
    	Name: "Manager",
    	Config: &admin.SelectOneConfig{
    		RemoteDataResource: managerSelector,
    	},
    })
  4. How to use remoteSelector with publish2.version for Has Many relationships

    master

    When using publish2.version with a has many relationship (e.g., Factory has many Items), you must ensure the 'many' side (Item) includes the resource.CompositePrimaryKeyField.

    To implement a remote selector that supports composite primary keys, you must configure an ID meta field using a Valuer that calls resource.GenCompositePrimaryKey(r.ID, r.GetVersionName()). Finally, apply this to the parent resource using admin.SelectManyConfig.

    // 1. Define models with CompositePrimaryKeyField on the 'many' side
    type Factory struct {
    	gorm.Model
    	Name string
    
    	publish2.Version
    	Items       []Item `gorm:"many2many:factory_items;association_autoupdate:false"`
    	ItemsSorter sorting.SortableCollection
    }
    
    type Item struct {
    	gorm.Model
    	Name string
    	publish2.Version
    
    	// github.com/qor/qor/resource
    	resource.CompositePrimaryKeyField // Required
    }
    
    // 2. Define a remote resource selector with a custom ID Valuer
    func generateRemoteItemSelector(adm *admin.Admin) (res *admin.Resource) {
    	res = adm.AddResource(&Item{}, &admin.Config{Name: "ItemSelector"})
    	res.IndexAttrs("ID", "Name")
    
    	res.Meta(&admin.Meta{
    	Name: "ID",
    	Valuer: func(value interface{}, ctx *qor.Context) interface{} {
    		if r, ok := value.(*Item); ok {
    			return resource.GenCompositePrimaryKey(r.ID, r.GetVersionName())
    		}
    		return ""
    	},
    	})
    
    	return res
    }
    
    // 3. Use it in the Factory resource
    itemSelector := generateRemoteItemSelector(adm)
    factoryRes.Meta(&admin.Meta{
    	Name: "Items",
    	Config: &admin.SelectManyConfig{
    		RemoteDataResource: itemSelector,
    	},
    })
  5. Assign associations when creating a new version

    master

    To automatically assign associations when creating a new version of an object, define an AssignVersionName method on your versioned struct. This method must have a pointer receiver, contain the logic for generating the new version name, and assign that name to the object.

    func (fac *Factory) AssignVersionName(db *gorm.DB) {
    	var count int
    	name := time.Now().Format("2006-01-02")
    	if err := db.Model(&CollectionWithVersion{}).Where("id = ? AND version_name like ?", fac.ID, name+"%").Count(&count).Error; err != nil {
        panic(err)
      }
    	fac.VersionName = fmt.Sprintf("%s-v%v", name, count+1)
    }
  6. Implement permission-based menu visibility

    master

    Menus can be hidden or shown based on user permissions using either a Permission object or a custom Permissioner.

    • Role-based: Assign a *roles.Permission to the Permission field. The menu will be visible only if the user's roles satisfy the permission.
    • Custom logic: Assign an object implementing HasPermission to the Permissioner field. This allows for dynamic checks (e.g., checking if a user owns a specific resource).

    Use HasPermission(mode roles.PermissionMode, context *qor.Context) to check if the current user context has access to the menu.

    // Example using a custom Permissioner
    type MyPermissioner struct{}
    
    func (p *MyPermissioner) HasPermission(mode roles.PermissionMode, context *qor.Context) bool {
        // Custom logic here
        return true
    }
    
    admin.AddMenu(&admin.Menu{
        Name:         "Secret Menu",
        Permissioner: &MyPermissioner{},
    })
  7. Automatic Type Inference for Fields

    master

    QOR Admin automatically infers the Type of a field based on the Go struct type and GORM relationship metadata.

    Relationship Mapping

    • has_one $\rightarrow$ single_edit
    • has_many $\rightarrow$ collection_edit
    • belongs_to $\rightarrow$ select_one
    • many_to_many $\rightarrow$ select_many

    Primitive Type Mapping

    • string (with SIZE > 255 or TYPE=text tag) $\rightarrow$ text
    • string $\rightarrow$ string
    • bool $\rightarrow$ checkbox
    • int/uint $\rightarrow$ number
    • float $\rightarrow$ float
    • time.Time $\rightarrow$ datetime
    • struct $\rightarrow$ single_edit
    • slice of structs $\rightarrow$ collection_edit
  8. Define field rendering and processing with Meta

    master

    The Meta struct is the core configuration object used to define how a specific field within a resource is rendered, processed, and handled in the admin interface. It wraps resource.Meta and provides additional controls for labeling, typing, and custom logic.

    Key fields include:

    • Name: The logical name of the field.
    • FieldName: The actual field name on the struct.
    • Label: The human-readable label (defaults to humanized Name).
    • Type: The UI component type (e.g., string, number, datetime, single_edit, collection_edit).
    • Setter: A function to save the value to the record.
    • Valuer: A function to retrieve the value from the record.
    • FormattedValuer: A function to retrieve a formatted version of the value for display.
    • Permission: Role-based access control for this specific field.
    • Config: An implementation of MetaConfigInterface for advanced configuration.
    // Example of conceptual usage (not a direct API call)
    meta := &admin.Meta{
        Name:     "Email",
        FieldName: "Email",
        Type:     "string",
        Setter: func(record interface{}, metaValue *resource.MetaValue, context *qor.Context) {
            // Custom logic to save the email
        },
    }
  9. Control action visibility and permissions

    master

    You can restrict who sees an action or who can execute it using two mechanisms:

    1. Visibility (Visible field)

    Use the Visible function in the Action struct to hide an action based on the state of a specific record or the current context.

    • If Visible returns false for any selected record in a bulk action, the action may be hidden.
    • If no record is provided (general context), it evaluates based on the context alone.

    2. Permissions (Permission field)

    Assign a *roles.Permission to an action. The system will then check if the current user's roles satisfy the permission requirements using HasPermission.

    If no specific Permission is set on the action, the system falls back to checking permissions on the associated Resource.

  10. Pagination in Searcher

    master

    The Searcher manages pagination state via the Pagination struct. When FindMany is called, the searcher automatically parses pagination parameters from the request (like page, per_page, or limit) if they are present in the form data. If not explicitly set via the API, it falls back to the request parameters, then to the resource's Config.PageCount, and finally to the global PaginationPageCount (defaulting to 20).

    Pagination fields:

    • Total: Total number of records matching the query.
    • Pages: Total number of pages available.
    • CurrentPage: The current active page.
    • PerPage: Number of records displayed per page.
  11. Use RemoteDataResource for asynchronous selection

    master

    When dealing with large datasets, instead of providing a static Collection, set the SelectMode to "select_async" or "bottom_sheet" and provide a RemoteDataResource.

    QOR will automatically handle the asynchronous fetching of data. If RemoteDataResource is not explicitly provided but a field type is detected, QOR will attempt to resolve the resource based on the field's underlying struct type.

    If the selected object contains a CompositePrimaryKey field (using resource.CompositePrimaryKeyFieldName), QOR will use a generated composite key for the selection value.

  12. Define a Resource in QOR Admin

    master

    In QOR Admin, every model is defined as a Resource. The Resource is the core abstraction used to generate the management interface. You initialize a resource using NewResource (or by registering it with an Admin instance). The Resource contains configuration like Config, parent-child relationships, and UI definitions for different pages (Index, New, Edit, Show).

    // Assuming 'admin' is an initialized *admin.Admin instance
    // and 'User' is your GORM model
    userResource := admin.NewResource(&User{}, &admin.Config{
        Name: "User",
        IconName: "user",
    })