@truedat/qx 5.20.3 → 6.0.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/qx",
3
- "version": "5.20.3",
3
+ "version": "6.0.0",
4
4
  "description": "Truedat Web Quality Experience package",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -35,7 +35,7 @@
35
35
  "@testing-library/react": "^12.0.0",
36
36
  "@testing-library/react-hooks": "^8.0.1",
37
37
  "@testing-library/user-event": "^13.2.1",
38
- "@truedat/test": "5.12.2",
38
+ "@truedat/test": "6.0.0",
39
39
  "babel-jest": "^28.1.0",
40
40
  "babel-plugin-dynamic-import-node": "^2.3.3",
41
41
  "babel-plugin-lodash": "^3.3.4",
@@ -84,7 +84,7 @@
84
84
  ]
85
85
  },
86
86
  "dependencies": {
87
- "@truedat/core": "5.20.3",
87
+ "@truedat/core": "6.0.0",
88
88
  "prop-types": "^15.8.1",
89
89
  "react-hook-form": "^7.45.4",
90
90
  "react-intl": "^5.20.10",
@@ -97,5 +97,5 @@
97
97
  "react-dom": ">= 16.8.6 < 17",
98
98
  "semantic-ui-react": ">= 2.0.3 < 2.2"
99
99
  },
100
- "gitHead": "11ee6981b87a3c8f2b99c7101b42cafc29dc4896"
100
+ "gitHead": "3627c716252d2920a5b70ee1f8e84c7afd3a991a"
101
101
  }
package/src/api.js CHANGED
@@ -11,6 +11,12 @@ const API_QUALITY_CONTROL_STATUS = "/api/quality_controls/:id/status";
11
11
  const API_QUALITY_CONTROL_DOMAINS = "/api/quality_controls/:id/domains";
12
12
  const API_QUALITY_CONTROL_SEARCH = "/api/quality_controls/search";
13
13
  const API_QUALITY_CONTROL_FILTERS = "/api/quality_controls/filters";
14
+ const API_QUALITY_CONTROL_EXECUTION_GROUPS_CREATE =
15
+ "/api/quality_controls/execution_groups/create";
16
+ const API_QUALITY_CONTROL_EXECUTION_GROUPS_INDEX =
17
+ "/api/quality_controls/execution_groups";
18
+ const API_QUALITY_CONTROL_EXECUTION_GROUP =
19
+ "/api/quality_controls/execution_groups/:id";
14
20
 
