feat: custom-api multiple API queries
This commit is contained in:
parent
6c8859863a
commit
8baa07d440
3 changed files with 142 additions and 45 deletions
|
@ -1290,11 +1290,34 @@ Examples:
|
|||
#### Properties
|
||||
| Name | Type | Required | Default |
|
||||
| ---- | ---- | -------- | ------- |
|
||||
| url | string | yes | |
|
||||
| url | string | yes, unless `api-queries` is set | |
|
||||
| headers | key (string) & value (string) | no | |
|
||||
| frameless | boolean | no | false |
|
||||
| template | string | yes | |
|
||||
| parameters | key & value | no | |
|
||||
| api-queries | list of urls, parameters & headers | no | |
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> `api-queries` will override `url`, `headers` and `parameters`
|
||||
> since it also provides its own options
|
||||
|
||||
##### `api-queries`
|
||||
A list of API queries, the name set will be the name of the json returned
|
||||
```yaml
|
||||
api-queries:
|
||||
sample-data1:
|
||||
url: https://domain.com/api
|
||||
parameters:
|
||||
foo: bar
|
||||
headers:
|
||||
x-api-key: your-api-key
|
||||
Accept: application/json
|
||||
sample-data2:
|
||||
url: https://another-domain.com/api
|
||||
```
|
||||
see [custom-api docs](./custom-api.md#api-queries)
|
||||
|
||||
|
||||
##### `url`
|
||||
The URL to fetch the data from. It must be accessible from the server that Glance is running on.
|
||||
|
|
|
@ -240,6 +240,28 @@ Output:
|
|||
|
||||
Other operations include `add`, `mul`, and `div`.
|
||||
|
||||
<hr>
|
||||
|
||||
#### API-Queries
|
||||
JSON response
|
||||
```json
|
||||
{
|
||||
"sample-data1": {
|
||||
"title": "My Title",
|
||||
"content": "My Content"
|
||||
},
|
||||
"sample-data2": [
|
||||
{
|
||||
"name": "John Doe"
|
||||
},
|
||||
{
|
||||
"name": "Jane Doe"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
In some instances, you may want to know the status code of the response. This can be done using the following:
|
||||
|
|
|
@ -3,6 +3,7 @@ package glance
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
@ -17,28 +18,70 @@ import (
|
|||
)
|
||||
|
||||
var customAPIWidgetTemplate = mustParseTemplate("custom-api.html", "widget-base.html")
|
||||
var customRandomKeyForSingleRequest = fmt.Sprintf("%x", time.Now().UnixNano())
|
||||
|
||||
type customAPIWidget struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
ApiQueries map[string]apiQueries `yaml:"api-queries"`
|
||||
URL string `yaml:"url"`
|
||||
Template string `yaml:"template"`
|
||||
Frameless bool `yaml:"frameless"`
|
||||
Headers map[string]string `yaml:"headers"`
|
||||
Parameters queryParametersField `yaml:"parameters"`
|
||||
APIRequest map[string]*http.Request `yaml:"-"`
|
||||
compiledTemplate *template.Template `yaml:"-"`
|
||||
CompiledHTML template.HTML `yaml:"-"`
|
||||
}
|
||||
|
||||
type apiQueries struct {
|
||||
URL string `yaml:"url"`
|
||||
Template string `yaml:"template"`
|
||||
Frameless bool `yaml:"frameless"`
|
||||
Headers map[string]string `yaml:"headers"`
|
||||
Parameters queryParametersField `yaml:"parameters"`
|
||||
APIRequest *http.Request `yaml:"-"`
|
||||
compiledTemplate *template.Template `yaml:"-"`
|
||||
CompiledHTML template.HTML `yaml:"-"`
|
||||
}
|
||||
|
||||
func (widget *customAPIWidget) initialize() error {
|
||||
widget.withTitle("Custom API").withCacheDuration(1 * time.Hour)
|
||||
|
||||
if widget.URL == "" {
|
||||
return errors.New("URL is required")
|
||||
}
|
||||
widget.APIRequest = make(map[string]*http.Request)
|
||||
if len(widget.ApiQueries) != 0 {
|
||||
for object, query := range widget.ApiQueries {
|
||||
if query.URL == "" {
|
||||
return errors.New("URL for each query is required")
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, query.URL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if widget.Template == "" {
|
||||
return errors.New("template is required")
|
||||
req.URL.RawQuery = query.Parameters.toQueryString()
|
||||
|
||||
for key, value := range query.Headers {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
|
||||
widget.APIRequest[object] = req
|
||||
}
|
||||
} else {
|
||||
if widget.URL == "" {
|
||||
return errors.New("URL is required")
|
||||
}
|
||||
|
||||
if widget.Template == "" {
|
||||
return errors.New("template is required")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, widget.URL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.URL.RawQuery = widget.Parameters.toQueryString()
|
||||
|
||||
for key, value := range widget.Headers {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
|
||||
widget.APIRequest[customRandomKeyForSingleRequest] = req
|
||||
}
|
||||
|
||||
compiledTemplate, err := template.New("").Funcs(customAPITemplateFuncs).Parse(widget.Template)
|
||||
|
@ -48,19 +91,6 @@ func (widget *customAPIWidget) initialize() error {
|
|||
|
||||
widget.compiledTemplate = compiledTemplate
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, widget.URL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.URL.RawQuery = widget.Parameters.toQueryString()
|
||||
|
||||
for key, value := range widget.Headers {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
|
||||
widget.APIRequest = req
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -77,36 +107,58 @@ func (widget *customAPIWidget) Render() template.HTML {
|
|||
return widget.renderTemplate(widget, customAPIWidgetTemplate)
|
||||
}
|
||||
|
||||
func fetchAndParseCustomAPI(req *http.Request, tmpl *template.Template) (template.HTML, error) {
|
||||
func fetchAndParseCustomAPI(requests map[string]*http.Request, tmpl *template.Template) (template.HTML, error) {
|
||||
emptyBody := template.HTML("")
|
||||
|
||||
resp, err := defaultHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return emptyBody, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return emptyBody, err
|
||||
}
|
||||
|
||||
body := strings.TrimSpace(string(bodyBytes))
|
||||
|
||||
if body != "" && !gjson.Valid(body) {
|
||||
truncatedBody, isTruncated := limitStringLength(body, 100)
|
||||
if isTruncated {
|
||||
truncatedBody += "... <truncated>"
|
||||
var resp *http.Response
|
||||
var err error
|
||||
body := make(map[string]string)
|
||||
mergedBody := "{}"
|
||||
for key, req := range requests {
|
||||
resp, err = defaultHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return emptyBody, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
slog.Error("Invalid response JSON in custom API widget", "url", req.URL.String(), "body", truncatedBody)
|
||||
return emptyBody, errors.New("invalid response JSON")
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return emptyBody, err
|
||||
}
|
||||
|
||||
body[key] = strings.TrimSpace(string(bodyBytes))
|
||||
|
||||
if body[key] != "" && !gjson.Valid(body[key]) {
|
||||
truncatedBody, isTruncated := limitStringLength(body[key], 100)
|
||||
if isTruncated {
|
||||
truncatedBody += "... <truncated>"
|
||||
}
|
||||
|
||||
slog.Error("Invalid response JSON in custom API widget", "url", req.URL.String(), key, truncatedBody)
|
||||
return emptyBody, errors.New("invalid response JSON")
|
||||
}
|
||||
}
|
||||
|
||||
if jsonBody, exists := body[customRandomKeyForSingleRequest]; exists {
|
||||
mergedBody = jsonBody
|
||||
} else {
|
||||
mergedMap := make(map[string]json.RawMessage)
|
||||
for key, jsonBody := range body {
|
||||
if !gjson.Valid(jsonBody) {
|
||||
continue
|
||||
}
|
||||
mergedMap[key] = json.RawMessage(jsonBody)
|
||||
}
|
||||
if len(mergedMap) > 0 {
|
||||
bytes, _ := json.Marshal(mergedMap)
|
||||
mergedBody = string(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
var templateBuffer bytes.Buffer
|
||||
|
||||
data := customAPITemplateData{
|
||||
JSON: decoratedGJSONResult{gjson.Parse(body)},
|
||||
JSON: decoratedGJSONResult{gjson.Parse(mergedBody)},
|
||||
Response: resp,
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue