Container
Containers
<Container />
is a React component, used to render widgets provided by ActiveUI SDK such as:
- tabular views
- charts
- rich-text editors
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]`;
export function App() {
return (
<Container
defaultValue={{
name: 'Simple Tabular View',
value: {
body: {
mdx,
},
containerKey: 'tabular-view',
showTitleBar: true,
},
}}
/>
);
}
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%;
}
As 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 are trying to achieve and start from their value for these props.
- Tweak the default bookmark of the corresponding Container.
- Experiment 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 is 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, as happens 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 is 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 us review which use cases Custom Containers solves 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 saveable 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 a permanent full-screen view, such as 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.
This plugin takes as its argument a React Component that you can use to control what the container renders, as well as its lifecycle.
Containers inherit Ant Design's CSS rules. In particular, all their child DOM elements have their box-sizing set to
border-box
.
Here's an example:
import {useActiveUI, 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', 'query', 'mdx'],
getChartMdx(threshold),
);
const headers = [
{value: 'x', caption: 'X', isNumeric: true},
{value: 'y', caption: 'Y', isNumeric: true},
{value: 'count', caption: 'Count', isNumeric: true},
{value: 'category', caption: 'Category', isNumeric: 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.
function CustomComponent(props) {
const activeUI = useActiveUI();
const {
data: {toTable},
} = activeUI;
const table = toTable(headers, content);
const [state, setState] = React.useState({
value: {
actions: ['clear-dock', 'save-as', 'remove-dock'],
body: {
showWizard: false,
query: {
mdx: getChartMdx(100),
updateMode: 'once',
},
configuration: {
type: 'plotly-line-chart',
// Override some options for Plotly.
// Requires the chart type to be 'plotly-line-chart'.
// Check Plotly's docs for more information.
plotly: {
layout: {
title: 'Custom Container Chart',
colorway: [
'#f3cec9',
'#e7a4b6',
'#cd7eaf',
'#a262a9',
'#6f4d96',
'#3d3b72',
'#182844',
],
},
config: {
displayModeBar: false,
},
data: {
additionalTraces: [
{},
{
name: 'Extra curve',
x: [
'Berlin',
'Johannesburg',
'London',
'New York',
'Paris',
'Tokyo',
],
line: {shape: 'spline'},
y: [350000, 150000, 150000, 200000, 350000, 30000],
mode: 'lines+markers',
},
],
commonTraceOverride: {
marker: {
size: 10,
},
},
overridesByTraceKey: {
'[Measures].[contributors.COUNT]': {
line: {width: 2},
},
},
overridesByTraceIndex: [
{},
{
mode: 'lines',
},
],
},
},
mapping: {
xAxis: ['[Geography].[City].[City]'],
values: [
'[Measures].[contributors.COUNT]',
'[Measures].[Threshold]',
],
splitBy: [],
horizontalSubplots: [],
verticalSubplots: [],
},
},
},
containerKey: 'chart',
showTitleBar: false,
},
});
const {onChange, value} = props;
const threshold = value.threshold || 200000;
const [featuredValuesApi, setFeaturedValuesApi] = React.useState(null);
// Run this when threshold is changed.
React.useEffect(() => {
if (featuredValuesApi && featuredValuesApi.getQuery()) {
featuredValuesApi.getQuery().setMdx(getFeaturedValuesMdx(threshold));
}
}, [threshold, featuredValuesApi]);
return (
<div
style={{
display: 'flex',
height: '100%',
width: '100%',
flexDirection: 'column',
}}
>
<div style={{flex: 0, margin: 20}}>
<h3>Threshold:</h3>
<InputNumber
autoFocus={true}
onChange={(newThreshold) => {
onChange({threshold: newThreshold});
}}
size="large"
step={10000}
value={threshold}
/>
</div>
<Container
childKey="tabular-view"
defaultValue={{
value: {
containerKey: 'static-tabular-view',
showTitleBar: false,
},
}}
onApiChange={(staticTabularViewApi) => {
staticTabularViewApi.setTable(table);
}}
/>
<Container
childKey="featured-values"
defaultValue={{
value: {
body: {
configuration: {
featuredValues: {
showWizard: false,
},
},
mdx: getFeaturedValuesMdx(threshold),
updateMode: 'once',
},
containerKey: 'featured-values',
showTitleBar: false,
},
}}
onApiChange={setFeaturedValuesApi}
/>
<Container
childKey="chart"
value={getChartValue(state, threshold)}
onChange={setState}
/>
</div>
);
}
CustomComponent.propTypes = {
containerApi: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
table: PropTypes.object.isRequired,
value: PropTypes.object.isRequired,
};
export const customContainerPlugin = {
key: 'custom',
staticProperties: {
component: CustomComponent,
},
};
export function App() {
return (
<Container
defaultValue={{
name: 'Custom Container',
value: {
containerKey: customContainerPlugin.key,
showTitleBar: true,
},
}}
/>
);
}
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 child 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 Higher Level 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 (for instance, where you display their results in a custom way). 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, useActiveUI} 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';
// Hook to fetch cities.
function useCities() {
const activeUI = useActiveUI();
const activePivotServer = activeUI.queries.serversPool.getActivePivotServer(
"https://your.activepivot.server",
);
const [cities, setCities] = React.useState([]);
React.useEffect(() => {
let isMounted = true;
async function fetchCities() {
const {
axes: [{positions}],
} = await activePivotServer
.createSelectQuery(
`SELECT
NON EMPTY ${hierarchy}.[City].Members ON ROWS
FROM [EquityDerivativesCube]`,
)
.executeOnceAndGetPromise();
return positions.map(
([
{
captionPath: [, city],
},
]) => city,
);
}
fetchCities().then((c) => {
if (isMounted) {
setCities(c);
}
});
return () => {
isMounted = false;
};
}, [activePivotServer]);
return cities;
}
function getSmiley(pnl) {
if (!pnl) {
return 'loading';
}
if (!pnl.value) {
return 'meh';
}
return pnl.value > 0 ? 'smile' : 'frown';
}
function SmileyContainer(props) {
const {onChange, value} = props;
// Default to realtime.
const realTime = value.realTime !== false;
const realTimeString = realTime ? 'realTime' : 'once';
const cityIndex = value.cityIndex || 0;
const activeUI = useActiveUI();
const [loaded, setLoaded] = React.useState(false);
const [pnl, setPnl] = React.useState(null);
const cities = useCities();
// Currently selected city.
const selectedCity = cities[cityIndex];
function getMdx(city) {
return `SELECT [Measures].[${measureName}] ON COLUMNS
FROM [EquityDerivativesCube]
WHERE ${hierarchy}.[ALL].[AllMember].[${city}]`;
}
// ContainerApi is not referentially equal across renders.
// In this case we can just use the first value each time.
// FIXME: After this is made referentially equal across renders we should not need this kind of hack.
// eslint-disable-next-line react-hooks/exhaustive-deps
const containerApi = React.useMemo(() => props.containerApi, []);
React.useEffect(() => {
if (!selectedCity) {
// No cleanup needed.
return () => {};
}
const mdx = getMdx(selectedCity);
const activePivotServer = activeUI.queries.serversPool.getActivePivotServer(
"https://your.activepivot.server",
);
const query = activePivotServer.createSelectQueryFromCube(
'EquityDerivativesCube',
);
const removeListener = query.addListener((results) => {
setPnl(results.content[0][0] || {caption: 'N/A', value: null});
setLoaded(true);
});
query.setMdx(mdx);
const unbindQuery = containerApi.bindQuery(query);
query.start(realTimeString);
return () => {
unbindQuery();
removeListener();
query.dispose();
};
}, [activeUI, selectedCity, realTimeString, containerApi]);
const smiley = getSmiley(pnl);
function shiftCityIndex(offset) {
onChange({
...value,
cityIndex: (cityIndex + cities.length + offset) % cities.length,
});
}
function toggleRealtime() {
onChange({...value, realTime: !realTime});
}
return (
<div className="smiley-container">
<Card
actions={[
<Icon
type="left"
title="Previous City"
onClick={() => shiftCityIndex(-1)}
/>,
<Icon
type={realTime ? 'pause' : 'caret-right'}
title={realTime ? 'Pause' : 'Enable real-time'}
onClick={toggleRealtime}
/>,
<Icon
type="right"
title="Next City"
onClick={() => shiftCityIndex(+1)}
/>,
]}
loading={!loaded}
style={{width: 300}}
>
<Meta
avatar={<Avatar icon={smiley} size="large" />}
title={loaded && selectedCity}
description={`${measureName}: ${loaded && pnl.caption}`}
/>
</Card>
</div>
);
}
SmileyContainer.propTypes = {
containerApi: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.object.isRequired,
};
export 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>
);
},
};
},
};
export const smileyContainerPlugin = {
key: 'smiley',
staticProperties: {
component: SmileyContainer,
iconKey: smileyIconPlugin.key,
},
};
export function App() {
return (
<Container
defaultValue={{
name: 'Dashboard With Smiley Container',
value: {
showTitleBar: true,
body: {
pages: [
{
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: 'Page Filters',
type: 'container',
value: {
containerKey: 'filters',
showTitleBar: true,
},
writable: false,
},
},
{
key: 'smiley',
bookmark: {
name: 'Smiley Container',
type: 'container',
value: {
actions: [
'save',
'save-as',
'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: 'content-editor',
showTitleBar: true,
body: {
'mdx-common': {
advancedModeEnabled: true,
advancedModeTab: 'mdx',
},
},
},
writable: false,
},
},
],
name: 'Page 1',
},
],
},
containerKey: 'dashboard',
},
}}
/>
);
}
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;
}