@truedat/dq 4.34.2 → 4.35.4

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/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [4.35.2] 2021-12-20
4
+
5
+ ### Fixed
6
+
7
+ - [TD-4380] Allow modifier in operator filter
8
+
9
+ ## [4.34.3] 2021-12-15
10
+
11
+ ### Added
12
+
13
+ - [TD-4273] Upload `RuleResults`
14
+
15
+ ## [4.34.2] 2021-12-14
16
+
17
+ ### Added
4
18
 
5
19
  - [TD-4314] Add BulkLoad for `rules`
6
20
  - [TD-4301] Add BulkLoad for `implementations`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dq",
3
- "version": "4.34.2",
3
+ "version": "4.35.4",
4
4
  "description": "Truedat Web Data Quality Module",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -82,8 +82,8 @@
82
82
  },
83
83
  "dependencies": {
84
84
  "@apollo/client": "^3.4.10",
85
- "@truedat/core": "4.34.2",
86
- "@truedat/df": "4.34.2",
85
+ "@truedat/core": "4.35.4",
86
+ "@truedat/df": "4.35.4",
87
87
  "axios": "^0.19.2",
88
88
  "graphql": "^15.5.3",
89
89
  "path-to-regexp": "^1.7.0",
@@ -103,5 +103,5 @@
103
103
  "react-dom": ">= 16.8.6 < 17",
104
104
  "semantic-ui-react": ">= 0.88.2 < 2.1"
105
105
  },
106
- "gitHead": "8b28a5faca902e9cab3e8b5ab2bc1f8626e3ec7b"
106
+ "gitHead": "b03fba26635e365bbab64a6de1dd9a155925b5d1"
107
107
  }
package/src/api.js CHANGED
@@ -16,6 +16,7 @@ const API_RULE_IMPLEMENTATION_FILTERS_SEARCH =
16
16
  "/api/rule_implementation_filters/search";
17
17
  const API_RULE_IMPLEMENTATIONS_UPLOAD = "/api/rule_implementations/upload";
18
18
  const API_RULE_RESULT = "/api/rule_results/:id";
19
+ const API_RULE_RESULTS = "/api/rule_results";
19
20
  const API_SUBSCRIPTIONS_SEARCH = "/api/subscriptions/user/me/search";
20
21
  const API_SUBSCRIPTION = "/api/subscriptions/:id";
21
22
  const API_SUBSCRIPTIONS = "/api/subscriptions";
