Add error display to file upload

This commit is contained in:
Eli Ribble 2026-04-15 19:02:25 +00:00
parent 344f4bcaa5
commit 66d35428fa
No known key found for this signature in database
7 changed files with 93 additions and 28 deletions

View file

@ -114,6 +114,15 @@ var FileuploadFiles = Table[
Generated: false, Generated: false,
AutoIncr: false, AutoIncr: false,
}, },
Error: column{
Name: "error",
DBType: "text",
Default: "",
Comment: "",
Nullable: false,
Generated: false,
AutoIncr: false,
},
}, },
Indexes: fileuploadFileIndexes{ Indexes: fileuploadFileIndexes{
FilePkey: index{ FilePkey: index{
@ -184,11 +193,12 @@ type fileuploadFileColumns struct {
SizeBytes column SizeBytes column
FileUUID column FileUUID column
Committer column Committer column
Error column
} }
func (c fileuploadFileColumns) AsSlice() []column { func (c fileuploadFileColumns) AsSlice() []column {
return []column{ return []column{
c.ID, c.ContentType, c.Created, c.CreatorID, c.Deleted, c.Name, c.OrganizationID, c.Status, c.SizeBytes, c.FileUUID, c.Committer, c.ID, c.ContentType, c.Created, c.CreatorID, c.Deleted, c.Name, c.OrganizationID, c.Status, c.SizeBytes, c.FileUUID, c.Committer, c.Error,
} }
} }

View file

@ -0,0 +1,6 @@
-- +goose Up
ALTER TABLE fileupload.file ADD COLUMN error TEXT;
UPDATE fileupload.file SET error = '';
ALTER TABLE fileupload.file ALTER COLUMN error SET NOT NULL;
-- +goose Down
ALTER TABLE fileupload.file DROP COLUMN error;

View file

@ -38,6 +38,7 @@ type FileuploadFile struct {
SizeBytes int32 `db:"size_bytes" ` SizeBytes int32 `db:"size_bytes" `
FileUUID uuid.UUID `db:"file_uuid" ` FileUUID uuid.UUID `db:"file_uuid" `
Committer null.Val[int32] `db:"committer" ` Committer null.Val[int32] `db:"committer" `
Error string `db:"error" `
R fileuploadFileR `db:"-" ` R fileuploadFileR `db:"-" `
} }
@ -65,7 +66,7 @@ type fileuploadFileR struct {
func buildFileuploadFileColumns(alias string) fileuploadFileColumns { func buildFileuploadFileColumns(alias string) fileuploadFileColumns {
return fileuploadFileColumns{ return fileuploadFileColumns{
ColumnsExpr: expr.NewColumnsExpr( ColumnsExpr: expr.NewColumnsExpr(
"id", "content_type", "created", "creator_id", "deleted", "name", "organization_id", "status", "size_bytes", "file_uuid", "committer", "id", "content_type", "created", "creator_id", "deleted", "name", "organization_id", "status", "size_bytes", "file_uuid", "committer", "error",
).WithParent("fileupload.file"), ).WithParent("fileupload.file"),
tableAlias: alias, tableAlias: alias,
ID: psql.Quote(alias, "id"), ID: psql.Quote(alias, "id"),
@ -79,6 +80,7 @@ func buildFileuploadFileColumns(alias string) fileuploadFileColumns {
SizeBytes: psql.Quote(alias, "size_bytes"), SizeBytes: psql.Quote(alias, "size_bytes"),
FileUUID: psql.Quote(alias, "file_uuid"), FileUUID: psql.Quote(alias, "file_uuid"),
Committer: psql.Quote(alias, "committer"), Committer: psql.Quote(alias, "committer"),
Error: psql.Quote(alias, "error"),
} }
} }
@ -96,6 +98,7 @@ type fileuploadFileColumns struct {
SizeBytes psql.Expression SizeBytes psql.Expression
FileUUID psql.Expression FileUUID psql.Expression
Committer psql.Expression Committer psql.Expression
Error psql.Expression
} }
func (c fileuploadFileColumns) Alias() string { func (c fileuploadFileColumns) Alias() string {
@ -121,10 +124,11 @@ type FileuploadFileSetter struct {
SizeBytes omit.Val[int32] `db:"size_bytes" ` SizeBytes omit.Val[int32] `db:"size_bytes" `
FileUUID omit.Val[uuid.UUID] `db:"file_uuid" ` FileUUID omit.Val[uuid.UUID] `db:"file_uuid" `
Committer omitnull.Val[int32] `db:"committer" ` Committer omitnull.Val[int32] `db:"committer" `
Error omit.Val[string] `db:"error" `
} }
func (s FileuploadFileSetter) SetColumns() []string { func (s FileuploadFileSetter) SetColumns() []string {
vals := make([]string, 0, 11) vals := make([]string, 0, 12)
if s.ID.IsValue() { if s.ID.IsValue() {
vals = append(vals, "id") vals = append(vals, "id")
} }
@ -158,6 +162,9 @@ func (s FileuploadFileSetter) SetColumns() []string {
if !s.Committer.IsUnset() { if !s.Committer.IsUnset() {
vals = append(vals, "committer") vals = append(vals, "committer")
} }
if s.Error.IsValue() {
vals = append(vals, "error")
}
return vals return vals
} }
@ -195,6 +202,9 @@ func (s FileuploadFileSetter) Overwrite(t *FileuploadFile) {
if !s.Committer.IsUnset() { if !s.Committer.IsUnset() {
t.Committer = s.Committer.MustGetNull() t.Committer = s.Committer.MustGetNull()
} }
if s.Error.IsValue() {
t.Error = s.Error.MustGet()
}
} }
func (s *FileuploadFileSetter) Apply(q *dialect.InsertQuery) { func (s *FileuploadFileSetter) Apply(q *dialect.InsertQuery) {
@ -203,7 +213,7 @@ func (s *FileuploadFileSetter) Apply(q *dialect.InsertQuery) {
}) })
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) {
vals := make([]bob.Expression, 11) vals := make([]bob.Expression, 12)
if s.ID.IsValue() { if s.ID.IsValue() {
vals[0] = psql.Arg(s.ID.MustGet()) vals[0] = psql.Arg(s.ID.MustGet())
} else { } else {
@ -270,6 +280,12 @@ func (s *FileuploadFileSetter) Apply(q *dialect.InsertQuery) {
vals[10] = psql.Raw("DEFAULT") vals[10] = psql.Raw("DEFAULT")
} }
if s.Error.IsValue() {
vals[11] = psql.Arg(s.Error.MustGet())
} else {
vals[11] = psql.Raw("DEFAULT")
}
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
})) }))
} }
@ -279,7 +295,7 @@ func (s FileuploadFileSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
} }
func (s FileuploadFileSetter) Expressions(prefix ...string) []bob.Expression { func (s FileuploadFileSetter) Expressions(prefix ...string) []bob.Expression {
exprs := make([]bob.Expression, 0, 11) exprs := make([]bob.Expression, 0, 12)
if s.ID.IsValue() { if s.ID.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
@ -358,6 +374,13 @@ func (s FileuploadFileSetter) Expressions(prefix ...string) []bob.Expression {
}}) }})
} }
if s.Error.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
psql.Quote(append(prefix, "error")...),
psql.Arg(s.Error),
}})
}
return exprs return exprs
} }
@ -1074,6 +1097,7 @@ type fileuploadFileWhere[Q psql.Filterable] struct {
SizeBytes psql.WhereMod[Q, int32] SizeBytes psql.WhereMod[Q, int32]
FileUUID psql.WhereMod[Q, uuid.UUID] FileUUID psql.WhereMod[Q, uuid.UUID]
Committer psql.WhereNullMod[Q, int32] Committer psql.WhereNullMod[Q, int32]
Error psql.WhereMod[Q, string]
} }
func (fileuploadFileWhere[Q]) AliasedAs(alias string) fileuploadFileWhere[Q] { func (fileuploadFileWhere[Q]) AliasedAs(alias string) fileuploadFileWhere[Q] {
@ -1093,6 +1117,7 @@ func buildFileuploadFileWhere[Q psql.Filterable](cols fileuploadFileColumns) fil
SizeBytes: psql.Where[Q, int32](cols.SizeBytes), SizeBytes: psql.Where[Q, int32](cols.SizeBytes),
FileUUID: psql.Where[Q, uuid.UUID](cols.FileUUID), FileUUID: psql.Where[Q, uuid.UUID](cols.FileUUID),
Committer: psql.WhereNull[Q, int32](cols.Committer), Committer: psql.WhereNull[Q, int32](cols.Committer),
Error: psql.Where[Q, string](cols.Error),
} }
} }

