Basic Container

Simple API to Build Custom Containers

Containers #

The Container interface describes a type of widget that can be displayed in ActiveUI, as well as the mechanism to load and save it into a bookmark.

The core product contains a dozen of containers, one for the TabularView, one for the Charts, one to display an iframe and so on… Containers are plugin values, so it is possible to write new containers in a project. You could do this using the Container interface, but this interface uses a very low level API very different from the imperative API used in snippets to build charts and tabular. This is why it is recommended not to use this interface and instead implement the BasicContainer builder.

The BasicContainer interface follows the same concept than extending BasicPostProcessor instead of implementing IPostProcessor, a lot of boilerplate technicalities are hidden and it provides a simpler, safer API to build components.

When Should I Create A New Container? #

Before getting into the detail of how to use this API, let’s review which use cases does the Container API solve and which one it doesn’t.

You Should Create A Container If #

  • You want to create a 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 Container If #

  • You want to create an always full-screen view, like a standalone application; in this case create a dedicated HTML application using ActiveUI Api
  • You want separate parts of your view to be saved separately; in this case create one container for each independent part and use a Dashboard to display them.
  • You want to create a new type of Chart; in this case create and register a new chart implementation
  • You want to display a TabularView, a PivotTable or a Chart with a lot of customization. In this case use directly the already existing core container and change the configuration of the view via the settings
  • You want to display a custom web page inside a dashboard. In this case use directly the HTTP container.

Usage of BasicContainer #

You can create a custom container using the registerBasicContainer function from the PluginsApi, which takes an object of type BasicContainerPluginDescription. Here’s a simple example:

activeui.jsscript.jsbody.htmlstyle.css
const containerKey = 'my-basic-container';
const activeUI = ActiveUI.initialize({
  fetchTranslation(locale, defaultFetchTranslation) {
    return defaultFetchTranslation().then(translation => {
      translation.bookmarks.new[containerKey] = {
        title: containerKey,
        description: 'A tabular view to play with',
      };
      return translation;
    });
  },
});
const servers = activeUI.queries.serversPool;
servers.addActivePivotServer({url: 'http://url/of/your/activepivot/server'});

export {activeUI, containerKey};

import React from 'react';

import {activeUI, containerKey} from './activeui';

class MyContainer extends React.Component {
  render() {
    const widgets = this.props.activeUIApi.widgets;
    return (
      <div className="tabularHolder">
        <widgets.TabularView name="tab1" />
      </div>
    );
  }
}

MyContainer.propTypes = {
  activeUIApi: React.PropTypes.object.isRequired,
};

activeUI.plugins.registerBasicContainer({
  key: containerKey,
  component: MyContainer,
  initialConfiguration: {
    containerKey,
    actions: [],
    body: {
      children: {
        tab1: {
          containerKey: 'tabular-view',
          body: {
            mdx: 'SELECT FROM EquityDerivativesCube',
            serverUrl: '',
            // The default configuration of the widgets can be found in the documentation,
            // under Guides > Default Configurations
            configuration: {tabular: {handlers: {}}},
          },
        },
      },
    },
  },
});

activeUI.widgets
  .createBasicContainer()
  .withKey(containerKey)
  .within('app');

<div id="app"></div>

body {
  margin: 0;
  width: 100%;
}

html,
body,
#app {
  height: 100%;
}

.tabularHolder {
  margin: 5px;
  height: 100%;
  width: 600px;
  display: flex;
  align-items: center;
}

What can your user do with this container implementation?

  • The state of the React component is saved in the bookmark when the user saves your container after having made changes
  • The state loaded from a bookmark is put in the state of your component when loaded back
  • Your component can contain any ActiveUI core component as children components in a React friendly way
  • The state of all your children is also stored in the bookmark; so that changes to the query or configuration of your container children views are maintained across saves
  • You can act on your children to read or change their configuration/query the same way than in the ActiveUI API
  • You can have your own logic run every time the user loads the bookmark
  • Unloading also can be customized to reverse what was done on load.

The React Component you give as input to the builder has no special lifecycle methods to implement nor mixin to use. In order to provide the features listed above it has the following characteristics:

Initial configuration #

An initial configuration can be specified for the basic container. This acts as an initial bookmark that will be used for your container, the first time that the user creates it. You can use that to set some initial state for your component or widgets that you are creating inside. This can be used for example to always have a table start with the same query in your container.

State constraint #

The first characteristic is the only constraint of the Basic Container API: everything you store in the state of your React Component must be plain JavaScript objects, i.e. you cannot store functions in the React state. This is because the component state will automatically be saved in the bookmarks so it need to have a JSON representation. You can write any function you want in your React component so there should be no need to store them in the React state.

