Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI-9384 - Preserve KPI titles when migrated KPI widgets from 4.3 to 5.0 #124

Merged
merged 6 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/4.3_to_5.0/__test_resources__/legacyComparisonValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export const legacyComparisonValues = {
showTitleBar: true,
body: {
serverUrl: "http://localhost:9090",
mdx:
"SELECT NON EMPTY Hierarchize(DrilldownLevel([Currency].[Currency].[ALL].[AllMember])) ON ROWS, NON EMPTY [Measures].[pnl.FOREX] ON COLUMNS, {[Booking].[Desk].[ALL].[AllMember].[LegalEntityA], [Booking].[Desk].[ALL].[AllMember].[LegalEntityB]} ON PAGES FROM [EquityDerivativesCube] CELL PROPERTIES VALUE, FORMATTED_VALUE, BACK_COLOR, FORE_COLOR, FONT_FLAGS",
mdx: "SELECT NON EMPTY Hierarchize(DrilldownLevel([Currency].[Currency].[ALL].[AllMember])) ON ROWS, NON EMPTY [Measures].[pnl.FOREX] ON COLUMNS, {[Booking].[Desk].[ALL].[AllMember].[LegalEntityA], [Booking].[Desk].[ALL].[AllMember].[LegalEntityB]} ON PAGES FROM [EquityDerivativesCube] CELL PROPERTIES VALUE, FORMATTED_VALUE, BACK_COLOR, FORE_COLOR, FONT_FLAGS",
contextValues: {},
updateMode: "once",
ranges: {
Expand All @@ -24,7 +23,13 @@ export const legacyComparisonValues = {
},
},
configuration: {
featuredValues: {},
featuredValues: {
locations: {
"[Currency].[Currency].[AllMember].[GBP],[Measures].[pnl.FOREX]": {
title: "Hello World",
},
},
},
},
},
containerKey: "featured-values",
Expand Down
10 changes: 9 additions & 1 deletion src/4.3_to_5.0/__test_resources__/legacyKpi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,15 @@ export const legacyKpi: LegacyWidgetState = {
},
},
configuration: {
featuredValues: {},
featuredValues: {
locations: {
// "EUR, USD" is not a member of the sandbox, but it is used here to check that the script supports member names including ",".
"[Currency].[Currency].[AllMember].[EUR, USD],[Measures].[contributors.COUNT]":
{
title: "Hello World",
},
},
},
},
},
containerKey: "featured-values",
Expand Down
40 changes: 40 additions & 0 deletions src/4.3_to_5.0/getMigratedKpiTitles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { dataModelsForTests } from "@activeviam/data-model-5.0";
import { getMigratedKpiTitles } from "./getMigratedKpiTitles";
import { legacyKpi } from "./__test_resources__/legacyKpi";
import { _getQueryInLegacyWidgetState } from "./_getQueryInLegacyWidgetState";
import { MdxSelect, parse } from "@activeviam/mdx-5.0";

const cube = dataModelsForTests.sandbox.catalogs[0].cubes[0];

describe("getMigratedKpiTitles", () => {
it("returns the migrated KPI titles corresponding to the legacy KPI state, ready to be used in Atoti UI 5.0", () => {
const legacyQuery = _getQueryInLegacyWidgetState(legacyKpi);

if (!legacyQuery || !legacyQuery.mdx) {
throw new Error("Expected the legacy KPI state to contain a query");
}

const legacyMdx = parse<MdxSelect>(legacyQuery.mdx);
const migratedKpiTitles = getMigratedKpiTitles(legacyKpi, {
cube,
legacyMdx,
mapping: {
columns: [],
measures: [{ type: "measure", measureName: "contributors.COUNT" }],
rows: [
{
type: "hierarchy",
dimensionName: "Currency",
hierarchyName: "Currency",
levelName: "Currency",
},
],
},
});
expect(migratedKpiTitles).toMatchInlineSnapshot(`
{
"[Measures].[contributors.COUNT],[Currency].[Currency].[AllMember].[EUR, USD]": "Hello World",
}
`);
});
});
141 changes: 141 additions & 0 deletions src/4.3_to_5.0/getMigratedKpiTitles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import {
Cube,
DataVisualizationWidgetMapping,
KpiWidgetState,
MdxUnknownCompoundIdentifier,
parse,
} from "@activeviam/activeui-sdk-5.0";
import {
getSpecificCompoundIdentifier,
quote,
getMeasuresAxisName,
getMeasuresPositionOnAxis,
MdxSelect,
} from "@activeviam/mdx-5.0";

interface LegacyKpiTitle {
title: string;
tuple: { [hierarchyUniqueName: string]: string[] };
}

/**
* Returns the legacy KPI titles attached to `legacyKpiState`.
*/
function _getLegacyKpiTitles(
// Legacy widget states are not typed.
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
legacyKpiState: any,
{ cube }: { cube: Cube },
): LegacyKpiTitle[] {
const locations =
legacyKpiState?.value?.body?.configuration?.featuredValues?.locations;

if (!locations) {
return [];
}

const legacyTitles: LegacyKpiTitle[] = [];

for (const tupleKey in locations) {
const { title } = locations[tupleKey];
const tuple: {
[hierarchyUniqueName: string]: string[];
} = {};
const memberUniqueNames = tupleKey.split(/,(?![^\[]*\])/);
for (const memberUniqueName of memberUniqueNames) {
const compoundIdentifier =
parse<MdxUnknownCompoundIdentifier>(memberUniqueName);
const specificCompoundIdentifier = getSpecificCompoundIdentifier(
compoundIdentifier,
{ cube },
);
if (specificCompoundIdentifier.type === "measure") {
tuple[`[Measures].[Measures]`] = [
specificCompoundIdentifier.measureName,
];
} else if (specificCompoundIdentifier.type === "member") {
const hierarchyUniqueName = quote(
specificCompoundIdentifier.dimensionName,
specificCompoundIdentifier.hierarchyName,
);
tuple[hierarchyUniqueName] = specificCompoundIdentifier.path;
}
}
legacyTitles.push({ title, tuple });
}

return legacyTitles;
}

