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 4 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
30 changes: 30 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,30 @@
import { dataModelsForTests } from "@activeviam/data-model-5.0";
import { getMigratedKpiTitles } from "./getMigratedKpiTitles";
import { legacyKpi } from "./__test_resources__/legacyKpi";

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 migratedKpiTitles = getMigratedKpiTitles(legacyKpi, {
cube,
mapping: {
columns: [{ type: "allMeasures" }],
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",
}
`);
});
});
106 changes: 106 additions & 0 deletions src/4.3_to_5.0/getMigratedKpiTitles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
Cube,
DataVisualizationWidgetMapping,
KpiWidgetState,
MdxUnknownCompoundIdentifier,
parse,
} from "@activeviam/activeui-sdk-5.0";
import { getSpecificCompoundIdentifier, quote } 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,
{ mapping, cube }: { mapping: DataVisualizationWidgetMapping; cube: Cube },
): KpiWidgetState["titles"] {
const legacyTitles = _getLegacyKpiTitles(legacyKpiState, { cube });
const migratedTitles: KpiWidgetState["titles"] = {};

const memberUniqueNames: string[] = [];
legacyTitles.forEach(({ title, tuple }) => {
// Atoti UI 5.0 KPI widgets expect tuple keys to express members in the following order:
// - column fields first
// - then row fields
const fields = [...(mapping.columns ?? []), ...(mapping.rows ?? [])];
fields.forEach((field) => {
if (field.type === "allMeasures") {
const measureName = tuple[`[Measures].[Measures]`]?.[0];
if (measureName) {
memberUniqueNames.push(`[Measures].[${measureName}]`);
}
} else if (field.type === "hierarchy") {
const hierarchyUniqueName = quote(
field.dimensionName,
field.hierarchyName,
);
const namePath = tuple[hierarchyUniqueName];
if (namePath) {
memberUniqueNames.push(
`${hierarchyUniqueName}.${quote(...namePath)}`,
);
}
}
});
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
49 changes: 48 additions & 1 deletion src/4.3_to_5.0/migrateKpi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,16 @@ import {
import {
getSpecificCompoundIdentifier,
findDescendant,
getMeasuresPositionOnAxis,
getMeasuresAxisName,
} from "@activeviam/mdx-5.0";
import { UnsupportedLegacyQueryUpdateModeError } from "./errors/UnsupportedLegacyQueryUpdateModeError";
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 +190,10 @@ export function migrateKpi(
shouldUpdateFiltersMdx,
});

const { mdx } = query;

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

try {
// Migrate manually entered KPI titles.
if (mdx && mapping.measures.length > 0) {
const measuresAxisName = getMeasuresAxisName(mdx);
const measuresAxis = mdx.axes.find(
(axis) => axis.name === measuresAxisName,
);
if (measuresAxis) {
const positionOfAllMeasuresWithinAxis =
getMeasuresPositionOnAxis(measuresAxis);

// pluginWidgetKpi has `doesSupportMeasuresRedirection: false`.
// For this reason, its "allMeasures" tile is omitted from the mapping generated from `deriveMappingFromMdx`.
// But the position of this tile needs to be known here, as the order of the members in the tuple matters for manually entered KPI titles to work in Atoti UI 5.0.
const mappingWithAllMeasuresTile = produce(mapping, (draft) => {
const attribute = ["ROWS", "1"].includes(measuresAxisName)
? draft.rows
: draft.columns;
attribute.splice(positionOfAllMeasuresWithinAxis, 0, {
type: "allMeasures",
});
});

const migratedTitles = getMigratedKpiTitles(legacyKpiState, {
cube,
mapping: mappingWithAllMeasuresTile,
});
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),
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be helpful to preserve the error message when wrapping it in PartialMigrationError, to help consumers of the CLI fix that error.

}

const serializedWidgetState = serializeWidgetState(migratedWidgetState);

if (isUsingUnsupportedUpdateMode) {
Expand Down
Loading