View file

@ -192,9 +192,11 @@ func JobImport(ctx context.Context, txn bob.Executor, file_id int32) error {
err = importCSV(ctx, file_id, parseCSVFlyover, processCSVFlyover) err = importCSV(ctx, file_id, parseCSVFlyover, processCSVFlyover)
} }
if err != nil { if err != nil {
log.Debug().Err(err).Msg("failed to import CSV")
_, err := psql.Update( _, err := psql.Update(
um.Table("fileupload.file"), um.Table("fileupload.file"),
um.SetCol("status").ToArg("error"), um.SetCol("status").ToArg("error"),
um.SetCol("error").ToArg(err.Error()),
um.Where(psql.Quote("id").EQ(psql.Arg(file_id))), um.Where(psql.Quote("id").EQ(psql.Arg(file_id))),
).Exec(ctx, db.PGInstance.BobDB) ).Exec(ctx, db.PGInstance.BobDB)
if err != nil { if err != nil {

View file

@ -36,6 +36,7 @@ const (
type Upload struct { type Upload struct {
Created time.Time `db:"created" json:"created"` Created time.Time `db:"created" json:"created"`
Error string `db:"error" json:"error"`
Filename string `db:"filename" json:"filename"` Filename string `db:"filename" json:"filename"`
ID int32 `db:"id" json:"id"` ID int32 `db:"id" json:"id"`
RecordCount int `db:"recordcount" json:"recordcount"` RecordCount int `db:"recordcount" json:"recordcount"`
@ -92,6 +93,7 @@ func NewUpload(ctx context.Context, u User, upload file.Upload, t enums.Fileuplo
Created: omit.From(time.Now()), Created: omit.From(time.Now()),
CreatorID: omit.From(int32(u.ID)), CreatorID: omit.From(int32(u.ID)),
Deleted: omitnull.FromPtr[time.Time](nil), Deleted: omitnull.FromPtr[time.Time](nil),
Error: omit.From(""),
Name: omit.From(upload.Name), Name: omit.From(upload.Name),
OrganizationID: omit.From(u.Organization.ID), OrganizationID: omit.From(u.Organization.ID),
Status: omit.From(enums.FileuploadFilestatustypeUploaded), Status: omit.From(enums.FileuploadFilestatustypeUploaded),
@ -167,6 +169,7 @@ func UploadList(ctx context.Context, org Organization) ([]Upload, error) {
"file.created AS created", "file.created AS created",
//"file.creator_id", //"file.creator_id",
//"file.deleted", //"file.deleted",
"file.error AS error",
"file.id AS id", "file.id AS id",
"file.name AS filename", "file.name AS filename",
//"file.organization_id", //"file.organization_id",
@ -235,9 +238,9 @@ func getUploadDetailPool(ctx context.Context, file *models.FileuploadFile) (*Upl
Tags: tags, Tags: tags,
}) })
} }
log.Debug().Str("status", file.Status.String()).Int32("id", file.ID).Msg("returning")
return &Upload{ return &Upload{
Created: file.Created, Created: file.Created,
Error: file.Error,
Filename: file.Name, Filename: file.Name,
ID: file.ID, ID: file.ID,
RecordCount: len(pool_rows), RecordCount: len(pool_rows),

View file

@ -559,6 +559,17 @@ export interface ReviewTaskListResponse {
} }
export interface UploadDTO { export interface UploadDTO {
created: string; created: string;
error: string;
filename: string;
id: number;
recordcount: number;
status: string;
type: string;
csv_pool?: CSVPoolDetail;
}
export interface UploadOptions {
created: Date;
error: string;
filename: string; filename: string;
id: number; id: number;
recordcount: number; recordcount: number;
@ -567,25 +578,29 @@ export interface UploadDTO {
csv_pool?: CSVPoolDetail; csv_pool?: CSVPoolDetail;
} }
export class Upload { export class Upload {
constructor( created: Date;
public created: Date, error: string;
public filename: string, filename: string;
public id: number, id: number;
public recordcount: number, recordcount: number;
public status: string, status: string;
public type: string, type: string;
public csv_pool?: CSVPoolDetail, csv_pool?: CSVPoolDetail;
) {} constructor(options: UploadOptions) {
this.created = options.created;
this.error = options.error;
this.filename = options.filename;
this.id = options.id;
this.recordcount = options.recordcount;
this.status = options.status;
this.type = options.type;
this.csv_pool = options.csv_pool;
}
static fromJSON(json: UploadDTO): Upload { static fromJSON(json: UploadDTO): Upload {
return new Upload( return new Upload({
new Date(json.created), ...json,
json.filename, created: new Date(json.created),
json.id, });
json.recordcount,
json.status,
json.type,
json.csv_pool,
);
} }
} }

View file

@ -150,12 +150,11 @@ tr.has-error {
<p>loading</p> <p>loading</p>
</div> </div>
<div v-else> <div v-else>
<MapMultipoint <MapLocator
:bounds="session.organization!.service_area"
:markers="[]" :markers="[]"
:organizationId="session.organization!.id" :organizationId="session.organization!.id"
:tegola="session.urls?.tegola ?? ''" :tegola="session.urls?.tegola ?? ''"
></MapMultipoint> />
</div> </div>
</div> </div>
@ -202,8 +201,13 @@ tr.has-error {
</div> </div>
<template v-else> <template v-else>
<div v-if="upload.error" class="alert alert-error" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Error:</strong> Your upload failed to parse correctly. The
specific error was: '{{ upload.error }}'
</div>
<div <div
v-if=" v-else-if="
!upload.csv_pool?.pools || upload.csv_pool.pools.length === 0 !upload.csv_pool?.pools || upload.csv_pool.pools.length === 0
" "
class="alert alert-warning" class="alert alert-warning"