Skip to main content

Migrate from ActiveUI 4 to 5

Migrating from ActiveUI 4 to 5 includes two parts:

  • content migration
  • code migration

Content migration#

During their time in ActiveUI, our users have saved dashboards, widgets, filters, measures and more. We refer to everything they saved as "content".

ActiveUI 5 is not backward compatible: this content needs to be migrated.

Fortunately, a tool will do it for us. It is named atoti-ui-migration.

Follow along its README. By the end of it, you will have migrated your content.

Code migration#

ActiveUI 5 comes with a major shift in technical design. ActiveUI 4 was an object-oriented JavaScript library. ActiveUI 5 on the other hand is an off-the-shelf (yet extensible) application, made with TypeScript and tightly integrated with React. It makes it more performant, more robust and easier to learn - assuming that you already have some experience with React.

Let's scratch the surface and understand what this means, concretely.

You are in one of these 3 scenarios:

  1. you used ActiveUI
  2. you used ActiveUI with extensions
  3. you developed your own application using ActiveUI SDK

You used ActiveUI#

Then you are already done. Conformism has perks sometimes! You can skip everything below 🎉

You used ActiveUI with extensions#

This section is addressed to you if your application contained custom widgets, actions, drawers, menus, etc.

As the API of the version 5 is quite different from its predecessor's, the best strategy is to start from scratch.

Download the ActiveUI starter (see setup).

Notice a couple of pleasant surprises:

  • it contains a lot fewer files than your previous project did.
  • it builds and starts faster.

Let's consider an example to get a feel for how we will be able to migrate these custom extensions. Suppose that your project contains a custom widget plugin allowing users to quickly filter a page on a city, say "Paris".

Brace yourself, lots of code incoming! You can either take your time and read through all of the v4 extension and its migrated v5 version or jump straight down to the analysis and allow yourself to go back up to investigate points of interest.

The v4 extension code looks like this:

  • pluginWidgetCustomFilter.tsx
import React, { FC } from "react";import { ReactContainerComponentProps } from "@activeviam/activeui-sdk";
import { isFilteredOnParis } from "./isFilteredOnParis";import { getUpdatedPageFilters } from "./getUpdatedPageFilters";
const cubeName = "MyCube";
/** * Displays a checkbox allowing to quickly filter a dashboard page on Paris. */const CustomFilter: FC<ReactContainerComponentProps> = function (props) {  // @ts-ignore `getParentPage` is not part of the definition types for `props.containerApi`, but it is defined during runtime ¯\_(ツ)_/¯  const pageApi = props.containerApi.getParentPage();  const pageFilters = pageApi.getPageFilters(); // Therefore `pageFilters` is any, unfortunately.
  const isChecked = isFilteredOnParis(pageFilters, cubeName);
  const handleChange = () => {    const updatedPageFilters = getUpdatedPageFilters(      pageFilters,      isChecked,      cubeName,    );    pageApi.setPageFilters(updatedPageFilters);  };
  return (    <div      style={{ display: "flex", flexDirection: "row", alignItems: "center" }}    >      <input        type="checkbox"        id="filter-on-paris"        checked={isChecked}        onChange={handleChange}      />      <label htmlFor="filter-on-paris">Filter on Paris</label>    </div>  );};
export const pluginWidgetCustomFilter = {  key: "custom-filter",  staticProperties: {    choosableFromUI: true,    component: CustomFilter,  },};
View v4 util functions
  • isFilteredOnParis.ts
/** * Returns whether `pageFilters` contains a filter on Paris. */export function isFilteredOnParis(  pageFilters: { [cubeName: string]: string[] },  cubeName: string,) {  return pageFilters[cubeName]?.includes(    "[Geography].[City].[ALL].[AllMember].[Paris]",  );}
  • getUpdatedPageFilters.ts
