Skip to content

Commit

Permalink
feat: support drilling
Browse files Browse the repository at this point in the history
JIRA: F1-266
  • Loading branch information
hackerstanislav committed Apr 15, 2024
1 parent 7ba4ab0 commit 813e173
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,7 @@ const style = { height: 300 };
| backend | false | IAnalyticalBackend | The object with the configuration related to communication with the backend and access to analytical workspaces |
| workspace | false | string | The workspace ID |
| locale | false | string | The localization of the chart. Defaults to `en-US`. |
| drillableItems | false | IDrillableItem[] | An array of attribute values to be drillable, **only attributes in columns can be drilled now** |
| onDrill | false | Function | A callback when a drill is triggered on the component |
| ErrorComponent | false | Component | A component to be rendered if this component is in error state |
| LoadingComponent | false | Component | A component to be rendered if this component is in loading state |
3 changes: 3 additions & 0 deletions libs/sdk-ui-charts/api/sdk-ui-charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { LodashIsEqual1x1 } from 'lodash/fp.js';
import { MeasureOrPlaceholder } from '@gooddata/sdk-ui';
import { MeasuresOrPlaceholders } from '@gooddata/sdk-ui';
import { NullableFiltersOrPlaceholders } from '@gooddata/sdk-ui';
import { OnFiredDrillEvent } from '@gooddata/sdk-ui';
import { default as React_2 } from 'react';
import { SortsOrPlaceholders } from '@gooddata/sdk-ui';
import { VisType } from '@gooddata/sdk-ui';
Expand Down Expand Up @@ -758,6 +759,8 @@ export interface IRepeaterMeasureColumnWidthItemBody {

// @beta (undocumented)
export interface IRepeaterProps extends IBucketChartProps, IRepeaterBucketProps {
drillableItems?: ExplicitDrill[];
onDrill?: OnFiredDrillEvent;
}

// @public (undocumented)
Expand Down
39 changes: 38 additions & 1 deletion libs/sdk-ui-charts/src/charts/repeater/CoreRepeater.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// (C) 2024 GoodData Corporation
import React, { useEffect, useMemo } from "react";
import { WrappedComponentProps, injectIntl, useIntl } from "react-intl";
import noop from "lodash/noop.js";
import {
LoadingComponent as SDKLoadingComponent,
ErrorComponent as SDKErrorComponent,
Expand All @@ -12,7 +13,7 @@ import {
BucketNames,
DataViewFacade,
} from "@gooddata/sdk-ui";
import { ITheme, bucketsFind } from "@gooddata/sdk-model";
import { ITheme, bucketsFind, isAttribute } from "@gooddata/sdk-model";
import { ThemeContextProvider, useTheme, withTheme } from "@gooddata/sdk-ui-theme-provider";
import { IChartConfig, ICoreChartProps } from "../../interfaces/index.js";
import { RepeaterChart } from "./internal/RepeaterChart.js";
Expand Down Expand Up @@ -47,6 +48,8 @@ export const CoreRepeaterImpl: React.FC<ICoreRepeaterChartProps> = (props) => {
onError,
onColumnResized,
config = {},
drillableItems = [],
onDrill = noop,
} = props;

const intl = useIntl();
Expand Down Expand Up @@ -107,6 +110,38 @@ export const CoreRepeaterImpl: React.FC<ICoreRepeaterChartProps> = (props) => {
}
}, [theme, configWithColorPalette.colorPalette, configWithColorPalette.colorMapping, pushData, result]);

useEffect(() => {
if (result) {
const columns = bucketsFind(result.definition.buckets, BucketNames.COLUMNS);

pushData?.({
availableDrillTargets: {
attributes: result
.meta()
.attributeDescriptors()
.filter((descriptor) =>
columns.items.find((item) => {
if (isAttribute(item)) {
return (
item.attribute.localIdentifier ===
descriptor.attributeHeader.localIdentifier
);
}
return false;
}),
)
.map((descriptor) => {
return {
attribute: descriptor,
intersectionAttributes: [descriptor],
};
}),
measures: [],
},
});
}
}, [pushData, result]);

