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 {useActiveUI, 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';
function DimensionInput(props) {
return (
<Input
style={{width: '47%'}}
value={props.paper[props.dimension] || ''}
onChange={(e) => {
const value = e.target.value;
props.onChange((s) => ({...s, [props.dimension]: value}));
}}
placeholder={_.capitalize(props.dimension)}
/>
);
}
DimensionInput.propTypes = {
dimension: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
paper: PropTypes.object.isRequired,
};
function PdfExportModalContent(props) {
const [name, setName] = React.useState(props.name);
const [paper, setPaper] = React.useState(props.paper);
const onChange = props.onChange;
React.useEffect(() => {
onChange({
name,
paper,
});
}, [paper, name, onChange]);
const isUsingCustomFormat = !paper.format;
return (
<div>
<Input
style={{width: '100%', marginBottom: 8}}
addonAfter={pdfExtension}
defaultValue={name}
onChange={(e) => setName(e.target.value)}
placeholder="Filename"
/>
<Select
style={{width: '100%', marginBottom: 8}}
onChange={(format) => {
setPaper(
format === customFormat.value
? _.fromPairs(
['height', 'width'].map((dimension) => [
dimension,
`${window[`inner${_.capitalize(dimension)}`]}px`,
]),
)
: {format, landscape: true},
);
}}
value={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%'}}>
<DimensionInput
dimension={'width'}
onChange={setPaper}
paper={paper}
/>
<div style={{padding: 10}}>x</div>
<DimensionInput
dimension={'height'}
onChange={setPaper}
paper={paper}
/>
</div>
) : null}
{
/* The landscape option only makes sense when using a built-in format. */ !isUsingCustomFormat ? (
<Checkbox
style={{width: '100%'}}
onChange={(e) => {
const checked = e.target.checked;
setPaper((s) => ({...s, landscape: checked}));
}}
checked={paper.landscape}
>
Landscape
</Checkbox>
) : null
}
</div>
);
}
PdfExportModalContent.propTypes = {
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
paper: PropTypes.object.isRequired,
};
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}
/>
),
icon: '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',
});
};
function RemoteBookmark(props) {
const [value, setValue] = React.useState(null);
const {bookmarkId} = props;
const activeUI = useActiveUI();
React.useEffect(() => {
async function getBookmark() {
const bookmarksApi = activeUI.getBookmarksApi("https://your.content.server");
const newValue = await bookmarksApi.getBookmark(bookmarkId);
setValue(newValue);
}
getBookmark();
}, [activeUI, bookmarkId]);
return value ? (
<Container defaultValue={value} />
) : (
`Loading bookmark ${bookmarkId} ...`
);
}
RemoteBookmark.propTypes = {
bookmarkId: PropTypes.string.isRequired,
};
export 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',
'separator',
'rename-bookmark',
'move-bookmark',
'separator',
'delete-bookmark',
],
containerKey: 'bookmark-tree',
showTitleBar: true,
};
const dashboard = {
showTitleBar: true,
body: {
pages: [
{
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,
},
},
],
name: 'Page 1',
},
],
},
containerKey: 'dashboard',
};
export function App() {
const activeUI = useActiveUI();
return bookmarkIdFromUrl ? (
<RemoteBookmark
bookmarksApi={activeUI.getBookmarksApi("https://your.content.server")}
bookmarkId={bookmarkIdFromUrl}
/>
) : (
<Container
defaultValue={{
name: 'PDF Export Demo',
value: dashboard,
writable: true,
}}
/>
);
}
import React from 'react';
import {render} from 'react-dom';
import {createActiveUI, ActiveUIProvider} from '@activeviam/activeui-sdk';
import {App, showPdfExportModalPlugin} from './App';
const dataWidgetsActions = [
'toggle-wizard',
'toggle-dock-title-bar',
'remove-dock',
];
const activeUI = createActiveUI({
defaultSettings: {
'chart.showWizard': true,
'pivot-table.showWizard': true,
'tabular-view.showWizard': true,
'drillthrough.showWizard': true,
'chart.actions': dataWidgetsActions,
'featured-values.actions': dataWidgetsActions,
'pivot-table.actions': dataWidgetsActions,
'tabular-view.actions': dataWidgetsActions,
'drillthrough.actions': dataWidgetsActions,
'featured-values.showWizard': true,
'dashboard.quickActions': ['undo', 'redo', 'add-widget'],
},
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%;
}