Specific ActiveUI API #

When using the Basic Container API, your React component receive in its props an activeUIApi object. The first argument of its load static method is the same activeUIApi object. This activeUIApi object represents ActiveUI API but is not the same as the one retrieved from ActiveUI.initialize(). You should only use this one inside your React component and inside your load method.

This activeUIApi is made to be easy to use from inside your React component, as opposed to the createX.with(...).within() methods on the API created with ActiveUI.initialize().

It contains the following base React components

  • activeUIApi.widgets.base.Button
  • activeUIApi.widgets.base.Checkbox
  • activeUIApi.widgets.base.DropDownMenu
  • activeUIApi.widgets.base.Slider
  • activeUIApi.widgets.base.TextField

These pure React components can be used as any non-ActiveUI React component, they just provide a consistent UI following the theme so you should use them over bare React components.

activeUIApi also contains the more advanced React components

  • activeUIApi.widgets.Chart
  • activeUIApi.widgets.PivotTable
  • activeUIApi.widgets.SingleValue
  • activeUIApi.widgets.TabularView
  • activeUIApi.widgets.StaticSingleValue
  • activeUIApi.widgets.StaticTabularView

When using any of these components, one just has to give a name to it as a prop. The name given here is like a key for this child: each child of your React component should have a different key. This key can be used in the load method or in any event handler of the React component to interact with this child component exactly the same way than in the imperative API.

So for instance if in your React component render method you have a child like <activeUIApi.widgets.Chart name="chart1"/>, you can in the load or on onClick handler change its query with activeUIApi.widgets.getChart('chart1').getQuery().setMdx(newMdx).

For all the advanced React component, the API you can use to interact with them has the same path than the component type except that we use a lower case type and take as parameter the name of the component.

Static Load Method #

The last characteristic of the custom containers using this API is the static load method. This method is called when a user loads your container from a bookmark or from an empty dock. Unlike the constructor, componentWillMount and componentDidMount lifecycle methods of your component, the static load method will not be called when you drag and drop the container around the dashboard. It is responsible of initializing your container according to your container logic. If for instance your container wants to display data coming from a REST query this is the perfect place to perform the REST call and then populate one of your children.

You will notice that in the sample the start of the query is surrounded by a if (tabular.getQuery() !== undefined) { /*...*/}. This is here because when the user will save your bookmark we will save the state of the child tabular view with it, including the query. The first time the user displays this container starting from an empty view for instance the load method will actually create the query to the server and plug it to the tabular view. If the user then adds new columns then saves it in a bookmark then load again this bookmark later on we will load and start the updated query for this tabular, simplifying the work of the load. If your use case requires you to control more what will be done in the container you can of course override what was stored in the bookmark at this stage and change the MDX query or the configuration. If your container reads data from a REST endpoint this is a good idea to perform the REST call every time the container loads (the basic container API will anyway not store static data in the bookmark so you will have to perform a query again).

This method is optional. There is also an optional corresponding unload method with the same signature.

An Exhaustive Example #

We can see below another snippet making use of all the features described above. It features:

  • multiple children
  • one child (the TabularView) has static data
  • a state (this.state.threshold)
  • a UI to change the component state
  • the use of the API to modify the queries via the container UI
activeui.jsscript.jsbody.htmlstyle.css
import ActiveUI from 'activeui';

const containerKey = 'my-basic-container';
const activeUI = ActiveUI.initialize({
  fetchTranslation(locale, defaultFetchTranslation) {
    return defaultFetchTranslation().then(translation => {
      translation.bookmarks.new[containerKey] = {
        title: containerKey,
        description: 'A widget to play with',
      };
      return translation;
    });
  },
});

const servers = activeUI.queries.serversPool;
servers.addActivePivotServer({url: 'http://url/of/your/activepivot/server'});

export {activeUI, containerKey};

import React from 'react';

import {activeUI, containerKey} from './activeui';

const mdxThresholdChart = 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 tabular1 = 'tabular1';
const singleValue1 = 'singleValue1';
const chart1 = 'chart1';

class MyContainer extends React.Component {
  constructor(...args) {
    super(...args);
    this.state = {threshold: 100};
  }

  setThreshold(newThreshold) {
    this.setState({threshold: Number.parseInt(newThreshold, 10)});
    const query = this.props.activeUIApi.widgets
      .getChart('chart1')
      .layer(0)
      .getQuery();
    const oldMdx = query.getMdx();
    const newMdx = oldMdx.replace(/AS \d+/, `AS ${newThreshold}`);
    query.setMdx(newMdx);
  }

