@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 +4 -4
- package/src/api.js +9 -0
- package/src/components/executions/ExecutionGroupBreadcrumbs.js +25 -0
- package/src/components/executions/ExecutionGroupContent.js +42 -0
- package/src/components/executions/ExecutionGroupLink.js +18 -0
- package/src/components/executions/ExecutionGroupMessage.js +27 -0
- package/src/components/executions/ExecutionGroupsHeader.js +22 -0
- package/src/components/executions/ExecutionGroupsTable.js +104 -0
- package/src/components/executions/ExecutionStatusDecorator.js +31 -0
- package/src/components/executions/executionGroupDetail.js +97 -0
- package/src/components/qualityControls/ExecutionForm.js +106 -0
- package/src/components/qualityControls/ExecutionPopup.js +63 -0
- package/src/components/qualityControls/QualityControlRoutes.js +58 -4
- package/src/components/qualityControls/QualityControls.js +64 -7
- package/src/components/qualityControls/QualityControlsTable.js +1 -1
- package/src/components/search/SearchContext.js +37 -7
- package/src/hooks/useExecutionGroups.js +34 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/qx",
|
|
3
|
-
"version": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
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={
|
|
34
|
-
render={() =>
|
|
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
|
|
23
|
+
export const QualityControls = ({ status }) => {
|
|
20
24
|
const { formatMessage } = useIntl();
|
|
25
|
+
const history = useHistory();
|
|
21
26
|
|
|
22
|
-
const { data
|
|
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: "
|
|
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
|
-
() =>
|
|
105
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
}
|
|
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
|
+
};
|