@truedat/dd 8.10.2 → 8.10.3

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.
@@ -0,0 +1,165 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { connect } from "react-redux";
4
+ import { FormattedMessage, useIntl } from "react-intl";
5
+ import { Button, Icon } from "semantic-ui-react";
6
+ import { fetchLineageEvents } from "../routines";
7
+ import LineageForm from "./LineageForm";
8
+ import LineageEventsPanel from "./LineageEventsPanel";
9
+ import { groupLineageEvents } from "./lineageEventHelpers";
10
+
11
+ export const LineageSelectionPanel = ({
12
+ collapsed,
13
+ fetchLineageEvents,
14
+ onGraphLoad,
15
+ sidebarHintActive,
16
+ sidebarHintOn,
17
+ sidebarHintToken,
18
+ lineageEvents = [],
19
+ onToggle,
20
+ }) => {
21
+ const { formatMessage } = useIntl();
22
+ const [activeSidebarTab, setActiveSidebarTab] = useState("lineage");
23
+ const sidebarRef = useRef();
24
+ const toggleLabel = formatMessage({
25
+ id: collapsed ? "lineage.sidebar.expand" : "lineage.sidebar.collapse",
26
+ });
27
+ const activeRailLabel =
28
+ activeSidebarTab === "events"
29
+ ? "lineage.sidebar.tabs.graphs"
30
+ : "lineage.sidebar.tabs.lineage";
31
+ const openTab = (tab) => {
32
+ setActiveSidebarTab(tab);
33
+ if (tab === "events") fetchLineageEvents();
34
+ };
35
+
36
+ useEffect(() => {
37
+ fetchLineageEvents();
38
+ }, [fetchLineageEvents]);
39
+
40
+ useEffect(() => {
41
+ if (collapsed) return;
42
+
43
+ const handleClickOutside = (e) => {
44
+ if (sidebarRef.current && !sidebarRef.current.contains(e.target)) {
45
+ onToggle();
46
+ }
47
+ };
48
+
49
+ document.addEventListener("mousedown", handleClickOutside);
50
+ return () => document.removeEventListener("mousedown", handleClickOutside);
51
+ }, [collapsed, onToggle]);
52
+
53
+ return (
54
+ <aside
55
+ className="lineage-view__sidebar"
56
+ aria-expanded={!collapsed}
57
+ ref={sidebarRef}
58
+ >
59
+ <div className="lineage-view__sidebar-panel" aria-hidden={collapsed}>
60
+ <div className="lineage-view__sidebar-header">
61
+ <div className="lineage-view__sidebar-tabs" role="tablist">
62
+ <button
63
+ aria-selected={activeSidebarTab === "lineage"}
64
+ className="lineage-view__sidebar-tab"
65
+ onClick={() => openTab("lineage")}
66
+ role="tab"
67
+ type="button"
68
+ >
69
+ <span className="lineage-view__sidebar-tab-icon">
70
+ <Icon name="sitemap" />
71
+ </span>
72
+ <span className="lineage-view__sidebar-tab-label">
73
+ <FormattedMessage
74
+ id="lineage.sidebar.tabs.lineage"
75
+ defaultMessage="Lineage"
76
+ />
77
+ </span>
78
+ </button>
79
+ <button
80
+ aria-selected={activeSidebarTab === "events"}
81
+ className="lineage-view__sidebar-tab"
82
+ onClick={() => openTab("events")}
83
+ role="tab"
84
+ type="button"
85
+ >
86
+ <span className="lineage-view__sidebar-tab-icon">
87
+ <Icon name="history" />
88
+ </span>
89
+ <span className="lineage-view__sidebar-tab-label">
90
+ <FormattedMessage
91
+ id="lineage.sidebar.tabs.graphs"
92
+ defaultMessage="Graphs"
93
+ />
94
+ </span>
95
+ <span className="lineage-view__sidebar-tab-count">
96
+ {groupLineageEvents(lineageEvents).length}
97
+ </span>
98
+ </button>
99
+ </div>
100
+ <Button
101
+ basic
102
+ icon
103
+ size="mini"
104
+ type="button"
105
+ aria-label={toggleLabel}
106
+ title={toggleLabel}
107
+ className="lineage-view__sidebar-toggle"
108
+ onClick={onToggle}
109
+ >
110
+ <Icon name="angle left" />
111
+ </Button>
112
+ </div>
113
+ <div
114
+ className={`lineage-view__sidebar-content lineage-view__sidebar-content--${activeSidebarTab}`}
115
+ >
116
+ {activeSidebarTab === "lineage" ? (
117
+ <LineageForm
118
+ autoCollapse={false}
119
+ collapsible={false}
120
+ onGraphSubmit={onGraphLoad}
121
+ showDownloadButton={false}
122
+ />
123
+ ) : (
124
+ <LineageEventsPanel onGraphLoad={onGraphLoad} />
125
+ )}
126
+ </div>
127
+ </div>
128
+ <Button
129
+ basic
130
+ icon
131
+ type="button"
132
+ aria-label={toggleLabel}
133
+ aria-hidden={!collapsed}
134
+ title={toggleLabel}
135
+ className={`lineage-view__sidebar-rail${
136
+ sidebarHintActive ? " lineage-view__sidebar-rail--hint" : ""
137
+ }${sidebarHintOn ? " lineage-view__sidebar-rail--hint-on" : ""}`}
138
+ key={`lineage-sidebar-rail-${sidebarHintToken}`}
139
+ onClick={onToggle}
140
+ >
141
+ <Icon name="angle right" />
142
+ <span>
143
+ <FormattedMessage id={activeRailLabel} />
144
+ </span>
145
+ </Button>
146
+ </aside>
147
+ );
148
+ };
149
+
150
+ LineageSelectionPanel.propTypes = {
151
+ collapsed: PropTypes.bool.isRequired,
152
+ fetchLineageEvents: PropTypes.func,
153
+ lineageEvents: PropTypes.array,
154
+ onGraphLoad: PropTypes.func.isRequired,
155
+ onToggle: PropTypes.func.isRequired,
156
+ sidebarHintActive: PropTypes.bool,
157
+ sidebarHintOn: PropTypes.bool,
158
+ sidebarHintToken: PropTypes.number,
159
+ };
160
+
161
+ const mapStateToProps = ({ lineageEvents }) => ({ lineageEvents });
162
+
163
+ export default connect(mapStateToProps, { fetchLineageEvents })(
164
+ LineageSelectionPanel,
165
+ );
@@ -1,7 +1,46 @@
1
+ import { waitFor } from "@testing-library/react";
1
2
  import userEvent from "@testing-library/user-event";
