console/filter: add completion endpoint

This commit is contained in:
Vincent Bernat
2022-05-26 15:58:52 +02:00
parent dc39c2dc14
commit 4114568600
12 changed files with 366 additions and 25 deletions

View File

@@ -1,7 +1,9 @@
package console
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
@@ -11,7 +13,7 @@ import (
// filterValidateHandlerInput describes the input for the /filter/validate endpoint.
type filterValidateHandlerInput struct {
Filter string `json:"filter"`
Filter string `json:"filter" binding:"required"`
}
// filterValidateHandlerOutput describes the output for the /filter/validate endpoint.
@@ -41,3 +43,185 @@ func (c *Component) filterValidateHandlerFunc(gc *gin.Context) {
Errors: filter.AllErrors(err),
})
}
// filterCompleteHandlerInput describes the input of the /filter/complete endpoint.
type filterCompleteHandlerInput struct {
What string `json:"what" binding:"required,oneof=column operator value"`
Column string `json:"column" binding:"required_unless=What column"`
Prefix string `json:"prefix"`
}
// filterCompleteHandlerOutput describes the output of the /filter/complete endpoint.
type filterCompleteHandlerOutput struct {
Completions []filterCompletion `json:"completions"`
}
type filterCompletion struct {
Label string `json:"label"`
Detail string `json:"detail,omitempty"`
Quoted bool `json:"quoted"` // should the return value be quoted?
}
func (c *Component) filterCompleteHandlerFunc(gc *gin.Context) {
ctx := c.t.Context(gc.Request.Context())
var input filterCompleteHandlerInput
if err := gc.ShouldBindJSON(&input); err != nil {
gc.JSON(http.StatusBadRequest, gin.H{"message": helpers.Capitalize(err.Error())})
return
}
completions := []filterCompletion{}
switch input.What {
case "column":
_, err := filter.Parse("", []byte{}, filter.Entrypoint("ConditionExpr"))
if err != nil {
for _, candidate := range filter.Expected(err) {
if !strings.HasSuffix(candidate, `"i`) {
continue
}
candidate = candidate[1 : len(candidate)-2]
completions = append(completions, filterCompletion{
Label: candidate,
Detail: "column name",
})
}
}
case "operator":
_, err := filter.Parse("",
[]byte(fmt.Sprintf("%s ", input.Column)),
filter.Entrypoint("ConditionExpr"))
if err != nil {
for _, candidate := range filter.Expected(err) {
if !strings.HasPrefix(candidate, `"`) {
continue
}
candidate = strings.TrimSuffix(
strings.TrimSuffix(candidate[1:len(candidate)-1], `"i`),
`"`)
if candidate != "--" && candidate != "/*" {
completions = append(completions, filterCompletion{
Label: candidate,
Detail: "condition operator",
})
}
}
}
case "value":
var column, detail string
switch strings.ToLower(input.Column) {
case "inifboundary", "outifboundary":
completions = append(completions, filterCompletion{
Label: "internal",
Detail: "network boundary",
}, filterCompletion{
Label: "external",
Detail: "network boundary",
}, filterCompletion{
Label: "undefined",
Detail: "network boundary",
})
case "etype":
completions = append(completions, filterCompletion{
Label: "IPv4",
Detail: "ethernet type",
}, filterCompletion{
Label: "IPv6",
Detail: "ethernet type",
})
case "proto":
// Do not complete from Clickhouse, we want a subset of options
completions = append(completions,
filterCompletion{"TCP", "protocol", true},
filterCompletion{"UDP", "protocol", true},
filterCompletion{"SCTP", "protocol", true},
filterCompletion{"ICMP", "protocol", true},
filterCompletion{"IPv6-ICMP", "protocol", true},
filterCompletion{"GRE", "protocol", true},
filterCompletion{"ESP", "protocol", true},
filterCompletion{"AH", "protocol", true},
filterCompletion{"IPIP", "protocol", true},
filterCompletion{"VRRP", "protocol", true},
filterCompletion{"L2TP", "protocol", true},
filterCompletion{"IGMP", "protocol", true},
filterCompletion{"PIM", "protocol", true},
filterCompletion{"IPv4", "protocol", true},
filterCompletion{"IPv6", "protocol", true})
case "srcas", "dstas":
// Query "asns" dictionary if we have at last 3 letters as a prefix
if len(input.Prefix) >= 3 {
sqlQuery := `
SELECT concat('AS', toString(asn)) AS label, detail
FROM asns
WHERE positionCaseInsensitive(name, $1) >= 1
ORDER BY positionCaseInsensitive(name, $1) ASC, asn ASC
LIMIT 20`
results := []struct {
Label string `ch:"label"`
Detail string `ch:"detail"`
}{}
if err := c.d.ClickHouseDB.Conn.Select(ctx, &results, sqlQuery, input.Prefix); err != nil {
c.r.Err(err).Msg("unable to query database")
break
}
for _, result := range results {
completions = append(completions, filterCompletion{
Label: result.Label,
Detail: result.Detail,
Quoted: false,
})
}
}
input.Prefix = "" // We have handled this internally
case "exportername":
column = "ExporterName"
detail = "exporter name"
case "exportergroup":
column = "ExporterGroup"
detail = "exporter group"
case "inifname", "outifname":
column = "IfName"
detail = "interface name"
case "inifdescription", "outifdescription":
column = "IfDescription"
detail = "interface description"
case "inifconnectivity", "outifconnectivity":
column = "IfConnectivity"
detail = "connectivity type"
case "inifprovider", "outifprovider":
column = "IfProvider"
detail = "provider name"
}
if column != "" {
// Query "exporter" table
sqlQuery := fmt.Sprintf(`
SELECT %s AS label
FROM exporters
WHERE positionCaseInsensitive(%s, $1) >= 1
GROUP BY %s
ORDER BY positionCaseInsensitive(%s, $1) ASC, %s ASC
LIMIT 20`, column, column, column, column, column)
results := []struct {
Label string `ch:"label"`
}{}
if err := c.d.ClickHouseDB.Conn.Select(ctx, &results, sqlQuery, input.Prefix); err != nil {
c.r.Err(err).Msg("unable to query database")
break
}
for _, result := range results {
completions = append(completions, filterCompletion{
Label: result.Label,
Detail: detail,
Quoted: true,
})
}
input.Prefix = ""
}
}
filteredCompletions := []filterCompletion{}
for _, completion := range completions {
if strings.HasPrefix(strings.ToLower(completion.Label), strings.ToLower(input.Prefix)) {
filteredCompletions = append(filteredCompletions, completion)
}
}
gc.JSON(http.StatusOK, filterCompleteHandlerOutput{filteredCompletions})
return
}