@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.
@@ -1,17 +1,11 @@
1
1
  import _ from "lodash/fp";
2
- import { useEffect, useState } from "react";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { connect } from "react-redux";
5
5
  import { FormattedMessage, useIntl } from "react-intl";
6
- import {
7
- Accordion,
8
- Form,
9
- Icon,
10
- Segment,
11
- Header,
12
- Button,
13
- } from "semantic-ui-react";
6
+ import { Accordion, Form, Icon, Header, Button } from "semantic-ui-react";
14
7
  import { DomainSelector } from "@truedat/core/components";
8
+ import { DOMAINS_QUERY } from "@truedat/core/api/queries";
15
9
  import {
16
10
  clearNodes,
17
11
  selectGroup,
@@ -26,53 +20,95 @@ import {
26
20
  lineageLevels,
27
21
  } from "../selectors";
28
22
  import LineageDownloadButton from "./LineageDownloadButton";
23
+ import { LineageResourceToggle } from "./LineageResourceToggle";
24
+ import LineageDomainSelectorSkeleton from "./LineageDomainSelectorSkeleton";
25
+ import {
26
+ LINEAGE_DOMAIN_CACHE_KEY,
27
+ LINEAGE_DOMAIN_CACHE_TTL,
28
+ checkDomainsLineage,
29
+ readDomainAvailabilityCache,
30
+ writeDomainAvailabilityCache,
31
+ } from "./lineageDomainCache";
32
+
33
+ const singleOptionValue = (options = []) =>
34
+ options.length === 1 ? options[0]?.value : undefined;
29
35
 
30
36
  export const LineageForm = ({
37
+ autoCollapse = true,
38
+ collapsible = true,
31
39
  createGraph,
32
40
  clearNodes,
41
+ domainAvailabilityChecker = checkDomainsLineage,
33
42
  fetchNodes,
34
43
  graph,
35
44
  graphLoading,
36
- groupOptions,
45
+ groupOptions = [],
37
46
  levelsSelection,
38
47
  lineageLevels,
39
48
  loading,
40
- resourceOptions,
49
+ onGraphSubmit,
50
+ resourceOptions = [],
41
51
  selectGroup,
42
52
  selectResources,
43
- selectedGroups,
44
- selectedResources,
53
+ selectedGroups = [],
54
+ selectedResources = [],
55
+ showDownloadButton = true,
45
56
  }) => {
46
57
  const [domainId, setDomainId] = useState(null);
47
58
  const [open, setOpen] = useState(true);
48
59
  const [selectLevels, setSelectLevels] = useState(levelsSelection);
60
+ const [unavailableDomainIds, setUnavailableDomainIds] = useState([]);
49
61
  const [levels, setLevels] = useState(lineageLevels);
62
+ const availabilityRequestId = useRef(0);
63
+ const resourceValues = _.map("value")(resourceOptions);
64
+ const allResourcesSelected =
65
+ !_.isEmpty(resourceValues) &&
66
+ _.isEmpty(_.difference(resourceValues, selectedResources || []));
50
67
  const handleSubmit = (type) => () => {
51
- _.isNil(levels)
52
- ? createGraph({
68
+ const graphParams = _.isNil(levels)
69
+ ? {
53
70
  ids: selectedResources,
54
71
  type,
55
72
  isRedirected: true,
56
- })
57
- : createGraph({
73
+ }
74
+ : {
58
75
  ids: selectedResources,
59
76
  type,
60
77
  levels,
61
78
  isRedirected: true,
62
- });
79
+ };
80
+
81
+ createGraph(graphParams);
82
+ setOpen(false);
83
+ onGraphSubmit && onGraphSubmit();
63
84
  };
64
85
  const disabled = _.isEmpty(selectedResources);
65
86
  useEffect(() => {
66
- setOpen(!graphLoading && _.isEmpty(graph));
67
- }, [graph, graphLoading]);
87
+ if (autoCollapse) setOpen(!graphLoading && _.isEmpty(graph));
88
+ }, [autoCollapse, graph, graphLoading]);
68
89
  useEffect(() => {
69
90
  selectLevels === "all" && setLevels();
70
91
  }, [selectLevels]);
