ActiveUI

ActiveUI

  • User Guide
  • Developer Documentation

›Guides

About

  • Introduction
  • Changelog

Getting Started

  • Step by Step
  • Development Environment
  • Artifacts
  • ActiveUI Application
  • Usage as an npm Dependency
  • Initialization
  • Project Architecture

Guides

  • Adding Servers
  • Authentication
  • Bookmark favorites
  • Charts
  • Configuring Widget Handlers and Actions
  • Container
  • Custom UI components with Ant Design
  • Data manipulation
  • Debugging
  • Deployment
  • Internationalization
  • MDX Manipulation
  • Plugins
  • Reporting
  • Settings
  • Tabular View and Pivot Tables
  • Testing

Reference

  • SDK API
  • Default Widget Configurations
  • Plugins
  • Settings

Advanced

  • Content Server Setup
  • Experimental Features
  • Maven Integration
  • Offline Installation
  • Script-based Integration

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.

App.js
index.js
body.html
style.css
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%;
}
← PluginsSettings →
Copyright © 2021 ActiveViam