foreman_snapshot_management 1.7.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/Rakefile +7 -2
  4. data/app/controllers/api/v2/snapshots_controller.rb +22 -6
  5. data/app/models/foreman_snapshot_management/proxmox_extensions.rb +1 -1
  6. data/app/models/foreman_snapshot_management/snapshot.rb +4 -0
  7. data/app/views/api/v2/snapshots/base.json.rabl +2 -0
  8. data/app/views/foreman_snapshot_management/snapshots/_index.html.erb +12 -80
  9. data/app/views/hosts/_snapshots_tab.html.erb +8 -0
  10. data/lib/foreman_snapshot_management/engine.rb +22 -21
  11. data/lib/foreman_snapshot_management/version.rb +1 -1
  12. data/lib/tasks/foreman_snapshot_management_tasks.rake +2 -2
  13. data/locale/de/LC_MESSAGES/foreman_snapshot_management.mo +0 -0
  14. data/locale/de/foreman_snapshot_management.po +52 -19
  15. data/locale/en/LC_MESSAGES/foreman_snapshot_management.mo +0 -0
  16. data/locale/en/foreman_snapshot_management.po +44 -11
  17. data/locale/foreman_snapshot_management.pot +87 -25
  18. data/package.json +46 -0
  19. data/test/controllers/api/v2/snapshots_test.rb +232 -109
  20. data/test/controllers/foreman_snapshot_management/snapshots_controller_test.rb +1 -1
  21. data/test/factories/proxmox_factory.rb +1 -1
  22. data/webpack/components/SnapshotManagement/SnapshotManagement.js +84 -0
  23. data/webpack/components/SnapshotManagement/SnapshotManagementActions.js +212 -0
  24. data/webpack/components/SnapshotManagement/SnapshotManagementConstants.js +9 -0
  25. data/webpack/components/SnapshotManagement/SnapshotManagementReducer.js +100 -0
  26. data/webpack/components/SnapshotManagement/SnapshotManagementSelectors.js +8 -0
  27. data/webpack/components/SnapshotManagement/__tests__/SnapshotManagementActions.test.js +123 -0
  28. data/webpack/components/SnapshotManagement/__tests__/SnapshotManagementReducer.test.js +157 -0
  29. data/webpack/components/SnapshotManagement/__tests__/__snapshots__/SnapshotManagementActions.test.js.snap +314 -0
  30. data/webpack/components/SnapshotManagement/__tests__/__snapshots__/SnapshotManagementReducer.test.js.snap +214 -0
  31. data/webpack/components/SnapshotManagement/components/SnapshotForm/SnapshotForm.js +118 -0
  32. data/webpack/components/SnapshotManagement/components/SnapshotForm/SnapshotFormConstants.js +5 -0
  33. data/webpack/components/SnapshotManagement/components/SnapshotForm/__tests__/SnapshotForm.test.js +26 -0
  34. data/webpack/components/SnapshotManagement/components/SnapshotForm/__tests__/__snapshots__/SnapshotForm.test.js.snap +476 -0
  35. data/webpack/components/SnapshotManagement/components/SnapshotForm/index.js +19 -0
  36. data/webpack/components/SnapshotManagement/components/SnapshotForm/snapshotForm.scss +3 -0
  37. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/SnapshotFormModal.js +37 -0
  38. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/SnapshotFormModalConstants.js +1 -0
  39. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/SnapshotFormModal.test.js +19 -0
  40. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/__snapshots__/SnapshotFormModal.test.js.snap +19 -0
  41. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/index.js +12 -0
  42. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/useSnapshotFormModal.js +7 -0
  43. data/webpack/components/SnapshotManagement/components/SnapshotList/SnapshotList.js +314 -0
  44. data/webpack/components/SnapshotManagement/components/SnapshotList/SnapshotListHelper.js +70 -0
  45. data/webpack/components/SnapshotManagement/components/SnapshotList/__tests__/SnapshotList.test.js +88 -0
  46. data/webpack/components/SnapshotManagement/components/SnapshotList/__tests__/__snapshots__/SnapshotList.test.js.snap +1081 -0
  47. data/webpack/components/SnapshotManagement/components/SnapshotList/snapshotList.scss +13 -0
  48. data/webpack/components/SnapshotManagement/index.js +33 -0
  49. data/webpack/components/SnapshotManagement/snapshotManagement.scss +5 -0
  50. data/webpack/global_index.js +7 -0
  51. data/webpack/global_test_setup.js +11 -0
  52. data/webpack/index.js +8 -0
  53. data/webpack/reducers.js +7 -0
  54. data/webpack/test_setup.js +17 -0
  55. metadata +39 -33
@@ -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
+ };