diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 7610e79326..ba5e5982d8 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,16 +1,16 @@ { "name": "@labkey/components", - "version": "7.11.0", + "version": "7.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.11.0", + "version": "7.12.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", - "@labkey/api": "1.44.0", + "@labkey/api": "1.44.1", "@testing-library/dom": "~10.4.1", "@testing-library/jest-dom": "~6.9.1", "@testing-library/react": "~16.3.0", @@ -3535,9 +3535,9 @@ } }, "node_modules/@labkey/api": { - "version": "1.44.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.44.0.tgz", - "integrity": "sha512-qfHSWENWN2E1KTRACDj/Qq4Rq/tq8KIr5l6XOnMGLEoepUe8DneAnfcIVD5239oxwFDxMLEFCH83EKeat0C/9g==", + "version": "1.44.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.44.1.tgz", + "integrity": "sha512-VUS4KLfwAsE45A3MnJUU3j97ei0ncQHv6OVVAN3kitID0xe8+mZ7B39zETVye3Dqgwa8TbYvsCp2t46QmBmwVQ==", "license": "Apache-2.0" }, "node_modules/@labkey/build": { diff --git a/packages/components/package.json b/packages/components/package.json index 84725424be..716b7874b0 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.11.0", + "version": "7.12.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ @@ -50,7 +50,7 @@ "homepage": "https://github.com/LabKey/labkey-ui-components#readme", "dependencies": { "@hello-pangea/dnd": "18.0.1", - "@labkey/api": "1.44.0", + "@labkey/api": "1.44.1", "@testing-library/dom": "~10.4.1", "@testing-library/jest-dom": "~6.9.1", "@testing-library/react": "~16.3.0", diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index cedc332aa0..6b72a1954c 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,6 +1,10 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version 7.12.0 +*Released*: 7 January 2026 +- Lineage: add "restricted" property + ### version 7.11.0 *Released*: 6 January 2026 - GitHub Issue 73: Field editor Advanced Settings to allow for non-unique constraint / index diff --git a/packages/components/src/internal/components/lineage/actions.ts b/packages/components/src/internal/components/lineage/actions.ts index fce4a012e7..709b15bccc 100644 --- a/packages/components/src/internal/components/lineage/actions.ts +++ b/packages/components/src/internal/components/lineage/actions.ts @@ -81,6 +81,10 @@ function applyLineageMetadata( meta: metadata[node.lsid], }; + if (!config.meta && node.restricted) { + config.meta = new LineageNodeMetadata({ displayType: 'Restricted' }); + } + // Unfortunately, Immutable.merge converts all types to Immutable types (e.g. {} -> Map) which // is not acceptable. Doing a manual merge... Object.keys(config).forEach(prop => { @@ -239,7 +243,7 @@ export interface LineageAPIWrapper { distance?: number, options?: LineageOptions ) => Promise; - loadNodeMetadata: (lineage: LineageResult) => Array>; + loadNodeMetadata: (lineage: LineageResult) => Promise[]; loadSampleStats: (lineageResult: LineageResult) => Promise; loadSeedResult: (seed: string, container?: string, options?: LineageOptions) => Promise; } @@ -352,7 +356,7 @@ export class ServerLineageAPIWrapper implements LineageAPIWrapper { return lineageResultCache[key]; }; - loadNodeMetadata = (lineage: LineageResult): Array> => { + loadNodeMetadata = (lineage: LineageResult): Promise[] => { // Node metadata does not support nodes with multiple primary keys. These could be supported, however, // each node would require it's own request for the unique keys combination. Also, nodes without any primary // keys cannot be filtered upon and thus are also not supported. @@ -414,7 +418,7 @@ export class TestLineageAPIWrapper extends ServerLineageAPIWrapper { this.result = result; this.metadata = metadata; } - loadNodeMetadata = (lineage: LineageResult): Array> => { + loadNodeMetadata = (lineage: LineageResult): Promise[] => { return this.metadata.map(m => Promise.resolve(m)); }; diff --git a/packages/components/src/internal/components/lineage/models.ts b/packages/components/src/internal/components/lineage/models.ts index 43cb850b4f..46936ac84f 100644 --- a/packages/components/src/internal/components/lineage/models.ts +++ b/packages/components/src/internal/components/lineage/models.ts @@ -287,6 +287,7 @@ export class LineageNode pkFilters: undefined, properties: undefined, queryName: undefined, + restricted: undefined, schemaName: undefined, steps: undefined, type: undefined, @@ -327,6 +328,7 @@ export class LineageNode declare pkFilters: Experiment.LineagePKFilter[]; declare properties: any; declare queryName: string; + declare restricted: boolean; declare schemaName: string; declare steps: List; declare type: string; @@ -352,6 +354,9 @@ export class LineageNode ...{ children: LineageLink.createList(values.children), lsid, + name: values.restricted + ? `Restricted ${values.type === 'Data' ? 'Source' : values.type}` + : values.name, parents: LineageLink.createList(values.parents), steps: List(values.steps?.map(stepProps => new LineageRunStep(stepProps))), }, diff --git a/packages/components/src/internal/components/lineage/node/LineageDetail.tsx b/packages/components/src/internal/components/lineage/node/LineageDetail.tsx index 6b9b7d7ae6..a1ff6a7255 100644 --- a/packages/components/src/internal/components/lineage/node/LineageDetail.tsx +++ b/packages/components/src/internal/components/lineage/node/LineageDetail.tsx @@ -45,8 +45,10 @@ LineageDetailImpl.displayName = 'LineageDetailImpl'; const LineageDetailWithQueryModels = withQueryModels(LineageDetailImpl); export const LineageDetail: FC = memo(({ item }) => { - const queryConfigs = useMemo( - () => ({ + const queryConfigs = useMemo(() => { + if (item.restricted) return {}; + + return { model: { baseFilters: item.pkFilters.map(pkFilter => Filter.create(pkFilter.fieldKey, pkFilter.value)), containerPath: item.containerPath, @@ -55,9 +57,12 @@ export const LineageDetail: FC = memo(({ item }) => { // Must specify '*' columns be requested to resolve "properties" columns requiredColumns: ['*', SAMPLE_STATE_COLOR_COLUMN_NAME, SAMPLE_STATE_TYPE_COLUMN_NAME], }, - }), - [item] - ); + }; + }, [item]); + + if (item.restricted) { + return This {item.name} cannot be viewed.; + } // providing "key" to allow for reload on lsid change return ; diff --git a/packages/components/src/internal/components/lineage/node/LineageNodeDetail.tsx b/packages/components/src/internal/components/lineage/node/LineageNodeDetail.tsx index ff9e1708f3..be76843b96 100644 --- a/packages/components/src/internal/components/lineage/node/LineageNodeDetail.tsx +++ b/packages/components/src/internal/components/lineage/node/LineageNodeDetail.tsx @@ -2,7 +2,7 @@ * Copyright (c) 2016-2020 LabKey Corporation. All rights reserved. No portion of this work may be reproduced in * any form or by any electronic or mechanical means without written permission from LabKey Corporation. */ -import React, { FC, memo, PureComponent, ReactNode, useCallback, useMemo, useState } from 'react'; +import React, { FC, memo, ReactNode, useCallback, useMemo, useState } from 'react'; import { List } from 'immutable'; import { Tab, Tabs } from '../../../Tabs'; @@ -12,6 +12,7 @@ import { isAliquotNode, LineageIOWithMetadata, LineageNode, + LineageNodeCollection, LineageNodeCollectionByType, } from '../models'; import { LineageOptions } from '../types'; @@ -32,47 +33,21 @@ interface LineageNodeDetailProps { seed: string; } -interface LineageNodeDetailState { - stepIdx: number; - tabKey: string; -} - -const initialState: LineageNodeDetailState = { - stepIdx: undefined, - tabKey: 'details', -}; - -export class LineageNodeDetail extends PureComponent { - readonly state: LineageNodeDetailState = initialState; - - componentDidUpdate(prevProps: Readonly): void { - const prevNode = prevProps.node; - const { node } = this.props; +export const LineageNodeDetail: FC = memo(props => { + const { seed, node, highlightNode, lineageOptions } = props; + const { isRun, restricted } = node; + const [stepIdx, setStepIdx] = useState(undefined); + const [tabKey, setTabKey] = useState('details'); + const onBack = useCallback(() => setStepIdx(undefined), []); - if ((prevNode.isRun || node.isRun) && prevNode.lsid !== node.lsid) { - this.setState(initialState); - } + if (isRun && stepIdx !== undefined) { + return ; } - changeTab = (tabKey: string): void => { - this.setState({ tabKey }); - }; - - selectStep = (stepIdx: number): void => { - this.setState({ stepIdx }); - }; - - render(): ReactNode { - const { seed, node, highlightNode, lineageOptions } = this.props; - const { stepIdx, tabKey } = this.state; - - if (node.isRun && stepIdx !== undefined) { - return this.selectStep(undefined)} stepIdx={stepIdx} />; - } - - const nodeDetails = ( - <> - + const nodeDetails = ( + <> + + {!restricted && ( - - ); - - return ( -
- - {node.isRun ? ( - - - {nodeDetails} - - - - - - - ) : ( - nodeDetails - )} -
- ); - } -} + )} + + ); + + return ( +
+ + {isRun && !restricted ? ( + + + {nodeDetails} + + + + + + + ) : ( + nodeDetails + )} +
+ ); +}); +LineageNodeDetail.displayName = 'LineageNodeDetail'; interface ClusterNodeDetailProps { highlightNode?: string; @@ -113,52 +89,43 @@ interface ClusterNodeDetailProps { parentNodeName?: string; } -export class ClusterNodeDetail extends PureComponent { - static getGroupDisplayName(nodesByType, groupName, parentNodeName?) { - const group = nodesByType[groupName]; - const isAliquot = isAliquotNode(group); - const aliquotDisplayName = (parentNodeName ? parentNodeName + ' ' : '') + 'Aliquots'; - return isAliquot ? aliquotDisplayName : group.displayType; - } +function getGroupDisplayName(nodeCollection: LineageNodeCollection, parentNodeName?: string): string { + if (isAliquotNode(nodeCollection)) return (parentNodeName ? parentNodeName + ' ' : '') + 'Aliquots'; + return nodeCollection.displayType; +} - render(): ReactNode { - const { highlightNode, nodes, options, parentNodeName } = this.props; - - const nodesByType = this.props.nodesByType ?? createLineageNodeCollections(nodes, options); - const groups = Object.keys(nodesByType).sort(); - - let iconURL; - let title; - if (groups.length === 1) { - title = nodes.length + ' ' + ClusterNodeDetail.getGroupDisplayName(nodesByType, groups[0]); - iconURL = nodes[0].iconProps.iconURL; - } else { - title = nodes.length + ' items of different types'; - iconURL = 'default'; - } - - return ( -
- - {groups.map(groupName => { - const groupDisplayName = ClusterNodeDetail.getGroupDisplayName( - nodesByType, - groupName, - parentNodeName - ); - return ( - - ); - })} -
- ); +export const ClusterNodeDetail: FC = memo(props => { + const { highlightNode, nodes, options, parentNodeName } = props; + const { groups, nodesByType } = useMemo(() => { + const nodesByType = props.nodesByType ?? createLineageNodeCollections(nodes, options); + return { groups: Object.keys(nodesByType).sort(), nodesByType }; + }, [nodes, options, props.nodesByType]); + + let iconURL: string; + let title: ReactNode; + if (groups.length === 1) { + title = nodes.length + ' ' + getGroupDisplayName(nodesByType[groups[0]]); + iconURL = nodes[0].iconProps.iconURL; + } else { + title = nodes.length + ' items of different types'; + iconURL = 'default'; } -} + + return ( +
+ + {groups.map(groupName => ( + + ))} +
+ ); +}); +ClusterNodeDetail.displayName = 'ClusterNodeDetail'; interface RunStepNodeDetailProps { node: LineageNode; diff --git a/packages/components/src/internal/components/lineage/node/LineageNodeDetailFactory.tsx b/packages/components/src/internal/components/lineage/node/LineageNodeDetailFactory.tsx index 4945cdd116..00971e5185 100644 --- a/packages/components/src/internal/components/lineage/node/LineageNodeDetailFactory.tsx +++ b/packages/components/src/internal/components/lineage/node/LineageNodeDetailFactory.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent, ReactNode } from 'react'; +import React, { FC, memo } from 'react'; import { LineageOptions } from '../types'; import { isBasicNode, isClusterNode, isCombinedNode, Lineage, VisGraphNodeType } from '../models'; @@ -14,68 +14,62 @@ export interface LineageNodeDetailFactoryProps { selectedNodes: VisGraphNodeType[]; } -export class LineageNodeDetailFactory extends PureComponent { - render(): ReactNode { - const { highlightNode, lineage, lineageOptions, selectedNodes } = this.props; +export const LineageNodeDetailFactory: FC = memo(props => { + const { highlightNode, lineage, lineageOptions, selectedNodes } = props; - if (!lineage || lineage.error) { - return null; - } + if (!lineage || lineage.error) return null; - if (!lineage.isLoaded()) { - // Render selected node if seed has been pre-fetched - if (lineage.isSeedLoaded()) { - return ( - - ); - } + if (!lineage.isLoaded()) { + // Render selected node if seed has been pre-fetched + if (!lineage.isSeedLoaded()) { return ; } - if (!selectedNodes || selectedNodes.length === 0) { - return Select a node from the graph to view the details.; - } + const node = lineage.seedResult.nodes.get(lineage.seed); + return ; + } - if (selectedNodes.length === 1) { - const node = selectedNodes[0]; + if (!selectedNodes || selectedNodes.length === 0) { + return Select a node from the graph to view the details.; + } - if (isBasicNode(node)) { - return ( - - ); - } else if (isCombinedNode(node)) { - return ( - - ); - } else if (isClusterNode(node)) { - return ( - n.kind === 'node' && n.lineageNode)} - options={lineageOptions} - /> - ); - } + if (selectedNodes.length === 1) { + const node = selectedNodes[0]; - throw new Error('unknown node kind'); + if (isBasicNode(node)) { + return ( + + ); + } else if (isCombinedNode(node)) { + return ( + + ); + } else if (isClusterNode(node)) { + return ( + n.kind === 'node' && n.lineageNode)} + options={lineageOptions} + /> + ); } - return
Multiple selected nodes
; + throw new Error('unknown node kind'); } -} + + return
Multiple selected nodes
; +}); +LineageNodeDetailFactory.displayName = 'LineageNodeDetailFactory'; diff --git a/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx b/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx index 3166d75907..5fcf1b0684 100644 --- a/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx +++ b/packages/components/src/internal/components/lineage/node/NodeDetailHeader.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren, PureComponent, ReactNode } from 'react'; +import React, { FC, memo, PropsWithChildren, ReactNode } from 'react'; import { LineageNode } from '../models'; import { LineageDataLink } from '../LineageDataLink'; @@ -9,58 +9,51 @@ export interface DetailHeaderProps extends PropsWithChildren { iconSrc: string; } -export class DetailHeader extends PureComponent { - render(): ReactNode { - const { children, header, iconSrc } = this.props; - - return ( -
- - - -
-
-

{header}

-
-
{children}
-
+export const DetailHeader: FC = memo(({ children, header, iconSrc }) => ( +
+ + + +
+
+

{header}

- ); - } -} +
{children}
+
+
+)); +DetailHeader.displayName = 'DetailHeader'; export interface NodeDetailHeaderProps { node: LineageNode; seed?: string; } -export class NodeDetailHeader extends PureComponent { - render(): ReactNode { - const { node, seed } = this.props; - const { links, meta, name } = node; - const lineageUrl = links.lineage; - const isSeed = seed === node.lsid; - - const aliases = meta?.aliases; - const description = meta?.description; - const displayType = meta?.displayType; - - const header = ( - <> - {(lineageUrl && !isSeed && {name}) || name} -
- Overview - Lineage -
- - ); - - return ( - - {displayType &&
{displayType}
} - {aliases &&
{aliases.join(', ')}
} - {description &&
{description}
} -
- ); - } -} +export const NodeDetailHeader: FC = memo(({ node, seed }) => { + const { links, meta, name } = node; + const lineageUrl = links.lineage; + const isSeed = seed === node.lsid; + + const aliases = meta?.aliases; + const description = meta?.description; + const displayType = meta?.displayType; + + const header = ( + <> + {(lineageUrl && !isSeed && {name}) || name} +
+ Overview + Lineage +
+ + ); + + return ( + + {displayType &&
{displayType}
} + {aliases &&
{aliases.join(', ')}
} + {description &&
{description}
} +
+ ); +}); +NodeDetailHeader.displayName = 'NodeDetailHeader'; diff --git a/packages/components/src/internal/components/lineage/utils.ts b/packages/components/src/internal/components/lineage/utils.ts index 8f4edc23ad..0c975ed469 100644 --- a/packages/components/src/internal/components/lineage/utils.ts +++ b/packages/components/src/internal/components/lineage/utils.ts @@ -105,6 +105,8 @@ export function resolveIconAndShapeForNode( if (queryInfoIconURL && queryInfoIconURL !== DEFAULT_ICON_URL) { iconURL = queryInfoIconURL.toLowerCase(); + } else if (item?.restricted) { + iconURL = 'lock'; } else if (item) { const schemaName = item.schemaName?.toLowerCase() ?? ''; const queryName = item.queryName?.toLowerCase() ?? ''; diff --git a/packages/components/src/internal/url/URLResolver.ts b/packages/components/src/internal/url/URLResolver.ts index d6aded2486..1e37874a8c 100644 --- a/packages/components/src/internal/url/URLResolver.ts +++ b/packages/components/src/internal/url/URLResolver.ts @@ -38,7 +38,7 @@ let resolvers = OrderedSet(); let urlMappers: List = List(); -export type URLMapperResolverValue = AppURL | string | boolean; +export type URLMapperResolverValue = AppURL | boolean | string; export type URLMapperResolver = ( url: string, row: Map, @@ -618,6 +618,10 @@ export class URLResolver { overview: item.url, }; + if (item.restricted) { + return metadata; + } + if (item.type && acceptedTypes.indexOf(item.type) >= 0 && (item.queryName || item.cpasType || item.lsid)) { // Issue 48836: Resolve lineage item URL from queryName if available let name = item.queryName;