/** * Returns an updated `pageFilters`, with a filter on Paris added if `isChecked` is false - or removed otherwise. * Does not mutate `pageFilters`. */export function getUpdatedPageFilters(  pageFilters: { [cubeName: string]: string[] },  isChecked: boolean,  cubeName: string,) {  if (isChecked) {    // Remove the filter on Paris.    return {      ...pageFilters,      [cubeName]: pageFilters[cubeName].filter(        (mdx) => mdx !== "[Geography].[City].[ALL].[AllMember].[Paris]",      ),    };  } else {    // Add a filter on Paris    return {      ...pageFilters,      [cubeName]: [        ...pageFilters[cubeName],        "[Geography].[City].[ALL].[AllMember].[Paris]",      ],    };  }}
View v4 plugin registration
  • en-US.json
"bookmarks": {  "new": {    "custom-filter": {      "title": "Custom filter"    }  }},
  • App.tsx
import { pluginWidgetCustomFilter } from "./pluginWidgetCustomFilter";
const activeUI = createActiveUI({  plugins: {    container: [pluginWidgetCustomFilter],  },});

And here it is, migrated to v5:

  • pluginWidgetCustomFilter.tsx
import React, { FC } from "react";import {  getPage,  isWidgetInDashboard,  useDataModels,  WidgetPlugin,  WidgetPluginProps,} from "@activeviam/activeui-sdk";
import { isFilterOnParis } from "./isFilterOnParis";import { getUpdatedDashboardState } from "./getUpdatedDashboardState";
const serverKey = "my-server";const cubeName = "MyCube";
/** * Displays a checkbox allowing to quickly filter a dashboard page on Paris. */const CustomFilter: FC<WidgetPluginProps> = function (props) {  const dataModels = useDataModels();  const dataModel = dataModels?.[serverKey];
  if (!dataModel) {    return <div>Loading the data model of server "{serverKey}"</div>;  }
  const cube = dataModel.catalogs[0].cubes.find(    ({ name }) => name === cubeName,  );
  if (!cube) {    return (      <div>        Could not find a cube with the name "{cubeName}" within the data model        of the server "{serverKey}".      </div>    );  }
  if (!isWidgetInDashboard(props)) {    return <div>Oops! This widget only works in a dashboard.</div>;  }
  const { dashboardState, pageKey } = props;  const pageState = getPage(dashboardState, pageKey);  const pageFilters = pageState?.filters ?? [];
  const isChecked = pageFilters?.some((mdx) => isFilterOnParis(mdx, cube));
  const handleChange = () => {    const updatedDashboardState = getUpdatedDashboardState(      dashboardState,      pageKey,      isChecked,      cube,    );    props.onDashboardChange(updatedDashboardState);  };
  return (    <div      style={{        display: "flex",        flexDirection: "row",        alignItems: "center",      }}    >      <input        type="checkbox"        id="filter-on-paris"        checked={isChecked}        onChange={handleChange}      />      <label htmlFor="filter-on-paris">Filter on Paris</label>    </div>  );};
export const pluginWidgetCustomFilter: WidgetPlugin = {  key: "custom-filter",  Component: CustomFilter,  initialState: {    widgetKey: "custom-filter",  },  translations: {    "en-US": {      key: "Custom filter",      defaultName: "New custom filter",    },  },};
View v5 util functions
  • isFilterOnParis.ts
import _ from "lodash";import { createFilter, Cube, Mdx } from "@activeviam/activeui-sdk";
/** * Returns whether `mdx` represents a filter on Paris. */export function isFilterOnParis(mdx: Mdx, cube: Cube) {  const filter = createFilter(mdx, cube);  return (    filter.type === "members" &&    _.isEqual(filter.members, [["AllMember", "Paris"]])  );}
  • getUpdatedDashboardState.ts
import { produce } from "immer";import {  createMdxForFilter,  Cube,  DashboardState,  getPage,} from "@activeviam/activeui-sdk";
import { isFilterOnParis } from "./isFilterOnParis";
/** * Returns an updated `dashboardState`, with a page filter on Paris added if `isChecked` is false - or removed otherwise. * Does not mutate `dashboardState`. */export function getUpdatedDashboardState(  dashboardState: DashboardState,  pageKey: string,  isChecked: boolean,  cube: Cube,) {  return produce(dashboardState, (draft) => {    const draftPageState = getPage(draft, pageKey);
    if (!draftPageState) {      return;    }
    if (!draftPageState.filters) {      draftPageState.filters = [];    }
    if (isChecked) {      // Remove the filter on Paris.      draftPageState.filters.splice(        draftPageState.filters.findIndex((mdx) => isFilterOnParis(mdx, cube)),        1,      );    } else {      // Add a filter on Paris.      draftPageState.filters.push(        createMdxForFilter(          {            type: "members",            dimensionName: "Geography",            hierarchyName: "City",            members: [["AllMember", "Paris"]],          },          cube,        ),      );    }
    return draft;  });}
View v5 plugin registration
  • plugins.ts
import { pluginWidgetCustomFilter } from "./pluginWidgetCustomFilter";
const widgetPlugins: Array<WidgetPlugin<any, any>> = [pluginWidgetCustomFilter];

Analysis#

Let's dive in.

How to access state?#
- const pageApi = props.containerApi.getParentPage();- const pageFilters = pageApi.getPageFilters();+ const { dashboardState, pageKey } = props;+ const pageState = getPage(dashboardState, pageKey);+ const pageFilters = pageState?.filters ?? [];
- pageApi.setPageFilters(updatedPageFilters);+ props.onDashboardChange(updatedDashboardState);

The containerApi object used to let you interact with specific bits of state through getters and setters.

Now you get a dashboardState prop instead, representing the whole state of the current dashboard. See WidgetPluginProps for more details.

This comes with benefits:

  • dashboardState is typed, making it easier to find the parts of it that are relevant to the task at hand, for instance by leveraging the autocompletion feature of your code editor.
  • its attributes are referentially stable, allowing us to leverage memoization. Here, we could memoize our Component based on pageState.filters, as it does not depend on anything else. There is no point in wasting resources to rerender it when the user edits things other than filters, as we know that it will never be visually impacted in this case.
note

Updating big objects is not always trivial, especially when you pay attention to not mutating them. For this purpose, we recommend using immer. It makes these operations less error-prone, and also nicer to write and to read! It is used in the v5 util functions above.

How to register extensions?#

This is visible in the plugin registrations above.

- const activeUI = createActiveUI({-   plugins: {-     container: [pluginWidgetCustomFilter],-   },- });+ const widgetPlugins: Array<WidgetPlugin<any, any>> = [pluginWidgetCustomFilter];

You used to pass your extensions to a function called createActiveUI.

Now, you must add them to a configuration object in your index.ts file. This is because ActiveUI 5 is a build with extension points, whereas ActiveUI 4 was a sandbox, that you cloned when starting your project.

This change also comes with benefits:

  • your project now only contains your own files.
  • it builds and starts faster.
  • future migrations will be easier: updates made by the ActiveUI team to the sources of the ActiveUI application will be transparent to you, as they should be.
The form of filters#

Notice that filters used to be represented as strings, whereas they are now represented as Mdx objects. This is visible in particular in the signature of the util functions. This change allows your extension - and ActiveUI 5 in general - to be more performant.

More possibilities for error handling#

ActiveUI 5 comes with exhaustive definition types and typeguards. Below is an example where our v4 code would have thrown an error if someone tried to use our extension outside a dashboard (for instance, in an atoti notebook). After the migration, TypeScript does not allow us to write unsafe code. And typeguards (such as isWidgetInDashboard) make our code more robust.

- // @ts-ignore `getParentPage` is not part of the definition types for `props.containerApi`, but it is defined during runtime ¯\_(ツ)_/¯- const pageApi = props.containerApi.getParentPage();+ if (!isWidgetInDashboard(props)) {+   return <div>Oops! This widget only works in a dashboard.</div>;+ }

To go further, you can read the ActiveUI tutorial.

You developed your own application using ActiveUI SDK#

The best strategy is to go through your @activeviam/activeui-sdk imports and replace them by equivalents in the new API.

In particular, note that Container can be replaced by Widget or Dashboard, depending on the use case.

To go further, you can read: integrate ActiveUI Components.