71
92
  useEffect(() => {
72
- domainId && _.isEmpty(groupOptions) && fetchNodes({ domain_id: domainId });
73
- }, [domainId, fetchNodes, groupOptions]);
93
+ domainId && fetchNodes({ domain_id: domainId });
94
+ }, [domainId, fetchNodes]);
95
+ useEffect(() => {
96
+ const nextGroupLevel = groupOptions.findIndex((options, level) => {
97
+ const selectedGroup = _.nth(level, selectedGroups);
98
+ const optionValues = _.map("value")(options);
99
+
100
+ return !_.includes(selectedGroup)(optionValues);
101
+ });
102
+ const value = singleOptionValue(groupOptions[nextGroupLevel]);
103
+
104
+ if (value !== undefined) {
105
+ selectGroup({ id: value, domain_id: domainId });
106
+ }
107
+ }, [domainId, groupOptions, selectGroup, selectedGroups]);
108
+
74
109
  useEffect(() => {
75
110
  return () => {
111
+ availabilityRequestId.current += 1;
76
112
  clearNodes();
77
113
  };
78
114
  }, [clearNodes]);
@@ -94,7 +130,6 @@ export const LineageForm = ({
94
130
  clearNodes();
95
131
  } else {
96
132
  setDomainId(value);
97
- fetchNodes({ domain_id: value });
98
133
  }
99
134
  };
100
135
 
@@ -104,56 +139,99 @@ export const LineageForm = ({
104
139
  }
105
140
  };
106
141
 
