Skip to content
Merged
12 changes: 6 additions & 6 deletions packages/components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/components/package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 7 additions & 3 deletions packages/components/src/internal/components/lineage/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -239,7 +243,7 @@ export interface LineageAPIWrapper {
distance?: number,
options?: LineageOptions
) => Promise<LineageResult>;
loadNodeMetadata: (lineage: LineageResult) => Array<Promise<ISelectRowsResult>>;
loadNodeMetadata: (lineage: LineageResult) => Promise<ISelectRowsResult>[];
loadSampleStats: (lineageResult: LineageResult) => Promise<any>;
loadSeedResult: (seed: string, container?: string, options?: LineageOptions) => Promise<LineageResult>;
}
Expand Down Expand Up @@ -352,7 +356,7 @@ export class ServerLineageAPIWrapper implements LineageAPIWrapper {
return lineageResultCache[key];
};

loadNodeMetadata = (lineage: LineageResult): Array<Promise<ISelectRowsResult>> => {
loadNodeMetadata = (lineage: LineageResult): Promise<ISelectRowsResult>[] => {
// 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.
Expand Down Expand Up @@ -414,7 +418,7 @@ export class TestLineageAPIWrapper extends ServerLineageAPIWrapper {
this.result = result;
this.metadata = metadata;
}
loadNodeMetadata = (lineage: LineageResult): Array<Promise<ISelectRowsResult>> => {
loadNodeMetadata = (lineage: LineageResult): Promise<ISelectRowsResult>[] => {
return this.metadata.map(m => Promise.resolve(m));
};

Expand Down
5 changes: 5 additions & 0 deletions packages/components/src/internal/components/lineage/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export class LineageNode
pkFilters: undefined,
properties: undefined,
queryName: undefined,
restricted: undefined,
schemaName: undefined,
steps: undefined,
type: undefined,
Expand Down Expand Up @@ -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<LineageRunStep>;
declare type: string;
Expand All @@ -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))),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ LineageDetailImpl.displayName = 'LineageDetailImpl';
const LineageDetailWithQueryModels = withQueryModels<LineageDetailProps>(LineageDetailImpl);

export const LineageDetail: FC<LineageDetailProps> = memo(({ item }) => {
const queryConfigs = useMemo<QueryConfigMap>(
() => ({
const queryConfigs = useMemo<QueryConfigMap>(() => {
if (item.restricted) return {};

return {
model: {
baseFilters: item.pkFilters.map(pkFilter => Filter.create(pkFilter.fieldKey, pkFilter.value)),
containerPath: item.containerPath,
Expand All @@ -55,9 +57,12 @@ export const LineageDetail: FC<LineageDetailProps> = 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 <Alert bsStyle="info">This {item.name} cannot be viewed.</Alert>;
}

// providing "key" to allow for reload on lsid change
return <LineageDetailWithQueryModels autoLoad item={item} key={item.lsid} queryConfigs={queryConfigs} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -12,6 +12,7 @@ import {
isAliquotNode,
LineageIOWithMetadata,
LineageNode,
LineageNodeCollection,
LineageNodeCollectionByType,
} from '../models';
import { LineageOptions } from '../types';
Expand All @@ -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<LineageNodeDetailProps, LineageNodeDetailState> {
readonly state: LineageNodeDetailState = initialState;

componentDidUpdate(prevProps: Readonly<LineageNodeDetailProps>): void {
const prevNode = prevProps.node;
const { node } = this.props;
export const LineageNodeDetail: FC<LineageNodeDetailProps> = memo(props => {
const { seed, node, highlightNode, lineageOptions } = props;
const { isRun, restricted } = node;
const [stepIdx, setStepIdx] = useState<number>(undefined);
const [tabKey, setTabKey] = useState<string>('details');
const onBack = useCallback(() => setStepIdx(undefined), []);

if ((prevNode.isRun || node.isRun) && prevNode.lsid !== node.lsid) {
this.setState(initialState);
}
if (isRun && stepIdx !== undefined) {
return <RunStepNodeDetail node={node} onBack={onBack} stepIdx={stepIdx} />;
}

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 <RunStepNodeDetail node={node} onBack={() => this.selectStep(undefined)} stepIdx={stepIdx} />;
}

const nodeDetails = (
<>
<LineageDetail item={node} />
const nodeDetails = (
<>
<LineageDetail item={node} />
{!restricted && (
<LineageSummary
{...lineageOptions}
containerPath={node.containerPath}
Expand All @@ -81,29 +56,30 @@ export class LineageNodeDetail extends PureComponent<LineageNodeDetailProps, Lin
lsid={node.lsid}
prefetchSeed={false}
/>
</>
);

return (
<div className="lineage-node-detail">
<NodeDetailHeader node={node} seed={seed} />
{node.isRun ? (
<Tabs activeKey={tabKey} onSelect={this.changeTab}>
<Tab eventKey="details" title="Details">
{nodeDetails}
</Tab>
<Tab eventKey="runProperties" title="Run Properties">
<DetailsListSteps node={node} onSelect={this.selectStep} />
<DetailsListLineageIO item={node} />
</Tab>
</Tabs>
) : (
nodeDetails
)}
</div>
);
}
}
)}
</>
);

return (
<div className="lineage-node-detail">
<NodeDetailHeader node={node} seed={seed} />
{isRun && !restricted ? (
<Tabs activeKey={tabKey} onSelect={setTabKey}>
<Tab eventKey="details" title="Details">
{nodeDetails}
</Tab>
<Tab eventKey="runProperties" title="Run Properties">
<DetailsListSteps node={node} onSelect={setStepIdx} />
<DetailsListLineageIO item={node} />
</Tab>
</Tabs>
) : (
nodeDetails
)}
</div>
);
});
LineageNodeDetail.displayName = 'LineageNodeDetail';

interface ClusterNodeDetailProps {
highlightNode?: string;
Expand All @@ -113,52 +89,43 @@ interface ClusterNodeDetailProps {
parentNodeName?: string;
}

export class ClusterNodeDetail extends PureComponent<ClusterNodeDetailProps> {
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 (
<div className="cluster-node-detail">
<DetailHeader header={title} iconSrc={iconURL} />
{groups.map(groupName => {
const groupDisplayName = ClusterNodeDetail.getGroupDisplayName(
nodesByType,
groupName,
parentNodeName
);
return (
<DetailsListNodes
highlightNode={highlightNode}
key={groupName}
nodes={nodesByType[groupName]}
title={groupDisplayName}
/>
);
})}
</div>
);
export const ClusterNodeDetail: FC<ClusterNodeDetailProps> = 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 (
<div className="cluster-node-detail">
<DetailHeader header={title} iconSrc={iconURL} />
{groups.map(groupName => (
<DetailsListNodes
highlightNode={highlightNode}
key={groupName}
nodes={nodesByType[groupName]}
title={getGroupDisplayName(nodesByType[groupName], parentNodeName)}
/>
))}
</div>
);
});
ClusterNodeDetail.displayName = 'ClusterNodeDetail';

interface RunStepNodeDetailProps {
node: LineageNode;
Expand Down
Loading