Mailing List Archive

rt branch 5.0/create-chart-images-for-dashboard-emails-2 created. rt-5.0.5-83-g541daa8737
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "rt".

The branch, 5.0/create-chart-images-for-dashboard-emails-2 has been created
at 541daa87378e8c9bb1f6730a829ae374addfd73e (commit)

- Log -----------------------------------------------------------------
commit 541daa87378e8c9bb1f6730a829ae374addfd73e
Author: Brian Conry <bconry@bestpractical.com>
Date: Thu Oct 6 13:01:35 2022 -0500

Email JSChart images with WWW::Mechanize::Chrome

This change allows the dashboard emails to contain image versions of
JSChart graphs obtained using WWW::Mechanize::Chrome.

Previously it was only possible to generate graph images for emails
using the GD module.

This Feature has been tested with Chrome, Chromium, Microsoft Edge, and
Opera.

diff --git a/docs/UPGRADING-5.0 b/docs/UPGRADING-5.0
index d12cb26e19..4015f71d01 100644
--- a/docs/UPGRADING-5.0
+++ b/docs/UPGRADING-5.0
@@ -634,4 +634,27 @@ messages, you may need to update your system to match the new format.

=back

+=head1 UPGRADING FROM 5.0.5 AND EARLIER
+
+=over 4
+
+=item * Additional options for charts in dashboard emails
+
+While it has been possible to use JSChart to generate chart images in the RT UI,
+because these images are generated client-side it hasn't been possible to include
+them in dashboard emails, so the GD-generated images have been the only option.
+
+It is now possible to use the optional Perl module L<WWW::Mechanize::Chrome> and
+a compatible server-side web brwoser to create images of the JSChart graphs for
+inclusion in emails.
+
+This is accomplished by setting C<$EmailDashboardJSChartImages> to '1' and
+maybe also setting C<$ChromePath> to the fully-qualified path of the executable
+for your chosen Chrome-based browser.
+
+This feature has been tested with Chrome, Chromium, Microsoft Edge, and Opera.
+Other Chrome-based browsers may also work.
+
+=back
+
=cut
diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 6433198a36..c1cef7c71c 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -971,6 +971,47 @@ With this enabled, some parts of the email won't look exactly like RT.

Set($EmailDashboardInlineCSS, 0);

+=item C<$EmailDashboardJSChartImages>
+
+To use the JSChart-generated images in emailed dashboards, install the
+optional module L<WWW::Mechanize::Chrome> and enable this option.
+
+=cut
+
+Set($EmailDashboardJSChartImages, 0);
+
+=item C<$ChromePath>
+
+This option contains the fully-qualified path for a compatible Chrome-based
+browser executable that will be used to generate static images for JSChart
+graphs for dashboard emails.
+
+Use this option to set the path to your executable if it is in a non-standard
+location or if the executable has a non-standard name.
+
+See also L<WWW::Mechanize::Chrome/launch_exe>
+
+=cut
+
+Set($ChromePath, '');
+
+=item C<@ChromeLaunchArguments>
+
+This option contains the launch arguments when initializing
+L<WWW::Mechanize::Chrome>.
+
+If you need to run L<rt-email-dashboards> as root, you probably need to add
+C<--no-sandbox> to get around Chrome's restriction:
+
+ Set(@ChromeLaunchArguments, '--no-sandbox');
+
+See also L<WWW::Mechanize::Chrome/launch_arg>
+
+=cut
+
+Set(@ChromeLaunchArguments, () );
+
+
=back


diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 04fbe3dcd5..36c7b547dc 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1994,6 +1994,15 @@ our %META;
EmailDashboardInlineCSS => {
Widget => '/Widgets/Form/Boolean',
},
+ EmailDashboardJSChartImages => {
+ Widget => '/Widgets/Form/Boolean',
+ },
+ ChromePath => {
+ Widget => '/Widgets/Form/String',
+ },
+ ChromeLaunchArguments => {
+ Type => 'ARRAY',
+ },
DefaultErrorMailPrecedence => {
Widget => '/Widgets/Form/String',
},
diff --git a/lib/RT/Dashboard/Mailer.pm b/lib/RT/Dashboard/Mailer.pm
index 84ce29e171..e5fe46e0f6 100644
--- a/lib/RT/Dashboard/Mailer.pm
+++ b/lib/RT/Dashboard/Mailer.pm
@@ -423,8 +423,6 @@ SUMMARY
}
}

- $content = ScrubContent($content);
-
$RT::Logger->debug("Got ".length($content)." characters of output.");