  render() {
    const widgets = this.props.activeUIApi.widgets;
    return (
      <div
        style={{
          border: '1px dashed blue',
          display: 'flex',
          height: '100%',
          boxSizing: 'border-box',
        }}
      >
        <div>
          <div
            style={{
              width: 400,
              height: 200,
              border: '1px dashed green',
              margin: 4,
            }}
          >
            <widgets.StaticTabularView name={tabular1} />
          </div>
          <div
            style={{
              width: 400,
              height: 100,
              border: '1px dashed green',
              margin: 4,
              display: 'flex',
            }}
          >
            <div
              style={{
                border: '1px dashed cyan',
                margin: 5,
                display: 'flex',
                alignItems: 'center',
              }}
            >
              <widgets.SingleValue name={singleValue1} />
            </div>
            <div style={{border: '1px dashed cyan', margin: 5}}>
              Threshold:
              <widgets.base.TextField
                value={this.state.threshold}
                onChange={v => this.setThreshold(v)}
                title="Threshold"
              />
              <widgets.base.Button label="+" onClick={() => this.setThreshold(this.state.threshold + 1)} />
            </div>
          </div>
        </div>
        <div
          style={{
            width: 400,
            height: 305,
            border: '1px dashed green',
            margin: 4,
          }}
        >
          <widgets.Chart name={chart1} />
        </div>
      </div>
    );
  }
}

MyContainer.propTypes = {
  activeUIApi: React.PropTypes.object.isRequired,
};

MyContainer.load = (activeUIApi, bookmarkState) => {
  // Define data headers and tuples
  const dataHeaders = [
    {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 tuples = [[2, 3, 10, 'A'], [3, 1, 20, 'A'], [1, 5, 13, 'B'], [4, 2, 23, 'B'], [1, 3, 18, 'B']];
  // Convert to a Table:
  const table = activeUI.data.toTable(dataHeaders, tuples);
  activeUIApi.widgets.getStaticTabularView(tabular1).setData(table);
  // The method can return a promise resolving to a state that will be accessible in the component savedState prop.
  // return Promise.resolve(Object.assign({}, bookmarkState, {newKey: 'value'}));
};

// Configure the initial configuration of widgets
// The default configuration of the widgets can be found in the documentation, under Guides > Default Configurations

const staticTabular1Configuration = {
  // We do not save the data in the bookmark, and instead generate it everytime we load the container. This
  // data could for example come from a remote server and we do not care storing it in the bookmark
  storeData: false,
  handlers: {},
};

const singleValue1Configuration = {
  singleValue: {
    handlers: {},
  },
};

const chart1Configuration = {
  layers: [
    {
      query: {
        mdx: mdxThresholdChart(100),
        serverUrl: '',
      },
      configuration: {
        type: 'combo-line',
        mapping: {
          x: {from: ['[Geography].[City].[City]']},
          y: {from: 'Value'},
          split: {
            from: ['numeric'],
            numericMembers: ['[Measures].[contributors.COUNT]', '[Measures].[Threshold]'],
          },
          color: {fromValue: 'layerDiscriminator'},
        },
        handlers: {},
      },
    },
  ],
};

const initialConfiguration = {
  actions: ['clear-dock', 'save-as', 'remove-dock'],
  containerKey,
  body: {
    children: {
      [tabular1]: {
        containerKey: 'static-tabular-view',
        body: {configuration: staticTabular1Configuration},
      },
      [singleValue1]: {
        containerKey: 'single-value',
        body: {
          mdx: 'SELECT FROM EquityDerivativesCube',
          serverUrl: '',
          configuration: singleValue1Configuration,
        },
      },
      [chart1]: {containerKey: 'chart', body: chart1Configuration},
    },
  },
};

activeUI.plugins.registerBasicContainer({
  key: containerKey,
  component: MyContainer,
  initialConfiguration,
});

const myBookmark = activeUI.widgets
  .createBasicContainer()
  .withKey(containerKey)
  .withName('My Basic Container')
  .toBookmark();

const bookmarkTree = activeUI.widgets
  .createBookmarkTree()
  .withName('Bookmarks')
  .notWritable()
  .withTitleBar()
  .toBookmark();

activeUI.widgets
  .createDashboard()
  .withName('Dashboard with basic container')
  .withStyle({head: {all: {borderBottom: null}}})
  .withConfiguration({
    content: [{key: '0', bookmark: myBookmark}, {key: '1', bookmark: bookmarkTree}],
    layout: {
      direction: 'row',
      children: {
        0: {size: 4 / 5},
        1: {},
      },
    },
  })
  .within('app');

<div id="app"></div>

body {
  margin: 0;
  width: 100%;
}

html,
body,
#app {
  height: 100%;
}