mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Rendering: Adds PDF support behind feature toggle (#81811)
* start pdf refactor * Update AppChrome.tsx * Update AppChrome.tsx * add encoding param to rendering grpc service * fix plugin mode * clean up * fix backend tests * fix lint errors * Support pdf encoding in render http api --------- Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
90a26e18db
commit
28e66b4ad8
@ -171,6 +171,7 @@ Experimental features might be changed or removed without prior notice.
|
|||||||
| `onPremToCloudMigrations` | In-development feature that will allow users to easily migrate their on-prem Grafana instances to Grafana Cloud. |
|
| `onPremToCloudMigrations` | In-development feature that will allow users to easily migrate their on-prem Grafana instances to Grafana Cloud. |
|
||||||
| `promQLScope` | In-development feature that will allow injection of labels into prometheus queries. |
|
| `promQLScope` | In-development feature that will allow injection of labels into prometheus queries. |
|
||||||
| `nodeGraphDotLayout` | Changed the layout algorithm for the node graph |
|
| `nodeGraphDotLayout` | Changed the layout algorithm for the node graph |
|
||||||
|
| `newPDFRendering` | New implementation for the dashboard to PDF rendering |
|
||||||
|
|
||||||
## Development feature toggles
|
## Development feature toggles
|
||||||
|
|
||||||
|
@ -174,4 +174,5 @@ export interface FeatureToggles {
|
|||||||
promQLScope?: boolean;
|
promQLScope?: boolean;
|
||||||
nodeGraphDotLayout?: boolean;
|
nodeGraphDotLayout?: boolean;
|
||||||
groupToNestedTableTransformation?: boolean;
|
groupToNestedTableTransformation?: boolean;
|
||||||
|
newPDFRendering?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,9 @@ func (hs *HTTPServer) RenderToPng(c *contextmodel.ReqContext) {
|
|||||||
hs.log.Error("Failed to parse user id", "err", errID)
|
hs.log.Error("Failed to parse user id", "err", errID)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := hs.RenderService.Render(c.Req.Context(), rendering.Opts{
|
encoding := queryReader.Get("encoding", "")
|
||||||
|
|
||||||
|
result, err := hs.RenderService.Render(c.Req.Context(), rendering.RenderPNG, rendering.Opts{
|
||||||
TimeoutOpts: rendering.TimeoutOpts{
|
TimeoutOpts: rendering.TimeoutOpts{
|
||||||
Timeout: time.Duration(timeout) * time.Second,
|
Timeout: time.Duration(timeout) * time.Second,
|
||||||
},
|
},
|
||||||
@ -72,7 +74,7 @@ func (hs *HTTPServer) RenderToPng(c *contextmodel.ReqContext) {
|
|||||||
Height: height,
|
Height: height,
|
||||||
Path: web.Params(c.Req)["*"] + queryParams,
|
Path: web.Params(c.Req)["*"] + queryParams,
|
||||||
Timezone: queryReader.Get("tz", ""),
|
Timezone: queryReader.Get("tz", ""),
|
||||||
Encoding: queryReader.Get("encoding", ""),
|
Encoding: encoding,
|
||||||
ConcurrentLimit: hs.Cfg.RendererConcurrentRequestLimit,
|
ConcurrentLimit: hs.Cfg.RendererConcurrentRequestLimit,
|
||||||
DeviceScaleFactor: scale,
|
DeviceScaleFactor: scale,
|
||||||
Headers: headers,
|
Headers: headers,
|
||||||
@ -88,7 +90,12 @@ func (hs *HTTPServer) RenderToPng(c *contextmodel.ReqContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Resp.Header().Set("Content-Type", "image/png")
|
if encoding == "pdf" {
|
||||||
|
c.Resp.Header().Set("Content-Type", "application/pdf")
|
||||||
|
} else {
|
||||||
|
c.Resp.Header().Set("Content-Type", "image/png")
|
||||||
|
}
|
||||||
|
|
||||||
c.Resp.Header().Set("Cache-Control", "private")
|
c.Resp.Header().Set("Cache-Control", "private")
|
||||||
http.ServeFile(c.Resp, c.Req, result.FilePath)
|
http.ServeFile(c.Resp, c.Req, result.FilePath)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.31.0
|
// protoc-gen-go v1.32.0
|
||||||
// protoc v4.25.1
|
// protoc v4.25.2
|
||||||
// source: rendererv2.proto
|
// source: rendererv2.proto
|
||||||
|
|
||||||
package pluginextensionv2
|
package pluginextensionv2
|
||||||
@ -83,6 +83,7 @@ type RenderRequest struct {
|
|||||||
Timezone string `protobuf:"bytes,9,opt,name=timezone,proto3" json:"timezone,omitempty"`
|
Timezone string `protobuf:"bytes,9,opt,name=timezone,proto3" json:"timezone,omitempty"`
|
||||||
Headers map[string]*StringList `protobuf:"bytes,10,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
Headers map[string]*StringList `protobuf:"bytes,10,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||||
AuthToken string `protobuf:"bytes,11,opt,name=authToken,proto3" json:"authToken,omitempty"`
|
AuthToken string `protobuf:"bytes,11,opt,name=authToken,proto3" json:"authToken,omitempty"`
|
||||||
|
Encoding string `protobuf:"bytes,12,opt,name=encoding,proto3" json:"encoding,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RenderRequest) Reset() {
|
func (x *RenderRequest) Reset() {
|
||||||
@ -194,6 +195,13 @@ func (x *RenderRequest) GetAuthToken() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *RenderRequest) GetEncoding() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Encoding
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type RenderResponse struct {
|
type RenderResponse struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@ -406,7 +414,7 @@ var file_rendererv2_proto_rawDesc = []byte{
|
|||||||
0x74, 0x6f, 0x12, 0x11, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73,
|
0x74, 0x6f, 0x12, 0x11, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73,
|
||||||
0x69, 0x6f, 0x6e, 0x76, 0x32, 0x22, 0x24, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c,
|
0x69, 0x6f, 0x6e, 0x76, 0x32, 0x22, 0x24, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c,
|
||||||
0x69, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20,
|
0x69, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20,
|
||||||
0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0xc7, 0x03, 0x0a, 0x0d,
|
0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0xe3, 0x03, 0x0a, 0x0d,
|
||||||
0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a,
|
0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a,
|
||||||
0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12,
|
0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12,
|
||||||
0x14, 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05,
|
0x14, 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05,
|
||||||
@ -429,56 +437,58 @@ var file_rendererv2_proto_rawDesc = []byte{
|
|||||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e,
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e,
|
||||||
0x74, 0x72, 0x79, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x0a, 0x09,
|
0x74, 0x72, 0x79, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x0a, 0x09,
|
||||||
0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52,
|
0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x09, 0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x1a, 0x59, 0x0a, 0x0c, 0x48, 0x65,
|
0x09, 0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e,
|
||||||
0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
|
0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e,
|
||||||
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x05,
|
0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x1a, 0x59, 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72,
|
||||||
0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x6c,
|
|
||||||
0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e,
|
|
||||||
0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
|
||||||
0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x26, 0x0a, 0x0e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52,
|
|
||||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
|
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xf1, 0x02,
|
|
||||||
0x0a, 0x10, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56, 0x52, 0x65, 0x71, 0x75, 0x65,
|
|
||||||
0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
|
||||||
0x03, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68,
|
|
||||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68,
|
|
||||||
0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20,
|
|
||||||
0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x16,
|
|
||||||
0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
|
|
||||||
0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75,
|
|
||||||
0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74,
|
|
||||||
0x12, 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x06, 0x20, 0x01,
|
|
||||||
0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x12, 0x4a, 0x0a, 0x07,
|
|
||||||
0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e,
|
|
||||||
0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76,
|
|
||||||
0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56, 0x52, 0x65, 0x71, 0x75, 0x65,
|
|
||||||
0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52,
|
|
||||||
0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68,
|
|
||||||
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, 0x74,
|
|
||||||
0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x1a, 0x59, 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72,
|
|
||||||
0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
|
0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e,
|
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e,
|
||||||
0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x72, 0x69,
|
0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x72, 0x69,
|
||||||
0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
|
0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
|
||||||
0x01, 0x22, 0x45, 0x0a, 0x11, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56, 0x52, 0x65,
|
0x01, 0x22, 0x26, 0x0a, 0x0e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18,
|
0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01,
|
||||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1a, 0x0a, 0x08,
|
0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xf1, 0x02, 0x0a, 0x10, 0x52, 0x65,
|
||||||
0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
|
0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10,
|
||||||
0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x32, 0xb1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x6e,
|
0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c,
|
||||||
0x64, 0x65, 0x72, 0x65, 0x72, 0x12, 0x4d, 0x0a, 0x06, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12,
|
0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01,
|
||||||
0x20, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f,
|
0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x1c, 0x0a, 0x09,
|
||||||
0x6e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x72, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x74, 0x1a, 0x21, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73,
|
0x09, 0x72, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f,
|
||||||
0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
|
0x6d, 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61,
|
||||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x09, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x53,
|
0x69, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20,
|
||||||
0x56, 0x12, 0x23, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73,
|
0x01, 0x28, 0x05, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x1a, 0x0a, 0x08,
|
||||||
0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56, 0x52,
|
0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
|
||||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65,
|
0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x12, 0x4a, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64,
|
||||||
0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65,
|
0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x70, 0x6c, 0x75, 0x67,
|
||||||
0x72, 0x43, 0x53, 0x56, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x16, 0x5a, 0x14,
|
0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x52, 0x65,
|
||||||
0x2e, 0x2f, 0x3b, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69,
|
0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48,
|
||||||
0x6f, 0x6e, 0x76, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x68, 0x65, 0x61,
|
||||||
|
0x64, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65,
|
||||||
|
0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b,
|
||||||
|
0x65, 0x6e, 0x1a, 0x59, 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74,
|
||||||
|
0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
|
0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20,
|
||||||
|
0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65,
|
||||||
|
0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69,
|
||||||
|
0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x45, 0x0a,
|
||||||
|
0x11, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||||
|
0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
|
0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65,
|
||||||
|
0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65,
|
||||||
|
0x4e, 0x61, 0x6d, 0x65, 0x32, 0xb1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x65,
|
||||||
|
0x72, 0x12, 0x4d, 0x0a, 0x06, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x70, 0x6c,
|
||||||
|
0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e,
|
||||||
|
0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e,
|
||||||
|
0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76,
|
||||||
|
0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||||
|
0x12, 0x56, 0x0a, 0x09, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56, 0x12, 0x23, 0x2e,
|
||||||
|
0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76,
|
||||||
|
0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
|
0x73, 0x74, 0x1a, 0x24, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e,
|
||||||
|
0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x43, 0x53, 0x56,
|
||||||
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x16, 0x5a, 0x14, 0x2e, 0x2f, 0x3b, 0x70,
|
||||||
|
0x6c, 0x75, 0x67, 0x69, 0x6e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x76, 0x32,
|
||||||
|
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -19,6 +19,7 @@ message RenderRequest {
|
|||||||
string timezone = 9;
|
string timezone = 9;
|
||||||
map<string, StringList> headers = 10;
|
map<string, StringList> headers = 10;
|
||||||
string authToken = 11;
|
string authToken = 11;
|
||||||
|
string encoding = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RenderResponse {
|
message RenderResponse {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.3.0
|
// - protoc-gen-go-grpc v1.3.0
|
||||||
// - protoc v4.25.1
|
// - protoc v4.25.2
|
||||||
// source: rendererv2.proto
|
// source: rendererv2.proto
|
||||||
|
|
||||||
package pluginextensionv2
|
package pluginextensionv2
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.31.0
|
// protoc-gen-go v1.32.0
|
||||||
// protoc v4.25.1
|
// protoc v4.25.2
|
||||||
// source: sanitizer.proto
|
// source: sanitizer.proto
|
||||||
|
|
||||||
package pluginextensionv2
|
package pluginextensionv2
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.3.0
|
// - protoc-gen-go-grpc v1.3.0
|
||||||
// - protoc v4.25.1
|
// - protoc v4.25.2
|
||||||
// source: sanitizer.proto
|
// source: sanitizer.proto
|
||||||
|
|
||||||
package pluginextensionv2
|
package pluginextensionv2
|
||||||
|
@ -232,7 +232,7 @@ func (n *notificationService) renderAndUploadImage(evalCtx *EvalContext, timeout
|
|||||||
|
|
||||||
n.log.Debug("Rendering alert panel image", "ruleId", evalCtx.Rule.ID, "urlPath", renderOpts.Path)
|
n.log.Debug("Rendering alert panel image", "ruleId", evalCtx.Rule.ID, "urlPath", renderOpts.Path)
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
result, err := n.renderService.Render(evalCtx.Ctx, renderOpts, nil)
|
result, err := n.renderService.Render(evalCtx.Ctx, rendering.RenderPNG, renderOpts, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -355,7 +355,7 @@ func (s *testRenderService) IsAvailable(ctx context.Context) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *testRenderService) Render(ctx context.Context, opts rendering.Opts, session rendering.Session) (*rendering.RenderResult, error) {
|
func (s *testRenderService) Render(ctx context.Context, _ rendering.RenderType, opts rendering.Opts, _ rendering.Session) (*rendering.RenderResult, error) {
|
||||||
if s.renderProvider != nil {
|
if s.renderProvider != nil {
|
||||||
return s.renderProvider(ctx, opts)
|
return s.renderProvider(ctx, opts)
|
||||||
}
|
}
|
||||||
|
@ -134,6 +134,7 @@ func (srv *CleanUpService) cleanUpTmpFiles(ctx context.Context) {
|
|||||||
folders := []string{
|
folders := []string{
|
||||||
srv.Cfg.ImagesDir,
|
srv.Cfg.ImagesDir,
|
||||||
srv.Cfg.CSVsDir,
|
srv.Cfg.CSVsDir,
|
||||||
|
srv.Cfg.PDFsDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range folders {
|
for _, f := range folders {
|
||||||
|
@ -1318,5 +1318,12 @@ var (
|
|||||||
Owner: grafanaDatavizSquad,
|
Owner: grafanaDatavizSquad,
|
||||||
Created: time.Date(2024, time.February, 5, 12, 0, 0, 0, time.UTC),
|
Created: time.Date(2024, time.February, 5, 12, 0, 0, 0, time.UTC),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "newPDFRendering",
|
||||||
|
Description: "New implementation for the dashboard to PDF rendering",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
Owner: grafanaSharingSquad,
|
||||||
|
Created: time.Date(2024, time.February, 8, 9, 51, 00, 00, time.UTC),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -155,3 +155,4 @@ alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,2024-01-22,fals
|
|||||||
promQLScope,experimental,@grafana/observability-metrics,2024-01-29,false,false,false
|
promQLScope,experimental,@grafana/observability-metrics,2024-01-29,false,false,false
|
||||||
nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,2024-01-02,false,false,true
|
nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,2024-01-02,false,false,true
|
||||||
groupToNestedTableTransformation,preview,@grafana/dataviz-squad,2024-02-05,false,false,true
|
groupToNestedTableTransformation,preview,@grafana/dataviz-squad,2024-02-05,false,false,true
|
||||||
|
newPDFRendering,experimental,@grafana/sharing-squad,2024-02-08,false,false,false
|
||||||
|
|
@ -630,4 +630,8 @@ const (
|
|||||||
// FlagGroupToNestedTableTransformation
|
// FlagGroupToNestedTableTransformation
|
||||||
// Enables the group to nested table transformation
|
// Enables the group to nested table transformation
|
||||||
FlagGroupToNestedTableTransformation = "groupToNestedTableTransformation"
|
FlagGroupToNestedTableTransformation = "groupToNestedTableTransformation"
|
||||||
|
|
||||||
|
// FlagNewPDFRendering
|
||||||
|
// New implementation for the dashboard to PDF rendering
|
||||||
|
FlagNewPDFRendering = "newPDFRendering"
|
||||||
)
|
)
|
||||||
|
@ -14,6 +14,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var netTransport = &http.Transport{
|
var netTransport = &http.Transport{
|
||||||
@ -36,8 +38,16 @@ var (
|
|||||||
remoteVersionRefreshInterval = time.Minute * 15
|
remoteVersionRefreshInterval = time.Minute * 15
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rs *RenderingService) renderViaHTTP(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) {
|
func (rs *RenderingService) renderViaHTTP(ctx context.Context, renderType RenderType, renderKey string, opts Opts) (*RenderResult, error) {
|
||||||
filePath, err := rs.getNewFilePath(RenderPNG)
|
if renderType == RenderPDF {
|
||||||
|
if !rs.features.IsEnabled(ctx, featuremgmt.FlagNewPDFRendering) {
|
||||||
|
return nil, fmt.Errorf("feature 'newPDFRendering' disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Encoding = "pdf"
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath, err := rs.getNewFilePath(renderType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ type RenderType string
|
|||||||
const (
|
const (
|
||||||
RenderCSV RenderType = "csv"
|
RenderCSV RenderType = "csv"
|
||||||
RenderPNG RenderType = "png"
|
RenderPNG RenderType = "png"
|
||||||
|
RenderPDF RenderType = "pdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TimeoutOpts struct {
|
type TimeoutOpts struct {
|
||||||
@ -93,7 +94,7 @@ type RenderCSVResult struct {
|
|||||||
FileName string
|
FileName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type renderFunc func(ctx context.Context, renderKey string, options Opts) (*RenderResult, error)
|
type renderFunc func(ctx context.Context, renderType RenderType, renderKey string, options Opts) (*RenderResult, error)
|
||||||
type renderCSVFunc func(ctx context.Context, renderKey string, options CSVOpts) (*RenderCSVResult, error)
|
type renderCSVFunc func(ctx context.Context, renderKey string, options CSVOpts) (*RenderCSVResult, error)
|
||||||
type sanitizeFunc func(ctx context.Context, req *SanitizeSVGRequest) (*SanitizeSVGResponse, error)
|
type sanitizeFunc func(ctx context.Context, req *SanitizeSVGRequest) (*SanitizeSVGResponse, error)
|
||||||
|
|
||||||
@ -121,7 +122,7 @@ type CapabilitySupportRequestResult struct {
|
|||||||
type Service interface {
|
type Service interface {
|
||||||
IsAvailable(ctx context.Context) bool
|
IsAvailable(ctx context.Context) bool
|
||||||
Version() string
|
Version() string
|
||||||
Render(ctx context.Context, opts Opts, session Session) (*RenderResult, error)
|
Render(ctx context.Context, renderType RenderType, opts Opts, session Session) (*RenderResult, error)
|
||||||
RenderCSV(ctx context.Context, opts CSVOpts, session Session) (*RenderCSVResult, error)
|
RenderCSV(ctx context.Context, opts CSVOpts, session Session) (*RenderCSVResult, error)
|
||||||
RenderErrorImage(theme models.Theme, error error) (*RenderResult, error)
|
RenderErrorImage(theme models.Theme, error error) (*RenderResult, error)
|
||||||
GetRenderUser(ctx context.Context, key string) (*RenderUser, bool)
|
GetRenderUser(ctx context.Context, key string) (*RenderUser, bool)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: github.com/grafana/grafana/pkg/services/rendering (interfaces: Service)
|
// Source: github.com/grafana/grafana/pkg/services/rendering (interfaces: Service)
|
||||||
|
|
||||||
// Package rendering is a generated GoMock package.
|
|
||||||
package rendering
|
package rendering
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -37,122 +36,122 @@ func (m *MockService) EXPECT() *MockServiceMockRecorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateRenderingSession mocks base method.
|
// CreateRenderingSession mocks base method.
|
||||||
func (m *MockService) CreateRenderingSession(arg0 context.Context, arg1 AuthOpts, arg2 SessionOpts) (Session, error) {
|
func (m *MockService) CreateRenderingSession(ctx context.Context, authOpts AuthOpts, sessionOpts SessionOpts) (Session, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "CreateRenderingSession", arg0, arg1, arg2)
|
ret := m.ctrl.Call(m, "CreateRenderingSession", ctx, authOpts, sessionOpts)
|
||||||
ret0, _ := ret[0].(Session)
|
ret0, _ := ret[0].(Session)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRenderingSession indicates an expected call of CreateRenderingSession.
|
// CreateRenderingSession indicates an expected call of CreateRenderingSession.
|
||||||
func (mr *MockServiceMockRecorder) CreateRenderingSession(arg0, arg1, arg2 any) *gomock.Call {
|
func (mr *MockServiceMockRecorder) CreateRenderingSession(ctx, authOpts, sessionOpts interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRenderingSession", reflect.TypeOf((*MockService)(nil).CreateRenderingSession), arg0, arg1, arg2)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRenderingSession", reflect.TypeOf((*MockService)(nil).CreateRenderingSession), ctx, authOpts, sessionOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRenderUser mocks base method.
|
// GetRenderUser mocks base method.
|
||||||
func (m *MockService) GetRenderUser(arg0 context.Context, arg1 string) (*RenderUser, bool) {
|
func (m *MockService) GetRenderUser(ctx context.Context, key string) (*RenderUser, bool) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "GetRenderUser", arg0, arg1)
|
ret := m.ctrl.Call(m, "GetRenderUser", ctx, key)
|
||||||
ret0, _ := ret[0].(*RenderUser)
|
ret0, _ := ret[0].(*RenderUser)
|
||||||
ret1, _ := ret[1].(bool)
|
ret1, _ := ret[1].(bool)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRenderUser indicates an expected call of GetRenderUser.
|
// GetRenderUser indicates an expected call of GetRenderUser.
|
||||||
func (mr *MockServiceMockRecorder) GetRenderUser(arg0, arg1 any) *gomock.Call {
|
func (mr *MockServiceMockRecorder) GetRenderUser(ctx, key interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRenderUser", reflect.TypeOf((*MockService)(nil).GetRenderUser), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRenderUser", reflect.TypeOf((*MockService)(nil).GetRenderUser), ctx, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasCapability mocks base method.
|
// HasCapability mocks base method.
|
||||||
func (m *MockService) HasCapability(arg0 context.Context, arg1 CapabilityName) (CapabilitySupportRequestResult, error) {
|
func (m *MockService) HasCapability(ctx context.Context, capability CapabilityName) (CapabilitySupportRequestResult, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "HasCapability", arg0, arg1)
|
ret := m.ctrl.Call(m, "HasCapability", ctx, capability)
|
||||||
ret0, _ := ret[0].(CapabilitySupportRequestResult)
|
ret0, _ := ret[0].(CapabilitySupportRequestResult)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasCapability indicates an expected call of HasCapability.
|
// HasCapability indicates an expected call of HasCapability.
|
||||||
func (mr *MockServiceMockRecorder) HasCapability(arg0, arg1 any) *gomock.Call {
|
func (mr *MockServiceMockRecorder) HasCapability(ctx, capability interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasCapability", reflect.TypeOf((*MockService)(nil).HasCapability), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasCapability", reflect.TypeOf((*MockService)(nil).HasCapability), ctx, capability)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAvailable mocks base method.
|
// IsAvailable mocks base method.
|
||||||
func (m *MockService) IsAvailable(arg0 context.Context) bool {
|
func (m *MockService) IsAvailable(ctx context.Context) bool {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "IsAvailable", arg0)
|
ret := m.ctrl.Call(m, "IsAvailable", ctx)
|
||||||
ret0, _ := ret[0].(bool)
|
ret0, _ := ret[0].(bool)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAvailable indicates an expected call of IsAvailable.
|
// IsAvailable indicates an expected call of IsAvailable.
|
||||||
func (mr *MockServiceMockRecorder) IsAvailable(arg0 any) *gomock.Call {
|
func (mr *MockServiceMockRecorder) IsAvailable(ctx interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAvailable", reflect.TypeOf((*MockService)(nil).IsAvailable), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAvailable", reflect.TypeOf((*MockService)(nil).IsAvailable), ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render mocks base method.
|
// Render mocks base method.
|
||||||
func (m *MockService) Render(arg0 context.Context, arg1 Opts, arg2 Session) (*RenderResult, error) {
|
func (m *MockService) Render(ctx context.Context, renderType RenderType, opts Opts, session Session) (*RenderResult, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "Render", arg0, arg1, arg2)
|
ret := m.ctrl.Call(m, "Render", ctx, renderType, opts, session)
|
||||||
ret0, _ := ret[0].(*RenderResult)
|
ret0, _ := ret[0].(*RenderResult)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render indicates an expected call of Render.
|
// Render indicates an expected call of Render.
|
||||||
func (mr *MockServiceMockRecorder) Render(arg0, arg1, arg2 any) *gomock.Call {
|
func (mr *MockServiceMockRecorder) Render(ctx, renderType, opts, session interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Render", reflect.TypeOf((*MockService)(nil).Render), arg0, arg1, arg2)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Render", reflect.TypeOf((*MockService)(nil).Render), ctx, renderType, opts, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderCSV mocks base method.
|
// RenderCSV mocks base method.
|
||||||
func (m *MockService) RenderCSV(arg0 context.Context, arg1 CSVOpts, arg2 Session) (*RenderCSVResult, error) {
|
func (m *MockService) RenderCSV(ctx context.Context, opts CSVOpts, session Session) (*RenderCSVResult, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "RenderCSV", arg0, arg1, arg2)
|
ret := m.ctrl.Call(m, "RenderCSV", ctx, opts, session)
|
||||||
ret0, _ := ret[0].(*RenderCSVResult)
|
ret0, _ := ret[0].(*RenderCSVResult)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderCSV indicates an expected call of RenderCSV.
|
// RenderCSV indicates an expected call of RenderCSV.
|
||||||
func (mr *MockServiceMockRecorder) RenderCSV(arg0, arg1, arg2 any) *gomock.Call {
|
func (mr *MockServiceMockRecorder) RenderCSV(ctx, opts, session interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenderCSV", reflect.TypeOf((*MockService)(nil).RenderCSV), arg0, arg1, arg2)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenderCSV", reflect.TypeOf((*MockService)(nil).RenderCSV), ctx, opts, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderErrorImage mocks base method.
|
// RenderErrorImage mocks base method.
|
||||||
func (m *MockService) RenderErrorImage(arg0 models.Theme, arg1 error) (*RenderResult, error) {
|
func (m *MockService) RenderErrorImage(theme models.Theme, err error) (*RenderResult, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "RenderErrorImage", arg0, arg1)
|
ret := m.ctrl.Call(m, "RenderErrorImage", theme, err)
|
||||||
ret0, _ := ret[0].(*RenderResult)
|
ret0, _ := ret[0].(*RenderResult)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderErrorImage indicates an expected call of RenderErrorImage.
|
// RenderErrorImage indicates an expected call of RenderErrorImage.
|
||||||
func (mr *MockServiceMockRecorder) RenderErrorImage(arg0, arg1 any) *gomock.Call {
|
func (mr *MockServiceMockRecorder) RenderErrorImage(theme, error interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenderErrorImage", reflect.TypeOf((*MockService)(nil).RenderErrorImage), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenderErrorImage", reflect.TypeOf((*MockService)(nil).RenderErrorImage), theme, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SanitizeSVG mocks base method.
|
// SanitizeSVG mocks base method.
|
||||||
func (m *MockService) SanitizeSVG(arg0 context.Context, arg1 *SanitizeSVGRequest) (*SanitizeSVGResponse, error) {
|
func (m *MockService) SanitizeSVG(ctx context.Context, req *SanitizeSVGRequest) (*SanitizeSVGResponse, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SanitizeSVG", arg0, arg1)
|
ret := m.ctrl.Call(m, "SanitizeSVG", ctx, req)
|
||||||
ret0, _ := ret[0].(*SanitizeSVGResponse)
|
ret0, _ := ret[0].(*SanitizeSVGResponse)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// SanitizeSVG indicates an expected call of SanitizeSVG.
|
// SanitizeSVG indicates an expected call of SanitizeSVG.
|
||||||
func (mr *MockServiceMockRecorder) SanitizeSVG(arg0, arg1 any) *gomock.Call {
|
func (mr *MockServiceMockRecorder) SanitizeSVG(ctx, req interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SanitizeSVG", reflect.TypeOf((*MockService)(nil).SanitizeSVG), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SanitizeSVG", reflect.TypeOf((*MockService)(nil).SanitizeSVG), ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version mocks base method.
|
// Version mocks base method.
|
||||||
|
@ -6,14 +6,23 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
|
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rs *RenderingService) renderViaPlugin(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) {
|
func (rs *RenderingService) renderViaPlugin(ctx context.Context, renderType RenderType, renderKey string, opts Opts) (*RenderResult, error) {
|
||||||
|
if renderType == RenderPDF {
|
||||||
|
if !rs.features.IsEnabled(ctx, featuremgmt.FlagNewPDFRendering) {
|
||||||
|
return nil, fmt.Errorf("feature 'newPDFRendering' disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Encoding = "pdf"
|
||||||
|
}
|
||||||
|
|
||||||
// gives plugin some additional time to timeout and return possible errors.
|
// gives plugin some additional time to timeout and return possible errors.
|
||||||
ctx, cancel := context.WithTimeout(ctx, getRequestTimeout(opts.TimeoutOpts))
|
ctx, cancel := context.WithTimeout(ctx, getRequestTimeout(opts.TimeoutOpts))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
filePath, err := rs.getNewFilePath(RenderPNG)
|
filePath, err := rs.getNewFilePath(renderType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -38,6 +47,7 @@ func (rs *RenderingService) renderViaPlugin(ctx context.Context, renderKey strin
|
|||||||
Domain: rs.domain,
|
Domain: rs.domain,
|
||||||
Headers: headers,
|
Headers: headers,
|
||||||
AuthToken: rs.Cfg.RendererAuthToken,
|
AuthToken: rs.Cfg.RendererAuthToken,
|
||||||
|
Encoding: opts.Encoding,
|
||||||
}
|
}
|
||||||
rs.log.Debug("Calling renderer plugin", "req", req)
|
rs.log.Debug("Calling renderer plugin", "req", req)
|
||||||
|
|
||||||
|
@ -58,16 +58,18 @@ type Plugin interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager, remoteCache *remotecache.RemoteCache, rm PluginManager) (*RenderingService, error) {
|
func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager, remoteCache *remotecache.RemoteCache, rm PluginManager) (*RenderingService, error) {
|
||||||
// ensure ImagesDir exists
|
folders := []string{
|
||||||
err := os.MkdirAll(cfg.ImagesDir, 0700)
|
cfg.ImagesDir,
|
||||||
if err != nil {
|
cfg.CSVsDir,
|
||||||
return nil, fmt.Errorf("failed to create images directory %q: %w", cfg.ImagesDir, err)
|
cfg.PDFsDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure CSVsDir exists
|
// ensure folders exists
|
||||||
err = os.MkdirAll(cfg.CSVsDir, 0700)
|
for _, f := range folders {
|
||||||
if err != nil {
|
err := os.MkdirAll(f, 0700)
|
||||||
return nil, fmt.Errorf("failed to create CSVs directory %q: %w", cfg.CSVsDir, err)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create directory %q: %w", f, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := log.New("rendering")
|
logger := log.New("rendering")
|
||||||
@ -248,14 +250,14 @@ func (rs *RenderingService) renderUnavailableImage() *RenderResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *RenderingService) Render(ctx context.Context, opts Opts, session Session) (*RenderResult, error) {
|
func (rs *RenderingService) Render(ctx context.Context, renderType RenderType, opts Opts, session Session) (*RenderResult, error) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
renderKeyProvider := rs.perRequestRenderKeyProvider
|
renderKeyProvider := rs.perRequestRenderKeyProvider
|
||||||
if session != nil {
|
if session != nil {
|
||||||
renderKeyProvider = session
|
renderKeyProvider = session
|
||||||
}
|
}
|
||||||
result, err := rs.render(ctx, opts, renderKeyProvider)
|
result, err := rs.render(ctx, renderType, opts, renderKeyProvider)
|
||||||
|
|
||||||
elapsedTime := time.Since(startTime).Milliseconds()
|
elapsedTime := time.Since(startTime).Milliseconds()
|
||||||
saveMetrics(elapsedTime, err, RenderPNG)
|
saveMetrics(elapsedTime, err, RenderPNG)
|
||||||
@ -263,7 +265,7 @@ func (rs *RenderingService) Render(ctx context.Context, opts Opts, session Sessi
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *RenderingService) render(ctx context.Context, opts Opts, renderKeyProvider renderKeyProvider) (*RenderResult, error) {
|
func (rs *RenderingService) render(ctx context.Context, renderType RenderType, opts Opts, renderKeyProvider renderKeyProvider) (*RenderResult, error) {
|
||||||
if int(atomic.LoadInt32(&rs.inProgressCount)) > opts.ConcurrentLimit {
|
if int(atomic.LoadInt32(&rs.inProgressCount)) > opts.ConcurrentLimit {
|
||||||
rs.log.Warn("Could not render image, hit the currency limit", "concurrencyLimit", opts.ConcurrentLimit, "path", opts.Path)
|
rs.log.Warn("Could not render image, hit the currency limit", "concurrencyLimit", opts.ConcurrentLimit, "path", opts.Path)
|
||||||
if opts.ErrorConcurrentLimitReached {
|
if opts.ErrorConcurrentLimitReached {
|
||||||
@ -306,7 +308,7 @@ func (rs *RenderingService) render(ctx context.Context, opts Opts, renderKeyProv
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
metrics.MRenderingQueue.Set(float64(atomic.AddInt32(&rs.inProgressCount, 1)))
|
metrics.MRenderingQueue.Set(float64(atomic.AddInt32(&rs.inProgressCount, 1)))
|
||||||
return rs.renderAction(ctx, renderKey, opts)
|
return rs.renderAction(ctx, renderType, renderKey, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *RenderingService) RenderCSV(ctx context.Context, opts CSVOpts, session Session) (*RenderCSVResult, error) {
|
func (rs *RenderingService) RenderCSV(ctx context.Context, opts CSVOpts, session Session) (*RenderCSVResult, error) {
|
||||||
@ -373,11 +375,18 @@ func (rs *RenderingService) getNewFilePath(rt RenderType) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
ext := "png"
|
var ext string
|
||||||
folder := rs.Cfg.ImagesDir
|
var folder string
|
||||||
if rt == RenderCSV {
|
switch rt {
|
||||||
|
case RenderCSV:
|
||||||
ext = "csv"
|
ext = "csv"
|
||||||
folder = rs.Cfg.CSVsDir
|
folder = rs.Cfg.CSVsDir
|
||||||
|
case RenderPDF:
|
||||||
|
ext = "pdf"
|
||||||
|
folder = rs.Cfg.PDFsDir
|
||||||
|
default:
|
||||||
|
ext = "png"
|
||||||
|
folder = rs.Cfg.ImagesDir
|
||||||
}
|
}
|
||||||
|
|
||||||
return filepath.Abs(filepath.Join(folder, fmt.Sprintf("%s.%s", rand, ext)))
|
return filepath.Abs(filepath.Join(folder, fmt.Sprintf("%s.%s", rand, ext)))
|
||||||
|
@ -110,7 +110,7 @@ func TestRenderUnavailableError(t *testing.T) {
|
|||||||
RendererPluginManager: &dummyPluginManager{},
|
RendererPluginManager: &dummyPluginManager{},
|
||||||
}
|
}
|
||||||
opts := Opts{ErrorOpts: ErrorOpts{ErrorRenderUnavailable: true}}
|
opts := Opts{ErrorOpts: ErrorOpts{ErrorRenderUnavailable: true}}
|
||||||
result, err := rs.Render(context.Background(), opts, nil)
|
result, err := rs.Render(context.Background(), RenderPNG, opts, nil)
|
||||||
assert.Equal(t, ErrRenderUnavailable, err)
|
assert.Equal(t, ErrRenderUnavailable, err)
|
||||||
assert.Nil(t, result)
|
assert.Nil(t, result)
|
||||||
}
|
}
|
||||||
@ -152,7 +152,7 @@ func TestRenderLimitImage(t *testing.T) {
|
|||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
opts := Opts{Theme: tc.theme, ConcurrentLimit: 1}
|
opts := Opts{Theme: tc.theme, ConcurrentLimit: 1}
|
||||||
result, err := rs.Render(context.Background(), opts, nil)
|
result, err := rs.Render(context.Background(), RenderPNG, opts, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, tc.expected, result.FilePath)
|
assert.Equal(t, tc.expected, result.FilePath)
|
||||||
})
|
})
|
||||||
@ -170,7 +170,7 @@ func TestRenderLimitImageError(t *testing.T) {
|
|||||||
ConcurrentLimit: 1,
|
ConcurrentLimit: 1,
|
||||||
Theme: models.ThemeDark,
|
Theme: models.ThemeDark,
|
||||||
}
|
}
|
||||||
result, err := rs.Render(context.Background(), opts, nil)
|
result, err := rs.Render(context.Background(), RenderPNG, opts, nil)
|
||||||
assert.Equal(t, ErrConcurrentLimitReached, err)
|
assert.Equal(t, ErrConcurrentLimitReached, err)
|
||||||
assert.Nil(t, result)
|
assert.Nil(t, result)
|
||||||
}
|
}
|
||||||
|
@ -126,7 +126,7 @@ func (s *HeadlessScreenshotService) Take(ctx context.Context, opts ScreenshotOpt
|
|||||||
Path: u.String(),
|
Path: u.String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := s.rs.Render(ctx, renderOpts, nil)
|
result, err := s.rs.Render(ctx, rendering.RenderPNG, renderOpts, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.instrumentError(err)
|
s.instrumentError(err)
|
||||||
return nil, fmt.Errorf("failed to take screenshot: %w", err)
|
return nil, fmt.Errorf("failed to take screenshot: %w", err)
|
||||||
|
@ -62,7 +62,7 @@ func TestHeadlessScreenshotService(t *testing.T) {
|
|||||||
opts.DashboardUID = "foo"
|
opts.DashboardUID = "foo"
|
||||||
opts.PanelID = 4
|
opts.PanelID = 4
|
||||||
r.EXPECT().
|
r.EXPECT().
|
||||||
Render(ctx, renderOpts, nil).
|
Render(ctx, rendering.RenderPNG, renderOpts, nil).
|
||||||
Return(&rendering.RenderResult{FilePath: "panel.png"}, nil)
|
Return(&rendering.RenderResult{FilePath: "panel.png"}, nil)
|
||||||
screenshot, err = s.Take(ctx, opts)
|
screenshot, err = s.Take(ctx, opts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -70,7 +70,7 @@ func TestHeadlessScreenshotService(t *testing.T) {
|
|||||||
|
|
||||||
// a timeout should return error
|
// a timeout should return error
|
||||||
r.EXPECT().
|
r.EXPECT().
|
||||||
Render(ctx, renderOpts, nil).
|
Render(ctx, rendering.RenderPNG, renderOpts, nil).
|
||||||
Return(nil, rendering.ErrTimeout)
|
Return(nil, rendering.ErrTimeout)
|
||||||
screenshot, err = s.Take(ctx, opts)
|
screenshot, err = s.Take(ctx, opts)
|
||||||
assert.EqualError(t, err, fmt.Sprintf("failed to take screenshot: %s", rendering.ErrTimeout))
|
assert.EqualError(t, err, fmt.Sprintf("failed to take screenshot: %s", rendering.ErrTimeout))
|
||||||
|
@ -143,6 +143,7 @@ type Cfg struct {
|
|||||||
// Rendering
|
// Rendering
|
||||||
ImagesDir string
|
ImagesDir string
|
||||||
CSVsDir string
|
CSVsDir string
|
||||||
|
PDFsDir string
|
||||||
RendererUrl string
|
RendererUrl string
|
||||||
RendererCallbackUrl string
|
RendererCallbackUrl string
|
||||||
RendererAuthToken string
|
RendererAuthToken string
|
||||||
@ -1765,6 +1766,7 @@ func (cfg *Cfg) readRenderingSettings(iniFile *ini.File) error {
|
|||||||
cfg.RendererRenderKeyLifeTime = renderSec.Key("render_key_lifetime").MustDuration(5 * time.Minute)
|
cfg.RendererRenderKeyLifeTime = renderSec.Key("render_key_lifetime").MustDuration(5 * time.Minute)
|
||||||
cfg.ImagesDir = filepath.Join(cfg.DataPath, "png")
|
cfg.ImagesDir = filepath.Join(cfg.DataPath, "png")
|
||||||
cfg.CSVsDir = filepath.Join(cfg.DataPath, "csv")
|
cfg.CSVsDir = filepath.Join(cfg.DataPath, "csv")
|
||||||
|
cfg.PDFsDir = filepath.Join(cfg.DataPath, "pdf")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -166,6 +166,14 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
|
'@media print': {
|
||||||
|
overflow: 'visible',
|
||||||
|
},
|
||||||
|
'@page': {
|
||||||
|
margin: 0,
|
||||||
|
size: 'auto',
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
skipLink: css({
|
skipLink: css({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
Loading…
Reference in New Issue
Block a user