if (error) {
const convertedError = convertError(error);
const errorMessage = convertedError.getMessage();
Expand Down Expand Up @@ -138,6 +173,8 @@ export const CoreRepeaterImpl: React.FC<ICoreRepeaterChartProps> = (props) => {
return (
<RepeaterChart
dataView={result}
drillableItems={drillableItems}
onDrill={onDrill}
config={configWithColorPalette}
onError={onError}
onColumnResized={onColumnResized}
Expand Down
15 changes: 14 additions & 1 deletion libs/sdk-ui-charts/src/charts/repeater/Repeater.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
ITranslationsComponentProps,
IntlTranslationsProvider,
IntlWrapper,
ExplicitDrill,
OnFiredDrillEvent,
} from "@gooddata/sdk-ui";
import { IBucketChartProps, ICoreChartProps } from "../../interfaces/index.js";
import omit from "lodash/omit.js";
Expand Down Expand Up @@ -56,7 +58,18 @@ export interface IRepeaterBucketProps {
/**
* @beta
*/
export interface IRepeaterProps extends IBucketChartProps, IRepeaterBucketProps {}
export interface IRepeaterProps extends IBucketChartProps, IRepeaterBucketProps {
/**
* Configure drillability; e.g. which parts of the visualization can be interacted with.
* LIMITATION: For now only attributes in columns can be drilled into.
*/
drillableItems?: ExplicitDrill[];

/**
* Called when user triggers a drill on a visualization.
*/
onDrill?: OnFiredDrillEvent;
}

const WrappedRepeater = withContexts(RenderRepeater);

Expand Down
150 changes: 150 additions & 0 deletions libs/sdk-ui-charts/src/charts/repeater/hooks/useDrilling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// (C) 2022-2024 GoodData Corporation

import { useCallback, useEffect, useRef, MutableRefObject } from "react";
import { ColDef, CellClickedEvent, GridReadyEvent } from "@ag-grid-community/all-modules";
import {
convertDrillableItemsToPredicates,
isSomeHeaderPredicateMatched,
DataViewFacade,
IHeaderPredicate,
VisualizationTypes,
IDrillEventIntersectionElement,
IDrillEvent,
IDrillEventContext,
} from "@gooddata/sdk-ui";
import { IAttributeOrMeasure, isAttributeDescriptor } from "@gooddata/sdk-model";

import { IRepeaterChartProps } from "../publicTypes.js";
import { DrillingState } from "../internal/privateTypes.js";
import cx from "classnames";

export function useDrilling(columnDefs: ColDef[], items: IAttributeOrMeasure[], props: IRepeaterChartProps) {
const drillingState = useRef<DrillingState>({
items,
columnApi: null,
drillablePredicates: [],
onDrill: props.onDrill,
dataView: props.dataView,
});

useEffect(() => {
drillingState.current.drillablePredicates = convertDrillableItemsToPredicates(props.drillableItems);
drillingState.current.onDrill = props.onDrill;
drillingState.current.dataView = props.dataView;
}, [props.dataView, props.drillableItems, props.onDrill]);

const onCellClicked = useCallback((cellEvent: CellClickedEvent) => {
const drillableItem = getDrillable(
drillingState.current.dataView,
drillingState.current.drillablePredicates,
cellEvent.colDef,
);

if (!drillableItem) {
return false;
}

const columnIndex = cellEvent.columnApi.getAllColumns().findIndex((col) => col === cellEvent.column);
const attributeHeaderItem = cellEvent.data[cellEvent.colDef.field];

const intersectionElement: IDrillEventIntersectionElement = {
header: {
...drillableItem,
attributeHeaderItem,
},
};

const drillEvent = createDrillEvent(
drillingState,
columnIndex,
cellEvent.rowIndex,
attributeHeaderItem?.name,
intersectionElement,
);
return fireDrillEvent(drillingState, drillEvent, cellEvent);
}, []);

const onGridReady = useCallback(
(readyEvent: GridReadyEvent) => {
drillingState.current.columnApi = readyEvent.columnApi;
},
[drillingState],
);

useEffect(() => {
applyDrillableItems(columnDefs, drillingState);
}, [columnDefs]);

return {
onGridReady,
onCellClicked,
};
}

function applyDrillableItems(columnDefs: ColDef[], drillingState: MutableRefObject<DrillingState>) {
const columnApi = drillingState.current.columnApi;

columnDefs.forEach((colDef) => {
const matched = getDrillable(
drillingState.current.dataView,
drillingState.current.drillablePredicates,
colDef,
);

if (matched && !columnApi) {
colDef.cellClass = cx(colDef.cellClass as string, "gd-repeater-cell-drillable");
}
});
}

function getDrillable(dataView: DataViewFacade, drillablePredicates: IHeaderPredicate[], colDef: ColDef) {
const descriptors = [...dataView.meta().attributeDescriptors(), ...dataView.meta().measureDescriptors()];

const found = descriptors.find((item) => {
if (isAttributeDescriptor(item)) {
return item.attributeHeader.localIdentifier === colDef.field;
}
return item.measureHeaderItem.localIdentifier === colDef.field;
});
return isSomeHeaderPredicateMatched(drillablePredicates, found, dataView) ? found : null;
}

function createDrillEvent(
drillingState: MutableRefObject<DrillingState>,
columnIndex: number,
rowIndex: number,
value: string | undefined,
el: IDrillEventIntersectionElement,
): IDrillEvent {
const drillContext: IDrillEventContext = {
type: VisualizationTypes.REPEATER,
element: "cell",
columnIndex,
rowIndex,
value,
intersection: [el],
};
return {
dataView: drillingState.current.dataView.dataView,
drillContext,
};
}

function fireDrillEvent(
drillingState: MutableRefObject<DrillingState>,
drillEvent: IDrillEvent,
cellEvent: CellClickedEvent,
) {
if (drillingState.current.onDrill?.(drillEvent)) {
// This is needed for /analyze/embedded/ drilling with post message
// More info: https://github.com/gooddata/gdc-analytical-designer/blob/develop/test/drillEventing/drillEventing_page.html
const event = new CustomEvent("drill", {
detail: drillEvent,
bubbles: true,
});
cellEvent.event?.target?.dispatchEvent(event);
return true;
}

return false;
}
14 changes: 12 additions & 2 deletions libs/sdk-ui-charts/src/charts/repeater/internal/RepeaterChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { InlineLineChart } from "./InlineLineChart.js";
import { InlineColumnChart } from "./InlineColumnChart.js";
import { RepeaterInlineVisualizationDataPoint } from "./dataViewToRepeaterData.js";
import isNil from "lodash/isNil.js";
import { useDrilling } from "../hooks/useDrilling.js";

const DEFAULT_COL_DEF = { resizable: true };

Expand Down Expand Up @@ -134,7 +135,12 @@ export const RepeaterChart: React.FC<IRepeaterChartProps> = (props) => {
config?.inlineVisualizations,
]);

const { onColumnResized, onGridReady, containerRef } = useResizing(columnDefs, items, props);
const {
onColumnResized,
onGridReady: onResizingGridReady,
containerRef,
} = useResizing(columnDefs, items, props);
const { onCellClicked, onGridReady: onDrillingGridReady } = useDrilling(columnDefs, items, props);

return (
<div className="gd-repeater ag-theme-balham s-repeater" ref={containerRef}>
Expand All @@ -157,7 +163,11 @@ export const RepeaterChart: React.FC<IRepeaterChartProps> = (props) => {
rowHeight={rowHeight}
suppressCellFocus={true}
suppressMovableColumns={true}
onGridReady={onGridReady}
onCellClicked={onCellClicked}
onGridReady={(e) => {
onResizingGridReady(e);
onDrillingGridReady(e);
}}
onColumnResized={onColumnResized}
/>
</div>
Expand Down
10 changes: 10 additions & 0 deletions libs/sdk-ui-charts/src/charts/repeater/internal/privateTypes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// (C) 2024 GoodData Corporation

import { ISeparators, IAttributeOrMeasure } from "@gooddata/sdk-model";
import { IHeaderPredicate, OnFiredDrillEvent, DataViewFacade } from "@gooddata/sdk-ui";
import { ColumnApi, Column } from "@ag-grid-community/all-modules";

import { RepeaterDefaultColumnWidth, RepeaterColumnResizedCallback } from "../publicTypes.js";
Expand Down Expand Up @@ -30,3 +31,12 @@ export type ResizingState = {
columnApi: ColumnApi | null;
manuallyResizedColumns: Column[];
};

export type DrillingState = {
items: IAttributeOrMeasure[];
columnApi: ColumnApi | null;
drillablePredicates: IHeaderPredicate[];
dataView: DataViewFacade;

onDrill: OnFiredDrillEvent | undefined;
};
12 changes: 11 additions & 1 deletion libs/sdk-ui-charts/src/charts/repeater/publicTypes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// (C) 2024 GoodData Corporation

import { DataViewFacade } from "@gooddata/sdk-ui";
import { DataViewFacade, ExplicitDrill, OnFiredDrillEvent } from "@gooddata/sdk-ui";

import { IChartConfig } from "../../interfaces/index.js";

Expand Down Expand Up @@ -66,4 +66,14 @@ export interface IRepeaterChartProps {
* @param columnWidths - new widths for columns
*/
onColumnResized?: RepeaterColumnResizedCallback;

/**
* Configure drillability; e.g. which parts of the visualization can be interacted with.
*/
drillableItems?: ExplicitDrill[];

/**
* Called when user triggers a drill on a visualization.
*/
onDrill?: OnFiredDrillEvent;
}
9 changes: 9 additions & 0 deletions libs/sdk-ui-charts/styles/scss/repeater.scss
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ $subtotal-odd-background-color: var(
}
}

.gd-repeater-cell-drillable {
font-weight: bold;
cursor: pointer;

&:hover {
text-decoration: underline;
}
}

&.gd-repeater-header-hide {
.ag-header-row {
display: none;
Expand Down

0 comments on commit 813e173

Please sign in to comment.