diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index ccb309d8722..fa8d6d5e02f 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -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. | | `promQLScope` | In-development feature that will allow injection of labels into prometheus queries. | | `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | +| `newPDFRendering` | New implementation for the dashboard to PDF rendering | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 6e886b9946a..31c426f63e5 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -174,4 +174,5 @@ export interface FeatureToggles { promQLScope?: boolean; nodeGraphDotLayout?: boolean; groupToNestedTableTransformation?: boolean; + newPDFRendering?: boolean; } diff --git a/pkg/api/render.go b/pkg/api/render.go index 6a9811b0fd6..35ef1563d06 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -59,7 +59,9 @@ func (hs *HTTPServer) RenderToPng(c *contextmodel.ReqContext) { 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{ Timeout: time.Duration(timeout) * time.Second, }, @@ -72,7 +74,7 @@ func (hs *HTTPServer) RenderToPng(c *contextmodel.ReqContext) { Height: height, Path: web.Params(c.Req)["*"] + queryParams, Timezone: queryReader.Get("tz", ""), - Encoding: queryReader.Get("encoding", ""), + Encoding: encoding, ConcurrentLimit: hs.Cfg.RendererConcurrentRequestLimit, DeviceScaleFactor: scale, Headers: headers, @@ -88,7 +90,12 @@ func (hs *HTTPServer) RenderToPng(c *contextmodel.ReqContext) { 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") http.ServeFile(c.Resp, c.Req, result.FilePath) } diff --git a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go index d43f372161b..4f235a4f61d 100644 --- a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go +++ b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 -// protoc v4.25.1 +// protoc-gen-go v1.32.0 +// protoc v4.25.2 // source: rendererv2.proto package pluginextensionv2 @@ -83,6 +83,7 @@ type RenderRequest struct { 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"` 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() { @@ -194,6 +195,13 @@ func (x *RenderRequest) GetAuthToken() string { return "" } +func (x *RenderRequest) GetEncoding() string { + if x != nil { + return x.Encoding + } + return "" +} + type RenderResponse struct { state protoimpl.MessageState 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, 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, - 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, 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, @@ -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, 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, - 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, 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, + 0x09, 0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, + 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, + 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 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, + 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, 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 ( diff --git a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.proto b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.proto index 77a1fdce8a0..0d7a0f91ebf 100644 --- a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.proto +++ b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2.proto @@ -19,6 +19,7 @@ message RenderRequest { string timezone = 9; map headers = 10; string authToken = 11; + string encoding = 12; } message RenderResponse { diff --git a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2_grpc.pb.go b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2_grpc.pb.go index 50272edf511..7625f9655e2 100644 --- a/pkg/plugins/backendplugin/pluginextensionv2/rendererv2_grpc.pb.go +++ b/pkg/plugins/backendplugin/pluginextensionv2/rendererv2_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.25.1 +// - protoc v4.25.2 // source: rendererv2.proto package pluginextensionv2 diff --git a/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.pb.go b/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.pb.go index 43cc136c2af..a420647a687 100644 --- a/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.pb.go +++ b/pkg/plugins/backendplugin/pluginextensionv2/sanitizer.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 -// protoc v4.25.1 +// protoc-gen-go v1.32.0 +// protoc v4.25.2 // source: sanitizer.proto package pluginextensionv2 diff --git a/pkg/plugins/backendplugin/pluginextensionv2/sanitizer_grpc.pb.go b/pkg/plugins/backendplugin/pluginextensionv2/sanitizer_grpc.pb.go index 65c050047f9..80a3f117df3 100644 --- a/pkg/plugins/backendplugin/pluginextensionv2/sanitizer_grpc.pb.go +++ b/pkg/plugins/backendplugin/pluginextensionv2/sanitizer_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.3.0 -// - protoc v4.25.1 +// - protoc v4.25.2 // source: sanitizer.proto package pluginextensionv2 diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 400355b1146..2216fb66626 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -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) 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 { return err } diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index b550586d436..2337d7acad3 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -355,7 +355,7 @@ func (s *testRenderService) IsAvailable(ctx context.Context) bool { 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 { return s.renderProvider(ctx, opts) } diff --git a/pkg/services/cleanup/cleanup.go b/pkg/services/cleanup/cleanup.go index a96d5f5a702..d7a25bec37e 100644 --- a/pkg/services/cleanup/cleanup.go +++ b/pkg/services/cleanup/cleanup.go @@ -134,6 +134,7 @@ func (srv *CleanUpService) cleanUpTmpFiles(ctx context.Context) { folders := []string{ srv.Cfg.ImagesDir, srv.Cfg.CSVsDir, + srv.Cfg.PDFsDir, } for _, f := range folders { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 85bab6072a7..ec7e30a1dc4 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1318,5 +1318,12 @@ var ( Owner: grafanaDatavizSquad, 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), + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 0c28678d94f..07677a1f734 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -155,3 +155,4 @@ alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,2024-01-22,fals promQLScope,experimental,@grafana/observability-metrics,2024-01-29,false,false,false nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,2024-01-02,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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 78711cfd750..1c3b7444e48 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -630,4 +630,8 @@ const ( // FlagGroupToNestedTableTransformation // Enables the group to nested table transformation FlagGroupToNestedTableTransformation = "groupToNestedTableTransformation" + + // FlagNewPDFRendering + // New implementation for the dashboard to PDF rendering + FlagNewPDFRendering = "newPDFRendering" ) diff --git a/pkg/services/rendering/http_mode.go b/pkg/services/rendering/http_mode.go index 6e5677f48e4..01d318d5a9a 100644 --- a/pkg/services/rendering/http_mode.go +++ b/pkg/services/rendering/http_mode.go @@ -14,6 +14,8 @@ import ( "os" "strconv" "time" + + "github.com/grafana/grafana/pkg/services/featuremgmt" ) var netTransport = &http.Transport{ @@ -36,8 +38,16 @@ var ( remoteVersionRefreshInterval = time.Minute * 15 ) -func (rs *RenderingService) renderViaHTTP(ctx context.Context, renderKey string, opts Opts) (*RenderResult, error) { - filePath, err := rs.getNewFilePath(RenderPNG) +func (rs *RenderingService) renderViaHTTP(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" + } + + filePath, err := rs.getNewFilePath(renderType) if err != nil { return nil, err } diff --git a/pkg/services/rendering/interface.go b/pkg/services/rendering/interface.go index 1748a37248e..076064b7109 100644 --- a/pkg/services/rendering/interface.go +++ b/pkg/services/rendering/interface.go @@ -20,6 +20,7 @@ type RenderType string const ( RenderCSV RenderType = "csv" RenderPNG RenderType = "png" + RenderPDF RenderType = "pdf" ) type TimeoutOpts struct { @@ -93,7 +94,7 @@ type RenderCSVResult struct { 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 sanitizeFunc func(ctx context.Context, req *SanitizeSVGRequest) (*SanitizeSVGResponse, error) @@ -121,7 +122,7 @@ type CapabilitySupportRequestResult struct { type Service interface { IsAvailable(ctx context.Context) bool 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) RenderErrorImage(theme models.Theme, error error) (*RenderResult, error) GetRenderUser(ctx context.Context, key string) (*RenderUser, bool) diff --git a/pkg/services/rendering/mock.go b/pkg/services/rendering/mock.go index 9404b30ec46..d6282220428 100644 --- a/pkg/services/rendering/mock.go +++ b/pkg/services/rendering/mock.go @@ -1,7 +1,6 @@ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/grafana/grafana/pkg/services/rendering (interfaces: Service) -// Package rendering is a generated GoMock package. package rendering import ( @@ -37,122 +36,122 @@ func (m *MockService) EXPECT() *MockServiceMockRecorder { } // 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() - ret := m.ctrl.Call(m, "CreateRenderingSession", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "CreateRenderingSession", ctx, authOpts, sessionOpts) ret0, _ := ret[0].(Session) ret1, _ := ret[1].(error) return ret0, ret1 } // 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() - 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. -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() - ret := m.ctrl.Call(m, "GetRenderUser", arg0, arg1) + ret := m.ctrl.Call(m, "GetRenderUser", ctx, key) ret0, _ := ret[0].(*RenderUser) ret1, _ := ret[1].(bool) return ret0, ret1 } // 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() - 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. -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() - ret := m.ctrl.Call(m, "HasCapability", arg0, arg1) + ret := m.ctrl.Call(m, "HasCapability", ctx, capability) ret0, _ := ret[0].(CapabilitySupportRequestResult) ret1, _ := ret[1].(error) return ret0, ret1 } // 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() - 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. -func (m *MockService) IsAvailable(arg0 context.Context) bool { +func (m *MockService) IsAvailable(ctx context.Context) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsAvailable", arg0) + ret := m.ctrl.Call(m, "IsAvailable", ctx) ret0, _ := ret[0].(bool) return ret0 } // 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() - 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. -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() - ret := m.ctrl.Call(m, "Render", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "Render", ctx, renderType, opts, session) ret0, _ := ret[0].(*RenderResult) ret1, _ := ret[1].(error) return ret0, ret1 } // 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() - 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. -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() - ret := m.ctrl.Call(m, "RenderCSV", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "RenderCSV", ctx, opts, session) ret0, _ := ret[0].(*RenderCSVResult) ret1, _ := ret[1].(error) return ret0, ret1 } // 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() - 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. -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() - ret := m.ctrl.Call(m, "RenderErrorImage", arg0, arg1) + ret := m.ctrl.Call(m, "RenderErrorImage", theme, err) ret0, _ := ret[0].(*RenderResult) ret1, _ := ret[1].(error) return ret0, ret1 } // 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() - 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. -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() - ret := m.ctrl.Call(m, "SanitizeSVG", arg0, arg1) + ret := m.ctrl.Call(m, "SanitizeSVG", ctx, req) ret0, _ := ret[0].(*SanitizeSVGResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // 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() - 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. diff --git a/pkg/services/rendering/plugin_mode.go b/pkg/services/rendering/plugin_mode.go index 5e4ff4897ff..5e8772c9c20 100644 --- a/pkg/services/rendering/plugin_mode.go +++ b/pkg/services/rendering/plugin_mode.go @@ -6,14 +6,23 @@ import ( "fmt" "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. ctx, cancel := context.WithTimeout(ctx, getRequestTimeout(opts.TimeoutOpts)) defer cancel() - filePath, err := rs.getNewFilePath(RenderPNG) + filePath, err := rs.getNewFilePath(renderType) if err != nil { return nil, err } @@ -38,6 +47,7 @@ func (rs *RenderingService) renderViaPlugin(ctx context.Context, renderKey strin Domain: rs.domain, Headers: headers, AuthToken: rs.Cfg.RendererAuthToken, + Encoding: opts.Encoding, } rs.log.Debug("Calling renderer plugin", "req", req) diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index ded649ef67a..75a6e5253ff 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -58,16 +58,18 @@ type Plugin interface { } func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager, remoteCache *remotecache.RemoteCache, rm PluginManager) (*RenderingService, error) { - // ensure ImagesDir exists - err := os.MkdirAll(cfg.ImagesDir, 0700) - if err != nil { - return nil, fmt.Errorf("failed to create images directory %q: %w", cfg.ImagesDir, err) + folders := []string{ + cfg.ImagesDir, + cfg.CSVsDir, + cfg.PDFsDir, } - // ensure CSVsDir exists - err = os.MkdirAll(cfg.CSVsDir, 0700) - if err != nil { - return nil, fmt.Errorf("failed to create CSVs directory %q: %w", cfg.CSVsDir, err) + // ensure folders exists + for _, f := range folders { + err := os.MkdirAll(f, 0700) + if err != nil { + return nil, fmt.Errorf("failed to create directory %q: %w", f, err) + } } 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() renderKeyProvider := rs.perRequestRenderKeyProvider if session != nil { renderKeyProvider = session } - result, err := rs.render(ctx, opts, renderKeyProvider) + result, err := rs.render(ctx, renderType, opts, renderKeyProvider) elapsedTime := time.Since(startTime).Milliseconds() saveMetrics(elapsedTime, err, RenderPNG) @@ -263,7 +265,7 @@ func (rs *RenderingService) Render(ctx context.Context, opts Opts, session Sessi 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 { rs.log.Warn("Could not render image, hit the currency limit", "concurrencyLimit", opts.ConcurrentLimit, "path", opts.Path) 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))) - 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) { @@ -373,11 +375,18 @@ func (rs *RenderingService) getNewFilePath(rt RenderType) (string, error) { return "", err } - ext := "png" - folder := rs.Cfg.ImagesDir - if rt == RenderCSV { + var ext string + var folder string + switch rt { + case RenderCSV: ext = "csv" 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))) diff --git a/pkg/services/rendering/rendering_test.go b/pkg/services/rendering/rendering_test.go index d8b808aa170..36e78f36644 100644 --- a/pkg/services/rendering/rendering_test.go +++ b/pkg/services/rendering/rendering_test.go @@ -110,7 +110,7 @@ func TestRenderUnavailableError(t *testing.T) { RendererPluginManager: &dummyPluginManager{}, } 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.Nil(t, result) } @@ -152,7 +152,7 @@ func TestRenderLimitImage(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { 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.Equal(t, tc.expected, result.FilePath) }) @@ -170,7 +170,7 @@ func TestRenderLimitImageError(t *testing.T) { ConcurrentLimit: 1, 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.Nil(t, result) } diff --git a/pkg/services/screenshot/screenshot.go b/pkg/services/screenshot/screenshot.go index 799d46538f1..7f351184c8c 100644 --- a/pkg/services/screenshot/screenshot.go +++ b/pkg/services/screenshot/screenshot.go @@ -126,7 +126,7 @@ func (s *HeadlessScreenshotService) Take(ctx context.Context, opts ScreenshotOpt Path: u.String(), } - result, err := s.rs.Render(ctx, renderOpts, nil) + result, err := s.rs.Render(ctx, rendering.RenderPNG, renderOpts, nil) if err != nil { s.instrumentError(err) return nil, fmt.Errorf("failed to take screenshot: %w", err) diff --git a/pkg/services/screenshot/screenshot_test.go b/pkg/services/screenshot/screenshot_test.go index e39b60224b6..d1c665dffa6 100644 --- a/pkg/services/screenshot/screenshot_test.go +++ b/pkg/services/screenshot/screenshot_test.go @@ -62,7 +62,7 @@ func TestHeadlessScreenshotService(t *testing.T) { opts.DashboardUID = "foo" opts.PanelID = 4 r.EXPECT(). - Render(ctx, renderOpts, nil). + Render(ctx, rendering.RenderPNG, renderOpts, nil). Return(&rendering.RenderResult{FilePath: "panel.png"}, nil) screenshot, err = s.Take(ctx, opts) require.NoError(t, err) @@ -70,7 +70,7 @@ func TestHeadlessScreenshotService(t *testing.T) { // a timeout should return error r.EXPECT(). - Render(ctx, renderOpts, nil). + Render(ctx, rendering.RenderPNG, renderOpts, nil). Return(nil, rendering.ErrTimeout) screenshot, err = s.Take(ctx, opts) assert.EqualError(t, err, fmt.Sprintf("failed to take screenshot: %s", rendering.ErrTimeout)) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 4ff97faf647..92127cc6314 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -143,6 +143,7 @@ type Cfg struct { // Rendering ImagesDir string CSVsDir string + PDFsDir string RendererUrl string RendererCallbackUrl 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.ImagesDir = filepath.Join(cfg.DataPath, "png") cfg.CSVsDir = filepath.Join(cfg.DataPath, "csv") + cfg.PDFsDir = filepath.Join(cfg.DataPath, "pdf") return nil } diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index ac5a9b62231..7ce833528be 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -166,6 +166,14 @@ const getStyles = (theme: GrafanaTheme2) => { minHeight: 0, minWidth: 0, overflow: 'auto', + '@media print': { + overflow: 'visible', + }, + '@page': { + margin: 0, + size: 'auto', + padding: 0, + }, }), skipLink: css({ position: 'absolute',