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

Container

Containers

<Container /> is a React component used to render widgets provided by ActiveUI SDK such as tabular views, charts, rich-text editors, etc. Custom widgets can be rendered by creating a container plugin.

Container Component

<Container /> expects these props. Here's how to use <Container /> to render a tabular view:

App.js
index.js
body.html
style.css
import {Container} from '@activeviam/activeui-sdk';
import React from 'react';

const mdx = `SELECT FROM [EquityDerivativesCube]`;

const App = () => (
  <Container
    defaultValue={{
      name: 'Simple Tabular View',
      value: {
        body: {
          mdx,
        },
        containerKey: 'tabular-view',
        showTitleBar: true,
      },
    }}
  />
);

export default App;
import React from 'react';
import {render} from 'react-dom';
import {createActiveUI, ActiveUIProvider} from '@activeviam/activeui-sdk';

import App from './App';

const activeUI = createActiveUI();

const servers = activeUI.queries.serversPool;
servers.addActivePivotServer({url: "https://your.activepivot.server"});

render(
  <ActiveUIProvider activeUI={activeUI}>
    <App />
  </ActiveUIProvider>,
  document.getElementById('root'),
);
<div id="root"></div>
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%;
}

Like shown in the example above, <Container /> should always be rendered as a React descendant of <ActiveUIProvider />.

The shape of the defaultValue/value props reflects the JSON data structure stored on the Content Server when saving a widget as a bookmark. There are several ways to create this data structure:

  • Look for snippets doing something similar to what you're trying to achieve and start from their value for these props.
  • Tweak the default configuration of the corresponding container.
  • Fiddle with a container in the UI to bring it to the desired state, save it as a bookmark, and grab the corresponding JSON representation.

The controlled mode is the most "React idiomatic" one as it's declarative: the current value prop always reflects the internal widget configuration and/or query.

However, there is currently a technical limitation when using a <Container /> in controlled mode: it will reload itself every time the value prop changes. Like when you use the undo/redo feature.

If the container has an underlying query, it will be stopped and restarted at every reload. When there are no underlying queries or when there is one but it's fast to load, the container will seem to "blink" when it reloads.

If this current limitation is not acceptable in your project, you can fallback to the uncontrolled mode with the defaultValue/onApiChange props.

Custom Container

When Should I Create A Custom Container?

Before getting into the detail of how to register a custom container, let's review which use cases custom containers solve and which it does not.

You should create a custom container if

  • You want to create a custom widget that will be displayed in dashboards.
  • Some attributes of this widget (like an underlying MDX query) should be savable and loadable via a bookmark.
  • Your widget needs to interact with other widgets in a dashboard.
  • Your widget is responsive and displays itself according to a size chosen by the user so that it can be in a Dashboard.

You should NOT create a custom container if

  • You want to create an always full-screen view, like a standalone application; in this case create a dedicated React component for your application.
  • You want separate parts of your widget to be saved separately; in this case create one container for each independent part and use a dashboard to lay them out.
  • You want to create a new type of chart; in this case create and register a new chart implementation.
  • You want to display a tabular view, a pivot table or a chart with a lot of customization. In this case directly use the built-in SDK container and change its configuration through the value/defaultValue props.
  • You want to display a custom web page inside a dashboard. In this case use the built-in HTTP container.

Usage

You can register a custom container by implementing the container plugin. Here's an example:

App.js
index.js
body.html
style.css
import {ActiveUIConsumer, Container} from '@activeviam/activeui-sdk';
import InputNumber from 'antd/lib/input-number';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';

const getFeaturedValuesMdx = threshold => `WITH
Member [Measures].[Threshold] AS ${threshold}
SELECT
  [Measures].[Threshold] ON COLUMNS
  FROM [EquityDerivativesCube]`;

const getChartMdx = threshold => `WITH
Member [Measures].[Threshold] AS ${threshold}
SELECT
  {
    [Measures].[contributors.COUNT],
    [Measures].[Threshold]
  } ON COLUMNS,
  NON EMPTY [Geography].[City].[City].Members ON ROWS
  FROM [EquityDerivativesCube]`;

const getChartValue = (value, threshold) =>
  _.set(
    _.cloneDeep(value),
    ['value', 'body', 'layers', '0', 'query', 'mdx'],
    getChartMdx(threshold),
  );

const headers = [
  {value: 'x', caption: 'X', numeric: true},
  {value: 'y', caption: 'Y', numeric: true},
  {value: 'count', caption: 'Count', numeric: true},
  {value: 'category', caption: 'Category', numeric: false},
];

const content = [
  [2, 3, 10, 'A'],
  [3, 1, 20, 'A'],
  [1, 5, 13, 'B'],
  [4, 2, 23, 'B'],
  [1, 3, 18, 'B'],
];

