Mercurial > gemma
changeset 2687:3b98de34de90
Merged import-overview-rework back into default.
author | Sascha L. Teichmann <sascha.teichmann@intevation.de> |
---|---|
date | Fri, 15 Mar 2019 15:59:40 +0100 |
parents | c4da269238a4 (current diff) 47b789a27618 (diff) |
children | d316a6e41f54 |
files | |
diffstat | 10 files changed, 258 insertions(+), 142 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/components/importoverview/AdditionalDetail.vue Fri Mar 15 12:43:30 2019 +0100 +++ b/client/src/components/importoverview/AdditionalDetail.vue Fri Mar 15 15:59:40 2019 +0100 @@ -2,13 +2,19 @@ <div> <FairwayDimensionDetail :entry="entry" + :details="details" v-if="isFairwayDimension" ></FairwayDimensionDetail> <ApprovedGaugeMeasurementDetail :entry="entry" + :details="details" v-if="isApprovedGaugeMeasurement" ></ApprovedGaugeMeasurementDetail> - <BottleneckDetail :entry="entry" v-if="isBottleneck"></BottleneckDetail> + <BottleneckDetail + :details="details" + :entry="entry" + v-if="isBottleneck" + ></BottleneckDetail> </div> </template> @@ -29,7 +35,7 @@ export default { name: "additionaldetail", - props: ["entry"], + props: ["entry", "details"], components: { BottleneckDetail: () => import("./BottleneckDetail.vue"), ApprovedGaugeMeasurementDetail: () =>
--- a/client/src/components/importoverview/AdditionalLog.vue Fri Mar 15 12:43:30 2019 +0100 +++ b/client/src/components/importoverview/AdditionalLog.vue Fri Mar 15 15:59:40 2019 +0100 @@ -49,37 +49,13 @@ * Author(s): * Thomas Junk <thomas.junk@intevation.de> */ -import { displayError } from "@/lib/errors.js"; -import { HTTP } from "@/lib/http.js"; - export default { name: "additionallogs", - props: ["entry"], + props: ["details"], data() { return { - logLines: [] + logLines: this.details.entries }; - }, - methods: { - loadEntries() { - HTTP.get("/imports/" + this.entry.id, { - headers: { "X-Gemma-Auth": localStorage.getItem("token") } - }) - .then(response => { - const { entries } = response.data; - this.logLines = entries; - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - } - }, - mounted() { - this.loadEntries(); } }; </script>
--- a/client/src/components/importoverview/ApprovedGaugeMeasurementDetail.vue Fri Mar 15 12:43:30 2019 +0100 +++ b/client/src/components/importoverview/ApprovedGaugeMeasurementDetail.vue Fri Mar 15 15:59:40 2019 +0100 @@ -1,6 +1,6 @@ <template> <div class="diffs"> - <div v-for="(result, index) in entry.summary" :key="index"> + <div v-for="(result, index) in details.summary" :key="index"> <div class="pl-2 d-flex flex-row"> <div @click="toggleDiff(index)" @@ -105,7 +105,7 @@ export default { name: "agmdetails", - props: ["entry"], + props: ["entry", "details"], data() { return { showDiff: NODIFF
--- a/client/src/components/importoverview/BottleneckDetail.vue Fri Mar 15 12:43:30 2019 +0100 +++ b/client/src/components/importoverview/BottleneckDetail.vue Fri Mar 15 15:59:40 2019 +0100 @@ -73,7 +73,7 @@ export default { name: "bottleneckdetails", - props: ["entry"], + props: ["entry", "details"], data() { return { bottlenecks: [], @@ -86,7 +86,7 @@ methods: { loadBottlenecks() { const generateFilter = () => { - const { bottlenecks } = this.entry.summary; + const { bottlenecks } = this.details.summary; if (bottlenecks.length === 1) return equalToFilter("bottleneck_id", bottlenecks[0]); const orExpressions = bottlenecks.map(x => {
--- a/client/src/components/importoverview/LogDetail.vue Fri Mar 15 12:43:30 2019 +0100 +++ b/client/src/components/importoverview/LogDetail.vue Fri Mar 15 15:59:40 2019 +0100 @@ -18,19 +18,24 @@ ></font-awesome-icon> <span class="text-info"><translate>Additional Info</translate></span> <span class="text-info" v-if="isApprovedGaugeMeasurement"> - ({{ entry.summary.length }})</span + ({{ details.summary.length }})</span > <span v-if="isBottleneck" class="text-info text-left"> - ({{ entry.summary.bottlenecks.length }})</span + ({{ details.summary.bottlenecks.length }})</span > <span class="text-left" v-if="isFairwayDimension" - >{{ entry.summary["source-organization"] }} (LOS: - {{ entry.summary.los }})</span + >{{ details.summary["source-organization"] }} (LOS: + {{ details.summary.los }})</span > </div> - <StretchDetail v-if="isStretch" :entry="entry"></StretchDetail> + <StretchDetail + v-if="isStretch" + :entry="entry" + :details="details" + ></StretchDetail> <SoundingResultDetail :entry="entry" + :details="details" v-if="isSoundingResult" ></SoundingResultDetail> </div> @@ -38,6 +43,7 @@ v-if="entry.id === showAdditional" class="ml-2 d-flex flex-row" :entry="entry" + :details="details" ></AdditionalDetail> <div class="d-flex fex-row"> <font-awesome-icon @@ -60,7 +66,7 @@ <AdditionalLog v-if="entry.id === showLogs" class="ml-4 d-flex flex-row mr-1" - :entry="entry" + :details="details" ></AdditionalLog> </div> </template> @@ -84,7 +90,7 @@ export default { name: "logdetail", - props: ["entry"], + props: ["entry", "details"], components: { SoundingResultDetail: () => import("./SoundingResultDetail.vue"), StretchDetail: () => import("./StretchDetails.vue"),
--- a/client/src/components/importoverview/LogEntry.vue Fri Mar 15 12:43:30 2019 +0100 +++ b/client/src/components/importoverview/LogEntry.vue Fri Mar 15 15:59:40 2019 +0100 @@ -58,7 +58,11 @@ </div> </div> <div class="ml-1 d-flex flex-row"> - <LogDetail :entry="entry" v-if="show === entry.id"></LogDetail> + <LogDetail + :entry="entry" + :details="details" + v-if="show === entry.id" + ></LogDetail> </div> </div> </template> @@ -79,10 +83,32 @@ */ import { mapState } from "vuex"; import { STATES } from "@/store/imports.js"; +import { displayError } from "@/lib/errors.js"; +import { HTTP } from "@/lib/http.js"; export default { name: "importlogentry", props: ["entry"], + data() { + return { + details: null + }; + }, + mounted() { + HTTP.get("/imports/" + this.entry.id, { + headers: { "X-Gemma-Auth": localStorage.getItem("token") } + }) + .then(response => { + this.details = response.data; + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }, components: { LogDetail: () => import("./LogDetail.vue") },
--- a/client/src/components/importoverview/SoundingResultDetail.vue Fri Mar 15 12:43:30 2019 +0100 +++ b/client/src/components/importoverview/SoundingResultDetail.vue Fri Mar 15 15:59:40 2019 +0100 @@ -2,7 +2,7 @@ <div> <span class="empty"></span> <a @click="zoomTo()" class="text-info pointer"> - {{ entry.summary.bottleneck }} + {{ details.summary.bottleneck }} </a> </div> </template> @@ -23,7 +23,7 @@ */ export default { name: "soundingresultdetails", - props: ["entry"], + props: ["entry", "details"], methods: { moveMap(coordinates) { this.$store.commit("map/moveMap", { @@ -33,7 +33,7 @@ }); }, zoomTo() { - const { lat, lon, bottleneck, date } = this.entry.summary; + const { lat, lon, bottleneck, date } = this.details.summary; const coordinates = [lat, lon]; this.moveMap(coordinates); this.$store
--- a/client/src/components/importoverview/StretchDetails.vue Fri Mar 15 12:43:30 2019 +0100 +++ b/client/src/components/importoverview/StretchDetails.vue Fri Mar 15 15:59:40 2019 +0100 @@ -2,7 +2,7 @@ <div> <span class="empty"> </span> <a @click="zoomToStretch()" class="text-info pointer">{{ - entry.summary.stretch + details.summary.stretch }}</a> </div> </template> @@ -26,7 +26,7 @@ export default { name: "stretchdetails", - props: ["entry"], + props: ["entry", "details"], methods: { moveToExtent(feature) { this.$store.commit("map/moveToExtent", { @@ -36,7 +36,7 @@ }); }, zoomToStretch() { - const name = this.entry.summary.stretch; + const name = this.details.summary.stretch; this.$store.commit("map/setLayerVisible", LAYERS.STRETCHES); this.$store .dispatch("imports/loadStretch", name)
--- a/pkg/controllers/importqueue.go Fri Mar 15 12:43:30 2019 +0100 +++ b/pkg/controllers/importqueue.go Fri Mar 15 15:59:40 2019 +0100 @@ -14,6 +14,7 @@ package controllers import ( + "context" "database/sql" "encoding/json" "fmt" @@ -21,6 +22,7 @@ "net/http" "strconv" "strings" + "time" "github.com/gorilla/mux" "github.com/jackc/pgx/pgtype" @@ -32,13 +34,17 @@ const ( warningSQLPrefix = ` -` - selectImportsSQL = ` WITH warned AS ( SELECT distinct(import_id) AS id FROM import.import_logs WHERE kind = 'warn'::log_type -) +)` + selectImportsCountSQL = warningSQLPrefix + ` +SELECT count(*) +FROM import.imports +WHERE +` + selectImportsSQL = warningSQLPrefix + ` SELECT imports.id AS id, state::varchar, @@ -46,21 +52,21 @@ kind, username, signer, - summary, - EXISTS ( - SELECT true FROM warned - WHERE warned.id = imports.id - ) AS has_warnings -` - withoutWarningsSQL = ` + summary IS NOT NULL AS has_summary, + imports.id IN (SELECT id FROM warned) AS has_warnings FROM import.imports +WHERE ` - withWarningsSQL = ` -FROM import.imports JOIN warned ON imports.id = warned.id + selectBeforeSQL = warningSQLPrefix + ` +SELECT enqueued FROM import.imports +WHERE ` - - selectHasImportSQL = ` -SELECT true FROM import.imports WHERE id = $1` + selectAfterSQL = warningSQLPrefix + ` +SELECT enqueued FROM import.imports +WHERE +` + selectImportSummaySQL = ` +SELECT summary FROM import.imports WHERE id = $1` selectHasNoRunningImportSQL = ` SELECT true FROM import.imports @@ -126,93 +132,142 @@ return &ta } -func queryImportListStmt( - conn *sql.Conn, - req *http.Request, -) (*sql.Rows, error) { +type filterBuilder struct { + stmt strings.Builder + args []interface{} + hasCond bool +} + +func (fb *filterBuilder) arg(format string, v ...interface{}) { + indices := make([]interface{}, len(v)) + for i := range indices { + indices[i] = len(fb.args) + i + 1 + } + fmt.Fprintf(&fb.stmt, format, indices...) + fb.args = append(fb.args, v...) +} + +func (fb *filterBuilder) cond(format string, v ...interface{}) { + if fb.hasCond { + fb.stmt.WriteString(" AND ") + } else { + fb.hasCond = true + } + fb.arg(format, v...) +} - var ( - stmt strings.Builder - args []interface{} - states *pgtype.TextArray - kinds *pgtype.TextArray - ids *pgtype.Int8Array - ) +func buildFilters(req *http.Request) (l, b, a *filterBuilder, err error) { + + l = new(filterBuilder) + a = new(filterBuilder) + b = new(filterBuilder) + + var noBefore, noAfter bool + + var counting bool - arg := func(format string, v interface{}) { - fmt.Fprintf(&stmt, format, len(args)+1) - args = append(args, v) + switch count := strings.ToLower(req.FormValue("count")); count { + case "1", "t", "true": + counting = true + l.stmt.WriteString(selectImportsCountSQL) + default: + l.stmt.WriteString(selectImportsSQL) + } + a.stmt.WriteString(selectAfterSQL) + b.stmt.WriteString(selectBeforeSQL) + + cond := func(format string, v ...interface{}) { + l.cond(format, v...) + a.cond(format, v...) + b.cond(format, v...) } - var warnings bool - - switch warn := strings.ToLower(req.FormValue("warnings")); warn { - case "1", "t", "true": - warnings = true + if query := req.FormValue("query"); query != "" { + query = "%" + query + "%" + cond(` (kind ILIKE $%d OR username ILIKE $%d OR signer ILIKE $%d OR `+ + `id IN (SELECT import_id FROM import.import_logs WHERE msg ILIKE $%d)) `, + query, query, query, query) } if st := req.FormValue("states"); st != "" { - states = toTextArray(st, imports.ImportStateNames) + states := toTextArray(st, imports.ImportStateNames) + cond(" state = ANY($%d) ", states) } if ks := req.FormValue("kinds"); ks != "" { - kinds = toTextArray(ks, imports.ImportKindNames()) + kinds := toTextArray(ks, imports.ImportKindNames()) + cond(" kind = ANY($%d) ", kinds) } if idss := req.FormValue("ids"); idss != "" { - ids = toInt8Array(idss) + ids := toInt8Array(idss) + cond(" id = ANY($%d) ", ids) } - stmt.WriteString(selectImportsSQL) - if warnings { - stmt.WriteString(withWarningsSQL) + if from := req.FormValue("from"); from != "" { + var fromTime time.Time + if fromTime, err = time.Parse(models.ImportTimeFormat, from); err != nil { + return + } + l.cond(" enqueued >= $%d ", fromTime) + b.cond(" enqueued < $%d", fromTime) } else { - stmt.WriteString(withoutWarningsSQL) + noBefore = true } - if states != nil || kinds != nil || ids != nil { - stmt.WriteString(" WHERE ") + if to := req.FormValue("to"); to != "" { + var toTime time.Time + if toTime, err = time.Parse(models.ImportTimeFormat, to); err != nil { + return + } + l.cond(" enqueued <= $%d ", toTime) + a.cond(" enqueued > $%d", toTime) + } else { + noAfter = true } - if states != nil { - arg(" state = ANY($%d) ", states) - } - - if states != nil && (kinds != nil || ids != nil) { - stmt.WriteString("AND") - } - - if kinds != nil { - arg(" kind = ANY($%d) ", kinds) + switch warn := strings.ToLower(req.FormValue("warnings")); warn { + case "1", "t", "true": + cond(" id IN (SELECT id FROM warned) ") } - if (states != nil || kinds != nil) && ids != nil { - stmt.WriteString("AND") + if !l.hasCond { + l.stmt.WriteString(" TRUE ") + } + if !b.hasCond { + b.stmt.WriteString(" TRUE ") + } + if !a.hasCond { + a.stmt.WriteString(" TRUE ") } - if ids != nil { - arg(" id = ANY($%d) ", ids) + if !counting { + l.stmt.WriteString(" ORDER BY enqueued DESC ") + a.stmt.WriteString(" ORDER BY enqueued LIMIT 1") + b.stmt.WriteString(" ORDER BY enqueued LIMIT 1") } - stmt.WriteString(" ORDER BY enqueued DESC ") + if noBefore { + b = nil + } + if noAfter { + a = nil + } + return +} - if lim := req.FormValue("limit"); lim != "" { - limit, err := strconv.ParseInt(lim, 10, 64) - if err != nil { - return nil, err - } - arg(" LIMIT $%d ", limit) - } +func neighbored(ctx context.Context, conn *sql.Conn, fb *filterBuilder) *models.ImportTime { - if ofs := req.FormValue("offset"); ofs != "" { - offset, err := strconv.ParseInt(ofs, 10, 64) - if err != nil { - return nil, err - } - arg(" OFFSET $%d ", offset) + var when time.Time + err := conn.QueryRowContext(ctx, fb.stmt.String(), fb.args...).Scan(&when) + switch { + case err == sql.ErrNoRows: + return nil + case err != nil: + log.Printf("warn: %v\n", err) + return nil } - - return conn.QueryContext(req.Context(), stmt.String(), args...) + return &models.ImportTime{when} } func listImports( @@ -221,27 +276,53 @@ conn *sql.Conn, ) (jr JSONResult, err error) { + var list, before, after *filterBuilder + + if list, before, after, err = buildFilters(req); err != nil { + return + } + + ctx := req.Context() + + // Fast path for counting + + switch count := strings.ToLower(req.FormValue("count")); count { + case "1", "t", "true": + var count int64 + err = conn.QueryRowContext(ctx, list.stmt.String(), list.args...).Scan(&count) + switch { + case err == sql.ErrNoRows: + count, err = 0, nil + case err != nil: + return + } + jr = JSONResult{Result: count} + return + } + + // Generate the list + var rows *sql.Rows - rows, err = queryImportListStmt(conn, req) - if err != nil { + if rows, err = conn.QueryContext(ctx, list.stmt.String(), list.args...); err != nil { return } defer rows.Close() imports := make([]*models.Import, 0, 20) - var signer, summary sql.NullString + var signer sql.NullString for rows.Next() { var it models.Import + var enqueued time.Time if err = rows.Scan( &it.ID, &it.State, - &it.Enqueued, + &enqueued, &it.Kind, &it.User, &signer, - &summary, + &it.Summary, &it.Warnings, ); err != nil { return @@ -249,12 +330,7 @@ if signer.Valid { it.Signer = signer.String } - if summary.Valid { - if err = json.NewDecoder( - strings.NewReader(summary.String)).Decode(&it.Summary); err != nil { - return - } - } + it.Enqueued = models.ImportTime{enqueued} imports = append(imports, &it) } @@ -262,11 +338,25 @@ return } + var prev, next *models.ImportTime + + if before != nil { + prev = neighbored(ctx, conn, before) + } + + if after != nil { + next = neighbored(ctx, conn, after) + } + jr = JSONResult{ Result: struct { - Imports []*models.Import `json:"imports"` + Prev *models.ImportTime `json:"prev,omitempty"` + Next *models.ImportTime `json:"next,omitempty"` + Imports []*models.Import `json:"imports"` }{ Imports: imports, + Prev: prev, + Next: next, }, } return @@ -283,8 +373,8 @@ id, _ := strconv.ParseInt(mux.Vars(req)["id"], 10, 64) // Check if he have such a import job first. - var dummy bool - err = conn.QueryRowContext(ctx, selectHasImportSQL, id).Scan(&dummy) + var summary sql.NullString + err = conn.QueryRowContext(ctx, selectImportSummaySQL, id).Scan(&summary) switch { case err == sql.ErrNoRows: err = JSONError{ @@ -296,6 +386,14 @@ return } + var sum interface{} + if summary.Valid { + if err = json.NewDecoder( + strings.NewReader(summary.String)).Decode(&sum); err != nil { + return + } + } + // We have it -> generate log entries. var rows *sql.Rows rows, err = conn.QueryContext(ctx, selectImportLogsSQL, id) @@ -320,8 +418,10 @@ jr = JSONResult{ Result: struct { + Summary interface{} `json:"summary,omitempty"` Entries []*models.ImportLogEntry `json:"entries"` }{ + Summary: sum, Entries: entries, }, }
--- a/pkg/models/import.go Fri Mar 15 12:43:30 2019 +0100 +++ b/pkg/models/import.go Fri Mar 15 15:59:40 2019 +0100 @@ -19,18 +19,20 @@ "time" ) +const ImportTimeFormat = "2006-01-02T15:04:05.000" + type ( ImportTime struct{ time.Time } Import struct { - ID int64 `json:"id"` - State string `json:"state"` - Enqueued ImportTime `json:"enqueued"` - Kind string `json:"kind"` - User string `json:"user"` - Signer string `json:"signer,omitempty"` - Summary interface{} `json:"summary,omitempty"` - Warnings bool `json:"warnings,omitempty"` + ID int64 `json:"id"` + State string `json:"state"` + Enqueued ImportTime `json:"enqueued"` + Kind string `json:"kind"` + User string `json:"user"` + Signer string `json:"signer,omitempty"` + Summary bool `json:"summary,omitempty"` + Warnings bool `json:"warnings,omitempty"` } ImportLogEntry struct { @@ -62,7 +64,7 @@ } func (it ImportTime) MarshalJSON() ([]byte, error) { - return json.Marshal(it.Format("2006-01-02T15:04:05.000")) + return json.Marshal(it.Format(ImportTimeFormat)) } func (it *ImportTime) Scan(x interface{}) error {