Initial implementation of inventree
A small IT asset inventory system built around a nested folder tree, matching the project brief: Go + html/template + Bootstrap 5 + vanilla JS, SQLite storage (WAL, FKs, migrations), embedded templates/static assets, single compiled binary. - AJAX-driven inventory tree: lazy-loaded folder expansion with per-user persisted expand state, drag-and-drop move for folders/assets, safe delete-with-relocate-or-cascade confirmation, search with breadcrumbs. - Assets: CRUD, duplicate, move, auto-generated IT-000001-style IDs, CSV import (with optional folder auto-create) and export. - Admin-defined item types with custom fields (text/long_text/number/date/ boolean/dropdown/url/ip_address). - Configurable statuses, session-based auth (bcrypt, secure cookies), admin/user roles, user management. - CLI: serve, version, database migrate, database backup; /healthz; graceful shutdown; Dockerfile/docker-compose/systemd unit; Go test suite.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"inventree/internal/assets"
|
||||
"inventree/internal/models"
|
||||
)
|
||||
|
||||
// treeAssetLimit bounds how many assets are rendered directly inside a
|
||||
// single tree node before showing a "N more" placeholder, so a folder with
|
||||
// thousands of assets doesn't blow up the tree fragment.
|
||||
const treeAssetLimit = 300
|
||||
|
||||
// expandedFolderIDs returns the set of folder IDs the given user has
|
||||
// persisted as expanded.
|
||||
func (a *App) expandedFolderIDs(ctx context.Context, userID int64) (map[int64]bool, error) {
|
||||
rows, err := a.DB.QueryContext(ctx, `SELECT folder_id FROM user_tree_state WHERE user_id = ?`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := map[int64]bool{}
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[id] = true
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (a *App) persistExpand(ctx context.Context, userID, folderID int64) error {
|
||||
_, err := a.DB.ExecContext(ctx,
|
||||
`INSERT INTO user_tree_state (user_id, folder_id) VALUES (?, ?) ON CONFLICT(user_id, folder_id) DO NOTHING`,
|
||||
userID, folderID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) persistCollapse(ctx context.Context, userID, folderID int64) error {
|
||||
_, err := a.DB.ExecContext(ctx, `DELETE FROM user_tree_state WHERE user_id = ? AND folder_id = ?`, userID, folderID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) persistCollapseAll(ctx context.Context, userID int64) error {
|
||||
_, err := a.DB.ExecContext(ctx, `DELETE FROM user_tree_state WHERE user_id = ?`, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// buildTree returns the ordered list of tree nodes (subfolders then assets)
|
||||
// directly under parentID. Folders present in the expanded set are
|
||||
// recursively expanded inline; others are returned collapsed for the client
|
||||
// to lazy-load on demand.
|
||||
func (a *App) buildTree(ctx context.Context, parentID *int64, expanded map[int64]bool) ([]models.TreeNode, error) {
|
||||
childFolders, err := a.Folders.Children(ctx, parentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var out []models.TreeNode
|
||||
for _, f := range childFolders {
|
||||
node := models.TreeNode{
|
||||
Type: "folder",
|
||||
ID: f.ID,
|
||||
Name: f.Name,
|
||||
Icon: f.Icon,
|
||||
ParentID: f.ParentID,
|
||||
HasChildren: f.FolderCount > 0 || f.AssetCount > 0,
|
||||
}
|
||||
if expanded[f.ID] {
|
||||
node.Expanded = true
|
||||
children, err := a.buildTree(ctx, &f.ID, expanded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node.Children = children
|
||||
}
|
||||
out = append(out, node)
|
||||
}
|
||||
|
||||
assetList, total, err := a.Assets.List(ctx, assets.ListFilter{
|
||||
FolderID: parentID, FolderSet: true, Page: 1, PageSize: treeAssetLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, as := range assetList {
|
||||
out = append(out, models.TreeNode{
|
||||
Type: "asset",
|
||||
ID: as.ID,
|
||||
Name: as.Name,
|
||||
Icon: as.ItemTypeIcon,
|
||||
AssetTag: as.AssetTag,
|
||||
StatusName: as.StatusName,
|
||||
StatusColor: as.StatusColor,
|
||||
})
|
||||
}
|
||||
if total > len(assetList) {
|
||||
out = append(out, models.TreeNode{
|
||||
Type: "more",
|
||||
Name: fmt.Sprintf("%d more asset(s) — open this folder to view all", total-len(assetList)),
|
||||
})
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func fieldValuesForCreate(fields []models.CustomFieldDef) []models.AssetFieldValue {
|
||||
out := make([]models.AssetFieldValue, len(fields))
|
||||
for i, f := range fields {
|
||||
out[i] = models.AssetFieldValue{Def: f, Value: f.DefaultValue}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user