// This component shows two different ways to interact with child containers:
// - The featured values container is uncontrolled: we give it a `defaultValue` prop.
//   To make it react to threshold changes, we use its widget API.
// - The chart container is controlled: we give it a  `value` prop.
//   To make it react to threshold changes, we pass a new `value`.
// See the "Container" page in the docs for more information on this.
class CustomComponent extends React.Component {
  state = {
    chart: {
      value: {
        actions: ['clear-dock', 'save-as', 'remove-dock'],
        body: {
          layers: [
            {
              query: {
                mdx: getChartMdx(100),
                updateMode: 'once',
              },
              configuration: {
                elementStylers: ['selection-highlight'],
                type: 'combo-line',
                mapping: {
                  x: {from: ['[Geography].[City].[City]']},
                  y: {from: 'Value'},
                  split: {
                    from: ['numeric'],
                    numericMembers: [
                      '[Measures].[contributors.COUNT]',
                      '[Measures].[Threshold]',
                    ],
                  },
                },
              },
            },
          ],
        },
        containerKey: 'chart',
        showTitleBar: false,
      },
    },
  };

  componentDidUpdate(prevProps) {
    const threshold = this.getThreshold();
    if (
      prevProps.value.threshold !== threshold &&
      this.featuredValuesApi &&
      this.featuredValuesApi.getQuery()
    ) {
      this.featuredValuesApi.getQuery().setMdx(getFeaturedValuesMdx(threshold));
    }
  }

  getThreshold = () => this.props.value.threshold || 100;

  render() {
    const {onChange, table} = this.props;

    const threshold = this.getThreshold();

    return (
      <div
        style={{
          display: 'flex',
          height: '100%',
        }}
      >
        <div
          style={{
            display: 'flex',
            flexDirection: 'column',
            height: '100%',
            width: '50%',
          }}
        >
          <div
            style={{
              height: '50%',
            }}
          >
            <Container
              childKey="tabular-view"
              defaultValue={{
                value: {
                  containerKey: 'static-tabular-view',
                  showTitleBar: false,
                },
              }}
              onApiChange={staticTabularViewApi => {
                staticTabularViewApi.setTable(table);
              }}
            />
          </div>
          <div
            style={{
              display: 'flex',
              flexGrow: 1,
              justifyContent: 'space-around',
            }}
          >
            <h3>Chart Threshold:</h3>
            <InputNumber
              autoFocus={true}
              onChange={newThreshold => {
                onChange({threshold: newThreshold});
              }}
              size="large"
              step={10}
              value={threshold}
            />
            <Container
              childKey="featured-values"
              defaultValue={{
                value: {
                  body: {
                    configuration: {
                      featuredValues: {
                        shouldRenderWizard: false,
                      },
                    },
                    mdx: getFeaturedValuesMdx(threshold),
                    updateMode: 'once',
                  },
                  containerKey: 'featured-values',
                  showTitleBar: false,
                },
              }}
              onApiChange={featuredValuesApi => {
                this.featuredValuesApi = featuredValuesApi;
              }}
            />
          </div>
        </div>
        <div
          style={{
            height: '100%',
            width: '50%',
          }}
        >
          <Container
            childKey="chart"
            value={getChartValue(this.state.chart, threshold)}
            onChange={newChartValue => {
              this.setState({chart: newChartValue});
            }}
          />
        </div>
      </div>
    );
  }
}

CustomComponent.propTypes = {
  containerApi: PropTypes.object.isRequired,
  onChange: PropTypes.func.isRequired,
  table: PropTypes.object.isRequired,
  value: PropTypes.object.isRequired,
};

const customContainerPlugin = {
  key: 'custom',
  staticProperties: {
    component: props => (
      <ActiveUIConsumer>
        {({data: {toTable}}) => (
          <CustomComponent {...props} table={toTable(headers, content)} />
        )}
      </ActiveUIConsumer>
    ),
  },
};

const App = () => (
  <Container
    defaultValue={{
      name: 'Custom Container',
      value: {
        containerKey: customContainerPlugin.key,
        showTitleBar: true,
      },
    }}
  />
);

export {customContainerPlugin};

export default App;
import React from 'react';
import {render} from 'react-dom';
import {createActiveUI, ActiveUIProvider} from '@activeviam/activeui-sdk';

import App, {customContainerPlugin} from './App';

const activeUI = createActiveUI({
  async fetchTranslation(locale, defaultFetchTranslation) {
    const translation = await defaultFetchTranslation();
    translation.bookmarks.new[customContainerPlugin.key] = {
      title: 'Custom Container',
      description: 'A container to play with',
      prefix: '',
      genitive: '',
    };
    return translation;
  },
  plugins: {container: [customContainerPlugin]},
});

const servers = activeUI.queries.serversPool;
servers.addActivePivotServer({url: "https://your.activepivot.server"});