15
21
  export {
16
22
  API_DATA_VIEWS,
@@ -26,4 +32,7 @@ export {
26
32
  API_QUALITY_CONTROL_DOMAINS,
27
33
  API_QUALITY_CONTROL_SEARCH,
28
34
  API_QUALITY_CONTROL_FILTERS,
35
+ API_QUALITY_CONTROL_EXECUTION_GROUPS_CREATE,
36
+ API_QUALITY_CONTROL_EXECUTION_GROUPS_INDEX,
37
+ API_QUALITY_CONTROL_EXECUTION_GROUP,
29
38
  };
@@ -0,0 +1,25 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import { Breadcrumb } from "semantic-ui-react";
4
+ import { Link } from "react-router-dom";
5
+ import { FormattedMessage } from "react-intl";
6
+ import { DateTime } from "@truedat/core/components";
7
+ import { QUALITY_CONTROLS_EXECUTION_GROUPS } from "@truedat/core/routes";
8
+
9
+ export const ExecutionGroupBreadcrumbs = ({ timestamp }) => (
10
+ <Breadcrumb>
11
+ <Breadcrumb.Section as={Link} to={QUALITY_CONTROLS_EXECUTION_GROUPS}>
12
+ <FormattedMessage id="sidemenu.executions" />
13
+ </Breadcrumb.Section>
14
+ <Breadcrumb.Divider icon="right angle" />
15
+ <Breadcrumb.Section active>
16
+ <DateTime value={timestamp} />
17
+ </Breadcrumb.Section>
18
+ </Breadcrumb>
19
+ );
20
+
21
+ ExecutionGroupBreadcrumbs.propTypes = {
22
+ timestamp: PropTypes.string,
23
+ };
24
+
25
+ export default ExecutionGroupBreadcrumbs;
@@ -0,0 +1,42 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import PropTypes from "prop-types";
4
+ import { useQuery } from "@apollo/client";
5
+ import { TEMPLATES_QUERY } from "@truedat/core/api/queries";
6
+
7
+ const DynamicFormViewer = React.lazy(() =>
8
+ import("@truedat/df/components/DynamicFormViewer")
9
+ );
10
+
11
+ const templateFieldNames = _.flow(
12
+ _.prop("content"),
13
+ _.flatMap("fields"),
14
+ _.map("name")
15
+ );
16
+
17
+ export const ExecutionGroupContent = ({ content }) => {
18
+ const { data } = useQuery(TEMPLATES_QUERY, {
19
+ variables: { scope: "qe" },
20
+ });
21
+ // NOTE: We don't have the template name, so select one with fields matching content
22
+ const contentFieldNames = _.keys(content);
23
+ const matchingFieldCount = _.flow(
24
+ templateFieldNames,
25
+ _.intersection(contentFieldNames),
26
+ _.size
27
+ );
28
+ const template = _.flow(
29
+ _.propOr([], "templates"),
30
+ _.maxBy(matchingFieldCount)
31
+ )(data);
32
+
33
+ return template ? (
34
+ <DynamicFormViewer template={template} content={content} />
35
+ ) : null;
36
+ };
37
+
38
+ ExecutionGroupContent.propTypes = {
39
+ content: PropTypes.object,
40
+ };
41
+
42
+ export default ExecutionGroupContent;
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import { Link } from "react-router-dom";
4
+ import { DateTime } from "@truedat/core/components";
5
+ import { linkTo } from "@truedat/core/routes";
6
+
7
+ export const ExecutionGroupLink = ({ id, created }) => (
8
+ <Link to={linkTo.QUALITY_CONTROLS_EXECUTION_GROUP({ id })}>
9
+ <DateTime value={created} />
10
+ </Link>
11
+ );
12
+
13
+ ExecutionGroupLink.propTypes = {
14
+ id: PropTypes.string,
15
+ created: PropTypes.string,
16
+ };
17
+
18
+ export default ExecutionGroupLink;
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import { useIntl } from "react-intl";
4
+ import { Message } from "semantic-ui-react";
5
+
6
+ export const ExecutionGroupMessage = ({ count, pending }) => {
7
+ const { formatMessage } = useIntl();
8
+ return pending ? (
9
+ <Message
10
+ header={formatMessage({ id: "executions.pending.header" })}
11
+ content={formatMessage({ id: "executions.pending.content" }, { pending })}
12
+ />
13
+ ) : (
14
+ <Message
15
+ success
16
+ header={formatMessage({ id: "executions.completed.header" })}
17
+ content={formatMessage({ id: "executions.completed.content" }, { count })}
18
+ />
19
+ );
20
+ };
21
+
22
+ ExecutionGroupMessage.propTypes = {
23
+ count: PropTypes.number,
24
+ pending: PropTypes.number,
25
+ };
26
+
27
+ export default ExecutionGroupMessage;
@@ -0,0 +1,22 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { FormattedMessage } from "react-intl";
4
+ import { Header, Icon, Grid, Segment } from "semantic-ui-react";
5
+
6
+ export default function ExecutionGroupsHeader({ children }) {
7
+ return (
8
+ <Segment>
9
+ <Grid>
10
+ <Grid.Column width={8}>
11
+ <Header as="h2">
12
+ <Icon circular name="tasks" />
13
+ <Header.Content>
14
+ <FormattedMessage id="sidemenu.quality_controls_execution_groups" />
15
+ </Header.Content>
16
+ </Header>
17
+ </Grid.Column>
18
+ </Grid>
19
+ {children}
20
+ </Segment>
21
+ );
22
+ }
@@ -0,0 +1,104 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useEffect } from "react";
3
+ import PropTypes from "prop-types";
4
+ import { FormattedMessage, useIntl } from "react-intl";
5
+ import { Table, Header, Icon } from "semantic-ui-react";
6
+ import { columnDecorator } from "@truedat/core/services";
7
+ import { useSearchContext } from "../search/SearchContext";
8
+ import ExecutionGroupLink from "./ExecutionGroupLink";
9
+
10
+ export const HeaderRow = ({ columns }) => (
11
+ <Table.Row>
12
+ {columns.map(({ name: id, textAlign }, i) => (
13
+ <Table.HeaderCell key={i} textAlign={textAlign}>
14
+ <FormattedMessage id={id} defaultMessage={id} />
15
+ </Table.HeaderCell>
16
+ ))}
17
+ </Table.Row>
18
+ );
19
+
20
+ HeaderRow.propTypes = {
21
+ columns: PropTypes.arrayOf(PropTypes.object),
22
+ };
23
+
24
+ export const ExecutionGroupRow = ({ columns, ...props }) => (
25
+ <Table.Row>
26
+ {columns.map((col, i) => (
27
+ <Table.Cell
28
+ key={i}
29
+ textAlign={col.textAlign}
30
+ content={columnDecorator(col)(props)}
31
+ />
32
+ ))}
33
+ </Table.Row>
34
+ );
35
+
36
+ ExecutionGroupRow.propTypes = {
37
+ columns: PropTypes.arrayOf(PropTypes.object),
38
+ };
39
+
40
+ export default function ExecutionGroupsTable() {
41
+ const columns = [
42
+ {
43
+ name: "quality_controls.table.header.created",
44
+ width: 2,
45
+ fieldSelector: _.identity,
46
+ fieldDecorator: ExecutionGroupLink,
47
+ },
48
+ {
49
+ name: "quality_controls.table.header.pending",
50
+ fieldSelector: "pending_count",
51
+ textAlign: "right",
52
+ width: 1,
53
+ },
54
+ {
55
+ name: "quality_controls.table.header.started",
56
+ fieldSelector: "started_count",
57
+ textAlign: "right",
58
+ width: 1,
59
+ },
60
+ {
61
+ name: "quality_controls.table.header.succeded",
62
+ fieldSelector: "succeeded_count",
63
+ textAlign: "right",
64
+ width: 1,
65
+ },
66
+ {
67
+ name: "quality_controls.table.header.failed",
68
+ fieldSelector: "failed_count",
69
+ textAlign: "right",
70
+ width: 1,
71
+ },
72
+ ];
73
+
74
+ const { listExecutionGroups, executionGroups } = useSearchContext();
75
+
76
+ useEffect(() => {
77
+ listExecutionGroups();
78
+ }, []);
79
+
80
+ return (
81
+ <>
82
+ {!_.isEmpty(executionGroups) && (
83
+ <Table>
84
+ <Table.Header>
85
+ <HeaderRow columns={columns} />
86
+ </Table.Header>
87
+ <Table.Body>
88
+ {executionGroups?.map((props, key) => (
89
+ <ExecutionGroupRow key={key} columns={columns} {...props} />
90
+ ))}
91
+ </Table.Body>
92
+ </Table>
93
+ )}
94
+ {_.isEmpty(executionGroups) && (
95
+ <Header as="h4">
96
+ <Icon name="search" />
97
+ <Header.Content>
98
+ <FormattedMessage id="quality_control_execution_group.search.results.empty" />
99
+ </Header.Content>
100
+ </Header>
101
+ )}
102
+ </>
103
+ );
104
+ }
@@ -0,0 +1,31 @@
1
+ import _ from "lodash/fp";
2
+ import { FormattedMessage } from "react-intl";
3
+ import React from "react";
4
+ import PropTypes from "prop-types";
5
+ import { Icon } from "semantic-ui-react";
6
+
7
+ const PENDING = { name: "info circle", color: "grey" };
8
+
9
+ export const icons = {
10
+ PENDING,
11
+ STARTED: { name: "play circle outline", color: "green" },
12
+ SUCCEEDED: { name: "check circle outline", color: "green" },
13
+ FAILED: { name: "warning circle", color: "red" },
14
+ };
15
+
16
+ export const ExecutionStatusDecorator = (status) => {
17
+ const icon = _.propOr(PENDING, _.toUpper(status))(icons);
18
+ const messageId = "quality_control_execution_group.status." + status;
19
+ return (
20
+ <>
21
+ <Icon size="large" {...icon} />
22
+ <FormattedMessage id={messageId} />
23
+ </>
24
+ );
25
+ };
26
+
27
+ ExecutionStatusDecorator.propTypes = {
28
+ status: PropTypes.string,
29
+ };
30
+
31
+ export default ExecutionStatusDecorator;
@@ -0,0 +1,97 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useEffect } from "react";
3
+ import PropTypes from "prop-types";
4
+ import { connect } from "react-redux";
5
+ import { useIntl } from "react-intl";
6
+ import { useParams } from "react-router-dom";
7
+ import { Table } from "semantic-ui-react";
8
+ import { DateTime } from "@truedat/core/components";
9
+ import { columnDecorator } from "@truedat/core/services";
10
+ import { useExecutionGroupsShow } from "../../hooks/useExecutionGroups";
11
+ import ExecutionGroupMessage from "./ExecutionGroupMessage";
12
+ // import RuleResultDecorator from "./RuleResultDecorator";
13
+ // import RuleImplementationResultsLink from "./RuleImplementationResultsLink";
14
+ import ExecutionStatusDecorator from "./ExecutionStatusDecorator";
15
+ import ExecutionGroupContent from "./ExecutionGroupContent";
16
+ import ExecutionGroupBreadcrumbs from "./ExecutionGroupBreadcrumbs";
17
+
18
+ const COLUMNS = [
19
+ {
20
+ name: "ruleImplementations.props.status",
21
+ fieldDecorator: ExecutionStatusDecorator,
22
+ fieldSelector: "status",
23
+ },
24
+ {
25
+ name: "ruleImplementations.props.implementation_key",
26
+ fieldSelector: "quality_control_name",
27
+ // fieldDecorator: RuleImplementationResultsLink,
28
+ },
29
+ ];
30
+
31
+ export const ExecutionRow = ({ execution }) => (
32
+ <Table.Row>
33
+ {COLUMNS.map(({ textAlign, ...column }, i) => (
34
+ <Table.Cell
35
+ key={i}
36
+ content={columnDecorator(column)(execution)}
37
+ textAlign={textAlign}
38
+ />
39
+ ))}
40
+ </Table.Row>
41
+ );
42
+
43
+ ExecutionRow.propTypes = {
44
+ execution: PropTypes.object,
45
+ };
46
+
47
+ const isCompleted = _.flow(_.prop("_embedded.status"), (s) =>
48
+ _.includes(s)(["SUCCEEDED", "FAILED"])
49
+ );
50
+
51
+ export const ExecutionGroup = () => {
52
+ const { formatMessage } = useIntl();
53
+ const { id } = useParams();
54
+
55
+ const executionGroup = useExecutionGroupsShow(id);
56
+ const executions = executionGroup.data?.data?.executions;
57
+ const count = _.size(executions);
58
+ const pendingCount = executionGroup.data?.data?.pending_count;
59
+ const content = executionGroup?.data?.data?.df_content;
60
+
61
+ console.log("execution_group => ", executionGroup);
62
+ console.log("executions => ", executions);
63
+
64
+ return _.isEmpty(executionGroup.data) === null ? null : (
65
+ <>
66
+ <ExecutionGroupBreadcrumbs timestamp={executionGroup?.inserted_at} />
67
+ {_.isEmpty(content) ? null : <ExecutionGroupContent content={content} />}
68
+ <ExecutionGroupMessage count={count} pending={pendingCount} />
69
+ <Table>
70
+ <Table.Header>
71
+ <Table.Row>
72
+ {COLUMNS.map(({ name: id, textAlign }, i) => (
73
+ <Table.HeaderCell
74
+ key={i}
75
+ content={formatMessage({ id })}
76
+ textAlign={textAlign}
77
+ />
78
+ ))}
79
+ </Table.Row>
80
+ </Table.Header>
81
+ <Table.Body>
82
+ {executions?.map((execution, i) => (
83
+ <ExecutionRow key={i} execution={execution} />
84
+ ))}
85
+ </Table.Body>
86
+ </Table>
87
+ </>
88
+ );
89
+ };
90
+
91
+ ExecutionGroup.propTypes = {
92
+ executionGroup: PropTypes.object,
93
+ };
94
+
95
+ export const mapStateToProps = ({ executionGroup }) => ({ executionGroup });
96
+
97
+ export default connect(mapStateToProps)(ExecutionGroup);
@@ -0,0 +1,106 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState } from "react";
3
+ import { Button, Form, Header } from "semantic-ui-react";
4
+ import PropTypes from "prop-types";
5
+ import { useIntl } from "react-intl";
6
+ import { TemplateSelector } from "@truedat/core/components";
7
+ import { validateContent } from "@truedat/df/utils";
8
+
9
+ const DynamicForm = React.lazy(() =>
10
+ import("@truedat/df/components/DynamicForm")
11
+ );
12
+
13
+ export const ExecutionForm = ({ count, onSubmit, onCancel }) => {
14
+ const { formatMessage } = useIntl();
15
+ const [content, setContent] = useState({});
16
+ const [template, setTemplate] = useState();
17
+ const [templatesLoading, setTemplatesLoading] = useState(true);
18
+
19
+ const handleContentChange = (content) => setContent(content);
20
+
21
+ const handleTemplatesLoaded = ({ templates }) => {
22
+ setTemplatesLoading(false);
23
+ if (templates?.length === 1) {
24
+ setTemplate(templates[0]);
25
+ }
26
+ };
27
+
28
+ const handleTemplateSelected = (e, { template }) => setTemplate(template);
29
+
30
+ const isInvalid = () =>
31
+ template && !_.isEmpty(validateContent(template)(content));
32
+
33
+ return (
34
+ <>
35
+ <Header
36
+ as="h2"
37
+ content={formatMessage({
38
+ id: "implementations.actions.execution.confirmation.header",
39
+ })}
40
+ />
41
+ <Form loading={templatesLoading}>
42
+ <TemplateSelector
43
+ scope="qe"
44
+ selectedValue={template?.id}
45
+ onChange={handleTemplateSelected}
46
+ onLoad={handleTemplatesLoaded}
47
+ clearable
48
+ />
49
+ {template?.id ? (
50
+ <>
51
+ <Header
52
+ as="h3"
53
+ content={formatMessage({
54
+ id: "implementations.actions.execution.confirmation.legend",
55
+ })}
56
+ />
57
+ <div
58
+ style={{
59
+ maxHeight: "calc(100vh - 20rem)",
60
+ overflowY: "auto",
61
+ }}
62
+ >
63
+ <DynamicForm
64
+ onChange={handleContentChange}
65
+ content={content}
66
+ template={template}
67
+ />
68
+ </div>
69
+ </>
70
+ ) : null}
71
+ <p>
72
+ {formatMessage(
73
+ {
74
+ id:
75
+ count === 1
76
+ ? "implementation.actions.execution.confirmation.content"
77
+ : "implementations.actions.execution.confirmation.content",
78
+ },
79
+ { implementations_count: count }
80
+ )}
81
+ </p>
82
+ <div className="actions">
83
+ <Button
84
+ secondary
85
+ onClick={onCancel}
86
+ content={formatMessage({ id: "actions.cancel" })}
87
+ />
88
+ <Button
89
+ primary
90
+ disabled={isInvalid()}
91
+ onClick={() => onSubmit(content)}
92
+ content={formatMessage({ id: "actions.create" })}
93
+ />
94
+ </div>
95
+ </Form>
96
+ </>
97
+ );
98
+ };
99
+
100
+ ExecutionForm.propTypes = {
101
+ count: PropTypes.number,
102
+ onSubmit: PropTypes.func,
103
+ onCancel: PropTypes.func,
104
+ };
105
+
106
+ export default ExecutionForm;
@@ -0,0 +1,63 @@
1
+ import React, { useState } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { connect } from "react-redux";
4
+ import { useIntl } from "react-intl";
5
+ import { Button, Popup } from "semantic-ui-react";
6
+ import ExecutionForm from "./ExecutionForm";
7
+
8
+ export const ExecutionPopup = ({
9
+ disabled,
10
+ executionGroupLoading,
11
+ count,
12
+ onSubmit,
13
+ executionGroupType = "implementation",
14
+ }) => {
15
+ const { formatMessage } = useIntl();
16
+ const [open, setOpen] = useState(false);
17
+
18
+ return (
19
+ <Popup
20
+ on="click"
21
+ basic
22
+ flowing
23
+ onOpen={() => setOpen(true)}
24
+ onClose={() => false}
25
+ open={open}
26
+ position="bottom center"
27
+ size="small"
28
+ trigger={
29
+ <Button
30
+ secondary
31
+ disabled={disabled}
32
+ loading={executionGroupLoading}
33
+ content={formatMessage({
34
+ id:
35
+ count === 1
36
+ ? executionGroupType + ".actions.do_execution"
37
+ : executionGroupType + "s.actions.do_execution",
38
+ })}
39
+ />
40
+ }
41
+ >
42
+ <ExecutionForm
43
+ count={count}
44
+ onSubmit={onSubmit}
45
+ onCancel={() => setOpen(false)}
46
+ />
47
+ </Popup>
48
+ );
49
+ };
50
+
51
+ ExecutionPopup.propTypes = {
52
+ count: PropTypes.number,
53
+ disabled: PropTypes.bool,
54
+ executionGroupLoading: PropTypes.bool,
55
+ onSubmit: PropTypes.func,
56
+ executionGroupType: PropTypes.string,
57
+ };
58
+
59
+ const mapStateToProps = ({ executionGroupLoading }) => ({
60
+ executionGroupLoading,
61
+ });
62
+
63
+ export default connect(mapStateToProps)(ExecutionPopup);
@@ -3,18 +3,28 @@ import { Route, Switch } from "react-router-dom";
3
3
  import { Unauthorized } from "@truedat/core/components";