2
3
  import { render, waitForLoad } from "@truedat/test/render";
3
- import { domainsMock } from "@truedat/test/mocks";
4
+ import { DOMAINS_QUERY } from "@truedat/core/api/queries";
4
5
  import { LineageForm } from "../LineageForm";
6
+ import { LINEAGE_DOMAIN_AVAILABILITY_CACHE_KEY } from "../lineageDomainCache";
7
+
8
+ const defaultDomains = [
9
+ {
10
+ __typename: "Domain",
11
+ name: "fooDomain",
12
+ id: "1",
13
+ externalId: "foo",
14
+ parentId: null,
15
+ actions: null,
16
+ },
17
+ {
18
+ __typename: "Domain",
19
+ name: "barDomain",
20
+ id: "2",
21
+ externalId: "bar",
22
+ parentId: null,
23
+ actions: null,
24
+ },
25
+ {
26
+ __typename: "Domain",
27
+ name: "bazDomain",
28
+ id: "3",
29
+ externalId: "baz",
30
+ parentId: null,
31
+ actions: null,
32
+ },
33
+ ];
34
+
35
+ const lineageDomainsMock = (domains = defaultDomains) => ({
36
+ request: {
37
+ query: DOMAINS_QUERY,
38
+ variables: { action: "viewLineage" },
39
+ },
40
+ result: {
41
+ data: { domains },
42
+ },
43
+ });
5
44
 