render(
  <ActiveUIProvider activeUI={activeUI}>
    <App />
  </ActiveUIProvider>,
  document.getElementById('root'),
);
<div id="root"></div>
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%;
}

What can you do with this container implementation?

  • The value/onChange props can be used to persist some serializable state of the container when saving it as a bookmark.
  • Your container can render other children containers in a React friendly way.
  • You can act on your children to read or change their configuration/query through their own container API.

Integration to Dashboard Filters and Context Values

Your custom container might run queries against ActivePivot. These queries can either:

  • Be created by other containers rendered as children of your custom container. In this case, the dashboard filters and context values will impact these queries automatically.
  • Be handled manually (you display their results in a custom way for instance). You can then opt to have them impacted by the dashboard filters and context values by calling this.props.containerApi.bindQuery. Check the following example:
App.js
index.js
body.html
style.css
import {Container, ActiveUIConsumer} from '@activeviam/activeui-sdk';
import Avatar from 'antd/lib/avatar';
import Card from 'antd/lib/card';
import Icon from 'antd/lib/icon';
import PropTypes from 'prop-types';
import React from 'react';

const {Meta} = Card;

const hierarchy = '[Geography].[City]';
const measureName = 'pnl.SUM';

class SmileyContainer extends React.Component {
  state = {
    cities: [],
    loaded: false,
    pnl: null,
  };

  async componentDidMount() {
    this.query = this.props.activePivotServer.createSelectQueryFromCube(
      'EquityDerivativesCube',
    );
    const removeListener = this.query.addListener(results => {
      this.setState({
        loaded: true,
        pnl: results.content[0][0] || {caption: 'N/A', value: null},
      });
    });
    this.props.containerApi.bindQuery(this.query);
    this.disposeQuery = () => {
      removeListener();
      this.query.dispose();
    };
    const cities = await this.fetchCities();
    this.setState({cities}, () => {
      this.query.setMdx(this.getMdx(this.getCityIndex()));
      this.changeQueryUpdateMode(this.props.value.realTime);
    });
  }

  componentWillUnmount() {
    if (this.disposeQuery) {
      this.disposeQuery();
    }
  }

  componentDidUpdate(prevProps) {
    const {cityIndex, realTime} = this.props.value;
    if (prevProps.value.realTime !== realTime) {
      this.changeQueryUpdateMode(realTime);
    }
    if (prevProps.value.cityIndex !== cityIndex) {
      this.query.setMdx(this.getMdx(cityIndex));
    }
  }

  fetchCities = async () => {
    const {
      axes: [{positions}],
    } = await this.props.activePivotServer
      .createSelectQuery(
        `SELECT
      NON EMPTY ${hierarchy}.[City].Members ON ROWS
      FROM [EquityDerivativesCube]`,
      )
      .executeOnceAndGetPromise();
    return positions.map(
      ([
        {
          captionPath: [, city],
        },
      ]) => city,
    );
  };

  changeQueryUpdateMode = realTime => {
    this.query.start(realTime !== false ? 'realTime' : 'once');
    this.forceUpdate();
  };

  getSmiley = () => {
    if (!this.state.pnl) {
      return 'loading';
    }
    const {value} = this.state.pnl;
    if (!value) {
      return 'meh';
    }
    return value > 0 ? 'smile' : 'frown';
  };

  getCityIndex = () => this.props.value.cityIndex || 0;

  getMdx = cityIndex => `SELECT [Measures].[${measureName}] ON COLUMNS
      FROM [EquityDerivativesCube]
      WHERE ${hierarchy}.[ALL].[AllMember].[${this.state.cities[cityIndex]}]`;

  getCityFromMdx = () => {
    const {mdxApi} = this.props;

    const statementAndDiscovery = mdxApi.getSnd(this.query.getDataSource());
    const selector = mdxApi.filters.selectors.HierarchySelector.create({
      hierarchy,
    });
    const [
      {
        value: {
          members: [cityUniqueName],
        },
      },
    ] = mdxApi.filters.selectFilters(statementAndDiscovery, selector);
    return mdxApi.base.unquote(cityUniqueName)[1];
  };

  render() {
    const {onChange, value} = this.props;
    const {cities, loaded, pnl} = this.state;

    const shiftCityIndex = offset =>
      onChange(
        Object.assign({}, value, {
          cityIndex:
            (this.getCityIndex() + cities.length + offset) % cities.length,
        }),
      );
    const isRealtime = this.query && this.query.getUpdateMode() === 'realTime';

    return (
      <div className="smiley-container">
        <Card
          actions={[
            <Icon
              type="left"
              title="Previous City"
              onClick={() => shiftCityIndex(-1)}
            />,
            <Icon
              type={isRealtime ? 'pause' : 'caret-right'}
              title={isRealtime ? 'Pause' : 'Enable real-time'}
              onClick={() =>
                onChange(Object.assign({}, value, {realTime: !isRealtime}))
              }
            />,
            <Icon
              type="right"
              title="Next City"
              onClick={() => shiftCityIndex(+1)}
            />,
          ]}
          loading={!loaded}
          style={{width: 300}}
        >
          <Meta
            avatar={<Avatar icon={this.getSmiley()} size="large" />}
            title={loaded && this.getCityFromMdx()}
            description={`${measureName}: ${loaded && pnl.caption}`}
          />
        </Card>
      </div>
    );
  }
}