107
- const handleDomainsLoaded = ({ domains }) => {
108
- if (domains?.length == 1) {
109
- setDomainId(domains[0].id);
110
- }
142
+ const handleDomainsLoaded = useCallback(
143
+ async ({ domains = [] }) => {
144
+ const requestId = availabilityRequestId.current + 1;
145
+
146
+ availabilityRequestId.current = requestId;
147
+
148
+ const cachedChecks = readDomainAvailabilityCache(domains);
149
+ const checks = cachedChecks || (await domainAvailabilityChecker(domains));
150
+ if (availabilityRequestId.current !== requestId) return;
151
+
152
+ if (!cachedChecks) writeDomainAvailabilityCache(domains, checks);
153
+
154
+ const availableDomainIds = checks
155
+ .filter(([, available]) => available)
156
+ .map(([id]) => id);
157
+ const unavailableDomainIds = checks
158
+ .filter(([, available]) => !available)
159
+ .map(([id]) => id);
160
+ const availableDomains = domains.filter(({ id }) =>
161
+ availableDomainIds.includes(`${id}`),
162
+ );
163
+
164
+ setUnavailableDomainIds(unavailableDomainIds);
165
+
166
+ if (availableDomains.length === 1) {
167
+ setDomainId(availableDomains[0].id);
168
+ }
169
+ },
170
+ [domainAvailabilityChecker],
171
+ );
172
+
173
+ const isLineageDomainDisabled = ({ id }) =>
174
+ unavailableDomainIds.includes(`${id}`);
175
+
176
+ const toggleAllResources = (e) => {
177
+ e && e.preventDefault();
178
+ selectResources(allResourcesSelected ? [] : resourceValues);
111
179
  };
112
180
 
113
- return (
114
- <Segment>
115
- <Accordion>
116
- <Accordion.Title
117
- active={open}
118
- onClick={() => setOpen(!open)}
119
- className="noselect"
120
- >
121
- <Header as="h4">
122
- <Icon name="dropdown" />
123
- <FormattedMessage id="lineage.form.header" />
124
- </Header>
125
- </Accordion.Title>
126
- <Accordion.Content active={open}>
127
- <Form size="small" loading={loading}>
128
- <DomainSelector
129
- action="viewLineage"
130
- labels
131
- onChange={handleDomainChange}
132
- onLoad={handleDomainsLoaded}
133
- value={domainId}
181
+ const clearSelection = () => {
182
+ setDomainId(null);
183
+ setSelectLevels(levelsSelection);
184
+ setLevels(lineageLevels);
185
+ selectResources([]);
186
+ clearNodes();
187
+ };
188
+
189
+ const form = (
190
+ <Form loading={loading} size="small" className="lineage-toolbar__form">
191
+ <div className="lineage-toolbar__fields">
192
+ <div className="lineage-selection-column">
193
+ <DomainSelector
194
+ action="viewLineage"
195
+ cacheKey={LINEAGE_DOMAIN_CACHE_KEY}
196
+ cacheTtl={LINEAGE_DOMAIN_CACHE_TTL}
197
+ className="lineage-domain-selector"
198
+ closeOnSelect
199
+ labels
200
+ loadingComponent={<LineageDomainSelectorSkeleton />}
201
+ onChange={handleDomainChange}
202
+ onLoad={handleDomainsLoaded}
203
+ optionDisabled={isLineageDomainDisabled}
204
+ query={DOMAINS_QUERY}
205
+ value={domainId}
206
+ />
207
+ </div>
208
+ {_.map.convert({ cap: false })((options, level) => (
209
+ <div className="lineage-selection-column" key={level}>
210
+ <Form.Dropdown
211
+ className="lineage-column-dropdown"
212
+ placeholder={formatMessage({
213
+ id:
214
+ level == 0
215
+ ? "lineage.group.placeholder"
216
+ : "lineage.subgroup.placeholder",
217
+ })}
218
+ fluid
219
+ search
220
+ selection
221
+ options={options}
222
+ loading={loading}
223
+ value={_.nth(level, selectedGroups) || null}
224
+ onChange={(_e, { value: id }) =>
225
+ selectGroup({ id, domain_id: domainId })
226
+ }
134
227
  />
135
- {_.map.convert({ cap: false })((options, level) => (
136
- <Form.Dropdown
137
- key={level}
138
- placeholder={formatMessage({
139
- id:
140
- level == 0
141
- ? "lineage.group.placeholder"
142
- : "lineage.subgroup.placeholder",
143
- })}
144
- fluid
145
- search
146
- selection
147
- options={options}
148
- loading={loading}
149
- value={_.nth(level, selectedGroups) || null}
150
- onChange={(_e, { value: id }) =>
151
- selectGroup({ id, domain_id: domainId })
152
- }
153
- />
154
- ))(groupOptions)}
155
- {_.isEmpty(resourceOptions) ? null : (
228
+ </div>
229
+ ))(groupOptions)}
230
+ {_.isEmpty(resourceOptions) ? null : (
231
+ <div className="lineage-selection-column lineage-selection-column--resource">
232
+ <div className="lineage-resource-field">
156
233
  <Form.Dropdown
234
+ className="lineage-column-dropdown lineage-resource-selector"
157
235
  placeholder={formatMessage({
158
236
  id: "lineage.resources.placeholder",
159
237
  })}
@@ -165,90 +243,131 @@ export const LineageForm = ({
165
243
  value={selectedResources}
166
244
  onChange={(_e, { value }) => selectResources(value)}
167
245
  />
168
- )}
169
- <Header as="h5">
170
- <FormattedMessage id="lineage.form.levels.header" />
171
- </Header>
172
- <Form.Group>
173
- <Form.Field>
174
- <Form.Radio
175
- disabled={disabled}
176
- name="allLevels"
177
- label={formatMessage({
178
- id: "lineage.form.levels.all.label",
179
- })}
180
- value="all"
181
- checked={selectLevels === "all"}
182
- onChange={(e, { value }) => setSelectLevels(value)}
183
- />
184
- </Form.Field>
185
- <Form.Field>
186
- <Form.Radio
187
- disabled={disabled}
188
- name="selectLevels"
189
- checked={selectLevels === "select"}
190
- value="select"
191
- onChange={(e, { value }) => setSelectLevels(value)}
192
- label={formatMessage({
193
- id: "lineage.form.levels.select.label",
194
- })}
195
- />
196
- </Form.Field>
197
- </Form.Group>
198
- <Form.Field disabled={selectLevels === "all" || disabled}>
199
- <Form.Input
200
- type="number"
201
- onKeyDown={handleKeyDown}
202
- control="input"
203
- placeholder={formatMessage({
204
- id: "lineage.form.levels.select.input",
246
+ <LineageResourceToggle
247
+ allResourcesSelected={allResourcesSelected}
248
+ disabled={_.isEmpty(resourceValues)}
249
+ onClick={toggleAllResources}
250
+ />
251
+ </div>
252
+ </div>
253
+ )}
254
+ </div>
255
+ <div className="lineage-toolbar__controls">
256
+ <div className="lineage-toolbar__levels">
257
+ <Header as="h5">
258
+ <FormattedMessage id="lineage.form.levels.header" />
259
+ </Header>
260
+ <Form.Group inline className="lineage-toolbar__level-options">
261
+ <Form.Field className="lineage-toolbar__level-option">
262
+ <Form.Radio
263
+ disabled={disabled}
264
+ name="allLevels"
265
+ label={formatMessage({
266
+ id: "lineage.form.levels.all.label",
205
267
  })}
206
- value={levels || ""}
207
- autoComplete="off"
208
- onChange={(e) => changeLevels(e)}
209
- min="1"
268
+ value="all"
269
+ checked={selectLevels === "all"}
270
+ onChange={(e, { value }) => setSelectLevels(value)}
210
271
  />
211
272
  </Form.Field>
212
- <Button
213
- secondary
214
- size="small"
215
- floated="right"
216
- disabled={_.isEmpty(selectedGroups)}
217
- onClick={() => {
218
- fetchNodes({});
219
- setSelectLevels(levelsSelection);
220
- setLevels(lineageLevels);
221
- }}
222
- >
223
- <FormattedMessage id="navigation.lineage.clean" />
224
- </Button>
225
- <Button
226
- primary
227
- size="small"
228
- disabled={disabled}
229
- onClick={handleSubmit("impact")}
230
- >
231
- <FormattedMessage id="navigation.lineage.impact" />
232
- </Button>
233
- <Button
234
- primary
235
- size="small"
236
- disabled={disabled}
237
- onClick={handleSubmit("lineage")}
238
- >
239
- <FormattedMessage id="navigation.lineage.lineage" />
240
- </Button>
241
- <LineageDownloadButton />
242
- </Form>
243
- </Accordion.Content>
244
- </Accordion>
245
- </Segment>
273
+ <div className="field lineage-toolbar__level-depth">
274
+ <Form.Radio
275
+ disabled={disabled}
276
+ name="selectLevels"
277
+ checked={selectLevels === "select"}
278
+ value="select"
279
+ onChange={(e, { value }) => setSelectLevels(value)}
280
+ label={formatMessage({
281
+ id: "lineage.form.levels.select.label",
282
+ })}
283
+ />
284
+ <Form.Field
285
+ className="lineage-toolbar__level-input"
286
+ disabled={selectLevels === "all" || disabled}
287
+ >
288
+ <Form.Input
289
+ type="number"
290
+ onKeyDown={handleKeyDown}
291
+ control="input"
292
+ value={levels || ""}
293
+ autoComplete="off"
294
+ onChange={(e) => changeLevels(e)}
295
+ min="1"
296
+ />
297
+ </Form.Field>
298
+ </div>
299
+ </Form.Group>
300
+ </div>
301
+ <div className="lineage-toolbar__actions">
302
+ <Button
303
+ secondary
304
+ size="small"
305
+ disabled={_.isEmpty(selectedGroups)}
306
+ onClick={clearSelection}
307
+ >
308
+ <FormattedMessage id="navigation.lineage.clean" />
309
+ </Button>
310
+ <Button
311
+ primary
312
+ size="small"
313
+ disabled={disabled}
314
+ onClick={handleSubmit("impact")}
315
+ >
316
+ <FormattedMessage id="navigation.lineage.impact" />
317
+ </Button>
318
+ <Button
319
+ primary
320
+ size="small"
321
+ disabled={disabled}
322
+ onClick={handleSubmit("lineage")}
323
+ >
324
+ <FormattedMessage id="navigation.lineage.lineage" />
325
+ </Button>
326
+ {showDownloadButton ? <LineageDownloadButton /> : null}
327
+ </div>
328
+ </div>
329
+ </Form>
330
+ );
331
+
332
+ if (!collapsible) {
333
+ return (
334
+ <div className="lineage-toolbar lineage-toolbar--panel">
335
+ <div className="lineage-events-panel">
336
+ <div className="lineage-events-panel__header">
337
+ <span>
338
+ <FormattedMessage id="lineage.form.panel.header" />
339
+ </span>
340
+ <Icon name="sitemap" />
341
+ </div>
342
+ {form}
343
+ </div>
344
+ </div>
345
+ );
346
+ }
347
+
348
+ return (
349
+ <Accordion className="lineage-toolbar">
350
+ <Accordion.Title
351
+ active={open}
352
+ onClick={() => setOpen(!open)}
353
+ className="noselect lineage-toolbar__title"
354
+ >
355
+ <Header as="h4">
356
+ <Icon name="dropdown" />
357
+ <FormattedMessage id="lineage.form.header" />
358
+ </Header>
359
+ </Accordion.Title>
360
+ <Accordion.Content active={open}>{form}</Accordion.Content>
361
+ </Accordion>
246
362
  );
247
363
  };
248
364
 
249
365
  LineageForm.propTypes = {
366
+ autoCollapse: PropTypes.bool,
367
+ collapsible: PropTypes.bool,
250
368
  clearNodes: PropTypes.func.isRequired,
251
369
  createGraph: PropTypes.func.isRequired,
370
+ domainAvailabilityChecker: PropTypes.func,
252
371
  fetchNodes: PropTypes.func,
253
372
  graph: PropTypes.object,
254
373
  graphLoading: PropTypes.bool,
@@ -256,11 +375,13 @@ LineageForm.propTypes = {
256
375
  levelsSelection: PropTypes.string,
257
376
  lineageLevels: PropTypes.number,
258
377
  loading: PropTypes.bool,
378
+ onGraphSubmit: PropTypes.func,
259
379
  resourceOptions: PropTypes.array,
260
380
  selectGroup: PropTypes.func.isRequired,
261
381
  selectResources: PropTypes.func.isRequired,
262
382
  selectedGroups: PropTypes.array,
263
383
  selectedResources: PropTypes.array,
384
+ showDownloadButton: PropTypes.bool,
264
385
  };
265
386
 
266
387
  const mapStateToProps = (state) => ({
@@ -253,7 +253,6 @@ export const LineageGraph = ({
253
253
  onPointerDown={onPointerDown}
254
254
  onPointerUp={onPointerUp}
255
255
  onPointerCancel={onPointerUp}
256
- style={{ border: "1px solid #d4d4d5", borderRadius: "5px" }}
257
256
  >
258
257
  <LineageDefs />
259
258
  <g className="svg-pan-zoom_viewport">
@@ -0,0 +1,33 @@
1
+ import { useIntl, FormattedMessage } from "react-intl";
2
+ import { Icon } from "semantic-ui-react";
3
+
4
+ const LineageHeader = () => {
5
+ const { formatMessage } = useIntl();
6
+
7
+ return (
8
+ <div className="lineage-view__header">
9
+ <div className="lineage-view__header-main">
10
+ <div className="lineage-view__header-icon" aria-hidden="true">
11
+ <Icon
12
+ name={formatMessage({
13
+ id: "lineage.header.icon",
14
+ defaultMessage: "sitemap",
15
+ })}
16
+ />
17
+ </div>
18
+ <div className="lineage-view__header-copy">
19
+ <div className="lineage-view__header-title-row">
20
+ <h2>
21
+ <FormattedMessage id="navigation.lineage" />
22
+ </h2>
23
+ </div>
24
+ <p>
25
+ <FormattedMessage id="lineage.header.description" />
26
+ </p>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ );
31
+ };
32
+
33
+ export default LineageHeader;
@@ -0,0 +1,56 @@
1
+ import PropTypes from "prop-types";
2
+ import { useIntl } from "react-intl";
3
+ import { Icon } from "semantic-ui-react";
4
+
5
+ export const LineageResourceToggle = ({
6
+ allResourcesSelected,
7
+ disabled,
8
+ onClick,
9
+ }) => {
10
+ const { formatMessage } = useIntl();
11
+ const state = allResourcesSelected ? "on" : "off";
12
+
13
+ const icon = formatMessage({
14
+ id: `lineage.resources.toggle.${state}.icon`,
15
+ defaultMessage: allResourcesSelected ? "minus" : "plus",
16
+ }).replace("none", "");
17
+
18
+ const label = formatMessage({
19
+ id: `lineage.resources.toggle.${state}.label`,
20
+ defaultMessage: allResourcesSelected ? "remove all" : "add all",
21
+ }).replace("none", "");
22
+
23
+ const hoverText = formatMessage({
24
+ id: `lineage.resources.toggle.${state}.hover`,
25
+ defaultMessage: allResourcesSelected ? "remove all" : "add all",
26
+ }).replace("none", "");
27
+
28
+ const ariaLabel = formatMessage({
29
+ id: allResourcesSelected
30
+ ? "lineage.resources.remove_all"
31
+ : "lineage.resources.add_all",
32
+ });
33
+
34
+ if (!icon && !label) return null;
35
+
36
+ return (
37
+ <button
38
+ type="button"
39
+ aria-label={ariaLabel}
40
+ className={`lineage-resource-toggle${state === "off" ? " lineage-resource-toggle--add" : ""}`}
41
+ disabled={disabled}
42
+ onClick={onClick}
43
+ >
44
+ <span title={hoverText} className="lineage-resource-toggle-content">
45
+ {icon ? <Icon name={icon} /> : null}
46
+ {label ? <span>{label}</span> : null}
47
+ </span>
48
+ </button>
49
+ );
50
+ };
51
+
52
+ LineageResourceToggle.propTypes = {
53
+ allResourcesSelected: PropTypes.bool,
54
+ disabled: PropTypes.bool,
55
+ onClick: PropTypes.func,
56
+ };
@@ -1,22 +1,9 @@
1
1
  import React from "react";
2
2
  import { Routes, Route } from "react-router";
3
3
  import { ProtectedRoute } from "@truedat/core/router";
4
- import { GRAPH, GRAPHS, LINEAGE_EVENTS, SAMPLE } from "@truedat/core/routes";
5
- import LineageEvents from "./LineageEvents";
6
- import LineageGraph from "./LineageGraph";
7
- import LineageSample from "./LineageSample";
8
- import LineageForm from "./LineageForm";
4
+ import { GRAPHS } from "@truedat/core/routes";
9
5
  import GraphLoader from "./GraphLoader";
10
- import GraphRedirector from "./GraphRedirector";
11
-
12
- const Graphs = () => (
13
- <>
14
- <LineageForm />
15
- <GraphRedirector />
16
- {/* Load LineageGraph here so that its <Loader> is already available when clicking the LinageForm */}
17
- <LineageGraph />
18
- </>
19
- );
6
+ import Graphs from "./Graphs";
20
7
 
21
8
  export const LineageRoutes = () => {
22
9
  return (
@@ -33,22 +20,6 @@ export const LineageRoutes = () => {
33
20
  }
34
21
  />
35
22
  </Route>
36
- <Route
37
- path={LINEAGE_EVENTS}
38
- element={
39
- <ProtectedRoute authorization="lineage">
40
- <LineageEvents />
41
- </ProtectedRoute>
42
- }
43
- />
44
- <Route
45
- path={SAMPLE}
46
- element={
47
- <ProtectedRoute authorization="lineage">
48
- <LineageSample />
49
- </ProtectedRoute>
50
- }
51
- />
52
23
  <Route path="*" element={null} />
53
24
  </Routes>
54
25
  );