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:
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 thevalue
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:
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:
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;
}