6
45
  describe("<LineageForm />", () => {
7
46
  const fetchNodes = jest.fn();
@@ -15,6 +54,9 @@ describe("<LineageForm />", () => {
15
54
  const props = {
16
55
  createGraph: jest.fn(),
17
56
  clearNodes: jest.fn(),
57
+ domainAvailabilityChecker: jest.fn(async (domains) =>
58
+ domains.map(({ id }) => [`${id}`, true]),
59
+ ),
18
60
  graph: {},
19
61
  groupOptions,
20
62
  resourceOptions,
@@ -28,15 +70,105 @@ describe("<LineageForm />", () => {
28
70
  };
29
71
 
30
72
  const renderOpts = {
31
- mocks: [domainsMock({ action: "viewLineage" })],
73
+ mocks: [lineageDomainsMock()],
32
74
  };
33
75
 
76
+ beforeEach(() => {
77
+ localStorage.clear();
78
+ });
79
+
34
80
  it("matches the latest snapshot", async () => {
35
81
  const rendered = render(<LineageForm {...props} />, renderOpts);
36
82
  await waitForLoad(rendered);
37
83
  expect(rendered.container).toMatchSnapshot();
38
84
  });
39
85
 
86
+ it("uses the form loading state while lineage options are loading", async () => {
87
+ const rendered = render(
88
+ <LineageForm {...props} collapsible={false} loading />,
89
+ renderOpts,
90
+ );
91
+
92
+ expect(
93
+ rendered.container.querySelector(".lineage-toolbar__form.loading"),
94
+ ).toBeInTheDocument();
95
+ expect(
96
+ rendered.queryByText("lineage.graph.loading"),
97
+ ).not.toBeInTheDocument();
98
+ expect(
99
+ rendered.container.querySelector(
100
+ ".lineage-events-panel__empty .circle.notched.loading.icon",
101
+ ),
102
+ ).not.toBeInTheDocument();
103
+ });
104
+
105
+ it("renders a domain dropdown skeleton while lineage domains are loading", async () => {
106
+ const rendered = render(<LineageForm {...props} collapsible={false} />, {
107
+ mocks: [
108
+ {
109
+ ...lineageDomainsMock(),
110
+ delay: 100,
111
+ },
112
+ ],
113
+ });
114
+
115
+ expect(
116
+ rendered.container.querySelector(".lineage-domain-selector-skeleton"),
117
+ ).toBeInTheDocument();
118
+
119
+ await waitFor(() => {
120
+ expect(
121
+ rendered.queryByText("domain.selector.placeholder"),
122
+ ).toBeInTheDocument();
123
+ });
124
+ });
125
+
126
+ it("uses cached lineage domain availability while it is valid", async () => {
127
+ const domainAvailabilityChecker = jest.fn();
128
+ localStorage.setItem(
129
+ LINEAGE_DOMAIN_AVAILABILITY_CACHE_KEY,
130
+ JSON.stringify({
131
+ checks: [
132
+ ["1", true],
133
+ ["2", false],
134
+ ["3", true],
135
+ ],
136
+ domainIds: ["1", "2", "3"],
137
+ expiresAt: Date.now() + 60000,
138
+ }),
139
+ );
140
+
141
+ const rendered = render(
142
+ <LineageForm
143
+ {...props}
144
+ domainAvailabilityChecker={domainAvailabilityChecker}
145
+ />,
146
+ renderOpts,
147
+ );
148
+
149
+ await waitFor(() => {
150
+ expect(
151
+ rendered.queryByText("domain.selector.placeholder"),
152
+ ).toBeInTheDocument();
153
+ });
154
+
155
+ expect(domainAvailabilityChecker).not.toHaveBeenCalled();
156
+ });
157
+
158
+ it("renders the lineage sidebar header with the same panel layout as my graphs", async () => {
159
+ const rendered = render(
160
+ <LineageForm {...props} collapsible={false} resourceOptions={[]} />,
161
+ renderOpts,
162
+ );
163
+ await waitForLoad(rendered);
164
+ const header = rendered
165
+ .getByText("lineage.form.panel.header")
166
+ .closest("div");
167
+
168
+ expect(header).toHaveClass("lineage-events-panel__header");
169
+ expect(header.querySelector(".sitemap.icon")).toBeInTheDocument();
170
+ });
171
+
40
172
  it("selecting group will call selectGroup function", async () => {
41
173
  const selectGroup = jest.fn();
42
174
  const rendered = render(
@@ -55,7 +187,14 @@ describe("<LineageForm />", () => {
55
187
  it("selecting resources will call selectResources function", async () => {
56
188
  const selectResources = jest.fn();
57
189
  const rendered = render(
58
- <LineageForm {...props} selectResources={selectResources} />,
190
+ <LineageForm
191
+ {...props}
192
+ resourceOptions={[
193
+ { key: "res1", text: "res1", value: "res1" },
194
+ { key: "res2", text: "res2", value: "res2" },
195
+ ]}
196
+ selectResources={selectResources}
197
+ />,
59
198
  renderOpts,
60
199
  );
61
200
  await waitForLoad(rendered);
@@ -68,13 +207,105 @@ describe("<LineageForm />", () => {
68
207
  expect(selectResources).toHaveBeenCalledWith(["res1"]);
69
208
  });
70
209
 
210
+ it("selects all resources from the icon toggle", async () => {
211
+ const selectResources = jest.fn();
212
+ const rendered = render(
213
+ <LineageForm
214
+ {...props}
215
+ resourceOptions={[
216
+ { key: "res1", text: "res1", value: "res1" },
217
+ { key: "res2", text: "res2", value: "res2" },
218
+ ]}
219
+ selectResources={selectResources}
220
+ selectedResources={[]}
221
+ />,
222
+ renderOpts,
223
+ );
224
+ await waitForLoad(rendered);
225
+ const user = userEvent.setup({ delay: null });
226
+
227
+ await user.click(
228
+ rendered.getByRole("button", { name: /lineage.resources.add_all/i }),
229
+ );
230
+
231
+ expect(selectResources).toHaveBeenCalledWith(["res1", "res2"]);
232
+ });
233
+
234
+ it("clears all resources from the icon toggle", async () => {
235
+ const selectResources = jest.fn();
236
+ const rendered = render(
237
+ <LineageForm
238
+ {...props}
239
+ resourceOptions={[
240
+ { key: "res1", text: "res1", value: "res1" },
241
+ { key: "res2", text: "res2", value: "res2" },
242
+ ]}
243
+ selectResources={selectResources}
244
+ selectedResources={["res1", "res2"]}
245
+ />,
246
+ renderOpts,
247
+ );
248
+ await waitForLoad(rendered);
249
+ const user = userEvent.setup({ delay: null });
250
+
251
+ await user.click(
252
+ rendered.getByRole("button", { name: /lineage.resources.remove_all/i }),
253
+ );
254
+
255
+ expect(selectResources).toHaveBeenCalledWith([]);
256
+ });
257
+
258
+ it("selects the next single group option automatically", async () => {
259
+ const selectGroup = jest.fn();
260
+ render(
261
+ <LineageForm
262
+ {...props}
263
+ groupOptions={[[{ key: "grp1", text: "grp1", value: "grp1" }]]}
264
+ resourceOptions={[]}
265
+ selectGroup={selectGroup}
266
+ />,
267
+ renderOpts,
268
+ );
269
+
270
+ await waitFor(() => {
271
+ expect(selectGroup).toHaveBeenCalledWith({
272
+ id: "grp1",
273
+ domain_id: null,
274
+ });
275
+ });
276
+ });
277
+
278
+ it("does not skip a previous group dropdown with multiple options", async () => {
279
+ const selectGroup = jest.fn();
280
+ const rendered = render(
281
+ <LineageForm
282
+ {...props}
283
+ groupOptions={[
284
+ [
285
+ { key: "grp1", text: "grp1", value: "grp1" },
286
+ { key: "grp2", text: "grp2", value: "grp2" },
287
+ ],
288
+ [{ key: "subgrp1", text: "subgrp1", value: "subgrp1" }],
289
+ ]}
290
+ resourceOptions={[]}
291
+ selectGroup={selectGroup}
292
+ />,
293
+ renderOpts,
294
+ );
295
+
296
+ await waitForLoad(rendered);
297
+ expect(selectGroup).not.toHaveBeenCalled();
298
+ });
299
+
71
300
  it("handle submit action", async () => {
72
301
  const selectedResources = ["id1", "id2"];
73
302
  const createGraph = jest.fn();
303
+ const onGraphSubmit = jest.fn();
74
304
  const rendered = render(
75
305
  <LineageForm
76
306
  {...props}
77
307
  createGraph={createGraph}
308
+ onGraphSubmit={onGraphSubmit}
78
309
  selectedResources={selectedResources}
79
310
  />,
80
311
  renderOpts,
@@ -96,6 +327,72 @@ describe("<LineageForm />", () => {
96
327
  levels: 4,
97
328
  isRedirected: true,
98
329
  });
330
+ expect(onGraphSubmit).toHaveBeenCalledTimes(1);
331
+ });
332
+
333
+ it("notifies graph submit when requesting impact", async () => {
334
+ const createGraph = jest.fn();
335
+ const onGraphSubmit = jest.fn();
336
+ const rendered = render(
337
+ <LineageForm
338
+ {...props}
339
+ createGraph={createGraph}
340
+ onGraphSubmit={onGraphSubmit}
341
+ selectedResources={["id1"]}
342
+ />,
343
+ renderOpts,
344
+ );
345
+ await waitForLoad(rendered);
346
+ const user = userEvent.setup({ delay: null });
347
+
348
+ await user.click(
349
+ rendered.getByRole("button", {
350
+ name: /navigation.lineage.impact/i,
351
+ }),
352
+ );
353
+
354
+ expect(createGraph).toHaveBeenCalledWith({
355
+ ids: ["id1"],
356
+ type: "impact",
357
+ levels: 4,
358
+ isRedirected: true,
359
+ });
360
+ expect(onGraphSubmit).toHaveBeenCalledTimes(1);
361
+ });
362
+
363
+ it("clears the whole lineage form including the selected domain", async () => {
364
+ const clearNodes = jest.fn();
365
+ const fetchNodes = jest.fn();
366
+ const selectResources = jest.fn();
367
+ const rendered = render(
368
+ <LineageForm
369
+ {...props}
370
+ clearNodes={clearNodes}
371
+ fetchNodes={fetchNodes}
372
+ selectResources={selectResources}
373
+ selectedGroups={["grp1"]}
374
+ selectedResources={["res1"]}
375
+ />,
376
+ renderOpts,
377
+ );
378
+ await waitForLoad(rendered);
379
+ const user = userEvent.setup({ delay: null });
380
+
381
+ await user.click(rendered.getByText(/foodomain/i));
382
+ await waitFor(() => {
383
+ expect(fetchNodes).toHaveBeenCalledWith({ domain_id: "1" });
384
+ });
385
+
386
+ await user.click(
387
+ rendered.getByRole("button", { name: /navigation.lineage.clean/i }),
388
+ );
389
+
390
+ expect(clearNodes).toHaveBeenCalledTimes(1);
391
+ expect(selectResources).toHaveBeenCalledWith([]);
392
+ expect(fetchNodes).not.toHaveBeenCalledWith({});
393
+ expect(
394
+ rendered.getByText("domain.selector.placeholder"),
395
+ ).toBeInTheDocument();
99
396
  });
100
397
 
101
398
  it("calls clearNodes when component unmounts but not when it mounts", async () => {
@@ -119,4 +416,47 @@ describe("<LineageForm />", () => {
119
416
  await user.click(rendered.getByRole("option", { name: /foodomain/i }));
120
417
  expect(fetchNodes).toHaveBeenCalledWith({ domain_id: "1" });
121
418
  });
419
+
420
+ it("loads nodes automatically when a single lineage domain is available", async () => {
421
+ const fetchNodes = jest.fn();
422
+ const rendered = render(
423
+ <LineageForm {...props} fetchNodes={fetchNodes} />,
424
+ {
425
+ mocks: [lineageDomainsMock([defaultDomains[0]])],
426
+ },
427
+ );
428
+ await waitForLoad(rendered);
429
+
430
+ await waitFor(() => {
431
+ expect(fetchNodes).toHaveBeenCalledWith({ domain_id: "1" });
432
+ });
433
+ });
434
+
435
+ it("disables domains without lineage", async () => {
436
+ const fetchNodes = jest.fn();
437
+ const rendered = render(
438
+ <LineageForm
439
+ {...props}
440
+ domainAvailabilityChecker={jest.fn(async () => [
441
+ ["1", true],
442
+ ["2", false],
443
+ ])}
444
+ fetchNodes={fetchNodes}
445
+ />,
446
+ {
447
+ mocks: [lineageDomainsMock([defaultDomains[0], defaultDomains[1]])],
448
+ },
449
+ );
450
+ await waitForLoad(rendered);
451
+ const user = userEvent.setup({ delay: null });
452
+
453
+ await waitFor(() => {
454
+ expect(rendered.getByRole("option", { name: /bardomain/i })).toHaveClass(
455
+ "disabled",
456
+ );
457
+ });
458
+ await user.click(rendered.getByRole("option", { name: /bardomain/i }));
459
+
460
+ expect(fetchNodes).not.toHaveBeenCalledWith({ domain_id: "2" });
461
+ });
122
462
  });