mirror of
https://github.com/grafana/grafana.git
synced 2024-11-24 09:50:29 -06:00
Swagger: Add a custom swagger/api page (#91785)
Co-authored-by: Kristian Bremberg <kristian.bremberg@grafana.com>
This commit is contained in:
parent
dacf11b048
commit
427dad26a2
@ -7300,6 +7300,23 @@ exports[`better eslint`] = {
|
||||
"public/app/types/unified-alerting-dto.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/swagger/K8sNameLookup.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/swagger/SwaggerPage.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/swagger/index.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"]
|
||||
],
|
||||
"public/swagger/plugins.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/test/core/redux/reduxTester.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
|
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@ -506,7 +506,8 @@ playwright.config.ts @grafana/plugins-platform-frontend
|
||||
/public/test/ @grafana/grafana-frontend-platform
|
||||
/public/test/helpers/alertingRuleEditor.tsx @grafana/alerting-frontend
|
||||
/public/views/ @grafana/grafana-frontend-platform
|
||||
/public/views/swagger.html @grafana/grafana-backend-group
|
||||
/public/views/swagger.html @grafana/grafana-app-platform-squad
|
||||
/public/swagger/ @grafana/grafana-app-platform-squad
|
||||
|
||||
/public/app/features/explore/Logs/ @grafana/observability-logs
|
||||
|
||||
|
@ -143,6 +143,7 @@
|
||||
"@types/slate": "0.47.11",
|
||||
"@types/slate-plain-serializer": "0.7.5",
|
||||
"@types/slate-react": "0.22.9",
|
||||
"@types/swagger-ui-react": "4.18.3",
|
||||
"@types/systemjs": "6.13.5",
|
||||
"@types/testing-library__jest-dom": "5.14.9",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
@ -394,6 +395,7 @@
|
||||
"slate": "0.47.9",
|
||||
"slate-plain-serializer": "0.7.13",
|
||||
"slate-react": "0.22.10",
|
||||
"swagger-ui-react": "5.17.14",
|
||||
"symbol-observable": "4.0.0",
|
||||
"systemjs": "6.15.1",
|
||||
"systemjs-cjs-extra": "0.2.1",
|
||||
|
@ -220,7 +220,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/public/plugins/:pluginId/*", hs.getPluginAssets)
|
||||
|
||||
// add swagger support
|
||||
registerSwaggerUI(r)
|
||||
hs.registerSwaggerUI(r)
|
||||
|
||||
r.Post("/api/user/auth-tokens/rotate", routing.Wrap(hs.RotateUserAuthToken))
|
||||
r.Get("/user/auth-tokens/rotate", routing.Wrap(hs.RotateUserAuthTokenRedirect))
|
||||
|
@ -41,6 +41,7 @@ type EntryPointAssets struct {
|
||||
JSFiles []EntryPointAsset `json:"jsFiles"`
|
||||
Dark string `json:"dark"`
|
||||
Light string `json:"light"`
|
||||
Swagger []EntryPointAsset `json:"swagger"`
|
||||
}
|
||||
|
||||
type EntryPointAsset struct {
|
||||
@ -58,4 +59,7 @@ func (a *EntryPointAssets) SetContentDeliveryURL(prefix string) {
|
||||
for i, p := range a.JSFiles {
|
||||
a.JSFiles[i].FilePath = prefix + p.FilePath
|
||||
}
|
||||
for i, p := range a.Swagger {
|
||||
a.Swagger[i].FilePath = prefix + p.FilePath
|
||||
}
|
||||
}
|
||||
|
@ -2,31 +2,42 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/api/webassets"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/util/errhttp"
|
||||
)
|
||||
|
||||
func registerSwaggerUI(r routing.RouteRegister) {
|
||||
func (hs *HTTPServer) registerSwaggerUI(r routing.RouteRegister) {
|
||||
// Deprecated
|
||||
r.Get("/swagger-ui", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "swagger", http.StatusMovedPermanently)
|
||||
})
|
||||
// Deprecated
|
||||
r.Get("/openapi3", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "swagger?show=v3", http.StatusMovedPermanently)
|
||||
http.Redirect(w, r, "swagger", http.StatusMovedPermanently)
|
||||
})
|
||||
|
||||
// The swagger based api navigator
|
||||
r.Get("/swagger", func(c *contextmodel.ReqContext) {
|
||||
data := map[string]any{
|
||||
"Nonce": c.RequestNonce,
|
||||
ctx := c.Context.Req.Context()
|
||||
assets, err := webassets.GetWebAssets(ctx, hs.Cfg, hs.License)
|
||||
if err != nil {
|
||||
errhttp.Write(ctx, err, c.Resp)
|
||||
return
|
||||
}
|
||||
|
||||
// Add CSP for unpkg.com to allow loading of Swagger UI assets
|
||||
if existingCSP := c.Resp.Header().Get("Content-Security-Policy"); existingCSP != "" {
|
||||
newCSP := strings.Replace(existingCSP, "style-src", "style-src https://unpkg.com/", 1)
|
||||
c.Resp.Header().Set("Content-Security-Policy", newCSP)
|
||||
data := map[string]any{
|
||||
"Nonce": c.RequestNonce,
|
||||
"Assets": assets,
|
||||
"FavIcon": "public/img/fav32.png",
|
||||
"AppleTouchIcon": "public/img/apple-touch-icon.png",
|
||||
}
|
||||
if hs.Cfg.CSPEnabled {
|
||||
data["CSPEnabled"] = true
|
||||
data["CSPContent"] = middleware.ReplacePolicyVariables(hs.Cfg.CSPTemplate, hs.Cfg.AppURL, c.RequestNonce)
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "swagger", data)
|
||||
|
3516
pkg/api/webassets/testdata/sample-assets-manifest.json
vendored
3516
pkg/api/webassets/testdata/sample-assets-manifest.json
vendored
File diff suppressed because it is too large
Load Diff
@ -20,9 +20,10 @@ type ManifestInfo struct {
|
||||
Integrity string `json:"integrity,omitempty"`
|
||||
|
||||
// The known entrypoints
|
||||
App *EntryPointInfo `json:"app,omitempty"`
|
||||
Dark *EntryPointInfo `json:"dark,omitempty"`
|
||||
Light *EntryPointInfo `json:"light,omitempty"`
|
||||
App *EntryPointInfo `json:"app,omitempty"`
|
||||
Dark *EntryPointInfo `json:"dark,omitempty"`
|
||||
Light *EntryPointInfo `json:"light,omitempty"`
|
||||
Swagger *EntryPointInfo `json:"swagger,omitempty"`
|
||||
}
|
||||
|
||||
type EntryPointInfo struct {
|
||||
@ -128,18 +129,28 @@ func readWebAssets(r io.Reader) (*dtos.EntryPointAssets, error) {
|
||||
if entryPoints.Light == nil || len(entryPoints.Light.Assets.CSS) == 0 {
|
||||
return nil, fmt.Errorf("missing light entry")
|
||||
}
|
||||
// if entryPoints.Swagger == nil || len(entryPoints.Swagger.Assets.JS) == 0 {
|
||||
// return nil, fmt.Errorf("missing swagger entry")
|
||||
// }
|
||||
|
||||
rsp := &dtos.EntryPointAssets{
|
||||
JSFiles: make([]dtos.EntryPointAsset, 0, len(entryPoints.App.Assets.JS)),
|
||||
Dark: entryPoints.Dark.Assets.CSS[0],
|
||||
Light: entryPoints.Light.Assets.CSS[0],
|
||||
Swagger: make([]dtos.EntryPointAsset, 0, len(entryPoints.Swagger.Assets.JS)),
|
||||
}
|
||||
|
||||
entryPointJSAssets := make([]dtos.EntryPointAsset, 0)
|
||||
for _, entry := range entryPoints.App.Assets.JS {
|
||||
entryPointJSAssets = append(entryPointJSAssets, dtos.EntryPointAsset{
|
||||
rsp.JSFiles = append(rsp.JSFiles, dtos.EntryPointAsset{
|
||||
FilePath: entry,
|
||||
Integrity: integrity[entry],
|
||||
})
|
||||
}
|
||||
|
||||
return &dtos.EntryPointAssets{
|
||||
JSFiles: entryPointJSAssets,
|
||||
Dark: entryPoints.Dark.Assets.CSS[0],
|
||||
Light: entryPoints.Light.Assets.CSS[0],
|
||||
}, nil
|
||||
for _, entry := range entryPoints.Swagger.Assets.JS {
|
||||
rsp.Swagger = append(rsp.Swagger, dtos.EntryPointAsset{
|
||||
FilePath: entry,
|
||||
Integrity: integrity[entry],
|
||||
})
|
||||
}
|
||||
return rsp, nil
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package webassets
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -14,76 +15,64 @@ func TestReadWebassets(t *testing.T) {
|
||||
|
||||
dto, err := json.MarshalIndent(assets, "", " ")
|
||||
require.NoError(t, err)
|
||||
//fmt.Printf("%s\n", string(dto))
|
||||
// fmt.Printf("%s\n", string(dto))
|
||||
|
||||
require.JSONEq(t, `{
|
||||
"jsFiles": [
|
||||
{
|
||||
"filePath": "public/build/runtime.20ed8c01880b812ed29f.js",
|
||||
"integrity": "sha256-rcdxIHk6cWgu4jiFa1a+pWlileYD/R72GaS8ZACBUdw= sha384-I/VJZQkt+TuJTvu61ihdWPds7EHfLrW5CxeQ0x9gtSqoPg9Z17Uawz1yoYaTdxqQ sha512-4CPAbh4KdTmGxHoQw4pgpYmgAquupVfwfo6UBV2cGU3vGFnEwkhq320037ETwWs+n9xB/bAMOvrdabp1SA1+8g=="
|
||||
},
|
||||
{
|
||||
"filePath": "public/build/3951.4e474348841d792ab1ba.js",
|
||||
"integrity": "sha256-dHqXXTRA3osYhHr9rol8hOV0nC4VP0pr5tbMp5VD95Q= sha384-4QJaSTibnxdYeYsLnmXtd1+If6IkAmXlLR0uYHN5+N+fS0FegHRH7MIFaRGjiO1B sha512-vRLEeEGbxBCx0z+l/m14fSK49reqWGA9zQzsCrD+TQQBmP07YIoRPwopMMyxtKljbbRFV0bW2bUZ7ZvzOZYoIQ=="
|
||||
},
|
||||
{
|
||||
"filePath": "public/build/3651.4e8f7603e9778e1e9b59.js",
|
||||
"integrity": "sha256-+N7caL91pVANd7C/aquAneRTjBQenCwaEKqj+3qkjxc= sha384-GQR7GyHPEwwEVph9gGYWEWvMYxkITwcOjieehbPidXZrybuQyw9cpDkjnWo1tj/w sha512-zyPM+8AxyLuECEXjb9w6Z2Sy8zmJdkfTWQphcvAb8AU4ZdkCqLmyjmOs/QQlpfKDe0wdOLyR3V9QgTDDlxtVlQ=="
|
||||
},
|
||||
{
|
||||
"filePath": "public/build/1272.8c79fc44bf7cd993c953.js",
|
||||
"integrity": "sha256-d7MRVimV83v4YQ5rdURfTaaFtiedXP3EMLT06gvvBuQ= sha384-8tRpYHQ+sEkZ8ptiIbKAbKPpHTJVnmaWDN56vJoWWUCzV1Q2w034wcJNKDJDJdAs sha512-cIZWoJHusF8qODBOj2j4b18ewcLLMo/92YQSwYQjln2G5e3o1bSO476ox2I2iecJ/tnhQK5j01h9BzTt3dNTrA=="
|
||||
},
|
||||
{
|
||||
"filePath": "public/build/6902.070074e8f5a989b8f4c3.js",
|
||||
"integrity": "sha256-TMo/uTZueyEHtkBzlLZzhwYKWF0epE4qbouo5xcwZkU= sha384-xylZJMtJ7+EsUBBdQZvPh+BeHJ3BnfclqI2vx/8QC9jvfYe/lhRsWW9OMJsxE/Aq sha512-EOmf+KZQMFPoTWAROL8bBLFfHhgvDH8ONycq37JaV7lz+sQOTaWBN2ZD0F/mMdOD5zueTg/Y1RAUP6apoEcHNQ=="
|
||||
},
|
||||
{
|
||||
"filePath": "public/build/app.0439db6f56ee4aa501b2.js",
|
||||
"integrity": "sha256-q6muaKY7BuN2Ff+00aw69628MXatcFnLNzWRnAD98DI= sha384-gv6lAbkngOHR05bvyOR8dm/J3wIjQQWSjyxK7W8vt2rG9uxcjvvDQV7aI6YbUhfX sha512-o/0mSlJ/OoqrpGdOIWCE3ZCe8n+qqLbgNCERtx9G8FIzsv++CvIWSGbbILjOTGfnEfEQWcKMH0macVpVBSe1Og=="
|
||||
}
|
||||
{
|
||||
"filePath": "public/build/runtime~app.1140e4861852d9e6f2e2.js",
|
||||
"integrity": "sha256-HwsNc3NYC6Ee9cwVjOiEvhAmcaoe8HIwlwiVu/gc+D4= sha384-tt6bizrzSNPJH+uQGvcBokWaWp34sh0jp015BO7ZAO7HujvBjlduOid/30hz64DK sha512-hC1VmDBaqkULR+iPfPyAY4ZwAa5BXrahre0UrvO7+gLIpv810eu3uCGm5YTm+lFb+YFuBPlJDC4X2DarrEc5sg=="
|
||||
},
|
||||
{
|
||||
"filePath": "public/build/app.5e6b0a8f6800cdea4a51.js",
|
||||
"integrity": "sha256-xLLqXfNJ3+ybFt0Y3519SRZ8BkZvYm30epIE2y5EaIs= sha384-A/LoksVaN1WL6LO53SVm4KcBciyP6leDAdLLrxk13g0iPRMqOPEF+r7qEl0QnHp1 sha512-amLUBch//DibrSurQ2Zt5oUDvdhl48z3Scw7gqp2ASR0jcsLeev3RHcUJKrpW0GDTMRbUEAhIqFL9he2iYzg9g=="
|
||||
}
|
||||
],
|
||||
"dark": "public/build/grafana.dark.a28b24b45b2bbcc628cc.css",
|
||||
"light": "public/build/grafana.light.3572f6d5f8b7daa8d8d0.css"
|
||||
}`, string(dto))
|
||||
"dark": "public/build/grafana.dark.d27cc3e33cf31ab577e7.css",
|
||||
"light": "public/build/grafana.light.e6be3c7d879fd499516e.css",
|
||||
"swagger": [
|
||||
{
|
||||
"filePath": "public/build/runtime~swagger.793c8e6ede4824f9b730.js",
|
||||
"integrity": "sha256-cXK7bq3M16fgI1WgELNFabHXVvn84rPN960QwrmTKiQ= sha384-Ysrs6mXl6RUK5nyttGRFQh4ABbpqxnsckFdJFI7FTU9ZQnyXl04DCam0ADj6t43G sha512-l5lei1D6HlvLrxCVovMdwNhMLDc0zO7u/fJwIvASbi49HjuJRsn6cTw/ZBfeghtqBJF0mZeg+fXo0ZKriY8Wlg=="
|
||||
},
|
||||
{
|
||||
"filePath": "public/build/swagger.789d2ab30a8124d2a92c.js",
|
||||
"integrity": "sha256-zvXf9SHyzCHFvuOGphlImo8zbJ6jnJVXQOzUFNMPMW8= sha384-GLZ2Z9nV1opFHaQXC6pehHmy4XOBImvDk1w7b8QdYeyobarSrA93CzxjfrtCnA2Y sha512-CZ2oIrYuWGaGU+EZr4/KEFerOZU9XS2NELNNsIwtzs7Zcw/rroa4fU0PiZvXbeT+B2kUy1rGLAQ5nwgR0sQlNA=="
|
||||
}
|
||||
]
|
||||
}`, string(dto))
|
||||
|
||||
assets.SetContentDeliveryURL("https://grafana-assets.grafana.net/grafana/10.3.0-64123/")
|
||||
|
||||
dto, err = json.MarshalIndent(assets, "", " ")
|
||||
require.NoError(t, err)
|
||||
//fmt.Printf("%s\n", string(dto))
|
||||
fmt.Printf("%s\n", string(dto))
|
||||
|
||||
require.JSONEq(t, `{
|
||||
"cdn": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/",
|
||||
"jsFiles": [
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/runtime.20ed8c01880b812ed29f.js",
|
||||
"integrity": "sha256-rcdxIHk6cWgu4jiFa1a+pWlileYD/R72GaS8ZACBUdw= sha384-I/VJZQkt+TuJTvu61ihdWPds7EHfLrW5CxeQ0x9gtSqoPg9Z17Uawz1yoYaTdxqQ sha512-4CPAbh4KdTmGxHoQw4pgpYmgAquupVfwfo6UBV2cGU3vGFnEwkhq320037ETwWs+n9xB/bAMOvrdabp1SA1+8g=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/3951.4e474348841d792ab1ba.js",
|
||||
"integrity": "sha256-dHqXXTRA3osYhHr9rol8hOV0nC4VP0pr5tbMp5VD95Q= sha384-4QJaSTibnxdYeYsLnmXtd1+If6IkAmXlLR0uYHN5+N+fS0FegHRH7MIFaRGjiO1B sha512-vRLEeEGbxBCx0z+l/m14fSK49reqWGA9zQzsCrD+TQQBmP07YIoRPwopMMyxtKljbbRFV0bW2bUZ7ZvzOZYoIQ=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/3651.4e8f7603e9778e1e9b59.js",
|
||||
"integrity": "sha256-+N7caL91pVANd7C/aquAneRTjBQenCwaEKqj+3qkjxc= sha384-GQR7GyHPEwwEVph9gGYWEWvMYxkITwcOjieehbPidXZrybuQyw9cpDkjnWo1tj/w sha512-zyPM+8AxyLuECEXjb9w6Z2Sy8zmJdkfTWQphcvAb8AU4ZdkCqLmyjmOs/QQlpfKDe0wdOLyR3V9QgTDDlxtVlQ=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/1272.8c79fc44bf7cd993c953.js",
|
||||
"integrity": "sha256-d7MRVimV83v4YQ5rdURfTaaFtiedXP3EMLT06gvvBuQ= sha384-8tRpYHQ+sEkZ8ptiIbKAbKPpHTJVnmaWDN56vJoWWUCzV1Q2w034wcJNKDJDJdAs sha512-cIZWoJHusF8qODBOj2j4b18ewcLLMo/92YQSwYQjln2G5e3o1bSO476ox2I2iecJ/tnhQK5j01h9BzTt3dNTrA=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/6902.070074e8f5a989b8f4c3.js",
|
||||
"integrity": "sha256-TMo/uTZueyEHtkBzlLZzhwYKWF0epE4qbouo5xcwZkU= sha384-xylZJMtJ7+EsUBBdQZvPh+BeHJ3BnfclqI2vx/8QC9jvfYe/lhRsWW9OMJsxE/Aq sha512-EOmf+KZQMFPoTWAROL8bBLFfHhgvDH8ONycq37JaV7lz+sQOTaWBN2ZD0F/mMdOD5zueTg/Y1RAUP6apoEcHNQ=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/app.0439db6f56ee4aa501b2.js",
|
||||
"integrity": "sha256-q6muaKY7BuN2Ff+00aw69628MXatcFnLNzWRnAD98DI= sha384-gv6lAbkngOHR05bvyOR8dm/J3wIjQQWSjyxK7W8vt2rG9uxcjvvDQV7aI6YbUhfX sha512-o/0mSlJ/OoqrpGdOIWCE3ZCe8n+qqLbgNCERtx9G8FIzsv++CvIWSGbbILjOTGfnEfEQWcKMH0macVpVBSe1Og=="
|
||||
}
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/runtime~app.1140e4861852d9e6f2e2.js",
|
||||
"integrity": "sha256-HwsNc3NYC6Ee9cwVjOiEvhAmcaoe8HIwlwiVu/gc+D4= sha384-tt6bizrzSNPJH+uQGvcBokWaWp34sh0jp015BO7ZAO7HujvBjlduOid/30hz64DK sha512-hC1VmDBaqkULR+iPfPyAY4ZwAa5BXrahre0UrvO7+gLIpv810eu3uCGm5YTm+lFb+YFuBPlJDC4X2DarrEc5sg=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/app.5e6b0a8f6800cdea4a51.js",
|
||||
"integrity": "sha256-xLLqXfNJ3+ybFt0Y3519SRZ8BkZvYm30epIE2y5EaIs= sha384-A/LoksVaN1WL6LO53SVm4KcBciyP6leDAdLLrxk13g0iPRMqOPEF+r7qEl0QnHp1 sha512-amLUBch//DibrSurQ2Zt5oUDvdhl48z3Scw7gqp2ASR0jcsLeev3RHcUJKrpW0GDTMRbUEAhIqFL9he2iYzg9g=="
|
||||
}
|
||||
],
|
||||
"dark": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/grafana.dark.a28b24b45b2bbcc628cc.css",
|
||||
"light": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/grafana.light.3572f6d5f8b7daa8d8d0.css"
|
||||
}`, string(dto))
|
||||
"dark": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/grafana.dark.d27cc3e33cf31ab577e7.css",
|
||||
"light": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/grafana.light.e6be3c7d879fd499516e.css",
|
||||
"swagger": [
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/runtime~swagger.793c8e6ede4824f9b730.js",
|
||||
"integrity": "sha256-cXK7bq3M16fgI1WgELNFabHXVvn84rPN960QwrmTKiQ= sha384-Ysrs6mXl6RUK5nyttGRFQh4ABbpqxnsckFdJFI7FTU9ZQnyXl04DCam0ADj6t43G sha512-l5lei1D6HlvLrxCVovMdwNhMLDc0zO7u/fJwIvASbi49HjuJRsn6cTw/ZBfeghtqBJF0mZeg+fXo0ZKriY8Wlg=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/swagger.789d2ab30a8124d2a92c.js",
|
||||
"integrity": "sha256-zvXf9SHyzCHFvuOGphlImo8zbJ6jnJVXQOzUFNMPMW8= sha384-GLZ2Z9nV1opFHaQXC6pehHmy4XOBImvDk1w7b8QdYeyobarSrA93CzxjfrtCnA2Y sha512-CZ2oIrYuWGaGU+EZr4/KEFerOZU9XS2NELNNsIwtzs7Zcw/rroa4fU0PiZvXbeT+B2kUy1rGLAQ5nwgR0sQlNA=="
|
||||
}
|
||||
]
|
||||
}`, string(dto))
|
||||
}
|
||||
|
||||
func TestReadWebassetsFromCDN(t *testing.T) {
|
||||
|
@ -155,6 +155,11 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) {
|
||||
"js": ["public/build/runtime.XYZ.js"]
|
||||
}
|
||||
},
|
||||
"swagger": {
|
||||
"assets": {
|
||||
"js": ["public/build/runtime.XYZ.js"]
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"assets": {
|
||||
"css": ["public/build/dark.css"]
|
||||
|
116
public/swagger/K8sNameLookup.tsx
Normal file
116
public/swagger/K8sNameLookup.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Select } from '@grafana/ui';
|
||||
|
||||
import { NamespaceContext, ResourceContext } from './plugins';
|
||||
|
||||
type Props = {
|
||||
value?: string;
|
||||
onChange: (v?: string) => void;
|
||||
|
||||
// The wrapped element
|
||||
Original: React.ElementType;
|
||||
props: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export function K8sNameLookup(props: Props) {
|
||||
const [focused, setFocus] = useState(false);
|
||||
const [group, setGroup] = useState<string>();
|
||||
const [version, setVersion] = useState<string>();
|
||||
const [resource, setResource] = useState<string>();
|
||||
const [namespace, setNamespace] = useState<string>();
|
||||
const [namespaced, setNamespaced] = useState<boolean>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [options, setOptions] = useState<Array<SelectableValue<string>>>();
|
||||
const [placeholder, setPlaceholder] = useState<string>('Enter kubernetes name');
|
||||
|
||||
useEffect(() => {
|
||||
if (focused && group && version && resource) {
|
||||
setLoading(true);
|
||||
setPlaceholder('Enter kubernetes name');
|
||||
const fn = async () => {
|
||||
const url = namespaced
|
||||
? `apis/${group}/${version}/namespaces/${namespace}/${resource}`
|
||||
: `apis/${group}/${version}/${resource}`;
|
||||
|
||||
const response = await fetch(url + '?limit=100', {
|
||||
headers: {
|
||||
Accept:
|
||||
'application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/jso',
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.warn('error loading names');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const table = await response.json();
|
||||
console.log('LIST', url, table);
|
||||
const options: Array<SelectableValue<string>> = [];
|
||||
if (table.rows?.length) {
|
||||
table.rows.forEach((row: any) => {
|
||||
const n = row.object?.metadata?.name;
|
||||
if (n) {
|
||||
options.push({ label: n, value: n });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPlaceholder('No items found');
|
||||
}
|
||||
setLoading(false);
|
||||
setOptions(options);
|
||||
};
|
||||
fn();
|
||||
}
|
||||
}, [focused, namespace, group, version, resource, namespaced]);
|
||||
|
||||
return (
|
||||
<NamespaceContext.Consumer>
|
||||
{(namespace) => {
|
||||
return (
|
||||
<ResourceContext.Consumer>
|
||||
{(info) => {
|
||||
// delay avoids Cannot update a component
|
||||
setTimeout(() => {
|
||||
setNamespace(namespace);
|
||||
setGroup(info?.group);
|
||||
setVersion(info?.version);
|
||||
setResource(info?.resource);
|
||||
setNamespaced(info?.namespaced);
|
||||
}, 200);
|
||||
if (info) {
|
||||
const value = props.value ? { label: props.value, value: props.value } : undefined;
|
||||
return (
|
||||
<Select
|
||||
allowCreateWhileLoading={true}
|
||||
allowCustomValue={true}
|
||||
placeholder={placeholder}
|
||||
loadingMessage="Loading kubernetes names..."
|
||||
formatCreateLabel={(v) => `Use: ${v}`}
|
||||
onFocus={() => {
|
||||
// Delay loading until we click on the name
|
||||
setFocus(true);
|
||||
}}
|
||||
options={options}
|
||||
isLoading={loading}
|
||||
isClearable={true}
|
||||
defaultOptions
|
||||
value={value}
|
||||
onChange={(v: SelectableValue<string>) => {
|
||||
props.onChange(v?.value ?? '');
|
||||
}}
|
||||
onCreateOption={(v) => {
|
||||
props.onChange(v);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <props.Original {...props.props} />;
|
||||
}}
|
||||
</ResourceContext.Consumer>
|
||||
);
|
||||
}}
|
||||
</NamespaceContext.Consumer>
|
||||
);
|
||||
}
|
105
public/swagger/SwaggerPage.tsx
Normal file
105
public/swagger/SwaggerPage.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import getDefaultMonacoLanguages from 'lib/monaco-languages';
|
||||
import { useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
|
||||
import { createTheme, monacoLanguageRegistry, SelectableValue } from '@grafana/data';
|
||||
import { Stack, Select } from '@grafana/ui';
|
||||
import { setMonacoEnv } from 'app/core/monacoEnv';
|
||||
import { ThemeProvider } from 'app/core/utils/ConfigProvider';
|
||||
|
||||
import { NamespaceContext, WrappedPlugins } from './plugins';
|
||||
|
||||
export const Page = () => {
|
||||
const theme = createTheme({ colors: { mode: 'light' } });
|
||||
const [url, setURL] = useState<SelectableValue<string>>();
|
||||
const urls = useAsync(async () => {
|
||||
const v2 = { label: 'Grafana API (OpenAPI v2)', key: 'openapi2', value: 'public/api-merged.json' };
|
||||
const v3 = { label: 'Grafana API (OpenAPI v3)', key: 'openapi3', value: 'public/openapi3.json' };
|
||||
const urls: Array<SelectableValue<string>> = [v2, v3];
|
||||
|
||||
const rsp = await fetch('openapi/v3');
|
||||
const apis = await rsp.json();
|
||||
for (const [key, val] of Object.entries<any>(apis.paths)) {
|
||||
const parts = key.split('/');
|
||||
if (parts.length === 3) {
|
||||
urls.push({
|
||||
key: `${parts[1]}-${parts[2]}`,
|
||||
label: `${parts[1]}/${parts[2]}`,
|
||||
value: val.serverRelativeURL.substring(1), // remove initial slash
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const api = urlParams.get('api');
|
||||
if (api) {
|
||||
urls.forEach((url, i) => {
|
||||
if (url.key === api) {
|
||||
idx = i;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
monacoLanguageRegistry.setInit(getDefaultMonacoLanguages);
|
||||
setMonacoEnv();
|
||||
|
||||
setURL(urls[idx]); // Remove to start at the generic landing page
|
||||
return urls;
|
||||
});
|
||||
|
||||
const namespace = useAsync(async () => {
|
||||
const response = await fetch('api/frontend/settings');
|
||||
if (!response.ok) {
|
||||
console.warn('No settings found');
|
||||
return '';
|
||||
}
|
||||
const val = await response.json();
|
||||
return val.namespace;
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ThemeProvider value={theme}>
|
||||
<NamespaceContext.Provider value={namespace.value}>
|
||||
<div style={{ backgroundColor: '#000', padding: '10px' }}>
|
||||
<Stack justifyContent={'space-between'}>
|
||||
<img height="40" src="public/img/grafana_icon.svg" alt="Grafana" />
|
||||
<Select
|
||||
options={urls.value}
|
||||
isClearable={false /* TODO -- when we allow a landing page, this can be true */}
|
||||
onChange={(v) => {
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = '';
|
||||
if (v?.key) {
|
||||
url.searchParams.set('api', v.key);
|
||||
} else {
|
||||
url.searchParams.delete('api');
|
||||
}
|
||||
history.pushState(null, '', url);
|
||||
setURL(v);
|
||||
}}
|
||||
value={url}
|
||||
isLoading={urls.loading}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
{url?.value && (
|
||||
<SwaggerUI
|
||||
url={url.value}
|
||||
presets={[WrappedPlugins]}
|
||||
deepLinking={true}
|
||||
tryItOutEnabled={true}
|
||||
queryConfigEnabled={false}
|
||||
persistAuthorization={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!url?.value && <div>...{/** TODO, we can make an api docs loading page here */}</div>}
|
||||
</NamespaceContext.Provider>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
52
public/swagger/index.tsx
Normal file
52
public/swagger/index.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import '../app/core/trustedTypePolicies';
|
||||
declare let __webpack_public_path__: string;
|
||||
declare let __webpack_nonce__: string;
|
||||
|
||||
// Check if we are hosting files on cdn and set webpack public path
|
||||
if (window.public_cdn_path) {
|
||||
__webpack_public_path__ = window.public_cdn_path;
|
||||
}
|
||||
|
||||
// This is a path to the public folder without '/build'
|
||||
window.__grafana_public_path__ =
|
||||
__webpack_public_path__.substring(0, __webpack_public_path__.lastIndexOf('build/')) || __webpack_public_path__;
|
||||
|
||||
if (window.nonce) {
|
||||
__webpack_nonce__ = window.nonce;
|
||||
}
|
||||
|
||||
import 'swagger-ui-react/swagger-ui.css';
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { textUtil } from '@grafana/data';
|
||||
|
||||
import { Page } from './SwaggerPage';
|
||||
|
||||
// Use dom purify for the default policy
|
||||
const tt = window.trustedTypes;
|
||||
if (tt?.createPolicy) {
|
||||
tt.createPolicy('default', {
|
||||
createHTML: (string, sink) => DOMPurify.sanitize(string, { RETURN_TRUSTED_TYPE: true }) as unknown as string,
|
||||
createScriptURL: (url, sink) => textUtil.sanitizeUrl(url) as unknown as string,
|
||||
createScript: (script, sink) => script,
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
// the trailing slash breaks relative URL loading
|
||||
if (window.location.pathname.endsWith('/')) {
|
||||
const idx = window.location.href.lastIndexOf('/');
|
||||
window.location.href = window.location.href.substring(0, idx);
|
||||
return;
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
alert('unable to find root element');
|
||||
return;
|
||||
}
|
||||
const root = createRoot(rootElement);
|
||||
root.render(<Page />);
|
||||
};
|
163
public/swagger/plugins.tsx
Normal file
163
public/swagger/plugins.tsx
Normal file
@ -0,0 +1,163 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { CodeEditor, Monaco } from '@grafana/ui';
|
||||
|
||||
import { K8sNameLookup } from './K8sNameLookup';
|
||||
|
||||
// swagger does not have types
|
||||
interface UntypedProps {
|
||||
[k: string]: any;
|
||||
}
|
||||
|
||||
export type SchemaType = Record<string, any> | undefined;
|
||||
export type ResourceInfo = {
|
||||
group: string;
|
||||
version: string;
|
||||
resource: string;
|
||||
namespaced: boolean;
|
||||
};
|
||||
|
||||
// Use react contexts to stash settings
|
||||
export const SchemaContext = createContext<SchemaType>(undefined);
|
||||
export const NamespaceContext = createContext<string | undefined>(undefined);
|
||||
export const ResourceContext = createContext<ResourceInfo | undefined>(undefined);
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
export const WrappedPlugins = function () {
|
||||
return {
|
||||
wrapComponents: {
|
||||
parameterRow: (Original: React.ElementType) => (props: UntypedProps) => {
|
||||
// When the parameter name is in the path, lets make it a drop down
|
||||
const name = props.param.get('name');
|
||||
const where = props.param.get('in');
|
||||
if (name === 'name' && where === 'path') {
|
||||
const path = props.specPath.get(1).split('/');
|
||||
if (path.length > 4 && path[1] === 'apis') {
|
||||
const info: ResourceInfo = {
|
||||
group: path[2],
|
||||
version: path[3],
|
||||
resource: path[4],
|
||||
namespaced: path[4] === 'namespaces',
|
||||
};
|
||||
if (info.namespaced) {
|
||||
info.resource = path[6];
|
||||
}
|
||||
// console.log('NAME (in path)', path, info);
|
||||
return (
|
||||
<ResourceContext.Provider value={info}>
|
||||
<Original {...props} />
|
||||
</ResourceContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <Original {...props} />;
|
||||
},
|
||||
|
||||
// https://github.com/swagger-api/swagger-ui/blob/v5.17.14/src/core/components/parameters/parameters.jsx#L235
|
||||
// https://github.com/swagger-api/swagger-ui/blob/v5.17.14/src/core/plugins/oas3/components/request-body.jsx#L35
|
||||
RequestBody: (Original: React.ElementType) => (props: UntypedProps) => {
|
||||
let v: SchemaType = undefined;
|
||||
const content = props.requestBody.get('content');
|
||||
if (content) {
|
||||
let mime = content.get('application/json') ?? content.get('*/*');
|
||||
if (mime) {
|
||||
v = mime.get('schema').toJS();
|
||||
}
|
||||
console.log('RequestBody', v, mime, props);
|
||||
}
|
||||
// console.log('RequestBody PROPS', props);
|
||||
return (
|
||||
<SchemaContext.Provider value={v}>
|
||||
<Original {...props} />
|
||||
</SchemaContext.Provider>
|
||||
);
|
||||
},
|
||||
|
||||
modelExample: (Original: React.ElementType) => (props: UntypedProps) => {
|
||||
if (props.isExecute && props.schema) {
|
||||
console.log('modelExample PROPS', props);
|
||||
return (
|
||||
<SchemaContext.Provider value={props.schema.toJS()}>
|
||||
<Original {...props} />
|
||||
</SchemaContext.Provider>
|
||||
);
|
||||
}
|
||||
return <Original {...props} />;
|
||||
},
|
||||
|
||||
JsonSchemaForm: (Original: React.ElementType) => (props: UntypedProps) => {
|
||||
const { description, disabled, required, onChange, value } = props;
|
||||
if (!disabled && required) {
|
||||
switch (description) {
|
||||
case 'namespace': {
|
||||
return (
|
||||
<NamespaceContext.Consumer>
|
||||
{(namespace) => {
|
||||
if (!value && namespace) {
|
||||
setTimeout(() => {
|
||||
// Fake type in the value
|
||||
onChange(namespace);
|
||||
}, 100);
|
||||
}
|
||||
return <Original {...props} />;
|
||||
}}
|
||||
</NamespaceContext.Consumer>
|
||||
);
|
||||
}
|
||||
case 'name': {
|
||||
return <K8sNameLookup onChange={onChange} value={value} Original={Original} props={props} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
return <Original {...props} />;
|
||||
},
|
||||
|
||||
// https://github.com/swagger-api/swagger-ui/blob/v5.17.14/src/core/plugins/oas3/components/request-body-editor.jsx
|
||||
TextArea: (Original: React.ElementType) => (props: UntypedProps) => {
|
||||
return (
|
||||
<SchemaContext.Consumer>
|
||||
{(schema) => {
|
||||
if (schema) {
|
||||
const val = props.value ?? props.defaultValue ?? '';
|
||||
//console.log('JSON TextArea', props, info);
|
||||
// Return a synthetic text area event
|
||||
const cb = (txt: string) => {
|
||||
props.onChange({
|
||||
target: {
|
||||
value: txt,
|
||||
},
|
||||
});
|
||||
};
|
||||
console.log('CodeEditor', schema);
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
value={val}
|
||||
height={300}
|
||||
language="application/json"
|
||||
showMiniMap={val.length > 500}
|
||||
onBlur={cb}
|
||||
onSave={cb}
|
||||
onBeforeEditorMount={(monaco: Monaco) => {
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
schemas: [
|
||||
{
|
||||
uri: schema['$$ref'] ?? '#internal',
|
||||
fileMatch: ['*'], // everything
|
||||
schema: schema,
|
||||
},
|
||||
],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <Original {...props} />;
|
||||
}}
|
||||
</SchemaContext.Consumer>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
@ -1,110 +1,44 @@
|
||||
<!-- HTML for static distribution bundle build -->
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Swagger UI</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui.css"
|
||||
integrity="sha384-wxLW6kwyHktdDGr6Pv1zgm/VGJh99lfUbzSn6HNHBENZlCN7W602k9VkGdxuFvPn"
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
<head>
|
||||
[[ if and .CSPEnabled .IsDevelopmentEnv ]]
|
||||
<!-- Cypress overwrites CSP headers in HTTP requests, so this is required for e2e tests-->
|
||||
<meta http-equiv="Content-Security-Policy" content="[[.CSPContent]]"/>
|
||||
[[ end ]]
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="theme-color" content="#000" />
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
<title>Grafana API Reference</title>
|
||||
|
||||
.swagger-ui .topbar a {
|
||||
content: url('public/img/grafana_icon.svg');
|
||||
height: 50px;
|
||||
flex: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<link rel="stylesheet" href="[[.Assets.Light]]" />
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<link rel="icon" type="image/png" href="[[.FavIcon]]" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="[[.AppleTouchIcon]]" />
|
||||
<link rel="mask-icon" href="[[.Assets.ContentDeliveryURL]]public/img/grafana_mask_icon.svg" color="#F05A28" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<script nonce="[[$.Nonce]]">
|
||||
[[if .Assets.ContentDeliveryURL]]
|
||||
window.public_cdn_path = '[[.Assets.ContentDeliveryURL]]public/build/';
|
||||
[[end]]
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
[[range $asset := .Assets.Swagger]]
|
||||
<script
|
||||
nonce="[[.Nonce]]"
|
||||
src="https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-bundle.js"
|
||||
charset="UTF-8"
|
||||
integrity="sha384-wmyclcVGX/WhUkdkATwhaK1X1JtiNrr2EoYJ+diV3vj4v6OC5yCeSu+yW13SYJep"
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
></script>
|
||||
<script
|
||||
nonce="[[.Nonce]]"
|
||||
src="https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-standalone-preset.js"
|
||||
charset="UTF-8"
|
||||
integrity="sha384-2YH8WDRaj7V2OqU/trsmzSagmk/E2SutiCsGkdgoQwC9pNUJV1u/141DHB6jgs8t"
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
></script>
|
||||
<script nonce="[[.Nonce]]">
|
||||
window.onload = async function () {
|
||||
// the trailing slash breaks relative URL loading
|
||||
if (window.location.pathname.endsWith('/')) {
|
||||
const idx = window.location.href.lastIndexOf('/');
|
||||
window.location.href = window.location.href.substring(0, idx);
|
||||
return;
|
||||
}
|
||||
nonce="[[$.Nonce]]"
|
||||
src="[[$asset.FilePath]]"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
[[end]]
|
||||
<script>
|
||||
</script>
|
||||
</body>
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const v2 = { name: 'Grafana API (OpenAPI v2)', url: 'public/api-merged.json' };
|
||||
const v3 = { name: 'Grafana API (OpenAPI v3)', url: 'public/openapi3.json' };
|
||||
const urls = urlParams.get('show') == 'v3' ? [v3, v2] : [v2, v3];
|
||||
try {
|
||||
const rsp = await fetch('openapi/v3');
|
||||
const apis = await rsp.json();
|
||||
for (const [key, value] of Object.entries(apis.paths)) {
|
||||
const parts = key.split('/');
|
||||
if (parts.length == 3) {
|
||||
urls.push({
|
||||
name: `${parts[1]}/${parts[2]}`,
|
||||
url: value.serverRelativeURL.substring(1), // remove initial slash
|
||||
});
|
||||
}
|
||||
}
|
||||
urls.push({ name: 'Grafana apps (OpenAPI v2)', url: 'openapi/v2' });
|
||||
} catch (err) {
|
||||
// console.warn('Error loading k8s apis', err);
|
||||
}
|
||||
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
urls,
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
||||
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
|
||||
layout: 'StandaloneLayout',
|
||||
filter: true,
|
||||
tagsSorter: 'alpha',
|
||||
tryItOutEnabled: true,
|
||||
queryConfigEnabled: true, // keeps the selected ?urls.primaryName=...
|
||||
});
|
||||
|
||||
window.ui = ui;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
@ -7,6 +7,7 @@ module.exports = {
|
||||
target: 'web',
|
||||
entry: {
|
||||
app: './public/app/index.ts',
|
||||
swagger: './public/swagger/index.tsx',
|
||||
},
|
||||
output: {
|
||||
clean: true,
|
||||
|
@ -17,6 +17,7 @@
|
||||
"extends": "@grafana/tsconfig/base.json",
|
||||
"include": [
|
||||
"public/app/**/*.ts*",
|
||||
"public/swagger/**/*.ts*",
|
||||
"public/e2e-test/**/*.ts",
|
||||
"public/test/**/*.ts*",
|
||||
"public/vendor/**/*.ts",
|
||||
|
Loading…
Reference in New Issue
Block a user