Skip to content

Commit a209591

Browse files
authored
Lineage: add "restricted" property (#1922)
1 parent 02a8168 commit a209591

11 files changed

Lines changed: 205 additions & 227 deletions

File tree

packages/components/package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/components/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@labkey/components",
3-
"version": "7.11.0",
3+
"version": "7.12.0",
44
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
55
"sideEffects": false,
66
"files": [
@@ -50,7 +50,7 @@
5050
"homepage": "https://github.com/LabKey/labkey-ui-components#readme",
5151
"dependencies": {
5252
"@hello-pangea/dnd": "18.0.1",
53-
"@labkey/api": "1.44.0",
53+
"@labkey/api": "1.44.1",
5454
"@testing-library/dom": "~10.4.1",
5555
"@testing-library/jest-dom": "~6.9.1",
5656
"@testing-library/react": "~16.3.0",

packages/components/releaseNotes/components.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 7.12.0
5+
*Released*: 7 January 2026
6+
- Lineage: add "restricted" property
7+
48
### version 7.11.0
59
*Released*: 6 January 2026
610
- GitHub Issue 73: Field editor Advanced Settings to allow for non-unique constraint / index

packages/components/src/internal/components/lineage/actions.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ function applyLineageMetadata(
8181
meta: metadata[node.lsid],
8282
};
8383

84+
if (!config.meta && node.restricted) {
85+
config.meta = new LineageNodeMetadata({ displayType: 'Restricted' });
86+
}
87+
8488
// Unfortunately, Immutable.merge converts all types to Immutable types (e.g. {} -> Map) which
8589
// is not acceptable. Doing a manual merge...
8690
Object.keys(config).forEach(prop => {
@@ -239,7 +243,7 @@ export interface LineageAPIWrapper {
239243
distance?: number,
240244
options?: LineageOptions
241245
) => Promise<LineageResult>;
242-
loadNodeMetadata: (lineage: LineageResult) => Array<Promise<ISelectRowsResult>>;
246+
loadNodeMetadata: (lineage: LineageResult) => Promise<ISelectRowsResult>[];
243247
loadSampleStats: (lineageResult: LineageResult) => Promise<any>;
244248
loadSeedResult: (seed: string, container?: string, options?: LineageOptions) => Promise<LineageResult>;
245249
}
@@ -352,7 +356,7 @@ export class ServerLineageAPIWrapper implements LineageAPIWrapper {
352356
return lineageResultCache[key];
353357
};
354358

355-
loadNodeMetadata = (lineage: LineageResult): Array<Promise<ISelectRowsResult>> => {
359+
loadNodeMetadata = (lineage: LineageResult): Promise<ISelectRowsResult>[] => {
356360
// Node metadata does not support nodes with multiple primary keys. These could be supported, however,
357361
// each node would require it's own request for the unique keys combination. Also, nodes without any primary
358362
// keys cannot be filtered upon and thus are also not supported.
@@ -414,7 +418,7 @@ export class TestLineageAPIWrapper extends ServerLineageAPIWrapper {
414418
this.result = result;
415419
this.metadata = metadata;
416420
}
417-
loadNodeMetadata = (lineage: LineageResult): Array<Promise<ISelectRowsResult>> => {
421+
loadNodeMetadata = (lineage: LineageResult): Promise<ISelectRowsResult>[] => {
418422
return this.metadata.map(m => Promise.resolve(m));
419423
};
420424

packages/components/src/internal/components/lineage/models.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ export class LineageNode
287287
pkFilters: undefined,
288288
properties: undefined,
289289
queryName: undefined,
290+
restricted: undefined,
290291
schemaName: undefined,
291292
steps: undefined,
292293
type: undefined,
@@ -327,6 +328,7 @@ export class LineageNode
327328
declare pkFilters: Experiment.LineagePKFilter[];
328329
declare properties: any;
329330
declare queryName: string;
331+
declare restricted: boolean;
330332
declare schemaName: string;
331333
declare steps: List<LineageRunStep>;
332334
declare type: string;
@@ -352,6 +354,9 @@ export class LineageNode
352354
...{
353355
children: LineageLink.createList(values.children),
354356
lsid,
357+
name: values.restricted
358+
? `Restricted ${values.type === 'Data' ? 'Source' : values.type}`
359+
: values.name,
355360
parents: LineageLink.createList(values.parents),
356361
steps: List(values.steps?.map(stepProps => new LineageRunStep(stepProps))),
357362
},

packages/components/src/internal/components/lineage/node/LineageDetail.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ LineageDetailImpl.displayName = 'LineageDetailImpl';
4545
const LineageDetailWithQueryModels = withQueryModels<LineageDetailProps>(LineageDetailImpl);
4646

4747
export const LineageDetail: FC<LineageDetailProps> = memo(({ item }) => {
48-
const queryConfigs = useMemo<QueryConfigMap>(
49-
() => ({
48+
const queryConfigs = useMemo<QueryConfigMap>(() => {
49+
if (item.restricted) return {};
50+
51+
return {
5052
model: {
5153
baseFilters: item.pkFilters.map(pkFilter => Filter.create(pkFilter.fieldKey, pkFilter.value)),
5254
containerPath: item.containerPath,
@@ -55,9 +57,12 @@ export const LineageDetail: FC<LineageDetailProps> = memo(({ item }) => {
5557
// Must specify '*' columns be requested to resolve "properties" columns
5658
requiredColumns: ['*', SAMPLE_STATE_COLOR_COLUMN_NAME, SAMPLE_STATE_TYPE_COLUMN_NAME],
5759
},
58-
}),
59-
[item]
60-
);
60+
};
61+
}, [item]);
62+
63+
if (item.restricted) {
64+
return <Alert bsStyle="info">This {item.name} cannot be viewed.</Alert>;
65+
}
6166

6267
// providing "key" to allow for reload on lsid change
6368
return <LineageDetailWithQueryModels autoLoad item={item} key={item.lsid} queryConfigs={queryConfigs} />;

packages/components/src/internal/components/lineage/node/LineageNodeDetail.tsx

Lines changed: 73 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Copyright (c) 2016-2020 LabKey Corporation. All rights reserved. No portion of this work may be reproduced in
33
* any form or by any electronic or mechanical means without written permission from LabKey Corporation.
44
*/
5-
import React, { FC, memo, PureComponent, ReactNode, useCallback, useMemo, useState } from 'react';
5+
import React, { FC, memo, ReactNode, useCallback, useMemo, useState } from 'react';
66
import { List } from 'immutable';
77

88
import { Tab, Tabs } from '../../../Tabs';
@@ -12,6 +12,7 @@ import {
1212
isAliquotNode,
1313
LineageIOWithMetadata,
1414
LineageNode,
15+
LineageNodeCollection,
1516
LineageNodeCollectionByType,
1617
} from '../models';
1718
import { LineageOptions } from '../types';
@@ -32,47 +33,21 @@ interface LineageNodeDetailProps {
3233
seed: string;
3334
}
3435

35-
interface LineageNodeDetailState {
36-
stepIdx: number;
37-
tabKey: string;
38-
}
39-
40-
const initialState: LineageNodeDetailState = {
41-
stepIdx: undefined,
42-
tabKey: 'details',
43-
};
44-
45-
export class LineageNodeDetail extends PureComponent<LineageNodeDetailProps, LineageNodeDetailState> {
46-
readonly state: LineageNodeDetailState = initialState;
47-
48-
componentDidUpdate(prevProps: Readonly<LineageNodeDetailProps>): void {
49-
const prevNode = prevProps.node;
50-
const { node } = this.props;
36+
export const LineageNodeDetail: FC<LineageNodeDetailProps> = memo(props => {
37+
const { seed, node, highlightNode, lineageOptions } = props;
38+
const { isRun, restricted } = node;
39+
const [stepIdx, setStepIdx] = useState<number>(undefined);
40+
const [tabKey, setTabKey] = useState<string>('details');
41+
const onBack = useCallback(() => setStepIdx(undefined), []);
5142

52-
if ((prevNode.isRun || node.isRun) && prevNode.lsid !== node.lsid) {
53-
this.setState(initialState);
54-
}
43+
if (isRun && stepIdx !== undefined) {
44+
return <RunStepNodeDetail node={node} onBack={onBack} stepIdx={stepIdx} />;
5545
}
5646

57-
changeTab = (tabKey: string): void => {
58-
this.setState({ tabKey });
59-
};
60-
61-
selectStep = (stepIdx: number): void => {
62-
this.setState({ stepIdx });
63-
};
64-
65-
render(): ReactNode {
66-
const { seed, node, highlightNode, lineageOptions } = this.props;
67-
const { stepIdx, tabKey } = this.state;
68-
69-
if (node.isRun && stepIdx !== undefined) {
70-
return <RunStepNodeDetail node={node} onBack={() => this.selectStep(undefined)} stepIdx={stepIdx} />;
71-
}
72-
73-
const nodeDetails = (
74-
<>
75-
<LineageDetail item={node} />
47+
const nodeDetails = (
48+
<>
49+
<LineageDetail item={node} />
50+
{!restricted && (
7651
<LineageSummary
7752
{...lineageOptions}
7853
containerPath={node.containerPath}
@@ -81,29 +56,30 @@ export class LineageNodeDetail extends PureComponent<LineageNodeDetailProps, Lin
8156
lsid={node.lsid}
8257
prefetchSeed={false}
8358
/>
84-
</>
85-
);
86-
87-
return (
88-
<div className="lineage-node-detail">
89-
<NodeDetailHeader node={node} seed={seed} />
90-
{node.isRun ? (
91-
<Tabs activeKey={tabKey} onSelect={this.changeTab}>
92-
<Tab eventKey="details" title="Details">
93-
{nodeDetails}
94-
</Tab>
95-
<Tab eventKey="runProperties" title="Run Properties">
96-
<DetailsListSteps node={node} onSelect={this.selectStep} />
97-
<DetailsListLineageIO item={node} />
98-
</Tab>
99-
</Tabs>
100-
) : (
101-
nodeDetails
102-
)}
103-
</div>
104-
);
105-
}
106-
}
59+
)}
60+
</>
61+
);
62+
63+
return (
64+
<div className="lineage-node-detail">
65+
<NodeDetailHeader node={node} seed={seed} />
66+
{isRun && !restricted ? (
67+
<Tabs activeKey={tabKey} onSelect={setTabKey}>
68+
<Tab eventKey="details" title="Details">
69+
{nodeDetails}
70+
</Tab>
71+
<Tab eventKey="runProperties" title="Run Properties">
72+
<DetailsListSteps node={node} onSelect={setStepIdx} />
73+
<DetailsListLineageIO item={node} />
74+
</Tab>
75+
</Tabs>
76+
) : (
77+
nodeDetails
78+
)}
79+
</div>
80+
);
81+
});
82+
LineageNodeDetail.displayName = 'LineageNodeDetail';
10783

10884
interface ClusterNodeDetailProps {
10985
highlightNode?: string;
@@ -113,52 +89,43 @@ interface ClusterNodeDetailProps {
11389
parentNodeName?: string;
11490
}
11591

116-
export class ClusterNodeDetail extends PureComponent<ClusterNodeDetailProps> {
117-
static getGroupDisplayName(nodesByType, groupName, parentNodeName?) {
118-
const group = nodesByType[groupName];
119-
const isAliquot = isAliquotNode(group);
120-
const aliquotDisplayName = (parentNodeName ? parentNodeName + ' ' : '') + 'Aliquots';
121-
return isAliquot ? aliquotDisplayName : group.displayType;
122-
}
92+
function getGroupDisplayName(nodeCollection: LineageNodeCollection, parentNodeName?: string): string {
93+
if (isAliquotNode(nodeCollection)) return (parentNodeName ? parentNodeName + ' ' : '') + 'Aliquots';
94+
return nodeCollection.displayType;
95+
}
12396

124-
render(): ReactNode {
125-
const { highlightNode, nodes, options, parentNodeName } = this.props;
126-
127-
const nodesByType = this.props.nodesByType ?? createLineageNodeCollections(nodes, options);
128-
const groups = Object.keys(nodesByType).sort();
129-
130-
let iconURL;
131-
let title;
132-
if (groups.length === 1) {
133-
title = nodes.length + ' ' + ClusterNodeDetail.getGroupDisplayName(nodesByType, groups[0]);
134-
iconURL = nodes[0].iconProps.iconURL;
135-
} else {
136-
title = nodes.length + ' items of different types';
137-
iconURL = 'default';
138-
}
139-
140-
return (
141-
<div className="cluster-node-detail">
142-
<DetailHeader header={title} iconSrc={iconURL} />
143-
{groups.map(groupName => {
144-
const groupDisplayName = ClusterNodeDetail.getGroupDisplayName(
145-
nodesByType,
146-
groupName,
147-
parentNodeName
148-
);
149-
return (
150-
<DetailsListNodes
151-
highlightNode={highlightNode}
152-
key={groupName}
153-
nodes={nodesByType[groupName]}
154-
title={groupDisplayName}
155-
/>
156-
);
157-
})}
158-
</div>
159-
);
97+
export const ClusterNodeDetail: FC<ClusterNodeDetailProps> = memo(props => {
98+
const { highlightNode, nodes, options, parentNodeName } = props;
99+
const { groups, nodesByType } = useMemo(() => {
100+
const nodesByType = props.nodesByType ?? createLineageNodeCollections(nodes, options);
101+
return { groups: Object.keys(nodesByType).sort(), nodesByType };
102+
}, [nodes, options, props.nodesByType]);
103+
104+
let iconURL: string;
105+
let title: ReactNode;
106+
if (groups.length === 1) {
107+
title = nodes.length + ' ' + getGroupDisplayName(nodesByType[groups[0]]);
108+
iconURL = nodes[0].iconProps.iconURL;
109+
} else {
110+
title = nodes.length + ' items of different types';
111+
iconURL = 'default';
160112
}
161-
}
113+
114+
return (
115+
<div className="cluster-node-detail">
116+
<DetailHeader header={title} iconSrc={iconURL} />
117+
{groups.map(groupName => (
118+
<DetailsListNodes
119+
highlightNode={highlightNode}
120+
key={groupName}
121+
nodes={nodesByType[groupName]}
122+
title={getGroupDisplayName(nodesByType[groupName], parentNodeName)}
123+
/>
124+
))}
125+
</div>
126+
);
127+
});
128+
ClusterNodeDetail.displayName = 'ClusterNodeDetail';
162129

163130
interface RunStepNodeDetailProps {
164131
node: LineageNode;

0 commit comments

Comments
 (0)