cli/bpcatalog/render.go (195 lines of code) (raw):
package bpcatalog
import (
"bytes"
"fmt"
"html/template"
"io"
"sort"
"strconv"
"strings"
"github.com/jedib0t/go-pretty/table"
)
// renderFormat defines the set of render options for catalog.
type renderFormat string
func (r *renderFormat) String() string {
return string(*r)
}
func (r *renderFormat) Empty() bool {
return r.String() == ""
}
func (r *renderFormat) Set(v string) error {
f, err := renderFormatFromString(v)
if err != nil {
return err
}
*r = f
return nil
}
func renderFormatFromString(s string) (renderFormat, error) {
format := renderFormat(s)
for _, stage := range renderFormats {
if format == stage {
return format, nil
}
}
return "", fmt.Errorf("one of %+v expected. unknown format: %s", renderFormats, s)
}
func (r *renderFormat) Type() string {
return "renderFormat"
}
const (
renderTable renderFormat = "table"
renderCSV renderFormat = "csv"
renderHTML renderFormat = "html"
renderTimeformat = "2006-01-02"
e2eLabel = "end-to-end"
htmlTemplate = `<table>
<thead>
<tr>
<th>Category</th>
<th>Blueprint</th>
<th>Description</th>
</tr>
</thead>
<tbody class="list">
{{range .}}{{if .Categories}}
<tr>
<td>{{.Categories}}</td>
<td><a
href="{{.URL}}" class="external">{{.DisplayName}}</a></td>
<td>{{.Description}}</td>
</tr>
{{end}}{{end}}
</tbody>
</table>`
)
var (
renderFormats = []renderFormat{renderTable, renderCSV, renderHTML}
// maps GH topics to categories
// https://github.com/GoogleCloudPlatform/cloud-foundation-toolkit/blob/29e980be9f3e3535f4b0b7314c9e1aea5ec2001f/infra/terraform/test-org/org/locals.tf#L39-L53
topicToCategory = map[string]string{
e2eLabel: "End-to-end",
"healthcare-life-sciences": "Healthcare and life sciences",
"serverless-computing": "Serverless computing",
"compute": "Compute",
"containers": "Containers",
"databases": "Databases",
"networking": "Networking",
"data-analytics": "Data analytics",
"storage": "Storage",
"operations": "Operations",
"developer-tools": "Developer tools",
"security-identity": "Security and identity",
"workspace": "Workspace",
}
// static display data for docs mode
// these repos are not currently auto discovered
staticDM = []displayMeta{
{
DisplayName: "fabric",
URL: "https://github.com/terraform-google-modules/cloud-foundation-fabric",
Categories: "End to end",
IsE2E: true,
Description: "Advanced examples designed for prototyping",
},
{
DisplayName: "ai-notebook",
URL: "https://github.com/GoogleCloudPlatform/notebooks-blueprint-security",
Categories: "End to end, Data analytics",
IsE2E: true,
Description: "Protect confidential data in Vertex AI Workbench notebooks",
},
}
)
// displayMeta stores processed display metadata.
// Currently it processes from repo info but
// may also pull from other sources like blueprint meta
// in the future.
type displayMeta struct {
Name string
DisplayName string
Stars string
CreatedAt string
Description string
Labels []string
URL string
Categories string
IsE2E bool
}
// render writes given repo information in the specified renderFormat to w.
func render(r repos, w io.Writer, format renderFormat, verbose bool) error {
dm := reposToDisplayMeta(r)
if format == renderHTML {
_, err := w.Write([]byte(renderDocHTML(append(dm, staticDM...))))
if err != nil {
return err
}
return nil
}
tbl := table.NewWriter()
tbl.SetOutputMirror(w)
h := table.Row{"Repo", "Stars", "Created"}
if verbose {
h = append(h, "Description")
}
tbl.AppendHeader(h)
for _, repo := range r {
row := table.Row{repo.GetName(), repo.GetStargazersCount(), repo.GetCreatedAt().Format(renderTimeformat)}
if verbose {
row = append(row, repo.GetDescription())
}
tbl.AppendRow(row)
}
switch format {
case renderTable:
tbl.Render()
case renderCSV:
tbl.RenderCSV()
default:
return fmt.Errorf("one of %+v expected. unknown format: %s", renderFormats, catalogListFlags.format)
}
return nil
}
// reposToDisplayMeta converts repo to displayMeta.
func reposToDisplayMeta(r repos) []displayMeta {
dm := make([]displayMeta, 0, len(r))
for _, repo := range r {
displayName := strings.TrimPrefix(repo.GetName(), "terraform-google-")
displayName = strings.TrimPrefix(displayName, "terraform-")
d := displayMeta{
Name: repo.GetName(),
DisplayName: displayName,
URL: repo.GetHTMLURL(),
Stars: strconv.Itoa(repo.GetStargazersCount()),
CreatedAt: repo.GetCreatedAt().Format(renderTimeformat),
Description: repo.GetDescription(),
Labels: repo.Topics,
}
// gh topics to categories
parsedCategories := []string{}
for _, topic := range repo.Topics {
p, exists := topicToCategory[topic]
if exists {
parsedCategories = append(parsedCategories, p)
}
if topic == e2eLabel {
d.IsE2E = true
}
}
if len(parsedCategories) > 0 {
sort.Strings(parsedCategories)
d.Categories = strings.Join(parsedCategories, ", ")
}
dm = append(dm, d)
}
return dm
}
// docSort sorts displayMeta surfacing e2e blueprints first for documentation.
func docSort(dm []displayMeta) []displayMeta {
sort.SliceStable(dm, func(i, j int) bool {
if dm[i].IsE2E && dm[j].IsE2E {
return dm[i].DisplayName < dm[j].DisplayName
}
return dm[i].IsE2E
})
return dm
}
// renderDocHTML renders html for documentation.
func renderDocHTML(dm []displayMeta) string {
htmlTmpl, err := template.New("htmlDoc").Parse(htmlTemplate)
if err != nil {
return fmt.Sprintf("error parsing template: %v", err)
}
var tpl bytes.Buffer
err = htmlTmpl.Execute(&tpl, docSort(dm))
if err != nil {
return fmt.Sprintf("error executing template: %v", err)
}
return tpl.String()
}