/**
* Returns the migrated KPI widget titles corresponding to legacyKpiState.
*/
export function getMigratedKpiTitles(
// Legacy widget states are not typed.
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
legacyKpiState: any,
{
legacyMdx,
mapping,
cube,
}: {
legacyMdx: MdxSelect;
mapping: DataVisualizationWidgetMapping;
cube: Cube;
},
): KpiWidgetState["titles"] {
const legacyTitles = _getLegacyKpiTitles(legacyKpiState, { cube });
const migratedTitles: KpiWidgetState["titles"] = {};

const numberOfColumnFields = mapping.columns?.length ?? 0;

const measuresAxisName = getMeasuresAxisName(legacyMdx);
const measuresAxis = legacyMdx.axes.find(
(axis) => axis.name === measuresAxisName,
);
let measuresPositionInTuple = -1;
if (measuresAxis) {
const measuresPositionOnAxis = getMeasuresPositionOnAxis(measuresAxis);
measuresPositionInTuple = ["0", "COLUMNS"].includes(measuresAxisName)
? measuresPositionOnAxis
: numberOfColumnFields + measuresPositionOnAxis;
}

// Atoti UI 5.0 KPI widgets expect tuple keys to express members in the following order:
// - column fields first
// - then row fields
const ordinalFields = [...(mapping.columns ?? []), ...(mapping.rows ?? [])];

legacyTitles.forEach(({ title, tuple }) => {
const memberUniqueNames: string[] = [];

ordinalFields.forEach((field) => {
if (field.type === "hierarchy") {
const hierarchyUniqueName = quote(
field.dimensionName,
field.hierarchyName,
);
const namePath = tuple[hierarchyUniqueName];
if (namePath) {
memberUniqueNames.push(
`${hierarchyUniqueName}.${quote(...namePath)}`,
);
}
}
});

if (measuresPositionInTuple > -1) {
const measureName = tuple[quote("Measures", "Measures")][0];
memberUniqueNames.splice(
measuresPositionInTuple,
0,
quote("Measures", measureName),
);
}

const tupleKey = memberUniqueNames.join(",");
migratedTitles[tupleKey] = title;
});

return migratedTitles;
}
6 changes: 6 additions & 0 deletions src/4.3_to_5.0/migrateKpi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ describe("migrateKpi", () => {
},
],
"serverKey": "my-server",
"titles": {
"[Measures].[contributors.COUNT],[Currency].[Currency].[AllMember].[EUR, USD]": "Hello World",
},
"widgetKey": "kpi",
}
`);
Expand Down Expand Up @@ -81,6 +84,9 @@ describe("migrateKpi", () => {
},
"queryContext": [],
"serverKey": "my-server",
"titles": {
"[Measures].[pnl.FOREX],[Currency].[Currency].[AllMember].[GBP]": "Hello World",
},
"widgetKey": "kpi",
}
`);
Expand Down
30 changes: 29 additions & 1 deletion src/4.3_to_5.0/migrateKpi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { _getQueryInLegacyWidgetState } from "./_getQueryInLegacyWidgetState";
import { _getTargetCubeFromServerUrl } from "./_getTargetCubeFromServerUrl";
import { _migrateQuery } from "./_migrateQuery";
import { produce } from "immer";
import { getMigratedKpiTitles } from "./getMigratedKpiTitles";
import { PartialMigrationError } from "../PartialMigrationError";

const moveExpressionToWithClause = (
draft: any,
Expand Down Expand Up @@ -186,8 +188,10 @@ export function migrateKpi(
shouldUpdateFiltersMdx,
});

const { mdx } = query;

const mapping = deriveMappingFromMdx({
mdx: query.mdx,
mdx,
cube,
widgetPlugin: pluginWidgetKpi,
});
Expand All @@ -206,6 +210,30 @@ export function migrateKpi(
}),
};

try {
if (legacyMdx) {
// Migrate manually entered KPI titles.
const migratedTitles = getMigratedKpiTitles(legacyKpiState, {
cube,
mapping,
legacyMdx,
});
if (migratedTitles && Object.keys(migratedTitles).length > 0) {
migratedWidgetState.titles = migratedTitles;
}
}
} catch (error) {
// Migrating the KPI titles is a best effort.
// The migration script should not fail if this part errors.
throw new PartialMigrationError(
`Could not migrate the titles of the featured values widget named "${legacyKpiState.name}"`,
serializeWidgetState(migratedWidgetState),
{
cause: error,
},
);
}

const serializedWidgetState = serializeWidgetState(migratedWidgetState);

if (isUsingUnsupportedUpdateMode) {
Expand Down
3 changes: 2 additions & 1 deletion src/PartialMigrationError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ export class PartialMigrationError extends Error {
constructor(
message: string,
migratedWidgetState: AWidgetState<"serialized">,
options?: ErrorOptions,
) {
super(message);
super(message, options);
this.migratedWidgetState = migratedWidgetState;
}
}
Loading