4
4
  import { useAuthorized } from "@truedat/core/hooks";
5
5
  import {
6
- QUALITY_CONTROLS,
6
+ QUALITY_CONTROLS_DEPRECATED,
7
+ QUALITY_CONTROLS_DRAFTS,
8
+ QUALITY_CONTROLS_PUBLISHED,
9
+ QUALITY_CONTROLS_EXECUTION_GROUPS,
10
+ QUALITY_CONTROLS_EXECUTION_GROUP,
7
11
  QUALITY_CONTROL_NEW,
8
12
  QUALITY_CONTROL_EDIT,
9
13
  QUALITY_CONTROL_NEW_DRAFT,
10
14
  QUALITY_CONTROL,
15
+ QUALITY_CONTROL_HISTORY,
11
16
  } from "@truedat/core/routes";
12
- import { QUALITY_CONTROL_HISTORY } from "../../../../core/src/routes";
17
+
13
18
  import { SearchContextProvider } from "../search/SearchContext";
19
+ import ExecutionGroupsTable from "../executions/ExecutionGroupsTable";
20
+ import ExecutionGroupsHeader from "../executions/ExecutionGroupsHeader";
21
+ import ExecutionGroupDetail from "../executions/executionGroupDetail";
22
+
14
23
  import QualityControls from "./QualityControls";
