@truedat/dd 8.13.6 → 8.13.8
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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { IntlProvider } from "react-intl";
|
|
3
|
+
import { MemoryRouter } from "react-router";
|
|
4
|
+
import { GraphGenerationDialog } from "../GraphGenerationDialog";
|
|
5
|
+
import { GraphGenerationNotices } from "../GraphGenerationNotices";
|
|
6
|
+
import en from "../../messages/en";
|
|
7
|
+
import es from "../../messages/es";
|
|
8
|
+
import migratedMessages from "./fixtures/graphGenerationMessages.json";
|
|
9
|
+
|
|
10
|
+
beforeEach(() => jest.useFakeTimers());
|
|
11
|
+
afterEach(() => jest.useRealTimers());
|
|
12
|
+
|
|
13
|
+
test.each([
|
|
14
|
+
["en", { ...en, ...migratedMessages.en }],
|
|
15
|
+
["es", { ...es, ...migratedMessages.es }],
|
|
16
|
+
])(
|
|
17
|
+
"renders translated generation controls and notices in %s",
|
|
18
|
+
(locale, messages) => {
|
|
19
|
+
const onError = jest.fn();
|
|
20
|
+
const view = render(
|
|
21
|
+
<IntlProvider locale={locale} messages={messages} onError={onError}>
|
|
22
|
+
<MemoryRouter>
|
|
23
|
+
<GraphGenerationDialog open requestKey="a" onBackground={jest.fn()} />
|
|
24
|
+
<GraphGenerationNotices
|
|
25
|
+
jobs={{
|
|
26
|
+
pending: {
|
|
27
|
+
requestId: "pending",
|
|
28
|
+
notify: true,
|
|
29
|
+
status: "pending",
|
|
30
|
+
},
|
|
31
|
+
completed: {
|
|
32
|
+
requestId: "completed",
|
|
33
|
+
notify: true,
|
|
34
|
+
status: "completed",
|
|
35
|
+
graphId: 42,
|
|
36
|
+
},
|
|
37
|
+
failed: { requestId: "failed", notify: true, status: "failed" },
|
|
38
|
+
}}
|
|
39
|
+
dismissGraphGeneration={jest.fn()}
|
|
40
|
+
/>
|
|
41
|
+
</MemoryRouter>
|
|
42
|
+
</IntlProvider>,
|
|
43
|
+
);
|
|
44
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
45
|
+
const text = (suffix) => messages[`lineage.graph.wait.${suffix}`];
|
|
46
|
+
["explanation", "background.notice", "completed", "failed"].forEach(
|
|
47
|
+
(suffix) => {
|
|
48
|
+
expect(text(suffix)).toEqual(expect.any(String));
|
|
49
|
+
expect(screen.getByText(text(suffix))).toBeInTheDocument();
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
["close", "continue", "background"].forEach((suffix) => {
|
|
53
|
+
expect(text(suffix)).toEqual(expect.any(String));
|
|
54
|
+
expect(
|
|
55
|
+
screen.getByRole("button", { name: text(suffix) }),
|
|
56
|
+
).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
expect(text("open")).toEqual(expect.any(String));
|
|
59
|
+
expect(text("dismiss")).toEqual(expect.any(String));
|
|
60
|
+
expect(
|
|
61
|
+
screen.getByRole("link", { name: text("open"), hidden: true }),
|
|
62
|
+
).toHaveAttribute("href", "/graphs/42");
|
|
63
|
+
expect(
|
|
64
|
+
screen.getAllByRole("button", { name: text("dismiss"), hidden: true }),
|
|
65
|
+
).toHaveLength(2);
|
|
66
|
+
fireEvent.click(screen.getByRole("button", { name: text("continue") }));
|
|
67
|
+
expect(text("waiting")).toEqual(expect.any(String));
|
|
68
|
+
expect(
|
|
69
|
+
screen.getByRole("button", { name: text("waiting") }),
|
|
70
|
+
).toBeDisabled();
|
|
71
|
+
expect(onError).not.toHaveBeenCalled();
|
|
72
|
+
view.unmount();
|
|
73
|
+
},
|
|
74
|
+
);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { fireEvent, screen } from "@testing-library/react";
|
|
2
|
+
import { useDispatch } from "react-redux";
|
|
3
|
+
import { Route, Routes, useLocation } from "react-router";
|
|
4
|
+
import { render } from "@truedat/test/render";
|
|
5
|
+
import ConnectedGraphLoader, { GraphLoader } from "../GraphLoader";
|
|
6
|
+
import GraphRedirector from "../GraphRedirector";
|
|
7
|
+
import { graph as graphReducer } from "../../reducers/graph";
|
|
8
|
+
import { graphRedirect as redirectReducer } from "../../reducers/graphRedirect";
|
|
9
|
+
import { createGraph, fetchGraph } from "../../routines";
|
|
10
|
+
|
|
11
|
+
const GenerateGraph = () => {
|
|
12
|
+
const dispatch = useDispatch();
|
|
13
|
+
const { pathname } = useLocation();
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<button
|
|
17
|
+
onClick={() =>
|
|
18
|
+
dispatch(
|
|
19
|
+
createGraph.success({ data: { id: 43, isRedirected: true } }),
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
>
|
|
23
|
+
Complete generation
|
|
24
|
+
</button>
|
|
25
|
+
<output>{pathname}</output>
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
test("redirects to the generated graph without refetching the previous graph", () => {
|
|
31
|
+
const requests = jest.fn();
|
|
32
|
+
render(
|
|
33
|
+
<Routes>
|
|
34
|
+
<Route
|
|
35
|
+
path="/graphs/:id"
|
|
36
|
+
element={
|
|
37
|
+
<>
|
|
38
|
+
<GenerateGraph />
|
|
39
|
+
<GraphRedirector />
|
|
40
|
+
<ConnectedGraphLoader />
|
|
41
|
+
</>
|
|
42
|
+
}
|
|
43
|
+
/>
|
|
44
|
+
</Routes>,
|
|
45
|
+
{
|
|
46
|
+
routes: ["/graphs/42"],
|
|
47
|
+
state: { graph: { id: 42 }, graphRedirect: "" },
|
|
48
|
+
reducer: (state, action) => {
|
|
49
|
+
if (action.type === fetchGraph.TRIGGER) requests(action.payload);
|
|
50
|
+
return {
|
|
51
|
+
graph: graphReducer(state.graph, action),
|
|
52
|
+
graphRedirect: redirectReducer(state.graphRedirect, action),
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
fireEvent.click(screen.getByRole("button", { name: "Complete generation" }));
|
|
58
|
+
expect(screen.getByText("/graphs/43")).toBeInTheDocument();
|
|
59
|
+
expect(requests).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("does not fetch repeatedly when a response is missing its graph id", () => {
|
|
63
|
+
const fetchGraph = jest.fn();
|
|
64
|
+
const clearGraph = jest.fn();
|
|
65
|
+
const view = (graph) => (
|
|
66
|
+
<Routes>
|
|
67
|
+
<Route
|
|
68
|
+
path="/graphs/:id"
|
|
69
|
+
element={
|
|
70
|
+
<GraphLoader
|
|
71
|
+
graph={graph}
|
|
72
|
+
fetchGraph={fetchGraph}
|
|
73
|
+
clearGraph={clearGraph}
|
|
74
|
+
/>
|
|
75
|
+
}
|
|
76
|
+
/>
|
|
77
|
+
</Routes>
|
|
78
|
+
);
|
|
79
|
+
const { rerender } = render(view({}), { routes: ["/graphs/42"] });
|
|
80
|
+
rerender(view({ data: { id: 42 } }));
|
|
81
|
+
rerender(view({ data: { id: 42 } }));
|
|
82
|
+
expect(fetchGraph).toHaveBeenCalledTimes(1);
|
|
83
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { fireEvent, screen, within } from "@testing-library/react";
|
|
1
|
+
import { act, fireEvent, screen, within } from "@testing-library/react";
|
|
2
2
|
import { Route, Routes, useLocation } from "react-router";
|
|
3
3
|
import { render } from "@truedat/test/render";
|
|
4
4
|
import { LineageGraph } from "../LineageGraph";
|
|
5
5
|
import { GraphLoader } from "../GraphLoader";
|
|
6
6
|
|
|
7
7
|
let mockGraphProps;
|
|
8
|
+
let mockDeferInitialFit = false;
|
|
8
9
|
|
|
9
10
|
jest.mock("@truedat/core/components/Graph", () => {
|
|
10
11
|
const React = require("react");
|
|
@@ -12,10 +13,9 @@ jest.mock("@truedat/core/components/Graph", () => {
|
|
|
12
13
|
return function Graph(props) {
|
|
13
14
|
mockGraphProps = props;
|
|
14
15
|
const [expanded, setExpanded] = React.useState(false);
|
|
15
|
-
React.useEffect(
|
|
16
|
-
()
|
|
17
|
-
|
|
18
|
-
);
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
if (!mockDeferInitialFit) props.onInitialFitComplete();
|
|
18
|
+
}, [props.onInitialFitComplete]);
|
|
19
19
|
const canvas = (name) => (
|
|
20
20
|
<div className="td-graph-container" data-testid={name}>
|
|
21
21
|
{props.nodes
|
|
@@ -50,7 +50,6 @@ const props = {
|
|
|
50
50
|
graphId: "graph-1",
|
|
51
51
|
loading: false,
|
|
52
52
|
simplified: true,
|
|
53
|
-
cancelPoll: jest.fn(),
|
|
54
53
|
groups: [],
|
|
55
54
|
resources: [
|
|
56
55
|
{ id: "orders", name: "Orders", x: 0, y: 0, w: 288, h: 100 },
|
|
@@ -72,6 +71,55 @@ const renderGraph = (extra = {}) =>
|
|
|
72
71
|
{ state },
|
|
73
72
|
);
|
|
74
73
|
|
|
74
|
+
it("keeps the unanswered loading dialog through polling and until the graph is fitted", () => {
|
|
75
|
+
mockDeferInitialFit = true;
|
|
76
|
+
const backgroundGraph = jest.fn();
|
|
77
|
+
const onGraphReady = jest.fn();
|
|
78
|
+
const generationJob = { requestId: "slow-graph", status: "pending" };
|
|
79
|
+
const pendingProps = {
|
|
80
|
+
...props,
|
|
81
|
+
simplified: false,
|
|
82
|
+
loading: true,
|
|
83
|
+
generationJob,
|
|
84
|
+
backgroundGraph,
|
|
85
|
+
onGraphReady,
|
|
86
|
+
};
|
|
87
|
+
const view = render(<LineageGraph {...pendingProps} />, { state });
|
|
88
|
+
try {
|
|
89
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
90
|
+
const continueButton = screen.getByRole("button", {
|
|
91
|
+
name: "lineage.graph.wait.continue",
|
|
92
|
+
});
|
|
93
|
+
for (let retry = 1; retry <= 3; retry += 1) {
|
|
94
|
+
view.rerender(
|
|
95
|
+
<LineageGraph
|
|
96
|
+
{...pendingProps}
|
|
97
|
+
generationJob={{ ...generationJob, hash: "slow-hash", retry }}
|
|
98
|
+
/>,
|
|
99
|
+
);
|
|
100
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
101
|
+
expect(continueButton).toBeVisible();
|
|
102
|
+
expect(continueButton).toBeEnabled();
|
|
103
|
+
}
|
|
104
|
+
view.rerender(
|
|
105
|
+
<LineageGraph
|
|
106
|
+
{...pendingProps}
|
|
107
|
+
loading={false}
|
|
108
|
+
generationJob={{ ...generationJob, status: "completed" }}
|
|
109
|
+
/>,
|
|
110
|
+
);
|
|
111
|
+
expect(continueButton).toBeVisible();
|
|
112
|
+
expect(onGraphReady).not.toHaveBeenCalled();
|
|
113
|
+
expect(backgroundGraph).not.toHaveBeenCalled();
|
|
114
|
+
act(() => mockGraphProps.onInitialFitComplete());
|
|
115
|
+
expect(continueButton).not.toBeInTheDocument();
|
|
116
|
+
expect(onGraphReady).toHaveBeenCalledTimes(1);
|
|
117
|
+
} finally {
|
|
118
|
+
mockDeferInitialFit = false;
|
|
119
|
+
view.unmount();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
75
123
|
it("selects successive nodes through a real portal and keeps dependencies active on return", () => {
|
|
76
124
|
renderGraph();
|
|
77
125
|
fireEvent.click(screen.getByText("Toggle expanded"));
|
|
@@ -225,7 +273,6 @@ it.each([
|
|
|
225
273
|
graph={graph}
|
|
226
274
|
fetchGraph={fetchGraph}
|
|
227
275
|
clearGraph={jest.fn()}
|
|
228
|
-
cancelPoll={jest.fn()}
|
|
229
276
|
/>
|
|
230
277
|
}
|
|
231
278
|
/>
|
|
@@ -236,3 +283,27 @@ it.each([
|
|
|
236
283
|
else expect(fetchGraph).not.toHaveBeenCalled();
|
|
237
284
|
},
|
|
238
285
|
);
|
|
286
|
+
|
|
287
|
+
test.each([
|
|
288
|
+
{ simplified: true },
|
|
289
|
+
{ simplified: false },
|
|
290
|
+
{
|
|
291
|
+
simplified: true,
|
|
292
|
+
generationJob: { requestId: "other", status: "pending" },
|
|
293
|
+
},
|
|
294
|
+
])("keeps loading inline without a background action: %j", (extra) => {
|
|
295
|
+
const backgroundGraph = jest.fn();
|
|
296
|
+
const view = renderGraph({ ...extra, loading: true, backgroundGraph });
|
|
297
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
298
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
299
|
+
expect(
|
|
300
|
+
screen.getByText("lineage.graph.loading").closest("[role=status]"),
|
|
301
|
+
).toBeInTheDocument();
|
|
302
|
+
expect(
|
|
303
|
+
screen.queryByRole("button", { name: "lineage.graph.wait.background" }),
|
|
304
|
+
).not.toBeInTheDocument();
|
|
305
|
+
fireEvent.keyDown(document, { key: "Escape", keyCode: 27 });
|
|
306
|
+
expect(backgroundGraph).not.toHaveBeenCalled();
|
|
307
|
+
expect(screen.getByText("lineage.graph.loading")).toBeInTheDocument();
|
|
308
|
+
view.unmount();
|
|
309
|
+
});
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`<LineageRoutes /> renders correctly with GRAPHS route 1`] = `
|
|
4
4
|
<div>
|
|
5
|
+
<div
|
|
6
|
+
aria-live="polite"
|
|
7
|
+
class="lineage-generation-notices"
|
|
8
|
+
/>
|
|
5
9
|
<div
|
|
6
10
|
class="ui icon visible bottom attached message center"
|
|
7
11
|
>
|
|
@@ -25,4 +29,11 @@ exports[`<LineageRoutes /> renders correctly with GRAPHS route 1`] = `
|
|
|
25
29
|
</div>
|
|
26
30
|
`;
|
|
27
31
|
|
|
28
|
-
exports[`<LineageRoutes /> renders correctly with default route 1`] =
|
|
32
|
+
exports[`<LineageRoutes /> renders correctly with default route 1`] = `
|
|
33
|
+
<div>
|
|
34
|
+
<div
|
|
35
|
+
aria-live="polite"
|
|
36
|
+
class="lineage-generation-notices"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
`;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"en": {
|
|
3
|
+
"lineage.graph.loading": "Analyzing...",
|
|
4
|
+
"lineage.graph.wait.background": "Generate in background",
|
|
5
|
+
"lineage.graph.wait.background.notice": "The graph is being generated in the background. We will notify you when it is ready.",
|
|
6
|
+
"lineage.graph.wait.close": "Close and continue generating in background",
|
|
7
|
+
"lineage.graph.wait.completed": "The graph is ready.",
|
|
8
|
+
"lineage.graph.wait.continue": "Keep waiting",
|
|
9
|
+
"lineage.graph.wait.dismiss": "Dismiss notification",
|
|
10
|
+
"lineage.graph.wait.explanation": "This graph is taking longer than expected. You can keep waiting or continue working while it is generated in the background.",
|
|
11
|
+
"lineage.graph.wait.failed": "Unable to complete graph generation. Please try again.",
|
|
12
|
+
"lineage.graph.wait.open": "Open graph",
|
|
13
|
+
"lineage.graph.wait.waiting": "Waiting…"
|
|
14
|
+
},
|
|
15
|
+
"es": {
|
|
16
|
+
"lineage.graph.loading": "Analizando...",
|
|
17
|
+
"lineage.graph.wait.background": "Generar en segundo plano",
|
|
18
|
+
"lineage.graph.wait.background.notice": "El grafo se está generando en segundo plano. Te avisaremos cuando esté listo.",
|
|
19
|
+
"lineage.graph.wait.close": "Cerrar y continuar generando en segundo plano",
|
|
20
|
+
"lineage.graph.wait.completed": "El grafo está listo.",
|
|
21
|
+
"lineage.graph.wait.continue": "Seguir esperando",
|
|
22
|
+
"lineage.graph.wait.dismiss": "Descartar notificación",
|
|
23
|
+
"lineage.graph.wait.explanation": "Este grafo está tardando más de lo esperado. Puedes seguir esperando o continuar trabajando mientras se genera en segundo plano.",
|
|
24
|
+
"lineage.graph.wait.failed": "No se ha podido completar la generación del grafo. Vuelve a intentarlo.",
|
|
25
|
+
"lineage.graph.wait.open": "Abrir grafo",
|
|
26
|
+
"lineage.graph.wait.waiting": "Esperando…"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/messages/en.js
CHANGED
|
@@ -195,35 +195,6 @@ export default {
|
|
|
195
195
|
"joinType.left": "outer left",
|
|
196
196
|
"joinType.right": "outer right",
|
|
197
197
|
"joinType.full_outer": "full outer join",
|
|
198
|
-
"lineage.events.header": "My graphs",
|
|
199
|
-
"lineage.events.props.graph_data": "Graph",
|
|
200
|
-
"lineage.events.props.inserted_at": "Date",
|
|
201
|
-
"lineage.events.props.message": "Message",
|
|
202
|
-
"lineage.events.props.status": "Status",
|
|
203
|
-
"lineage.events.search.results.empty": "No graphs found",
|
|
204
|
-
"lineage.events.subheader": "Lineage graphs",
|
|
205
|
-
"lineage.form.header": "Select Structures",
|
|
206
|
-
"lineage.form.levels.all.label": "All",
|
|
207
|
-
"lineage.form.levels.header": "Levels",
|
|
208
|
-
"lineage.form.levels.select.input": "Select...",
|
|
209
|
-
"lineage.form.levels.select.label": "Depth",
|
|
210
|
-
"lineage.graph.loading": "Analyzing...",
|
|
211
|
-
"lineage.graph.loading.background.launched": "Background task launched",
|
|
212
|
-
"lineage.graph.loading.background.existing":
|
|
213
|
-
"The task for this graph was already launched",
|
|
214
|
-
"lineage.graph.loading.polling": "Poll retry #{retry}",
|
|
215
|
-
"lineage.graph.status.already_calculated": "Already calculated",
|
|
216
|
-
"lineage.graph.status.already_started": "Already started",
|
|
217
|
-
"lineage.graph.status.completed": "Completed",
|
|
218
|
-
"lineage.graph.status.failed": "Failed",
|
|
219
|
-
"lineage.graph.status.just_started": "Just started",
|
|
220
|
-
"lineage.graph.status.started": "Started",
|
|
221
|
-
"lineage.graph.status.timed_out": "Timed out",
|
|
222
|
-
"lineage.graph.metadata.header": "Information",
|
|
223
|
-
"lineage.graph.metadata.close.button": "Close",
|
|
224
|
-
"lineage.group.placeholder": "Select Top Level Structure",
|
|
225
|
-
"lineage.resources.placeholder": "Select Child Structures",
|
|
226
|
-
"lineage.subgroup.placeholder": "Select Child Structure",
|
|
227
198
|
metadata: "Metadata",
|
|
228
199
|
"metadataView.fields": "Available metadata fields",
|
|
229
200
|
"metadataView.fields.placeholder":
|
package/src/messages/es.js
CHANGED
|
@@ -199,36 +199,6 @@ export default {
|
|
|
199
199
|
"joinType.left": "outer left",
|
|
200
200
|
"joinType.right": "outer right",
|
|
201
201
|
"joinType.full_outer": "full outer join",
|
|
202
|
-
"lineage.events.header": "Mis grafos",
|
|
203
|
-
"lineage.events.props.graph_data": "Grafo",
|
|
204
|
-
"lineage.events.props.inserted_at": "Fecha",
|
|
205
|
-
"lineage.events.props.message": "Mensaje",
|
|
206
|
-
"lineage.events.props.status": "Estado",
|
|
207
|
-
"lineage.events.search.results.empty": "Ningún grafo encontrado",
|
|
208
|
-
"lineage.events.subheader": "Grafos de linaje",
|
|
209
|
-
"lineage.form.header": "Seleccionar Estructuras",
|
|
210
|
-
"lineage.form.levels.all.label": "Completo",
|
|
211
|
-
"lineage.form.levels.header": "Niveles",
|
|
212
|
-
"lineage.form.levels.select.input": "Seleccionar...",
|
|
213
|
-
"lineage.form.levels.select.label": "Profundidad",
|
|
214
|
-
"lineage.graph.loading": "Analizando...",
|
|
215
|
-
"lineage.graph.loading.background.launched":
|
|
216
|
-
"Tarea en segundo plano lanzada para grafo",
|
|
217
|
-
"lineage.graph.loading.background.existing":
|
|
218
|
-
"La tarea en segundo plano para este grafo ya fue lanzada",
|
|
219
|
-
"lineage.graph.loading.polling": "Sondeo #{retry}",
|
|
220
|
-
"lineage.graph.status.already_calculated": "Previamente calculado",
|
|
221
|
-
"lineage.graph.status.already_started": "Previamente iniciado",
|
|
222
|
-
"lineage.graph.status.completed": "Finalizado",
|
|
223
|
-
"lineage.graph.status.failed": "Fallido",
|
|
224
|
-
"lineage.graph.status.just_started": "Acaba de iniciarse",
|
|
225
|
-
"lineage.graph.status.started": "Iniciado",
|
|
226
|
-
"lineage.graph.status.timed_out": "Tiempo de espera máximo sobrepasado",
|
|
227
|
-
"lineage.graph.metadata.header": "Información",
|
|
228
|
-
"lineage.graph.metadata.close.button": "Cerrar",
|
|
229
|
-
"lineage.group.placeholder": "Seleccionar Estructura Padre",
|
|
230
|
-
"lineage.resources.placeholder": "Seleccionar Estructuras Hijos",
|
|
231
|
-
"lineage.subgroup.placeholder": "Seleccionar Estructura Hijo",
|
|
232
202
|
metadata: "Metadata",
|
|
233
203
|
"metadataView.fields": "Campos",
|
|
234
204
|
"metadataView.fields.placeholder":
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { graphGenerationState as reduce } from "../graphGenerationState";
|
|
2
|
+
import {
|
|
3
|
+
backgroundGraph,
|
|
4
|
+
clearGraph,
|
|
5
|
+
graphGeneration,
|
|
6
|
+
fetchLineageEvents,
|
|
7
|
+
} from "../../routines";
|
|
8
|
+
|
|
9
|
+
test("a new request backgrounds the previous one and ignores unrelated events", () => {
|
|
10
|
+
let state = reduce(undefined, graphGeneration.request({ requestId: "a" }));
|
|
11
|
+
state = reduce(state, graphGeneration({ requestId: "a", hash: "hash-a" }));
|
|
12
|
+
state = reduce(state, graphGeneration.request({ requestId: "b" }));
|
|
13
|
+
state = reduce(
|
|
14
|
+
state,
|
|
15
|
+
fetchLineageEvents.success({
|
|
16
|
+
data: [{ graph_hash: "other", graph_id: 2, status: "COMPLETED" }],
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
expect(state.jobs.a).toMatchObject({ status: "pending", background: true });
|
|
20
|
+
expect(state.jobs.b.background).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("background completion matches a graph id, is stable and cannot be reverted by a late poll", () => {
|
|
24
|
+
let state = reduce(undefined, graphGeneration.request({ requestId: "a" }));
|
|
25
|
+
state = reduce(state, backgroundGraph());
|
|
26
|
+
state = reduce(
|
|
27
|
+
state,
|
|
28
|
+
graphGeneration.success({
|
|
29
|
+
requestId: "a",
|
|
30
|
+
graphId: 5,
|
|
31
|
+
status: "awaitingEvent",
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
const complete = fetchLineageEvents.success({
|
|
35
|
+
data: [{ graph_id: 5, status: "COMPLETED" }],
|
|
36
|
+
});
|
|
37
|
+
state = reduce(state, complete);
|
|
38
|
+
expect(state.jobs.a.status).toBe("completed");
|
|
39
|
+
expect(reduce(state, complete)).toEqual(state);
|
|
40
|
+
expect(
|
|
41
|
+
reduce(
|
|
42
|
+
state,
|
|
43
|
+
graphGeneration.success({ requestId: "a", status: "awaitingEvent" }),
|
|
44
|
+
),
|
|
45
|
+
).toEqual(state);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("failed events notify failure and completed events without an id cannot be opened", () => {
|
|
49
|
+
let state = reduce(undefined, graphGeneration.request({ requestId: "a" }));
|
|
50
|
+
state = reduce(state, graphGeneration({ requestId: "a", hash: "a" }));
|
|
51
|
+
state = reduce(state, backgroundGraph());
|
|
52
|
+
state = reduce(
|
|
53
|
+
state,
|
|
54
|
+
fetchLineageEvents.success({
|
|
55
|
+
data: [{ graph_hash: "a", status: "COMPLETED" }],
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
expect(state.jobs.a.status).toBe("pending");
|
|
59
|
+
state = reduce(
|
|
60
|
+
state,
|
|
61
|
+
fetchLineageEvents.success({
|
|
62
|
+
data: [{ graph_hash: "a", status: "FAILED" }],
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
expect(state.jobs.a.status).toBe("failed");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test.each([clearGraph(), backgroundGraph({ notify: false })])(
|
|
69
|
+
"implicit background and manual close do not enable notifications: %j",
|
|
70
|
+
(action) => {
|
|
71
|
+
let state = reduce(undefined, graphGeneration.request({ requestId: "a" }));
|
|
72
|
+
state = reduce(state, action);
|
|
73
|
+
expect(state.jobs.a.notify).toBe(false);
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
test("only an explicit background choice enables notifications", () => {
|
|
77
|
+
let state = reduce(undefined, graphGeneration.request({ requestId: "a" }));
|
|
78
|
+
state = reduce(state, graphGeneration.request({ requestId: "b" }));
|
|
79
|
+
expect(state.jobs.a.notify).not.toBe(true);
|
|
80
|
+
state = reduce(state, backgroundGraph());
|
|
81
|
+
expect(state.jobs.b.notify).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("unrelated and repeated events preserve state and job references", () => {
|
|
85
|
+
let state = reduce(undefined, graphGeneration.request({ requestId: "a" }));
|
|
86
|
+
state = reduce(state, graphGeneration({ requestId: "a", hash: "a" }));
|
|
87
|
+
state = reduce(state, backgroundGraph());
|
|
88
|
+
expect(reduce(state, fetchLineageEvents.success({ data: [] }))).toBe(state);
|
|
89
|
+
const event = fetchLineageEvents.success({
|
|
90
|
+
data: [{ graph_hash: "a", graph_id: 5, status: "COMPLETED" }],
|
|
91
|
+
});
|
|
92
|
+
const completed = reduce(state, event);
|
|
93
|
+
expect(completed.jobs.a.status).toBe("completed");
|
|
94
|
+
expect(reduce(completed, event)).toBe(completed);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("event indexing preserves the first match across graph id and hash", () => {
|
|
98
|
+
const state = {
|
|
99
|
+
active: "a",
|
|
100
|
+
jobs: { a: { graphId: 5, hash: "a", notify: true, status: "pending" } },
|
|
101
|
+
};
|
|
102
|
+
const next = reduce(
|
|
103
|
+
state,
|
|
104
|
+
fetchLineageEvents.success({
|
|
105
|
+
data: [
|
|
106
|
+
{ graph_hash: "a", graph_id: 6, status: "COMPLETED" },
|
|
107
|
+
{ graph_id: 5, status: "FAILED" },
|
|
108
|
+
{ graph_hash: "a", graph_id: 7, status: "FAILED" },
|
|
109
|
+
],
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
expect(next.jobs.a).toMatchObject({ graphId: 6, status: "completed" });
|
|
113
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
backgroundGraph,
|
|
3
|
+
clearGraph,
|
|
4
|
+
dismissGraphGeneration,
|
|
5
|
+
graphGeneration,
|
|
6
|
+
fetchLineageEvents,
|
|
7
|
+
} from "../routines";
|
|
8
|
+
import {
|
|
9
|
+
isCompletedStatus,
|
|
10
|
+
isFailedStatus,
|
|
11
|
+
} from "../components/lineageEventHelpers";
|
|
12
|
+
|
|
13
|
+
export const initialState = { active: null, jobs: {} };
|
|
14
|
+
|
|
15
|
+
export const graphGenerationState = (
|
|
16
|
+
state = initialState,
|
|
17
|
+
{ type, payload },
|
|
18
|
+
) => {
|
|
19
|
+
const activeJob = state.jobs[state.active];
|
|
20
|
+
switch (type) {
|
|
21
|
+
case graphGeneration.REQUEST:
|
|
22
|
+
return {
|
|
23
|
+
active: payload.requestId,
|
|
24
|
+
jobs: {
|
|
25
|
+
...Object.fromEntries(
|
|
26
|
+
Object.entries(state.jobs).filter(
|
|
27
|
+
([, job]) =>
|
|
28
|
+
job.notify || (!job.background && job.status === "failed"),
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
...(activeJob?.status === "pending"
|
|
32
|
+
? { [state.active]: { ...activeJob, background: true } }
|
|
33
|
+
: {}),
|
|
34
|
+
[payload.requestId]: {
|
|
35
|
+
requestId: payload.requestId,
|
|
36
|
+
status: "pending",
|
|
37
|
+
background: false,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
case backgroundGraph.TRIGGER:
|
|
42
|
+
case clearGraph.TRIGGER:
|
|
43
|
+
return activeJob?.status === "pending"
|
|
44
|
+
? {
|
|
45
|
+
...state,
|
|
46
|
+
jobs: {
|
|
47
|
+
...state.jobs,
|
|
48
|
+
[state.active]: {
|
|
49
|
+
...activeJob,
|
|
50
|
+
background: true,
|
|
51
|
+
notify:
|
|
52
|
+
type === backgroundGraph.TRIGGER
|
|
53
|
+
? payload?.notify !== false
|
|
54
|
+
: activeJob.notify === true,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
: state;
|
|
59
|
+
case graphGeneration.TRIGGER:
|
|
60
|
+
case graphGeneration.SUCCESS:
|
|
61
|
+
case graphGeneration.FAILURE: {
|
|
62
|
+
const job = state.jobs[payload.requestId];
|
|
63
|
+
if (!job || ["completed", "failed"].includes(job.status)) return state;
|
|
64
|
+
return {
|
|
65
|
+
...state,
|
|
66
|
+
jobs: { ...state.jobs, [payload.requestId]: { ...job, ...payload } },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
case fetchLineageEvents.SUCCESS: {
|
|
70
|
+
const byId = new Map();
|
|
71
|
+
const byHash = new Map();
|
|
72
|
+
(payload.data || []).forEach((event, index) => {
|
|
73
|
+
const entry = { event, index };
|
|
74
|
+
if (event.graph_id != null && !byId.has(String(event.graph_id)))
|
|
75
|
+
byId.set(String(event.graph_id), entry);
|
|
76
|
+
if (event.graph_hash && !byHash.has(event.graph_hash))
|
|
77
|
+
byHash.set(event.graph_hash, entry);
|
|
78
|
+
});
|
|
79
|
+
const entries = Object.entries(state.jobs).map(([key, job]) => {
|
|
80
|
+
if (!job.notify || !["pending", "awaitingEvent"].includes(job.status))
|
|
81
|
+
return [key, job];
|
|
82
|
+
const idMatch =
|
|
83
|
+
job.graphId != null ? byId.get(String(job.graphId)) : undefined;
|
|
84
|
+
const hashMatch = job.hash ? byHash.get(job.hash) : undefined;
|
|
85
|
+
const event = (
|
|
86
|
+
idMatch && (!hashMatch || idMatch.index < hashMatch.index)
|
|
87
|
+
? idMatch
|
|
88
|
+
: hashMatch
|
|
89
|
+
)?.event;
|
|
90
|
+
if (isFailedStatus(event?.status))
|
|
91
|
+
return [key, { ...job, status: "failed" }];
|
|
92
|
+
if (isCompletedStatus(event?.status) && event.graph_id != null)
|
|
93
|
+
return [
|
|
94
|
+
key,
|
|
95
|
+
{ ...job, status: "completed", graphId: event.graph_id },
|
|
96
|
+
];
|
|
97
|
+
return [key, job];
|
|
98
|
+
});
|
|
99
|
+
return entries.some(([key, job]) => state.jobs[key] !== job)
|
|
100
|
+
? { ...state, jobs: Object.fromEntries(entries) }
|
|
101
|
+
: state;
|
|
102
|
+
}
|
|
103
|
+
case dismissGraphGeneration.TRIGGER:
|
|
104
|
+
return {
|
|
105
|
+
...state,
|
|
106
|
+
jobs: Object.fromEntries(
|
|
107
|
+
Object.entries(state.jobs).filter(([key]) => key !== payload),
|
|
108
|
+
),
|
|
109
|
+
};
|
|
110
|
+
default:
|
|
111
|
+
return state;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
backgroundGraph,
|
|
2
3
|
clearGraph,
|
|
3
4
|
clearStructure,
|
|
4
5
|
createGraph,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
|
|
11
12
|
export const graphLoading = (state = false, { type, payload }) => {
|
|
12
13
|
switch (type) {
|
|
14
|
+
case backgroundGraph.TRIGGER:
|
|
13
15
|
case clearGraph.TRIGGER:
|
|
14
16
|
return false;
|
|
15
17
|
case clearStructure.TRIGGER:
|
package/src/reducers/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { grantSelectedFilter } from "./grantSelectedFilter";
|
|
|
19
19
|
import { grantsPageSize } from "./grantsPageSize";
|
|
20
20
|
|
|
21
21
|
import { graph } from "./graph";
|
|
22
|
+
import { graphGenerationState } from "./graphGenerationState";
|
|
22
23
|
import { graphLoading } from "./graphLoading";
|
|
23
24
|
import { graphRedirect } from "./graphRedirect";
|
|
24
25
|
import { lineageEvents } from "./lineageEvents";
|
|
@@ -98,6 +99,7 @@ export {
|
|
|
98
99
|
grantSelectedFilter,
|
|
99
100
|
grantsPageSize,
|
|
100
101
|
graph,
|
|
102
|
+
graphGenerationState,
|
|
101
103
|
graphLoading,
|
|
102
104
|
graphRedirect,
|
|
103
105
|
lineageEvents,
|