@truedat/dd 8.13.6 → 8.13.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/components/GraphGenerationDialog.js +125 -0
- package/src/components/GraphGenerationNotices.js +67 -0
- package/src/components/GraphLoader.js +12 -17
- package/src/components/GraphRedirector.js +4 -4
- package/src/components/LineageGraph.js +38 -49
- package/src/components/LineageRoutes.js +20 -13
- package/src/components/__tests__/GraphGenerationDialog.spec.js +117 -0
- package/src/components/__tests__/GraphGenerationNotices.spec.js +60 -0
- package/src/components/__tests__/GraphGenerationTranslations.spec.js +74 -0
- package/src/components/__tests__/GraphLoader.spec.js +83 -0
- package/src/components/__tests__/LineageGraphInteraction.spec.js +78 -7
- package/src/components/__tests__/__snapshots__/LineageRoutes.spec.js.snap +12 -1
- package/src/components/__tests__/fixtures/graphGenerationMessages.json +28 -0
- package/src/messages/en.js +0 -29
- package/src/messages/es.js +0 -30
- package/src/reducers/__tests__/graphGenerationState.spec.js +113 -0
- package/src/reducers/graphGenerationState.js +113 -0
- package/src/reducers/graphLoading.js +2 -0
- package/src/reducers/index.js +2 -0
- package/src/routines.js +3 -0
- package/src/sagas/__tests__/createGraph.spec.js +57 -122
- package/src/sagas/__tests__/fetchGraph.spec.js +19 -0
- package/src/sagas/createGraph.js +13 -65
- package/src/sagas/fetchGraph.js +6 -38
- package/src/sagas/graph/__tests__/backgroundGraphs.spec.js +22 -0
- package/src/sagas/graph/__tests__/generation.spec.js +247 -0
- package/src/sagas/graph/__tests__/polling.spec.js +88 -0
- package/src/sagas/graph/generation.js +99 -0
- package/src/sagas/graph/polling.js +26 -35
- package/src/sagas/index.js +2 -0
- package/src/styles/LineageGraph.less +117 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/dd",
|
|
3
|
-
"version": "8.13.
|
|
3
|
+
"version": "8.13.7",
|
|
4
4
|
"description": "Truedat Web Data Dictionary",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@testing-library/jest-dom": "^6.6.3",
|
|
55
55
|
"@testing-library/react": "^16.3.0",
|
|
56
56
|
"@testing-library/user-event": "^14.6.1",
|
|
57
|
-
"@truedat/test": "8.13.
|
|
57
|
+
"@truedat/test": "8.13.7",
|
|
58
58
|
"identity-obj-proxy": "^3.0.0",
|
|
59
59
|
"jest": "^29.7.0",
|
|
60
60
|
"redux-saga-test-plan": "^4.0.6"
|
|
@@ -90,5 +90,5 @@
|
|
|
90
90
|
"svg-pan-zoom": "^3.6.2",
|
|
91
91
|
"swr": "^2.3.3"
|
|
92
92
|
},
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "704dcfcba34b6094a368a93059de7f4937502f32"
|
|
94
94
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import { FormattedMessage, useIntl } from "react-intl";
|
|
4
|
+
import { Button, Modal } from "semantic-ui-react";
|
|
5
|
+
|
|
6
|
+
export const GraphGenerationDialog = ({
|
|
7
|
+
open,
|
|
8
|
+
requestKey,
|
|
9
|
+
onBackground,
|
|
10
|
+
onClose = onBackground,
|
|
11
|
+
}) => {
|
|
12
|
+
const { formatMessage } = useIntl();
|
|
13
|
+
const [delayed, setDelayed] = useState(false);
|
|
14
|
+
const [waiting, setWaiting] = useState(false);
|
|
15
|
+
const previousFocus = useRef(null);
|
|
16
|
+
const attachDialog = useCallback((node) => {
|
|
17
|
+
if (node) {
|
|
18
|
+
previousFocus.current = document.activeElement;
|
|
19
|
+
node.querySelector("button")?.focus();
|
|
20
|
+
} else if (previousFocus.current?.isConnected) {
|
|
21
|
+
previousFocus.current.focus();
|
|
22
|
+
}
|
|
23
|
+
}, []);
|
|
24
|
+
const trapFocus = (event) => {
|
|
25
|
+
if (event.key !== "Tab") return;
|
|
26
|
+
const buttons = [
|
|
27
|
+
...event.currentTarget.querySelectorAll("button:not(:disabled)"),
|
|
28
|
+
];
|
|
29
|
+
const first = buttons[0];
|
|
30
|
+
const last = buttons[buttons.length - 1];
|
|
31
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
last?.focus();
|
|
34
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
first?.focus();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
setDelayed(false);
|
|
42
|
+
setWaiting(false);
|
|
43
|
+
if (!open) return undefined;
|
|
44
|
+
// One timer per request; polling updates do not restart the explanation.
|
|
45
|
+
const timeout = setTimeout(() => setDelayed(true), 3000);
|
|
46
|
+
return () => clearTimeout(timeout);
|
|
47
|
+
}, [open, requestKey]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Modal
|
|
51
|
+
open={open}
|
|
52
|
+
basic
|
|
53
|
+
size="tiny"
|
|
54
|
+
className={`lineage-generation-dialog${delayed ? " lineage-generation-dialog--expanded" : ""}`}
|
|
55
|
+
dimmer={{ className: "lineage-generation-dimmer" }}
|
|
56
|
+
onClose={onClose}
|
|
57
|
+
closeOnDimmerClick={false}
|
|
58
|
+
aria-labelledby="lineage-generation-title"
|
|
59
|
+
aria-describedby={delayed ? "lineage-generation-description" : undefined}
|
|
60
|
+
>
|
|
61
|
+
<div
|
|
62
|
+
className="lineage-graph__loading-card"
|
|
63
|
+
ref={attachDialog}
|
|
64
|
+
onKeyDown={trapFocus}
|
|
65
|
+
>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
className="lineage-generation-dialog__close"
|
|
69
|
+
onClick={onClose}
|
|
70
|
+
aria-label={formatMessage({ id: "lineage.graph.wait.close" })}
|
|
71
|
+
>
|
|
72
|
+
×
|
|
73
|
+
</button>
|
|
74
|
+
<span className="lineage-graph__loading-spinner" aria-hidden="true" />
|
|
75
|
+
<p
|
|
76
|
+
id="lineage-generation-title"
|
|
77
|
+
className="lineage-graph__loading-title"
|
|
78
|
+
role="status"
|
|
79
|
+
>
|
|
80
|
+
<FormattedMessage
|
|
81
|
+
id="lineage.graph.loading"
|
|
82
|
+
defaultMessage="Analyzing..."
|
|
83
|
+
/>
|
|
84
|
+
</p>
|
|
85
|
+
{delayed ? (
|
|
86
|
+
<>
|
|
87
|
+
<p
|
|
88
|
+
id="lineage-generation-description"
|
|
89
|
+
className="lineage-graph__loading-note"
|
|
90
|
+
>
|
|
91
|
+
{formatMessage({ id: "lineage.graph.wait.explanation" })}
|
|
92
|
+
</p>
|
|
93
|
+
<div className="lineage-generation-dialog__actions">
|
|
94
|
+
<Button
|
|
95
|
+
type="button"
|
|
96
|
+
primary
|
|
97
|
+
disabled={waiting}
|
|
98
|
+
onClick={(event) => {
|
|
99
|
+
setWaiting(true);
|
|
100
|
+
event.currentTarget.nextElementSibling?.focus();
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{formatMessage({
|
|
104
|
+
id: waiting
|
|
105
|
+
? "lineage.graph.wait.waiting"
|
|
106
|
+
: "lineage.graph.wait.continue",
|
|
107
|
+
})}
|
|
108
|
+
</Button>
|
|
109
|
+
<Button type="button" onClick={onBackground}>
|
|
110
|
+
{formatMessage({ id: "lineage.graph.wait.background" })}
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
</>
|
|
114
|
+
) : null}
|
|
115
|
+
</div>
|
|
116
|
+
</Modal>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
GraphGenerationDialog.propTypes = {
|
|
120
|
+
open: PropTypes.bool.isRequired,
|
|
121
|
+
requestKey: PropTypes.string,
|
|
122
|
+
onBackground: PropTypes.func.isRequired,
|
|
123
|
+
onClose: PropTypes.func,
|
|
124
|
+
};
|
|
125
|
+
export default GraphGenerationDialog;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useIntl } from "react-intl";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import { connect } from "react-redux";
|
|
4
|
+
import { Link } from "react-router";
|
|
5
|
+
import { linkTo } from "@truedat/core/routes";
|
|
6
|
+
import { dismissGraphGeneration } from "../routines";
|
|
7
|
+
|
|
8
|
+
export const GraphGenerationNotices = ({
|
|
9
|
+
jobs = {},
|
|
10
|
+
dismissGraphGeneration,
|
|
11
|
+
}) => {
|
|
12
|
+
const { formatMessage } = useIntl();
|
|
13
|
+
return (
|
|
14
|
+
<div className="lineage-generation-notices" aria-live="polite">
|
|
15
|
+
{Object.values(jobs)
|
|
16
|
+
.filter(
|
|
17
|
+
(job) => job.notify || (!job.background && job.status === "failed"),
|
|
18
|
+
)
|
|
19
|
+
.map((job) => {
|
|
20
|
+
const pending = ["pending", "awaitingEvent"].includes(job.status);
|
|
21
|
+
const message = pending
|
|
22
|
+
? "background.notice"
|
|
23
|
+
: job.status === "failed"
|
|
24
|
+
? "failed"
|
|
25
|
+
: "completed";
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={`lineage-generation-notices__item${job.status === "completed" ? " lineage-generation-notices__item--ready" : ""}`}
|
|
29
|
+
key={job.requestId}
|
|
30
|
+
role={job.status === "failed" ? "alert" : "status"}
|
|
31
|
+
>
|
|
32
|
+
<span>
|
|
33
|
+
{formatMessage({ id: `lineage.graph.wait.${message}` })}
|
|
34
|
+
</span>
|
|
35
|
+
{job.status === "completed" && job.graphId != null ? (
|
|
36
|
+
<Link
|
|
37
|
+
to={linkTo.GRAPH({ id: job.graphId })}
|
|
38
|
+
onClick={() => dismissGraphGeneration(job.requestId)}
|
|
39
|
+
>
|
|
40
|
+
{formatMessage({ id: "lineage.graph.wait.open" })}
|
|
41
|
+
</Link>
|
|
42
|
+
) : null}
|
|
43
|
+
{!pending ? (
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={() => dismissGraphGeneration(job.requestId)}
|
|
47
|
+
aria-label={formatMessage({
|
|
48
|
+
id: "lineage.graph.wait.dismiss",
|
|
49
|
+
})}
|
|
50
|
+
>
|
|
51
|
+
×
|
|
52
|
+
</button>
|
|
53
|
+
) : null}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
})}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
GraphGenerationNotices.propTypes = {
|
|
61
|
+
jobs: PropTypes.object,
|
|
62
|
+
dismissGraphGeneration: PropTypes.func.isRequired,
|
|
63
|
+
};
|
|
64
|
+
export default connect(
|
|
65
|
+
({ graphGenerationState }) => ({ jobs: graphGenerationState?.jobs }),
|
|
66
|
+
{ dismissGraphGeneration },
|
|
67
|
+
)(GraphGenerationNotices);
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
|
-
import { useEffect } from "react";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
3
|
import PropTypes from "prop-types";
|
|
4
4
|
import { connect } from "react-redux";
|
|
5
|
-
import { useParams, useNavigate } from "react-router";
|
|
5
|
+
import { useParams, useNavigate, useLocation } from "react-router";
|
|
6
6
|
import { GRAPH } from "@truedat/core/routes";
|
|
7
|
-
import { fetchGraph, clearGraph
|
|
7
|
+
import { fetchGraph, clearGraph } from "../routines";
|
|
8
8
|
|
|
9
9
|
export const GraphLoader = ({
|
|
10
10
|
clearGraph,
|
|
11
11
|
fetchGraph,
|
|
12
12
|
graph,
|
|
13
|
-
cancelPoll,
|
|
14
13
|
graphRedirect,
|
|
15
14
|
}) => {
|
|
16
15
|
const { id } = useParams();
|
|
17
16
|
|
|
18
17
|
const navigate = useNavigate();
|
|
18
|
+
const { key: navigationKey } = useLocation();
|
|
19
|
+
const routeKey = `${navigationKey}:${id}`;
|
|
20
|
+
const requestedId = useRef(null);
|
|
19
21
|
|
|
20
22
|
useEffect(() => {
|
|
21
23
|
return () => {
|
|
@@ -24,27 +26,21 @@ export const GraphLoader = ({
|
|
|
24
26
|
}, [clearGraph]);
|
|
25
27
|
|
|
26
28
|
useEffect(() => {
|
|
27
|
-
|
|
29
|
+
// Keep one request per route, including failed or still pending responses.
|
|
30
|
+
if (id && requestedId.current !== routeKey) {
|
|
28
31
|
if (_.isEmpty(graph)) {
|
|
32
|
+
requestedId.current = routeKey;
|
|
29
33
|
fetchGraph({ id });
|
|
30
34
|
/* If graphRedirect is present, a new graph has already been loaded, so
|
|
31
35
|
* there is no need to fetchGraph. The URL is being changed by
|
|
32
36
|
* GraphLoader history.push to the new graph id.
|
|
33
37
|
*/
|
|
34
38
|
} else if (!graphRedirect && id != graph.id) {
|
|
39
|
+
requestedId.current = routeKey;
|
|
35
40
|
fetchGraph({ id });
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
|
-
}, [graph, graphRedirect, fetchGraph, id]);
|
|
39
|
-
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
return () => {
|
|
42
|
-
// && history.location.pathname === "any specific path")
|
|
43
|
-
if (history.action === "POP") {
|
|
44
|
-
cancelPoll();
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
}, [history]);
|
|
43
|
+
}, [graph, graphRedirect, fetchGraph, id, routeKey]);
|
|
48
44
|
|
|
49
45
|
return null;
|
|
50
46
|
};
|
|
@@ -53,7 +49,6 @@ GraphLoader.propTypes = {
|
|
|
53
49
|
clearGraph: PropTypes.func.isRequired,
|
|
54
50
|
fetchGraph: PropTypes.func.isRequired,
|
|
55
51
|
graph: PropTypes.object,
|
|
56
|
-
cancelPoll: PropTypes.func,
|
|
57
52
|
graphRedirect: PropTypes.string,
|
|
58
53
|
};
|
|
59
54
|
|
|
@@ -62,6 +57,6 @@ const mapStateToProps = ({ graph, graphRedirect }) => ({
|
|
|
62
57
|
graphRedirect,
|
|
63
58
|
});
|
|
64
59
|
|
|
65
|
-
export default connect(mapStateToProps, { clearGraph, fetchGraph
|
|
60
|
+
export default connect(mapStateToProps, { clearGraph, fetchGraph })(
|
|
66
61
|
GraphLoader,
|
|
67
62
|
);
|
|
@@ -10,11 +10,11 @@ export const GraphRedirector = ({ graph, graphRedirect, clearRedirect }) => {
|
|
|
10
10
|
const { pathname } = useLocation();
|
|
11
11
|
const navigate = useNavigate();
|
|
12
12
|
useEffect(() => {
|
|
13
|
-
if (graphRedirect
|
|
14
|
-
navigate(graphRedirect);
|
|
15
|
-
clearRedirect();
|
|
13
|
+
if (graphRedirect) {
|
|
14
|
+
if (!_.isEqual(pathname, graphRedirect)) navigate(graphRedirect);
|
|
15
|
+
else clearRedirect();
|
|
16
16
|
}
|
|
17
|
-
}, [
|
|
17
|
+
}, [navigate, pathname, graphRedirect, clearRedirect]);
|
|
18
18
|
return null;
|
|
19
19
|
};
|
|
20
20
|
|
|
@@ -5,7 +5,7 @@ import { connect } from "react-redux";
|
|
|
5
5
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
6
6
|
import Graph from "@truedat/core/components/Graph";
|
|
7
7
|
import { EdgeHoverContext } from "@truedat/core/components/graph/ColoredEdge";
|
|
8
|
-
import {
|
|
8
|
+
import { backgroundGraph } from "../routines";
|
|
9
9
|
import { layoutVisibleLineageGraph } from "../utils/lineageLayout";
|
|
10
10
|
import {
|
|
11
11
|
createLineageGraphIndex,
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
spreadLegacyFieldIndexes,
|
|
63
63
|
legacyPathEdges,
|
|
64
64
|
} from "../utils/lineageGraph";
|
|
65
|
+
import GraphGenerationDialog from "./GraphGenerationDialog";
|
|
65
66
|
import LineageGraphNode from "./LineageGraphNode";
|
|
66
67
|
import LineageModuleButton from "./LineageModuleButton";
|
|
67
68
|
import LineageGraphToolbar, { visibleLegendKeys } from "./LineageGraphToolbar";
|
|
@@ -126,8 +127,8 @@ export const LineageGraph = ({
|
|
|
126
127
|
resources: legacyResources = [],
|
|
127
128
|
topology,
|
|
128
129
|
loading,
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
generationJob,
|
|
131
|
+
backgroundGraph,
|
|
131
132
|
graphType = "impact",
|
|
132
133
|
nodeDesign = "redesigned",
|
|
133
134
|
onGraphReady,
|
|
@@ -188,7 +189,10 @@ export const LineageGraph = ({
|
|
|
188
189
|
const defaultCollapseSignatureRef = useRef("");
|
|
189
190
|
const layoutAnchorNodeIdRef = useRef(null);
|
|
190
191
|
const previousLayoutRef = useRef([]);
|
|
191
|
-
const
|
|
192
|
+
const [waitDismissed, setWaitDismissed] = useState(false);
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
setWaitDismissed(false);
|
|
195
|
+
}, [generationJob?.requestId, loading]);
|
|
192
196
|
const positionedLayout = useMemo(
|
|
193
197
|
() =>
|
|
194
198
|
Boolean(preparedTopology) ||
|
|
@@ -223,8 +227,6 @@ export const LineageGraph = ({
|
|
|
223
227
|
[groups, resources],
|
|
224
228
|
);
|
|
225
229
|
|
|
226
|
-
useEffect(() => () => cancelPoll(), [cancelPoll]);
|
|
227
|
-
|
|
228
230
|
useEffect(() => {
|
|
229
231
|
const timeoutId = setTimeout(() => setSearchTerm(searchInput), 300);
|
|
230
232
|
|
|
@@ -1438,51 +1440,33 @@ export const LineageGraph = ({
|
|
|
1438
1440
|
return (
|
|
1439
1441
|
<div className="lineage-graph">
|
|
1440
1442
|
<div className="lineage-graph__canvas">
|
|
1441
|
-
{
|
|
1442
|
-
<
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1443
|
+
{!simplified && generationJob ? (
|
|
1444
|
+
<GraphGenerationDialog
|
|
1445
|
+
open={
|
|
1446
|
+
!generationJob?.background &&
|
|
1447
|
+
!waitDismissed &&
|
|
1448
|
+
(loading || (hasGraph && fittedGraphKey !== graphIdForFit))
|
|
1449
|
+
}
|
|
1450
|
+
requestKey={generationJob?.requestId}
|
|
1451
|
+
onClose={() => {
|
|
1452
|
+
setWaitDismissed(true);
|
|
1453
|
+
backgroundGraph?.({ notify: false });
|
|
1454
|
+
}}
|
|
1455
|
+
onBackground={() => {
|
|
1456
|
+
setWaitDismissed(true);
|
|
1457
|
+
backgroundGraph?.();
|
|
1458
|
+
}}
|
|
1459
|
+
/>
|
|
1460
|
+
) : loading ? (
|
|
1461
|
+
<div className="lineage-graph__loading" role="status">
|
|
1447
1462
|
<div className="lineage-graph__loading-card">
|
|
1448
1463
|
<span
|
|
1449
1464
|
className="lineage-graph__loading-spinner"
|
|
1450
1465
|
aria-hidden="true"
|
|
1451
1466
|
/>
|
|
1452
1467
|
<p className="lineage-graph__loading-title">
|
|
1453
|
-
<FormattedMessage
|
|
1454
|
-
id="lineage.graph.loading"
|
|
1455
|
-
defaultMessage="Analyzing..."
|
|
1456
|
-
/>
|
|
1468
|
+
<FormattedMessage id="lineage.graph.loading" />
|
|
1457
1469
|
</p>
|
|
1458
|
-
{loadingInfo?.data?.graph_hash && loadingInfo?.data?.status ? (
|
|
1459
|
-
<p className="lineage-graph__loading-note">
|
|
1460
|
-
{formatMessage({
|
|
1461
|
-
id:
|
|
1462
|
-
_.toLower(loadingInfo?.data?.status) === "already_started"
|
|
1463
|
-
? "lineage.graph.loading.background.existing"
|
|
1464
|
-
: _.toLower(loadingInfo?.data?.status) ===
|
|
1465
|
-
"just_started" &&
|
|
1466
|
-
"lineage.graph.loading.background.launched",
|
|
1467
|
-
})}
|
|
1468
|
-
</p>
|
|
1469
|
-
) : null}
|
|
1470
|
-
{loadingInfo?.data?.graph_hash ? (
|
|
1471
|
-
<p className="lineage-graph__loading-note">
|
|
1472
|
-
Graph hash:{" "}
|
|
1473
|
-
<span className="graph-hash">
|
|
1474
|
-
{loadingInfo?.data?.graph_hash}
|
|
1475
|
-
</span>
|
|
1476
|
-
</p>
|
|
1477
|
-
) : null}
|
|
1478
|
-
{loadingInfo?.data?.retry ? (
|
|
1479
|
-
<p className="lineage-graph__loading-note">
|
|
1480
|
-
{formatMessage(
|
|
1481
|
-
{ id: "lineage.graph.loading.polling" },
|
|
1482
|
-
{ retry: loadingInfo?.data?.retry },
|
|
1483
|
-
)}
|
|
1484
|
-
</p>
|
|
1485
|
-
) : null}
|
|
1486
1470
|
</div>
|
|
1487
1471
|
</div>
|
|
1488
1472
|
) : null}
|
|
@@ -1569,8 +1553,8 @@ LineageGraph.propTypes = {
|
|
|
1569
1553
|
topology: PropTypes.object,
|
|
1570
1554
|
height: PropTypes.string,
|
|
1571
1555
|
loading: PropTypes.bool,
|
|
1572
|
-
|
|
1573
|
-
|
|
1556
|
+
generationJob: PropTypes.object,
|
|
1557
|
+
backgroundGraph: PropTypes.func,
|
|
1574
1558
|
graphType: PropTypes.string,
|
|
1575
1559
|
nodeDesign: PropTypes.oneOf(["classic", "redesigned"]),
|
|
1576
1560
|
onGraphReady: PropTypes.func,
|
|
@@ -1582,7 +1566,10 @@ LineageGraph.propTypes = {
|
|
|
1582
1566
|
structureId: PropTypes.string,
|
|
1583
1567
|
};
|
|
1584
1568
|
|
|
1585
|
-
const mapStateToProps = (
|
|
1569
|
+
const mapStateToProps = (
|
|
1570
|
+
{ graph = {}, graphLoading, graphGenerationState },
|
|
1571
|
+
ownProps,
|
|
1572
|
+
) => {
|
|
1586
1573
|
const graphData = _.propOr(graph, "graph")(ownProps);
|
|
1587
1574
|
const paths = _.propOr([], "paths")(graphData);
|
|
1588
1575
|
const links = _.propOr(paths, "links")(graphData);
|
|
@@ -1599,11 +1586,13 @@ const mapStateToProps = ({ graph = {}, graphLoading }, ownProps) => {
|
|
|
1599
1586
|
groups: _.prop("groups")(graphData),
|
|
1600
1587
|
resources: _.prop("resources")(graphData),
|
|
1601
1588
|
loading: graphLoading !== false,
|
|
1602
|
-
|
|
1589
|
+
generationJob: ownProps.simplified
|
|
1590
|
+
? undefined
|
|
1591
|
+
: graphGenerationState?.jobs[graphGenerationState.active],
|
|
1603
1592
|
};
|
|
1604
1593
|
};
|
|
1605
1594
|
|
|
1606
|
-
export default connect(mapStateToProps, {
|
|
1595
|
+
export default connect(mapStateToProps, { backgroundGraph })(LineageGraph);
|
|
1607
1596
|
|
|
1608
1597
|
export const LineageGraphEmptyState = () => {
|
|
1609
1598
|
const { formatMessage } = useIntl();
|
|
@@ -4,23 +4,30 @@ import { ProtectedRoute } from "@truedat/core/router";
|
|
|
4
4
|
import { GRAPHS } from "@truedat/core/routes";
|
|
5
5
|
import GraphLoader from "./GraphLoader";
|
|
6
6
|
import Graphs from "./Graphs";
|
|
7
|
+
import GraphGenerationNotices from "./GraphGenerationNotices";
|
|
7
8
|
|
|
8
9
|
export const LineageRoutes = () => {
|
|
9
10
|
return (
|
|
10
|
-
|
|
11
|
-
<
|
|
11
|
+
<>
|
|
12
|
+
<GraphGenerationNotices />
|
|
13
|
+
<Routes>
|
|
12
14
|
<Route
|
|
13
|
-
path=
|
|
14
|
-
element={
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
path={GRAPHS}
|
|
16
|
+
element={<ProtectedRoute authorization="lineage" />}
|
|
17
|
+
>
|
|
18
|
+
<Route
|
|
19
|
+
path=":id?"
|
|
20
|
+
element={
|
|
21
|
+
<>
|
|
22
|
+
<Graphs />
|
|
23
|
+
<GraphLoader />
|
|
24
|
+
</>
|
|
25
|
+
}
|
|
26
|
+
/>
|
|
27
|
+
</Route>
|
|
28
|
+
<Route path="*" element={null} />
|
|
29
|
+
</Routes>
|
|
30
|
+
</>
|
|
24
31
|
);
|
|
25
32
|
};
|
|
26
33
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { act, fireEvent, screen } from "@testing-library/react";
|
|
2
|
+
import { render } from "@truedat/test/render";
|
|
3
|
+
import { GraphGenerationDialog } from "../GraphGenerationDialog";
|
|
4
|
+
|
|
5
|
+
beforeEach(() => jest.useFakeTimers());
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
jest.runOnlyPendingTimers();
|
|
8
|
+
jest.useRealTimers();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("reveals choices after three seconds and keeps waiting without closing", () => {
|
|
12
|
+
const onBackground = jest.fn();
|
|
13
|
+
render(
|
|
14
|
+
<GraphGenerationDialog open requestKey="a" onBackground={onBackground} />,
|
|
15
|
+
);
|
|
16
|
+
act(() => jest.advanceTimersByTime(2999));
|
|
17
|
+
expect(
|
|
18
|
+
screen.queryByRole("button", { name: "lineage.graph.wait.continue" }),
|
|
19
|
+
).not.toBeInTheDocument();
|
|
20
|
+
act(() => jest.advanceTimersByTime(1));
|
|
21
|
+
fireEvent.click(
|
|
22
|
+
screen.getByRole("button", { name: "lineage.graph.wait.continue" }),
|
|
23
|
+
);
|
|
24
|
+
expect(
|
|
25
|
+
screen.getByRole("button", { name: "lineage.graph.wait.waiting" }),
|
|
26
|
+
).toBeDisabled();
|
|
27
|
+
expect(onBackground).not.toHaveBeenCalled();
|
|
28
|
+
fireEvent.click(
|
|
29
|
+
screen.getByRole("button", { name: "lineage.graph.wait.background" }),
|
|
30
|
+
);
|
|
31
|
+
expect(onBackground).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("manual close is available before the delay without enabling notifications", () => {
|
|
35
|
+
const onClose = jest.fn();
|
|
36
|
+
const onBackground = jest.fn();
|
|
37
|
+
render(
|
|
38
|
+
<GraphGenerationDialog
|
|
39
|
+
open
|
|
40
|
+
requestKey="a"
|
|
41
|
+
onClose={onClose}
|
|
42
|
+
onBackground={onBackground}
|
|
43
|
+
/>,
|
|
44
|
+
);
|
|
45
|
+
fireEvent.click(
|
|
46
|
+
screen.getByRole("button", { name: "lineage.graph.wait.close" }),
|
|
47
|
+
);
|
|
48
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(onBackground).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("escape uses manual close and unmount cancels its timer", () => {
|
|
53
|
+
const onClose = jest.fn();
|
|
54
|
+
const onBackground = jest.fn();
|
|
55
|
+
const { unmount } = render(
|
|
56
|
+
<GraphGenerationDialog
|
|
57
|
+
open
|
|
58
|
+
requestKey="a"
|
|
59
|
+
onClose={onClose}
|
|
60
|
+
onBackground={onBackground}
|
|
61
|
+
/>,
|
|
62
|
+
);
|
|
63
|
+
fireEvent.keyDown(document, { key: "Escape", keyCode: 27 });
|
|
64
|
+
expect(onClose).toHaveBeenCalled();
|
|
65
|
+
expect(onBackground).not.toHaveBeenCalled();
|
|
66
|
+
unmount();
|
|
67
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("progress rerenders do not restart the timer, and a new request resets the choice", () => {
|
|
71
|
+
const onBackground = jest.fn();
|
|
72
|
+
const { rerender } = render(
|
|
73
|
+
<GraphGenerationDialog open requestKey="a" onBackground={onBackground} />,
|
|
74
|
+
);
|
|
75
|
+
act(() => jest.advanceTimersByTime(2000));
|
|
76
|
+
rerender(
|
|
77
|
+
<GraphGenerationDialog open requestKey="a" onBackground={onBackground} />,
|
|
78
|
+
);
|
|
79
|
+
act(() => jest.advanceTimersByTime(1000));
|
|
80
|
+
fireEvent.click(
|
|
81
|
+
screen.getByRole("button", { name: "lineage.graph.wait.continue" }),
|
|
82
|
+
);
|
|
83
|
+
rerender(
|
|
84
|
+
<GraphGenerationDialog open requestKey="b" onBackground={onBackground} />,
|
|
85
|
+
);
|
|
86
|
+
expect(
|
|
87
|
+
screen.queryByRole("button", { name: "lineage.graph.wait.waiting" }),
|
|
88
|
+
).not.toBeInTheDocument();
|
|
89
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
90
|
+
expect(
|
|
91
|
+
screen.getByRole("button", { name: "lineage.graph.wait.continue" }),
|
|
92
|
+
).toBeEnabled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("traps keyboard focus inside the dialog and restores it after closing", () => {
|
|
96
|
+
const trigger = document.createElement("button");
|
|
97
|
+
document.body.appendChild(trigger);
|
|
98
|
+
trigger.focus();
|
|
99
|
+
const { unmount } = render(
|
|
100
|
+
<GraphGenerationDialog open requestKey="a" onBackground={jest.fn()} />,
|
|
101
|
+
);
|
|
102
|
+
const close = screen.getByRole("button", {
|
|
103
|
+
name: "lineage.graph.wait.close",
|
|
104
|
+
});
|
|
105
|
+
expect(close).toHaveFocus();
|
|
106
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
107
|
+
fireEvent.keyDown(close, { key: "Tab", shiftKey: true });
|
|
108
|
+
const background = screen.getByRole("button", {
|
|
109
|
+
name: "lineage.graph.wait.background",
|
|
110
|
+
});
|
|
111
|
+
expect(background).toHaveFocus();
|
|
112
|
+
fireEvent.keyDown(background, { key: "Tab" });
|
|
113
|
+
expect(close).toHaveFocus();
|
|
114
|
+
unmount();
|
|
115
|
+
expect(trigger).toHaveFocus();
|
|
116
|
+
trigger.remove();
|
|
117
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { fireEvent, screen } from "@testing-library/react";
|
|
2
|
+
import { render } from "@truedat/test/render";
|
|
3
|
+
import { GraphGenerationNotices } from "../GraphGenerationNotices";
|
|
4
|
+
|
|
5
|
+
test("completion gives an explicit link without automatic navigation", () => {
|
|
6
|
+
const dismiss = jest.fn();
|
|
7
|
+
render(
|
|
8
|
+
<GraphGenerationNotices
|
|
9
|
+
jobs={{
|
|
10
|
+
a: {
|
|
11
|
+
requestId: "a",
|
|
12
|
+
background: true,
|
|
13
|
+
notify: true,
|
|
14
|
+
status: "completed",
|
|
15
|
+
graphId: 42,
|
|
16
|
+
},
|
|
17
|
+
}}
|
|
18
|
+
dismissGraphGeneration={dismiss}
|
|
19
|
+
/>,
|
|
20
|
+
);
|
|
21
|
+
expect(
|
|
22
|
+
screen.getByRole("link", { name: "lineage.graph.wait.open" }),
|
|
23
|
+
).toHaveAttribute("href", "/graphs/42");
|
|
24
|
+
expect(dismiss).not.toHaveBeenCalled();
|
|
25
|
+
fireEvent.click(
|
|
26
|
+
screen.getByRole("button", { name: "lineage.graph.wait.dismiss" }),
|
|
27
|
+
);
|
|
28
|
+
expect(dismiss).toHaveBeenCalledWith("a");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("a failure is announced and has no success link", () => {
|
|
32
|
+
render(
|
|
33
|
+
<GraphGenerationNotices
|
|
34
|
+
jobs={{ a: { requestId: "a", status: "failed" } }}
|
|
35
|
+
dismissGraphGeneration={jest.fn()}
|
|
36
|
+
/>,
|
|
37
|
+
);
|
|
38
|
+
expect(screen.getByRole("alert")).toHaveTextContent(
|
|
39
|
+
"lineage.graph.wait.failed",
|
|
40
|
+
);
|
|
41
|
+
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("background work without explicit notification consent stays silent", () => {
|
|
45
|
+
render(
|
|
46
|
+
<GraphGenerationNotices
|
|
47
|
+
jobs={{
|
|
48
|
+
a: {
|
|
49
|
+
requestId: "a",
|
|
50
|
+
background: true,
|
|
51
|
+
status: "completed",
|
|
52
|
+
graphId: 42,
|
|
53
|
+
},
|
|
54
|
+
}}
|
|
55
|
+
dismissGraphGeneration={jest.fn()}
|
|
56
|
+
/>,
|
|
57
|
+
);
|
|
58
|
+
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
|
59
|
+
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
60
|
+
});
|