@@ -37,6 +38,7 @@ export {
37
38
  API_RULE_IMPLEMENTATION_FILTERS_SEARCH,
38
39
  API_RULE_IMPLEMENTATIONS_UPLOAD,
39
40
  API_RULE_RESULT,
41
+ API_RULE_RESULTS,
40
42
  API_SUBSCRIPTIONS_SEARCH,
41
43
  API_SUBSCRIPTION,
42
44
  API_SUBSCRIPTIONS,
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import PropTypes from "prop-types";
3
3
  import { connect } from "react-redux";
4
- import { Button } from "semantic-ui-react";
4
+ import { Dropdown } from "semantic-ui-react";
5
5
  import { FormattedMessage, useIntl } from "react-intl";
6
6
  import { UploadModal } from "@truedat/core/components";
7
7
  import { API_RULE_IMPLEMENTATIONS_UPLOAD } from "../api";
@@ -21,14 +21,12 @@ export const ImplementationsUploadButton = ({
21
21
  <UploadModal
22
22
  icon="upload"
23
23
  trigger={
24
- <Button
25
- secondary
26
- floated="right"
24
+ <Dropdown.Item
27
25
  icon="upload"
28
- loading={loading}
29
- data-tooltip={formatMessage({
26
+ text={formatMessage({
30
27
  id: "ruleImplementations.actions.upload.tooltip",
31
28
  })}
29
+ disabled={loading}
32
30
  />
33
31
  }
34
32
  header={
@@ -2,9 +2,7 @@ import _ from "lodash/fp";
2
2
  import React from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { connect } from "react-redux";
5
- import { Button, Checkbox } from "semantic-ui-react";
6
- import { useIntl } from "react-intl";
7
- import { downloadImplementations, uploadImplementations } from "../routines";
5
+ import { Checkbox } from "semantic-ui-react";
8
6
  import { getImplementationsExecution } from "../selectors";
9
7
  import {
10
8
  addImplementationFilter,
@@ -12,38 +10,12 @@ import {
12
10
  removeImplementationFilter,
13
11
  createExecutionGroup,
14
12
  } from "../routines";
15
- import ImplementationsUploadButton from "./ImplementationsUploadButton";
16
-
17
13
  import ExecutionPopup from "./ExecutionPopup";
18
-
19
- const staticHeaderLabels = [
20
- "implementation_key",
21
- "implementation_type",
22
- "rule",
23
- "template",
24
- "goal",
25
- "minimum",
26
- "business_concept",
27
- "last_execution_at",
28
- "result",
29
- "execution",
30
- "inserted_at",
31
- "executable",
32
- ];
33
-
34
- const staticContentLabels = [
35
- "quality_result.under_minimum",
36
- "quality_result.under_goal",
37
- "quality_result.over_goal",
38
- "quality_result.no_execution",
39
- "executable.true",
40
- "executable.false",
41
- ];
14
+ import RuleImplementationsOptions from "./RuleImplementationsOptions";
42
15
 
43
16
  export const RuleImplementationsActions = ({
44
17
  addImplementationFilter,
45
18
  canExecute,
46
- downloadImplementations,
47
19
  executeImplementationsOn,
48
20
  toggleImplementationFilterValue,
49
21
  removeImplementationFilter,
@@ -53,22 +25,8 @@ export const RuleImplementationsActions = ({
53
25
  setMode,
54
26
  implementationsExecution,
55
27
  ruleImplementationCount,
56
- ruleImplementationsDownloading,
57
28
  ruleImplementationsLoading,
58
- upload,
59
29
  }) => {
60
- const { formatMessage } = useIntl();
61
-
62
- const headerLabels = _.flow(
63
- _.map((l) => [l, formatMessage({ id: `ruleImplementations.props.${l}` })]),
64
- _.fromPairs
65
- )(staticHeaderLabels);
66
-
67
- const contentLabels = _.flow(
68
- _.map((l) => [l, formatMessage({ id: `ruleImplementations.props.${l}` })]),
69
- _.fromPairs
70
- )(staticContentLabels);
71
-
72
30
  const showExecutableInfo = () => {
73
31
  addImplementationFilter({ filter: "executable" });
74
32
  toggleImplementationFilterValue({
@@ -115,29 +73,13 @@ export const RuleImplementationsActions = ({
115
73
  />
116
74
  </>
117
75
  )}
118
- {!ruleImplementationsLoading && (
119
- <Button
120
- floated="right"
121
- secondary
122
- icon="download"
123
- data-tooltip={formatMessage({
124
- id: "implementations.actions.download.tooltip",
125
- })}
126
- onClick={() =>
127
- downloadImplementations({ contentLabels, headerLabels })
128
- }
129
- loading={ruleImplementationsDownloading}
130
- />
131
- )}
132
- {upload && <ImplementationsUploadButton />}
133
- <ImplementationsUploadButton />
76
+ <RuleImplementationsOptions loading={ruleImplementationsLoading} />
134
77
  </div>
135
78
  );
136
79
  };
137
80
 
138
81
  RuleImplementationsActions.propTypes = {
139
82
  addImplementationFilter: PropTypes.func,
140
- downloadImplementations: PropTypes.func,
141
83
  canExecute: PropTypes.bool,
142
84
  createExecutionGroup: PropTypes.func,
143
85
  executeImplementationsOn: PropTypes.bool,
@@ -148,23 +90,19 @@ RuleImplementationsActions.propTypes = {
148
90
  selectedImplementations: PropTypes.array,
149
91
  setMode: PropTypes.func,
150
92
  toggleImplementationFilterValue: PropTypes.func,
151
- ruleImplementationsDownloading: PropTypes.bool,
152
93
  ruleImplementationsLoading: PropTypes.bool,
153
- upload: PropTypes.bool,
154
94
  };
155
95
 
156
96
  const mapStateToProps = (state) => ({
157
97
  canExecute: _.propOr(false, "userImplementationsPermissions.execute")(state),
158
98
  ruleImplementationCount: state.ruleImplementationCount,
159
99
  implementationsExecution: getImplementationsExecution(state),
160
- ruleImplementationsDownloading: state.ruleImplementationsDownloading,
161
100
  ruleImplementationsLoading: state.ruleImplementationsLoading,
162
101
  });
163
102
 
164
103
  export default connect(mapStateToProps, {
165
104
  addImplementationFilter,
166
- downloadImplementations,
167
105
  toggleImplementationFilterValue,
168
106
  removeImplementationFilter,
169
107
  createExecutionGroup,
170
- })(RuleImplementationsActions, uploadImplementations);
108
+ })(RuleImplementationsActions);
@@ -0,0 +1,86 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import PropTypes from "prop-types";
4
+ import { connect } from "react-redux";
5
+ import { Dropdown } from "semantic-ui-react";
6
+ import { useIntl } from "react-intl";
7
+ import { downloadImplementations } from "../routines";
8
+
9
+ const staticHeaderLabels = [
10
+ "implementation_key",
11
+ "implementation_type",
12
+ "rule",
13
+ "template",
14
+ "goal",
15
+ "minimum",
16
+ "business_concept",
17
+ "last_execution_at",
18
+ "result",
19
+ "execution",
20
+ "inserted_at",
21
+ "executable",
22
+ ];
23
+
24
+ const staticContentLabels = [
25
+ "quality_result.under_minimum",
26
+ "quality_result.under_goal",
27
+ "quality_result.over_goal",
28
+ "quality_result.no_execution",
29
+ "executable.true",
30
+ "executable.false",
31
+ ];
32
+
33
+ export const RuleImplementationsDownload = ({
34
+ downloadImplementations,
35
+ ruleImplementationsDownloading,
36
+ ruleImplementations,
37
+ }) => {
38
+ const { formatMessage } = useIntl();
39
+
40
+ const headerLabels = _.flow(
41
+ _.map((l) => [l, formatMessage({ id: `ruleImplementations.props.${l}` })]),
42
+ _.fromPairs
43
+ )(staticHeaderLabels);
44
+
45
+ const contentLabels = _.flow(
46
+ _.map((l) => [l, formatMessage({ id: `ruleImplementations.props.${l}` })]),
47
+ _.fromPairs
48
+ )(staticContentLabels);
49
+
50
+ const isDisabled = _.isEmpty(ruleImplementations);
51
+
52
+ return (
53
+ <Dropdown.Item
54
+ icon="download"
55
+ content={
56
+ <>
57
+ <span>
58
+ {formatMessage({ id: "implementations.actions.download.tooltip" })}
59
+ </span>
60
+ {isDisabled && (
61
+ <p className="menu-item-description">
62
+ {formatMessage({ id: "implementations.actions.download.empty" })}
63
+ </p>
64
+ )}
65
+ </>
66
+ }
67
+ onClick={() => downloadImplementations({ contentLabels, headerLabels })}
68
+ disabled={isDisabled || ruleImplementationsDownloading}
69
+ />
70
+ );
71
+ };
72
+
73
+ RuleImplementationsDownload.propTypes = {
74
+ downloadImplementations: PropTypes.func,
75
+ ruleImplementationsDownloading: PropTypes.bool,
76
+ ruleImplementations: PropTypes.array,
77
+ };
78
+
79
+ const mapStateToProps = _.pick([
80
+ "ruleImplementationsDownloading",
81
+ "ruleImplementations",
82
+ ]);
83
+
84
+ export default connect(mapStateToProps, {
85
+ downloadImplementations,
86
+ })(RuleImplementationsDownload);
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import { Dropdown } from "semantic-ui-react";
4
+ import RuleImplementationsDownload from "./RuleImplementationsDownload";
5
+ import RuleResultsUpload from "./RuleResultsUpload";
6
+ import ImplementationsUploadButton from "./ImplementationsUploadButton";
7
+
8
+ export const RuleImplementationsOptions = ({ loading }) => (
9
+ <Dropdown
10
+ icon="ellipsis vertical"
11
+ className="button icon group-actions button-update"
12
+ direction="left"
13
+ floating
14
+ disabled={loading}
15
+ >
16
+ <Dropdown.Menu>
17
+ <RuleImplementationsDownload />
18
+ <ImplementationsUploadButton />
19
+ <RuleResultsUpload />
20
+ </Dropdown.Menu>
21
+ </Dropdown>
22
+ );
23
+
24
+ RuleImplementationsOptions.propTypes = {
25
+ loading: PropTypes.bool,
26
+ };
27
+
28
+ export default RuleImplementationsOptions;
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import { connect } from "react-redux";
4
+ import { Dropdown } from "semantic-ui-react";
5
+ import { useIntl } from "react-intl";
6
+ import { UploadModal } from "@truedat/core/components";
7
+ import { uploadResults } from "../routines";
8
+
9
+ export const RuleResultsUpload = ({ uploadResults, loading }) => {
10
+ const { formatMessage } = useIntl();
11
+
12
+ return (
13
+ <UploadModal
14
+ icon="upload"
15
+ trigger={
16
+ <Dropdown.Item
17
+ icon="upload"
18
+ text={formatMessage({ id: "ruleResults.actions.upload.tooltip" })}
19
+ disabled={loading}
20
+ />
21
+ }
22
+ header={formatMessage({
23
+ id: "ruleResults.actions.upload.confirmation.header",
24
+ })}
25
+ content={formatMessage({
26
+ id: "uploadModal.actions.upload.confirmation.content",
27
+ })}
28
+ param={"rule_results"}
29
+ handleSubmit={(data) =>
30
+ uploadResults({
31
+ data,
32
+ })
33
+ }
34
+ />
35
+ );
36
+ };
37
+
38
+ RuleResultsUpload.propTypes = {
39
+ uploadResults: PropTypes.func,
40
+ loading: PropTypes.bool,
41
+ };
42
+
43
+ const mapStateToProps = ({ uploadingResults }) => ({
44
+ loading: uploadingResults,
45
+ });
46
+
47
+ export default connect(mapStateToProps, { uploadResults })(RuleResultsUpload);
@@ -8,7 +8,6 @@ jest.spyOn(React, "useContext").mockImplementation(() => intl);
8
8
 
9
9
  describe("<RuleImplementationsActions />", () => {
10
10
  const addImplementationFilter = jest.fn();
11
- const downloadImplementations = jest.fn();
12
11
  const toggleImplementationFilterValue = jest.fn();
13
12
  const setMode = jest.fn();
14
13
  const removeImplementationFilter = jest.fn();
@@ -17,9 +16,7 @@ describe("<RuleImplementationsActions />", () => {
17
16
  executionGroupLoading: false,
18
17
  ruleImplementationCount: 12,
19
18
  implementationsExecution: true,
20
- ruleImplementationsDownloading: false,
21
19
  ruleImplementationsLoading: false,
22
- downloadImplementations,
23
20
  toggleImplementationFilterValue,
24
21
  addImplementationFilter,
25
22
  executeImplementationsOn: false,
@@ -31,12 +28,6 @@ describe("<RuleImplementationsActions />", () => {
31
28
  expect(wrapper).toMatchSnapshot();
32
29
  });
33
30
 
34
- it("calls downloadImplementations on button click", () => {
35
- const wrapper = shallow(<RuleImplementationsActions {...props} />);
36
- wrapper.find("Button").simulate("click");
37
- expect(downloadImplementations.mock.calls.length).toBe(1);
38
- });
39
-
40
31
  it("as user with permissions I see execute implementations button and checkbox", () => {
41
32
  const props = {
42
33
  canExecute: true,
@@ -0,0 +1,18 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { shallow } from "enzyme";
4
+ import { intl } from "@truedat/test/intl-stub";
5
+ import { RuleImplementationsOptions } from "../RuleImplementationsOptions";
6
+
7
+ jest.spyOn(React, "useContext").mockImplementation(() => intl);
8
+
9
+ describe("<RuleImplementationsOptions />", () => {
10
+ it("matches the latest snapshot", () => {
11
+ const wrapper = shallow(<RuleImplementationsOptions />);
12
+ expect(wrapper).toMatchSnapshot();
13
+ });
14
+ it("matches the latest loading snapshot", () => {
15
+ const wrapper = shallow(<RuleImplementationsOptions loading />);
16
+ expect(wrapper).toMatchSnapshot();
17
+ });
18
+ });
@@ -0,0 +1,18 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { shallow } from "enzyme";
4
+ import { intl } from "@truedat/test/intl-stub";
5
+ import { RuleResultsUpload } from "../RuleResultsUpload";
6
+
7
+ jest.spyOn(React, "useContext").mockImplementation(() => intl);
8
+
9
+ describe("<RuleResultsUpload />", () => {
10
+ it("matches the latest snapshot", () => {
11
+ const props = {
12
+ uploadResults: jest.fn(),
13
+ loading: false,
14
+ };
15
+ const wrapper = shallow(<RuleResultsUpload {...props} />);
16
+ expect(wrapper).toMatchSnapshot();
17
+ });
18
+ });
@@ -27,15 +27,8 @@ exports[`<RuleImplementationsActions /> matches the latest snapshot 1`] = `
27
27
  disabled={true}
28
28
  handleSubmit={[Function]}
29
29
  />
30
- <Button
31
- as="button"
32
- data-tooltip="implementations.actions.download.tooltip"
33
- floated="right"
34
- icon="download"
30
+ <RuleImplementationsOptions
35
31
  loading={false}
36
- onClick={[Function]}
37
- secondary={true}
38
32
  />
39
- <Connect(ImplementationsUploadButton) />
40
33
  </div>
41
34
  `;
@@ -0,0 +1,58 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<RuleImplementationsOptions /> matches the latest loading snapshot 1`] = `
4
+ <Dropdown
5
+ additionLabel="Add "
6
+ additionPosition="top"
7
+ className="button icon group-actions button-update"
8
+ closeOnBlur={true}
9
+ closeOnEscape={true}
10
+ deburr={false}
11
+ direction="left"
12
+ disabled={true}
13
+ floating={true}
14
+ icon="ellipsis vertical"
15
+ minCharacters={1}
16
+ noResultsMessage="No results found."
17
+ openOnFocus={true}
18
+ renderLabel={[Function]}
19
+ searchInput="text"
20
+ selectOnBlur={true}
21
+ selectOnNavigation={true}
22
+ wrapSelection={true}
23
+ >
24
+ <DropdownMenu>
25
+ <Connect(RuleImplementationsDownload) />
26
+ <Connect(ImplementationsUploadButton) />
27
+ <Connect(RuleResultsUpload) />
28
+ </DropdownMenu>
29
+ </Dropdown>
30
+ `;
31
+
32
+ exports[`<RuleImplementationsOptions /> matches the latest snapshot 1`] = `
33
+ <Dropdown
34
+ additionLabel="Add "
35
+ additionPosition="top"
36
+ className="button icon group-actions button-update"
37
+ closeOnBlur={true}
38
+ closeOnEscape={true}
39
+ deburr={false}
40
+ direction="left"
41
+ floating={true}
42
+ icon="ellipsis vertical"
43
+ minCharacters={1}
44
+ noResultsMessage="No results found."
45
+ openOnFocus={true}
46
+ renderLabel={[Function]}
47
+ searchInput="text"
48
+ selectOnBlur={true}
49
+ selectOnNavigation={true}
50
+ wrapSelection={true}
51
+ >
52
+ <DropdownMenu>
53
+ <Connect(RuleImplementationsDownload) />
54
+ <Connect(ImplementationsUploadButton) />
55
+ <Connect(RuleResultsUpload) />
56
+ </DropdownMenu>
57
+ </Dropdown>
58
+ `;
@@ -0,0 +1,18 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<RuleResultsUpload /> matches the latest snapshot 1`] = `
4
+ <UploadModal
5
+ content="uploadModal.actions.upload.confirmation.content"
6
+ handleSubmit={[Function]}
7
+ header="ruleResults.actions.upload.confirmation.header"
8
+ icon="upload"
9
+ param="rule_results"
10
+ trigger={
11
+ <DropdownItem
12
+ disabled={false}
13
+ icon="upload"
14
+ text="ruleResults.actions.upload.tooltip"
15
+ />
16
+ }
17
+ />
18
+ `;
@@ -110,6 +110,7 @@ const Filter = ({
110
110
  composeValue={composeValue}
111
111
  population={clause?.population || [{}]}
112
112
  onChange={onValueConditionChange}
113
+ onModifierChange={onModifierChange}
113
114
  parentStructures={parentStructures}
114
115
  siblings={findSiblings(clause)}
115
116
  scope="population"
@@ -64,6 +64,16 @@ export const ValueConditions = ({
64
64
 
65
65
  const onRowAddition = () => onChange(_.concat(population)([{}]));
66
66
 
67
+ const onModifierChange = (index, modifier) => {
68
+ const clause = _.nth(index)(population);
69
+ const updatedCondition = {
70
+ ...clause,
71
+ modifier,
72
+ };
73
+ const modifiedPopulation = replaceAt(index, updatedCondition)(population);
74
+ onChange && onChange(modifiedPopulation);
75
+ };
76
+
67
77
  return population.map((clause, index) => (
68
78
  <React.Fragment key={index}>
69
79
  <Grid.Row>
@@ -74,6 +84,7 @@ export const ValueConditions = ({
74
84
  onOperatorChange={onOperatorChange}
75
85
  onStructureChange={onStructureChange}
76
86
  onValueChange={onValueChange}
87
+ onModifierChange={onModifierChange}
77
88
  parentStructures={parentStructures}
78
89
  scope={scope}
79
90
  siblings={siblings}
@@ -19,13 +19,14 @@ export default {
19
19
  "createRule.error.domain_id.required": "Domain is required",
20
20
  "createRuleImplementation.error.implementation_key.duplicated":
21
21
  "Duplicated implementation key",
22
- "createRule.error.rule_name_bc_id.unique_constraint": "Duplicated rule name. Edit name or associated business concept.",
22
+ "createRule.error.rule_name_bc_id.unique_constraint":
23
+ "Duplicated rule name. Edit name or associated business concept.",
23
24
  "dataset.form.button.add_join.popup":
24
25
  "Select both fields to add a new clause",
25
26
  "dataset.form.button.add_structure": "Add Structure",
26
27
  "datasetForm.implementation_key.tooltip":
27
28
  "Leave empty for autogeneration of implementation key after saving implementation",
28
- "executionGroup": "Executions",
29
+ executionGroup: "Executions",
29
30
  "executions.completed.content": "{count} executions completed.",
30
31
  "executions.completed.header": "Executions completed",
31
32
  "executions.pending.content":
@@ -53,9 +54,13 @@ export default {
53
54
  "filtersGrid.field.operator": "Operator",
54
55
  "filtersGrid.field.value": "Value",
55
56
  "implementations.actions.do_execution": "Execute implementations",
56
- "implementations.actions.download.tooltip": "Download to csv",
57
- "implementations.actions.execution.confirmation.content": "{implementations_count} implementations will be executed. Are you sure?",
58
- "implementations.actions.execution.confirmation.header": "Implementations execution",
57
+ "implementations.actions.download.tooltip": "Download with csv format",
58
+ "implementations.actions.download.empty":
59
+ "There are no Implementations to be downloaded",
60
+ "implementations.actions.execution.confirmation.content":
61
+ "{implementations_count} implementations will be executed. Are you sure?",
62
+ "implementations.actions.execution.confirmation.header":
63
+ "Implementations execution",
59
64
  "implementations.actions.execution.confirmation.legend": "Template fields",
60
65
  "implementations.execute.filtered": "{count} implementations to execute",
61
66
  "implementations.search.placeholder": "Search implementations...",
@@ -118,7 +123,7 @@ export default {
118
123
  "quality_result.under_minimum": "Under threshold",
119
124
  "quality_result.under_minumum": "Under threshold",
120
125
  "quality_result.failed": "Failed",
121
- "rule": "Quality Rule",
126
+ rule: "Quality Rule",
122
127
  "rule.date.placeholder": "YYYY-MM-DD",
123
128
  "rule.error.goal.required": "Goal required",
124
129
  "rule.error.minimum.required": "Minimun threshold required",
@@ -456,7 +461,8 @@ export default {
456
461
  "ruleImplementations.actions.edit": "Edit Implementation",
457
462
  "ruleImplementations.actions.popup.deprecated": "Deprecated Implementations",
458
463
  "ruleImplementations.actions.upload.tooltip": "Upload Implementations",
459
- "ruleImplementations.actions.upload.confirmation.header": "Confirm bulk upload",
464
+ "ruleImplementations.actions.upload.confirmation.header":
465
+ "Confirm bulk upload",
460
466
  "ruleImplementations.header": "Quality Implementations",
461
467
  "ruleImplementations.props.business_concept": "Concept",
462
468
  "ruleImplementations.props.status": "Status",
@@ -492,11 +498,20 @@ export default {
492
498
  "ruleImplementations.summary.headers.implementation": "Rule Implementation",
493
499
  "ruleImplementations.summary.headers.population": "Population",
494
500
  "ruleImplementations.summary.headers.validations": "Validation",
495
- "ruleImplementations.upload.success.errors": "Error in {implementation_key} attribute: {key} message: {message} ",
496
- "ruleImplementations.upload.success.header": "Upload success! Loaded {count_ids} omitted {count_errors}:",
497
- "ruleImplementations.upload.failed.header": "Error uploading file. No rules have been created.",
501
+ "ruleImplementations.upload.success.errors":
502
+ "Error in {implementation_key} attribute: {key} message: {message} ",
503
+ "ruleImplementations.upload.success.header":
504
+ "Upload success! Loaded {count_ids} omitted {count_errors}:",
505
+ "ruleImplementations.upload.failed.header":
506
+ "Error uploading file. No rules have been created.",
498
507
  "ruleImplementations.upload.failed.misssing_required_columns":
499
508
  "Faltan columnas obligatorias en el fichero. Esperado [{expected}]. Recibido [{found}].",
509
+ "ruleResults.actions.upload.tooltip": "Upload rule results",
510
+ "ruleResults.actions.upload.confirmation.header": "Upload rule results",
511
+ "ruleResults.upload.success.header": "Upload successful!",
512
+ "ruleResults.upload.failed.header":
513
+ "Error uploading file. No results have been created.",
514
+ "ruleResults.upload.failed.content": "Error in line {row}.",
500
515
  "ruleResult.props.header.information": "Information",
501
516
  "ruleResult.props.header.details": "Details",
502
517
  "ruleResult.props.date": "Date",
@@ -519,9 +534,12 @@ export default {
519
534
  "rules.retrieved.results": "{count} rules found",
520
535
  "rules.search.placeholder": "Search rules...",
521
536
  "rules.searching": "Searching...",
522
- "rules.upload.success.errors": "Error in {rule_name} attribute: {key} message: {message} ",
523
- "rules.upload.success.header": "Upload success! Loaded {count_ids} omitted {count_errors}:",
524
- "rules.upload.failed.header": "Error uploading file. No rules have been created.",
537
+ "rules.upload.success.errors":
538
+ "Error in {rule_name} attribute: {key} message: {message} ",
539
+ "rules.upload.success.header":
540
+ "Upload successful! Loaded {count_ids} omitted {count_errors}:",
541
+ "rules.upload.failed.header":
542
+ "Error uploading file. No rules have been created.",
525
543
  "rules.upload.failed.misssing_required_columns":
526
544
  "Missing required columns. Expected [{expected}]. Found [{found}].",
527
545
  "structureFields.dropdown.label": "Select Field",
@@ -19,13 +19,14 @@ export default {
19
19
  "createRule.error.domain_id.required": "Dominio es requerido",
20
20
  "createRuleImplementation.error.implementation_key.duplicated":
21
21
  "Identificador duplicado",
22
- "createRule.error.rule_name_bc_id.unique_constraint": "La regla ya existe con ese nombre o concepto asociado",
22
+ "createRule.error.rule_name_bc_id.unique_constraint":
23
+ "La regla ya existe con ese nombre o concepto asociado",
23
24
  "dataset.form.button.add_join.popup":
24
25
  "Seleccionar ambos campos para añadir una nueva cláusula",
25
26
  "dataset.form.button.add_structure": "Añadir Estructura",
26
27
  "datasetForm.implementation_key.tooltip":
27
28
  "En caso de no introducir clave de implementación, se autogenerará al guardar la implementación",
28
- "executionGroup": "Ejecuciones",
29
+ executionGroup: "Ejecuciones",
29
30
  "executions.completed.content": "Se han ejecutado {count} implementaciones.",
30
31
  "executions.completed.header": "Ejecuciones finalizadas",
31
32
  "executions.pending.content":
@@ -53,10 +54,15 @@ export default {
53
54
  "filtersGrid.field.operator": "Operador",
54
55
  "filtersGrid.field.value": "Valor",
55
56
  "implementations.actions.do_execution": "Ejecutar implementaciones",
56
- "implementations.actions.download.tooltip": "Descargar en csv",
57
- "implementations.actions.execution.confirmation.content": "Se va a solicitar la ejecución de {implementations_count} implementaciones. ¿Estás seguro?",
58
- "implementations.actions.execution.confirmation.header": "Solicitar ejecución de implementaciones",
59
- "implementations.actions.execution.confirmation.legend": "Campos de la plantilla",
57
+ "implementations.actions.download.tooltip": "Descargar en formato csv",
58
+ "implementations.actions.download.empty":
59
+ "No hay implementaciones para descargar",
60
+ "implementations.actions.execution.confirmation.content":
61
+ "Se va a solicitar la ejecución de {implementations_count} implementaciones. ¿Estás seguro?",
62
+ "implementations.actions.execution.confirmation.header":
63
+ "Solicitar ejecución de implementaciones",
64
+ "implementations.actions.execution.confirmation.legend":
65
+ "Campos de la plantilla",
60
66
  "implementations.execute.filtered": "{count} implementaciones a ejecutar",
61
67
  "implementations.search.placeholder": "Buscar implementaciones...",
62
68
  "navigation.quality.rules": "Reglas",
@@ -214,7 +220,7 @@ export default {
214
220
  "rule.type.numeric_format": "Tiene formato numérico",
215
221
  "rule.type.unique_values": "Tiene valores únicos",
216
222
  "rule.values_list": "Valores de la lista",
217
- "rule": "Regla de Calidad",
223
+ rule: "Regla de Calidad",
218
224
  "ruleImplementation.action.deprecate": "archivada",
219
225
  "ruleImplementation.actions.clone": "Clonar implementación",
220
226
  "ruleImplementation.actions.delete.confirmation.content":
@@ -235,7 +241,8 @@ export default {
235
241
  "Restaurar Implementación",
236
242
  "ruleImplementation.actions.restore": "Restaurar Implementación",
237
243
  "ruleImplementations.actions.upload.tooltip": "Subir Implementaciones",
238
- "ruleImplementations.actions.upload.confirmation.header": "Subir ficheros de implementaciones",
244
+ "ruleImplementations.actions.upload.confirmation.header":
245
+ "Subir ficheros de implementaciones",
239
246
  "ruleImplementation.column.placeholder": "Seleccionar columna",
240
247
  "ruleImplementation.column": "Columna",
241
248
  "ruleImplementation.delete.folder": '"Eliminado"',
@@ -509,13 +516,21 @@ export default {
509
516
  "Implementación de una Regla",
510
517
  "ruleImplementations.summary.headers.population": "Población",
511
518
  "ruleImplementations.summary.headers.validations": "Validaciones",
512
- "ruleImplementations.upload.success.errors": "Error en {name} atributo: {key} mensaje: {message} ",
513
- "ruleImplementations.upload.success.header": "¡Fichero subido correctamente! subidos {count_ids} errores {count_errors}:",
514
- "ruleImplementations.upload.failed.header": "Error al subir el fichero. No se ha realizado ninguna inserción.",
519
+ "ruleImplementations.upload.success.errors":
520
+ "Error en {name} atributo: {key} mensaje: {message} ",
521
+ "ruleImplementations.upload.success.header":
522
+ "¡Fichero subido correctamente! subidos {count_ids} errores {count_errors}:",
523
+ "ruleImplementations.upload.failed.header":
524
+ "Error al subir el fichero. No se ha realizado ninguna inserción.",
515
525
  "ruleImplementations.upload.failed.misssing_required_columns":
516
526
  "Faltan columnas obligatorias en el fichero. Esperado [{expected}]. Recibido [{found}].",
517
-
518
-
527
+ "ruleResults.actions.upload.tooltip": "Subir resultados",
528
+ "ruleResults.actions.upload.confirmation.header":
529
+ "Subir resultados de implementaciones",
530
+ "ruleResults.upload.success.header": "¡Fichero subido correctamente!",
531
+ "ruleResults.upload.failed.header":
532
+ "Error al subir fichero. No se ha realizado ninguna inserción.",
533
+ "ruleResults.upload.failed.content": "Error en la línea {row}.",
519
534
  "ruleResult.props.date": "Fecha",
520
535
  "ruleResult.props.details": "Detalles",
521
536
  "ruleResult.props.errors": "Errores",
@@ -534,9 +549,12 @@ export default {
534
549
  "rules.retrieved.results": "{count} reglas encontradas",
535
550
  "rules.search.placeholder": "Buscar reglas...",
536
551
  "rules.searching": "Buscando...",
537
- "rules.upload.success.errors": "Error en {rule_name} atributo: {key} mensaje: {message} ",
538
- "rules.upload.success.header": "¡Fichero subido correctamente! subidos {count_ids} errores {count_errors}:",
539
- "rules.upload.failed.header": "Error al subir el fichero. No se ha realizado ninguna inserción.",
552
+ "rules.upload.success.errors":
553
+ "Error en {rule_name} atributo: {key} mensaje: {message} ",
554
+ "rules.upload.success.header":
555
+ "¡Fichero subido correctamente! subidos {count_ids} errores {count_errors}:",
556
+ "rules.upload.failed.header":
557
+ "Error al subir el fichero. No se ha realizado ninguna inserción.",
540
558
  "rules.upload.failed.misssing_required_columns":
541
559
  "Faltan columnas obligatorias en el fichero. Esperado [{expected}]. Recibido [{found}].",
542
560
  "ruleSubscription.actions.remove": "Eliminar",
@@ -0,0 +1,28 @@
1
+ import { uploadResults } from "../../routines";
2
+ import { uploadingResults } from "..";
3
+
4
+ const fooState = { foo: "bar" };
5
+
6
+ const initialState = false;
7
+
8
+ describe("reducers: previousRuleImplementationQuery", () => {
9
+ it("should provide the initial state", () => {
10
+ expect(uploadingResults(undefined, {})).toEqual(initialState);
11
+ });
12
+
13
+ it("should handle the uploadResults.TRIGGER action", () => {
14
+ expect(
15
+ uploadingResults(fooState, {
16
+ type: uploadResults.TRIGGER,
17
+ })
18
+ ).toBeTruthy();
19
+ });
20
+
21
+ it("should handle the uploadResults.FULFILL action", () => {
22
+ expect(
23
+ uploadingResults(fooState, {
24
+ type: uploadResults.FULFILL,
25
+ })
26
+ ).toBeFalsy();
27
+ });
28
+ });
@@ -1,6 +1,6 @@
1
1
  import _ from "lodash/fp";
2
2
  import { dismissAlert } from "@truedat/core/routines";
3
- import { uploadRules, uploadImplementations } from "../routines";
3
+ import { uploadRules, uploadImplementations, uploadResults } from "../routines";
4
4
 
5
5
  const initialState = {};
6
6
 
@@ -95,6 +95,27 @@ const dqMessage = (state = initialState, { type, payload }) => {
95
95
  } else {
96
96
  return null;
97
97
  }
98
+ case uploadResults.SUCCESS:
99
+ return {
100
+ error: false,
101
+ header: "ruleResults.upload.success.header",
102
+ icon: "check",
103
+ color: "green",
104
+ text: "",
105
+ };
106
+ case uploadResults.FAILURE:
107
+ if (payload.status != 500) {
108
+ return {
109
+ error: true,
110
+ header: "ruleResults.upload.failed.header",
111
+ content: `ruleResults.upload.failed.content`,
112
+ icon: "attention",
113
+ text: "",
114
+ fields: { row: _.path("data.row")(payload) },
115
+ };
116
+ } else {
117
+ return null;
118
+ }
98
119
 
99
120
  default:
100
121
  return state;
@@ -42,6 +42,7 @@ import { uploadRulesFile } from "./uploadRulesFile";
42
42
  import { userRulePermissions } from "./userRulePermissions";
43
43
  import { userRulesPermissions } from "./userRulesPermissions";
44
44
  import { userImplementationsPermissions } from "./userImplementationsPermissions";
45
+ import { uploadingResults } from "./uploadingResults";
45
46
 
46
47
  export {
47
48
  conceptRules,
@@ -88,4 +89,5 @@ export {
88
89
  userRulePermissions,
89
90
  userRulesPermissions,
90
91
  userImplementationsPermissions,
92
+ uploadingResults,
91
93
  };
@@ -0,0 +1,16 @@
1
+ import { uploadResults } from "../routines";
2
+
3
+ const initialState = false;
4
+
5
+ export const uploadingResults = (state = initialState, { type }) => {
6
+ switch (type) {
7
+ case uploadResults.TRIGGER:
8
+ return true;
9
+ case uploadResults.FULFILL:
10
+ return false;
11
+ default:
12
+ return state;
13
+ }
14
+ };
15
+
16
+ export default uploadingResults;
package/src/routines.js CHANGED
@@ -89,6 +89,7 @@ export const openImplementationFilter = createRoutine(
89
89
  export const downloadImplementations = createRoutine(
90
90
  "DOWNLOAD_IMPLEMENTATIONS"
91
91
  );
92
+ export const uploadResults = createRoutine("UPLOAD_RESULTS");
92
93
  export const createExecutionGroup = createRoutine("CREATE_EXECUTION_GROUP");
93
94
  export const fetchExecutionGroup = createRoutine("FETCH_EXECUTION_GROUP");
94
95
  export const clearExecutionGroup = createRoutine("CLEAR_EXECUTION_GROUP");
@@ -0,0 +1,65 @@
1
+ import { testSaga } from "redux-saga-test-plan";
2
+ import { apiJsonPost, UPLOAD_JSON_OPTS } from "@truedat/core/services/api";
3
+ import { uploadResultsRequestSaga, uploadResultsSaga } from "../uploadResults";
4
+ import { uploadResults } from "../../routines";
5
+ import { API_RULE_RESULTS } from "../../api";
6
+
7
+ describe("sagas: uploadResultsRequestSaga", () => {
8
+ it("should invoke uploadResultsSaga on trigger", () => {
9
+ expect(() => {
10
+ testSaga(uploadResultsRequestSaga)
11
+ .next()
12
+ .takeLatest(uploadResults.TRIGGER, uploadResultsSaga)
13
+ .finish()
14
+ .isDone();
15
+ }).not.toThrow();
16
+ });
17
+
18
+ it("should throw exception if an unhandled action is received", () => {
19
+ expect(() => {
20
+ testSaga(uploadResultsRequestSaga)
21
+ .next()
22
+ .takeLatest("FOO", uploadResultsSaga);
23
+ }).toThrow();
24
+ });
25
+ });
26
+
27
+ describe("sagas: uploadResultsSaga", () => {
28
+ const body = { rule_results: "content" };
29
+ const payload = { data: body };
30
+ const data = { message: [1, 2] };
31
+ it("should put a success action when a response is returned", () => {
32
+ expect(() => {
33
+ testSaga(uploadResultsSaga, { payload })
34
+ .next()
35
+ .put(uploadResults.request())
36
+ .next()
37
+ .call(apiJsonPost, API_RULE_RESULTS, body, UPLOAD_JSON_OPTS)
38
+ .next({ data })
39
+ .put(uploadResults.success(data))
40
+ .next()
41
+ .put(uploadResults.fulfill())
42
+ .next()
43
+ .isDone();
44
+ }).not.toThrow();
45
+ });
46
+
47
+ it("should put a failure action when the call returns an error", () => {
48
+ const message = "Request failed";
49
+ const error = { message };
50
+
51
+ expect(() => {
52
+ testSaga(uploadResultsSaga, { payload })
53
+ .next()
54
+ .put(uploadResults.request())
55
+ .next()
56
+ .call(apiJsonPost, API_RULE_RESULTS, body, UPLOAD_JSON_OPTS)
57
+ .throw(error)
58
+ .put(uploadResults.failure(message))
59
+ .next()
60
+ .put(uploadResults.fulfill())
61
+ .next()
62
+ .isDone();
63
+ }).not.toThrow();
64
+ });
65
+ });
@@ -18,6 +18,7 @@ import { uploadImplementationsRequestsSaga } from "./uploadImplementations";
18
18
  import { uploadRulesRequestsSaga } from "./uploadRules";
19
19
  import { updateRuleImplementationRequestSaga } from "./updateRuleImplementation";
20
20
  import { updateRuleRequestSaga } from "./updateRule";
21
+ import { uploadResultsRequestSaga } from "./uploadResults";
21
22
 
22
23
  export {
23
24
  createExecutionGroupRequestSaga,
@@ -40,6 +41,7 @@ export {
40
41
  uploadImplementationsRequestsSaga,
41
42
  updateRuleImplementationRequestSaga,
42
43
  updateRuleRequestSaga,
44
+ uploadResultsRequestSaga,
43
45
  };
44
46
 
45
47
  export default [
@@ -63,4 +65,5 @@ export default [
63
65
  uploadImplementationsRequestsSaga(),
64
66
  updateRuleImplementationRequestSaga(),
65
67
  updateRuleRequestSaga(),
68
+ uploadResultsRequestSaga(),
66
69
  ];
@@ -0,0 +1,32 @@
1
+ import { call, put, takeLatest } from "redux-saga/effects";
2
+ import { apiJsonPost, UPLOAD_JSON_OPTS } from "@truedat/core/services/api";
3
+ import { uploadResults } from "../routines";
4
+ import { API_RULE_RESULTS } from "../api";
5
+
6
+ export function* uploadResultsSaga({ payload }) {
7
+ try {
8
+ const { data: body } = payload;
9
+ yield put(uploadResults.request());
10
+ const { data } = yield call(
11
+ apiJsonPost,
12
+ API_RULE_RESULTS,
13
+ body,
14
+ UPLOAD_JSON_OPTS
15
+ );
16
+
17
+ yield put(uploadResults.success(data));
18
+ } catch (error) {
19
+ if (error.response) {
20
+ const { status, data } = error.response;
21
+ yield put(uploadResults.failure({ status, data }));
22
+ } else {
23
+ yield put(uploadResults.failure(error.message));
24
+ }
25
+ } finally {
26
+ yield put(uploadResults.fulfill());
27
+ }
28
+ }
29
+
30
+ export function* uploadResultsRequestSaga() {
31
+ yield takeLatest(uploadResults.TRIGGER, uploadResultsSaga);
32
+ }