diff --git a/.changeset/full-donuts-shake.md b/.changeset/full-donuts-shake.md new file mode 100644 index 00000000000..5043aa1e537 --- /dev/null +++ b/.changeset/full-donuts-shake.md @@ -0,0 +1,5 @@ +--- +'@finos/legend-lego': patch +--- + +DataGrid - allows users to make their own onGridReady callback diff --git a/.changeset/goofy-lions-invite.md b/.changeset/goofy-lions-invite.md new file mode 100644 index 00000000000..cfb0a6fb1d6 --- /dev/null +++ b/.changeset/goofy-lions-invite.md @@ -0,0 +1,5 @@ +--- +'@finos/legend-extension-dsl-data-product': patch +--- + +Fixed link copying, direction on loading, and added APG anchors diff --git a/packages/legend-extension-dsl-data-product/src/components/DataProduct/DataProductDataAccess.tsx b/packages/legend-extension-dsl-data-product/src/components/DataProduct/DataProductDataAccess.tsx index ffe2abbc353..ab04e30ab89 100644 --- a/packages/legend-extension-dsl-data-product/src/components/DataProduct/DataProductDataAccess.tsx +++ b/packages/legend-extension-dsl-data-product/src/components/DataProduct/DataProductDataAccess.tsx @@ -498,13 +498,27 @@ const AccessPointTable = observer( setGridApi(params.api)} domLayout={ (accessPointState.relationType?.columns.length ?? 0) > MAX_GRID_AUTO_HEIGHT_ROWS ? 'normal' : 'autoHeight' } + onGridReady={(params) => { + setGridApi(params.api); + if (!accessPointState.relationType?.columns.length) { + accessPointState.apgState.dataProductViewerState.layoutState.markGridAsRendered(); + } + }} + onFirstDataRendered={() => { + if ( + accessPointState.relationType?.columns.length !== + undefined && + accessPointState.relationType.columns.length > 0 + ) { + accessPointState.apgState.dataProductViewerState.layoutState.markGridAsRendered(); + } + }} /> ) : ( @@ -674,6 +688,19 @@ export const DataProductAccessPointGroupViewer = observer( const [isEntitledButtonGroupMenuOpen, setIsEntitledButtonGroupMenuOpen] = useState(false); const requestAccessButtonGroupRef = useRef(null); + const sectionRef = useRef(null); + const anchor = generateAnchorForSection(`apg-${apgState.apg.id}`); + + useEffect(() => { + if (sectionRef.current) { + apgState.dataProductViewerState.layoutState.setWikiPageAnchor( + anchor, + sectionRef.current, + ); + } + return () => + apgState.dataProductViewerState.layoutState.unsetWikiPageAnchor(anchor); + }, [apgState, anchor]); const entitlementsDataContractViewerState = useMemo(() => { return dataAccessState?.contractViewerContractAndSubscription && @@ -863,7 +890,10 @@ export const DataProductAccessPointGroupViewer = observer( }; return ( -
+
@@ -875,6 +905,10 @@ export const DataProductAccessPointGroupViewer = observer( @@ -1004,7 +1038,10 @@ export const DataProducteDataAccess = observer( diff --git a/packages/legend-extension-dsl-data-product/src/components/DataProduct/DataProductSupportInfo.tsx b/packages/legend-extension-dsl-data-product/src/components/DataProduct/DataProductSupportInfo.tsx index 176daa28ed6..07c6d3f3c34 100644 --- a/packages/legend-extension-dsl-data-product/src/components/DataProduct/DataProductSupportInfo.tsx +++ b/packages/legend-extension-dsl-data-product/src/components/DataProduct/DataProductSupportInfo.tsx @@ -176,7 +176,10 @@ export const DataProductSupportInfo = observer( diff --git a/packages/legend-extension-dsl-data-product/src/components/ProductWiki.tsx b/packages/legend-extension-dsl-data-product/src/components/ProductWiki.tsx index ab643fdd7c6..d3f85f4a89e 100644 --- a/packages/legend-extension-dsl-data-product/src/components/ProductWiki.tsx +++ b/packages/legend-extension-dsl-data-product/src/components/ProductWiki.tsx @@ -44,6 +44,7 @@ import { V1_ExternalDataProductType } from '@finos/legend-graph'; import { prettyCONSTName } from '@finos/legend-shared'; import { ModelsDocumentation } from '@finos/legend-lego/model-documentation'; import { DiagramViewer } from '@finos/legend-extension-dsl-diagram'; +import { useNavigationZone } from '@finos/legend-application/browser'; export const ProductWikiPlaceholder: React.FC<{ message: string }> = ( props, @@ -211,6 +212,14 @@ export const ProductWiki = observer( | undefined; }) => { const { productViewerState, productDataAccessState } = props; + const navigationZone = useNavigationZone(); + + useEffect(() => { + productViewerState.layoutState.setWikiPageAnchorToNavigate({ + anchor: navigationZone, + }); + productViewerState.changeZone(navigationZone); + }, [productViewerState, navigationZone]); useEffect(() => { if ( diff --git a/packages/legend-extension-dsl-data-product/src/components/__tests__/DataProductViewer.test.tsx b/packages/legend-extension-dsl-data-product/src/components/__tests__/DataProductViewer.test.tsx index 6058ae2a3bb..387b0e059e7 100644 --- a/packages/legend-extension-dsl-data-product/src/components/__tests__/DataProductViewer.test.tsx +++ b/packages/legend-extension-dsl-data-product/src/components/__tests__/DataProductViewer.test.tsx @@ -55,6 +55,12 @@ import type { ProjectGAVCoordinates, StoredFileGeneration, } from '@finos/legend-storage'; +import { + MockedMonacoEditorAPI, + MockedMonacoEditorInstance, + MockedMonacoEditorModel, +} from '@finos/legend-lego/code-editor/test'; +import { BrowserRouter } from '@finos/legend-application/browser'; jest.mock('react-oidc-context', () => { const { MOCK__reactOIDCContext } = jest.requireActual<{ @@ -254,12 +260,14 @@ const setupLakehouseDataProductTest = async ( await act(async () => { await flowResult(dataProductDataAccessState?.init(undefined)); renderResult = render( - - - , + + + + + , ); await new Promise((resolve) => setTimeout(resolve, 0)); // wait for async state updates diff --git a/packages/legend-extension-dsl-data-product/src/stores/BaseLayoutState.ts b/packages/legend-extension-dsl-data-product/src/stores/BaseLayoutState.ts index 5ad4ce4c79f..7dcf7e590d2 100644 --- a/packages/legend-extension-dsl-data-product/src/stores/BaseLayoutState.ts +++ b/packages/legend-extension-dsl-data-product/src/stores/BaseLayoutState.ts @@ -16,10 +16,15 @@ import { action, computed, makeObservable, observable } from 'mobx'; import { isNonNullable } from '@finos/legend-shared'; import { - DATA_PRODUCT_VIEWER_ANCHORS, + DATA_PRODUCT_DEFAULT_ANCHORS, + DATA_PRODUCT_MODELAPG_ANCHORS, + DATA_PRODUCT_VDP_ANCHORS, + generateAnchorForSection, TERMINAL_PRODUCT_VIEWER_ANCHORS, } from './ProductViewerNavigation.js'; import { NAVIGATION_ZONE_SEPARATOR } from '@finos/legend-application'; +import type { V1_DataProduct } from '@finos/legend-graph'; +import type { DataProductViewerState } from './DataProduct/DataProductViewerState.js'; export type WikiPageNavigationCommand = { anchor: string }; @@ -33,6 +38,7 @@ export abstract class BaseLayoutState { private wikiPageAnchorIndex = new Map(); wikiPageNavigationCommand?: WikiPageNavigationCommand | undefined; private wikiPageVisibleAnchors: string[] = []; + private renderedGrids = 0; private wikiPageScrollIntersectionObserver?: IntersectionObserver | undefined; constructor() { @@ -41,6 +47,8 @@ export abstract class BaseLayoutState { | 'wikiPageAnchorIndex' | 'wikiPageVisibleAnchors' | 'updatePageVisibleAnchors' + | 'renderedGrids' + | 'expectedGridCount' >(this, { currentNavigationZone: observable, isExpandedModeEnabled: observable, @@ -49,13 +57,16 @@ export abstract class BaseLayoutState { frame: observable.ref, wikiPageAnchorIndex: observable, wikiPageNavigationCommand: observable.ref, + renderedGrids: observable, isWikiPageFullyRendered: computed, + expectedGridCount: computed, registerWikiPageScrollObserver: action, setCurrentNavigationZone: action, enableExpandedMode: action, setFrame: action, setTopScrollerVisible: action, setWikiPageAnchor: action, + markGridAsRendered: action, unsetWikiPageAnchor: action, setWikiPageAnchorToNavigate: action, updatePageVisibleAnchors: action, @@ -63,15 +74,25 @@ export abstract class BaseLayoutState { } protected abstract getValidAnchors(): string[]; + protected abstract get expectedGridCount(): number; get isWikiPageFullyRendered(): boolean { - return ( + const allAnchorsPresent = Boolean(this.frame) && this.getValidAnchors().every((anchor) => this.wikiPageAnchorIndex.has(anchor), ) && - Array.from(this.wikiPageAnchorIndex.values()).every(isNonNullable) - ); + Array.from(this.wikiPageAnchorIndex.values()).every(isNonNullable); + + const areAllGridsRendered = + this.expectedGridCount === 0 || + this.renderedGrids >= this.expectedGridCount; + + return allAnchorsPresent && areAllGridsRendered; + } + + markGridAsRendered(): void { + this.renderedGrids += 1; } setCurrentNavigationZone(val: string): void { @@ -208,8 +229,41 @@ export abstract class BaseLayoutState { } export class DataProductLayoutState extends BaseLayoutState { + private dataProduct: V1_DataProduct; + private dataProductViewerState!: DataProductViewerState; + + constructor(dataProduct: V1_DataProduct) { + super(); + this.dataProduct = dataProduct; + } + + get expectedGridCount(): number { + return this.dataProduct.accessPointGroups.reduce( + (total, apg) => total + apg.accessPoints.length, + 0, + ); + } + + setViewerState(viewerState: DataProductViewerState): void { + this.dataProductViewerState = viewerState; + } + protected getValidAnchors(): string[] { - return DATA_PRODUCT_VIEWER_ANCHORS; + const anchors = [...DATA_PRODUCT_DEFAULT_ANCHORS] + .concat( + this.dataProductViewerState.isVDP ? [...DATA_PRODUCT_VDP_ANCHORS] : [], + ) + .concat( + this.dataProductViewerState.getModelAccessPointGroup() + ? [...DATA_PRODUCT_MODELAPG_ANCHORS] + : [], + ); + + const apgAnchors = this.dataProduct.accessPointGroups.map((apg) => + generateAnchorForSection(`apg-${apg.id}`), + ); + + return [...anchors, ...apgAnchors]; } } @@ -217,4 +271,8 @@ export class TerminalProductLayoutState extends BaseLayoutState { protected getValidAnchors(): string[] { return TERMINAL_PRODUCT_VIEWER_ANCHORS; } + + protected get expectedGridCount(): number { + return 0; + } } diff --git a/packages/legend-extension-dsl-data-product/src/stores/BaseViewerState.ts b/packages/legend-extension-dsl-data-product/src/stores/BaseViewerState.ts index 161309666eb..212c16113e4 100644 --- a/packages/legend-extension-dsl-data-product/src/stores/BaseViewerState.ts +++ b/packages/legend-extension-dsl-data-product/src/stores/BaseViewerState.ts @@ -74,4 +74,18 @@ export abstract class BaseViewerState< this.layoutState.setCurrentNavigationZone(zone); } } + + copyLinkToClipboard(zone: NavigationZone): void { + const url = `${window.location.origin}${window.location.pathname}#${zone}`; + this.applicationStore.clipboardService + .copyTextToClipboard(url) + .then(() => + this.applicationStore.notificationService.notifySuccess( + 'Copied to clipboard', + undefined, + 2500, + ), + ) + .catch(this.applicationStore.alertUnhandledError); + } } diff --git a/packages/legend-extension-dsl-data-product/src/stores/DataProduct/DataProductViewerState.ts b/packages/legend-extension-dsl-data-product/src/stores/DataProduct/DataProductViewerState.ts index 17ab6fdf075..17b342e7c26 100644 --- a/packages/legend-extension-dsl-data-product/src/stores/DataProduct/DataProductViewerState.ts +++ b/packages/legend-extension-dsl-data-product/src/stores/DataProduct/DataProductViewerState.ts @@ -107,7 +107,14 @@ export class DataProductViewerState extends BaseViewerState< openDataCube?: (sourceData: object) => void; }, ) { - super(product, applicationStore, new DataProductLayoutState(), actions); + super( + product, + applicationStore, + new DataProductLayoutState(product), + actions, + ); + + this.layoutState.setViewerState(this); makeObservable(this, { dataProductArtifact: observable, @@ -148,6 +155,14 @@ export class DataProductViewerState extends BaseViewerState< ); } + getModelAccessPointGroup(): V1_ModelAccessPointGroup | undefined { + const modelapg = this.product.accessPointGroups.find( + (apg): apg is V1_ModelAccessPointGroup => + apg instanceof V1_ModelAccessPointGroup, + ); + return modelapg; + } + get isVDP(): boolean { const vendorProfile = this.dataProductConfig?.vendorTaggedValue.profile; if (!vendorProfile) { @@ -160,13 +175,6 @@ export class DataProductViewerState extends BaseViewerState< ); } - getModelAccessPointGroup(): V1_ModelAccessPointGroup | undefined { - return this.product.accessPointGroups.find( - (apg): apg is V1_ModelAccessPointGroup => - apg instanceof V1_ModelAccessPointGroup, - ); - } - getModelAccessPointDiagrams(): DiagramAnalysisResult[] { const modelAPG = this.getModelAccessPointGroup(); diff --git a/packages/legend-extension-dsl-data-product/src/stores/ProductViewerNavigation.ts b/packages/legend-extension-dsl-data-product/src/stores/ProductViewerNavigation.ts index b4fc9e655e1..4a31391f787 100644 --- a/packages/legend-extension-dsl-data-product/src/stores/ProductViewerNavigation.ts +++ b/packages/legend-extension-dsl-data-product/src/stores/ProductViewerNavigation.ts @@ -17,20 +17,32 @@ import { NAVIGATION_ZONE_SEPARATOR } from '@finos/legend-application'; import type { DiagramAnalysisResult } from '@finos/legend-extension-dsl-diagram'; -export enum DATA_PRODUCT_VIEWER_SECTION { +export enum TERMINAL_PRODUCT_VIEWER_SECTION { + DESCRIPTION = 'description', + PRICE = 'price', +} + +export enum DATA_PRODUCT_DEFAULT_SECTION { DATA_ACCESS = 'data-access', DESCRIPTION = 'description', + SUPPORT_INFO = 'support-info', +} + +export enum DATA_PRODUCT_MODELAPG_SECTION { DIAGRAM_VIEWER = 'diagram-viewer', MODELS_DOCUMENTATION = 'models-documentation', - SUPPORT_INFO = 'support-info', - VENDOR_DATA = 'vendor-data', } -export enum TERMINAL_PRODUCT_VIEWER_SECTION { - DESCRIPTION = 'description', - PRICE = 'price', +export enum DATA_PRODUCT_VDP_SECTION { + VENDOR_DATA = 'vendor-data', } +export const DATA_PRODUCT_VIEWER_SECTION = { + ...DATA_PRODUCT_DEFAULT_SECTION, + ...DATA_PRODUCT_VDP_SECTION, + ...DATA_PRODUCT_MODELAPG_SECTION, +}; + const generateAnchorChunk = (text: string): string => encodeURIComponent( text @@ -42,14 +54,31 @@ const generateAnchorChunk = (text: string): string => export const generateAnchorForSection = (activity: string): string => generateAnchorChunk(activity); -export const DATA_PRODUCT_VIEWER_ANCHORS = Object.values( - DATA_PRODUCT_VIEWER_SECTION, -).map((activity) => generateAnchorForSection(activity)); +export const generateAnchorsFromSections = ( + sections: readonly string[], +): string[] => { + return sections.map((section) => generateAnchorForSection(section)); +}; + +export const DATA_PRODUCT_VIEWER_ANCHORS = generateAnchorsFromSections( + Object.values(DATA_PRODUCT_VIEWER_SECTION), +); + +export const DATA_PRODUCT_VDP_ANCHORS = generateAnchorsFromSections( + Object.values(DATA_PRODUCT_VDP_SECTION), +); + +export const DATA_PRODUCT_DEFAULT_ANCHORS = generateAnchorsFromSections( + Object.values(DATA_PRODUCT_DEFAULT_SECTION), +); -export const TERMINAL_PRODUCT_VIEWER_ANCHORS = Object.values( - TERMINAL_PRODUCT_VIEWER_SECTION, -).map((activity) => generateAnchorForSection(activity)); +export const DATA_PRODUCT_MODELAPG_ANCHORS = generateAnchorsFromSections( + Object.values(DATA_PRODUCT_MODELAPG_SECTION), +); +export const TERMINAL_PRODUCT_VIEWER_ANCHORS = generateAnchorsFromSections( + Object.values(TERMINAL_PRODUCT_VIEWER_SECTION), +); export const extractSectionFromAnchor = (anchor: string): string => decodeURIComponent(anchor); @@ -57,6 +86,6 @@ export const generateAnchorForDiagram = ( diagram: DiagramAnalysisResult, ): string => [ - DATA_PRODUCT_VIEWER_SECTION.DIAGRAM_VIEWER, + DATA_PRODUCT_MODELAPG_SECTION.DIAGRAM_VIEWER, generateAnchorChunk(diagram.title), ].join(NAVIGATION_ZONE_SEPARATOR); diff --git a/packages/legend-lego/src/data-grid/DataGrid.tsx b/packages/legend-lego/src/data-grid/DataGrid.tsx index 9f55b3a3ec8..0b2f5811cd5 100644 --- a/packages/legend-lego/src/data-grid/DataGrid.tsx +++ b/packages/legend-lego/src/data-grid/DataGrid.tsx @@ -72,7 +72,8 @@ export function DataGrid( // NOTE: for test, we don't want to handle the error messages outputed by ag-grid so // we disable enterprise features for now modules={[AllCommunityModule, AllEnterpriseModule]} - onGridReady={() => { + onGridReady={(params) => { + props.onGridReady?.(params); // eslint-disable-next-line no-process-env if (process.env.NODE_ENV !== 'production') { // restore original error logging