SmileyContainer.propTypes = {
  activePivotServer: PropTypes.object.isRequired,
  containerApi: PropTypes.object.isRequired,
  mdxApi: PropTypes.object.isRequired,
  onChange: PropTypes.func.isRequired,
  value: PropTypes.object.isRequired,
};

const smileyIconPlugin = {
  key: 'smiley',
  createProperties() {
    return {
      render(palette, props) {
        return (
          <svg viewBox="64 64 896 896" {...props}>
            <path d="M288 421a48 48 0 1 0 96 0 48 48 0 1 0-96 0zm352 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 0 1 248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 0 1 249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 0 1 775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 0 1 775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 0 0-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 0 0-8-8.4z" />
          </svg>
        );
      },
    };
  },
};

const smileyContainerPlugin = {
  key: 'smiley',
  staticProperties: {
    component: props => (
      <ActiveUIConsumer>
        {({mdx: mdxApi, queries: {serversPool}}) => (
          <SmileyContainer
            {...props}
            activePivotServer={serversPool.getActivePivotServer(
              "https://your.activepivot.server",
            )}
            mdxApi={mdxApi}
          />
        )}
      </ActiveUIConsumer>
    ),
    iconKey: smileyIconPlugin.key,
  },
};

const App = () => (
  <Container
    defaultValue={{
      name: 'Dashboard With Smiley Container',
      value: {
        showTitleBar: true,
        body: {
          layout: {
            direction: 'row',
            children: {
              0: {
                size: 0.8,
                direction: 'column',
                children: {
                  0: {
                    size: 0.1,
                    ck: 'filters',
                  },
                  1: {
                    ck: 'smiley',
                  },
                },
              },
              1: {
                ck: 'editor',
              },
            },
          },
          content: [
            {
              key: 'filters',
              bookmark: {
                name: 'Dashboard Filters',
                type: 'container',
                value: {
                  containerKey: 'filters',
                  showTitleBar: true,
                },
                writable: false,
              },
            },
            {
              key: 'smiley',
              bookmark: {
                name: 'Smiley Container',
                type: 'container',
                value: {
                  actions: [
                    'save',
                    'save-as',
                    'toggle-filters',
                    'clear-dock',
                    'toggle-dock-title-bar',
                    'remove-dock',
                  ],
                  containerKey: smileyContainerPlugin.key,
                  showTitleBar: true,
                },
                writable: false,
              },
            },
            {
              key: 'editor',
              bookmark: {
                name: 'MDX Editor',
                type: 'container',
                value: {
                  containerKey: 'mdx-editor',
                  showTitleBar: true,
                },
                writable: false,
              },
            },
          ],
        },
        containerKey: 'dashboard',
      },
    }}
  />
);

export {smileyContainerPlugin, smileyIconPlugin};

export default App;
import React from 'react';
import {render} from 'react-dom';
import {createActiveUI, ActiveUIProvider} from '@activeviam/activeui-sdk';

import App, {smileyContainerPlugin, smileyIconPlugin} from './App';

const activeUI = createActiveUI({
  async fetchTranslation(locale, defaultFetchTranslation) {
    const translation = await defaultFetchTranslation();
    translation.bookmarks.new[smileyContainerPlugin.key] = {
      title: 'Smiley Container',
      description: 'A container to play with pnl.SUM',
      prefix: '',
      genitive: '',
    };
    return translation;
  },
  plugins: {container: [smileyContainerPlugin], icon: [smileyIconPlugin]},
});

const servers = activeUI.queries.serversPool;
servers.addActivePivotServer({url: "https://your.activepivot.server"});

render(
  <ActiveUIProvider activeUI={activeUI}>
    <App />
  </ActiveUIProvider>,
  document.getElementById('root'),
);
<div id="root"></div>
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%;
}

.smiley-container .ant-card-actions {
  margin-bottom: 0px !important;
}

.smiley-container .ant-card-actions li {
  box-sizing: border-box !important;
}
← Configuring Widget Handlers and ActionsCustom UI components with Ant Design →
  • Containers
  • Container Component
  • Custom Container
    • When Should I Create A Custom Container?
    • Usage
  • Integration to Dashboard Filters and Context Values
Copyright © 2021 ActiveViam