foreman_rh_cloud 3.0.21.1 → 3.0.22
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.
- checksums.yaml +4 -4
- data/app/controllers/insights_cloud/api/machine_telemetries_controller.rb +17 -7
- data/app/models/insights_client_report_status.rb +58 -0
- data/app/services/foreman_rh_cloud/cloud_auth.rb +12 -0
- data/app/services/foreman_rh_cloud/cloud_request.rb +14 -0
- data/app/services/foreman_rh_cloud/cloud_request_forwarder.rb +1 -6
- data/app/services/foreman_rh_cloud/remediations_retriever.rb +1 -4
- data/app/subscribers/foreman_rh_cloud/insights_subscriber.rb +9 -0
- data/lib/foreman_inventory_upload/generators/fact_helpers.rb +19 -0
- data/lib/foreman_inventory_upload/generators/slice.rb +6 -6
- data/lib/foreman_rh_cloud/engine.rb +4 -1
- data/lib/foreman_rh_cloud/version.rb +1 -1
- data/lib/insights_cloud/async/insights_full_sync.rb +4 -14
- data/lib/insights_cloud/async/insights_resolutions_sync.rb +1 -4
- data/lib/insights_cloud/async/insights_rules_sync.rb +2 -7
- data/lib/inventory_sync/async/inventory_full_sync.rb +2 -1
- data/lib/inventory_sync/async/inventory_scheduled_sync.rb +8 -0
- data/lib/inventory_sync/async/query_inventory_job.rb +1 -4
- data/lib/tasks/rh_cloud_inventory.rake +6 -0
- data/package.json +2 -2
- data/test/factories/inventory_upload_factories.rb +1 -1
- data/test/jobs/insights_full_sync_test.rb +3 -0
- data/test/jobs/insights_resolutions_sync_test.rb +3 -0
- data/test/jobs/insights_rules_sync_test.rb +3 -0
- data/test/jobs/inventory_full_sync_test.rb +3 -0
- data/test/models/insights_client_report_status_test.rb +77 -0
- data/test/unit/slice_generator_test.rb +66 -29
- data/webpack/ForemanInventoryUpload/Components/FullScreenModal/FullScreenModal.js +1 -1
- data/webpack/ForemanInventoryUpload/Components/FullScreenModal/__tests__/__snapshots__/FullScreenModal.test.js.snap +1 -1
- data/webpack/ForemanInventoryUpload/Components/FullScreenModal/fullScreenModal.scss +14 -16
- data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/SyncButtonActions.js +28 -63
- data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/__snapshots__/integrations.test.js.snap +2 -3
- data/webpack/ForemanInventoryUpload/Components/Terminal/Terminal.js +1 -1
- data/webpack/ForemanInventoryUpload/Components/Terminal/__tests__/Terminal.test.js +1 -1
- data/webpack/ForemanInventoryUpload/Components/Terminal/__tests__/__snapshots__/Terminal.test.js.snap +2 -2
- data/webpack/ForemanInventoryUpload/Components/Terminal/terminal.scss +25 -27
- data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsTableActions.js +19 -19
- data/webpack/InsightsCloudSync/Components/InsightsTable/__tests__/__snapshots__/InsightsTableActions.test.js.snap +14 -14
- data/webpack/InsightsCloudSync/Components/RemediationModal/RemediateButton.js +1 -0
- data/webpack/InsightsCloudSync/InsightsCloudSync.js +4 -1
- data/webpack/InsightsCloudSync/InsightsCloudSyncActions.js +44 -20
- data/webpack/InsightsCloudSync/InsightsCloudSyncConstants.js +2 -0
- data/webpack/InsightsCloudSync/__tests__/__snapshots__/InsightsCloudSyncActions.test.js.snap +11 -7
- data/webpack/common/ForemanTasks/ForemanTasksActions.js +64 -0
- data/webpack/common/ForemanTasks/ForemanTasksHelpers.js +7 -0
- data/webpack/common/ForemanTasks/index.js +1 -0
- metadata +10 -2
data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/SyncButtonActions.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import { withInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
|
|
2
|
+
import { post } from 'foremanReact/redux/API';
|
|
4
3
|
import { addToast } from 'foremanReact/redux/actions/toasts';
|
|
5
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
6
5
|
import { inventoryUrl } from '../../../../ForemanInventoryHelpers';
|
|
@@ -9,7 +8,10 @@ import {
|
|
|
9
8
|
INVENTORY_SYNC,
|
|
10
9
|
INVENTORY_SYNC_TASK_UPDATE,
|
|
11
10
|
} from './SyncButtonConstants';
|
|
12
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
setupTaskPolling,
|
|
13
|
+
taskRelatedToast,
|
|
14
|
+
} from '../../../../../common/ForemanTasks';
|
|
13
15
|
|
|
14
16
|
export const handleSync = () => dispatch => {
|
|
15
17
|
dispatch(
|
|
@@ -21,9 +23,9 @@ export const handleSync = () => dispatch => {
|
|
|
21
23
|
task: { id },
|
|
22
24
|
},
|
|
23
25
|
}) => {
|
|
24
|
-
dispatch(
|
|
26
|
+
dispatch(setupInventorySyncTaskPolling(id, dispatch));
|
|
25
27
|
return dispatch(
|
|
26
|
-
|
|
28
|
+
taskRelatedToast(id, 'info', __('Inventory sync has started:'))
|
|
27
29
|
);
|
|
28
30
|
},
|
|
29
31
|
errorToast: inventorySyncErrorToast,
|
|
@@ -31,62 +33,25 @@ export const handleSync = () => dispatch => {
|
|
|
31
33
|
);
|
|
32
34
|
};
|
|
33
35
|
|
|
34
|
-
export const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
),
|
|
52
|
-
})
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
if (result === 'error') {
|
|
56
|
-
dispatch(
|
|
57
|
-
taskPageRefererToast(
|
|
58
|
-
id,
|
|
59
|
-
'error',
|
|
60
|
-
__('Inventory sync has failed:'),
|
|
61
|
-
true
|
|
62
|
-
)
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
stopTaskInterval();
|
|
66
|
-
},
|
|
67
|
-
errorToast: inventorySyncErrorToast,
|
|
68
|
-
})
|
|
69
|
-
)
|
|
70
|
-
);
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const inventorySyncErrorToast = ({ message, response }) =>
|
|
74
|
-
`${__('Inventory sync has failed: ')} ${response.data?.message || message}`;
|
|
75
|
-
|
|
76
|
-
const taskPageRefererToast = (taskID, toastType, prefix, sticky = false) =>
|
|
77
|
-
addToast({
|
|
78
|
-
sticky,
|
|
79
|
-
type: toastType,
|
|
80
|
-
message: (
|
|
81
|
-
<span>
|
|
82
|
-
{prefix}{' '}
|
|
83
|
-
<a
|
|
84
|
-
target="_blank"
|
|
85
|
-
rel="noopener noreferrer"
|
|
86
|
-
href={foremanUrl(`/foreman_tasks/tasks/${taskID}`)}
|
|
87
|
-
>
|
|
88
|
-
{__('view the task page for more details')}
|
|
89
|
-
</a>
|
|
90
|
-
</span>
|
|
91
|
-
),
|
|
36
|
+
export const setupInventorySyncTaskPolling = (id, dispatch) =>
|
|
37
|
+
setupTaskPolling({
|
|
38
|
+
taskId: id,
|
|
39
|
+
key: INVENTORY_SYNC_TASK_UPDATE,
|
|
40
|
+
onTaskSuccess: ({
|
|
41
|
+
output: {
|
|
42
|
+
host_statuses: { sync, disconnect },
|
|
43
|
+
},
|
|
44
|
+
}) =>
|
|
45
|
+
dispatch(
|
|
46
|
+
addToast({
|
|
47
|
+
sticky: true,
|
|
48
|
+
type: 'success',
|
|
49
|
+
message: <Toast syncHosts={sync} disconnectHosts={disconnect} />,
|
|
50
|
+
})
|
|
51
|
+
),
|
|
52
|
+
dispatch,
|
|
92
53
|
});
|
|
54
|
+
|
|
55
|
+
const inventorySyncErrorToast = message =>
|
|
56
|
+
`${__('Inventory sync has failed: ')} ${message.response?.data?.message ||
|
|
57
|
+
message}`;
|
|
@@ -22,7 +22,7 @@ Array [
|
|
|
22
22
|
"errorToast": [Function],
|
|
23
23
|
"interval": 3000,
|
|
24
24
|
"type": "API_GET",
|
|
25
|
-
"url": "/
|
|
25
|
+
"url": "/foreman_tasks/api/tasks/1/details?include_permissions",
|
|
26
26
|
},
|
|
27
27
|
],
|
|
28
28
|
Array [
|
|
@@ -31,7 +31,7 @@ Array [
|
|
|
31
31
|
"message": Object {
|
|
32
32
|
"message": <span>
|
|
33
33
|
Inventory sync has started:
|
|
34
|
-
|
|
34
|
+
<br />
|
|
35
35
|
<a
|
|
36
36
|
href="/foreman_tasks/tasks/1"
|
|
37
37
|
rel="noopener noreferrer"
|
|
@@ -40,7 +40,6 @@ Array [
|
|
|
40
40
|
view the task page for more details
|
|
41
41
|
</a>
|
|
42
42
|
</span>,
|
|
43
|
-
"sticky": false,
|
|
44
43
|
"type": "info",
|
|
45
44
|
},
|
|
46
45
|
},
|
|
@@ -29,6 +29,6 @@ describe('Terminal', () => {
|
|
|
29
29
|
const text = 'some-string-log';
|
|
30
30
|
const modifiedProps = { ...props, logs: text };
|
|
31
31
|
const wrapper = mount(<Terminal {...modifiedProps} />);
|
|
32
|
-
expect(wrapper.find('.terminal p').text()).toEqual(text);
|
|
32
|
+
expect(wrapper.find('.rh-cloud-inventory-terminal p').text()).toEqual(text);
|
|
33
33
|
});
|
|
34
34
|
});
|
|
@@ -7,7 +7,7 @@ exports[`Terminal rendering render with props 1`] = `
|
|
|
7
7
|
sm={12}
|
|
8
8
|
>
|
|
9
9
|
<div
|
|
10
|
-
className="terminal"
|
|
10
|
+
className="rh-cloud-inventory-terminal"
|
|
11
11
|
onScroll={[Function]}
|
|
12
12
|
>
|
|
13
13
|
<Grid
|
|
@@ -65,7 +65,7 @@ exports[`Terminal rendering render without Props 1`] = `
|
|
|
65
65
|
sm={12}
|
|
66
66
|
>
|
|
67
67
|
<div
|
|
68
|
-
className="terminal"
|
|
68
|
+
className="rh-cloud-inventory-terminal"
|
|
69
69
|
onScroll={[Function]}
|
|
70
70
|
>
|
|
71
71
|
<Grid
|
|
@@ -1,34 +1,32 @@
|
|
|
1
|
-
.rh-cloud-inventory-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
overflow-x: hidden;
|
|
1
|
+
.rh-cloud-inventory-terminal {
|
|
2
|
+
height: 200px;
|
|
3
|
+
background-color: #222;
|
|
4
|
+
padding: 10px 0;
|
|
5
|
+
margin-bottom: 20px;
|
|
6
|
+
overflow-y: scroll;
|
|
7
|
+
overflow-x: hidden;
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
p {
|
|
10
|
+
font-family: monospace;
|
|
11
|
+
font-size: 16px;
|
|
12
|
+
color: #22da26;
|
|
13
|
+
overflow-wrap: anywhere;
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
15
|
+
&.terminal_error {
|
|
16
|
+
color: #f00;
|
|
19
17
|
}
|
|
18
|
+
}
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
&::-webkit-scrollbar {
|
|
21
|
+
width: 12px;
|
|
22
|
+
height: 12px;
|
|
23
|
+
background: #aaa;
|
|
24
|
+
}
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
26
|
+
&::-webkit-scrollbar-thumb {
|
|
27
|
+
background: #222;
|
|
28
|
+
border-radius: 6px;
|
|
29
|
+
border: 3px solid transparent;
|
|
30
|
+
background-clip: content-box;
|
|
33
31
|
}
|
|
34
32
|
}
|
|
@@ -19,25 +19,6 @@ export const fetchInsights = (queryParams = {}) => (dispatch, getState) => {
|
|
|
19
19
|
...queryParams,
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
dispatch(
|
|
23
|
-
get({
|
|
24
|
-
key: INSIGHTS_HITS_API_KEY,
|
|
25
|
-
url: INSIGHTS_HITS_PATH,
|
|
26
|
-
params: {
|
|
27
|
-
page,
|
|
28
|
-
per_page: perPage,
|
|
29
|
-
search: query,
|
|
30
|
-
order: `${sortBy} ${sortOrder}`,
|
|
31
|
-
},
|
|
32
|
-
handleSuccess: response => {
|
|
33
|
-
if (isSelectAll) {
|
|
34
|
-
selectAllIds(dispatch, response.data.hits || []);
|
|
35
|
-
dispatch(selectAll());
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
})
|
|
39
|
-
);
|
|
40
|
-
|
|
41
22
|
const uri = new URI();
|
|
42
23
|
uri.search({
|
|
43
24
|
page,
|
|
@@ -58,6 +39,25 @@ export const fetchInsights = (queryParams = {}) => (dispatch, getState) => {
|
|
|
58
39
|
if (!isSelectAll) {
|
|
59
40
|
dispatch(setSelectAllAlert(false));
|
|
60
41
|
}
|
|
42
|
+
|
|
43
|
+
return dispatch(
|
|
44
|
+
get({
|
|
45
|
+
key: INSIGHTS_HITS_API_KEY,
|
|
46
|
+
url: INSIGHTS_HITS_PATH,
|
|
47
|
+
params: {
|
|
48
|
+
page,
|
|
49
|
+
per_page: perPage,
|
|
50
|
+
search: query,
|
|
51
|
+
order: `${sortBy} ${sortOrder}`,
|
|
52
|
+
},
|
|
53
|
+
handleSuccess: response => {
|
|
54
|
+
if (isSelectAll) {
|
|
55
|
+
selectAllIds(dispatch, response.data.hits || []);
|
|
56
|
+
dispatch(selectAll());
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
const selectAllIds = (dispatch, results, prevSelectedIds = {}) => {
|
|
@@ -26,6 +26,20 @@ Array [
|
|
|
26
26
|
|
|
27
27
|
exports[`insights table actions should fetchInsights 1`] = `
|
|
28
28
|
Array [
|
|
29
|
+
Array [
|
|
30
|
+
Object {
|
|
31
|
+
"payload": Object {
|
|
32
|
+
"args": Array [
|
|
33
|
+
Object {
|
|
34
|
+
"pathname": "/foreman_rh_cloud/insights_cloud",
|
|
35
|
+
"search": "?page=2&per_page=7&search=&sort_by=&sort_order=&select_all=true",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
"method": "push",
|
|
39
|
+
},
|
|
40
|
+
"type": "@@router/CALL_HISTORY_METHOD",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
29
43
|
Array [
|
|
30
44
|
Object {
|
|
31
45
|
"payload": Object {
|
|
@@ -61,20 +75,6 @@ Array [
|
|
|
61
75
|
"url": "/insights_cloud/hits",
|
|
62
76
|
},
|
|
63
77
|
],
|
|
64
|
-
Array [
|
|
65
|
-
Object {
|
|
66
|
-
"payload": Object {
|
|
67
|
-
"args": Array [
|
|
68
|
-
Object {
|
|
69
|
-
"pathname": "/foreman_rh_cloud/insights_cloud",
|
|
70
|
-
"search": "?page=2&per_page=7&search=&sort_by=&sort_order=&select_all=true",
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
"method": "push",
|
|
74
|
-
},
|
|
75
|
-
"type": "@@router/CALL_HISTORY_METHOD",
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
78
|
]
|
|
79
79
|
`;
|
|
80
80
|
|
|
@@ -34,6 +34,7 @@ const RemediateButton = ({ isExperimentalMode, selectedIds, toggleModal }) => {
|
|
|
34
34
|
button = (
|
|
35
35
|
<Popover
|
|
36
36
|
isVisible={isVisible}
|
|
37
|
+
aria-label={__('Please enable lab features to use this button')}
|
|
37
38
|
bodyContent={
|
|
38
39
|
<div dangerouslySetInnerHTML={{ __html: popoverContent }} />
|
|
39
40
|
}
|
|
@@ -36,7 +36,10 @@ const InsightsCloudSync = ({
|
|
|
36
36
|
toolbarButtons={
|
|
37
37
|
<>
|
|
38
38
|
<RemediationModal />
|
|
39
|
-
<Button
|
|
39
|
+
<Button
|
|
40
|
+
variant="secondary"
|
|
41
|
+
onClick={() => syncInsights(fetchInsights, query)}
|
|
42
|
+
>
|
|
40
43
|
{__('Start recommendations sync')}
|
|
41
44
|
</Button>
|
|
42
45
|
</>
|
|
@@ -1,25 +1,49 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
import { post } from 'foremanReact/redux/API';
|
|
3
2
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
4
3
|
import { insightsCloudUrl } from './InsightsCloudSyncHelpers';
|
|
5
|
-
import {
|
|
6
|
-
|
|
4
|
+
import {
|
|
5
|
+
INSIGHTS_CLOUD_SYNC,
|
|
6
|
+
INSIGHTS_CLOUD_SYNC_TASK,
|
|
7
|
+
} from './InsightsCloudSyncConstants';
|
|
8
|
+
import { setupTaskPolling, taskRelatedToast } from '../common/ForemanTasks';
|
|
7
9
|
|
|
8
|
-
export const syncInsights = () =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
export const syncInsights = (fetchInsights, query) => dispatch =>
|
|
11
|
+
dispatch(
|
|
12
|
+
post({
|
|
13
|
+
key: INSIGHTS_CLOUD_SYNC,
|
|
14
|
+
url: insightsCloudUrl('tasks'),
|
|
15
|
+
handleSuccess: ({
|
|
16
|
+
data: {
|
|
17
|
+
task: { id },
|
|
18
|
+
},
|
|
19
|
+
}) => {
|
|
20
|
+
dispatch(syncInsightsStartedToast(id));
|
|
21
|
+
dispatch(setupInsightsTaskPolling(id, fetchInsights, query, dispatch));
|
|
22
|
+
},
|
|
23
|
+
errorToast: error => syncInsightsError(error),
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const syncInsightsError = error =>
|
|
28
|
+
`${__('Recommendation sync has failed: ')} ${error}`;
|
|
29
|
+
|
|
30
|
+
const syncInsightsStartedToast = taskId =>
|
|
31
|
+
taskRelatedToast(taskId, 'info', __('Recommendation sync has started: '));
|
|
32
|
+
|
|
33
|
+
const setupInsightsTaskPolling = (taskId, fetchInsights, query, dispatch) =>
|
|
34
|
+
setupTaskPolling({
|
|
35
|
+
taskId,
|
|
36
|
+
key: INSIGHTS_CLOUD_SYNC_TASK,
|
|
37
|
+
onTaskSuccess: () => {
|
|
38
|
+
fetchInsights({ query, page: 1 });
|
|
39
|
+
dispatch(
|
|
40
|
+
taskRelatedToast(
|
|
41
|
+
taskId,
|
|
42
|
+
'success',
|
|
43
|
+
__('Recommendations synced successfully')
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
taskErrorMessage: data => syncInsightsError(data.humanized.errors),
|
|
48
|
+
dispatch,
|
|
25
49
|
});
|
|
@@ -4,6 +4,8 @@ import { foremanUrl } from '../ForemanRhCloudHelpers';
|
|
|
4
4
|
|
|
5
5
|
export const INSIGHTS_CLOUD_SYNC = 'INSIGHTS_CLOUD_SYNC';
|
|
6
6
|
|
|
7
|
+
export const INSIGHTS_CLOUD_SYNC_TASK = 'INSIGHTS_CLOUD_SYNC_TASK';
|
|
8
|
+
|
|
7
9
|
export const INSIGHTS_SYNC_PAGE_TITLE = __('Red Hat Insights');
|
|
8
10
|
|
|
9
11
|
export const INSIGHTS_PATH = foremanUrl('/foreman_rh_cloud/insights_cloud');
|
data/webpack/InsightsCloudSync/__tests__/__snapshots__/InsightsCloudSyncActions.test.js.snap
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
3
|
exports[`Insights cloud sync actions should syncInsights 1`] = `
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
Array [
|
|
5
|
+
Array [
|
|
6
|
+
Object {
|
|
7
|
+
"errorToast": [Function],
|
|
8
|
+
"handleSuccess": [Function],
|
|
9
|
+
"key": "INSIGHTS_CLOUD_SYNC",
|
|
10
|
+
"type": "post-some-type",
|
|
11
|
+
"url": "/insights_cloud/tasks",
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
]
|
|
11
15
|
`;
|