15
24
  import QualityControl from "./QualityControl";
16
25
  import QualityControlHeader from "./QualityControlHeader";
17
26
  import QualityControlHistory from "./QualityControlHistory";
27
+
18
28
  import NewQualityControl from "./NewQualityControl";
19
29
  import EditQualityControl from "./EditQualityControl";
20
30
  import NewDraftQualityControl from "./NewDraftQualityControl";
@@ -30,8 +40,52 @@ export default function QxRoutes() {
30
40
  <Switch>
31
41
  <Route
32
42
  exact
33
- path={QUALITY_CONTROLS}
34
- render={() => (authorized ? <QualityControls /> : <Unauthorized />)}
43
+ path={QUALITY_CONTROLS_EXECUTION_GROUPS}
44
+ render={() =>
45
+ authorized ? (
46
+ <ExecutionGroupsHeader>
47
+ <ExecutionGroupsTable />
48
+ </ExecutionGroupsHeader>
49
+ ) : (
50
+ <Unauthorized />
51
+ )
52
+ }
53
+ />
54
+ <Route
55
+ exact
56
+ path={QUALITY_CONTROLS_EXECUTION_GROUP}
57
+ render={() =>
58
+ authorized ? <ExecutionGroupDetail /> : <Unauthorized />
59
+ }
60
+ />
61
+ <Route
62
+ exact
63
+ path={QUALITY_CONTROLS_PUBLISHED}
64
+ render={() =>
65
+ authorized ? (
66
+ <QualityControls status="published" />
67
+ ) : (
68
+ <Unauthorized />
69
+ )
70
+ }
71
+ />
72
+ <Route
73
+ exact
74
+ path={QUALITY_CONTROLS_DEPRECATED}
75
+ render={() =>
76
+ authorized ? (
77
+ <QualityControls status="deprecated" />
78
+ ) : (
79
+ <Unauthorized />
80
+ )
81
+ }
82
+ />
83
+ <Route
84
+ exact
85
+ path={QUALITY_CONTROLS_DRAFTS}
86
+ render={() =>
87
+ authorized ? <QualityControls status="draft" /> : <Unauthorized />
88
+ }
35
89
  />
36
90
  <Route
37
91
  exact
@@ -1,28 +1,62 @@
1
1
  import _ from "lodash/fp";
2
- import React from "react";
2
+ import React, { useEffect } from "react";
3
3
  import { useIntl, FormattedMessage } from "react-intl";
4
- import { Link } from "react-router-dom";
4
+ import { Link, useHistory } from "react-router-dom";
5
5
  import {
6
6
  Button,
7
+ Checkbox,
7
8
  Container,
8
9
  Header,
9
10
  Icon,
10
11
  Segment,
11
12
  Grid,
12
13
  } from "semantic-ui-react";
13
- import { QUALITY_CONTROL_NEW } from "@truedat/core/routes";
14
+ import { linkTo, QUALITY_CONTROL_NEW } from "@truedat/core/routes";
14
15
  import useAuthorizedAction from "@truedat/core/hooks/useAuthorizedAction";
15
16
  import Loading from "@truedat/core/src/components/Loading";
17
+ import { useSearchContext } from "../search/SearchContext";
16
18
  import QualityControlsSearch from "../search/QualityControlsSearch";
19
+ import { useExecutionGroupsCreate } from "../../hooks/useExecutionGroups";
20
+ import ExecutionPopup from "./ExecutionPopup";
17
21
  import QualityControlsTable from "./QualityControlsTable";
18
22
 
19
- export default function QualityControls() {
23
+ export const QualityControls = ({ status }) => {
20
24
  const { formatMessage } = useIntl();
25
+ const history = useHistory();
21
26
 
22
- const { data, loading } = useAuthorizedAction({
27
+ const { data } = useAuthorizedAction({
23
28
  action: "createQualityControls",
24
29
  });
25
30
  const canCreate = _.prop("has_any_domain")(data);
31
+
32
+ const {
33
+ query,
34
+ searchMust,
35
+ loadingFilters: loading,
36
+ canExecuteQualityControl,
37
+ setCanExecuteQualityControl,
38
+ qualityControlsActions,
39
+ setQualityControlsStatus,
40
+ qualityControls,
41
+ } = useSearchContext();
42
+
43
+ const { trigger: triggerCreateExecutionGroup } = useExecutionGroupsCreate();
44
+
45
+ const handleSubmit = (df_content) => {
46
+ triggerCreateExecutionGroup({
47
+ query,
48
+ must: searchMust,
49
+ df_content,
50
+ }).then(({ data }) => {
51
+ const id = _.prop("data.id")(data);
52
+ history.push(linkTo.QUALITY_CONTROLS_EXECUTION_GROUP({ id }));
53
+ });
54
+ };
55
+
56
+ useEffect(() => {
57
+ setQualityControlsStatus(status);
58
+ }, [status, setQualityControlsStatus]);
59
+
26
60
  return (
27
61
  <Segment loading={false}>
28
62
  <Header as="h2">
@@ -45,6 +79,27 @@ export default function QualityControls() {
45
79
  <Loading />
46
80
  ) : (
47
81
  <>
82
+ {status === "published" &&
83
+ _.size(qualityControls) > 0 &&
84
+ qualityControlsActions?.execute ? (
85
+ <>
86
+ <Checkbox
87
+ id="execute_checkbox"
88
+ className="bgOrange"
89
+ toggle
90
+ onChange={() =>
91
+ setCanExecuteQualityControl(!canExecuteQualityControl)
92
+ }
93
+ checked={canExecuteQualityControl}
94
+ style={{ top: "6px", marginRight: "7.5px" }}
95
+ />
96
+ <ExecutionPopup
97
+ disabled={!canExecuteQualityControl}
98
+ onSubmit={handleSubmit}
99
+ executionGroupType={"quality_control"}
100
+ />
101
+ </>
102
+ ) : null}
48
103
  {canCreate ? (
49
104
  <Button
50
105
  primary
@@ -60,7 +115,9 @@ export default function QualityControls() {
60
115
  </Container>
61
116
  </Grid.Column>
62
117
  </Grid>
63
- <QualityControlsTable />
118
+ <QualityControlsTable status={status} />
64
119
  </Segment>
65
120
  );
66
- }
121
+ };
122
+
123
+ export default QualityControls;
@@ -130,7 +130,7 @@ export default function QualityControlsTable() {
130
130
  <Header as="h4">
131
131
  <Icon name="search" />
132
132
  <Header.Content>
133
- {formatMessage({ id: "ruleImplementations.search.results.empty" })}
133
+ {formatMessage({ id: "quality_controls.search.results.empty" })}
134
134
  </Header.Content>
135
135
  </Header>
136
136
  )}
@@ -7,6 +7,7 @@ import React, {
7
7
  useMemo,
8
8
  } from "react";
9
9
  import { useIntl } from "react-intl";
10
+
10
11
  import {
11
12
  toFilterValues,
12
13
  formatFilterValues,
@@ -18,6 +19,8 @@ import {
18
19
  useQualityControlsFilters,
19
20
  } from "../../hooks/useQualityControls";
20
21
 
22
+ import { useExecutionGroupsIndex } from "../../hooks/useExecutionGroups";
23
+
21
24
  const SearchContext = createContext();
22
25
 
23
26
  export const SearchContextProvider = (props) => {
@@ -26,6 +29,13 @@ export const SearchContextProvider = (props) => {
26
29
  const initialSortDirection = _.prop("initialSortDirection")(props);
27
30
  const { formatMessage } = useIntl();
28
31
 
32
+ const [qualityControlsActions, setQualityControlsActions] = useState({});
33
+ const [executionGroups, setExecutionGroups] = useState([]);
34
+ const [qualityControlsStatus, setQualityControlsStatus] = useState();
35
+
36
+ const [canExecuteQualityControl, setCanExecuteQualityControl] =
37
+ useState(false);
38
+
29
39
  const [loadingSearch, setLoadingSearch] = useState(true);
30
40
  const [qualityControls, setQualityControls] = useState([]);
31
41
 
@@ -99,10 +109,13 @@ export const SearchContextProvider = (props) => {
99
109
  _.propOr([], activeFilterName),
100
110
  toFilterValues
101
111
  )(allActiveFilters);
102
-
103
112
  const searchMust = useMemo(
104
- () => _.pickBy(_.negate(_.isEmpty))(allActiveFilters),
105
- [allActiveFilters]
113
+ () => ({
114
+ ..._.pickBy(_.negate(_.isEmpty))(allActiveFilters),
115
+ status: [qualityControlsStatus],
116
+ ...(canExecuteQualityControl && { executable: ["true"] }),
117
+ }),
118
+ [allActiveFilters, canExecuteQualityControl, qualityControlsStatus]
106
119
  );
107
120
  const filterMust = useMemo(
108
121
  () =>
@@ -135,11 +148,21 @@ export const SearchContextProvider = (props) => {
135
148
  const { trigger: triggerSearch } = useQualityControlsSearch();
136
149
  useEffect(() => {
137
150
  setLoadingSearch(true);
138
- triggerSearch({ query, must: searchMust, sort }).then(({ data }) => {
139
- setQualityControls(data?.data);
140
- setLoadingSearch(false);
151
+
152
+ !_.isEmpty(qualityControlsStatus) &&
153
+ triggerSearch({ query, must: searchMust, sort }).then(({ data }) => {
154
+ setQualityControlsActions(data?.actions);
155
+ setQualityControls(data?.data);
156
+ setLoadingSearch(false);
157
+ });
158
+ }, [query, searchMust, sort, triggerSearch, qualityControlsStatus]);
159
+
160
+ const { trigger: triggerListExecutionGroups } = useExecutionGroupsIndex();
161
+ const listExecutionGroups = () => {
162
+ triggerListExecutionGroups().then(({ data }) => {
163
+ setExecutionGroups(data?.data);
141
164
  });
142
- }, [query, searchMust, sort, triggerSearch]);
165
+ };
143
166
 
144
167
  const context = {
145
168
  disabled: false,
@@ -160,15 +183,22 @@ export const SearchContextProvider = (props) => {
160
183
  closeFilter,
161
184
  removeFilter,
162
185
  toggleFilterValue,
186
+ searchMust,
163
187
  setQuery,
164
188
 
189
+ canExecuteQualityControl,
190
+ setCanExecuteQualityControl,
165
191
  qualityControls,
192
+ qualityControlsActions,
166
193
  loadingSearch,
194
+ listExecutionGroups,
195
+ executionGroups,
167
196
 
168
197
  sortColumn,
169
198
  sortDirection,
170
199
  setSortColumn,
171
200
  setSortDirection,
201
+ setQualityControlsStatus,
172
202
  };
173
203
 
174
204
  return (
@@ -0,0 +1,34 @@
1
+ import { compile } from "path-to-regexp";
2
+ import useSWR from "swr";
3
+ import useSWRMutations from "swr/mutation";
4
+ import {
5
+ apiJson,
6
+ apiJsonPost,
7
+ apiJsonPatch,
8
+ apiJsonDelete,
9
+ } from "@truedat/core/services/api";
10
+ import {
11
+ API_QUALITY_CONTROL_EXECUTION_GROUPS_CREATE,
12
+ API_QUALITY_CONTROL_EXECUTION_GROUPS_INDEX,
13
+ API_QUALITY_CONTROL_EXECUTION_GROUP,
14
+ } from "../api";
15
+
16
+ export const useExecutionGroupsCreate = () => {
17
+ return useSWRMutations(
18
+ API_QUALITY_CONTROL_EXECUTION_GROUPS_CREATE,
19
+ (url, { arg }) => apiJsonPost(url, arg)
20
+ );
21
+ };
22
+
23
+ export const useExecutionGroupsIndex = () => {
24
+ return useSWRMutations(
25
+ API_QUALITY_CONTROL_EXECUTION_GROUPS_INDEX,
26
+ (url, { arg }) => apiJson(url, arg)
27
+ );
28
+ };
29
+
30
+ export const useExecutionGroupsShow = (id) => {
31
+ const url = compile(API_QUALITY_CONTROL_EXECUTION_GROUP)({ id });
32
+ const { data, error, mutate } = useSWR(url, apiJson);
33
+ return { data: data?.data, error, loading: !error && !data, mutate };
34
+ };