// The Running tab's and the instances API's shared list controls: a // substring match, a health-word filter, and an opaque after-this-host // cursor. GET-param driven and server-computed on purpose — a filter the // server applies is one the API and CLI can apply identically (every carlos // command works for a user with no box access), and a bookmarked filtered // URL keeps meaning the same thing. package console import ( "encoding/base64" "encoding/json" "net/url" "sort" "strings" ) // instancePageSize is the Running tab's page length. const instancePageSize = 50 // healthWords is every word healthWord can emit, in the order the header // lists them. A new word joins healthWord, this list, and nothing else — // slugs, filters and links all derive from here. Waking/Crashed/Never // started are sidecarHealthWord's own vocabulary (overview.go); Running and // Asleep are shared with the instance family, not duplicated here. var healthWords = []string{"Running", "Serving", "Setting up", "Asleep", "No readings", "Not responding", "Failed", "Waking", "Crashed", "Never started"} // instanceQuery is one request's list controls. Zero value = everything, // first page. type instanceQuery struct { Channel string Group bool Q string // substring match on host, trimmed Health string // canonical health word ("" = all) Cursor string // opaque; list resumes after the host inside it } func parseInstanceQuery(v url.Values) instanceQuery { return instanceQuery{ Channel: strings.TrimSpace(v.Get("channel")), Group: v.Get("group") == "channel", Q: strings.TrimSpace(v.Get("q")), Health: healthFromSlug(v.Get("health")), Cursor: v.Get("cursor"), } } // healthSlug is a health word's URL form: "Not responding" ↔ // "not-responding". func healthSlug(word string) string { return strings.ReplaceAll(strings.ToLower(word), " ", "-") } func healthTone(word string) string { switch word { case "Running", "Serving": return "ok" case "Failed", "Not responding": return "attention" case "Asleep", "Setting up": return "notdeployed" default: return "unknown" } } func hasInstanceChannel(channels []string, selected string) bool { for _, channel := range channels { if channel == selected { return true } } return false } // healthFromSlug inverts healthSlug. An unknown slug returns "" — no filter: // a mistyped URL shows everything rather than a mysteriously empty list. func healthFromSlug(slug string) string { for _, w := range healthWords { if healthSlug(w) == slug { return w } } return "" } type instanceCursor struct { Channel string `json:"channel,omitempty"` Grouped bool `json:"grouped,omitempty"` Host string `json:"host"` } func encodeInstanceCursor(host string) string { b, _ := json.Marshal(instanceCursor{Host: host}) return base64.RawURLEncoding.EncodeToString(b) } // decodeInstanceCursor degrades on any malformed input to "no cursor" — // page one, never an error page (applog.DecodeCursor's rule). func decodeInstanceCursor(s string) (string, bool) { b, err := base64.RawURLEncoding.DecodeString(s) if err != nil { return "", false } var c instanceCursor if json.Unmarshal(b, &c) != nil || c.Host == "" { return "", false } return c.Host, true } // healthLink is one header chip: a word, its count over ALL the app's rows, // and the link that filters to it. Precomputed — templates render facts. type healthLink struct { Word, Slug string Count int Active bool URL string } // instancePage is one screen of the list: the rows to render plus the // numbers and links the header states. type instancePage struct { Rows []InstanceRow Matching []InstanceRow // rows matching Q+Health+Channel, across all pages Counts map[string]int // health word -> count over ALL rows Total int // rows matching Q+Health+Channel, across all pages Links []healthLink // non-zero words, healthWords order NextCursor string // "" on the last page NextURL string // the whole next-page href; "" on the last page } // pageInstanceRows applies q to rows (which arrive in host order and stay in // it) and cuts one page. Counts cover every row regardless of filter — the // header's chips are the app's whole health story, and double as filters. func pageInstanceRows(rows []InstanceRow, q instanceQuery, limit int) instancePage { p := instancePage{Counts: map[string]int{}} var matched []InstanceRow for _, r := range rows { p.Counts[r.HealthWord]++ if q.Q != "" && !strings.Contains(r.Host, q.Q) { continue } if q.Channel != "" && r.Channel != q.Channel { continue } if q.Health != "" && r.HealthWord != q.Health { continue } matched = append(matched, r) } if q.Group { sort.SliceStable(matched, func(i, j int) bool { if matched[i].Channel != matched[j].Channel { return matched[i].Channel < matched[j].Channel } return matched[i].Host < matched[j].Host }) } p.Total = len(matched) p.Matching = matched start := 0 if after, ok := decodePageCursor(q.Cursor, q.Group); ok { for i, r := range matched { if (!q.Group && r.Host > after.Host) || (q.Group && (r.Channel > after.Channel || (r.Channel == after.Channel && r.Host > after.Host))) { start = i break } start = i + 1 } } end := start + limit if end < len(matched) { p.NextCursor = encodePageCursor(matched[end-1], q.Group) } else { end = len(matched) } p.Rows = matched[start:end] for _, w := range healthWords { if p.Counts[w] == 0 { continue } linkQuery := q linkQuery.Health, linkQuery.Cursor = w, "" p.Links = append(p.Links, healthLink{Word: w, Slug: healthSlug(w), Count: p.Counts[w], Active: q.Health == w, URL: instanceQuerySuffix(linkQuery)}) } if p.NextCursor != "" { next := q next.Cursor = p.NextCursor p.NextURL = instanceQuerySuffix(next) } return p }