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.
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 can create a custom container using the registerBasicContainer
function from the PluginsApi, which takes an object of type BasicContainerPluginDescription.
Here’s a simple example:
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?
ActiveUI
core component as children components in a React friendly wayActiveUI
APIThe 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:
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.
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.
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.
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.
We can see below another snippet making use of all the features described above. It features:
this.state.threshold
)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%;
}