Reporting
Some server-side configuration and client-side routing are required to benefit from the reporting and scheduling features.
Everything is explained in this Confluence article.
The following snippet contains the minimal code that should be added to an ActiveUI-based project to offer reporting capabilities to its end-users.
It relies on the ContentServer.prototype.report()
to trigger the server-side report generation.
import {ActiveUIConsumer, Container} from '@activeviam/activeui-sdk';
import Checkbox from 'antd/lib/checkbox';
import Input from 'antd/lib/input';
import Modal from 'antd/lib/modal';
import Select from 'antd/lib/select';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
const {Option} = Select;
const {confirm} = Modal;
// See https://stackoverflow.com/a/326076.
const isRunningInIframe = window.top !== window.self;
const {location} = window;
// Here we use the browser built-in URLSearchParams to flag and recognize the presence of a bookmark ID in the URL
// (e.g. 1337 in https://customer.delivery.activeviam.com/index.html?bookmarkId=1337).
// This is perfectly suitable to enable the reporting feature in production systems.
//
// If you need to handle other requirements (e.g. logic that needs to listen to URL changes, with queries and hash
// support) in a same project, you may consider using a dedicated routing library such as "react-router" and then
// ensure that it also implements a similar logic (as below) to enable the reporting feature.
const getSearchParams = () => new URLSearchParams(location.search);
const downloadFile = url => {
const link = document.createElement('a');
link.href = url;
link.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const bookmarkIdSearchParam = 'bookmarkId';
const bookmarkIdFromUrl = getSearchParams().get(bookmarkIdSearchParam);
const customFormat = {
value: 'custom',
caption: 'Custom',
};
const builtInPaperFormats = {
letter: 'US letter',
a3: 'A3',
a4: 'A4',
a5: 'A5',
};
const pdfExtension = '.pdf';
class PdfExportModalContent extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
paper: PropTypes.object.isRequired,
};
state = _.pick(this.props, ['name', 'paper']);
handleChange = state => {
this.setState(state, () => this.props.onChange(this.state));
};
renderDimensionInput(dimension) {
return (
<Input
style={{width: '47%'}}
value={this.state.paper[dimension] || ''}
onChange={value => {
this.handleChange({
paper: Object.assign({}, this.state.paper, {[dimension]: value}),
});
}}
placeholder={_.capitalize(dimension)}
/>
);
}
render() {
const isUsingCustomFormat = !this.state.paper.format;
const {name} = this.state;
return (
<div>
<Input
style={{width: '100%', marginBottom: 8}}
addonAfter={pdfExtension}
defaultValue={name}
onChange={({target: {value}}) => {
this.handleChange({name: value});
}}
placeholder="Filename"
/>
<Select
style={{width: '100%', marginBottom: 8}}
onChange={format => {
const paper =
format === customFormat.value
? _.fromPairs(
['height', 'width'].map(dimension => [
dimension,
`${window[`inner${_.capitalize(dimension)}`]}px`,
]),
)
: {format, landscape: true};
this.handleChange({paper});
}}
value={this.state.paper.format || customFormat.value}
>
{<Option key={customFormat.value}>{customFormat.caption}</Option>}
{Object.entries(builtInPaperFormats).map(
([formatValue, formatName]) => (
<Option key={formatValue}>{formatName}</Option>
),
)}
</Select>
{isUsingCustomFormat ? (
<div style={{display: 'flex', width: '100%'}}>
{this.renderDimensionInput('width')}
<div style={{padding: 10}}>x</div>
{this.renderDimensionInput('height')}
</div>
) : null}
{/* The landscape option only makes sense when using a built-in format. */ !isUsingCustomFormat ? (
<Checkbox
style={{width: '100%'}}
onChange={({target: {checked: landscape}}) => {
this.handleChange({
paper: Object.assign({}, this.state.paper, {landscape}),
});
}}
checked={this.state.paper.landscape === true}
>
Landscape
</Checkbox>
) : null}
</div>
);
}
}
const showPdfExportModal = ({activeUI, bookmarkId, bookmarkName}) => {
let details = {
name: bookmarkName,
paper: {
format: 'a4',
landscape: true,
},
};
confirm({
content: (
<PdfExportModalContent
name={details.name}
onChange={newDetails => {
details = newDetails;
}}
paper={details.paper}
/>
),
iconType: 'file-pdf',
maskClosable: false,
okText: 'Export',
async onOk() {
const {name, paper} = details;
const searchParams = getSearchParams();
searchParams.set(bookmarkIdSearchParam, bookmarkId);
const {origin, pathname, hash} = location;
const url = `${origin}${pathname}?${searchParams}${hash}`;
const now = new Date();
const filename = `${name} - ${now.toDateString()} ${now.toLocaleTimeString()}${pdfExtension}`;
const contentServer = activeUI.queries.serversPool.getContentServer(
"https://your.content.server",
);
try {
const {
data: {link},
} = await contentServer.report({
producer: {
type: 'pdf',
payload: {
urls: [url],
paper,
},
},
consumer: {
type: 'download-link',
payload: {
filename,
},
},
});
downloadFile(link);
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
// eslint-disable-next-line no-alert
alert(
[
'Failed to generate the report.',
'Open the Web Console to see the error and make sure you are connected to a Content Server >= 5.6.4 with reporting REST services enabled.',
'See https://support.activeviam.com/confluence/display/AP5/Reporting+and+scheduling',
].join('\n\n'),
);
}
},
title: 'Export Bookmark to PDF',
});
};
class RemoteBookmark extends React.Component {
state = {
apiLoaded: false,
value: null,
};
async componentDidMount() {
const value = await this.props.bookmarksApi.getBookmark(
this.props.bookmarkId,
);
this.setState({value});
}
handleApiChange = () => {
const {onLoad} = this.props;
const {apiLoaded} = this.state;
if (!apiLoaded) {
this.setState({apiLoaded: true}, onLoad);
}
};
render() {
const {bookmarkId} = this.props;
const {value} = this.state;
return value ? (
<Container defaultValue={value} onApiChange={this.handleApiChange} />
) : (
`Loading bookmark ${bookmarkId} ...`
);
}
}
RemoteBookmark.propTypes = {
bookmarkId: PropTypes.string.isRequired,
bookmarksApi: PropTypes.object.isRequired,
onLoad: PropTypes.func.isRequired,
};
const showPdfExportModalPlugin = {
key: 'show-pdf-export-modal',
createProperties(parameters, activeUI) {
return {
isAvailable(payload) {
return _.get(payload, ['node', 'sourceObject', 'type']) === 'container';
},
isDisabled() {
// For the routing logic to work correctly and the PDF output to be good looking,
// we only enable the action when the snippet is running in full page.
return isRunningInIframe;
},
getIconSrcKey() {
return 'menuItem.icon.pdfExport';
},
getCaption() {
return {
text: isRunningInIframe
? 'Open Snippet in Full Page to Export to PDF'
: 'Export to PDF',
};
},
execute(event, payload) {
const {id, name} = payload.node.sourceObject;
showPdfExportModal({activeUI, bookmarkId: id, bookmarkName: name});
},
};
},
};
const bookmarkTree = {
'bookmark-tree.actions': [],
'bookmark-tree.handlers.contextmenu': [
showPdfExportModalPlugin.key,
'load-bookmark',
'create-folder',
'edit-bookmark',
'delete-bookmark',
],
containerKey: 'bookmark-tree',
showTitleBar: true,
};
const dashboard = {
showTitleBar: true,
body: {
layout: {
direction: 'row',
children: {
0: {
size: 0.2,
ck: 'bookmark-tree',
},
1: {},
},
},
content: [
{
key: 'bookmark-tree',
bookmark: {
name: 'Bookmarks',
type: 'container',
value: bookmarkTree,
writable: false,
},
},
],
},
containerKey: 'dashboard',
'dashboard.quickActions': [
'activemonitor-messages',
'activemonitor-alerts',
'undo',
'redo',
'reset-to-default-view',
'add-widget',
],
};
const App = () =>
bookmarkIdFromUrl ? (
<ActiveUIConsumer>
{activeUI => (
<RemoteBookmark
bookmarksApi={activeUI.getBookmarksApi("https://your.content.server")}
bookmarkId={bookmarkIdFromUrl}
onLoad={async () => {
await activeUI.widgets.waitUntilAllLoaded();
// Now that all the widgets are loaded, we let the reporting service capture the PDF.
window.renderComplete = true;
}}
/>
)}
</ActiveUIConsumer>
) : (
<Container
defaultValue={{
name: 'PDF Export Demo',
value: dashboard,
writable: true,
}}
/>
);
export {showPdfExportModalPlugin};
export default App;
import React from 'react';
import {render} from 'react-dom';
import {createActiveUI, ActiveUIProvider} from '@activeviam/activeui-sdk';
import App, {showPdfExportModalPlugin} from './App';
const activeUI = createActiveUI({
plugins: {action: [showPdfExportModalPlugin]},
});
const servers = activeUI.queries.serversPool;
const contentServer = servers.addContentServer({url: "https://your.content.server"});
servers.addActivePivotServer({url: "https://your.activepivot.server", contentServer});
render(
<ActiveUIProvider activeUI={activeUI}>
<App />
</ActiveUIProvider>,
document.getElementById('root'),
);
<div id="root"></div>
<script>
// Rendering flag used by the reporting service to know when it's fine to capture the PDF.
// Will be set to `true` in `App.js`.
window.renderComplete = false;
</script>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-variant: tabular-nums;
}
.CodeMirror pre {
/* Using !important because CodeMirror will apply its own style after but we want to force this font stack. */
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace !important;
}
body {
margin: 0;
}
html,
body,
#root {
height: 100%;
}