$content = HTML::RewriteAttributes::Links->rewrite(
@@ -536,6 +534,8 @@ sub EmailDashboard {
$RT::Logger->debug("Done sending dashboard to ".$currentuser->Name." <$email>");
}

+my $chrome;
+
sub BuildEmail {
my $self = shift;
my %args = (
@@ -592,6 +592,132 @@ sub BuildEmail {
inline_imports => 1,
);

+ # This needs to be done after all of the CSS has been imported (by
+ # inline_css above, which is distinct from the work done by CSS::Inliner
+ # below) and before all of the scripts are scrubbed away.
+ if ( RT->Config->Get('EmailDashboardJSChartImages') ) {
+ if ( RT::StaticUtil::RequireModule("WWW::Mechanize::Chrome") ) {
+
+ # WWW::Mechanize::Chrome uses Log::Log4perl and calls trace sometimes.
+ # Here we turn off trace and also mimic trace as debug.
+ local *Log::Dispatch::is_trace = sub {0};
+ local *Log::Dispatch::trace = sub {
+ my $self = shift;
+ return $self->debug(@_);
+ };
+
+ my ( $width, $height );
+ my @launch_arguments = RT->Config->Get('ChromeLaunchArguments');
+
+ for my $arg (@launch_arguments) {
+ if ( $arg =~ /^--window-size=(\d+)x(\d+)$/ ) {
+ $width = $1;
+ $height = $2;
+ last;
+ }
+ }
+
+ $width ||= 2560;
+ $height ||= 1440;
+
+ $chrome ||= WWW::Mechanize::Chrome->new(
+ autodie => 0,
+ headless => 1,
+ autoclose => 1,
+ separate_session => 1,
+ log => RT->Logger,
+ launch_arg => \@launch_arguments,
+ RT->Config->Get('ChromePath') ? ( launch_exe => RT->Config->Get('ChromePath') ) : (),
+ );
+
+ # copy the content
+ my $content_with_script = $content;
+
+ # copy in the text of the linked js
+ $content_with_script
+ =~ s{<script type="text/javascript" src="([^"]+)"></script>}{<script type="text/javascript">@{ [(GetResource( $1 ))[0]] }</script>}g;
+
+ # write the complete content to a temp file
+ my $temp_fh = File::Temp->new(
+ UNLINK => 1,
+ TEMPLATE => 'email-dashboard-XXXXXX',
+ SUFFIX => '.html',
+ DIR => $RT::VarPath, # $chrome can't get the file if saved to /tmp
+ );
+ print $temp_fh Encode::encode( 'UTF-8', $content_with_script );
+ close $temp_fh;
+
+ $chrome->viewport_size( { width => $width, height => $height } );
+ $chrome->get_local( $temp_fh->filename );
+ $chrome->wait_until_visible( selector => 'div.dashboard' );
+
+ # grab the list of canvas elements
+ my @canvases = $chrome->selector('div.chart canvas');
+ if (@canvases) {
+
+ my $max_extent = 0;
+
+ # ... and their coordinates
+ foreach my $canvas_data (@canvases) {
+ my $coords = $canvas_data->{coords} = $chrome->element_coordinates($canvas_data);
+ if ( $max_extent < $coords->{top} + $coords->{height} ) {
+ $max_extent = int( $coords->{top} + $coords->{height} ) + 1;
+ }
+ }
+
+ # make sure that all of them are "visible" in the headless instance
+ if ( $height < $max_extent ) {
+ $chrome->viewport_size( { width => $width, height => $max_extent } );
+ }
+
+ # capture the entire page as an image
+ my $page_image = $chrome->_content_as_png( undef, { width => $width, height => $height } )->get;
+
+ my $cid = time() . $$;
+ foreach my $canvas_data (@canvases) {
+ $cid++;
+
+ my $coords = $canvas_data->{coords};
+ my $canvas_image = $page_image->crop(
+ left => $coords->{left},
+ top => $coords->{top},
+ width => $coords->{width},
+ height => $coords->{height},
+ );
+ my $canvas_data;
+ $canvas_image->write( data => \$canvas_data, type => 'png' );
+
+ # replace each canvas in the original content with an image tag
+ $content =~ s{<canvas [^>]+>}{<img src="cid:$cid"/>};
+
+ push @parts,
+ MIME::Entity->build(
+ Top => 0,
+ Data => $canvas_data,
+ Type => 'image/png',
+ Encoding => 'base64',
+ Disposition => 'inline',
+ 'Content-Id' => "<$cid>",
+ );
+ }
+ }
+
+ # Shut down chrome if it's a test email from web UI, to reduce memory usage.
+ # Unset $chrome so next time it can re-create a new one.
+ if ( $args{Test} ) {
+ $chrome->close;
+ undef $chrome;
+ }
+ }
+ else {
+ RT->Logger->warn(
+ 'EmailDashboardJSChartImages is enabled but WWW::Mechanize::Chrome is not installed. Install WWW::Mechanize::Chrome to use this feature.'
+ );
+ }
+ }
+
+ $content =~ s{<link rel="shortcut icon"[^>]+/>}{};
+
# Inline the CSS if CSS::Inliner is installed and can be loaded
if ( RT->Config->Get('EmailDashboardInlineCSS') ) {
if ( RT::StaticUtil::RequireModule('CSS::Inliner') ) {
@@ -609,6 +735,8 @@ sub BuildEmail {
}
}

+ $content = ScrubContent($content);
+
my $entity = MIME::Entity->build(
From => Encode::encode("UTF-8", $args{From}),
To => Encode::encode("UTF-8", $args{To}),
diff --git a/sbin/rt-email-dashboards.in b/sbin/rt-email-dashboards.in
index 4b3b008782..8cfbdef3dd 100644
--- a/sbin/rt-email-dashboards.in
+++ b/sbin/rt-email-dashboards.in
@@ -89,8 +89,8 @@ RT::LoadConfig();
# adjust logging to the screen according to options
RT->Config->Set( LogToSTDERR => $opts{log} ) if $opts{log};

-# Disable JS chart as email clients don't support it
-RT->Config->Set( EnableJSChart => 0 );
+# Disable JS chart unless EmailDashboardJSChartImages is true
+RT->Config->Set( EnableJSChart => RT->Config->Get( 'EmailDashboardJSChartImages' ) );

# Disable inline editing as email clients don't support it
RT->Config->Set( InlineEdit => 0 );

-----------------------------------------------------------------------


hooks/post-receive
--
rt
_______________________________________________
rt-commit mailing list
rt-commit@lists.bestpractical.com
https://lists.bestpractical.com/mailman/listinfo/rt-commit