foreman_snapshot_management 1.6.1 → 2.0.1

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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -2
  3. data/Rakefile +7 -2
  4. data/app/controllers/api/v2/snapshots_controller.rb +37 -7
  5. data/app/controllers/concerns/foreman/controller/parameters/snapshot.rb +1 -1
  6. data/app/controllers/foreman_snapshot_management/snapshots_controller.rb +5 -5
  7. data/app/models/concerns/fog_extensions/proxmox/snapshots/mock.rb +24 -0
  8. data/app/models/foreman_snapshot_management/proxmox_extensions.rb +101 -0
  9. data/app/models/foreman_snapshot_management/snapshot.rb +28 -28
  10. data/app/models/foreman_snapshot_management/vmware_extensions.rb +40 -13
  11. data/app/views/api/v2/snapshots/base.json.rabl +2 -0
  12. data/app/views/api/v2/snapshots/main.json.rabl +2 -2
  13. data/app/views/foreman_snapshot_management/snapshots/_index.html.erb +12 -74
  14. data/app/views/hosts/_snapshots_tab.html.erb +8 -0
  15. data/lib/foreman_snapshot_management/engine.rb +35 -16
  16. data/lib/foreman_snapshot_management/version.rb +1 -1
  17. data/lib/tasks/foreman_snapshot_management_tasks.rake +2 -2
  18. data/locale/de/LC_MESSAGES/foreman_snapshot_management.mo +0 -0
  19. data/locale/de/foreman_snapshot_management.po +195 -0
  20. data/locale/en/LC_MESSAGES/foreman_snapshot_management.mo +0 -0
  21. data/locale/en/foreman_snapshot_management.po +179 -11
  22. data/locale/foreman_snapshot_management.pot +259 -8
  23. data/locale/gemspec.rb +1 -1
  24. data/package.json +46 -0
  25. data/test/controllers/api/v2/snapshots_test.rb +250 -39
  26. data/test/controllers/foreman_snapshot_management/snapshots_controller_test.rb +61 -9
  27. data/test/factories/proxmox_factory.rb +18 -0
  28. data/test/test_plugin_helper.rb +3 -0
  29. data/webpack/components/SnapshotManagement/SnapshotManagement.js +84 -0
  30. data/webpack/components/SnapshotManagement/SnapshotManagementActions.js +212 -0
  31. data/webpack/components/SnapshotManagement/SnapshotManagementConstants.js +9 -0
  32. data/webpack/components/SnapshotManagement/SnapshotManagementReducer.js +100 -0
  33. data/webpack/components/SnapshotManagement/SnapshotManagementSelectors.js +8 -0
  34. data/webpack/components/SnapshotManagement/__tests__/SnapshotManagementActions.test.js +123 -0
  35. data/webpack/components/SnapshotManagement/__tests__/SnapshotManagementReducer.test.js +157 -0
  36. data/webpack/components/SnapshotManagement/__tests__/__snapshots__/SnapshotManagementActions.test.js.snap +314 -0
  37. data/webpack/components/SnapshotManagement/__tests__/__snapshots__/SnapshotManagementReducer.test.js.snap +214 -0
  38. data/webpack/components/SnapshotManagement/components/SnapshotForm/SnapshotForm.js +118 -0
  39. data/webpack/components/SnapshotManagement/components/SnapshotForm/SnapshotFormConstants.js +5 -0
  40. data/webpack/components/SnapshotManagement/components/SnapshotForm/__tests__/SnapshotForm.test.js +26 -0
  41. data/webpack/components/SnapshotManagement/components/SnapshotForm/__tests__/__snapshots__/SnapshotForm.test.js.snap +476 -0
  42. data/webpack/components/SnapshotManagement/components/SnapshotForm/index.js +19 -0
  43. data/webpack/components/SnapshotManagement/components/SnapshotForm/snapshotForm.scss +3 -0
  44. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/SnapshotFormModal.js +37 -0
  45. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/SnapshotFormModalConstants.js +1 -0
  46. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/SnapshotFormModal.test.js +19 -0
  47. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/__snapshots__/SnapshotFormModal.test.js.snap +19 -0
  48. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/index.js +12 -0
  49. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/useSnapshotFormModal.js +7 -0
  50. data/webpack/components/SnapshotManagement/components/SnapshotList/SnapshotList.js +314 -0
  51. data/webpack/components/SnapshotManagement/components/SnapshotList/SnapshotListHelper.js +70 -0
  52. data/webpack/components/SnapshotManagement/components/SnapshotList/__tests__/SnapshotList.test.js +88 -0
  53. data/webpack/components/SnapshotManagement/components/SnapshotList/__tests__/__snapshots__/SnapshotList.test.js.snap +1081 -0
  54. data/webpack/components/SnapshotManagement/components/SnapshotList/snapshotList.scss +13 -0
  55. data/webpack/components/SnapshotManagement/index.js +33 -0
  56. data/webpack/components/SnapshotManagement/snapshotManagement.scss +5 -0
  57. data/webpack/global_index.js +7 -0
  58. data/webpack/global_test_setup.js +11 -0
  59. data/webpack/index.js +8 -0
  60. data/webpack/reducers.js +7 -0
  61. data/webpack/test_setup.js +17 -0
  62. metadata +50 -37
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { useDispatch } from 'react-redux';
3
+
4
+ import { submitForm } from 'foremanReact/redux/actions/common/forms';
5
+
6
+ import SnapshotForm from './SnapshotForm';
7
+
8
+ const WrappedSnapshotForm = props => {
9
+ const dispatch = useDispatch();
10
+
11
+ return (
12
+ <SnapshotForm
13
+ submitForm={(...args) => dispatch(submitForm(...args))}
14
+ {...props}
15
+ />
16
+ );
17
+ };
18
+
19
+ export default WrappedSnapshotForm;
@@ -0,0 +1,3 @@
1
+ textarea.form-control {
2
+ resize: vertical;
3
+ }
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ import ForemanModal from 'foremanReact/components/ForemanModal';
5
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
6
+
7
+ import SnapshotForm from '../SnapshotForm';
8
+
9
+ import { SNAPSHOT_FORM_MODAL } from './SnapshotFormModalConstants';
10
+
11
+ const SnapshotFormModal = ({ host, setModalClosed, ...props }) => (
12
+ <ForemanModal
13
+ id={SNAPSHOT_FORM_MODAL}
14
+ title={sprintf(__('Create Snapshot for %s'), host.name)}
15
+ enforceFocus
16
+ >
17
+ <ForemanModal.Header closeButton={false} />
18
+
19
+ <div>
20
+ <SnapshotForm
21
+ setModalClosed={setModalClosed}
22
+ hostId={host.id}
23
+ {...props}
24
+ />
25
+ </div>
26
+ </ForemanModal>
27
+ );
28
+
29
+ SnapshotFormModal.propTypes = {
30
+ host: PropTypes.shape({
31
+ id: PropTypes.number.isRequired,
32
+ name: PropTypes.string.isRequired,
33
+ }).isRequired,
34
+ setModalClosed: PropTypes.func.isRequired,
35
+ };
36
+
37
+ export default SnapshotFormModal;
@@ -0,0 +1 @@
1
+ export const SNAPSHOT_FORM_MODAL = 'foremanSnapshotMgmtSnapshotFormModal';
@@ -0,0 +1,19 @@
1
+ import {
2
+ testComponentSnapshotsWithFixtures,
3
+ // shallowRenderComponentWithFixtures,
4
+ } from 'react-redux-test-utils';
5
+ // import configureStore from 'redux-mock-store';
6
+
7
+ import SnapshotFormModal from '../SnapshotFormModal';
8
+
9
+ // const mockStore = configureStore([]);
10
+
11
+ const setModalClosed = () => null;
12
+ const fixtures = {
13
+ normal: { host: { id: 42, name: 'deep.thought' }, setModalClosed },
14
+ };
15
+
16
+ describe('SnapshotFormModal', () => {
17
+ describe('renders', () =>
18
+ testComponentSnapshotsWithFixtures(SnapshotFormModal, fixtures));
19
+ });
@@ -0,0 +1,19 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`SnapshotFormModal renders normal 1`] = `
4
+ <ConnectedForemanModal
5
+ enforceFocus={true}
6
+ id="foremanSnapshotMgmtSnapshotFormModal"
7
+ title="Create Snapshot for deep.thought"
8
+ >
9
+ <ForemanModalHeader
10
+ closeButton={false}
11
+ />
12
+ <div>
13
+ <WrappedSnapshotForm
14
+ hostId={42}
15
+ setModalClosed={[Function]}
16
+ />
17
+ </div>
18
+ </ConnectedForemanModal>
19
+ `;
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+
3
+ import useSnapshotFormModal from './useSnapshotFormModal';
4
+ import SnapshotFormModal from './SnapshotFormModal';
5
+
6
+ const WrappedSnapshotFormModal = props => {
7
+ const { setModalClosed } = useSnapshotFormModal();
8
+
9
+ return <SnapshotFormModal setModalClosed={setModalClosed} {...props} />;
10
+ };
11
+
12
+ export default WrappedSnapshotFormModal;
@@ -0,0 +1,7 @@
1
+ import { useForemanModal } from 'foremanReact/components/ForemanModal/ForemanModalHooks';
2
+
3
+ import { SNAPSHOT_FORM_MODAL } from './SnapshotFormModalConstants';
4
+
5
+ const useSnapshotFormModal = () => useForemanModal({ id: SNAPSHOT_FORM_MODAL });
6
+
7
+ export default useSnapshotFormModal;
@@ -0,0 +1,314 @@
1
+ import React, { Component } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ Alert,
5
+ // Button,
6
+ Table as PfTable,
7
+ FormControl,
8
+ inlineEditFormatterFactory,
9
+ } from 'patternfly-react';
10
+ import { cloneDeep, findIndex } from 'lodash';
11
+
12
+ import { translate as __ } from 'foremanReact/common/I18n';
13
+ import {
14
+ Table,
15
+ TableBody,
16
+ column,
17
+ headerFormatterWithProps,
18
+ cellFormatterWithProps,
19
+ } from 'foremanReact/components/common/table';
20
+ import ShortDateTime from 'foremanReact/components/common/dates/ShortDateTime';
21
+ import AlertBody from 'foremanReact/components/common/Alert/AlertBody';
22
+ import Loading from 'foremanReact/components/Loading/Loading';
23
+ import 'foremanReact/redux/API';
24
+
25
+ import { renderListEntryButtons } from './SnapshotListHelper';
26
+ import './snapshotList.scss';
27
+
28
+ class SnapshotList extends Component {
29
+ constructor(props) {
30
+ super(props);
31
+ this.state = {
32
+ columns: this.defineColumns(),
33
+ editMode: false,
34
+ rows: cloneDeep(props.snapshots),
35
+ };
36
+ }
37
+
38
+ defineColumns() {
39
+ const {
40
+ canDelete,
41
+ canRevert,
42
+ canUpdate,
43
+ host,
44
+ deleteAction,
45
+ updateAction,
46
+ rollbackAction,
47
+ } = this.props;
48
+
49
+ const inlineEditController = {
50
+ isEditing: ({ rowData }) => rowData.backup !== undefined,
51
+ onActivate: ({ rowData }) => {
52
+ const rows = cloneDeep(this.state.rows);
53
+ const index = findIndex(rows, { id: rowData.id });
54
+
55
+ rows[index].backup = cloneDeep(rows[index]);
56
+
57
+ this.setState({ rows, editMode: true });
58
+ },
59
+ onConfirm: ({ rowData }) => {
60
+ const rows = cloneDeep(this.state.rows);
61
+ const index = findIndex(rows, { id: rowData.id });
62
+
63
+ delete rows[index].backup;
64
+
65
+ this.setState({ rows, editMode: false });
66
+ updateAction(host, rowData);
67
+ },
68
+ onCancel: ({ rowData }) => {
69
+ const rows = cloneDeep(this.state.rows);
70
+ const index = findIndex(rows, { id: rowData.id });
71
+
72
+ rows[index] = cloneDeep(rows[index].backup);
73
+ delete rows[index].backup;
74
+
75
+ this.setState({ rows, editMode: false });
76
+ },
77
+ onChange: (value, { rowData, property }) => {
78
+ const rows = cloneDeep(this.state.rows);
79
+ const index = findIndex(rows, { id: rowData.id });
80
+
81
+ rows[index][property] = value;
82
+
83
+ this.setState({ rows });
84
+ },
85
+ };
86
+ this.inlineEditController = inlineEditController;
87
+
88
+ const renderButtons = renderListEntryButtons(
89
+ canDelete,
90
+ canRevert,
91
+ canUpdate,
92
+ host,
93
+ rollbackAction,
94
+ deleteAction,
95
+ inlineEditController
96
+ );
97
+ const inlineEditButtonCellFormatter = inlineEditFormatterFactory({
98
+ isEditing: additionalData => this.state.editMode,
99
+ renderValue: renderButtons(false),
100
+ renderEdit: renderButtons(true),
101
+ });
102
+
103
+ const inlineEditFormatter = inlineEditFormatterFactory({
104
+ isEditing: additionalData => {
105
+ if (
106
+ additionalData.property === 'name' &&
107
+ !this.props.capabilities.editableSnapshotName
108
+ )
109
+ return false;
110
+ return inlineEditController.isEditing(additionalData);
111
+ },
112
+ renderValue: (value, additionalData) => {
113
+ let date = '';
114
+ if (
115
+ additionalData.property === 'name' &&
116
+ additionalData.rowData.formatted_created_at
117
+ )
118
+ date = (
119
+ <span className="snapshot-date">
120
+ <ShortDateTime
121
+ date={new Date(additionalData.rowData.formatted_created_at)}
122
+ defaultValue={__('N/A')}
123
+ showRelativeTimeTooltip
124
+ />
125
+ </span>
126
+ );
127
+ return (
128
+ <span className="static description">
129
+ {value}
130
+ <br />
131
+ {date}
132
+ </span>
133
+ );
134
+ },
135
+ renderEdit: (value, additionalData) => {
136
+ let type = 'input';
137
+ if (additionalData.property === 'description') type = 'textarea';
138
+ return (
139
+ <FormControl
140
+ type="text"
141
+ defaultValue={value}
142
+ onBlur={e =>
143
+ inlineEditController.onChange(e.target.value, additionalData)
144
+ }
145
+ componentClass={type}
146
+ />
147
+ );
148
+ },
149
+ });
150
+ const editCellFormatters = [cellFormatterWithProps];
151
+ if (canUpdate) editCellFormatters.unshift(inlineEditFormatter);
152
+
153
+ const columns = [
154
+ column(
155
+ 'name',
156
+ __('Snapshot'),
157
+ [headerFormatterWithProps],
158
+ editCellFormatters
159
+ ),
160
+ column(
161
+ 'description',
162
+ __('Description'),
163
+ [headerFormatterWithProps],
164
+ editCellFormatters
165
+ ),
166
+ ];
167
+ if (canDelete || canUpdate || canRevert)
168
+ columns.push(
169
+ column(
170
+ '',
171
+ __('Action'),
172
+ [headerFormatterWithProps],
173
+ [inlineEditButtonCellFormatter],
174
+ { className: 'action-buttons' }
175
+ )
176
+ );
177
+ return columns;
178
+ }
179
+
180
+ componentDidMount() {
181
+ this.props.loadSnapshots(this.props.host.id);
182
+ }
183
+
184
+ // FIXME: Remove soon, as this is deprecated!
185
+ componentWillReceiveProps(newProps) {
186
+ if (newProps.snapshots !== this.props.snapshots) {
187
+ this.setState({ rows: newProps.snapshots });
188
+ }
189
+ if (newProps.needsReload) {
190
+ newProps.loadSnapshots(newProps.host.id);
191
+ }
192
+ }
193
+
194
+ getBodyMessage() {
195
+ if (this.props.isLoading || this.props.isWorking)
196
+ return <Loading textSize="sm" />;
197
+
198
+ if (this.props.hasError)
199
+ return (
200
+ <Alert
201
+ variant="danger"
202
+ isInline
203
+ title={__('Failed to load snapshot list')}
204
+ >
205
+ <AlertBody title={__('Failed to load snapshot list')}>
206
+ {/* IMHO the line-break should be done by AlertBody :-( */}
207
+ <br />
208
+ {this.props.error.message}
209
+ </AlertBody>
210
+ </Alert>
211
+ );
212
+
213
+ return undefined;
214
+ }
215
+
216
+ render() {
217
+ const { columns } = this.state;
218
+ const bodyMessage = this.getBodyMessage();
219
+
220
+ return (
221
+ <div>
222
+ {/*
223
+ <Button
224
+ disabled={this.props.isLoading}
225
+ onClick={() => this.props.loadSnapshots(this.props.host.id)}
226
+ >
227
+ ReloadData
228
+ </Button>
229
+ */}
230
+ <Table
231
+ caption="Snapshot List"
232
+ columns={columns}
233
+ rows={this.state.rows}
234
+ bodyMessage={bodyMessage}
235
+ inlineEdit
236
+ components={{
237
+ body: {
238
+ row: PfTable.InlineEditRow,
239
+ cell: cellProps => cellProps.children,
240
+ },
241
+ }}
242
+ >
243
+ <PfTable.Header key="header" />
244
+ <TableBody
245
+ key="body"
246
+ columns={columns}
247
+ rows={this.state.rows}
248
+ message={bodyMessage}
249
+ rowKey="id"
250
+ onRow={(rowData, { rowIndex }) => ({
251
+ role: 'row',
252
+ isEditing: () => this.inlineEditController.isEditing({ rowData }),
253
+ onCancel: () =>
254
+ this.inlineEditController.onCancel({ rowData, rowIndex }),
255
+ onConfirm: () =>
256
+ this.inlineEditController.onConfirm({ rowData, rowIndex }),
257
+ last: rowIndex === this.state.rows.length - 1,
258
+ })}
259
+ />
260
+ </Table>
261
+ </div>
262
+ );
263
+ }
264
+ }
265
+
266
+ SnapshotList.propTypes = {
267
+ /*
268
+ children: PropTypes.node,
269
+ className: PropTypes.string,
270
+ */
271
+ host: PropTypes.shape({
272
+ id: PropTypes.number.isRequired,
273
+ name: PropTypes.string.isRequired,
274
+ }).isRequired,
275
+ loadSnapshots: PropTypes.func.isRequired,
276
+ deleteAction: PropTypes.func.isRequired,
277
+ updateAction: PropTypes.func.isRequired,
278
+ rollbackAction: PropTypes.func.isRequired,
279
+ isLoading: PropTypes.bool,
280
+ isWorking: PropTypes.bool,
281
+ hasError: PropTypes.bool,
282
+ error: PropTypes.shape({ message: PropTypes.string }),
283
+ snapshots: PropTypes.array,
284
+
285
+ // permissions:
286
+ canDelete: PropTypes.bool,
287
+ canRevert: PropTypes.bool,
288
+ canUpdate: PropTypes.bool,
289
+
290
+ // capabilities
291
+ capabilities: PropTypes.shape({
292
+ editableSnapshotName: PropTypes.bool,
293
+ }),
294
+ };
295
+
296
+ SnapshotList.defaultProps = {
297
+ /*
298
+ className: '',
299
+ children: null,
300
+ */
301
+ isLoading: true,
302
+ isWorking: false,
303
+ hasError: false,
304
+ error: undefined,
305
+ snapshots: [],
306
+ canDelete: false,
307
+ canRevert: false,
308
+ canUpdate: false,
309
+ capabilities: {
310
+ editableSnapshotName: true,
311
+ },
312
+ };
313
+
314
+ export default SnapshotList;
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { Button, Icon } from 'patternfly-react';
3
+
4
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ import './snapshotList.scss';
7
+
8
+ export const renderListEntryButtons = (
9
+ canDelete,
10
+ canRevert,
11
+ canUpdate,
12
+ host,
13
+ snapshotRollbackAction,
14
+ snapshotDeleteAction,
15
+ inlineEditController
16
+ ) => disabled => (value, additionalData) => {
17
+ const buttons = [];
18
+
19
+ // Edit Button
20
+ if (canUpdate) {
21
+ buttons.push(
22
+ <Button
23
+ key="edit-button"
24
+ bsStyle="default"
25
+ disabled={disabled}
26
+ onClick={() => inlineEditController.onActivate(additionalData)}
27
+ >
28
+ <Icon type="pf" name="edit" title={__('edit entry')} />
29
+ </Button>
30
+ );
31
+ }
32
+
33
+ // Rollback button
34
+ if (canRevert) {
35
+ buttons.push(
36
+ <Button
37
+ key="rollback-button"
38
+ bsStyle="default"
39
+ disabled={disabled}
40
+ onClick={() =>
41
+ window.confirm(
42
+ sprintf(__('Rollback to "%s"?'), additionalData.rowData.name)
43
+ ) && snapshotRollbackAction(host, additionalData.rowData)
44
+ }
45
+ >
46
+ <Icon type="pf" name="history" title={__('Rollback')} />
47
+ </Button>
48
+ );
49
+ }
50
+
51
+ // Delete Button
52
+ if (canDelete) {
53
+ buttons.push(
54
+ <Button
55
+ key="delete-button"
56
+ bsStyle="default"
57
+ disabled={disabled}
58
+ onClick={() =>
59
+ window.confirm(
60
+ sprintf(__('Delete Snapshot "%s"?'), additionalData.rowData.name)
61
+ ) && snapshotDeleteAction(host, additionalData.rowData)
62
+ }
63
+ >
64
+ <Icon type="pf" name="delete" title={__('Delete')} />
65
+ </Button>
66
+ );
67
+ }
68
+
69
+ return <td className="action-buttons">{buttons}</td>;
70
+ };